role-os 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.1
4
+
5
+ ### Added
6
+ - **`roleos audit` CLI** — first-class entry point for deep audit with subcommands: `audit`, `audit manifest`, `audit manifest --generate`, `audit status`, `audit verify`
7
+ - **Shared state machine** (`src/state-machine.mjs`) — canonical step/run transitions shared by both runners
8
+ - **Shared tool profiles** (`src/tool-profiles.mjs`) — extracted from dispatch.mjs to break trial→dispatch coupling
9
+
10
+ ### Fixed
11
+ - **P3-1:** Cycle detection in composite execution (`detectCycles` + visited-set guard in `findUnreachable`)
12
+ - **P3-2:** Dual-active guard in `startNext`/`startNextStep` prevents two steps active simultaneously
13
+ - **P3-3:** Atomic persistence — `saveRun` writes to temp file then renames
14
+ - **P4-1:** Dependency Auditor has own artifact contract (`dependency-audit`), pack handoff corrected
15
+ - **P4-2:** `partitionBrief` returns topic-only for unknown roles instead of full brief
16
+ - **P4-3:** Atom kind normalization layer bridges scout `.kind` and atom `.claim_kind`
17
+ - **P4-4:** `/dev/stdin` → `readFileSync(0)` for Windows compatibility in all 5 hooks
18
+ - **P4-5:** TOOL_PROFILES extracted to shared module, eliminating trial→dispatch coupling
19
+ - Node 18 compatibility fix for `import.meta.dirname` in deep-audit-proof test
20
+
21
+ ### Tests
22
+ - 18 new tests (audit-cmd, audit-p5, deep-audit-proof) — total: 954
23
+
3
24
  ## 2.2.0
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -134,6 +134,12 @@ roleos complete artifact.md # Complete with artifact
134
134
  roleos explain # Show full state
135
135
  roleos report # Completion report
136
136
 
137
+ # Deep audit:
138
+ roleos audit manifest --generate # Create audit-manifest.json
139
+ roleos audit # Start component-level deep audit
140
+ roleos audit status # Check audit progress
141
+ roleos audit verify # Verify manifest and outputs
142
+
137
143
  # Or go manual:
138
144
  roleos start "fix the crash" # Entry decision only (no run)
139
145
  roleos packet new feature
@@ -207,18 +213,21 @@ role-os/
207
213
  entry-cmd.mjs ← `roleos start` CLI command
208
214
  run.mjs ← Persistent run engine: create → step → pause → resume → report
209
215
  run-cmd.mjs ← `roleos run/resume/next/explain/complete/fail` + interventions
210
- mission.mjs ← 7 named mission types (feature, bugfix, treatment, docs, security, research, brainstorm)
216
+ mission.mjs ← 8 named mission types (feature, bugfix, treatment, docs, security, research, brainstorm, deep-audit)
211
217
  mission-run.mjs ← Mission runner: create → step → complete → report
212
218
  mission-cmd.mjs ← `roleos mission` CLI commands
219
+ audit-cmd.mjs ← `roleos audit` — deep audit entry point with manifest generation
213
220
  route.mjs ← 54-role routing + dynamic chain builder
214
221
  packs.mjs ← 9 calibrated team packs + auto-selection
215
222
  conflicts.mjs ← 4-pass conflict detection
216
223
  escalation.mjs ← Auto-routing for blocked/rejected/split
217
224
  evidence.mjs ← Structured evidence + role-aware requirements
218
225
  dispatch.mjs ← Runtime dispatch manifests for multi-claude
226
+ tool-profiles.mjs ← Per-role tool sandboxing (shared by dispatch + trial)
227
+ state-machine.mjs ← Canonical step/run transition maps
219
228
  artifacts.mjs ← Per-role artifact contracts + pack handoffs
220
229
  decompose.mjs ← Composite task detection + splitting
221
- composite.mjs ← Dependency-ordered execution + recovery
230
+ composite.mjs ← Dependency-ordered execution + recovery + cycle detection
222
231
  replan.mjs ← Mid-run adaptive replanning
223
232
  calibration.mjs ← Outcome recording + weight tuning
224
233
  hooks.mjs ← 5 lifecycle hooks for runtime enforcement
@@ -226,7 +235,7 @@ role-os/
226
235
  brainstorm.mjs ← Evidence modes, request validation, finding/synthesis/judge schemas
227
236
  brainstorm-roles.mjs ← Role-native schemas, input partitioning, blindspot enforcement, cross-exam
228
237
  brainstorm-render.mjs ← Two-layer rendering: lexical bans, render schemas, debate transcript
229
- test/ ← 936 tests across 31 test files
238
+ test/ ← 954 tests across 33 test files
230
239
  starter-pack/ ← Drop-in role contracts, policies, schemas, workflows
231
240
  ```
232
241
 
package/bin/roleos.mjs CHANGED
@@ -12,6 +12,7 @@ import { packsCommand } from "../src/packs-cmd.mjs";
12
12
  import { scaffoldClaude, doctor, formatDoctor } from "../src/session.mjs";
13
13
  import { artifactsCommand } from "../src/artifacts-cmd.mjs";
14
14
  import { missionCommand } from "../src/mission-cmd.mjs";
15
+ import { auditCommand } from "../src/audit-cmd.mjs";
15
16
  import { startCommand } from "../src/entry-cmd.mjs";
16
17
  import {
17
18
  runCommand, resumeCommand, nextCommand, explainCommand,
@@ -59,6 +60,11 @@ Usage:
59
60
  roleos artifacts show <role> Show artifact contract for a role
60
61
  roleos artifacts validate <role> <file> Validate a file against a contract
61
62
  roleos artifacts chain <pack> Show pack handoff flow
63
+ roleos audit Start a deep audit on the current repo
64
+ roleos audit manifest Show the audit manifest
65
+ roleos audit manifest --generate Generate a skeleton manifest from src/
66
+ roleos audit status Show audit run progress
67
+ roleos audit verify Verify manifest and audit outputs
62
68
  roleos mission list List all missions
63
69
  roleos mission show <key> Show full mission detail
64
70
  roleos mission suggest <text> Suggest a mission for a task
@@ -181,6 +187,9 @@ try {
181
187
  case "friction":
182
188
  await frictionCommand(args);
183
189
  break;
190
+ case "audit":
191
+ await auditCommand(args);
192
+ break;
184
193
  case "mission":
185
194
  await missionCommand(args);
186
195
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "role-os",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Role OS — a multi-Claude operating system where 54 specialized roles execute work through contracts, conflict detection, escalation, and structured evidence. 9 team packs, 8 missions including deep audit with manifest-scaled dynamic dispatch and brainstorm with traceable disagreement.",
5
5
  "homepage": "https://mcp-tool-shop-org.github.io/role-os/",
6
6
  "bugs": {
package/src/artifacts.mjs CHANGED
@@ -106,6 +106,14 @@ export const ROLE_ARTIFACT_CONTRACTS = {
106
106
  consumedBy: ["Backend Engineer", "Coverage Auditor", "Security Reviewer"],
107
107
  completionRule: "Entrypoints listed. Module responsibilities described. Commands documented.",
108
108
  },
109
+ "Dependency Auditor": {
110
+ artifactType: "dependency-audit",
111
+ requiredSections: ["vulnerability-summary", "outdated-inventory"],
112
+ optionalSections: ["supply-chain-risks", "update-recommendations", "license-audit"],
113
+ requiredEvidence: [],
114
+ consumedBy: ["Critic Reviewer", "Security Reviewer"],
115
+ completionRule: "Vulnerabilities triaged. Outdated deps inventoried with severity.",
116
+ },
109
117
  "Metadata Curator": {
110
118
  artifactType: "metadata-audit",
111
119
  requiredSections: ["manifest-audit", "registry-alignment"],
@@ -380,7 +388,7 @@ export const PACK_HANDOFF_CONTRACTS = {
380
388
  security: {
381
389
  flow: [
382
390
  { role: "Security Reviewer", produces: "security-findings", consumedBy: "Critic Reviewer" },
383
- { role: "Dependency Auditor", produces: "metadata-audit", consumedBy: "Critic Reviewer" },
391
+ { role: "Dependency Auditor", produces: "dependency-audit", consumedBy: "Critic Reviewer" },
384
392
  { role: "Critic Reviewer", produces: "verdict", consumedBy: null },
385
393
  ],
386
394
  },
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Audit CLI — Deep Audit entry point.
3
+ *
4
+ * roleos audit Run deep audit on current repo
5
+ * roleos audit manifest Show or generate the audit manifest
6
+ * roleos audit status Show audit run progress
7
+ * roleos audit verify Re-verify findings against current code
8
+ *
9
+ * This is a first-class shortcut into the deep-audit mission.
10
+ * Under the hood it creates a mission run with dynamic dispatch.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { getMission, suggestMission } from "./mission.mjs";
16
+ import {
17
+ createRun,
18
+ startNextStep,
19
+ getRunPosition,
20
+ getArtifactChain,
21
+ generateCompletionReport,
22
+ formatCompletionReport,
23
+ } from "./mission-run.mjs";
24
+ import {
25
+ createPersistentRun, findActiveRun, listRuns, loadRun,
26
+ startNext, explainRun, getPosition, saveRun,
27
+ } from "./run.mjs";
28
+
29
+ // ── Constants ────────────────────────────────────────────────────────────────
30
+
31
+ const MANIFEST_FILE = "audit-manifest.json";
32
+
33
+ // ── Main dispatch ────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * @param {string[]} args
37
+ */
38
+ export async function auditCommand(args) {
39
+ const sub = args[0] || "run";
40
+
41
+ switch (sub) {
42
+ case "run":
43
+ case "start":
44
+ return cmdRun(args.slice(1));
45
+ case "manifest":
46
+ return cmdManifest(args.slice(1));
47
+ case "status":
48
+ return cmdStatus();
49
+ case "verify":
50
+ return cmdVerify();
51
+ case "help":
52
+ return cmdHelp();
53
+ default:
54
+ // If the first arg isn't a subcommand, treat everything as a task description
55
+ if (!["run", "start", "manifest", "status", "verify", "help"].includes(sub)) {
56
+ return cmdRun(args);
57
+ }
58
+ cmdHelp();
59
+ }
60
+ }
61
+
62
+ // ── roleos audit [run] ───────────────────────────────────────────────────────
63
+
64
+ function cmdRun(extraArgs) {
65
+ const cwd = process.cwd();
66
+ const manifestPath = join(cwd, MANIFEST_FILE);
67
+
68
+ // Check if manifest exists
69
+ if (!existsSync(manifestPath)) {
70
+ console.log("\nNo audit-manifest.json found in current directory.");
71
+ console.log("Generate one first with: roleos audit manifest --generate\n");
72
+ console.log("The manifest defines your repo's components and boundaries.");
73
+ console.log("Deep audit uses it to dispatch one auditor per component.\n");
74
+ process.exit(1);
75
+ }
76
+
77
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
78
+
79
+ // Validate manifest shape
80
+ const issues = validateManifest(manifest);
81
+ if (issues.length > 0) {
82
+ console.log("\nAudit manifest has issues:\n");
83
+ for (const issue of issues) {
84
+ console.log(` - ${issue}`);
85
+ }
86
+ console.log("\nFix the manifest and re-run.\n");
87
+ process.exit(1);
88
+ }
89
+
90
+ const taskDesc = extraArgs.length > 0
91
+ ? extraArgs.join(" ")
92
+ : `Deep audit of ${manifest.repo || "current repo"}`;
93
+
94
+ // Create a persistent run via the deep-audit mission
95
+ const run = createPersistentRun(taskDesc, cwd, { forceMission: "deep-audit" });
96
+
97
+ console.log(`\nDeep Audit Started`);
98
+ console.log(`──────────────────`);
99
+ console.log(`Run: ${run.id}`);
100
+ console.log(`Repo: ${manifest.repo || "unknown"}`);
101
+ console.log(`Components: ${manifest.components?.length || 0}`);
102
+ console.log(`Boundaries: ${manifest.boundaries?.length || 0}`);
103
+ console.log(`Steps: ${run.steps.length}`);
104
+ console.log(`\nThe audit will dispatch:`);
105
+ console.log(` - Component Auditor ×${manifest.components?.length || 0}`);
106
+ console.log(` - Test Truth Auditor ×${manifest.components?.length || 0}`);
107
+ console.log(` - Seam Auditor ×${manifest.boundaries?.length || 0}`);
108
+ console.log(` - Audit Synthesizer ×1`);
109
+ console.log(` - Critic Reviewer ×1`);
110
+ console.log(`\nRun 'roleos next' to begin the first step.`);
111
+ console.log(`Run 'roleos audit status' to check progress.\n`);
112
+ }
113
+
114
+ // ── roleos audit manifest ────────────────────────────────────────────────────
115
+
116
+ function cmdManifest(args) {
117
+ const cwd = process.cwd();
118
+ const manifestPath = join(cwd, MANIFEST_FILE);
119
+
120
+ if (args.includes("--generate") || args.includes("-g")) {
121
+ return generateManifest(cwd, manifestPath);
122
+ }
123
+
124
+ if (!existsSync(manifestPath)) {
125
+ console.log("\nNo audit-manifest.json found.");
126
+ console.log("Run 'roleos audit manifest --generate' to create one.\n");
127
+ return;
128
+ }
129
+
130
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
131
+ const issues = validateManifest(manifest);
132
+
133
+ console.log(`\nAudit Manifest: ${manifest.repo || "unknown"}`);
134
+ console.log(`──────────────────────────────────────────`);
135
+ console.log(`Version: ${manifest.version || "unknown"}`);
136
+ console.log(`Language: ${manifest.language || "unknown"}`);
137
+ console.log(`Source: ${manifest.total_source_lines || "?"} lines`);
138
+ console.log(`Tests: ${manifest.total_test_lines || "?"} lines`);
139
+ console.log(`Components: ${manifest.components?.length || 0}`);
140
+ console.log(`Boundaries: ${manifest.boundaries?.length || 0}`);
141
+
142
+ if (manifest.components?.length > 0) {
143
+ console.log(`\nComponents:`);
144
+ for (const c of manifest.components) {
145
+ const paths = c.owned_paths?.length || 0;
146
+ console.log(` - ${c.id}: ${c.description || ""} (${paths} paths)`);
147
+ }
148
+ }
149
+
150
+ if (manifest.boundaries?.length > 0) {
151
+ console.log(`\nBoundaries:`);
152
+ for (const b of manifest.boundaries) {
153
+ const label = b.id || `${b.from} → ${b.to}`;
154
+ console.log(` - ${label}: ${b.contract || b.description || ""}`);
155
+ }
156
+ }
157
+
158
+ if (issues.length > 0) {
159
+ console.log(`\nIssues:`);
160
+ for (const issue of issues) {
161
+ console.log(` ! ${issue}`);
162
+ }
163
+ } else {
164
+ console.log(`\nManifest is valid.`);
165
+ }
166
+
167
+ console.log("");
168
+ }
169
+
170
+ function generateManifest(cwd, manifestPath) {
171
+ if (existsSync(manifestPath)) {
172
+ console.log(`\nManifest already exists at ${MANIFEST_FILE}.`);
173
+ console.log("Edit it manually or delete it to regenerate.\n");
174
+ return;
175
+ }
176
+
177
+ // Build a skeleton manifest by scanning src/
178
+ const srcDir = join(cwd, "src");
179
+ const components = [];
180
+
181
+ if (existsSync(srcDir)) {
182
+ const files = readdirSync(srcDir).filter(f => f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".ts"));
183
+ // Group by common prefix
184
+ const seen = new Set();
185
+ for (const f of files) {
186
+ const base = f.replace(/(-cmd)?\.m?[jt]s$/, "");
187
+ if (seen.has(base)) continue;
188
+ seen.add(base);
189
+
190
+ const paths = files.filter(ff => ff.startsWith(base)).map(ff => `src/${ff}`);
191
+ components.push({
192
+ id: base,
193
+ description: `TODO: describe ${base}`,
194
+ owned_paths: paths,
195
+ forbidden_paths: [],
196
+ upstream_deps: [],
197
+ downstream_consumers: [],
198
+ public_interfaces: [],
199
+ });
200
+ }
201
+ }
202
+
203
+ const manifest = {
204
+ repo: "TODO: org/repo-name",
205
+ version: "0.0.0",
206
+ timestamp: new Date().toISOString(),
207
+ language: "TODO",
208
+ runtime: "TODO",
209
+ total_source_lines: 0,
210
+ total_test_lines: 0,
211
+ external_dependencies: 0,
212
+ components,
213
+ boundaries: [],
214
+ };
215
+
216
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
217
+ console.log(`\nGenerated skeleton ${MANIFEST_FILE} with ${components.length} components.`);
218
+ console.log("Edit the manifest to:");
219
+ console.log(" 1. Fill in repo, version, language, runtime");
220
+ console.log(" 2. Write real descriptions for each component");
221
+ console.log(" 3. Define boundaries between components");
222
+ console.log(" 4. Set upstream_deps and downstream_consumers");
223
+ console.log(`\nThen run 'roleos audit' to start the audit.\n`);
224
+ }
225
+
226
+ // ── roleos audit status ──────────────────────────────────────────────────────
227
+
228
+ function cmdStatus() {
229
+ const cwd = process.cwd();
230
+
231
+ // Find the most recent deep-audit run
232
+ const runs = listRuns(cwd);
233
+ const auditRuns = runs.filter(r =>
234
+ r.task.toLowerCase().includes("audit") ||
235
+ r.level === "mission"
236
+ );
237
+
238
+ if (auditRuns.length === 0) {
239
+ console.log("\nNo audit runs found. Start one with: roleos audit\n");
240
+ return;
241
+ }
242
+
243
+ // Show the most recent
244
+ const latest = auditRuns[0];
245
+ console.log(`\nLatest Audit Run`);
246
+ console.log(`────────────────`);
247
+ console.log(`ID: ${latest.id}`);
248
+ console.log(`Task: ${latest.task}`);
249
+ console.log(`Status: ${latest.status.toUpperCase()}`);
250
+ console.log(`Created: ${latest.createdAt}`);
251
+
252
+ const full = loadRun(cwd, latest.id);
253
+ if (full) {
254
+ const pos = getPosition(full);
255
+ console.log(`Progress: ${pos.progress}`);
256
+
257
+ const byStatus = {};
258
+ for (const s of full.steps) {
259
+ byStatus[s.status] = (byStatus[s.status] || 0) + 1;
260
+ }
261
+ console.log(`\nSteps:`);
262
+ for (const [status, count] of Object.entries(byStatus)) {
263
+ const icon = status === "completed" ? "[x]" :
264
+ status === "active" ? "[>]" :
265
+ status === "failed" ? "[!]" :
266
+ status === "blocked" ? "[-]" : "[ ]";
267
+ console.log(` ${icon} ${status}: ${count}`);
268
+ }
269
+ }
270
+
271
+ console.log(`\nRun 'roleos explain ${latest.id}' for full detail.\n`);
272
+ }
273
+
274
+ // ── roleos audit verify ──────────────────────────────────────────────────────
275
+
276
+ function cmdVerify() {
277
+ const cwd = process.cwd();
278
+ const manifestPath = join(cwd, MANIFEST_FILE);
279
+
280
+ if (!existsSync(manifestPath)) {
281
+ console.log("\nNo audit-manifest.json found. Nothing to verify.\n");
282
+ process.exit(1);
283
+ }
284
+
285
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
286
+ const issues = validateManifest(manifest);
287
+
288
+ console.log(`\nAudit Verification`);
289
+ console.log(`──────────────────`);
290
+
291
+ // 1. Manifest valid
292
+ if (issues.length === 0) {
293
+ console.log(` [PASS] Manifest is valid`);
294
+ } else {
295
+ console.log(` [FAIL] Manifest has ${issues.length} issue(s)`);
296
+ for (const i of issues) console.log(` - ${i}`);
297
+ }
298
+
299
+ // 2. Owned paths exist
300
+ let pathsOk = 0;
301
+ let pathsMissing = 0;
302
+ for (const c of manifest.components || []) {
303
+ for (const p of c.owned_paths || []) {
304
+ if (existsSync(join(cwd, p))) {
305
+ pathsOk++;
306
+ } else {
307
+ pathsMissing++;
308
+ if (pathsMissing <= 5) {
309
+ console.log(` [WARN] Missing path: ${p} (${c.id})`);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ if (pathsMissing === 0) {
315
+ console.log(` [PASS] All ${pathsOk} owned paths exist`);
316
+ } else {
317
+ console.log(` [WARN] ${pathsMissing} owned path(s) missing (${pathsOk} ok)`);
318
+ }
319
+
320
+ // 3. Check for audit output files
321
+ const auditFiles = ["AUDIT-SUMMARY.md", "AUDIT-ACTION-PLAN.md", "AUDIT-CRITIC-VERDICT.md"];
322
+ const existing = auditFiles.filter(f => existsSync(join(cwd, f)));
323
+ if (existing.length === auditFiles.length) {
324
+ console.log(` [PASS] All audit output files present (${existing.length}/${auditFiles.length})`);
325
+ } else if (existing.length > 0) {
326
+ console.log(` [WARN] Partial audit outputs (${existing.length}/${auditFiles.length})`);
327
+ const missing = auditFiles.filter(f => !existsSync(join(cwd, f)));
328
+ for (const f of missing) console.log(` missing: ${f}`);
329
+ } else {
330
+ console.log(` [INFO] No audit outputs yet — run 'roleos audit' first`);
331
+ }
332
+
333
+ // 4. Check parcel reports
334
+ const parcelFiles = readdirSync(cwd).filter(f => f.startsWith("AUDIT-PARCEL-"));
335
+ if (parcelFiles.length > 0) {
336
+ console.log(` [PASS] ${parcelFiles.length} parcel report(s) found`);
337
+ } else {
338
+ console.log(` [INFO] No parcel reports yet`);
339
+ }
340
+
341
+ const healthy = issues.length === 0 && pathsMissing === 0;
342
+ console.log(`\n${healthy ? "Audit infrastructure verified." : "Some issues found — fix before re-auditing."}\n`);
343
+ if (!healthy) process.exit(1);
344
+ }
345
+
346
+ // ── Help ─────────────────────────────────────────────────────────────────────
347
+
348
+ function cmdHelp() {
349
+ console.log(`
350
+ roleos audit — Deep Audit CLI
351
+
352
+ Usage:
353
+ roleos audit Start a deep audit on the current repo
354
+ roleos audit manifest Show the audit manifest
355
+ roleos audit manifest --generate Generate a skeleton manifest from src/
356
+ roleos audit status Show audit run progress
357
+ roleos audit verify Verify manifest and audit outputs
358
+ roleos audit help Show this help
359
+
360
+ The deep audit decomposes a repo into bounded components, dispatches one
361
+ auditor per component, inspects seams between components, checks test truth,
362
+ then synthesizes into a ranked action plan.
363
+
364
+ Workflow:
365
+ 1. roleos audit manifest --generate Create audit-manifest.json
366
+ 2. Edit the manifest Define components and boundaries
367
+ 3. roleos audit Start the audit run
368
+ 4. roleos next Step through each auditor
369
+ 5. roleos audit status Check progress
370
+ 6. roleos audit verify Verify everything landed
371
+ `);
372
+ }
373
+
374
+ // ── Manifest validation ──────────────────────────────────────────────────────
375
+
376
+ function validateManifest(manifest) {
377
+ const issues = [];
378
+
379
+ if (!manifest.components || !Array.isArray(manifest.components)) {
380
+ issues.push("components must be an array");
381
+ } else {
382
+ if (manifest.components.length === 0) {
383
+ issues.push("components array is empty — add at least one component");
384
+ }
385
+ for (let i = 0; i < manifest.components.length; i++) {
386
+ const c = manifest.components[i];
387
+ if (!c.id) issues.push(`components[${i}] missing id`);
388
+ if (!c.owned_paths || c.owned_paths.length === 0) {
389
+ issues.push(`components[${i}] (${c.id || "?"}) has no owned_paths`);
390
+ }
391
+ }
392
+ }
393
+
394
+ if (!manifest.boundaries || !Array.isArray(manifest.boundaries)) {
395
+ issues.push("boundaries must be an array");
396
+ }
397
+
398
+ return issues;
399
+ }
400
+
401
+ export { validateManifest };
@@ -139,7 +139,10 @@ export const INPUT_PARTITIONS = {
139
139
  */
140
140
  export function partitionBrief(request, roleName) {
141
141
  const partition = INPUT_PARTITIONS[roleName];
142
- if (!partition) return request; // Unknown role gets full brief (fallback)
142
+ if (!partition) {
143
+ // Unknown roles receive only the topic — minimal brief, not full access
144
+ return request.topic !== undefined ? { topic: request.topic } : {};
145
+ }
143
146
 
144
147
  const filtered = {};
145
148
  for (const field of partition.permitted) {
@@ -702,6 +705,46 @@ export function translateToAtoms(roleName, roleOutput) {
702
705
  return atoms;
703
706
  }
704
707
 
708
+ /**
709
+ * Map from specific claim_kind (atom layer) to broad statement kind (scout layer).
710
+ * This bridges the two naming conventions:
711
+ * - Scout findings use `.kind` ∈ {"claim", "opportunity", "risk", "tension", "unknown"}
712
+ * - Translated atoms use `.claim_kind` with specific types per role
713
+ */
714
+ export const CLAIM_KIND_TO_STATEMENT_KIND = {
715
+ // Context Analyst → claim
716
+ definition: "claim", category: "claim", lineage: "claim", boundary: "claim",
717
+ // User Value Analyst → opportunity or claim
718
+ need: "opportunity", desire: "opportunity", friction: "risk", willingness: "claim",
719
+ // Mechanics Analyst → claim or risk
720
+ loop: "claim", dependency: "claim", failure_mode: "risk", mechanism: "claim",
721
+ // Positioning Analyst → opportunity
722
+ substitute: "tension", wedge: "opportunity", category_frame: "claim",
723
+ positioning: "claim", timing: "opportunity",
724
+ // Contrarian Analyst → tension
725
+ challenge: "tension", contradiction: "tension", overstatement: "tension", gap: "risk",
726
+ // Normalizer
727
+ adjacency: "claim", avoidance: "risk", constraint: "claim",
728
+ };
729
+
730
+ /**
731
+ * Normalize an atom to include both `.claim_kind` (specific) and `.kind` (broad).
732
+ * Ensures atoms are compatible with both the scout validation layer and
733
+ * the cross-examination layer.
734
+ *
735
+ * @param {object} atom - Atom with claim_kind
736
+ * @returns {object} Atom with both claim_kind and kind fields
737
+ */
738
+ export function normalizeAtomKind(atom) {
739
+ if (atom.kind && !atom.claim_kind) {
740
+ return { ...atom, claim_kind: atom.kind };
741
+ }
742
+ if (atom.claim_kind && !atom.kind) {
743
+ return { ...atom, kind: CLAIM_KIND_TO_STATEMENT_KIND[atom.claim_kind] || "unknown" };
744
+ }
745
+ return atom;
746
+ }
747
+
705
748
  /**
706
749
  * Validate that a translated atom preserves all required provenance fields.
707
750
  *
package/src/composite.mjs CHANGED
@@ -308,14 +308,20 @@ export function failChild(exec, category, reason) {
308
308
 
309
309
  /**
310
310
  * Find all categories that transitively depend on a given category.
311
+ * Uses a visited set to guard against circular dependency graphs.
311
312
  */
312
313
  function findUnreachable(exec, failedCategory) {
313
314
  const unreachable = new Set();
315
+ const visited = new Set();
314
316
  let changed = true;
315
317
  while (changed) {
316
318
  changed = false;
317
319
  for (const child of exec.children) {
318
320
  if (unreachable.has(child.category)) continue;
321
+ if (visited.has(child.category) && !child.dependsOn.includes(failedCategory) && !child.dependsOn.some(d => unreachable.has(d))) {
322
+ continue; // already evaluated without being unreachable — skip
323
+ }
324
+ visited.add(child.category);
319
325
  if (child.dependsOn.includes(failedCategory) || child.dependsOn.some(d => unreachable.has(d))) {
320
326
  unreachable.add(child.category);
321
327
  changed = true;
@@ -325,6 +331,41 @@ function findUnreachable(exec, failedCategory) {
325
331
  return [...unreachable];
326
332
  }
327
333
 
334
+ /**
335
+ * Check for circular dependencies in the execution plan.
336
+ * Returns an array of categories involved in cycles (empty if none).
337
+ *
338
+ * @param {CompositeExecution} exec
339
+ * @returns {string[]}
340
+ */
341
+ export function detectCycles(exec) {
342
+ const resolved = new Set();
343
+ const visiting = new Set();
344
+ const cycles = [];
345
+
346
+ function visit(category) {
347
+ if (resolved.has(category)) return;
348
+ if (visiting.has(category)) {
349
+ cycles.push(category);
350
+ return;
351
+ }
352
+ visiting.add(category);
353
+ const child = exec.children.find(c => c.category === category);
354
+ if (child) {
355
+ for (const dep of child.dependsOn) {
356
+ visit(dep);
357
+ }
358
+ }
359
+ visiting.delete(category);
360
+ resolved.add(category);
361
+ }
362
+
363
+ for (const child of exec.children) {
364
+ visit(child.category);
365
+ }
366
+ return cycles;
367
+ }
368
+
328
369
  // ── Invalidation ──────────────────────────────────────────────────────────────
329
370
 
330
371
  /**
package/src/dispatch.mjs CHANGED
@@ -16,85 +16,7 @@
16
16
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
17
17
  import { join, resolve } from "node:path";
18
18
  import { resolveBlocked, resolveRejected } from "./escalation.mjs";
19
-
20
- // ── Tool profiles per role ────────────────────────────────────────────────────
21
- // What tools each role is allowed to use. Sandboxing by contract.
22
-
23
- const TOOL_PROFILES = {
24
- // Core
25
- "Orchestrator": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
26
- "Product Strategist": ["Read", "Glob", "Grep", "Write"],
27
- "Critic Reviewer": ["Read", "Glob", "Grep", "Bash"],
28
-
29
- // Design
30
- "UI Designer": ["Read", "Glob", "Grep", "Write", "Edit"],
31
- "Brand Guardian": ["Read", "Glob", "Grep"],
32
-
33
- // Engineering
34
- "Backend Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
35
- "Frontend Developer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
36
- "Test Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
37
- "Performance Engineer": ["Read", "Glob", "Grep", "Bash"],
38
- "Refactor Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
39
- "Security Reviewer": ["Read", "Glob", "Grep", "Bash"],
40
- "Dependency Auditor": ["Read", "Glob", "Grep", "Bash"],
41
-
42
- // Treatment
43
- "Repo Researcher": ["Read", "Glob", "Grep", "Bash"],
44
- "Repo Translator": ["Read", "Glob", "Grep", "Write", "Edit"],
45
- "Docs Architect": ["Read", "Glob", "Grep", "Write", "Edit"],
46
- "Metadata Curator": ["Read", "Glob", "Grep", "Write", "Edit"],
47
- "Coverage Auditor": ["Read", "Glob", "Grep", "Bash"],
48
- "Deployment Verifier": ["Read", "Glob", "Grep", "Bash"],
49
- "Release Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
50
-
51
- // Growth / Marketing
52
- "Launch Strategist": ["Read", "Glob", "Grep", "Write"],
53
- "Content Strategist": ["Read", "Glob", "Grep", "Write"],
54
- "Community Manager": ["Read", "Glob", "Grep", "Write"],
55
- "Support Triage Lead": ["Read", "Glob", "Grep", "Write"],
56
- "Launch Copywriter": ["Read", "Glob", "Grep", "Write", "Edit"],
57
-
58
- // Product
59
- "Feedback Synthesizer": ["Read", "Glob", "Grep"],
60
- "Roadmap Prioritizer": ["Read", "Glob", "Grep", "Write"],
61
- "Spec Writer": ["Read", "Glob", "Grep", "Write", "Edit"],
62
-
63
- // Research
64
- "UX Researcher": ["Read", "Glob", "Grep"],
65
- "Competitive Analyst": ["Read", "Glob", "Grep"],
66
- "Trend Researcher": ["Read", "Glob", "Grep"],
67
- "User Interview Synthesizer": ["Read", "Glob", "Grep"],
68
-
69
- // Brainstorm
70
- "Context Scout": ["Read", "Glob", "Grep"],
71
- "User Value Scout": ["Read", "Glob", "Grep"],
72
- "Creative Leap Scout": ["Read", "Glob", "Grep"],
73
- "Normalizer": ["Read", "Glob", "Grep"],
74
- "Synthesizer": ["Read", "Glob", "Grep", "Write"],
75
- "Product Expander": ["Read", "Glob", "Grep", "Write"],
76
- "Judge": ["Read", "Glob", "Grep"],
77
- "Mechanics Scout": ["Read", "Glob", "Grep"],
78
- "Market Scout": ["Read", "Glob", "Grep"],
79
- "Contrarian Scout": ["Read", "Glob", "Grep"],
80
- "Feasibility Scout": ["Read", "Glob", "Grep"],
81
- "Quality Bar Scout": ["Read", "Glob", "Grep"],
82
- "Scenario Expander": ["Read", "Glob", "Grep", "Write"],
83
- "Moat Expander": ["Read", "Glob", "Grep", "Write"],
84
-
85
- // Brainstorm v0.3 analysts
86
- "Context Analyst": ["Read", "Glob", "Grep"],
87
- "User Value Analyst": ["Read", "Glob", "Grep"],
88
- "Mechanics Analyst": ["Read", "Glob", "Grep"],
89
- "Positioning Analyst": ["Read", "Glob", "Grep"],
90
- "Contrarian Analyst": ["Read", "Glob", "Grep"],
91
-
92
- // Deep Audit
93
- "Component Auditor": ["Read", "Glob", "Grep"],
94
- "Seam Auditor": ["Read", "Glob", "Grep"],
95
- "Test Truth Auditor": ["Read", "Glob", "Grep"],
96
- "Audit Synthesizer": ["Read", "Glob", "Grep", "Write"],
97
- };
19
+ import { TOOL_PROFILES } from "./tool-profiles.mjs";
98
20
 
99
21
  // ── Default role config ─────────────────────────────────────────────────────
100
22
 
package/src/hooks.mjs CHANGED
@@ -322,7 +322,7 @@ function generateSessionStartScript() {
322
322
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
323
323
  import { join } from "node:path";
324
324
 
325
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
325
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
326
326
  const cwd = input.cwd || process.cwd();
327
327
  const stateDir = join(cwd, ".claude", "hooks");
328
328
  mkdirSync(stateDir, { recursive: true });
@@ -356,7 +356,7 @@ function generatePromptSubmitScript() {
356
356
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
357
357
  import { join } from "node:path";
358
358
 
359
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
359
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
360
360
  const cwd = input.cwd || process.cwd();
361
361
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
362
362
 
@@ -389,7 +389,7 @@ function generatePreToolUseScript() {
389
389
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
390
390
  import { join } from "node:path";
391
391
 
392
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
392
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
393
393
  const cwd = input.cwd || process.cwd();
394
394
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
395
395
 
@@ -421,7 +421,7 @@ function generateSubagentStartScript() {
421
421
  import { readFileSync, existsSync } from "node:fs";
422
422
  import { join } from "node:path";
423
423
 
424
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
424
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
425
425
  const cwd = input.cwd || process.cwd();
426
426
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
427
427
 
@@ -444,7 +444,7 @@ function generateStopScript() {
444
444
  import { readFileSync, existsSync } from "node:fs";
445
445
  import { join } from "node:path";
446
446
 
447
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
447
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
448
448
  const cwd = input.cwd || process.cwd();
449
449
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
450
450
 
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { MISSIONS, getMission, validateMission } from "./mission.mjs";
13
13
  import { validateArtifact } from "./artifacts.mjs";
14
+ import { STEP_TRANSITIONS, isValidStepTransition } from "./state-machine.mjs";
14
15
 
15
16
  let _runCounter = 0;
16
17
 
@@ -198,6 +199,10 @@ function buildDynamicSteps(mission, manifest) {
198
199
  * @returns {MissionStep|null} The started step, or null if no pending steps
199
200
  */
200
201
  export function startNextStep(run) {
202
+ // Guard: refuse to activate a new step if one is already active (prevents dual-active)
203
+ const alreadyActive = run.steps.find((s) => s.status === "active");
204
+ if (alreadyActive) return null;
205
+
201
206
  const next = run.steps.find((s) => s.status === "pending");
202
207
  if (!next) return null;
203
208
 
package/src/run.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
  * Interventions: reroute, split, escalate, retry, block, reopen
13
13
  */
14
14
 
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "node:fs";
15
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { decideEntry } from "./entry.mjs";
18
18
  import { getMission } from "./mission.mjs";
@@ -285,6 +285,10 @@ function guessArtifact(roleName) {
285
285
  * @returns {RunStep|null}
286
286
  */
287
287
  export function startNext(run, cwd) {
288
+ // Guard: refuse to activate a new step if one is already active (prevents dual-active)
289
+ const alreadyActive = run.steps.find(s => s.status === "active");
290
+ if (alreadyActive) return null;
291
+
288
292
  const next = run.steps.find(s => s.status === "pending");
289
293
  if (!next) return null;
290
294
 
@@ -854,7 +858,10 @@ export function formatReport(report) {
854
858
  export function saveRun(cwd, run) {
855
859
  const dir = runsDir(cwd);
856
860
  mkdirSync(dir, { recursive: true });
857
- writeFileSync(runPath(cwd, run.id), JSON.stringify(run, null, 2));
861
+ const target = runPath(cwd, run.id);
862
+ const tmp = target + ".tmp";
863
+ writeFileSync(tmp, JSON.stringify(run, null, 2));
864
+ renameSync(tmp, target);
858
865
  }
859
866
 
860
867
  /**
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Shared State Machine — valid transitions for run steps.
3
+ *
4
+ * Both run.mjs (persistent runner) and mission-run.mjs (mission runner)
5
+ * use the same step lifecycle. This module defines the canonical transitions
6
+ * so they cannot diverge.
7
+ */
8
+
9
+ /**
10
+ * Valid step status transitions.
11
+ * Key: current status. Value: array of allowed next statuses.
12
+ */
13
+ export const STEP_TRANSITIONS = {
14
+ pending: ["active"],
15
+ active: ["completed", "partial", "failed", "blocked"],
16
+ completed: ["pending"], // re-opened by escalation
17
+ partial: ["pending"], // retried
18
+ failed: ["pending"], // retried
19
+ blocked: ["pending"], // unblocked
20
+ skipped: [], // terminal
21
+ };
22
+
23
+ /**
24
+ * Valid run status transitions.
25
+ */
26
+ export const RUN_TRANSITIONS = {
27
+ planning: ["running"],
28
+ running: ["paused", "completed", "partial", "failed"],
29
+ paused: ["running"],
30
+ completed: ["paused"], // re-opened by escalation/reopen
31
+ partial: ["paused"], // retried
32
+ failed: ["paused"], // retried
33
+ };
34
+
35
+ /**
36
+ * Check if a step transition is valid.
37
+ * @param {string} from - Current status
38
+ * @param {string} to - Desired status
39
+ * @returns {boolean}
40
+ */
41
+ export function isValidStepTransition(from, to) {
42
+ const allowed = STEP_TRANSITIONS[from];
43
+ if (!allowed) return false;
44
+ return allowed.includes(to);
45
+ }
46
+
47
+ /**
48
+ * Check if a run transition is valid.
49
+ * @param {string} from - Current status
50
+ * @param {string} to - Desired status
51
+ * @returns {boolean}
52
+ */
53
+ export function isValidRunTransition(from, to) {
54
+ const allowed = RUN_TRANSITIONS[from];
55
+ if (!allowed) return false;
56
+ return allowed.includes(to);
57
+ }
58
+
59
+ /**
60
+ * Assert a step transition is valid, or throw.
61
+ * @param {string} from
62
+ * @param {string} to
63
+ * @param {string} [context] - Description for error message
64
+ */
65
+ export function assertStepTransition(from, to, context = "") {
66
+ if (!isValidStepTransition(from, to)) {
67
+ const prefix = context ? `${context}: ` : "";
68
+ throw new Error(`${prefix}Invalid step transition "${from}" → "${to}"`);
69
+ }
70
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Tool Profiles — per-role tool sandboxing.
3
+ *
4
+ * Extracted to a shared module so that both dispatch.mjs and trial.mjs
5
+ * can import it without creating a circular dependency.
6
+ */
7
+
8
+ export const TOOL_PROFILES = {
9
+ // Core
10
+ "Orchestrator": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
11
+ "Product Strategist": ["Read", "Glob", "Grep", "Write"],
12
+ "Critic Reviewer": ["Read", "Glob", "Grep", "Bash"],
13
+
14
+ // Design
15
+ "UI Designer": ["Read", "Glob", "Grep", "Write", "Edit"],
16
+ "Brand Guardian": ["Read", "Glob", "Grep"],
17
+
18
+ // Engineering
19
+ "Backend Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
20
+ "Frontend Developer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
21
+ "Test Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
22
+ "Performance Engineer": ["Read", "Glob", "Grep", "Bash"],
23
+ "Refactor Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
24
+ "Security Reviewer": ["Read", "Glob", "Grep", "Bash"],
25
+ "Dependency Auditor": ["Read", "Glob", "Grep", "Bash"],
26
+
27
+ // Treatment
28
+ "Repo Researcher": ["Read", "Glob", "Grep", "Bash"],
29
+ "Repo Translator": ["Read", "Glob", "Grep", "Write", "Edit"],
30
+ "Docs Architect": ["Read", "Glob", "Grep", "Write", "Edit"],
31
+ "Metadata Curator": ["Read", "Glob", "Grep", "Write", "Edit"],
32
+ "Coverage Auditor": ["Read", "Glob", "Grep", "Bash"],
33
+ "Deployment Verifier": ["Read", "Glob", "Grep", "Bash"],
34
+ "Release Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
35
+
36
+ // Growth / Marketing
37
+ "Launch Strategist": ["Read", "Glob", "Grep", "Write"],
38
+ "Content Strategist": ["Read", "Glob", "Grep", "Write"],
39
+ "Community Manager": ["Read", "Glob", "Grep", "Write"],
40
+ "Support Triage Lead": ["Read", "Glob", "Grep", "Write"],
41
+ "Launch Copywriter": ["Read", "Glob", "Grep", "Write", "Edit"],
42
+
43
+ // Product
44
+ "Feedback Synthesizer": ["Read", "Glob", "Grep"],
45
+ "Roadmap Prioritizer": ["Read", "Glob", "Grep", "Write"],
46
+ "Spec Writer": ["Read", "Glob", "Grep", "Write", "Edit"],
47
+
48
+ // Research
49
+ "UX Researcher": ["Read", "Glob", "Grep"],
50
+ "Competitive Analyst": ["Read", "Glob", "Grep"],
51
+ "Trend Researcher": ["Read", "Glob", "Grep"],
52
+ "User Interview Synthesizer": ["Read", "Glob", "Grep"],
53
+
54
+ // Brainstorm
55
+ "Context Scout": ["Read", "Glob", "Grep"],
56
+ "User Value Scout": ["Read", "Glob", "Grep"],
57
+ "Creative Leap Scout": ["Read", "Glob", "Grep"],
58
+ "Normalizer": ["Read", "Glob", "Grep"],
59
+ "Synthesizer": ["Read", "Glob", "Grep", "Write"],
60
+ "Product Expander": ["Read", "Glob", "Grep", "Write"],
61
+ "Judge": ["Read", "Glob", "Grep"],
62
+ "Mechanics Scout": ["Read", "Glob", "Grep"],
63
+ "Market Scout": ["Read", "Glob", "Grep"],
64
+ "Contrarian Scout": ["Read", "Glob", "Grep"],
65
+ "Feasibility Scout": ["Read", "Glob", "Grep"],
66
+ "Quality Bar Scout": ["Read", "Glob", "Grep"],
67
+ "Scenario Expander": ["Read", "Glob", "Grep", "Write"],
68
+ "Moat Expander": ["Read", "Glob", "Grep", "Write"],
69
+
70
+ // Brainstorm v0.3 analysts
71
+ "Context Analyst": ["Read", "Glob", "Grep"],
72
+ "User Value Analyst": ["Read", "Glob", "Grep"],
73
+ "Mechanics Analyst": ["Read", "Glob", "Grep"],
74
+ "Positioning Analyst": ["Read", "Glob", "Grep"],
75
+ "Contrarian Analyst": ["Read", "Glob", "Grep"],
76
+
77
+ // Deep Audit
78
+ "Component Auditor": ["Read", "Glob", "Grep"],
79
+ "Seam Auditor": ["Read", "Glob", "Grep"],
80
+ "Test Truth Auditor": ["Read", "Glob", "Grep"],
81
+ "Audit Synthesizer": ["Read", "Glob", "Grep", "Write"],
82
+ };
package/src/trial.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { ROLE_CATALOG, scoreRole, MIN_SCORE_THRESHOLD } from "./route.mjs";
13
- import { TOOL_PROFILES } from "./dispatch.mjs";
13
+ import { TOOL_PROFILES } from "./tool-profiles.mjs";
14
14
  import { getRequirements } from "./evidence.mjs";
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
16
16
  import { join, resolve } from "node:path";