cclaw-cli 0.46.1 → 0.46.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.
@@ -3,13 +3,10 @@ import type { StageAutoSubagentDispatch, StageSchema } from "./stages/schema-typ
3
3
  export type { ArtifactValidation, CrossStageTrace, ReviewSection, StageAutoSubagentDispatch, StageGate, StageSchema, StageSchemaInput } from "./stages/schema-types.js";
4
4
  /** Transition guard: agents with `mode: "mandatory"` in auto-subagent dispatch for this stage. */
5
5
  export declare function mandatoryDelegationsForStage(stage: FlowStage): string[];
6
- /** Conditional dispatches that become mandatory only when their `condition` predicate evaluates true. */
7
- export declare function conditionalDispatchesForStage(stage: FlowStage): StageAutoSubagentDispatch[];
8
6
  export declare function stageSchema(stage: FlowStage): StageSchema;
9
7
  export declare function orderedStageSchemas(): StageSchema[];
10
8
  export declare function stageGateIds(stage: FlowStage): string[];
11
9
  export declare function stageRecommendedGateIds(stage: FlowStage): string[];
12
- export declare function stageConditionalGateIds(stage: FlowStage): string[];
13
10
  export declare function nextCclawCommand(stage: FlowStage): string;
14
11
  export declare function buildTransitionRules(): TransitionRule[];
15
12
  export declare function stagePolicyNeedles(stage: FlowStage): string[];
@@ -10,7 +10,6 @@ import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stag
10
10
  * Gate tiers:
11
11
  * - required: blocking for stage completion.
12
12
  * - recommended: quality signal; unmet -> DONE_WITH_CONCERNS, not BLOCKED.
13
- * - conditional: becomes blocking only when triggered.
14
13
  */
15
14
  const REQUIRED_GATE_IDS = {
16
15
  brainstorm: [
@@ -196,8 +195,7 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
196
195
  },
197
196
  {
198
197
  agent: "reviewer",
199
- mode: "conditional",
200
- condition: "diff_lines_gt:100||files_touched_gt:10||trust_boundary_changed",
198
+ mode: "proactive",
201
199
  when: "When the diff exceeds 100 changed lines, touches more than 10 files, or modifies trust boundaries — dispatch a SECOND, independent reviewer with the adversarial-review skill loaded so the review army has at least two voices on a high-blast-radius change.",
202
200
  purpose: "Adversarial second-opinion review on large or trust-sensitive diffs. The second reviewer treats the implementation as hostile and tries to break it (hostile-user, future-maintainer, competitor lenses) instead of sympathetically explaining it.",
203
201
  requiresUserGate: false,
@@ -227,10 +225,6 @@ export function mandatoryDelegationsForStage(stage) {
227
225
  .filter((d) => d.mode === "mandatory")
228
226
  .map((d) => d.agent);
229
227
  }
230
- /** Conditional dispatches that become mandatory only when their `condition` predicate evaluates true. */
231
- export function conditionalDispatchesForStage(stage) {
232
- return STAGE_AUTO_SUBAGENT_DISPATCH[stage].filter((d) => d.mode === "conditional");
233
- }
234
228
  export function stageSchema(stage) {
235
229
  const base = STAGE_SCHEMA_MAP[stage];
236
230
  const tieredGates = tieredStageGates(stage, base.requiredGates);
@@ -255,11 +249,6 @@ export function stageRecommendedGateIds(stage) {
255
249
  .filter((gate) => gate.tier === "recommended")
256
250
  .map((gate) => gate.id);
257
251
  }
258
- export function stageConditionalGateIds(stage) {
259
- // Conditional gate DSL removed in favor of explicit required/recommended tiers.
260
- void stage;
261
- return [];
262
- }
263
252
  export function nextCclawCommand(stage) {
264
253
  const next = stageSchema(stage).next;
265
254
  return next === "done" ? "none" : `/cc-${next}`;
@@ -2,9 +2,7 @@ import type { FlowStage } from "../../types.js";
2
2
  export interface StageGate {
3
3
  id: string;
4
4
  description: string;
5
- tier?: "required" | "recommended" | "conditional";
6
- /** Used when tier=conditional. Predicate syntax mirrors conditional delegation rules. */
7
- condition?: string;
5
+ tier?: "required" | "recommended";
8
6
  }
9
7
  export interface ReviewSection {
10
8
  title: string;
@@ -19,9 +17,7 @@ export interface CrossStageTrace {
19
17
  export interface ArtifactValidation {
20
18
  section: string;
21
19
  required: boolean;
22
- tier?: "required" | "recommended" | "conditional";
23
- /** Optional predicate for conditional validations. */
24
- condition?: string;
20
+ tier?: "required" | "recommended";
25
21
  validationRule: string;
26
22
  }
27
23
  export interface StageAutoSubagentDispatch {
@@ -29,20 +25,11 @@ export interface StageAutoSubagentDispatch {
29
25
  /**
30
26
  * - `mandatory` — must be dispatched (or explicitly waived) before stage transition.
31
27
  * - `proactive` — should be dispatched automatically when context matches `when`.
32
- * - `conditional` — dispatched only when `condition` evaluates true at runtime; counted as
33
- * mandatory **only when the condition holds**.
34
28
  */
35
- mode: "mandatory" | "proactive" | "conditional";
29
+ mode: "mandatory" | "proactive";
36
30
  when: string;
37
31
  purpose: string;
38
32
  requiresUserGate: boolean;
39
- /**
40
- * Optional machine-friendly trigger expression for `conditional` rows.
41
- * Supported predicates: `diff_lines_gt:<N>`, `files_touched_gt:<N>`,
42
- * `trust_boundary_changed`, `release_blast_radius_high`.
43
- * Multiple predicates joined by `||` mean ANY trigger satisfies the condition.
44
- */
45
- condition?: string;
46
33
  /** Optional skill folder the dispatched agent should load as additional context. */
47
34
  skill?: string;
48
35
  }
@@ -599,146 +599,6 @@ Process (mandatory):
599
599
  - Report: FILES_EDITED, GREEN_COMMAND_RUN, REFACTOR_NOTES, STATUS: DONE|BLOCKED.
600
600
  \`\`\`
601
601
 
602
- `;
603
- }
604
- function repoResearchAnalystEnhancedBody() {
605
- return `
606
-
607
- ## Task Tool Delegation
608
-
609
- Launch **read-only repo exploration** at the start of brainstorm/scope/design so the primary agent plans on a grounded map, not guesses. Use this as an in-thread research procedure.
610
-
611
- \`\`\`
612
- You are a repo research analyst subagent.
613
-
614
- TASK DOMAIN: {1-sentence description of the feature/fix/refactor being planned}
615
- REPO HINTS: {known directories, module names, patterns the primary agent already knows}
616
- OUT OF SCOPE: {paths not to read (large vendor dirs, generated code)}
617
-
618
- Deliverables:
619
- - Relevant modules: list of \`path — purpose\` (cite file:line on ambiguous claims).
620
- - Reuse candidates: list of \`file:line — why this absorbs the change\`.
621
- - Ownership hints: CODEOWNERS / README / comment signals.
622
- - Gaps: capabilities NOT yet present that the task would need.
623
-
624
- Rules:
625
- - Read-only. Do NOT edit files.
626
- - Cite file:line for every claim; never invent paths.
627
- - If the scope is too large to fully explore, say so and bound your search.
628
- \`\`\`
629
-
630
- `;
631
- }
632
- function learningsResearcherEnhancedBody() {
633
- return `
634
-
635
- ## Task Tool Delegation
636
-
637
- Dispatch before any non-trivial stage to stream \`.cclaw/knowledge.jsonl\` and surface prior learnings. Cheap \`fast\` tier — fan out with other research agents.
638
-
639
- \`\`\`
640
- You are a learnings researcher subagent.
641
-
642
- TASK DESCRIPTION: {verbatim prompt + current stage}
643
- DOMAIN HINTS: {keywords from Task Classification / Origin Docs}
644
-
645
- Deliverables:
646
- - Matched rules: list of \`trigger → action (confidence)\`.
647
- - Matched patterns: list of \`trigger → action (confidence)\`.
648
- - Matched lessons: list of \`trigger → action (confidence)\`.
649
- - Matched compounds: list of \`trigger → action (confidence)\`.
650
- - No-match note (if nothing relevant exists).
651
-
652
- Rules:
653
- - Read-only; NEVER rewrite or delete entries.
654
- - Return at most 10 entries, ranked by confidence then recency.
655
- - Quote the entries verbatim — do NOT paraphrase.
656
- \`\`\`
657
-
658
- `;
659
- }
660
- function frameworkDocsResearcherEnhancedBody() {
661
- return `
662
-
663
- ## Task Tool Delegation
664
-
665
- Use for any task that depends on a specific framework/library/SDK/CLI. Prefer context7 MCP when available for version-accurate docs; otherwise WebSearch/WebFetch official sources.
666
-
667
- \`\`\`
668
- You are a framework documentation researcher subagent.
669
-
670
- LIBRARY + VERSION: {name + resolved version from lockfile / pyproject / go.mod / Cargo.toml / pom.xml / build.gradle}
671
- TASK USAGE: {which APIs the task will actually call}
672
- CONTEXT7: {"available" | "not available"}
673
-
674
- Deliverables:
675
- - Key APIs: list of signatures the task will touch.
676
- - Breaking changes since the last major release relevant to the task.
677
- - Gotchas: deprecated paths, version-gated flags, platform caveats.
678
- - Source: URL(s) or MCP reference used.
679
-
680
- Rules:
681
- - Never invent APIs. Prefer silence + UNKNOWN over speculation.
682
- - Tie every statement to an authoritative source; avoid blog posts when official docs exist.
683
- \`\`\`
684
-
685
- `;
686
- }
687
- function bestPracticesResearcherEnhancedBody() {
688
- return `
689
-
690
- ## Task Tool Delegation
691
-
692
- Use when the task touches a well-known domain (auth, caching, rate limiting, observability, accessibility, etc.) and the primary agent needs a short, citable best-practice summary.
693
-
694
- \`\`\`
695
- You are a best-practices researcher subagent.
696
-
697
- DOMAIN: {one word, e.g. auth, caching, rate-limiting, a11y, observability, retries}
698
- SUB-PROBLEM: {narrow one-sentence statement of what the task is actually deciding}
699
-
700
- Deliverables:
701
- - Recommended practices: 5-8 entries of \`practice — rationale — source\`.
702
- - Common traps / anti-patterns: list of \`trap — why it fails — source\`.
703
- - Decision hooks: 1-3 explicit questions the primary agent must answer.
704
-
705
- Rules:
706
- - Cite 3-5 authoritative sources (official docs, IETF/W3C/OWASP, well-known standards).
707
- - If the domain has no authoritative answer, say so; do NOT substitute opinion.
708
- \`\`\`
709
-
710
- `;
711
- }
712
- function gitHistoryAnalyzerEnhancedBody() {
713
- return `
714
-
715
- ## Task Tool Delegation
716
-
717
- Use when the task touches existing code, so the primary agent can see prior attempts, reverts, and owners before proposing changes.
718
-
719
- \`\`\`
720
- You are a git history analyzer subagent.
721
-
722
- IMPACTED PATHS: {list of files/directories the task plans to touch}
723
- WINDOW: {default 90 days; adjust only if explicitly needed}
724
-
725
- Commands to run (read-only):
726
- - git log --follow -n 20 -- <path>
727
- - git blame <path>
728
- - git log --since="<window>" --grep="revert|regression" -- <path>
729
- - git log --since="<window>" --format="%an" -- <path> | sort | uniq -c | sort -nr
730
-
731
- Deliverables:
732
- - Recent themes: 3-5 bullets on what changed lately per path.
733
- - Revert/regression signals: list with commit SHAs.
734
- - Owners: best-guess from blame + committer frequency.
735
- - Collision risks: in-flight refactors/migrations visible in log.
736
-
737
- Rules:
738
- - Read-only. Never amend history, never git push.
739
- - If a path is new (no history), say so explicitly rather than fabricating context.
740
- \`\`\`
741
-
742
602
  `;
743
603
  }
744
604
  function docUpdaterEnhancedBody() {
@@ -1,6 +1,6 @@
1
1
  import { type SubagentFallback } from "./harness-adapters.js";
2
2
  import type { FlowStage } from "./types.js";
3
- export type DelegationMode = "mandatory" | "proactive" | "conditional";
3
+ export type DelegationMode = "mandatory" | "proactive";
4
4
  export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
5
5
  /**
6
6
  * How a delegation was actually fulfilled. Advisory — mirrors the harness
@@ -45,10 +45,7 @@ export type DelegationEntry = {
45
45
  * consumers treat missing runId as unscoped (conservatively excluded from current-run checks).
46
46
  */
47
47
  runId?: string;
48
- /**
49
- * For `conditional` rows: the trigger predicate that fired (e.g. `diff_lines_gt:100`).
50
- * Recorded for audit so reviewers can see why the second pass was required.
51
- */
48
+ /** Legacy field kept for backward compatibility with historical ledgers. */
52
49
  conditionTrigger?: string;
53
50
  /** Optional token usage captured from the delegated run. */
54
51
  tokens?: DelegationTokenUsage;
@@ -30,7 +30,7 @@ function isDelegationEntry(value) {
30
30
  if (!value || typeof value !== "object" || Array.isArray(value))
31
31
  return false;
32
32
  const o = value;
33
- const modeOk = o.mode === "mandatory" || o.mode === "proactive" || o.mode === "conditional";
33
+ const modeOk = o.mode === "mandatory" || o.mode === "proactive";
34
34
  const statusOk = o.status === "scheduled" ||
35
35
  o.status === "completed" ||
36
36
  o.status === "failed" ||
package/dist/doctor.js CHANGED
@@ -20,6 +20,7 @@ import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClos
20
20
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
21
21
  import { stageSkillFolder } from "./content/skills.js";
22
22
  import { doctorCheckMetadata } from "./doctor-registry.js";
23
+ import { resolveTrackFromPrompt } from "./track-heuristics.js";
23
24
  import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
24
25
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
25
26
  import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
@@ -95,6 +96,18 @@ function collectHookCommands(value) {
95
96
  const nested = collectHookCommands(obj.hooks);
96
97
  return [...direct, ...nested];
97
98
  }
99
+ function extractUserPromptFromIdeaArtifact(markdown) {
100
+ const normalized = markdown.replace(/\r\n?/gu, "\n");
101
+ const heading = /^##\s+User prompt\s*$/imu.exec(normalized);
102
+ if (!heading || heading.index === undefined) {
103
+ return null;
104
+ }
105
+ const sectionStart = heading.index + heading[0].length;
106
+ const tail = normalized.slice(sectionStart).replace(/^\s*\n/gu, "");
107
+ const nextHeadingIndex = tail.search(/^##\s+/mu);
108
+ const body = (nextHeadingIndex >= 0 ? tail.slice(0, nextHeadingIndex) : tail).trim();
109
+ return body.length > 0 ? body : null;
110
+ }
98
111
  async function commandAvailable(command) {
99
112
  try {
100
113
  await execFileAsync("bash", ["-lc", `command -v ${command} >/dev/null 2>&1`]);
@@ -1221,6 +1234,41 @@ export async function doctorChecks(projectRoot, options = {}) {
1221
1234
  ? `active track "${activeTrack}" (${trackStageList.length}/${COMMAND_FILE_ORDER.length} stages: ${trackStageList.join(" → ")})${expectedSkipped.length > 0 ? `; skippedStages=${expectedSkipped.join(", ")}` : ""}`
1222
1235
  : `track "${activeTrack}" expects skippedStages=[${expectedSkipped.join(", ")}] but flow-state has [${skippedFromState.join(", ")}] — run \`cclaw sync\` to repair`
1223
1236
  });
1237
+ if (parsedConfig?.trackHeuristics) {
1238
+ const ideaArtifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
1239
+ let heuristicsAligned = true;
1240
+ let heuristicsDetails = "trackHeuristics configured; advisory alignment check skipped.";
1241
+ if (!(await exists(ideaArtifactPath))) {
1242
+ heuristicsDetails = `trackHeuristics configured but ${RUNTIME_ROOT}/artifacts/00-idea.md is missing; advisory alignment check skipped.`;
1243
+ }
1244
+ else {
1245
+ const ideaMarkdown = await fs.readFile(ideaArtifactPath, "utf8");
1246
+ if (/^Reclassification:\s*/imu.test(ideaMarkdown)) {
1247
+ heuristicsDetails = "00-idea.md contains Reclassification entry; advisory heuristic mismatch check skipped.";
1248
+ }
1249
+ else {
1250
+ const userPrompt = extractUserPromptFromIdeaArtifact(ideaMarkdown);
1251
+ if (!userPrompt) {
1252
+ heuristicsDetails = "00-idea.md has no `## User prompt` section; advisory heuristic mismatch check skipped.";
1253
+ }
1254
+ else {
1255
+ const resolution = resolveTrackFromPrompt(userPrompt, parsedConfig.trackHeuristics);
1256
+ const tokenNote = resolution.matchedTokens.length > 0
1257
+ ? `matched: ${resolution.matchedTokens.join(", ")}`
1258
+ : "matched: none (fallback)";
1259
+ heuristicsAligned = resolution.track === activeTrack;
1260
+ heuristicsDetails = heuristicsAligned
1261
+ ? `trackHeuristics advisory matches active track "${activeTrack}" (${tokenNote}).`
1262
+ : `warning: trackHeuristics advisory predicts "${resolution.track}" (${tokenNote}; ${resolution.reason}) but flow-state track is "${activeTrack}". Re-run classification or add Reclassification in 00-idea.md if override was intentional.`;
1263
+ }
1264
+ }
1265
+ }
1266
+ checks.push({
1267
+ name: "warning:track_heuristics:advisory_alignment",
1268
+ ok: heuristicsAligned,
1269
+ details: heuristicsDetails
1270
+ });
1271
+ }
1224
1272
  checks.push({
1225
1273
  name: "flow_state:track_completed_in_track",
1226
1274
  ok: flowState.completedStages.every((stage) => trackStageList.includes(stage) || expectedSkipped.includes(stage)),
@@ -1,5 +1,5 @@
1
1
  import { COMMAND_FILE_ORDER } from "./constants.js";
2
- import { buildTransitionRules, orderedStageSchemas, stageConditionalGateIds, stageGateIds, stageRecommendedGateIds } from "./content/stage-schema.js";
2
+ import { buildTransitionRules, orderedStageSchemas, stageGateIds, stageRecommendedGateIds } from "./content/stage-schema.js";
3
3
  import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
4
4
  export const TRANSITION_RULES = buildTransitionRules();
5
5
  /**
@@ -63,7 +63,7 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
63
63
  stageGateCatalog[schema.stage] = {
64
64
  required: stageGateIds(schema.stage),
65
65
  recommended: stageRecommendedGateIds(schema.stage),
66
- conditional: stageConditionalGateIds(schema.stage),
66
+ conditional: [],
67
67
  triggered: [],
68
68
  passed: [],
69
69
  blocked: []
@@ -42,13 +42,10 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
42
42
  const recommended = schema.requiredGates
43
43
  .filter((gate) => gate.tier === "recommended")
44
44
  .map((gate) => gate.id);
45
- const conditional = schema.requiredGates
46
- .filter((gate) => gate.tier === "conditional")
47
- .map((gate) => gate.id);
45
+ const conditional = [];
48
46
  const requiredSet = new Set(required);
49
47
  const recommendedSet = new Set(recommended);
50
- const conditionalSet = new Set(conditional);
51
- const allowedSet = new Set([...required, ...recommended, ...conditional]);
48
+ const allowedSet = new Set([...required, ...recommended]);
52
49
  const issues = [];
53
50
  const catalogRequired = unique(catalog.required);
54
51
  const catalogRecommended = unique(catalog.recommended ?? []);
@@ -58,8 +55,6 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
58
55
  const unexpectedInCatalog = catalogRequired.filter((gateId) => !requiredSet.has(gateId));
59
56
  const missingRecommendedInCatalog = recommended.filter((gateId) => !catalogRecommended.includes(gateId));
60
57
  const unexpectedRecommendedInCatalog = catalogRecommended.filter((gateId) => !recommendedSet.has(gateId));
61
- const missingConditionalInCatalog = conditional.filter((gateId) => !catalogConditional.includes(gateId));
62
- const unexpectedConditionalInCatalog = catalogConditional.filter((gateId) => !conditionalSet.has(gateId));
63
58
  for (const gateId of missingInCatalog) {
64
59
  issues.push(`gate "${gateId}" missing from stageGateCatalog.required for stage "${stage}".`);
65
60
  }
@@ -72,16 +67,11 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
72
67
  for (const gateId of unexpectedRecommendedInCatalog) {
73
68
  issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.recommended for stage "${stage}".`);
74
69
  }
75
- for (const gateId of missingConditionalInCatalog) {
76
- issues.push(`gate "${gateId}" missing from stageGateCatalog.conditional for stage "${stage}".`);
77
- }
78
- for (const gateId of unexpectedConditionalInCatalog) {
79
- issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.conditional for stage "${stage}".`);
70
+ for (const gateId of catalogConditional) {
71
+ issues.push(`stale conditional gate "${gateId}" found in stageGateCatalog.conditional for stage "${stage}" (conditional gate DSL removed).`);
80
72
  }
81
73
  for (const gateId of catalogTriggered) {
82
- if (!conditionalSet.has(gateId)) {
83
- issues.push(`triggered gate "${gateId}" is not defined as conditional for stage "${stage}".`);
84
- }
74
+ issues.push(`stale triggered conditional gate "${gateId}" found in stageGateCatalog.triggered for stage "${stage}" (conditional gate DSL removed).`);
85
75
  }
86
76
  const blockedSet = new Set(catalog.blocked);
87
77
  for (const gateId of catalog.passed) {
@@ -126,23 +116,15 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
126
116
  }
127
117
  }
128
118
  const passedSet = new Set(catalog.passed);
129
- const triggeredConditionalSet = new Set([
130
- ...catalogTriggered.filter((gateId) => conditionalSet.has(gateId)),
131
- ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
132
- ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
133
- ]);
134
119
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
135
120
  const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
136
- const missingTriggeredConditional = [...triggeredConditionalSet].filter((gateId) => !passedSet.has(gateId));
137
- const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId) || triggeredConditionalSet.has(gateId));
138
- const complete = missingRequired.length === 0 && missingTriggeredConditional.length === 0 && blockingBlocked.length === 0;
121
+ const missingTriggeredConditional = [];
122
+ const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId));
123
+ const complete = missingRequired.length === 0 && blockingBlocked.length === 0;
139
124
  if (flowState.completedStages.includes(stage) && !complete) {
140
125
  if (missingRequired.length > 0) {
141
126
  issues.push(`stage "${stage}" is marked completed but required gates are not passed: ${missingRequired.join(", ")}.`);
142
127
  }
143
- if (missingTriggeredConditional.length > 0) {
144
- issues.push(`stage "${stage}" is marked completed but triggered conditional gates are not passed: ${missingTriggeredConditional.join(", ")}.`);
145
- }
146
128
  if (blockingBlocked.length > 0) {
147
129
  issues.push(`stage "${stage}" is marked completed but has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
148
130
  }
@@ -154,7 +136,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
154
136
  requiredCount: required.length,
155
137
  recommendedCount: recommended.length,
156
138
  conditionalCount: conditional.length,
157
- triggeredConditionalCount: triggeredConditionalSet.size,
139
+ triggeredConditionalCount: 0,
158
140
  passedCount: catalog.passed.length,
159
141
  blockedCount: catalog.blocked.length,
160
142
  complete,
@@ -172,19 +154,10 @@ export function verifyCompletedStagesGateClosure(flowState) {
172
154
  const required = schema.requiredGates
173
155
  .filter((gate) => gate.tier === "required")
174
156
  .map((gate) => gate.id);
175
- const conditional = schema.requiredGates
176
- .filter((gate) => gate.tier === "conditional")
177
- .map((gate) => gate.id);
178
- const conditionalSet = new Set(conditional);
179
157
  const passedSet = new Set(catalog.passed);
180
- const triggeredSet = new Set([
181
- ...(catalog.triggered ?? []).filter((gateId) => conditionalSet.has(gateId)),
182
- ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
183
- ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
184
- ]);
185
158
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
186
- const missingTriggeredConditional = [...triggeredSet].filter((gateId) => !passedSet.has(gateId));
187
- const blockingBlocked = catalog.blocked.filter((gateId) => required.includes(gateId) || triggeredSet.has(gateId));
159
+ const missingTriggeredConditional = [];
160
+ const blockingBlocked = catalog.blocked.filter((gateId) => required.includes(gateId));
188
161
  if (missingRequired.length > 0 || missingTriggeredConditional.length > 0 || blockingBlocked.length > 0) {
189
162
  openStages.push({
190
163
  stage,
@@ -195,9 +168,6 @@ export function verifyCompletedStagesGateClosure(flowState) {
195
168
  if (missingRequired.length > 0) {
196
169
  issues.push(`completed stage "${stage}" has unpassed required gates: ${missingRequired.join(", ")}.`);
197
170
  }
198
- if (missingTriggeredConditional.length > 0) {
199
- issues.push(`completed stage "${stage}" has unpassed triggered conditional gates: ${missingTriggeredConditional.join(", ")}.`);
200
- }
201
171
  if (blockingBlocked.length > 0) {
202
172
  issues.push(`completed stage "${stage}" still has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
203
173
  }
@@ -213,13 +183,10 @@ export function reconcileCurrentStageGateCatalog(flowState) {
213
183
  const recommended = stageSchema(stage).requiredGates
214
184
  .filter((gate) => gate.tier === "recommended")
215
185
  .map((gate) => gate.id);
216
- const conditional = stageSchema(stage).requiredGates
217
- .filter((gate) => gate.tier === "conditional")
218
- .map((gate) => gate.id);
186
+ const conditional = [];
219
187
  const requiredSet = new Set(required);
220
188
  const recommendedSet = new Set(recommended);
221
- const conditionalSet = new Set(conditional);
222
- const allowedSet = new Set([...required, ...recommended, ...conditional]);
189
+ const allowedSet = new Set([...required, ...recommended]);
223
190
  const catalog = flowState.stageGateCatalog[stage];
224
191
  const notes = [];
225
192
  const before = {
@@ -244,17 +211,13 @@ export function reconcileCurrentStageGateCatalog(flowState) {
244
211
  }
245
212
  return keep;
246
213
  }));
247
- const triggeredSet = new Set(unique(catalog.triggered).filter((gateId) => {
248
- const keep = conditionalSet.has(gateId);
249
- if (!keep) {
250
- notes.push(`removed unknown triggered gate "${gateId}"`);
251
- }
252
- return keep;
253
- }));
254
- for (const gateId of [...passedSet, ...blockedSet]) {
255
- if (conditionalSet.has(gateId)) {
256
- triggeredSet.add(gateId);
257
- }
214
+ const staleConditional = unique(catalog.conditional).filter((gateId) => !allowedSet.has(gateId));
215
+ for (const gateId of staleConditional) {
216
+ notes.push(`removed stale conditional gate "${gateId}" (conditional gate DSL removed)`);
217
+ }
218
+ const staleTriggered = unique(catalog.triggered);
219
+ for (const gateId of staleTriggered) {
220
+ notes.push(`removed stale triggered gate "${gateId}" (conditional gate DSL removed)`);
258
221
  }
259
222
  for (const gateId of [...passedSet]) {
260
223
  if (!blockedSet.has(gateId))
@@ -280,9 +243,9 @@ export function reconcileCurrentStageGateCatalog(flowState) {
280
243
  required: [...required],
281
244
  recommended: [...recommended],
282
245
  conditional: [...conditional],
283
- triggered: conditional.filter((gateId) => triggeredSet.has(gateId)),
284
- passed: [...required, ...recommended, ...conditional].filter((gateId) => passedSet.has(gateId)),
285
- blocked: [...required, ...recommended, ...conditional].filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
246
+ triggered: [],
247
+ passed: [...required, ...recommended].filter((gateId) => passedSet.has(gateId)),
248
+ blocked: [...required, ...recommended].filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
286
249
  };
287
250
  const changed = !sameStringArray(before.required, after.required) ||
288
251
  !sameStringArray(before.recommended, after.recommended) ||
@@ -12,6 +12,65 @@ function toObject(value) {
12
12
  }
13
13
  return value;
14
14
  }
15
+ function isNonEmptyString(value) {
16
+ return typeof value === "string" && value.trim().length > 0;
17
+ }
18
+ function isPositiveNumber(value) {
19
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
20
+ }
21
+ function validateCursorEvent(eventName, eventEntries, errors) {
22
+ for (let index = 0; index < eventEntries.length; index += 1) {
23
+ const rawEntry = eventEntries[index];
24
+ const entry = toObject(rawEntry);
25
+ if (!entry) {
26
+ errors.push(`hooks.${eventName}[${index}] must be an object`);
27
+ continue;
28
+ }
29
+ if (!isNonEmptyString(entry.command)) {
30
+ errors.push(`hooks.${eventName}[${index}].command must be a non-empty string`);
31
+ }
32
+ if (entry.matcher !== undefined && typeof entry.matcher !== "string") {
33
+ errors.push(`hooks.${eventName}[${index}].matcher must be a string when present`);
34
+ }
35
+ if (entry.timeout !== undefined && !isPositiveNumber(entry.timeout)) {
36
+ errors.push(`hooks.${eventName}[${index}].timeout must be a positive number when present`);
37
+ }
38
+ }
39
+ }
40
+ function validateClaudeLikeEvent(eventName, eventEntries, errors) {
41
+ for (let index = 0; index < eventEntries.length; index += 1) {
42
+ const rawEntry = eventEntries[index];
43
+ const entry = toObject(rawEntry);
44
+ if (!entry) {
45
+ errors.push(`hooks.${eventName}[${index}] must be an object`);
46
+ continue;
47
+ }
48
+ if (entry.matcher !== undefined && typeof entry.matcher !== "string") {
49
+ errors.push(`hooks.${eventName}[${index}].matcher must be a string when present`);
50
+ }
51
+ if (!Array.isArray(entry.hooks) || entry.hooks.length === 0) {
52
+ errors.push(`hooks.${eventName}[${index}].hooks must be a non-empty array`);
53
+ continue;
54
+ }
55
+ for (let hookIndex = 0; hookIndex < entry.hooks.length; hookIndex += 1) {
56
+ const rawHook = entry.hooks[hookIndex];
57
+ const hook = toObject(rawHook);
58
+ if (!hook) {
59
+ errors.push(`hooks.${eventName}[${index}].hooks[${hookIndex}] must be an object`);
60
+ continue;
61
+ }
62
+ if (hook.type !== "command") {
63
+ errors.push(`hooks.${eventName}[${index}].hooks[${hookIndex}].type must be "command"`);
64
+ }
65
+ if (!isNonEmptyString(hook.command)) {
66
+ errors.push(`hooks.${eventName}[${index}].hooks[${hookIndex}].command must be a non-empty string`);
67
+ }
68
+ if (hook.timeout !== undefined && !isPositiveNumber(hook.timeout)) {
69
+ errors.push(`hooks.${eventName}[${index}].hooks[${hookIndex}].timeout must be a positive number when present`);
70
+ }
71
+ }
72
+ }
73
+ }
15
74
  export function validateHookDocument(harness, document) {
16
75
  const descriptor = SCHEMA_MAP[harness];
17
76
  const root = toObject(document);
@@ -35,6 +94,13 @@ export function validateHookDocument(harness, document) {
35
94
  const eventValue = hooks[eventName];
36
95
  if (!Array.isArray(eventValue) || eventValue.length === 0) {
37
96
  errors.push(`missing required event array "${eventName}"`);
97
+ continue;
98
+ }
99
+ if (harness === "cursor") {
100
+ validateCursorEvent(eventName, eventValue, errors);
101
+ }
102
+ else {
103
+ validateClaudeLikeEvent(eventName, eventValue, errors);
38
104
  }
39
105
  }
40
106
  }
@@ -7,6 +7,7 @@
7
7
  "SessionStart",
8
8
  "PreToolUse",
9
9
  "PostToolUse",
10
- "Stop"
10
+ "Stop",
11
+ "PreCompact"
11
12
  ]
12
13
  }
package/dist/runs.js CHANGED
@@ -374,6 +374,19 @@ async function uniqueArchiveId(projectRoot, baseId) {
374
374
  function retroArtifactPath(projectRoot) {
375
375
  return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
376
376
  }
377
+ function parseIsoTimestamp(value) {
378
+ if (!value || value.trim().length === 0)
379
+ return null;
380
+ const parsed = Date.parse(value);
381
+ return Number.isFinite(parsed) ? parsed : null;
382
+ }
383
+ function inInclusiveWindow(timestamp, windowStartMs, windowEndMs) {
384
+ if (windowStartMs !== null && timestamp < windowStartMs)
385
+ return false;
386
+ if (windowEndMs !== null && timestamp > windowEndMs)
387
+ return false;
388
+ return true;
389
+ }
377
390
  async function evaluateRetroGate(projectRoot, state) {
378
391
  const required = state.completedStages.includes("ship");
379
392
  const artifactFile = retroArtifactPath(projectRoot);
@@ -387,11 +400,15 @@ async function evaluateRetroGate(projectRoot, state) {
387
400
  hasRetroArtifact = false;
388
401
  }
389
402
  }
403
+ let compoundEntries = state.retro.compoundEntries;
404
+ const windowStartMs = parseIsoTimestamp(state.closeout.retroDraftedAt);
405
+ const windowEndMs = parseIsoTimestamp(state.closeout.retroAcceptedAt) ?? parseIsoTimestamp(state.retro.completedAt);
406
+ const shouldFallbackScan = compoundEntries <= 0 && (windowStartMs !== null || windowEndMs !== null);
390
407
  const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
391
- let compoundEntries = 0;
392
- if (await exists(knowledgeFile)) {
408
+ if (shouldFallbackScan && (await exists(knowledgeFile))) {
393
409
  try {
394
410
  const raw = await fs.readFile(knowledgeFile, "utf8");
411
+ compoundEntries = 0;
395
412
  for (const line of raw.split(/\r?\n/)) {
396
413
  const trimmed = line.trim();
397
414
  if (!trimmed)
@@ -401,6 +418,10 @@ async function evaluateRetroGate(projectRoot, state) {
401
418
  if (parsed.type !== "compound") {
402
419
  continue;
403
420
  }
421
+ const created = typeof parsed.created === "string" ? parseIsoTimestamp(parsed.created) : null;
422
+ if (created === null || !inInclusiveWindow(created, windowStartMs, windowEndMs)) {
423
+ continue;
424
+ }
404
425
  const source = typeof parsed.source === "string"
405
426
  ? parsed.source.trim().toLowerCase()
406
427
  : null;
@@ -580,7 +601,7 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
580
601
  typeof sourceState.closeout.retroSkipReason === "string" &&
581
602
  sourceState.closeout.retroSkipReason.trim().length > 0;
582
603
  const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
583
- if (shipCompleted && !readyForArchive && !skipRetro && !retroSkippedInCloseout) {
604
+ if (shipCompleted && !readyForArchive && !skipRetro) {
584
605
  throw new Error("Archive blocked: closeout is not ready_to_archive. " +
585
606
  "Resume /cc-next until closeout reaches ready_to_archive, " +
586
607
  "or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.1",
3
+ "version": "0.46.2",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {