cclaw-cli 0.48.0 → 0.48.2

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.
@@ -1,4 +1,4 @@
1
- import { type FlowStage } from "./types.js";
1
+ import { type FlowStage, type FlowTrack } from "./types.js";
2
2
  export interface LintFinding {
3
3
  section: string;
4
4
  required: boolean;
@@ -46,7 +46,7 @@ export interface LearningsParseResult {
46
46
  details: string;
47
47
  }
48
48
  export declare function parseLearningsSection(sectionBody: string): LearningsParseResult;
49
- export declare function lintArtifact(projectRoot: string, stage: FlowStage): Promise<LintResult>;
49
+ export declare function lintArtifact(projectRoot: string, stage: FlowStage, track?: FlowTrack): Promise<LintResult>;
50
50
  export declare function validateReviewArmy(projectRoot: string): Promise<{
51
51
  valid: boolean;
52
52
  errors: string[];
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { RUNTIME_ROOT } from "./constants.js";
3
+ import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "./constants.js";
4
4
  import { exists } from "./fs-utils.js";
5
5
  import { stageSchema } from "./content/stage-schema.js";
6
6
  import { FLOW_STAGES } from "./types.js";
@@ -137,13 +137,7 @@ function tokensFromRule(rule) {
137
137
  return [...new Set(allCaps)];
138
138
  }
139
139
  if (/finalization enum token/iu.test(rule)) {
140
- return [
141
- "FINALIZE_MERGE_LOCAL",
142
- "FINALIZE_OPEN_PR",
143
- "FINALIZE_KEEP_BRANCH",
144
- "FINALIZE_DISCARD_BRANCH",
145
- "FINALIZE_NO_VCS"
146
- ];
140
+ return [...SHIP_FINALIZATION_MODES];
147
141
  }
148
142
  if (/final verdict/iu.test(rule)) {
149
143
  return ["APPROVED", "APPROVED_WITH_CONCERNS", "BLOCKED"];
@@ -795,8 +789,8 @@ function validateSectionBody(sectionBody, rule, sectionName) {
795
789
  details: "Section heading and content satisfy lint heuristics."
796
790
  };
797
791
  }
798
- export async function lintArtifact(projectRoot, stage) {
799
- const schema = stageSchema(stage);
792
+ export async function lintArtifact(projectRoot, stage, track = "standard") {
793
+ const schema = stageSchema(stage, track);
800
794
  const { absPath: absFile, relPath: relFile } = await resolveArtifactPath(projectRoot, schema.artifactFile);
801
795
  const findings = [];
802
796
  if (!(await exists(absFile))) {
@@ -3,6 +3,12 @@ import type { FlowStage, HarnessId } from "./types.js";
3
3
  export declare const RUNTIME_ROOT = ".cclaw";
4
4
  export declare const CCLAW_VERSION: string;
5
5
  export declare const FLOW_VERSION = "1.0.0";
6
+ /**
7
+ * Canonical ship finalization enums used across stage schema, linting, and
8
+ * runtime gate evidence checks.
9
+ */
10
+ export declare const SHIP_FINALIZATION_MODES: readonly ["FINALIZE_MERGE_LOCAL", "FINALIZE_OPEN_PR", "FINALIZE_KEEP_BRANCH", "FINALIZE_DISCARD_BRANCH", "FINALIZE_NO_VCS"];
11
+ export type ShipFinalizationMode = (typeof SHIP_FINALIZATION_MODES)[number];
6
12
  export declare const DEFAULT_HARNESSES: HarnessId[];
7
13
  /**
8
14
  * Evals subtree. Scaffolds the directory layout and a default config.yaml; the
package/dist/constants.js CHANGED
@@ -35,6 +35,17 @@ function readPackageVersion() {
35
35
  }
36
36
  export const CCLAW_VERSION = readPackageVersion();
37
37
  export const FLOW_VERSION = "1.0.0";
38
+ /**
39
+ * Canonical ship finalization enums used across stage schema, linting, and
40
+ * runtime gate evidence checks.
41
+ */
42
+ export const SHIP_FINALIZATION_MODES = [
43
+ "FINALIZE_MERGE_LOCAL",
44
+ "FINALIZE_OPEN_PR",
45
+ "FINALIZE_KEEP_BRANCH",
46
+ "FINALIZE_DISCARD_BRANCH",
47
+ "FINALIZE_NO_VCS"
48
+ ];
38
49
  export const DEFAULT_HARNESSES = [
39
50
  "claude",
40
51
  "cursor",
@@ -24,8 +24,60 @@ export interface AgentDefinition {
24
24
  }
25
25
  /**
26
26
  * Canonical specialist roster (core-5) materialized under `.cclaw/agents/`.
27
+ *
28
+ * Declared with `satisfies` so the array retains literal `name` types for
29
+ * downstream type-level consumers (e.g. `AgentName`), while still being
30
+ * checked against the `AgentDefinition` shape at compile time. Do not add
31
+ * an explicit `AgentDefinition[]` annotation here — it would widen `name`
32
+ * to `string` and break the compile-time drift guard.
33
+ */
34
+ export declare const CCLAW_AGENTS: readonly [{
35
+ readonly name: "planner";
36
+ readonly description: "MANDATORY for scope/design/plan and PROACTIVE for high-ambiguity work. MUST BE USED when sequencing, dependency mapping, or risk trade-offs are required before coding.";
37
+ readonly tools: ["Read", "Grep", "Glob", "WebSearch"];
38
+ readonly model: "deep";
39
+ readonly activation: "mandatory";
40
+ readonly relatedStages: ["brainstorm", "scope", "design", "spec", "plan"];
41
+ readonly body: string;
42
+ }, {
43
+ readonly name: "reviewer";
44
+ readonly description: "MANDATORY during review. MUST BE USED to run a two-pass audit: spec compliance first, then correctness/maintainability/performance/architecture.";
45
+ readonly tools: ["Read", "Grep", "Glob"];
46
+ readonly model: "balanced";
47
+ readonly activation: "mandatory";
48
+ readonly relatedStages: ["spec", "review", "ship"];
49
+ readonly body: string;
50
+ }, {
51
+ readonly name: "security-reviewer";
52
+ readonly description: "MANDATORY during review; PROACTIVE during design/ship for trust-boundary changes. Always produce an explicit no-change attestation when no security-relevant surface moved.";
53
+ readonly tools: ["Read", "Grep", "Glob"];
54
+ readonly model: "balanced";
55
+ readonly activation: "mandatory";
56
+ readonly relatedStages: ["design", "review", "ship"];
57
+ readonly body: string;
58
+ }, {
59
+ readonly name: "test-author";
60
+ readonly description: "MANDATORY in TDD stage. MUST BE USED for RED -> GREEN -> REFACTOR with evidence-first discipline.";
61
+ readonly tools: ["Read", "Write", "Edit", "Grep", "Glob", "Bash"];
62
+ readonly model: "balanced";
63
+ readonly activation: "mandatory";
64
+ readonly relatedStages: ["tdd"];
65
+ readonly body: string;
66
+ }, {
67
+ readonly name: "doc-updater";
68
+ readonly description: "MANDATORY at ship and PROACTIVE when behavior/config/public API changes. Keep docs and runbooks in lockstep with shipped behavior.";
69
+ readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
70
+ readonly model: "fast";
71
+ readonly activation: "mandatory";
72
+ readonly relatedStages: ["tdd", "ship"];
73
+ readonly body: string;
74
+ }];
75
+ /**
76
+ * Union of known agent names (compile-time). Use this in content that
77
+ * references agents by name so the TypeScript compiler catches renames
78
+ * and typos instead of letting them slip into generated artifacts.
27
79
  */
28
- export declare const CCLAW_AGENTS: AgentDefinition[];
80
+ export type AgentName = (typeof CCLAW_AGENTS)[number]["name"];
29
81
  /**
30
82
  * Render a complete cclaw agent markdown file (YAML frontmatter + body).
31
83
  */
@@ -15,6 +15,12 @@ function yamlFlowSequence(values) {
15
15
  }
16
16
  /**
17
17
  * Canonical specialist roster (core-5) materialized under `.cclaw/agents/`.
18
+ *
19
+ * Declared with `satisfies` so the array retains literal `name` types for
20
+ * downstream type-level consumers (e.g. `AgentName`), while still being
21
+ * checked against the `AgentDefinition` shape at compile time. Do not add
22
+ * an explicit `AgentDefinition[]` annotation here — it would widen `name`
23
+ * to `string` and break the compile-time drift guard.
18
24
  */
19
25
  export const CCLAW_AGENTS = [
20
26
  {
@@ -1094,11 +1094,20 @@ export function claudeHooksJsonWithObservation() {
1094
1094
  }]
1095
1095
  }],
1096
1096
  PreToolUse: [{
1097
+ // `prompt-guard.sh` inspects tool inputs across all tool calls;
1098
+ // it has to stay on `*` so it sees MCP/Edit/Write/WebSearch
1099
+ // traffic too. `workflow-guard.sh`, however, only checks TDD
1100
+ // ordering on write-like operations — it is a no-op for reads.
1101
+ // Splitting the two matchers cuts Claude's per-read hook
1102
+ // overhead in half without reducing coverage on write paths.
1097
1103
  matcher: "*",
1098
1104
  hooks: [{
1099
1105
  type: "command",
1100
1106
  command: hookDispatcherCommand("prompt-guard.sh")
1101
- }, {
1107
+ }]
1108
+ }, {
1109
+ matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
1110
+ hooks: [{
1102
1111
  type: "command",
1103
1112
  command: hookDispatcherCommand("workflow-guard.sh")
1104
1113
  }]
@@ -1196,6 +1205,18 @@ export function codexHooksJsonWithObservation() {
1196
1205
  hooks: [{
1197
1206
  type: "command",
1198
1207
  command: hookDispatcherCommand("prompt-guard.sh")
1208
+ }, {
1209
+ // `workflow-guard.sh` also runs here because Codex's PreToolUse
1210
+ // only sees Bash; Write/Edit/MCP writes never reach the hook
1211
+ // surface. Running workflow-guard on UserPromptSubmit catches
1212
+ // TDD-order violations that originate from the user's prompt
1213
+ // text (e.g. "edit X.ts to ..."). Payload is a prompt envelope,
1214
+ // not a tool call, so the script's TOOL extraction falls back
1215
+ // to "unknown" and advisory mode is a no-op by design — the
1216
+ // value is that prompt text is scanned for write-shaped intent
1217
+ // via the existing PAYLOAD_LOWER heuristics.
1218
+ type: "command",
1219
+ command: hookDispatcherCommand("workflow-guard.sh")
1199
1220
  }, {
1200
1221
  type: "command",
1201
1222
  command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw internal verify-current-state --quiet >/dev/null || true; else npx -y cclaw-cli internal verify-current-state --quiet >/dev/null || true; fi'"
@@ -233,11 +233,15 @@ export default function cclawPlugin(ctx) {
233
233
  eventType === "session.created" ||
234
234
  eventType === "session.resumed" ||
235
235
  eventType === "session.compacted" ||
236
- eventType === "session.cleared"
236
+ eventType === "session.cleared" ||
237
+ eventType === "session.updated"
237
238
  ) {
238
239
  // Avoid writing directly to stdout in lifecycle hooks because it can
239
240
  // interfere with OpenCode TUI rendering. Bootstrap is injected via
240
241
  // the system transform hook instead.
242
+ // session.updated covers config reloads and artifact/rules edits
243
+ // that happen mid-session; without it the cache would stay stale
244
+ // until the next compaction or restart.
241
245
  refreshBootstrapCache();
242
246
  }
243
247
  if (eventType === "session.compacted") {
@@ -5,6 +5,7 @@ import { tddStageForTrack } from "./stages/tdd.js";
5
5
  const ARTIFACT_STAGE_BY_PATH = {
6
6
  ".cclaw/artifacts/01-brainstorm.md": "brainstorm",
7
7
  ".cclaw/artifacts/02-scope.md": "scope",
8
+ ".cclaw/artifacts/02a-research.md": "design",
8
9
  ".cclaw/artifacts/03-design.md": "design",
9
10
  ".cclaw/artifacts/04-spec.md": "spec",
10
11
  ".cclaw/artifacts/05-plan.md": "plan",
@@ -46,6 +47,7 @@ const REQUIRED_GATE_IDS = {
46
47
  "tdd_green_full_suite",
47
48
  "tdd_refactor_completed",
48
49
  "tdd_verified_before_complete",
50
+ "tdd_docs_drift_check",
49
51
  ...(track === "quick" ? [] : ["tdd_traceable_to_plan"])
50
52
  ],
51
53
  review: (track) => [
@@ -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,10 +1,12 @@
1
+ import { CCLAW_VERSION, SHIP_FINALIZATION_MODES } from "../constants.js";
1
2
  import { orderedStageSchemas } from "./stage-schema.js";
2
3
  import { FLOW_STAGES } from "../types.js";
4
+ const SHIP_FINALIZATION_ENUM_LINES = SHIP_FINALIZATION_MODES.map((mode) => ` - ${mode}`).join("\n");
3
5
  export const ARTIFACT_TEMPLATES = {
4
6
  "01-brainstorm.md": `---
5
7
  stage: brainstorm
6
8
  schema_version: 1
7
- version: 0.18.0
9
+ version: ${CCLAW_VERSION}
8
10
  feature: <feature-id>
9
11
  locked_decisions: []
10
12
  inputs_hash: sha256:pending
@@ -52,7 +54,7 @@ inputs_hash: sha256:pending
52
54
  "02-scope.md": `---
53
55
  stage: scope
54
56
  schema_version: 1
55
- version: 0.18.0
57
+ version: ${CCLAW_VERSION}
56
58
  feature: <feature-id>
57
59
  locked_decisions: []
58
60
  inputs_hash: sha256:pending
@@ -158,7 +160,7 @@ inputs_hash: sha256:pending
158
160
  "02a-research.md": `---
159
161
  stage: design
160
162
  schema_version: 1
161
- version: 0.18.0
163
+ version: ${CCLAW_VERSION}
162
164
  feature: <feature-id>
163
165
  locked_decisions: []
164
166
  inputs_hash: sha256:pending
@@ -199,7 +201,7 @@ inputs_hash: sha256:pending
199
201
  "03-design.md": `---
200
202
  stage: design
201
203
  schema_version: 1
202
- version: 0.18.0
204
+ version: ${CCLAW_VERSION}
203
205
  feature: <feature-id>
204
206
  locked_decisions: []
205
207
  inputs_hash: sha256:pending
@@ -303,7 +305,7 @@ inputs_hash: sha256:pending
303
305
  "04-spec.md": `---
304
306
  stage: spec
305
307
  schema_version: 1
306
- version: 0.18.0
308
+ version: ${CCLAW_VERSION}
307
309
  feature: <feature-id>
308
310
  locked_decisions: []
309
311
  inputs_hash: sha256:pending
@@ -359,7 +361,7 @@ inputs_hash: sha256:pending
359
361
  "05-plan.md": `---
360
362
  stage: plan
361
363
  schema_version: 1
362
- version: 0.18.0
364
+ version: ${CCLAW_VERSION}
363
365
  feature: <feature-id>
364
366
  locked_decisions: []
365
367
  inputs_hash: sha256:pending
@@ -438,7 +440,7 @@ Execution rule: complete and verify each batch before starting the next batch.
438
440
  "06-tdd.md": `---
439
441
  stage: tdd
440
442
  schema_version: 1
441
- version: 0.18.0
443
+ version: ${CCLAW_VERSION}
442
444
  feature: <feature-id>
443
445
  locked_decisions: []
444
446
  inputs_hash: sha256:pending
@@ -505,7 +507,7 @@ inputs_hash: sha256:pending
505
507
  "07-review.md": `---
506
508
  stage: review
507
509
  schema_version: 1
508
- version: 0.18.0
510
+ version: ${CCLAW_VERSION}
509
511
  feature: <feature-id>
510
512
  locked_decisions: []
511
513
  inputs_hash: sha256:pending
@@ -614,7 +616,7 @@ inputs_hash: sha256:pending
614
616
  "08-ship.md": `---
615
617
  stage: ship
616
618
  schema_version: 1
617
- version: 0.18.0
619
+ version: ${CCLAW_VERSION}
618
620
  feature: <feature-id>
619
621
  locked_decisions: []
620
622
  inputs_hash: sha256:pending
@@ -644,11 +646,7 @@ inputs_hash: sha256:pending
644
646
 
645
647
  ## Finalization
646
648
  - Selected enum (exactly one):
647
- - FINALIZE_MERGE_LOCAL
648
- - FINALIZE_OPEN_PR
649
- - FINALIZE_KEEP_BRANCH
650
- - FINALIZE_DISCARD_BRANCH
651
- - FINALIZE_NO_VCS
649
+ ${SHIP_FINALIZATION_ENUM_LINES}
652
650
  - Selected label (A/B/C/D/E):
653
651
  - Execution result:
654
652
  - PR URL / merge commit / kept branch / discard confirmation:
@@ -669,7 +667,7 @@ inputs_hash: sha256:pending
669
667
  "09-retro.md": `---
670
668
  stage: retro
671
669
  schema_version: 1
672
- version: 0.18.0
670
+ version: ${CCLAW_VERSION}
673
671
  feature: <feature-id>
674
672
  locked_decisions: []
675
673
  inputs_hash: sha256:pending
@@ -48,4 +48,10 @@ export declare const LANGUAGE_RULE_PACK_GENERATORS: Record<string, () => string>
48
48
  */
49
49
  export declare const LEGACY_LANGUAGE_RULE_PACK_FOLDERS: readonly ["language-typescript", "language-python", "language-go"];
50
50
  export declare const UTILITY_SKILL_FOLDERS: readonly ["security", "debugging", "performance", "ci-cd", "docs", "executing-plans", "verification-before-completion", "finishing-a-development-branch", "context-engineering", "source-driven-development", "frontend-accessibility", "landscape-check", "adversarial-review", "security-audit", "knowledge-curation", "retrospective", "document-review", "receiving-code-review"];
51
- export declare const UTILITY_SKILL_MAP: Record<string, () => string>;
51
+ export type UtilitySkillFolder = (typeof UTILITY_SKILL_FOLDERS)[number];
52
+ /**
53
+ * One entry per `UTILITY_SKILL_FOLDERS` slot. Typed via the tuple so that
54
+ * adding a folder without a generator (or vice versa) is a TypeScript
55
+ * error — keeps the two sources of truth in lockstep at compile time.
56
+ */
57
+ export declare const UTILITY_SKILL_MAP: Record<UtilitySkillFolder, () => string>;
@@ -1735,6 +1735,11 @@ export const UTILITY_SKILL_FOLDERS = [
1735
1735
  "document-review",
1736
1736
  "receiving-code-review"
1737
1737
  ];
1738
+ /**
1739
+ * One entry per `UTILITY_SKILL_FOLDERS` slot. Typed via the tuple so that
1740
+ * adding a folder without a generator (or vice versa) is a TypeScript
1741
+ * error — keeps the two sources of truth in lockstep at compile time.
1742
+ */
1738
1743
  export const UTILITY_SKILL_MAP = {
1739
1744
  security: securityReviewSkill,
1740
1745
  debugging: debuggingSkill,
@@ -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
@@ -66,6 +68,16 @@ export type DelegationLedger = {
66
68
  runId: string;
67
69
  entries: DelegationEntry[];
68
70
  };
71
+ /**
72
+ * Heuristic: does a changed file path strongly imply a trust-boundary
73
+ * surface? Used to gate adversarial-reviewer requirements on review.
74
+ *
75
+ * Matches authN/Z, credentials, crypto, policy, or explicit sanitization
76
+ * or injection handling. Intentionally excludes broad terms like `input`
77
+ * and `validation` because they match innocuous paths such as
78
+ * `form-input.ts` or `number-validation.ts` and produce false positives.
79
+ */
80
+ export declare function isTrustBoundaryPath(filePath: string): boolean;
69
81
  export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
70
82
  export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
71
83
  /**
@@ -75,7 +87,9 @@ export declare function appendDelegation(projectRoot: string, entry: DelegationE
75
87
  * strongest guarantee.
76
88
  */
77
89
  export declare function expectedFulfillmentMode(fallbacks: SubagentFallback[]): DelegationFulfillmentMode;
78
- export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage): Promise<{
90
+ export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage, options?: {
91
+ repairFeatureSystem?: boolean;
92
+ }): Promise<{
79
93
  satisfied: boolean;
80
94
  missing: string[];
81
95
  waived: string[];
@@ -53,6 +53,18 @@ async function resolveReviewDiffBase(projectRoot) {
53
53
  return null;
54
54
  }
55
55
  }
56
+ /**
57
+ * Heuristic: does a changed file path strongly imply a trust-boundary
58
+ * surface? Used to gate adversarial-reviewer requirements on review.
59
+ *
60
+ * Matches authN/Z, credentials, crypto, policy, or explicit sanitization
61
+ * or injection handling. Intentionally excludes broad terms like `input`
62
+ * and `validation` because they match innocuous paths such as
63
+ * `form-input.ts` or `number-validation.ts` and produce false positives.
64
+ */
65
+ export function isTrustBoundaryPath(filePath) {
66
+ return /(auth|security|secret|token|credential|permission|acl|policy|oauth|session|encrypt|decrypt|sanitize|untrusted|csrf|xss|injection|taint)/iu.test(filePath);
67
+ }
56
68
  async function detectReviewTriggers(projectRoot) {
57
69
  const empty = {
58
70
  changedFiles: 0,
@@ -81,7 +93,7 @@ async function detectReviewTriggers(projectRoot) {
81
93
  .split(/\r?\n/gu)
82
94
  .map((line) => line.trim())
83
95
  .filter((line) => line.length > 0);
84
- const trustBoundaryChanged = changedPaths.some((filePath) => /(auth|security|secret|token|credential|permission|acl|policy|oauth|session|encrypt|decrypt|input|validation)/iu.test(filePath));
96
+ const trustBoundaryChanged = changedPaths.some((p) => isTrustBoundaryPath(p));
85
97
  const requireAdversarialReviewer = changedLines > 100 || changedFiles > 10 || trustBoundaryChanged;
86
98
  return {
87
99
  changedFiles,
@@ -142,6 +154,7 @@ function isDelegationEntry(value) {
142
154
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
143
155
  retryOk &&
144
156
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
157
+ (o.skill === undefined || typeof o.skill === "string") &&
145
158
  (o.schemaVersion === undefined || o.schemaVersion === 1));
146
159
  }
147
160
  function parseLedger(raw, runId) {
@@ -245,9 +258,11 @@ export function expectedFulfillmentMode(fallbacks) {
245
258
  return "role-switch";
246
259
  return "harness-waiver";
247
260
  }
248
- export async function checkMandatoryDelegations(projectRoot, stage) {
261
+ export async function checkMandatoryDelegations(projectRoot, stage, options = {}) {
249
262
  const mandatory = stageSchema(stage).mandatoryDelegations;
250
- const { activeRunId } = await readFlowState(projectRoot);
263
+ const { activeRunId } = await readFlowState(projectRoot, {
264
+ repairFeatureSystem: options.repairFeatureSystem
265
+ });
251
266
  const ledger = await readDelegationLedger(projectRoot);
252
267
  const forStage = ledger.entries.filter((e) => e.stage === stage);
253
268
  const forRun = forStage.filter((e) => e.runId === activeRunId);
@@ -267,14 +282,15 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
267
282
  const rows = forRun.filter((e) => e.agent === agent);
268
283
  const completedRows = rows.filter((e) => e.status === "completed");
269
284
  const waivedRows = rows.filter((e) => e.status === "waived");
270
- const requiredCompletedCount = stage === "review" &&
285
+ const adversarialReviewerRequired = stage === "review" &&
271
286
  agent === "reviewer" &&
272
- reviewTriggers?.requireAdversarialReviewer
273
- ? 2
274
- : 1;
287
+ reviewTriggers?.requireAdversarialReviewer === true;
288
+ const requiredCompletedCount = adversarialReviewerRequired ? 2 : 1;
275
289
  const hasCompleted = completedRows.length >= requiredCompletedCount;
276
290
  const hasWaived = waivedRows.length > 0;
277
- const ok = hasCompleted || hasWaived;
291
+ const hasAdversarialSkill = !adversarialReviewerRequired ||
292
+ completedRows.some((row) => row.skill === "adversarial-review");
293
+ const ok = hasWaived || (hasCompleted && hasAdversarialSkill);
278
294
  if (!ok) {
279
295
  missing.push(agent);
280
296
  continue;