cclaw-cli 0.48.1 → 0.48.3

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.
Files changed (41) hide show
  1. package/README.md +10 -3
  2. package/dist/artifact-linter.js +2 -8
  3. package/dist/cli.js +8 -1
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +13 -3
  6. package/dist/constants.d.ts +6 -0
  7. package/dist/constants.js +11 -0
  8. package/dist/content/contracts.d.ts +2 -2
  9. package/dist/content/contracts.js +2 -2
  10. package/dist/content/core-agents.d.ts +1 -1
  11. package/dist/content/core-agents.js +1 -1
  12. package/dist/content/hooks.js +16 -15
  13. package/dist/content/next-command.js +4 -2
  14. package/dist/content/observe.d.ts +2 -2
  15. package/dist/content/observe.js +83 -13
  16. package/dist/content/opencode-plugin.js +227 -45
  17. package/dist/content/stage-schema.js +1 -1
  18. package/dist/content/stages/ship.js +2 -5
  19. package/dist/content/templates.js +3 -6
  20. package/dist/delegation.d.ts +5 -1
  21. package/dist/delegation.js +12 -8
  22. package/dist/doctor.js +132 -15
  23. package/dist/eval/runner.js +36 -4
  24. package/dist/feature-system.d.ts +11 -4
  25. package/dist/feature-system.js +54 -10
  26. package/dist/flow-state.d.ts +2 -0
  27. package/dist/flow-state.js +19 -2
  28. package/dist/fs-utils.d.ts +4 -1
  29. package/dist/fs-utils.js +20 -4
  30. package/dist/gate-evidence.d.ts +2 -0
  31. package/dist/gate-evidence.js +13 -4
  32. package/dist/install.js +25 -23
  33. package/dist/internal/advance-stage.js +49 -10
  34. package/dist/knowledge-store.d.ts +8 -0
  35. package/dist/knowledge-store.js +113 -33
  36. package/dist/retro-gate.js +33 -23
  37. package/dist/run-archive.js +166 -128
  38. package/dist/run-persistence.d.ts +8 -1
  39. package/dist/run-persistence.js +7 -6
  40. package/dist/trace-matrix.js +7 -7
  41. package/package.json +1 -1
@@ -2,7 +2,8 @@ import { RUNTIME_ROOT } from "../constants.js";
2
2
  import { META_SKILL_NAME } from "./meta-skill.js";
3
3
  export function opencodePluginJs(_options = {}) {
4
4
  return `// cclaw OpenCode plugin — generated by cclaw sync
5
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync } from "node:fs";
6
+ import { readFile, stat } from "node:fs/promises";
6
7
  import { join } from "node:path";
7
8
 
8
9
  export default function cclawPlugin(ctx) {
@@ -33,9 +34,9 @@ export default function cclawPlugin(ctx) {
33
34
  }
34
35
  }
35
36
 
36
- function readFlowState() {
37
+ async function readFlowState() {
37
38
  try {
38
- const raw = readFileSync(flowStatePath, "utf8");
39
+ const raw = await readFile(flowStatePath, "utf8");
39
40
  const state = JSON.parse(raw);
40
41
  return {
41
42
  stage: typeof state.currentStage === "string" ? state.currentStage : "none",
@@ -47,23 +48,23 @@ export default function cclawPlugin(ctx) {
47
48
  }
48
49
  }
49
50
 
50
- function readFileText(filePath) {
51
+ async function readFileText(filePath) {
51
52
  try {
52
- return readFileSync(filePath, "utf8");
53
+ return await readFile(filePath, "utf8");
53
54
  } catch {
54
55
  return "";
55
56
  }
56
57
  }
57
58
 
58
- function readTailLines(filePath, maxLines) {
59
- const text = readFileText(filePath).trim();
59
+ async function readTailLines(filePath, maxLines) {
60
+ const text = (await readFileText(filePath)).trim();
60
61
  if (!text) return [];
61
62
  return text.split(/\\r?\\n/).slice(-maxLines);
62
63
  }
63
64
 
64
- function readCheckpointSummary() {
65
+ async function readCheckpointSummary() {
65
66
  try {
66
- const raw = readFileText(checkpointPath);
67
+ const raw = await readFileText(checkpointPath);
67
68
  if (!raw) return "";
68
69
  const cp = JSON.parse(raw);
69
70
  return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
@@ -72,10 +73,10 @@ export default function cclawPlugin(ctx) {
72
73
  }
73
74
  }
74
75
 
75
- function readContextMode() {
76
+ async function readContextMode() {
76
77
  let mode = "default";
77
78
  try {
78
- const parsed = JSON.parse(readFileText(contextModePath));
79
+ const parsed = JSON.parse(await readFileText(contextModePath));
79
80
  if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
80
81
  mode = parsed.activeMode.trim();
81
82
  }
@@ -87,9 +88,9 @@ export default function cclawPlugin(ctx) {
87
88
  return { mode, guide };
88
89
  }
89
90
 
90
- function readRecentActivity() {
91
+ async function readRecentActivity() {
91
92
  try {
92
- const lines = readTailLines(activityPath, 5);
93
+ const lines = await readTailLines(activityPath, 5);
93
94
  if (lines.length === 0) return [];
94
95
  return lines
95
96
  .map((line) => {
@@ -106,9 +107,9 @@ export default function cclawPlugin(ctx) {
106
107
  }
107
108
  }
108
109
 
109
- function readLatestContextWarning() {
110
+ async function readLatestContextWarning() {
110
111
  try {
111
- const line = readTailLines(contextWarningsPath, 1)[0];
112
+ const line = (await readTailLines(contextWarningsPath, 1))[0];
112
113
  if (!line) return "";
113
114
  try {
114
115
  const parsed = JSON.parse(line);
@@ -122,8 +123,8 @@ export default function cclawPlugin(ctx) {
122
123
  }
123
124
  }
124
125
 
125
- function readKnowledgeDigest() {
126
- const digest = readFileText(knowledgeDigestPath).trim();
126
+ async function readKnowledgeDigest() {
127
+ const digest = (await readFileText(knowledgeDigestPath)).trim();
127
128
  if (!digest) {
128
129
  return readTailLines(knowledgePath, 12);
129
130
  }
@@ -134,68 +135,219 @@ export default function cclawPlugin(ctx) {
134
135
  .filter((line) => !line.startsWith("#"));
135
136
  }
136
137
 
137
- function buildBootstrap() {
138
- const flow = readFlowState();
138
+ const BOOTSTRAP_MARKER = "<!-- cclaw-bootstrap-v1 -->";
139
+
140
+ async function buildBootstrap() {
141
+ const flow = await readFlowState();
139
142
  const parts = [
143
+ BOOTSTRAP_MARKER,
140
144
  \`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
141
145
  ];
142
- const contextMode = readContextMode();
146
+ const contextMode = await readContextMode();
143
147
  parts.push(
144
148
  contextMode.guide
145
149
  ? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
146
150
  : \`Context mode: \${contextMode.mode}\`
147
151
  );
148
152
 
149
- const checkpoint = readCheckpointSummary();
153
+ const checkpoint = await readCheckpointSummary();
150
154
  if (checkpoint) parts.push(checkpoint);
151
155
 
152
- const digest = readFileText(sessionDigestPath).trim();
156
+ const digest = (await readFileText(sessionDigestPath)).trim();
153
157
  if (digest) parts.push("Last session:", digest);
154
158
 
155
- const activity = readRecentActivity();
159
+ const activity = await readRecentActivity();
156
160
  if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
157
161
 
158
- const warning = readLatestContextWarning();
162
+ const warning = await readLatestContextWarning();
159
163
  if (warning) parts.push("Latest context warning:", warning);
160
164
 
161
- const knowledge = readKnowledgeDigest();
165
+ const knowledge = await readKnowledgeDigest();
162
166
  if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
163
167
 
164
168
  parts.push(
165
169
  "If you discover a non-obvious rule or pattern, append one strict-schema JSON line to .cclaw/knowledge.jsonl using type: rule, pattern, lesson, or compound."
166
170
  );
167
171
 
168
- const meta = readFileText(metaSkillPath).trim();
172
+ const meta = (await readFileText(metaSkillPath)).trim();
169
173
  if (meta) parts.push("", meta);
170
174
  return parts.join("\\n");
171
175
  }
172
176
 
173
177
  let bootstrapCache = "";
178
+ let bootstrapMtimes = new Map();
179
+ let bootstrapRefreshPromise = null;
180
+ const BOOTSTRAP_SOURCE_PATHS = [
181
+ flowStatePath,
182
+ checkpointPath,
183
+ activityPath,
184
+ contextWarningsPath,
185
+ contextModePath,
186
+ sessionDigestPath,
187
+ knowledgePath,
188
+ knowledgeDigestPath,
189
+ metaSkillPath
190
+ ];
191
+
192
+ async function readMtimeMs(filePath) {
193
+ try {
194
+ const st = await stat(filePath);
195
+ return Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
196
+ } catch {
197
+ return 0;
198
+ }
199
+ }
200
+
201
+ async function snapshotBootstrapMtimes() {
202
+ const next = new Map();
203
+ for (const filePath of BOOTSTRAP_SOURCE_PATHS) {
204
+ next.set(filePath, await readMtimeMs(filePath));
205
+ }
206
+ return next;
207
+ }
208
+
209
+ async function bootstrapNeedsRefresh() {
210
+ if (!bootstrapCache) return true;
211
+ for (const filePath of BOOTSTRAP_SOURCE_PATHS) {
212
+ const prev = bootstrapMtimes.get(filePath) ?? 0;
213
+ const now = await readMtimeMs(filePath);
214
+ if (prev !== now) return true;
215
+ }
216
+ return false;
217
+ }
174
218
 
175
- function refreshBootstrapCache() {
176
- bootstrapCache = buildBootstrap();
219
+ async function refreshBootstrapCache(force = false) {
220
+ if (!force && !(await bootstrapNeedsRefresh())) {
221
+ return bootstrapCache;
222
+ }
223
+ if (bootstrapRefreshPromise) {
224
+ return bootstrapRefreshPromise;
225
+ }
226
+ bootstrapRefreshPromise = (async () => {
227
+ const nextBootstrap = await buildBootstrap();
228
+ const nextMtimes = await snapshotBootstrapMtimes();
229
+ bootstrapCache = nextBootstrap;
230
+ bootstrapMtimes = nextMtimes;
231
+ return bootstrapCache;
232
+ })();
233
+ try {
234
+ return await bootstrapRefreshPromise;
235
+ } finally {
236
+ bootstrapRefreshPromise = null;
237
+ }
177
238
  }
178
239
 
179
240
  function getBootstrap() {
180
- if (!bootstrapCache) refreshBootstrapCache();
181
241
  return bootstrapCache;
182
242
  }
183
243
 
244
+ const MAX_CONCURRENT_HOOKS = 2;
245
+ let runningHookTasks = 0;
246
+ const pendingHookTasks = [];
247
+
248
+ function runNextHookTask() {
249
+ if (runningHookTasks >= MAX_CONCURRENT_HOOKS) return;
250
+ const queued = pendingHookTasks.shift();
251
+ if (!queued) return;
252
+ runningHookTasks += 1;
253
+ queued()
254
+ .catch(() => false)
255
+ .finally(() => {
256
+ runningHookTasks -= 1;
257
+ runNextHookTask();
258
+ });
259
+ }
260
+
261
+ function scheduleHookTask(task) {
262
+ return new Promise((resolve) => {
263
+ const wrapped = async () => {
264
+ try {
265
+ resolve(await task());
266
+ } catch {
267
+ resolve(false);
268
+ }
269
+ };
270
+ pendingHookTasks.push(wrapped);
271
+ runNextHookTask();
272
+ });
273
+ }
274
+
184
275
  async function runHookScript(scriptFileName, payload = {}) {
185
- const { spawnSync } = await import("node:child_process");
276
+ const { spawn } = await import("node:child_process");
186
277
  const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
187
278
  const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
188
- try {
189
- const result = spawnSync("bash", [scriptPath], {
190
- cwd: root,
191
- timeout: 20000,
192
- stdio: ["pipe", "ignore", "ignore"],
193
- input
279
+ return scheduleHookTask(() => new Promise((resolve) => {
280
+ let stderr = "";
281
+ let settled = false;
282
+ const finish = (ok) => {
283
+ if (settled) return;
284
+ settled = true;
285
+ resolve(ok);
286
+ };
287
+
288
+ let child;
289
+ try {
290
+ child = spawn("bash", [scriptPath], {
291
+ cwd: root,
292
+ stdio: ["pipe", "ignore", "pipe"]
293
+ });
294
+ } catch {
295
+ finish(false);
296
+ return;
297
+ }
298
+
299
+ const timer = setTimeout(() => {
300
+ child.kill("SIGKILL");
301
+ if (stderr.length > 0) {
302
+ console.error("[cclaw] opencode hook timeout: " + scriptFileName + " stderr=" + stderr.slice(-1200));
303
+ }
304
+ finish(false);
305
+ }, 20_000);
306
+
307
+ child.stderr?.on("data", (chunk) => {
308
+ stderr += String(chunk ?? "");
309
+ if (stderr.length > 4000) {
310
+ stderr = stderr.slice(-4000);
311
+ }
194
312
  });
195
- return typeof result.status === "number" ? result.status === 0 : false;
196
- } catch {
197
- return false;
198
- }
313
+ child.on("error", () => {
314
+ clearTimeout(timer);
315
+ finish(false);
316
+ });
317
+ child.on("close", (code) => {
318
+ clearTimeout(timer);
319
+ const ok = code === 0;
320
+ if (!ok && stderr.length > 0) {
321
+ console.error("[cclaw] opencode hook failed: " + scriptFileName + " stderr=" + stderr.slice(-1200));
322
+ }
323
+ finish(ok);
324
+ });
325
+ if (child.stdin) {
326
+ child.stdin.on("error", (error) => {
327
+ const code =
328
+ error && typeof error === "object" && "code" in error
329
+ ? String(error.code)
330
+ : "";
331
+ if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
332
+ return;
333
+ }
334
+ clearTimeout(timer);
335
+ finish(false);
336
+ });
337
+ try {
338
+ child.stdin.end(input);
339
+ } catch (error) {
340
+ const code =
341
+ error && typeof error === "object" && "code" in error
342
+ ? String(error.code)
343
+ : "";
344
+ if (code !== "EPIPE" && code !== "ERR_STREAM_DESTROYED") {
345
+ clearTimeout(timer);
346
+ finish(false);
347
+ }
348
+ }
349
+ }
350
+ }));
199
351
  }
200
352
 
201
353
  function normalizeToolPayload(input, output) {
@@ -207,9 +359,15 @@ export default function cclawPlugin(ctx) {
207
359
  if (typeof payload === "string") return payload;
208
360
  if (payload && typeof payload === "object") {
209
361
  if (typeof payload.type === "string") return payload.type;
362
+ if (typeof payload.eventType === "string") return payload.eventType;
363
+ if (typeof payload.kind === "string") return payload.kind;
364
+ if (typeof payload.topic === "string") return payload.topic;
210
365
  if (typeof payload.name === "string") return payload.name;
211
366
  if (payload.event && typeof payload.event === "object") {
212
367
  if (typeof payload.event.type === "string") return payload.event.type;
368
+ if (typeof payload.event.eventType === "string") return payload.event.eventType;
369
+ if (typeof payload.event.kind === "string") return payload.event.kind;
370
+ if (typeof payload.event.topic === "string") return payload.event.topic;
213
371
  if (typeof payload.event.name === "string") return payload.event.name;
214
372
  }
215
373
  }
@@ -217,18 +375,40 @@ export default function cclawPlugin(ctx) {
217
375
  }
218
376
 
219
377
  function resolveEventData(payload) {
220
- if (payload && typeof payload === "object" && payload.event && typeof payload.event === "object") {
221
- return payload.event;
378
+ if (payload && typeof payload === "object") {
379
+ if (payload.event && typeof payload.event === "object") {
380
+ if (payload.event.data && typeof payload.event.data === "object") {
381
+ return payload.event.data;
382
+ }
383
+ if (payload.event.payload && typeof payload.event.payload === "object") {
384
+ return payload.event.payload;
385
+ }
386
+ return payload.event;
387
+ }
388
+ if (payload.data && typeof payload.data === "object") {
389
+ return payload.data;
390
+ }
391
+ if (payload.payload && typeof payload.payload === "object") {
392
+ return payload.payload;
393
+ }
222
394
  }
223
395
  return payload;
224
396
  }
225
397
 
226
398
  ensureRuntimeDirs();
399
+ void refreshBootstrapCache(true);
227
400
 
228
401
  return {
229
402
  event: async (payload) => {
230
403
  const eventType = resolveEventType(payload);
231
404
  const eventData = resolveEventData(payload);
405
+ if (!eventType) {
406
+ const keys =
407
+ payload && typeof payload === "object"
408
+ ? Object.keys(payload).slice(0, 10).join(", ")
409
+ : typeof payload;
410
+ console.error("[cclaw] opencode unknown event payload keys: " + keys);
411
+ }
232
412
  if (
233
413
  eventType === "session.created" ||
234
414
  eventType === "session.resumed" ||
@@ -242,7 +422,7 @@ export default function cclawPlugin(ctx) {
242
422
  // session.updated covers config reloads and artifact/rules edits
243
423
  // that happen mid-session; without it the cache would stay stale
244
424
  // until the next compaction or restart.
245
- refreshBootstrapCache();
425
+ await refreshBootstrapCache(true);
246
426
  }
247
427
  if (eventType === "session.compacted") {
248
428
  await runHookScript("pre-compact.sh", eventData ?? {});
@@ -264,14 +444,16 @@ export default function cclawPlugin(ctx) {
264
444
  "tool.execute.after": async (input, output) => {
265
445
  const payload = normalizeToolPayload(input, output);
266
446
  await runHookScript("context-monitor.sh", payload);
447
+ void refreshBootstrapCache(false);
267
448
  },
268
449
  "experimental.chat.system.transform": (payload) => {
269
450
  const bootstrap = getBootstrap();
451
+ if (!bootstrap) return payload;
270
452
  if (typeof payload === "string") {
271
- return payload.includes("cclaw loaded.") ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
453
+ return payload.includes(BOOTSTRAP_MARKER) ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
272
454
  }
273
455
  if (payload && typeof payload === "object" && typeof payload.system === "string") {
274
- if (payload.system.includes("cclaw loaded.")) return payload;
456
+ if (payload.system.includes(BOOTSTRAP_MARKER)) return payload;
275
457
  return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
276
458
  }
277
459
  return payload;
@@ -198,7 +198,7 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
198
198
  {
199
199
  agent: "doc-updater",
200
200
  mode: "proactive",
201
- when: "When public behavior, APIs, or config surfaces change.",
201
+ when: "Proactive in tdd when public behavior, APIs, or config surfaces change.",
202
202
  purpose: "Prevent code/docs drift before review and ship.",
203
203
  requiresUserGate: false
204
204
  }
@@ -1,3 +1,4 @@
1
+ import { SHIP_FINALIZATION_MODES } from "../../constants.js";
1
2
  // ---------------------------------------------------------------------------
2
3
  // SHIP — reference: superpowers finishing-a-development-branch + gstack /ship
3
4
  // ---------------------------------------------------------------------------
@@ -93,11 +94,7 @@ export const SHIP = {
93
94
  "Pre-Ship Checks",
94
95
  "Release Notes",
95
96
  "Rollback Plan",
96
- "FINALIZE_MERGE_LOCAL",
97
- "FINALIZE_OPEN_PR",
98
- "FINALIZE_KEEP_BRANCH",
99
- "FINALIZE_DISCARD_BRANCH",
100
- "FINALIZE_NO_VCS"
97
+ ...SHIP_FINALIZATION_MODES
101
98
  ],
102
99
  artifactFile: "08-ship.md",
103
100
  // `done` exits the stage pipeline. Archive semantics are handled by the
@@ -1,6 +1,7 @@
1
- import { CCLAW_VERSION } from "../constants.js";
1
+ import { CCLAW_VERSION, SHIP_FINALIZATION_MODES } from "../constants.js";
2
2
  import { orderedStageSchemas } from "./stage-schema.js";
3
3
  import { FLOW_STAGES } from "../types.js";
4
+ const SHIP_FINALIZATION_ENUM_LINES = SHIP_FINALIZATION_MODES.map((mode) => ` - ${mode}`).join("\n");
4
5
  export const ARTIFACT_TEMPLATES = {
5
6
  "01-brainstorm.md": `---
6
7
  stage: brainstorm
@@ -645,11 +646,7 @@ inputs_hash: sha256:pending
645
646
 
646
647
  ## Finalization
647
648
  - Selected enum (exactly one):
648
- - FINALIZE_MERGE_LOCAL
649
- - FINALIZE_OPEN_PR
650
- - FINALIZE_KEEP_BRANCH
651
- - FINALIZE_DISCARD_BRANCH
652
- - FINALIZE_NO_VCS
649
+ ${SHIP_FINALIZATION_ENUM_LINES}
653
650
  - Selected label (A/B/C/D/E):
654
651
  - Execution result:
655
652
  - PR URL / merge commit / kept branch / discard confirmation:
@@ -53,6 +53,8 @@ export type DelegationEntry = {
53
53
  retryCount?: number;
54
54
  /** Optional references to evidence anchors in artifacts. */
55
55
  evidenceRefs?: string[];
56
+ /** Optional skill marker used for role-specific mandatory checks. */
57
+ skill?: string;
56
58
  /**
57
59
  * Fulfillment mode this entry was executed under. Omitted on legacy rows
58
60
  * (treated as `"isolated"` for Claude, otherwise inferred from the active
@@ -85,7 +87,9 @@ export declare function appendDelegation(projectRoot: string, entry: DelegationE
85
87
  * strongest guarantee.
86
88
  */
87
89
  export declare function expectedFulfillmentMode(fallbacks: SubagentFallback[]): DelegationFulfillmentMode;
88
- export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage): Promise<{
90
+ export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage, options?: {
91
+ repairFeatureSystem?: boolean;
92
+ }): Promise<{
89
93
  satisfied: boolean;
90
94
  missing: string[];
91
95
  waived: string[];
@@ -154,6 +154,7 @@ function isDelegationEntry(value) {
154
154
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
155
155
  retryOk &&
156
156
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
157
+ (o.skill === undefined || typeof o.skill === "string") &&
157
158
  (o.schemaVersion === undefined || o.schemaVersion === 1));
158
159
  }
159
160
  function parseLedger(raw, runId) {
@@ -237,7 +238,7 @@ export async function appendDelegation(projectRoot, entry) {
237
238
  runId: activeRunId,
238
239
  entries: [...prior.entries, stamped]
239
240
  };
240
- await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`);
241
+ await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
241
242
  });
242
243
  }
243
244
  /**
@@ -257,9 +258,11 @@ export function expectedFulfillmentMode(fallbacks) {
257
258
  return "role-switch";
258
259
  return "harness-waiver";
259
260
  }
260
- export async function checkMandatoryDelegations(projectRoot, stage) {
261
+ export async function checkMandatoryDelegations(projectRoot, stage, options = {}) {
261
262
  const mandatory = stageSchema(stage).mandatoryDelegations;
262
- const { activeRunId } = await readFlowState(projectRoot);
263
+ const { activeRunId } = await readFlowState(projectRoot, {
264
+ repairFeatureSystem: options.repairFeatureSystem
265
+ });
263
266
  const ledger = await readDelegationLedger(projectRoot);
264
267
  const forStage = ledger.entries.filter((e) => e.stage === stage);
265
268
  const forRun = forStage.filter((e) => e.runId === activeRunId);
@@ -279,14 +282,15 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
279
282
  const rows = forRun.filter((e) => e.agent === agent);
280
283
  const completedRows = rows.filter((e) => e.status === "completed");
281
284
  const waivedRows = rows.filter((e) => e.status === "waived");
282
- const requiredCompletedCount = stage === "review" &&
285
+ const adversarialReviewerRequired = stage === "review" &&
283
286
  agent === "reviewer" &&
284
- reviewTriggers?.requireAdversarialReviewer
285
- ? 2
286
- : 1;
287
+ reviewTriggers?.requireAdversarialReviewer === true;
288
+ const requiredCompletedCount = adversarialReviewerRequired ? 2 : 1;
287
289
  const hasCompleted = completedRows.length >= requiredCompletedCount;
288
290
  const hasWaived = waivedRows.length > 0;
289
- const ok = hasCompleted || hasWaived;
291
+ const hasAdversarialSkill = !adversarialReviewerRequired ||
292
+ completedRows.some((row) => row.skill === "adversarial-review");
293
+ const ok = hasWaived || (hasCompleted && hasAdversarialSkill);
290
294
  if (!ok) {
291
295
  missing.push(agent);
292
296
  continue;