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.
- package/dist/artifact-linter.d.ts +2 -1
- package/dist/artifact-linter.js +18 -9
- package/dist/content/harnesses-doc.js +1 -3
- package/dist/content/ideate-command.d.ts +0 -7
- package/dist/content/ideate-command.js +0 -7
- package/dist/content/learnings.d.ts +3 -2
- package/dist/content/learnings.js +8 -5
- package/dist/content/retro-command.js +2 -2
- package/dist/content/stage-schema.d.ts +0 -3
- package/dist/content/stage-schema.js +1 -12
- package/dist/content/stages/brainstorm.js +3 -5
- package/dist/content/stages/design.js +7 -9
- package/dist/content/stages/plan.js +1 -3
- package/dist/content/stages/review.js +4 -10
- package/dist/content/stages/schema-types.d.ts +3 -16
- package/dist/content/stages/scope.js +8 -11
- package/dist/content/stages/ship.js +1 -4
- package/dist/content/stages/spec.js +1 -3
- package/dist/content/stages/tdd.js +2 -6
- package/dist/content/start-command.js +5 -5
- package/dist/content/subagents.js +0 -140
- package/dist/delegation.d.ts +2 -5
- package/dist/delegation.js +1 -1
- package/dist/doctor.js +48 -0
- package/dist/flow-state.js +2 -2
- package/dist/gate-evidence.js +23 -60
- package/dist/hook-schema.js +66 -0
- package/dist/hook-schemas/claude-hooks.v1.json +2 -1
- package/dist/internal/advance-stage.js +15 -6
- package/dist/knowledge-store.d.ts +4 -0
- package/dist/knowledge-store.js +21 -2
- package/dist/runs.js +44 -5
- package/dist/tdd-cycle.js +16 -0
- package/package.json +1 -1
|
@@ -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() {
|
package/dist/delegation.d.ts
CHANGED
|
@@ -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"
|
|
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;
|
package/dist/delegation.js
CHANGED
|
@@ -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"
|
|
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)),
|
package/dist/flow-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { COMMAND_FILE_ORDER } from "./constants.js";
|
|
2
|
-
import { buildTransitionRules, orderedStageSchemas,
|
|
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:
|
|
66
|
+
conditional: [],
|
|
67
67
|
triggered: [],
|
|
68
68
|
passed: [],
|
|
69
69
|
blocked: []
|
package/dist/gate-evidence.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
76
|
-
issues.push(`gate "${gateId}"
|
|
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
|
-
|
|
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 = [
|
|
137
|
-
const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId)
|
|
138
|
-
const complete = missingRequired.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:
|
|
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 = [
|
|
187
|
-
const blockingBlocked = catalog.blocked.filter((gateId) => required.includes(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 =
|
|
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
|
|
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
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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:
|
|
284
|
-
passed: [...required, ...recommended
|
|
285
|
-
blocked: [...required, ...recommended
|
|
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) ||
|
package/dist/hook-schema.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 {
|
package/dist/knowledge-store.js
CHANGED
|
@@ -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
|
-
|
|
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();
|