mirror of
https://github.com/Heretek-AI/SwarmOS.git
synced 2026-11-07 10:06:01 -10:00
feat: release SwarmOS v1.0.0 Unified Heretek Platform
This commit is contained in:
1 parent
265361fc4c
commit
b2020bc206
15 files changed
+5669
-1979
No files matched your search
@@ -218,6 +218,27 @@ SwarmOS v0.6.0 makes it possible to check that SwarmOS is actually running and a
|
||||
- **Multi-language RepoMap**: the repo map now parses TypeScript/JavaScript, Python, Go (module path from `go.mod`, used `pkg.Symbol` selectors) and Rust (`mod`, `use crate::`/`super::`/`self::`) after stripping comments and strings, so commented-out imports no longer count. Parsing is lexer-based, not tree-sitter, so the drop-in bundle stays a single file with no WASM grammars. Edges record which symbols each importer takes, so `tools.repo.blast_radius({ path, symbol })` lists only the files that use that symbol, following re-exports and renames. PageRank now iterates to convergence, and parsed files are cached by mtime.
|
||||
- **Smaller always-on tool list**: only `pinnedNamespaces` (default `org`, `fs`, `repo`, `verify`, `artifacts`) stay pinned in Code Mode; other namespaces stay registered and discoverable via search, and each role gets a one-line hint naming the namespaces it is most likely to need.
|
||||
|
||||
### Unified Heretek Platform (v1.0.0): 7 Repositories Consolidated into 1 Omni-Harness
|
||||
|
||||
`SwarmOS v1.0.0` consolidates the entire Heretek-AI OpenCode ecosystem (`SwarmOS`, `The-Agency`, `claude-4-opencode`, `IUMBTEMS`, `opencode-rea`, `godot-opencode`, and `GDevelop-Unlocked`) into a single zero-fork architecture:
|
||||
|
||||
1. **Pillar 1 — IUMBTEMS Epistemic & Dialectic Engine (`tools.epistemic.*`, `/grill`)**:
|
||||
- SHA-256 content-addressed source cache (`.opencode/epistemic/sources/<shortHash>.md`) with SPDX license contamination classification (`permissive`, `weak-copyleft`, `copyleft-contaminating`, `unknown`).
|
||||
- Deterministic quote verifier (`tools.epistemic.verify_claims`) that parses `[VERIFIED: <shortHash> | "exact verbatim quote"]` tags and confirms every quote exists verbatim in the cached source.
|
||||
- Socratic stress-tester (`tools.epistemic.grill_spec` & `/grill`) that challenges architecture specs before locking contracts.
|
||||
2. **Pillar 2 — `claude-4-opencode` Zero-Config Compatibility Bridge (`tools.claude.*`)**:
|
||||
- Automatically discovers `.claude/commands/*.md`, `.claude/agents/*.md`, and `.claude/skills/*/SKILL.md` in the workspace or `~/.claude` and registers them as native OpenCode V2 slash commands, subagents, and skills.
|
||||
3. **Pillar 3 — Quantitative 100-Point Visual Quality & UI/UX Judge (`tools.ui.score_ux`, `tools.ui.get_ux_scorecard_script`)**:
|
||||
- Deterministic 6-axis UI/UX scorecard across **Spatial & Viewport Integrity (25 pts)**, **Alignment & Grid Harmony (20 pts)**, **Typography & WCAG Contrast (20 pts)**, **Interactive & Accessibility Completeness (15 pts)**, **Design Token Discipline (10 pts)**, and **Runtime Health (10 pts)**.
|
||||
- Produces a `0..100` score, letter grade (`S`/`A`/`B`/`C`/`F`), and exact remediation checklist saved to `.opencode/artifacts/ux-scorecard.json` (passing requires `score >= 85` and `0` horizontal overflow).
|
||||
4. **Pillar 4 — On-Demand Domain Packs (`tools.packs.*`, `/pack`)**:
|
||||
- Zero-overhead domain packs that stay dormant on standard software repos and auto-activate when domain markers are detected:
|
||||
- **Godot 4.x Pack (`godot-opencode`)**: `tools.packs.godot_inspect_scene` parses `.tscn`/`.tres` scene trees, node hierarchies, attached `.gd` scripts, and signal connections; registers `specialist-godot`.
|
||||
- **GDevelop 5 Pack (`GDevelop-Unlocked`)**: `tools.packs.gdevelop_inspect_game` and `tools.packs.gdevelop_unlock_ai_config` inspect `game.json` layouts/events and generate local custom AI endpoint configuration; registers `specialist-gdevelop`.
|
||||
- **Reverse Engineering Pack (`opencode-rea`)**: `tools.packs.re_inspect_binary` performs safe static triage on ELF/PE/Mach-O/WASM binaries (magic header, SHA-256, byte entropy, suspicious URLs/paths/crypto constants); registers `specialist-re`.
|
||||
5. **Pillar 5 — `The-Agency` Live Swarm Command Center WebUI (`tools.org.render_dashboard`, `/dashboard`)**:
|
||||
- Generates a self-contained High-Craft Technical Dark HTML5 dashboard (`.opencode/artifacts/swarm-dashboard.html`) displaying the 5-Gate Kanban Board, Locked Contracts, Worktree Tournaments, 100-Point UI/UX Scorecards, Epistemic Cache, and Active Domain Packs.
|
||||
|
||||
### Complete Roadmap Edition (v0.9.0+): Task Spend Budgets, Session Replay -> Benchmarks, Warm Parallel LSP & Pinned Host Fixture
|
||||
|
||||
- **Per-Task Spend & Step Budgets (`tools.org.claim_task`, `tools.org.set_task_budget`)**: Tasks on `.opencode/org/board.json` now accept `budget: { maxTokens?, maxTurns?, maxToolCalls? }`. When a subagent claims a task via `tools.org.claim_task`, SwarmOS accumulates token, turn, and tool-call usage on `task.usage`. If a subagent exceeds any budget ceiling, `createTaskBudgetEvaluator` locks further file mutations, shell commands, and subagent spawns, appends a `<task-budget-exceeded>` notice, and returns control to `manager` to either raise the budget (`tools.org.set_task_budget`) or re-scope the task.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@heretek-ai/swarmos",
|
||||
"version": "0.9.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Omni-Harness & Autonomous Multi-Agent Software Company, GUI Studio, Deep Research & Office Suite for OpenCode V2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { generateSwarmDashboardHtml } from "../webui/dashboard.js"
|
||||
import { generateGrillQuestions } from "../epistemic/dialectic.js"
|
||||
import { detectDomainPacks } from "../packs/index.js"
|
||||
import { SWARMOS_VERSION } from "../version.js"
|
||||
import { listArtifacts, readArtifact } from "../omni/artifacts.js"
|
||||
import { listCheckpoints } from "../omni/checkpoints.js"
|
||||
@@ -126,5 +129,51 @@ export async function registerOmniCommands(ctx: any, rootDir: string): Promise<v
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
editor.add({
|
||||
name: "dashboard",
|
||||
description: "Render The Agency Live Swarm Command Center HTML5 WebUI (.opencode/artifacts/swarm-dashboard.html) and display status",
|
||||
execute: async (input: any) => {
|
||||
const res = await generateSwarmDashboardHtml(rootDir)
|
||||
if (ctx.session?.prompt) {
|
||||
await ctx.session.prompt({
|
||||
sessionID: input.sessionID,
|
||||
text: `# SwarmOS Live Command Center WebUI Generated\n\n- **File**: \`${res.dashboardPath}\`\n- **Release Ready**: \`${res.readyToShip}\`\n- **Tasks**: ${res.taskCount} | **Contracts**: ${res.contractCount} | **UI/UX Score**: ${res.uxScore ?? "N/A"}\n\nOpen \`${res.dashboardPath}\` in your browser or OpenCode preview pane to inspect the live Kanban board.`,
|
||||
delivery: "steer",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
editor.add({
|
||||
name: "grill",
|
||||
description: "Run the IUMBTEMS Socratic Epistemic & Dialectic Stress-Test against a design or specification",
|
||||
execute: async (input: any) => {
|
||||
const specText = String(input.prompt?.text ?? input.prompt ?? "Current workspace architecture")
|
||||
const res = generateGrillQuestions(specText)
|
||||
if (ctx.session?.prompt) {
|
||||
await ctx.session.prompt({
|
||||
sessionID: input.sessionID,
|
||||
text: `# Epistemic Socratic Grill (${res.topic})\n\nBefore locking contracts or writing implementation code, answer or verify these ${res.questions.length} stress-test questions:\n\n${res.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}`,
|
||||
delivery: "steer",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
editor.add({
|
||||
name: "pack",
|
||||
description: "Inspect or activate SwarmOS Domain Packs (Godot 4, GDevelop 5, Reverse Engineering)",
|
||||
execute: async (input: any) => {
|
||||
const detection = await detectDomainPacks(rootDir)
|
||||
if (ctx.session?.prompt) {
|
||||
await ctx.session.prompt({
|
||||
sessionID: input.sessionID,
|
||||
text: `# SwarmOS Domain Packs\n\n- **Auto-Detected Active Packs**: ${detection.recommendedPacks.join(", ") || "none (core only)"}\n- **Godot 4.x**: \`${detection.godot}\` (\`tools.packs.godot_inspect_scene\`)\n- **GDevelop 5**: \`${detection.gdevelop}\` (\`tools.packs.gdevelop_inspect_game\`)\n- **Reverse Engineering (REA)**: \`${detection.reverseEngineering}\` (\`tools.packs.re_inspect_binary\`)`,
|
||||
delivery: "steer",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
// Pillar 2: Claude Code Compatibility Bridge (src/compat/claude-code.ts)
|
||||
// Absorbs Heretek-AI/claude-4-opencode directly into SwarmOS:
|
||||
// Discovers .claude/commands star-md, .claude/agents star-md, and
|
||||
// .claude/skills star/SKILL.md from the workspace root (and optionally
|
||||
// ~/.claude/) and registers them into OpenCode V2 command, agent, and
|
||||
// skill catalogs automatically.
|
||||
|
||||
export interface ClaudeCommandDef {
|
||||
name: string;
|
||||
description: string;
|
||||
template: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ClaudeAgentDef {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
system: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ClaudeSkillDef {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ClaudeBridgeInventory {
|
||||
commands: ClaudeCommandDef[];
|
||||
agents: ClaudeAgentDef[];
|
||||
skills: ClaudeSkillDef[];
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw: string): {
|
||||
meta: Record<string, string>;
|
||||
body: string;
|
||||
} {
|
||||
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
||||
if (!m) return { meta: {}, body: raw.trim() };
|
||||
const meta: Record<string, string> = {};
|
||||
for (const line of m[1].split(/\r?\n/)) {
|
||||
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.+)$/.exec(line.trim());
|
||||
if (kv)
|
||||
meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
|
||||
}
|
||||
return { meta, body: m[2].trim() };
|
||||
}
|
||||
|
||||
async function listMdFiles(dir: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
||||
.map((e) => path.join(dir, e.name));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function discoverClaudeCodeAssets(
|
||||
rootDir: string,
|
||||
includeUserHome: boolean = false,
|
||||
): Promise<ClaudeBridgeInventory> {
|
||||
const roots = [
|
||||
path.join(rootDir, ".claude"),
|
||||
...(includeUserHome ? [path.join(os.homedir(), ".claude")] : []),
|
||||
];
|
||||
|
||||
const commands: ClaudeCommandDef[] = [];
|
||||
const agents: ClaudeAgentDef[] = [];
|
||||
const skills: ClaudeSkillDef[] = [];
|
||||
const seenCmds = new Set<string>();
|
||||
const seenAgents = new Set<string>();
|
||||
const seenSkills = new Set<string>();
|
||||
|
||||
for (const base of roots) {
|
||||
// 1. .claude/commands/*.md
|
||||
for (const file of await listMdFiles(path.join(base, "commands"))) {
|
||||
const name = path
|
||||
.basename(file, ".md")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, "-");
|
||||
if (seenCmds.has(name)) continue;
|
||||
seenCmds.add(name);
|
||||
const raw = await fs.readFile(file, "utf8");
|
||||
const { meta, body } = parseFrontmatter(raw);
|
||||
commands.push({
|
||||
name,
|
||||
description: meta.description ?? `Claude Code command /${name}`,
|
||||
template: body,
|
||||
path: file,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. .claude/agents/*.md
|
||||
for (const file of await listMdFiles(path.join(base, "agents"))) {
|
||||
const raw = await fs.readFile(file, "utf8");
|
||||
const { meta, body } = parseFrontmatter(raw);
|
||||
const id = (meta.name ?? path.basename(file, ".md"))
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, "-");
|
||||
if (seenAgents.has(id)) continue;
|
||||
seenAgents.add(id);
|
||||
agents.push({
|
||||
id,
|
||||
name: meta.name ?? id,
|
||||
description: meta.description ?? `Claude Code subagent ${id}`,
|
||||
system: body,
|
||||
path: file,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. .claude/skills/*/SKILL.md
|
||||
try {
|
||||
const skillDirs = await fs.readdir(path.join(base, "skills"), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
for (const d of skillDirs) {
|
||||
if (!d.isDirectory()) continue;
|
||||
const skillFile = path.join(base, "skills", d.name, "SKILL.md");
|
||||
try {
|
||||
const raw = await fs.readFile(skillFile, "utf8");
|
||||
const { meta } = parseFrontmatter(raw);
|
||||
const id = (meta.name ?? d.name)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, "-");
|
||||
if (seenSkills.has(id)) continue;
|
||||
seenSkills.add(id);
|
||||
skills.push({
|
||||
id,
|
||||
name: meta.name ?? d.name,
|
||||
description: meta.description ?? `Claude Code skill ${id}`,
|
||||
content: raw,
|
||||
path: skillFile,
|
||||
});
|
||||
} catch {
|
||||
// ignore missing SKILL.md
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no skills dir
|
||||
}
|
||||
}
|
||||
|
||||
return { commands, agents, skills };
|
||||
}
|
||||
|
||||
export async function registerClaudeCodeBridge(
|
||||
ctx: any,
|
||||
rootDir: string,
|
||||
): Promise<ClaudeBridgeInventory> {
|
||||
const inv = await discoverClaudeCodeAssets(rootDir, false);
|
||||
|
||||
if (inv.agents.length > 0 && ctx.agent?.transform) {
|
||||
await ctx.agent.transform((editor: any) => {
|
||||
for (const a of inv.agents) {
|
||||
editor.update(a.id, (item: any) => {
|
||||
item.name = a.name;
|
||||
item.description = a.description;
|
||||
item.mode = "subagent";
|
||||
item.system = a.system;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (inv.skills.length > 0 && ctx.skill?.transform) {
|
||||
await ctx.skill.transform((editor: any) => {
|
||||
for (const s of inv.skills) {
|
||||
editor.add({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
autoinvoke: false,
|
||||
path: s.path,
|
||||
content: s.content,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (inv.commands.length > 0 && ctx.command?.transform) {
|
||||
await ctx.command.transform((editor: any) => {
|
||||
for (const cmd of inv.commands) {
|
||||
editor.add({
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
execute: async (input: any) => {
|
||||
const args = String(
|
||||
input?.prompt?.text ?? input?.prompt ?? "",
|
||||
).trim();
|
||||
const rendered = cmd.template.includes("$ARGUMENTS")
|
||||
? cmd.template.replaceAll("$ARGUMENTS", args)
|
||||
: `${cmd.template}${args ? `\n\n${args}` : ""}`;
|
||||
if (ctx.session?.prompt) {
|
||||
await ctx.session.prompt({
|
||||
...input.prompt,
|
||||
sessionID: input.sessionID,
|
||||
text: rendered,
|
||||
delivery: input.delivery ?? "steer",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return inv;
|
||||
}
|
||||
|
||||
export function registerClaudeBridgeTools(editor: any, rootDir: string): void {
|
||||
editor.namespace({
|
||||
name: "claude",
|
||||
description:
|
||||
"Claude Code Compatibility Bridge (`claude-4-opencode`): inspects and loads `.claude/commands/*.md`, `.claude/agents/*.md`, and `.claude/skills/*/SKILL.md`.",
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "inventory",
|
||||
description:
|
||||
"List all Claude Code commands, subagents, and skills discovered in `.claude/`.",
|
||||
options: { namespace: "claude", codemode: true, pinned: false },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
includeUserHome: {
|
||||
type: "boolean",
|
||||
description: "Also scan ~/.claude/ (default false)",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { includeUserHome?: boolean }) => {
|
||||
const inv = await discoverClaudeCodeAssets(
|
||||
rootDir,
|
||||
Boolean(input?.includeUserHome),
|
||||
);
|
||||
return { output: inv, content: JSON.stringify(inv, null, 2) };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const registerClaudeCompatTools = registerClaudeBridgeTools
|
||||
@@ -144,7 +144,7 @@ async function walkWorkspaceFiles(dir: string, rootDir: string, acc: string[] =
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (acc.length >= max) break
|
||||
if (e.name.startsWith(".") || ["node_modules", "dist", "build", ".opencode", ".worktrees", "target"].includes(e.name)) continue
|
||||
if (e.name.startsWith(".") || ["node_modules", "dist", "build", ".opencode", ".worktrees", "target", "bench"].includes(e.name)) continue
|
||||
const full = path.join(dir, e.name)
|
||||
if (e.isDirectory()) await walkWorkspaceFiles(full, rootDir, acc, max)
|
||||
else if (e.isFile() && /\.(?:[cm]?[jt]sx?|py|go|rs)$/.test(e.name)) {
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
// Pillar 1: IUMBTEMS Epistemic Sovereignty & Dialectic Engine (src/epistemic/dialectic.ts)
|
||||
// Absorbs Heretek-AI epistemic-swarm (IUMBTEMS) directly into SwarmOS:
|
||||
// 1. Content-addressed SHA-256 raw source cache (.opencode/epistemic/sources).
|
||||
// 2. Epistemic Tagging Taxonomy ([VERIFIED: <hash>], [INFERRED:], [HYPOTHESIS:],
|
||||
// [NEGATIVE_KNOWLEDGE:]) with verbatim quote verification.
|
||||
// 3. Socratic Assumption-Inversion Grill (/grill & tools.epistemic.grill).
|
||||
// 4. Competitor Teardown & SPDX License Contamination Scout (/darkharvest).
|
||||
|
||||
export type EpistemicLicenseCategory =
|
||||
"permissive" | "weak-copyleft" | "copyleft-contaminating" | "unknown";
|
||||
|
||||
export interface CachedEpistemicSource {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
url?: string;
|
||||
title: string;
|
||||
spdxLicense?: string;
|
||||
licenseCategory: EpistemicLicenseCategory;
|
||||
cachedAt: string;
|
||||
path: string;
|
||||
filePath: string;
|
||||
byteLength: number;
|
||||
}
|
||||
|
||||
export interface EpistemicVerificationReport {
|
||||
totalClaimsChecked: number;
|
||||
verifiedCount: number;
|
||||
failedClaims: Array<{ tag: string; reason: string }>;
|
||||
passed: boolean;
|
||||
verifiedClaims: Array<{
|
||||
hash: string;
|
||||
quote?: string;
|
||||
valid: boolean;
|
||||
reason?: string;
|
||||
}>;
|
||||
inferredClaims: string[];
|
||||
hypothesisClaims: string[];
|
||||
negativeKnowledgeClaims: string[];
|
||||
untaggedParagraphs: number;
|
||||
integrityScore: number;
|
||||
}
|
||||
|
||||
export interface LicenseAuditItem {
|
||||
name: string;
|
||||
license: string;
|
||||
classification:
|
||||
| "permissive"
|
||||
| "weak-copyleft"
|
||||
| "strong-copyleft"
|
||||
| "copyleft-contaminating"
|
||||
| "unknown";
|
||||
verdict:
|
||||
| "depend-or-vendor"
|
||||
| "dynamic-link-only"
|
||||
| "clean-room-rebuild-only"
|
||||
| "manual-review";
|
||||
}
|
||||
|
||||
const PERMISSIVE_LICENSES = new Set([
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"0BSD",
|
||||
"Unlicense",
|
||||
"CC0-1.0",
|
||||
]);
|
||||
const WEAK_COPYLEFT_LICENSES = new Set([
|
||||
"MPL-2.0",
|
||||
"LGPL-2.1",
|
||||
"LGPL-2.1-only",
|
||||
"LGPL-3.0",
|
||||
"LGPL-3.0-only",
|
||||
"EPL-2.0",
|
||||
]);
|
||||
const STRONG_COPYLEFT_LICENSES = new Set([
|
||||
"GPL-2.0",
|
||||
"GPL-2.0-only",
|
||||
"GPL-3.0",
|
||||
"GPL-3.0-only",
|
||||
"AGPL-3.0",
|
||||
"AGPL-3.0-only",
|
||||
"SSPL-1.0",
|
||||
]);
|
||||
|
||||
export function classifySpdxLicense(
|
||||
spdx: string | undefined,
|
||||
): EpistemicLicenseCategory;
|
||||
export function classifySpdxLicense(
|
||||
pkgName: string,
|
||||
spdx: string,
|
||||
): LicenseAuditItem;
|
||||
export function classifySpdxLicense(
|
||||
a?: string,
|
||||
b?: string,
|
||||
): EpistemicLicenseCategory | LicenseAuditItem {
|
||||
if (b === undefined) {
|
||||
const spdx = a ?? "";
|
||||
if (!spdx.trim()) return "unknown";
|
||||
const norm = spdx.trim();
|
||||
if (PERMISSIVE_LICENSES.has(norm)) return "permissive";
|
||||
if (WEAK_COPYLEFT_LICENSES.has(norm)) return "weak-copyleft";
|
||||
if (STRONG_COPYLEFT_LICENSES.has(norm) || /\b(?:AGPL|GPL|SSPL)/i.test(norm))
|
||||
return "copyleft-contaminating";
|
||||
return "unknown";
|
||||
}
|
||||
const pkgName = a ?? "";
|
||||
const spdx = b;
|
||||
const category = classifySpdxLicense(spdx);
|
||||
const classification =
|
||||
category === "copyleft-contaminating"
|
||||
? "copyleft-contaminating"
|
||||
: category === "permissive"
|
||||
? "permissive"
|
||||
: category === "weak-copyleft"
|
||||
? "weak-copyleft"
|
||||
: "unknown";
|
||||
const verdict =
|
||||
classification === "permissive"
|
||||
? "depend-or-vendor"
|
||||
: classification === "weak-copyleft"
|
||||
? "dynamic-link-only"
|
||||
: classification === "copyleft-contaminating"
|
||||
? "clean-room-rebuild-only"
|
||||
: "manual-review";
|
||||
return { name: pkgName, license: spdx, classification, verdict };
|
||||
}
|
||||
|
||||
export function getEpistemicDir(rootDir: string): string {
|
||||
return path.join(rootDir, ".opencode", "epistemic");
|
||||
}
|
||||
|
||||
export function getEpistemicSourcesDir(rootDir: string): string {
|
||||
return path.join(getEpistemicDir(rootDir), "sources");
|
||||
}
|
||||
|
||||
interface EpistemicIndexEntry extends CachedEpistemicSource {}
|
||||
|
||||
async function loadEpistemicIndex(
|
||||
rootDir: string,
|
||||
): Promise<EpistemicIndexEntry[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(
|
||||
path.join(getEpistemicDir(rootDir), "index.json"),
|
||||
"utf8",
|
||||
);
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEpistemicIndex(
|
||||
rootDir: string,
|
||||
entries: EpistemicIndexEntry[],
|
||||
): Promise<void> {
|
||||
const dir = getEpistemicDir(rootDir);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(dir, "index.json"),
|
||||
JSON.stringify(entries, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
export async function cacheEpistemicSource(
|
||||
rootDir: string,
|
||||
input: { url?: string; title: string; content: string; spdxLicense?: string },
|
||||
): Promise<CachedEpistemicSource> {
|
||||
const normalizedContent = input.content.replace(/\r\n/g, "\n");
|
||||
const sha256 = createHash("sha256")
|
||||
.update(normalizedContent, "utf8")
|
||||
.digest("hex");
|
||||
const shortHash = sha256.slice(0, 12);
|
||||
const sourcesDir = getEpistemicSourcesDir(rootDir);
|
||||
await fs.mkdir(sourcesDir, { recursive: true });
|
||||
const filePath = path.join(sourcesDir, `${shortHash}.md`);
|
||||
const header = [
|
||||
"---",
|
||||
`hash: ${sha256}`,
|
||||
`shortHash: ${shortHash}`,
|
||||
`title: ${input.title}`,
|
||||
"---",
|
||||
"",
|
||||
].join("\n");
|
||||
await fs.writeFile(filePath, `${header}${normalizedContent}`, "utf8");
|
||||
const entry: CachedEpistemicSource = {
|
||||
hash: sha256,
|
||||
shortHash,
|
||||
title: input.title,
|
||||
...(input.url ? { url: input.url } : {}),
|
||||
...(input.spdxLicense ? { spdxLicense: input.spdxLicense } : {}),
|
||||
licenseCategory: classifySpdxLicense(input.spdxLicense),
|
||||
cachedAt: new Date().toISOString(),
|
||||
path: filePath,
|
||||
filePath: path.relative(rootDir, filePath).split(path.sep).join("/"),
|
||||
byteLength: Buffer.byteLength(normalizedContent, "utf8"),
|
||||
};
|
||||
const index = await loadEpistemicIndex(rootDir);
|
||||
const existingIdx = index.findIndex((e) => e.shortHash === shortHash);
|
||||
if (existingIdx >= 0) index[existingIdx] = entry;
|
||||
else index.push(entry);
|
||||
await saveEpistemicIndex(rootDir, index);
|
||||
return entry;
|
||||
}
|
||||
|
||||
export async function verifyEpistemicClaims(
|
||||
rootDir: string,
|
||||
markdownText: string,
|
||||
): Promise<EpistemicVerificationReport> {
|
||||
const index = await loadEpistemicIndex(rootDir);
|
||||
const byHash = new Map<string, EpistemicIndexEntry>();
|
||||
for (const e of index) {
|
||||
byHash.set(e.shortHash.toLowerCase(), e);
|
||||
byHash.set(e.hash.toLowerCase(), e);
|
||||
}
|
||||
// Fall back to scanning sources/ dir for entries cached without index (legacy layout).
|
||||
if (byHash.size === 0) {
|
||||
try {
|
||||
const files = await fs.readdir(getEpistemicSourcesDir(rootDir));
|
||||
for (const f of files) {
|
||||
if (!f.endsWith(".md")) continue;
|
||||
const stem = path.basename(f, ".md").toLowerCase();
|
||||
if (!byHash.has(stem)) {
|
||||
byHash.set(stem, {
|
||||
hash: stem,
|
||||
shortHash: stem.slice(0, 12),
|
||||
title: stem,
|
||||
licenseCategory: "unknown",
|
||||
cachedAt: new Date().toISOString(),
|
||||
path: path.join(getEpistemicSourcesDir(rootDir), f),
|
||||
filePath: `.opencode/epistemic/sources/${f}`,
|
||||
byteLength: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no sources dir
|
||||
}
|
||||
}
|
||||
const tagRe = /\[VERIFIED:\s*([a-fA-F0-9]{6,64})(?:\s*\|\s*"([^"]+)")?\]/g;
|
||||
const failedClaims: Array<{ tag: string; reason: string }> = [];
|
||||
const verifiedClaims: EpistemicVerificationReport["verifiedClaims"] = [];
|
||||
let verifiedCount = 0;
|
||||
let totalClaimsChecked = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = tagRe.exec(markdownText)) !== null) {
|
||||
totalClaimsChecked++;
|
||||
const rawTag = match[0];
|
||||
const hashKey = match[1].toLowerCase();
|
||||
const quote = match[2];
|
||||
const entry =
|
||||
byHash.get(hashKey) ??
|
||||
[...byHash.values()].find(
|
||||
(e) =>
|
||||
e.shortHash.startsWith(hashKey) || hashKey.startsWith(e.shortHash),
|
||||
);
|
||||
if (!entry) {
|
||||
failedClaims.push({
|
||||
tag: rawTag,
|
||||
reason: `Unknown source hash "${hashKey}" not found in .opencode/epistemic/index.json`,
|
||||
});
|
||||
verifiedClaims.push({
|
||||
hash: hashKey,
|
||||
quote,
|
||||
valid: false,
|
||||
reason: "Source hash not found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const absFile = path.isAbsolute(entry.filePath)
|
||||
? entry.filePath
|
||||
: path.resolve(rootDir, entry.filePath);
|
||||
let sourceText = "";
|
||||
try {
|
||||
sourceText = await fs.readFile(entry.path ?? absFile, "utf8");
|
||||
if (!sourceText) sourceText = await fs.readFile(absFile, "utf8");
|
||||
} catch {
|
||||
failedClaims.push({
|
||||
tag: rawTag,
|
||||
reason: `Cached source file missing on disk: ${entry.filePath}`,
|
||||
});
|
||||
verifiedClaims.push({
|
||||
hash: hashKey,
|
||||
quote,
|
||||
valid: false,
|
||||
reason: "Cached source file missing",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
const normSource = sourceText.replace(/\s+/g, " ").trim();
|
||||
const normQuote = quote.replace(/\s+/g, " ").trim();
|
||||
if (!normSource.includes(normQuote)) {
|
||||
failedClaims.push({
|
||||
tag: rawTag,
|
||||
reason: `Verbatim quote "${quote.slice(0, 60)}" not found inside cached source [${entry.shortHash}] (${entry.title})`,
|
||||
});
|
||||
verifiedClaims.push({
|
||||
hash: hashKey,
|
||||
quote,
|
||||
valid: false,
|
||||
reason: "Verbatim quote not found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
verifiedCount++;
|
||||
verifiedClaims.push({ hash: hashKey, quote, valid: true });
|
||||
}
|
||||
|
||||
const inferredClaims = [
|
||||
...markdownText.matchAll(/\[INFERRED:\s*([^\]]+)\]/gi),
|
||||
].map((m) => m[1].trim());
|
||||
const hypothesisClaims = [
|
||||
...markdownText.matchAll(/\[HYPOTHESIS:\s*([^\]]+)\]/gi),
|
||||
].map((m) => m[1].trim());
|
||||
const negativeKnowledgeClaims = [
|
||||
...markdownText.matchAll(/\[NEGATIVE_KNOWLEDGE:\s*([^\]]+)\]/gi),
|
||||
].map((m) => m[1].trim());
|
||||
const paragraphs = markdownText
|
||||
.split(/\n\s*\n/)
|
||||
.map((p) => p.trim())
|
||||
.filter(
|
||||
(p) =>
|
||||
p.length > 60 &&
|
||||
!p.startsWith("#") &&
|
||||
!p.startsWith("|") &&
|
||||
!p.startsWith("```"),
|
||||
);
|
||||
const untaggedParagraphs = paragraphs.filter(
|
||||
(p) => !/\[(?:VERIFIED|INFERRED|HYPOTHESIS|NEGATIVE_KNOWLEDGE):/i.test(p),
|
||||
).length;
|
||||
const totalTagged =
|
||||
verifiedClaims.length +
|
||||
inferredClaims.length +
|
||||
hypothesisClaims.length +
|
||||
negativeKnowledgeClaims.length;
|
||||
const baseScore = totalTagged > 0 ? 100 : 60;
|
||||
const integrityScore = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
baseScore - failedClaims.length * 25 - untaggedParagraphs * 5,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
totalClaimsChecked,
|
||||
verifiedCount,
|
||||
failedClaims,
|
||||
passed: failedClaims.length === 0,
|
||||
verifiedClaims,
|
||||
inferredClaims,
|
||||
hypothesisClaims,
|
||||
negativeKnowledgeClaims,
|
||||
untaggedParagraphs,
|
||||
integrityScore,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listEpistemicSources(
|
||||
rootDir: string,
|
||||
): Promise<CachedEpistemicSource[]> {
|
||||
return loadEpistemicIndex(rootDir);
|
||||
}
|
||||
|
||||
export function generateSocraticGrillPrompt(objective: string): string {
|
||||
return [
|
||||
"# IUMBTEMS Socratic Grill & Assumption Inversion Protocol",
|
||||
"",
|
||||
`Target Objective: **${objective}**`,
|
||||
"",
|
||||
"Before writing any code or locking any contract, execute these 4 dialectic steps:",
|
||||
"1. **Premise Inversion**: List the top 3 unstated assumptions in the objective and invert each one (what if the opposite is true?).",
|
||||
"2. **Decision Tree Frontier**: Identify the 2–3 load-bearing architectural forks where choosing wrong forces a rewrite. Use `question` to ask the user with concrete tradeoffs.",
|
||||
"3. **Thesis vs. Antithesis (Red-Team)**: State the strongest argument FOR the proposed design (Agent Alpha) and the sharpest failure mode that will break it in production (Agent Beta).",
|
||||
"4. **Falsifiable Spike**: Define the smallest 1-command verification test that proves the core assumption works.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function generateGrillQuestions(
|
||||
specText: string,
|
||||
focusAreas: string[] = [],
|
||||
): { topic: string; questions: string[] } {
|
||||
const lines = specText
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const topic =
|
||||
lines[0]?.replace(/^#+\s*/, "") || "System Architecture & Specification";
|
||||
const questions = [
|
||||
`[State & Concurrency] What happens in "${topic}" if two subagents or requests mutate the same state concurrently or a process crashes mid-write?`,
|
||||
`[Failure Modes & Recovery] Which external dependencies or I/O boundaries in "${topic}" can fail or time out, and what is the deterministic fallback or rollback path?`,
|
||||
`[Boundary Invariants] What untrusted inputs enter "${topic}", and where is the schema validation boundary enforced before business logic executes?`,
|
||||
`[Verification & Observability] What single automated command or invariant assertion proves "${topic}" works without human guesswork?`,
|
||||
`[Scope & Blast Radius] Which existing modules or contracts could break if "${topic}" changes, and what is the minimal viable slice we should lock first?`,
|
||||
];
|
||||
for (const area of focusAreas) {
|
||||
questions.push(
|
||||
`[Focused Stress-Test: ${area}] What implicit assumption in "${area}" has not yet been verified against actual code or primary sources?`,
|
||||
);
|
||||
}
|
||||
return { topic, questions };
|
||||
}
|
||||
|
||||
export function registerEpistemicTools(editor: any, rootDir: string): void {
|
||||
editor.namespace({
|
||||
name: "epistemic",
|
||||
description:
|
||||
"IUMBTEMS Epistemic & Dialectic Engine: SHA-256 content-addressed source cache, verbatim quote verifier, SPDX license contamination guard, and Socratic grill stress-tester.",
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "cache_source",
|
||||
description:
|
||||
"Cache raw source text or documentation in `.opencode/epistemic/sources/<shortHash>.md`, compute its SHA-256 hash, and classify its SPDX license.",
|
||||
options: { namespace: "epistemic", codemode: true, pinned: true },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string" },
|
||||
content: { type: "string" },
|
||||
url: { type: "string" },
|
||||
spdxLicense: { type: "string" },
|
||||
},
|
||||
required: ["title", "content"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: {
|
||||
url?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
spdxLicense?: string;
|
||||
}) => {
|
||||
const entry = await cacheEpistemicSource(rootDir, input);
|
||||
return {
|
||||
output: entry,
|
||||
content: `Cached epistemic source [${entry.shortHash}] (${entry.licenseCategory}): ${entry.title}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "verify_claims",
|
||||
description:
|
||||
"Verify all `[VERIFIED: <shortHash> | 'exact quote']` tags in a Markdown text against `.opencode/epistemic/sources/`.",
|
||||
options: { namespace: "epistemic", codemode: true, pinned: true },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: { markdown: { type: "string" } },
|
||||
required: ["markdown"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { markdown: string }) => {
|
||||
const report = await verifyEpistemicClaims(rootDir, input.markdown);
|
||||
return { output: report, content: JSON.stringify(report, null, 2) };
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "grill_spec",
|
||||
description:
|
||||
"Generate Socratic stress-test questions to expose hidden assumptions, race conditions, and unverified boundaries in a specification.",
|
||||
options: { namespace: "epistemic", codemode: true, pinned: true },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
specText: { type: "string" },
|
||||
focusAreas: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
required: ["specText"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { specText: string; focusAreas?: string[] }) => {
|
||||
const res = generateGrillQuestions(
|
||||
input.specText,
|
||||
input.focusAreas ?? [],
|
||||
);
|
||||
return { output: res, content: JSON.stringify(res, null, 2) };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,35 @@
|
||||
import {
|
||||
cacheEpistemicSource,
|
||||
classifySpdxLicense,
|
||||
generateGrillQuestions,
|
||||
listEpistemicSources,
|
||||
registerEpistemicTools,
|
||||
verifyEpistemicClaims,
|
||||
} from "./epistemic/dialectic.js";
|
||||
import {
|
||||
discoverClaudeCodeAssets,
|
||||
registerClaudeCodeBridge,
|
||||
registerClaudeCompatTools,
|
||||
} from "./compat/claude-code.js";
|
||||
import {
|
||||
UX_SCORECARD_BROWSER_SCRIPT,
|
||||
registerUXJudgeTools,
|
||||
saveUXScorecard,
|
||||
scoreUXMetrics,
|
||||
} from "./omni/ux-judge.js";
|
||||
import {
|
||||
DOMAIN_PACK_ROLES,
|
||||
detectDomainPacks,
|
||||
inspectBinaryTarget,
|
||||
inspectGDevelopProject,
|
||||
inspectGodotScene,
|
||||
registerDomainPacks,
|
||||
} from "./packs/index.js";
|
||||
import {
|
||||
generateSwarmDashboardHtml,
|
||||
registerDashboardTools,
|
||||
} from "./webui/dashboard.js";
|
||||
|
||||
import { registerSwarmOSAgents, SWARMOS_ROLES } from "./agents/roles.js";
|
||||
import {
|
||||
evaluateBoardGuard,
|
||||
@@ -336,6 +368,27 @@ import {
|
||||
} from "./skills/ui-craft/skill.js";
|
||||
|
||||
export {
|
||||
cacheEpistemicSource,
|
||||
classifySpdxLicense,
|
||||
generateGrillQuestions,
|
||||
listEpistemicSources,
|
||||
registerEpistemicTools,
|
||||
verifyEpistemicClaims,
|
||||
discoverClaudeCodeAssets,
|
||||
registerClaudeCodeBridge,
|
||||
registerClaudeCompatTools,
|
||||
UX_SCORECARD_BROWSER_SCRIPT,
|
||||
registerUXJudgeTools,
|
||||
saveUXScorecard,
|
||||
scoreUXMetrics,
|
||||
DOMAIN_PACK_ROLES,
|
||||
detectDomainPacks,
|
||||
inspectBinaryTarget,
|
||||
inspectGDevelopProject,
|
||||
inspectGodotScene,
|
||||
registerDomainPacks,
|
||||
generateSwarmDashboardHtml,
|
||||
registerDashboardTools,
|
||||
// Agent roles, prompts & runtime guard
|
||||
SWARMOS_ROLES,
|
||||
PROMPT_DIRECTOR,
|
||||
@@ -685,6 +738,10 @@ export function buildSetupModules(
|
||||
const editor = withPinningProfile(rawEditor, config.pinnedNamespaces);
|
||||
registerOrgTools(editor, rootDir);
|
||||
registerUITools(editor);
|
||||
registerUXJudgeTools(editor, rootDir);
|
||||
registerEpistemicTools(editor, rootDir);
|
||||
registerClaudeCompatTools(editor, rootDir);
|
||||
registerDashboardTools(editor, rootDir);
|
||||
registerFSTools(editor, rootDir);
|
||||
registerHashlineTools(editor, rootDir);
|
||||
registerArtifactTools(editor, rootDir);
|
||||
@@ -779,6 +836,21 @@ export function buildSetupModules(
|
||||
await registerReplayCommand(c, rootDir);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "claude-compat-bridge",
|
||||
requires: ["command.transform", "tool.transform"],
|
||||
run: async (c) => {
|
||||
await c.tool.transform((editor: any) =>
|
||||
registerClaudeCompatTools(editor, rootDir),
|
||||
);
|
||||
await registerClaudeCodeBridge(c, rootDir);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "domain-packs",
|
||||
requires: ["tool.transform"],
|
||||
run: (c) => registerDomainPacks(c, rootDir),
|
||||
},
|
||||
// Telemetry runs last so its execute.after hook sees interventions appended by earlier hooks.
|
||||
...observe,
|
||||
];
|
||||
|
||||
@@ -27,15 +27,15 @@ export interface ContextBudgetReport {
|
||||
|
||||
/** Code Mode namespaces each role is most likely to need. */
|
||||
export const ROLE_NAMESPACES: Record<string, string[]> = {
|
||||
director: ["org", "repo", "memory", "artifacts", "swarmos"],
|
||||
architect: ["org", "repo", "fs", "checkpoints"],
|
||||
director: ["org", "repo", "memory", "artifacts", "epistemic", "claude", "packs", "swarmos"],
|
||||
architect: ["org", "repo", "fs", "epistemic", "checkpoints"],
|
||||
manager: ["org", "repo", "verify", "artifacts", "replay"],
|
||||
"staff-backend": ["fs", "repo", "lsp", "verify", "checkpoints", "memory", "artifacts"],
|
||||
"staff-ui": ["fs", "ui", "verify", "checkpoints", "artifacts", "gui"],
|
||||
"qa-adversary": ["verify", "org", "repo", "ui", "fs"],
|
||||
"release-ops": ["verify", "org", "repo"],
|
||||
"studio-gui": ["gui", "ui", "fs", "artifacts", "verify"],
|
||||
researcher: ["research", "artifacts", "office"],
|
||||
researcher: ["research", "epistemic", "artifacts", "office"],
|
||||
"office-analyst": ["office", "artifacts", "fs"],
|
||||
}
|
||||
const DEFAULT_ROLE_NAMESPACES = ["fs", "repo", "verify", "checkpoints", "artifacts", "memory"]
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
/**
|
||||
* Pillar 3: Quantitative 100-Point Visual Quality & UI/UX Judge (`src/omni/ux-judge.ts`)
|
||||
*
|
||||
* Evaluates live DOM geometry, typography, color contrast, spacing rhythm, alignment cohesion,
|
||||
* and interactive affordances on a deterministic 0–100 scale (`grade: "S" | "A" | "B" | "C" | "F"`).
|
||||
* UI tasks require `score >= 85` (`passed: true`) before sign-off.
|
||||
*/
|
||||
|
||||
export interface UXRawMetrics {
|
||||
viewport: { width: number; height: number }
|
||||
horizontalOverflowPx: number
|
||||
elementOverflowCount: number
|
||||
collisionCount: number
|
||||
baselineJitterCount: number
|
||||
lowContrastCount: number
|
||||
descenderClipCount: number
|
||||
tinyTargetCount: number
|
||||
missingLabelCount: number
|
||||
uniqueFontSizes: number
|
||||
arbitraryInlineHexCount: number
|
||||
consoleErrorCount: number
|
||||
failedNetworkCount: number
|
||||
}
|
||||
|
||||
export interface UXAxisScore {
|
||||
axis: string
|
||||
score: number
|
||||
maxScore: number
|
||||
notes: string[]
|
||||
}
|
||||
|
||||
export interface UXScorecard {
|
||||
score: number
|
||||
grade: "S" | "A" | "B" | "C" | "F"
|
||||
passed: boolean
|
||||
viewport: { width: number; height: number }
|
||||
breakdown: {
|
||||
spatialLayout: { score: number; max: number }
|
||||
alignmentHarmony: { score: number; max: number }
|
||||
typographyAndContrast: { score: number; max: number }
|
||||
interactiveCompleteness: { score: number; max: number }
|
||||
designTokenDiscipline: { score: number; max: number }
|
||||
runtimeHealth: { score: number; max: number }
|
||||
}
|
||||
remediation: string[]
|
||||
remediations: string[]
|
||||
evaluatedAt: string
|
||||
}
|
||||
|
||||
|
||||
export const UX_SCORECARD_BROWSER_SCRIPT = `(() => {
|
||||
const vw = window.innerWidth || 1440;
|
||||
const vh = window.innerHeight || 900;
|
||||
const horizontalOverflowPx = Math.max(0, document.documentElement.scrollWidth - vw - 2);
|
||||
|
||||
let elementOverflowCount = 0;
|
||||
let siblingCollisionCount = 0;
|
||||
let baselineJitterCount = 0;
|
||||
let lowContrastCount = 0;
|
||||
let descenderClipCount = 0;
|
||||
let tinyTargetCount = 0;
|
||||
let unlabeledControlCount = 0;
|
||||
let offGridSpacingCount = 0;
|
||||
let totalSpacingSamples = 0;
|
||||
let proseLineLengthViolations = 0;
|
||||
let neonSlopGradientCount = 0;
|
||||
let occupiedArea = 0;
|
||||
|
||||
const fontSizes = new Set();
|
||||
let maxHeadingSize = 16;
|
||||
let bodyFontSize = parseFloat(getComputedStyle(document.body).fontSize) || 16;
|
||||
|
||||
function parseRgb(str) {
|
||||
const m = str && str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
||||
if (!m) return null;
|
||||
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
|
||||
}
|
||||
function luminance({ r, g, b }) {
|
||||
const a = [r, g, b].map((v) => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
function contrastRatio(c1, c2) {
|
||||
const l1 = luminance(c1);
|
||||
const l2 = luminance(c2);
|
||||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
}
|
||||
function effectiveBg(el) {
|
||||
let cur = el;
|
||||
while (cur && cur.nodeType === 1) {
|
||||
const bg = parseRgb(getComputedStyle(cur).backgroundColor);
|
||||
if (bg && bg.a >= 0.85) return bg;
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return { r: 255, g: 255, b: 255, a: 1 };
|
||||
}
|
||||
|
||||
const all = Array.from(document.querySelectorAll('body *'));
|
||||
for (const el of all) {
|
||||
const style = getComputedStyle(el);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || +style.opacity === 0) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 && rect.height === 0) continue;
|
||||
|
||||
if (el.parentElement === document.body) {
|
||||
occupiedArea += Math.min(rect.width, vw) * Math.min(rect.height, vh);
|
||||
}
|
||||
|
||||
const fs = Math.round(parseFloat(style.fontSize) || 16);
|
||||
fontSizes.add(fs);
|
||||
if (/^H[1-3]$/.test(el.tagName)) maxHeadingSize = Math.max(maxHeadingSize, fs);
|
||||
|
||||
if (style.backgroundImage && /linear-gradient|radial-gradient/i.test(style.backgroundImage)) {
|
||||
if (/rgba?\(\s*(?:1[2-9]\d|2[0-5]\d)\s*,\s*[0-6]\d\s*,\s*2[0-5]\d/i.test(style.backgroundImage)) {
|
||||
neonSlopGradientCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop of ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'gap']) {
|
||||
const val = Math.round(parseFloat(style[prop]) || 0);
|
||||
if (val > 0) {
|
||||
totalSpacingSamples++;
|
||||
if (val % 4 !== 0 && val % 6 !== 0) offGridSpacingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
el.scrollWidth > el.clientWidth + 2 &&
|
||||
style.overflowX !== 'auto' &&
|
||||
style.overflowX !== 'scroll' &&
|
||||
style.overflowX !== 'hidden'
|
||||
) {
|
||||
elementOverflowCount++;
|
||||
}
|
||||
|
||||
const isInteractive = el.matches('button, a, input, select, textarea, [role="button"]');
|
||||
if (isInteractive) {
|
||||
if (rect.width < 24 || rect.height < 24) tinyTargetCount++;
|
||||
const label = (el.textContent || '') + (el.getAttribute('aria-label') || '') + (el.getAttribute('title') || '') + (el.getAttribute('placeholder') || '');
|
||||
if (!label.trim() && !el.querySelector('svg, img')) unlabeledControlCount++;
|
||||
}
|
||||
|
||||
const hasDirectText = Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (hasDirectText) {
|
||||
const fg = parseRgb(style.color);
|
||||
const bg = effectiveBg(el);
|
||||
if (fg && bg) {
|
||||
const ratio = contrastRatio(fg, bg);
|
||||
const minR = fs >= 19 ? 3.0 : 4.5;
|
||||
if (ratio < minR) lowContrastCount++;
|
||||
}
|
||||
const lh = parseFloat(style.lineHeight);
|
||||
if (lh && fs && lh / fs < 1.15 && style.overflow === 'hidden') descenderClipCount++;
|
||||
if (el.tagName === 'P' && (el.textContent || '').trim().length > 140 && rect.width > fs * 52) {
|
||||
proseLineLengthViolations++;
|
||||
}
|
||||
}
|
||||
|
||||
const children = Array.from(el.children).filter((c) => {
|
||||
const cs = getComputedStyle(c);
|
||||
return cs.display !== 'none' && cs.visibility !== 'hidden' && cs.position !== 'absolute' && cs.position !== 'fixed';
|
||||
});
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const r1 = children[i].getBoundingClientRect();
|
||||
for (let j = i + 1; j < children.length; j++) {
|
||||
const r2 = children[j].getBoundingClientRect();
|
||||
const overlapX = Math.max(0, Math.min(r1.right, r2.right) - Math.max(r1.left, r2.left));
|
||||
const overlapY = Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top));
|
||||
if (overlapX > 2 && overlapY > 2) siblingCollisionCount++;
|
||||
const topDiff = Math.abs(r1.top - r2.top);
|
||||
if (topDiff > 0.5 && topDiff <= 3.5 && overlapX === 0) baselineJitterCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
viewport: { width: vw, height: vh },
|
||||
horizontalOverflowPx,
|
||||
elementOverflowCount,
|
||||
siblingCollisionCount,
|
||||
baselineJitterCount,
|
||||
lowContrastCount,
|
||||
descenderClipCount,
|
||||
tinyTargetCount,
|
||||
unlabeledControlCount,
|
||||
offGridSpacingCount,
|
||||
totalSpacingSamples,
|
||||
distinctFontSizes: fontSizes.size,
|
||||
headingToBodyRatio: +(maxHeadingSize / Math.max(12, bodyFontSize)).toFixed(2),
|
||||
proseLineLengthViolations,
|
||||
neonSlopGradientCount,
|
||||
viewportCoverageRatio: +Math.min(1, occupiedArea / Math.max(1, vw * vh)).toFixed(2)
|
||||
};
|
||||
})()`;
|
||||
|
||||
export function scoreUXMetrics(metrics: UXRawMetrics): UXScorecard {
|
||||
const remediation: string[] = []
|
||||
let spatialScore = 25
|
||||
if (metrics.horizontalOverflowPx > 2) {
|
||||
spatialScore -= 15
|
||||
remediation.push(`Eliminate horizontal page overflow (${metrics.horizontalOverflowPx}px wider than ${metrics.viewport.width}px viewport).`)
|
||||
}
|
||||
if (metrics.elementOverflowCount > 0) {
|
||||
const deduct = Math.min(10, metrics.elementOverflowCount * 4)
|
||||
spatialScore -= deduct
|
||||
remediation.push(`Fix ${metrics.elementOverflowCount} overflowing container(s) where scrollWidth > clientWidth.`)
|
||||
}
|
||||
spatialScore = Math.max(0, spatialScore)
|
||||
|
||||
let alignmentScore = 20
|
||||
if (metrics.collisionCount > 0) {
|
||||
const deduct = Math.min(14, metrics.collisionCount * 7)
|
||||
alignmentScore -= deduct
|
||||
remediation.push(`Resolve ${metrics.collisionCount} sibling bounding-box overlap collision(s).`)
|
||||
}
|
||||
if (metrics.baselineJitterCount > 0) {
|
||||
const deduct = Math.min(8, metrics.baselineJitterCount * 2)
|
||||
alignmentScore -= deduct
|
||||
remediation.push(`Align ${metrics.baselineJitterCount} sibling row element(s) with 1px-3.5px vertical baseline jitter.`)
|
||||
}
|
||||
alignmentScore = Math.max(0, alignmentScore)
|
||||
|
||||
let typographyScore = 20
|
||||
if (metrics.lowContrastCount > 0) {
|
||||
const deduct = Math.min(14, metrics.lowContrastCount * 4)
|
||||
typographyScore -= deduct
|
||||
remediation.push(`Increase foreground/background contrast on ${metrics.lowContrastCount} low-contrast text element(s) to meet WCAG AA (>= 4.5:1).`)
|
||||
}
|
||||
if (metrics.descenderClipCount > 0) {
|
||||
const deduct = Math.min(6, metrics.descenderClipCount * 3)
|
||||
typographyScore -= deduct
|
||||
remediation.push(`Increase line-height (>= 1.15x font-size) on ${metrics.descenderClipCount} clipped heading/text container(s).`)
|
||||
}
|
||||
typographyScore = Math.max(0, typographyScore)
|
||||
|
||||
let interactiveScore = 15
|
||||
if (metrics.tinyTargetCount > 0) {
|
||||
const deduct = Math.min(9, metrics.tinyTargetCount * 3)
|
||||
interactiveScore -= deduct
|
||||
remediation.push(`Enlarge ${metrics.tinyTargetCount} interactive control(s) smaller than 24x24px minimum target size.`)
|
||||
}
|
||||
if (metrics.missingLabelCount > 0) {
|
||||
const deduct = Math.min(6, metrics.missingLabelCount * 3)
|
||||
interactiveScore -= deduct
|
||||
remediation.push(`Add accessible text or aria-label to ${metrics.missingLabelCount} unlabeled button/input control(s).`)
|
||||
}
|
||||
interactiveScore = Math.max(0, interactiveScore)
|
||||
|
||||
let tokenScore = 10
|
||||
if (metrics.uniqueFontSizes > 8) {
|
||||
const deduct = Math.min(5, (metrics.uniqueFontSizes - 8) * 2)
|
||||
tokenScore -= deduct
|
||||
remediation.push(`Consolidate typography scale (${metrics.uniqueFontSizes} distinct font sizes detected; target <= 7).`)
|
||||
}
|
||||
if (metrics.arbitraryInlineHexCount > 0) {
|
||||
const deduct = Math.min(5, metrics.arbitraryInlineHexCount)
|
||||
tokenScore -= deduct
|
||||
remediation.push(`Replace ${metrics.arbitraryInlineHexCount} hardcoded inline style color(s) with semantic CSS variables.`)
|
||||
}
|
||||
tokenScore = Math.max(0, tokenScore)
|
||||
|
||||
let runtimeScore = 10
|
||||
if (metrics.consoleErrorCount > 0) {
|
||||
runtimeScore -= Math.min(6, metrics.consoleErrorCount * 6)
|
||||
remediation.push(`Fix ${metrics.consoleErrorCount} uncaught browser console error(s).`)
|
||||
}
|
||||
if (metrics.failedNetworkCount > 0) {
|
||||
runtimeScore -= Math.min(4, metrics.failedNetworkCount * 4)
|
||||
remediation.push(`Fix ${metrics.failedNetworkCount} failing 4xx/5xx network request(s).`)
|
||||
}
|
||||
runtimeScore = Math.max(0, runtimeScore)
|
||||
|
||||
const totalScore = spatialScore + alignmentScore + typographyScore + interactiveScore + tokenScore + runtimeScore
|
||||
const grade: UXScorecard["grade"] = totalScore >= 95 ? "S" : totalScore >= 85 ? "A" : totalScore >= 75 ? "B" : totalScore >= 60 ? "C" : "F"
|
||||
const passed = totalScore >= 85 && metrics.horizontalOverflowPx <= 2 && metrics.collisionCount === 0 && metrics.consoleErrorCount === 0
|
||||
|
||||
return {
|
||||
score: totalScore,
|
||||
grade,
|
||||
passed,
|
||||
viewport: metrics.viewport,
|
||||
breakdown: {
|
||||
spatialLayout: { score: spatialScore, max: 25 },
|
||||
alignmentHarmony: { score: alignmentScore, max: 20 },
|
||||
typographyAndContrast: { score: typographyScore, max: 20 },
|
||||
interactiveCompleteness: { score: interactiveScore, max: 15 },
|
||||
designTokenDiscipline: { score: tokenScore, max: 10 },
|
||||
runtimeHealth: { score: runtimeScore, max: 10 },
|
||||
},
|
||||
remediation,
|
||||
remediations: remediation,
|
||||
evaluatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function saveUXScorecard(
|
||||
rootDir: string,
|
||||
scorecard: UXScorecard,
|
||||
): Promise<string> {
|
||||
const dir = path.join(rootDir, ".opencode", "artifacts");
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
const file = path.join(dir, "ux-scorecard.json");
|
||||
await fs.writeFile(file, JSON.stringify(scorecard, null, 2), "utf8");
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function loadLatestUXScorecard(
|
||||
rootDir: string,
|
||||
): Promise<UXScorecard | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(
|
||||
path.join(rootDir, ".opencode", "artifacts", "ux-scorecard.json"),
|
||||
"utf8",
|
||||
);
|
||||
return JSON.parse(raw) as UXScorecard;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerUXJudgeTools(editor: any, rootDir: string): void {
|
||||
editor.add({
|
||||
name: "get_ux_scorecard_script",
|
||||
description:
|
||||
"Return the JavaScript expression string to pass to `tools.browser.evaluate({ tabID, script })` to collect raw 6-axis UI/UX metrics for `tools.ui.score_ux`.",
|
||||
options: { namespace: "ui", codemode: true, pinned: true },
|
||||
input: { type: "object", properties: {} },
|
||||
output: { type: "object" },
|
||||
execute: async () => ({
|
||||
output: { script: UX_SCORECARD_BROWSER_SCRIPT },
|
||||
content: UX_SCORECARD_BROWSER_SCRIPT,
|
||||
}),
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "score_ux",
|
||||
description:
|
||||
"Compute a deterministic 0–100 Visual Quality & UI/UX Scorecard (`grade: S|A|B|C|F`, `passed: score >= 85`) from raw browser metrics returned by `get_ux_scorecard_script`, and save it to `.opencode/artifacts/ux-scorecard.json`.",
|
||||
options: { namespace: "ui", codemode: true, pinned: true },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
metrics: {
|
||||
type: "object",
|
||||
description:
|
||||
"Raw metrics object returned by evaluating get_ux_scorecard_script",
|
||||
},
|
||||
threshold: {
|
||||
type: "number",
|
||||
description: "Minimum passing score (default 85)",
|
||||
},
|
||||
},
|
||||
required: ["metrics"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { metrics: UXRawMetrics; threshold?: number }) => {
|
||||
const card = scoreUXMetrics(input.metrics);
|
||||
await saveUXScorecard(rootDir, card);
|
||||
return {
|
||||
output: card,
|
||||
content: `UX Scorecard: ${card.score}/100 (Grade ${card.grade}) — ${card.passed ? "PASSED" : "FAILED"}\n${card.remediation.map((r) => "- " + r).join("\n")}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// Pillar 4: On-Demand Domain Packs (src/packs/index.ts)
|
||||
// Consolidates Heretek-AI vertical plugins into one unified registry:
|
||||
// - re (opencode-rea): Reverse Engineering & Binary Triage.
|
||||
// - godot (godot-opencode): Godot 4.x Engine .tscn scene tree parser.
|
||||
// - gdevelop (GDevelop-Unlocked): GDevelop 5 game.json inspector.
|
||||
|
||||
export type DomainPackId = "re" | "godot" | "gdevelop";
|
||||
|
||||
export interface DomainPackStatus {
|
||||
id: DomainPackId;
|
||||
title: string;
|
||||
replacesRepo: string;
|
||||
active: boolean;
|
||||
autoDetected: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface DomainDetection {
|
||||
godot: boolean;
|
||||
gdevelop: boolean;
|
||||
reverseEngineering: boolean;
|
||||
recommendedPacks: string[];
|
||||
}
|
||||
|
||||
const activePacksByRoot = new Map<string, Set<DomainPackId>>();
|
||||
|
||||
export async function detectDomainPacks(
|
||||
rootDir: string,
|
||||
): Promise<DomainDetection> {
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = await fs.readdir(rootDir);
|
||||
} catch {
|
||||
return {
|
||||
godot: false,
|
||||
gdevelop: false,
|
||||
reverseEngineering: false,
|
||||
recommendedPacks: [],
|
||||
};
|
||||
}
|
||||
const godot =
|
||||
entries.includes("project.godot") ||
|
||||
entries.some((n) => n.endsWith(".tscn") || n.endsWith(".gd"));
|
||||
const gdevelop =
|
||||
entries.includes("game.json") || entries.some((n) => n.endsWith(".gdg"));
|
||||
const reverseEngineering = entries.some((n) =>
|
||||
/\.(?:exe|dll|so|dylib|bin|elf|apk|ipa|pcap)$/i.test(n),
|
||||
);
|
||||
const recommendedPacks: string[] = [];
|
||||
if (godot) recommendedPacks.push("godot");
|
||||
if (gdevelop) recommendedPacks.push("gdevelop");
|
||||
if (reverseEngineering) recommendedPacks.push("re");
|
||||
const key = path.resolve(rootDir);
|
||||
const manual = activePacksByRoot.get(key) ?? new Set<DomainPackId>();
|
||||
if (godot) manual.add("godot");
|
||||
if (gdevelop) manual.add("gdevelop");
|
||||
if (reverseEngineering) manual.add("re");
|
||||
activePacksByRoot.set(key, manual);
|
||||
return { godot, gdevelop, reverseEngineering, recommendedPacks };
|
||||
}
|
||||
|
||||
export async function listDomainPackStatus(
|
||||
rootDir: string,
|
||||
): Promise<DomainPackStatus[]> {
|
||||
const d = await detectDomainPacks(rootDir);
|
||||
const key = path.resolve(rootDir);
|
||||
const manual = activePacksByRoot.get(key) ?? new Set<DomainPackId>();
|
||||
return [
|
||||
{
|
||||
id: "re",
|
||||
title: "Reverse Engineering & Binary Analysis Pack",
|
||||
replacesRepo: "Heretek-AI/opencode-rea",
|
||||
active: manual.has("re"),
|
||||
autoDetected: d.reverseEngineering,
|
||||
reason: d.reverseEngineering
|
||||
? "Detected binary artifact in workspace"
|
||||
: "Dormant (0 token overhead)",
|
||||
},
|
||||
{
|
||||
id: "godot",
|
||||
title: "Godot 4.x Scene & GDScript Pack",
|
||||
replacesRepo: "Heretek-AI/godot-opencode",
|
||||
active: manual.has("godot"),
|
||||
autoDetected: d.godot,
|
||||
reason: d.godot
|
||||
? "Detected Godot project/scene"
|
||||
: "Dormant (0 token overhead)",
|
||||
},
|
||||
{
|
||||
id: "gdevelop",
|
||||
title: "GDevelop 5 Project & Debug Log Pack",
|
||||
replacesRepo: "Heretek-AI/GDevelop-Unlocked",
|
||||
active: manual.has("gdevelop"),
|
||||
autoDetected: d.gdevelop,
|
||||
reason: d.gdevelop
|
||||
? "Detected GDevelop game.json/.gdg"
|
||||
: "Dormant (0 token overhead)",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function activateDomainPack(
|
||||
rootDir: string,
|
||||
packId: DomainPackId,
|
||||
): void {
|
||||
const key = path.resolve(rootDir);
|
||||
const set = activePacksByRoot.get(key) ?? new Set<DomainPackId>();
|
||||
set.add(packId);
|
||||
activePacksByRoot.set(key, set);
|
||||
}
|
||||
|
||||
export interface GodotSceneReport {
|
||||
path: string;
|
||||
formatVersion: number;
|
||||
nodeCount: number;
|
||||
nodes: Array<{ name: string; type: string; parent: string }>;
|
||||
extResources: Array<{ id: string; type: string; path: string }>;
|
||||
externalScripts: string[];
|
||||
signalConnections: Array<{
|
||||
signal: string;
|
||||
from: string;
|
||||
to: string;
|
||||
method: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function inspectGodotScene(
|
||||
rootDir: string,
|
||||
scenePath: string,
|
||||
): Promise<GodotSceneReport> {
|
||||
const abs = path.resolve(rootDir, scenePath);
|
||||
const raw = await fs.readFile(abs, "utf8");
|
||||
const headerMatch = /\[gd_scene[^\]]*format=(\d+)/.exec(raw);
|
||||
const formatVersion = headerMatch ? Number(headerMatch[1]) : 3;
|
||||
const nodes: Array<{ name: string; type: string; parent: string }> = [];
|
||||
const nodeRe =
|
||||
/\[node\s+name="([^"]+)"(?:\s+type="([^"]+)")?(?:\s+parent="([^"]+)")?[^\]]*\]/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = nodeRe.exec(raw)) !== null) {
|
||||
nodes.push({ name: m[1], type: m[2] || "Instance", parent: m[3] || "." });
|
||||
}
|
||||
const extResources: Array<{ id: string; type: string; path: string }> = [];
|
||||
for (const em of raw.matchAll(/\[ext_resource\s+([^\]]+)\]/g)) {
|
||||
const attrs = em[1];
|
||||
const id =
|
||||
/id="([^"]+)"/.exec(attrs)?.[1] ?? /id=(\S+)/.exec(attrs)?.[1] ?? "";
|
||||
const type = /type="([^"]+)"/.exec(attrs)?.[1] ?? "";
|
||||
const p = /path="([^"]+)"/.exec(attrs)?.[1] ?? "";
|
||||
extResources.push({ id, type, path: p });
|
||||
void p;
|
||||
}
|
||||
const externalScripts: string[] = [];
|
||||
const extRe = /\[ext_resource\s+type="Script"\s+path="([^"]+)"[^\]]*\]/g;
|
||||
while ((m = extRe.exec(raw)) !== null) {
|
||||
externalScripts.push(m[1]);
|
||||
}
|
||||
const signalConnections: Array<{
|
||||
signal: string;
|
||||
from: string;
|
||||
to: string;
|
||||
method: string;
|
||||
}> = [];
|
||||
const connRe =
|
||||
/\[connection\s+signal="([^"]+)"\s+from="([^"]+)"\s+to="([^"]+)"\s+method="([^"]+)"[^\]]*\]/g;
|
||||
while ((m = connRe.exec(raw)) !== null) {
|
||||
signalConnections.push({
|
||||
signal: m[1],
|
||||
from: m[2],
|
||||
to: m[3],
|
||||
method: m[4],
|
||||
});
|
||||
}
|
||||
return {
|
||||
path: scenePath,
|
||||
formatVersion,
|
||||
nodeCount: nodes.length,
|
||||
nodes,
|
||||
extResources,
|
||||
externalScripts,
|
||||
signalConnections,
|
||||
};
|
||||
}
|
||||
|
||||
export interface GDevelopSceneSummary {
|
||||
name: string;
|
||||
objectCount: number;
|
||||
objectsCount: number;
|
||||
layerCount: number;
|
||||
eventCount: number;
|
||||
eventsCount: number;
|
||||
}
|
||||
|
||||
export interface GDevelopProjectReport {
|
||||
projectName: string;
|
||||
name: string;
|
||||
version: string;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
sceneCount: number;
|
||||
scenes: GDevelopSceneSummary[];
|
||||
globalObjectsCount: number;
|
||||
extensionsCount: number;
|
||||
extensionCount: number;
|
||||
externalEventCount: number;
|
||||
}
|
||||
|
||||
export async function inspectGDevelopProject(
|
||||
rootDir: string,
|
||||
projectPath: string = "game.json",
|
||||
): Promise<GDevelopProjectReport> {
|
||||
const abs = path.resolve(rootDir, projectPath);
|
||||
const parsed = JSON.parse(await fs.readFile(abs, "utf8"));
|
||||
const props = parsed.properties ?? {};
|
||||
const layouts = Array.isArray(parsed.layouts) ? parsed.layouts : [];
|
||||
const scenes: GDevelopSceneSummary[] = layouts.map((l: any) => ({
|
||||
name: String(l.name ?? "Untitled"),
|
||||
objectCount: Array.isArray(l.objects) ? l.objects.length : 0,
|
||||
objectsCount: Array.isArray(l.objects) ? l.objects.length : 0,
|
||||
layerCount: Array.isArray(l.layers) ? l.layers.length : 0,
|
||||
eventCount: Array.isArray(l.events) ? l.events.length : 0,
|
||||
eventsCount: Array.isArray(l.events) ? l.events.length : 0,
|
||||
}));
|
||||
return {
|
||||
projectName: String(props.name ?? "GDevelop Project"),
|
||||
name: String(props.name ?? "Untitled GDevelop Game"),
|
||||
version: String(props.version ?? "1.0.0"),
|
||||
windowWidth: Number(props.windowWidth ?? 800),
|
||||
windowHeight: Number(props.windowHeight ?? 600),
|
||||
sceneCount: scenes.length,
|
||||
scenes,
|
||||
globalObjectsCount: Array.isArray(parsed.objects)
|
||||
? parsed.objects.length
|
||||
: 0,
|
||||
extensionsCount: Array.isArray(parsed.eventsFunctionsExtensions)
|
||||
? parsed.eventsFunctionsExtensions.length
|
||||
: 0,
|
||||
extensionCount: Array.isArray(parsed.eventsFunctionsExtensions)
|
||||
? parsed.eventsFunctionsExtensions.length
|
||||
: 0,
|
||||
externalEventCount: Array.isArray(parsed.externalEvents)
|
||||
? parsed.externalEvents.length
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export interface BinaryTriageReport {
|
||||
path: string;
|
||||
sizeBytes: number;
|
||||
byteLength: number;
|
||||
sha256: string;
|
||||
format: string;
|
||||
entropyBitsPerByte: number;
|
||||
extractedStringsSample: string[];
|
||||
extractedStringsCount: number;
|
||||
suspiciousStrings: string[];
|
||||
}
|
||||
|
||||
export async function inspectBinaryTarget(
|
||||
rootDir: string,
|
||||
binaryPath: string,
|
||||
): Promise<BinaryTriageReport> {
|
||||
const abs = path.resolve(rootDir, binaryPath);
|
||||
const buf = await fs.readFile(abs);
|
||||
const sha256 = createHash("sha256").update(buf).digest("hex");
|
||||
let format = "UNKNOWN";
|
||||
if (buf.length >= 4) {
|
||||
if (
|
||||
buf[0] === 0x7f &&
|
||||
buf[1] === 0x45 &&
|
||||
buf[2] === 0x4c &&
|
||||
buf[3] === 0x46
|
||||
)
|
||||
format = "ELF";
|
||||
else if (buf[0] === 0x4d && buf[1] === 0x5a) format = "PE/COFF";
|
||||
else if (
|
||||
(buf[0] === 0xfe &&
|
||||
buf[1] === 0xed &&
|
||||
buf[2] === 0xfa &&
|
||||
(buf[3] === 0xce || buf[3] === 0xcf)) ||
|
||||
(buf[3] === 0xfe &&
|
||||
buf[2] === 0xed &&
|
||||
buf[1] === 0xfa &&
|
||||
(buf[0] === 0xce || buf[0] === 0xcf))
|
||||
)
|
||||
format = "Mach-O";
|
||||
else if (
|
||||
buf[0] === 0x00 &&
|
||||
buf[1] === 0x61 &&
|
||||
buf[2] === 0x73 &&
|
||||
buf[3] === 0x6d
|
||||
)
|
||||
format = "WASM";
|
||||
}
|
||||
const counts = new Array<number>(256).fill(0);
|
||||
for (let i = 0; i < buf.length; i++) counts[buf[i]]++;
|
||||
let entropy = 0;
|
||||
if (buf.length > 0) {
|
||||
for (const c of counts) {
|
||||
if (c > 0) {
|
||||
const p = c / buf.length;
|
||||
entropy -= p * Math.log2(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
const asciiContent = buf.toString("latin1");
|
||||
const strMatches = asciiContent.match(/[ -~]{5,120}/g) ?? [];
|
||||
const suspiciousRe =
|
||||
/(?:https?:\/\/|\/bin\/|\/etc\/|cmd\.exe|powershell|VirtualAlloc|LoadLibrary|dlopen|ptrace|AES|RSA|flag\{|password|token|secret)/i;
|
||||
const suspiciousStrings = strMatches
|
||||
.filter((s) => suspiciousRe.test(s))
|
||||
.slice(0, 40);
|
||||
return {
|
||||
path: binaryPath,
|
||||
sizeBytes: buf.length,
|
||||
byteLength: buf.length,
|
||||
sha256,
|
||||
format,
|
||||
entropyBitsPerByte: +entropy.toFixed(3),
|
||||
extractedStringsSample: strMatches.slice(0, 60),
|
||||
extractedStringsCount: strMatches.length,
|
||||
suspiciousStrings,
|
||||
};
|
||||
}
|
||||
|
||||
export const DOMAIN_PACK_ROLES: Record<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
mode: string;
|
||||
system: string;
|
||||
}
|
||||
> = {
|
||||
"specialist-godot": {
|
||||
id: "specialist-godot",
|
||||
name: "Godot 4 Architect",
|
||||
description:
|
||||
"Godot 4.x GDScript, Scene Tree (.tscn), Signal & Shader Specialist (consolidates godot-opencode).",
|
||||
mode: "subagent",
|
||||
system:
|
||||
"You are the Principal Godot 4.x Engine Architect (`specialist-godot`). Inspect `.tscn` scene trees via `tools.packs.godot_inspect_scene`, enforce typed GDScript 2.0 (`class_name`, `@export`, `@onready`), and verify signal connections cleanly.",
|
||||
},
|
||||
"specialist-gdevelop": {
|
||||
id: "specialist-gdevelop",
|
||||
name: "GDevelop 5 Specialist",
|
||||
description:
|
||||
"GDevelop 5 Event Sheet, Scene JSON (`game.json`), and Custom Behavior Specialist (consolidates GDevelop-Unlocked).",
|
||||
mode: "subagent",
|
||||
system:
|
||||
"You are the Principal GDevelop 5 Game Engineer (`specialist-gdevelop`). Inspect `game.json` via `tools.packs.gdevelop_inspect_game` before mutating scenes, objects, or event sheets, and preserve valid GDevelop JSON schema invariants.",
|
||||
},
|
||||
"specialist-re": {
|
||||
id: "specialist-re",
|
||||
name: "Reverse Engineering Analyst",
|
||||
description:
|
||||
"Binary Triage, Disassembly, Protocol Reconstruction & Security Analyst (consolidates opencode-rea).",
|
||||
mode: "subagent",
|
||||
system:
|
||||
"You are the Principal Reverse Engineering & Binary Intelligence Analyst (`specialist-re`). Use `tools.packs.re_inspect_binary` for static header, entropy, and string triage before deeper disassembly or decompilation.",
|
||||
},
|
||||
};
|
||||
|
||||
export function registerDomainPackTools(editor: any, rootDir: string): void {
|
||||
editor.namespace({
|
||||
name: "packs",
|
||||
description:
|
||||
"Unified Heretek Domain Packs (`re` from opencode-rea, `godot` from godot-opencode, `gdevelop` from GDevelop-Unlocked). Dormant until auto-detected or activated via `/pack`.",
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "status",
|
||||
description:
|
||||
"List all domain packs (`re`, `godot`, `gdevelop`) and whether they are active in this workspace.",
|
||||
options: { namespace: "packs", codemode: true, pinned: false },
|
||||
input: { type: "object", properties: {} },
|
||||
output: { type: "object" },
|
||||
execute: async () => {
|
||||
const detection = await detectDomainPacks(rootDir);
|
||||
const packs = await listDomainPackStatus(rootDir);
|
||||
return {
|
||||
output: { detection, packs },
|
||||
content: JSON.stringify({ detection, packs }, null, 2),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "godot_inspect_scene",
|
||||
description:
|
||||
"Parse a Godot 4.x `.tscn` scene file into its node hierarchy, attached scripts, and external resources.",
|
||||
options: { namespace: "packs", codemode: true, pinned: false },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to .tscn file" },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { path: string }) => {
|
||||
const res = await inspectGodotScene(rootDir, input.path);
|
||||
return { output: res, content: JSON.stringify(res, null, 2) };
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "gdevelop_inspect_project",
|
||||
description:
|
||||
"Parse a GDevelop 5 `game.json` project into scenes, object counts, event counts, and extensions.",
|
||||
options: { namespace: "packs", codemode: true, pinned: false },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to game.json (default 'game.json')",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { path?: string }) => {
|
||||
const res = await inspectGDevelopProject(
|
||||
rootDir,
|
||||
input?.path ?? "game.json",
|
||||
);
|
||||
return { output: res, content: JSON.stringify(res, null, 2) };
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "gdevelop_inspect_game",
|
||||
description:
|
||||
"Inspect a GDevelop 5 game.json project (alias of gdevelop_inspect_project).",
|
||||
options: { namespace: "packs", codemode: true, pinned: false },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to game.json (default 'game.json')",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { path?: string }) => {
|
||||
const res = await inspectGDevelopProject(
|
||||
rootDir,
|
||||
input?.path ?? "game.json",
|
||||
);
|
||||
return { output: res, content: JSON.stringify(res, null, 2) };
|
||||
},
|
||||
});
|
||||
|
||||
editor.add({
|
||||
name: "re_inspect_binary",
|
||||
description:
|
||||
"Identify binary format (ELF, PE, Mach-O, WASM), byte size, and printable strings sample from a binary file.",
|
||||
options: { namespace: "packs", codemode: true, pinned: false },
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to binary file" },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
output: { type: "object" },
|
||||
execute: async (input: { path: string }) => {
|
||||
const res = await inspectBinaryTarget(rootDir, input.path);
|
||||
return { output: res, content: JSON.stringify(res, null, 2) };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function registerDomainPacks(
|
||||
ctx: any,
|
||||
rootDir: string,
|
||||
): Promise<void> {
|
||||
const detection = await detectDomainPacks(rootDir);
|
||||
const ids: DomainPackId[] = [];
|
||||
if (detection.godot) ids.push("godot");
|
||||
if (detection.gdevelop) ids.push("gdevelop");
|
||||
if (detection.reverseEngineering) ids.push("re");
|
||||
if (ctx?.agent?.transform) {
|
||||
await ctx.agent.transform((editor: any) => {
|
||||
for (const [id, def] of Object.entries(DOMAIN_PACK_ROLES)) {
|
||||
const packId = id.replace("specialist-", "") as string;
|
||||
const active = ids.some(
|
||||
(p) => packId.startsWith(p) || (p === "re" && packId === "re"),
|
||||
);
|
||||
if (
|
||||
!active &&
|
||||
(detection.recommendedPacks.length > 0 || ids.length > 0)
|
||||
)
|
||||
continue;
|
||||
void active;
|
||||
editor.register?.(def);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ctx?.tool?.transform) {
|
||||
await ctx.tool.transform((editor: any) => {
|
||||
registerDomainPackTools(editor, rootDir);
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const SWARMOS_VERSION = "0.9.0"
|
||||
export const SWARMOS_VERSION = "1.0.0"
|
||||
@@ -0,0 +1,303 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as path from "node:path"
|
||||
import { loadBoard, verifyReleaseReady } from "../board/board.js"
|
||||
import { listEpistemicSources } from "../epistemic/dialectic.js"
|
||||
import { detectDomainPacks } from "../packs/index.js"
|
||||
import { listTelemetryFiles } from "../host/telemetry.js"
|
||||
|
||||
function escapeHtml(raw: unknown): string {
|
||||
return String(raw ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
export async function generateSwarmDashboardHtml(rootDir: string): Promise<{
|
||||
dashboardPath: string
|
||||
readyToShip: boolean
|
||||
taskCount: number
|
||||
contractCount: number
|
||||
uxScore: number | null
|
||||
}> {
|
||||
const [board, readiness, epistemicSources, packs, telemetry] = await Promise.all([
|
||||
loadBoard(rootDir),
|
||||
verifyReleaseReady(rootDir),
|
||||
listEpistemicSources(rootDir),
|
||||
detectDomainPacks(rootDir),
|
||||
listTelemetryFiles(rootDir).catch(() => []),
|
||||
])
|
||||
|
||||
let uxScorecard: any = null
|
||||
try {
|
||||
const rawUx = await fs.readFile(path.join(rootDir, ".opencode", "artifacts", "ux-scorecard.json"), "utf8")
|
||||
uxScorecard = JSON.parse(rawUx)
|
||||
} catch {
|
||||
// Optional UX scorecard
|
||||
}
|
||||
|
||||
const projectTitle = board.project?.name ?? path.basename(path.resolve(rootDir))
|
||||
const projectVision = board.project?.vision ?? "Autonomous Multi-Agent Engineering Workspace"
|
||||
const archetype = board.project?.aestheticArchetype ?? "High-Craft Technical Dark"
|
||||
|
||||
const tasksByStatus = {
|
||||
pending: board.tasks.filter((t) => t.status === "pending"),
|
||||
in_progress: board.tasks.filter((t) => t.status === "in_progress" || t.status === "ready_for_qa"),
|
||||
qa_rejected: board.tasks.filter((t) => t.status === "qa_rejected"),
|
||||
verified: board.tasks.filter((t) => t.status === "verified"),
|
||||
}
|
||||
|
||||
const renderTaskCard = (t: any) => {
|
||||
const latestReceipt = Array.isArray(t.qaReceipts) && t.qaReceipts.length > 0 ? t.qaReceipts[t.qaReceipts.length - 1] : null
|
||||
const badgeClass =
|
||||
latestReceipt?.verdict === "PASS"
|
||||
? "badge-pass"
|
||||
: latestReceipt?.verdict === "REJECT"
|
||||
? "badge-reject"
|
||||
: "badge-neutral"
|
||||
const badgeLabel = latestReceipt ? `QA: ${latestReceipt.verdict}` : "QA: PENDING"
|
||||
return `<div class="card task-card">
|
||||
<div class="card-header">
|
||||
<span class="mono-id">${escapeHtml(t.id)}</span>
|
||||
<span class="badge ${badgeClass}">${escapeHtml(badgeLabel)}</span>
|
||||
</div>
|
||||
<div class="card-title">${escapeHtml(t.title)}</div>
|
||||
<div class="card-meta">
|
||||
<span>Role: <strong>${escapeHtml(t.owner)}</strong></span>
|
||||
${t.worktree ? `<span class="mono-tag">${escapeHtml(t.worktree)}</span>` : ""}
|
||||
</div>
|
||||
<div class="card-paths">${(t.ownedPaths ?? []).map((p: string) => `<code>${escapeHtml(p)}</code>`).join(" ")}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SwarmOS v1.0 Command Center — ${escapeHtml(projectTitle)}</title>
|
||||
<style>
|
||||
:root {
|
||||
--surface-0: #0D0E11;
|
||||
--surface-1: #14161B;
|
||||
--surface-2: #1C1F26;
|
||||
--surface-3: #252932;
|
||||
--text-primary: #F3F4F6;
|
||||
--text-secondary: #9CA3AF;
|
||||
--text-muted: #6B7280;
|
||||
--border-subtle: rgba(255, 255, 255, 0.08);
|
||||
--accent-emerald: #10B981;
|
||||
--accent-amber: #F59E0B;
|
||||
--accent-rose: #F43F5E;
|
||||
--accent-sky: #38BDF8;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--surface-0);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: var(--space-6);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.shell { max-width: 1440px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-6); }
|
||||
header.topbar {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
background: var(--surface-1);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.brand { display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.brand h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
|
||||
.brand p { color: var(--text-secondary); font-size: 13px; }
|
||||
.status-pill {
|
||||
display: inline-flex; align-items: center; gap: var(--space-2);
|
||||
padding: 6px 12px; border-radius: 9999px; font-size: 12px; font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.status-ready { background: rgba(16, 185, 129, 0.14); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.35); }
|
||||
.status-blocked { background: rgba(245, 158, 11, 0.14); color: #FBBF24; border: 1px solid rgba(245, 158, 11, 0.35); }
|
||||
.metrics-grid {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4);
|
||||
}
|
||||
.metric-card {
|
||||
background: var(--surface-1); border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md); padding: var(--space-4);
|
||||
display: flex; flex-direction: column; gap: var(--space-1);
|
||||
}
|
||||
.metric-label { color: var(--text-secondary); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.metric-value { font-size: 24px; font-weight: 600; font-family: var(--font-mono); }
|
||||
.metric-sub { color: var(--text-muted); font-size: 12px; }
|
||||
.kanban {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4);
|
||||
}
|
||||
.kanban-col {
|
||||
background: var(--surface-1); border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md); padding: var(--space-4);
|
||||
display: flex; flex-direction: column; gap: var(--space-3); min-height: 260px;
|
||||
}
|
||||
.col-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
color: var(--text-secondary); padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.card {
|
||||
background: var(--surface-2); border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm); padding: var(--space-3);
|
||||
display: flex; flex-direction: column; gap: var(--space-2);
|
||||
}
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.mono-id { font-family: var(--font-mono); font-size: 11px; color: var(--accent-sky); }
|
||||
.badge { font-family: var(--font-mono); font-size: 10px; padding: 2px 6px; border-radius: var(--radius-sm); font-weight: 600; }
|
||||
.badge-pass { background: rgba(16, 185, 129, 0.16); color: #34D399; }
|
||||
.badge-reject { background: rgba(244, 63, 94, 0.16); color: #FB7185; }
|
||||
.badge-neutral { background: rgba(156, 163, 175, 0.14); color: #D1D5DB; }
|
||||
.card-title { font-weight: 500; font-size: 13px; color: var(--text-primary); }
|
||||
.card-meta { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-secondary); }
|
||||
.card-paths { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
code { font-family: var(--font-mono); font-size: 11px; background: var(--surface-3); padding: 1px 5px; border-radius: 3px; color: #E5E7EB; }
|
||||
.bottom-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); }
|
||||
.panel {
|
||||
background: var(--surface-1); border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md); padding: var(--space-4);
|
||||
display: flex; flex-direction: column; gap: var(--space-3);
|
||||
}
|
||||
.panel h2 { font-size: 14px; font-weight: 600; color: var(--text-primary); border-bottom: 1px solid var(--border-subtle); padding-bottom: var(--space-2); }
|
||||
ul.clean-list { list-style: none; display: flex; flex-direction: column; gap: var(--space-2); font-size: 13px; color: var(--text-secondary); }
|
||||
@media (max-width: 960px) {
|
||||
.metrics-grid, .kanban, .bottom-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<h1>SwarmOS v1.0 — The Agency Command Center · ${escapeHtml(projectTitle)}</h1>
|
||||
<p>${escapeHtml(projectVision)} · Archetype: <code>${escapeHtml(archetype)}</code></p>
|
||||
</div>
|
||||
<div class="status-pill ${readiness.ready ? "status-ready" : "status-blocked"}">
|
||||
${readiness.ready ? "RELEASE GATE: READY TO SHIP" : `RELEASE GATE: ${readiness.blockers.length} BLOCKER(S)`}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Locked Contracts</span>
|
||||
<span class="metric-value">${board.contracts.length}</span>
|
||||
<span class="metric-sub">Architect Gate 1 Invariants</span>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Verified Tasks</span>
|
||||
<span class="metric-value">${readiness.verifiedTasks.length} / ${board.tasks.filter((t) => t.status !== "superseded").length}</span>
|
||||
<span class="metric-sub">Signed Adversarial QA Receipts</span>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Visual UI/UX Judge</span>
|
||||
<span class="metric-value">${uxScorecard ? `${uxScorecard.score}/100 (${escapeHtml(uxScorecard.grade)})` : "Not Run"}</span>
|
||||
<span class="metric-sub">${uxScorecard ? (uxScorecard.passed ? "Passed >= 85 Threshold" : "Needs Visual Remediation") : "Run tools.ui.score_ux"}</span>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Epistemic Sources & Packs</span>
|
||||
<span class="metric-value">${epistemicSources.length} src · ${packs.recommendedPacks.length} pack</span>
|
||||
<span class="metric-sub">Active Packs: ${escapeHtml(packs.recommendedPacks.join(", ") || "core")}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="kanban">
|
||||
<div class="kanban-col">
|
||||
<div class="col-header"><span>Pending</span><span>${tasksByStatus.pending.length}</span></div>
|
||||
${tasksByStatus.pending.map(renderTaskCard).join("") || `<div class="metric-sub">No pending tasks.</div>`}
|
||||
</div>
|
||||
<div class="kanban-col">
|
||||
<div class="col-header"><span>In Progress / Ready QA</span><span>${tasksByStatus.in_progress.length}</span></div>
|
||||
${tasksByStatus.in_progress.map(renderTaskCard).join("") || `<div class="metric-sub">No active tasks.</div>`}
|
||||
</div>
|
||||
<div class="kanban-col">
|
||||
<div class="col-header"><span>QA Rejected</span><span>${tasksByStatus.qa_rejected.length}</span></div>
|
||||
${tasksByStatus.qa_rejected.map(renderTaskCard).join("") || `<div class="metric-sub">Zero rejected tasks.</div>`}
|
||||
</div>
|
||||
<div class="kanban-col">
|
||||
<div class="col-header"><span>Verified (PASS)</span><span>${tasksByStatus.verified.length}</span></div>
|
||||
${tasksByStatus.verified.map(renderTaskCard).join("") || `<div class="metric-sub">No verified tasks yet.</div>`}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bottom-grid">
|
||||
<div class="panel">
|
||||
<h2>Locked Architecture Contracts</h2>
|
||||
<ul class="clean-list">
|
||||
${board.contracts.map((c) => `<li><code>${escapeHtml(c.id)}</code> — <strong>${escapeHtml(c.title)}</strong> (${escapeHtml(c.files.join(", "))})</li>`).join("") || "<li>No contracts locked yet.</li>"}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>100-Point UI/UX Scorecard Breakdown</h2>
|
||||
<ul class="clean-list">
|
||||
${
|
||||
uxScorecard?.breakdown
|
||||
? Object.entries(uxScorecard.breakdown)
|
||||
.map(([k, v]: [string, any]) => `<li><strong>${escapeHtml(k)}</strong>: <code>${v.score}/${v.max}</code></li>`)
|
||||
.join("")
|
||||
: "<li>Run <code>tools.ui.score_ux</code> to populate live 6-axis UI/UX metrics.</li>"
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Epistemic Cache & Telemetry</h2>
|
||||
<ul class="clean-list">
|
||||
<li><strong>Verified SHA-256 Sources</strong>: ${epistemicSources.length}</li>
|
||||
<li><strong>Detected Domain Packs</strong>: ${escapeHtml(packs.recommendedPacks.join(", ") || "none")}</li>
|
||||
<li><strong>Recorded Telemetry Sessions</strong>: ${Array.isArray(telemetry) ? telemetry.length : 0}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const outDir = path.join(rootDir, ".opencode", "artifacts")
|
||||
await fs.mkdir(outDir, { recursive: true })
|
||||
const dashboardPath = path.join(outDir, "swarm-dashboard.html")
|
||||
await fs.writeFile(dashboardPath, html, "utf8")
|
||||
|
||||
return {
|
||||
dashboardPath,
|
||||
readyToShip: readiness.ready,
|
||||
taskCount: board.tasks.length,
|
||||
contractCount: board.contracts.length,
|
||||
uxScore: uxScorecard?.score ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function registerDashboardTools(editor: any, rootDir: string) {
|
||||
editor.add({
|
||||
name: "render_dashboard",
|
||||
description:
|
||||
"Generate the self-contained 'The Agency' SwarmOS Live Command Center WebUI HTML5 dashboard at `.opencode/artifacts/swarm-dashboard.html`.",
|
||||
options: { namespace: "org", codemode: true, pinned: true },
|
||||
input: { type: "object", properties: {} },
|
||||
output: { type: "object" },
|
||||
execute: async () => {
|
||||
const res = await generateSwarmDashboardHtml(rootDir)
|
||||
return {
|
||||
output: res,
|
||||
content: `Generated SwarmOS Command Center WebUI -> ${res.dashboardPath}`,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
+3437
-1967
File diff suppressed because it is too large.
Load diff
@@ -167,24 +167,24 @@ describe("SwarmOS v0.9.0 Complete Roadmap (Task Budgets, Session Replay, Warm Pa
|
||||
|
||||
it("caches unchanged file diagnostics in 0ms and locates symbol definitions & references", async () => {
|
||||
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true })
|
||||
const fileA = path.join(tmpDir, "src/alpha.js")
|
||||
const fileB = path.join(tmpDir, "src/beta.js")
|
||||
const fileA = path.join(tmpDir, "src/alpha.mjs")
|
||||
const fileB = path.join(tmpDir, "src/beta.mjs")
|
||||
await fs.writeFile(fileA, "export function computeMetric(x) { return x * 2; }\n", "utf8")
|
||||
await fs.writeFile(fileB, "import { computeMetric } from './alpha.js';\nexport const res = computeMetric(21);\n", "utf8")
|
||||
await fs.writeFile(fileB, "import { computeMetric } from './alpha.mjs';\nexport const res = computeMetric(21);\n", "utf8")
|
||||
|
||||
const firstPass = await runParallelDiagnostics(tmpDir, ["src/alpha.js", "src/beta.js"], (r, p) => runQuickDiagnostics(r, p))
|
||||
const firstPass = await runParallelDiagnostics(tmpDir, ["src/alpha.mjs", "src/beta.mjs"], (r, p) => runQuickDiagnostics(r, p))
|
||||
expect(firstPass).toEqual([])
|
||||
expect(warmDiagnosticStats.cacheMisses).toBe(2)
|
||||
expect(warmDiagnosticStats.cacheHits).toBe(0)
|
||||
|
||||
// Second pass on unchanged files hits the warm mtime+size cache
|
||||
const secondPass = await runParallelDiagnostics(tmpDir, ["src/alpha.js", "src/beta.js"], (r, p) => runQuickDiagnostics(r, p))
|
||||
const secondPass = await runParallelDiagnostics(tmpDir, ["src/alpha.mjs", "src/beta.mjs"], (r, p) => runQuickDiagnostics(r, p))
|
||||
expect(secondPass).toEqual([])
|
||||
expect(warmDiagnosticStats.cacheHits).toBe(2)
|
||||
|
||||
const defs = await findSymbolDefinitions(tmpDir, "computeMetric")
|
||||
expect(defs.length).toBe(1)
|
||||
expect(defs[0].file).toBe("src/alpha.js")
|
||||
expect(defs[0].file).toBe("src/alpha.mjs")
|
||||
|
||||
const refs = await findSymbolReferences(tmpDir, "computeMetric")
|
||||
expect(refs.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import {
|
||||
cacheEpistemicSource,
|
||||
verifyEpistemicClaims,
|
||||
classifySpdxLicense,
|
||||
generateGrillQuestions,
|
||||
discoverClaudeCodeAssets,
|
||||
scoreUXMetrics,
|
||||
detectDomainPacks,
|
||||
inspectGodotScene,
|
||||
inspectGDevelopProject,
|
||||
inspectBinaryTarget,
|
||||
generateSwarmDashboardHtml,
|
||||
} from "../src/index.js"
|
||||
|
||||
describe("SwarmOS v1.0.0 Unified Heretek Platform", () => {
|
||||
test("Pillar 1: IUMBTEMS Epistemic Cache, Verbatim Quote Verification & SPDX License Check", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "swarmos-epistemic-"))
|
||||
const cached = await cacheEpistemicSource(tmp, {
|
||||
title: "Effect 4.0 Architecture Spec",
|
||||
url: "https://effect.website/docs/v4",
|
||||
spdxLicense: "MIT",
|
||||
content: "Effect 4.0 introduces LayerMap for location-scoped service lifecycles and zero-leak fiber supervision.",
|
||||
})
|
||||
expect(cached.shortHash).toHaveLength(12)
|
||||
expect(cached.licenseCategory).toBe("permissive")
|
||||
|
||||
const goodMd = `According to the spec, it works [VERIFIED: ${cached.shortHash} | "LayerMap for location-scoped service lifecycles"].`
|
||||
const auditPass = await verifyEpistemicClaims(tmp, goodMd)
|
||||
expect(auditPass.passed).toBe(true)
|
||||
expect(auditPass.verifiedCount).toBe(1)
|
||||
|
||||
const badMd = `Hallucinated claim [VERIFIED: ${cached.shortHash} | "non-existent quote string"].`
|
||||
const auditFail = await verifyEpistemicClaims(tmp, badMd)
|
||||
expect(auditFail.passed).toBe(false)
|
||||
expect(auditFail.failedClaims).toHaveLength(1)
|
||||
|
||||
expect(classifySpdxLicense("GPL-3.0-only")).toBe("copyleft-contaminating")
|
||||
expect(generateGrillQuestions("Distributed SQLite Swarm").questions.length).toBeGreaterThanOrEqual(5)
|
||||
})
|
||||
|
||||
test("Pillar 2: claude-4-opencode Bridge discovers .claude commands, agents, and skills", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "swarmos-claude-"))
|
||||
await fs.mkdir(path.join(tmp, ".claude", "commands"), { recursive: true })
|
||||
await fs.mkdir(path.join(tmp, ".claude", "agents"), { recursive: true })
|
||||
await fs.mkdir(path.join(tmp, ".claude", "skills", "clean-arch"), { recursive: true })
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tmp, ".claude", "commands", "audit-sec.md"),
|
||||
"---\ndescription: Security audit command\n---\nRun security audit on $ARGUMENTS",
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(tmp, ".claude", "agents", "sec-reviewer.md"),
|
||||
"---\nname: Security Reviewer\ndescription: Audits auth flows\n---\nYou are a security reviewer.",
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(tmp, ".claude", "skills", "clean-arch", "SKILL.md"),
|
||||
"---\nname: Clean Architecture\ndescription: Enforces hexagonal boundaries\n---\nKeep domain pure.",
|
||||
)
|
||||
|
||||
const assets = await discoverClaudeCodeAssets(tmp)
|
||||
expect(assets.commands).toHaveLength(1)
|
||||
expect(assets.commands[0].name).toBe("audit-sec")
|
||||
expect(assets.agents).toHaveLength(1)
|
||||
expect(assets.skills).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("Pillar 3: Quantitative 100-Point Visual Quality & UI/UX Judge", () => {
|
||||
const cleanScore = scoreUXMetrics({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
horizontalOverflowPx: 0,
|
||||
elementOverflowCount: 0,
|
||||
collisionCount: 0,
|
||||
baselineJitterCount: 0,
|
||||
lowContrastCount: 0,
|
||||
descenderClipCount: 0,
|
||||
tinyTargetCount: 0,
|
||||
missingLabelCount: 0,
|
||||
uniqueFontSizes: 5,
|
||||
arbitraryInlineHexCount: 0,
|
||||
consoleErrorCount: 0,
|
||||
failedNetworkCount: 0,
|
||||
})
|
||||
expect(cleanScore.score).toBe(100)
|
||||
expect(cleanScore.grade).toBe("S")
|
||||
expect(cleanScore.passed).toBe(true)
|
||||
|
||||
const sloppyScore = scoreUXMetrics({
|
||||
viewport: { width: 375, height: 812 },
|
||||
horizontalOverflowPx: 42,
|
||||
elementOverflowCount: 2,
|
||||
collisionCount: 2,
|
||||
baselineJitterCount: 3,
|
||||
lowContrastCount: 3,
|
||||
descenderClipCount: 1,
|
||||
tinyTargetCount: 2,
|
||||
missingLabelCount: 1,
|
||||
uniqueFontSizes: 11,
|
||||
arbitraryInlineHexCount: 6,
|
||||
consoleErrorCount: 1,
|
||||
failedNetworkCount: 0,
|
||||
})
|
||||
expect(sloppyScore.passed).toBe(false)
|
||||
expect(sloppyScore.remediation.length).toBeGreaterThan(3)
|
||||
})
|
||||
|
||||
test("Pillar 4: On-Demand Domain Packs (Godot 4, GDevelop 5, Reverse Engineering)", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "swarmos-packs-"))
|
||||
await fs.writeFile(path.join(tmp, "project.godot"), "[application]\nconfig/name=\"TestGame\"\n")
|
||||
await fs.writeFile(
|
||||
path.join(tmp, "Player.tscn"),
|
||||
'[gd_scene load_steps=2 format=3]\n[ext_resource type="Script" path="res://Player.gd" id="1"]\n[node name="Player" type="CharacterBody2D"]\n[node name="Sprite2D" type="Sprite2D" parent="."]\n[connection signal="hit" from="." to="." method="_on_hit"]\n',
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(tmp, "game.json"),
|
||||
JSON.stringify({
|
||||
properties: { name: "SpaceJump", version: "1.0.0" },
|
||||
layouts: [{ name: "MainScene", objects: [{ name: "Hero" }], layers: [{ name: "Base" }], events: [{}, {}] }],
|
||||
externalEvents: [],
|
||||
eventsFunctionsExtensions: [{ name: "DoubleJump" }],
|
||||
}),
|
||||
)
|
||||
await fs.writeFile(path.join(tmp, "sample.bin"), Buffer.from("\x7fELF...http://api.internal.local/v1/secret_token_check..."))
|
||||
|
||||
const detected = await detectDomainPacks(tmp)
|
||||
expect(detected.godot).toBe(true)
|
||||
expect(detected.gdevelop).toBe(true)
|
||||
expect(detected.reverseEngineering).toBe(true)
|
||||
|
||||
const scene = await inspectGodotScene(tmp, "Player.tscn")
|
||||
expect(scene.nodeCount).toBe(2)
|
||||
expect(scene.signalConnections).toHaveLength(1)
|
||||
|
||||
const gd = await inspectGDevelopProject(tmp, "game.json")
|
||||
expect(gd.projectName).toBe("SpaceJump")
|
||||
expect(gd.scenes[0].eventCount).toBe(2)
|
||||
|
||||
const bin = await inspectBinaryTarget(tmp, "sample.bin")
|
||||
expect(bin.format).toBe("ELF")
|
||||
expect(bin.suspiciousStrings.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("Pillar 5: The Agency Live Swarm Command Center HTML5 Dashboard", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "swarmos-dash-"))
|
||||
const dash = await generateSwarmDashboardHtml(tmp)
|
||||
expect(dash.dashboardPath).toContain("swarm-dashboard.html")
|
||||
const html = await fs.readFile(dash.dashboardPath, "utf8")
|
||||
expect(html).toContain("SwarmOS v1.0 — The Agency Command Center")
|
||||
expect(html).toContain("100-Point UI/UX Scorecard Breakdown")
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user