cclaw-cli 0.46.0 → 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.
@@ -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
  }
@@ -380,15 +380,24 @@ async function runAdvanceStage(projectRoot, args, io) {
380
380
  ...nextPassed.filter((gateId) => conditional.has(gateId)),
381
381
  ...nextBlocked.filter((gateId) => conditional.has(gateId))
382
382
  ]);
383
+ const missingGuardEvidence = nextPassed.filter((gateId) => {
384
+ const existing = flowState.guardEvidence[gateId];
385
+ if (typeof existing === "string" && existing.trim().length > 0) {
386
+ return false;
387
+ }
388
+ const provided = args.evidenceByGate[gateId];
389
+ return !(typeof provided === "string" && provided.trim().length > 0);
390
+ });
391
+ if (missingGuardEvidence.length > 0) {
392
+ io.stderr.write(`cclaw internal advance-stage: missing --evidence-json entries for passed gates: ${missingGuardEvidence.join(", ")}.\n`);
393
+ return 1;
394
+ }
383
395
  const nextGuardEvidence = { ...flowState.guardEvidence };
384
396
  for (const gateId of nextPassed) {
385
- const existing = nextGuardEvidence[gateId];
386
- if (typeof existing === "string" && existing.trim().length > 0)
387
- continue;
388
397
  const provided = args.evidenceByGate[gateId];
389
- nextGuardEvidence[gateId] = provided && provided.trim().length > 0
390
- ? provided.trim()
391
- : `stage-complete helper auto-evidence for ${gateId} @ ${new Date().toISOString()} (${schema.artifactFile})`;
398
+ if (typeof provided === "string" && provided.trim().length > 0) {
399
+ nextGuardEvidence[gateId] = provided.trim();
400
+ }
392
401
  }
393
402
  const nextStageCatalog = {
394
403
  required: [...catalog.required],
@@ -3,6 +3,7 @@ export type KnowledgeEntryType = "rule" | "pattern" | "lesson" | "compound";
3
3
  export type KnowledgeEntryConfidence = "high" | "medium" | "low";
4
4
  export type KnowledgeEntryUniversality = "project" | "personal" | "universal";
5
5
  export type KnowledgeEntryMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
6
+ export type KnowledgeEntrySource = "stage" | "retro" | "compound" | "ideate" | "manual";
6
7
  export interface KnowledgeEntry {
7
8
  type: KnowledgeEntryType;
8
9
  trigger: string;
@@ -19,6 +20,7 @@ export interface KnowledgeEntry {
19
20
  first_seen_ts: string;
20
21
  last_seen_ts: string;
21
22
  project: string | null;
23
+ source?: KnowledgeEntrySource | null;
22
24
  }
23
25
  export interface KnowledgeSeedEntry {
24
26
  type: KnowledgeEntryType;
@@ -36,12 +38,14 @@ export interface KnowledgeSeedEntry {
36
38
  first_seen_ts?: string;
37
39
  last_seen_ts?: string;
38
40
  project?: string | null;
41
+ source?: KnowledgeEntrySource | null;
39
42
  }
40
43
  export interface AppendKnowledgeDefaults {
41
44
  stage?: FlowStage | null;
42
45
  originStage?: FlowStage | null;
43
46
  originFeature?: string | null;
44
47
  project?: string | null;
48
+ source?: KnowledgeEntrySource | null;
45
49
  nowIso?: string;
46
50
  }
47
51
  export interface AppendKnowledgeResult {
@@ -7,6 +7,13 @@ const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
7
7
  const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
8
8
  const KNOWLEDGE_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
9
9
  const KNOWLEDGE_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
10
+ const KNOWLEDGE_SOURCE_SET = new Set([
11
+ "stage",
12
+ "retro",
13
+ "compound",
14
+ "ideate",
15
+ "manual"
16
+ ]);
10
17
  const FLOW_STAGE_SET = new Set(FLOW_STAGES);
11
18
  const KNOWLEDGE_REQUIRED_KEYS = [
12
19
  "type",
@@ -26,6 +33,7 @@ const KNOWLEDGE_REQUIRED_KEYS = [
26
33
  "project"
27
34
  ];
28
35
  const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
36
+ KNOWLEDGE_ALLOWED_KEYS.add("source");
29
37
  function knowledgePath(projectRoot) {
30
38
  return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
31
39
  }
@@ -51,7 +59,8 @@ function dedupeKey(entry) {
51
59
  entry.origin_stage ?? "null",
52
60
  entry.origin_feature === null ? "null" : normalizeText(entry.origin_feature),
53
61
  entry.universality,
54
- entry.project === null ? "null" : normalizeText(entry.project)
62
+ entry.project === null ? "null" : normalizeText(entry.project),
63
+ entry.source === undefined || entry.source === null ? "null" : entry.source
55
64
  ].join("|");
56
65
  }
57
66
  function isIsoUtcTimestamp(value) {
@@ -123,13 +132,19 @@ export function validateKnowledgeEntry(entry) {
123
132
  if (!isNullableString(obj.project)) {
124
133
  errors.push("project must be string or null.");
125
134
  }
135
+ if (obj.source !== undefined &&
136
+ obj.source !== null &&
137
+ (typeof obj.source !== "string" || !KNOWLEDGE_SOURCE_SET.has(obj.source))) {
138
+ errors.push("source must be one of: stage, retro, compound, ideate, manual, or null.");
139
+ }
126
140
  return { ok: errors.length === 0, errors };
127
141
  }
128
142
  export function materializeKnowledgeEntry(seed, defaults = {}) {
129
143
  const now = normalizeUtcIso(defaults.nowIso ?? nowUtcIso());
130
144
  const stage = seed.stage ?? defaults.stage ?? null;
131
145
  const originStage = seed.origin_stage ?? defaults.originStage ?? stage ?? null;
132
- return {
146
+ const source = seed.source ?? defaults.source ?? null;
147
+ const entry = {
133
148
  type: seed.type,
134
149
  trigger: seed.trigger.trim(),
135
150
  action: seed.action.trim(),
@@ -146,6 +161,10 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
146
161
  last_seen_ts: normalizeUtcIso(seed.last_seen_ts ?? now),
147
162
  project: seed.project ?? defaults.project ?? null
148
163
  };
164
+ if (source !== null) {
165
+ entry.source = source;
166
+ }
167
+ return entry;
149
168
  }
150
169
  async function readExistingKnowledgeKeys(filePath) {
151
170
  const keys = new Set();