cclaw-cli 0.48.4 → 0.48.6
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.js +32 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +44 -5
- package/dist/content/hooks.js +111 -42
- package/dist/content/ideate-command.js +11 -0
- package/dist/content/iron-laws.d.ts +134 -0
- package/dist/content/iron-laws.js +182 -0
- package/dist/content/meta-skill.js +1 -0
- package/dist/content/next-command.js +12 -0
- package/dist/content/observe.js +189 -4
- package/dist/content/ops-command.js +11 -0
- package/dist/content/session-hooks.js +3 -1
- package/dist/content/stage-schema.d.ts +16 -0
- package/dist/content/stage-schema.js +82 -5
- package/dist/content/stages/review.js +2 -2
- package/dist/content/stages/tdd.js +7 -7
- package/dist/content/start-command.js +12 -0
- package/dist/content/subagents.js +26 -0
- package/dist/content/view-command.js +11 -0
- package/dist/eval/agents/workflow.js +22 -2
- package/dist/gate-evidence.js +30 -1
- package/dist/harness-adapters.js +3 -0
- package/dist/install.js +8 -0
- package/dist/internal/advance-stage.js +10 -2
- package/dist/internal/envelope-validate.d.ts +7 -0
- package/dist/internal/envelope-validate.js +66 -0
- package/dist/internal/knowledge-digest.d.ts +7 -0
- package/dist/internal/knowledge-digest.js +93 -0
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +95 -0
- package/dist/tdd-cycle.d.ts +7 -0
- package/dist/tdd-cycle.js +29 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -22,13 +22,13 @@ export const TDD = {
|
|
|
22
22
|
checklist: [
|
|
23
23
|
"Select plan slice — pick one task from the plan. Do not batch multiple tasks.",
|
|
24
24
|
"Map to acceptance criterion — identify the specific spec criterion this test proves.",
|
|
25
|
-
"Dispatch mandatory `test-author`
|
|
25
|
+
"Dispatch mandatory `tdd-red` execution (or `test-author` in TEST_RED_ONLY mode) — produce failing behavior tests and RED evidence only (no production edits). Set `CCLAW_ACTIVE_AGENT=tdd-red` when supported.",
|
|
26
26
|
"RED: Capture failure output — copy the exact failure output as RED evidence. Record in artifact.",
|
|
27
|
-
"Dispatch `test-author`
|
|
27
|
+
"Dispatch `tdd-green` execution (or `test-author` in BUILD_GREEN_REFACTOR mode) — minimal implementation + full-suite GREEN. Set `CCLAW_ACTIVE_AGENT=tdd-green` when supported.",
|
|
28
28
|
"GREEN: Run full suite — execute ALL tests, not just the ones you wrote. The full suite must be GREEN.",
|
|
29
29
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
30
30
|
"Run verification-before-completion discipline for the slice — capture a fresh test command, commit SHA, and explicit PASS/FAIL status before completion claims.",
|
|
31
|
-
"REFACTOR:
|
|
31
|
+
"REFACTOR: Dispatch `tdd-refactor` execution (or dedicated refactor mode) to improve code quality without behavior changes. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when supported.",
|
|
32
32
|
"Record evidence — capture RED failure, GREEN output, and REFACTOR notes in the TDD artifact.",
|
|
33
33
|
"Annotate traceability — link to plan task ID and spec criterion.",
|
|
34
34
|
"Per-Slice Review (conditional) — if `.cclaw/config.yaml::sliceReview.enabled` is true and the slice meets any trigger (touchCount >= filesChangedThreshold, touchPaths match touchTriggers, or highRisk=true), append a `## Per-Slice Review` entry for this slice before moving on (see the dedicated section below).",
|
|
@@ -36,7 +36,7 @@ export const TDD = {
|
|
|
36
36
|
],
|
|
37
37
|
interactionProtocol: [
|
|
38
38
|
"Pick one planned slice at a time.",
|
|
39
|
-
"Controller owns orchestration; execution runs through
|
|
39
|
+
"Controller owns orchestration; execution runs through phase-specific delegation (`tdd-red` -> `tdd-green` -> `tdd-refactor`) or equivalent `test-author` modes.",
|
|
40
40
|
"Write behavior-focused tests before changing implementation (RED).",
|
|
41
41
|
"Capture and store failing output as RED evidence.",
|
|
42
42
|
"Apply minimal change to satisfy RED tests (GREEN).",
|
|
@@ -49,12 +49,12 @@ export const TDD = {
|
|
|
49
49
|
],
|
|
50
50
|
process: [
|
|
51
51
|
"Select slice and map to acceptance criterion.",
|
|
52
|
-
"Dispatch `test-author`
|
|
52
|
+
"Dispatch `tdd-red` (or `test-author` TEST_RED_ONLY mode) and produce failing test(s) for expected reason (RED).",
|
|
53
53
|
"Run tests and capture failure output.",
|
|
54
|
-
"Dispatch `test-author`
|
|
54
|
+
"Dispatch `tdd-green` (or `test-author` BUILD_GREEN_REFACTOR mode) and implement smallest change needed for GREEN.",
|
|
55
55
|
"Run full tests and build checks.",
|
|
56
56
|
"Run a fresh verification-before-completion check and capture command + commit SHA + PASS/FAIL in guard evidence.",
|
|
57
|
-
"
|
|
57
|
+
"Dispatch `tdd-refactor` pass preserving behavior.",
|
|
58
58
|
"Record RED, GREEN, and REFACTOR evidence in artifact.",
|
|
59
59
|
"Annotate traceability to plan task and spec criterion; on `sliceReview` triggers, append a Per-Slice Review entry before closing the slice."
|
|
60
60
|
],
|
|
@@ -100,6 +100,18 @@ If during any stage the agent discovers evidence that contradicts the initial Ph
|
|
|
100
100
|
2. If flow state is missing → run \`cclaw init\` guidance and stop.
|
|
101
101
|
3. Behave exactly like \`/cc-next\`: check current stage gates, resume if incomplete, advance if complete.
|
|
102
102
|
|
|
103
|
+
## Headless mode
|
|
104
|
+
|
|
105
|
+
When called by another skill or subagent in machine mode, emit exactly one
|
|
106
|
+
JSON envelope (no prose) and stop:
|
|
107
|
+
|
|
108
|
+
\`\`\`json
|
|
109
|
+
{"version":"1","kind":"stage-output","stage":"brainstorm","payload":{"command":"/cc","track":"standard","action":"start_or_resume"},"emittedAt":"<ISO-8601>"}
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
Validate envelopes with:
|
|
113
|
+
\`cclaw internal envelope-validate --stdin\`
|
|
114
|
+
|
|
103
115
|
## Primary skill
|
|
104
116
|
|
|
105
117
|
**${RUNTIME_ROOT}/skills/${START_SKILL_FOLDER}/SKILL.md**
|
|
@@ -39,6 +39,32 @@ For cclaw flow stages, machine-only specialist work should auto-dispatch without
|
|
|
39
39
|
|
|
40
40
|
Human input remains mandatory only at explicit approval gates (plan approval, user challenge resolution, release finalization mode).
|
|
41
41
|
|
|
42
|
+
### Review parallel fan-out protocol
|
|
43
|
+
|
|
44
|
+
In review stage, prefer a fixed six-pass fan-out before reconciliation:
|
|
45
|
+
|
|
46
|
+
1. \`review-spec\` (Layer 1)
|
|
47
|
+
2. \`review-correctness\` (Layer 2a)
|
|
48
|
+
3. \`review-security\` (Layer 2b)
|
|
49
|
+
4. \`review-performance\` (Layer 2c)
|
|
50
|
+
5. \`review-architecture\` (Layer 2d)
|
|
51
|
+
6. \`review-external-safety\` (Layer 2e)
|
|
52
|
+
|
|
53
|
+
Dispatch these in parallel where the harness supports isolated workers, then run
|
|
54
|
+
one reconciliation pass that merges findings into \`.cclaw/artifacts/07-review-army.json\`
|
|
55
|
+
with explicit source tags per finding.
|
|
56
|
+
|
|
57
|
+
### TDD phase fan-out protocol
|
|
58
|
+
|
|
59
|
+
Treat RED, GREEN, and REFACTOR as separate delegated intents:
|
|
60
|
+
|
|
61
|
+
- \`tdd-red\`: tests only, no production writes
|
|
62
|
+
- \`tdd-green\`: minimal production implementation, no new RED tests
|
|
63
|
+
- \`tdd-refactor\`: cleanup only after GREEN is proven
|
|
64
|
+
|
|
65
|
+
Set \`CCLAW_ACTIVE_AGENT\` to the active phase name when possible so workflow-guard
|
|
66
|
+
can enforce phase-appropriate write boundaries.
|
|
67
|
+
|
|
42
68
|
## Model & Harness Routing Notes
|
|
43
69
|
|
|
44
70
|
### Harness routing
|
|
@@ -27,6 +27,17 @@ Subcommands:
|
|
|
27
27
|
- \`diff\` -> load \`${RUNTIME_ROOT}/commands/diff.md\` + \`${RUNTIME_ROOT}/skills/flow-diff/SKILL.md\`
|
|
28
28
|
3. Unknown subcommand -> print supported values and stop.
|
|
29
29
|
|
|
30
|
+
## Headless mode
|
|
31
|
+
|
|
32
|
+
For machine orchestration, emit one JSON envelope:
|
|
33
|
+
|
|
34
|
+
\`\`\`json
|
|
35
|
+
{"version":"1","kind":"stage-output","stage":"review","payload":{"command":"/cc-view","subcommand":"status","summary":"<short>"},"emittedAt":"<ISO-8601>"}
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
Validate envelopes with:
|
|
39
|
+
\`cclaw internal envelope-validate --stdin\`
|
|
40
|
+
|
|
30
41
|
## Primary skill
|
|
31
42
|
|
|
32
43
|
**${RUNTIME_ROOT}/skills/${VIEW_SKILL_FOLDER}/SKILL.md**
|
|
@@ -29,9 +29,17 @@ import fs from "node:fs/promises";
|
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { createSandbox } from "../sandbox.js";
|
|
31
31
|
import { loadStageSkill } from "./single-shot.js";
|
|
32
|
-
import { runWithTools } from "./with-tools.js";
|
|
32
|
+
import { MaxTurnsExceededError, runWithTools } from "./with-tools.js";
|
|
33
33
|
const STAGES_SUBDIR = "stages";
|
|
34
34
|
const ARTIFACT_CANDIDATES = ["artifact.md", "artifact.txt", "ARTIFACT.md"];
|
|
35
|
+
const DEFAULT_WORKFLOW_MAX_TOTAL_TURNS = 40;
|
|
36
|
+
const DEFAULT_STAGE_TURN_CAP = 8;
|
|
37
|
+
function clampPositive(value, fallback) {
|
|
38
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
35
43
|
export async function runWorkflow(input) {
|
|
36
44
|
const { workflow, config, projectRoot, client } = input;
|
|
37
45
|
const sandboxFactory = input.createSandboxFn ?? createSandbox;
|
|
@@ -43,9 +51,17 @@ export async function runWorkflow(input) {
|
|
|
43
51
|
const artifacts = new Map();
|
|
44
52
|
let totalUsageUsd = 0;
|
|
45
53
|
let totalDurationMs = 0;
|
|
54
|
+
let totalTurns = 0;
|
|
55
|
+
const workflowTurnBudget = clampPositive(config.workflowMaxTotalTurns, DEFAULT_WORKFLOW_MAX_TOTAL_TURNS);
|
|
46
56
|
try {
|
|
47
57
|
await fs.mkdir(await sandbox.resolve(STAGES_SUBDIR, { allowMissing: true }), { recursive: true });
|
|
48
58
|
for (const step of workflow.stages) {
|
|
59
|
+
const remainingWorkflowTurns = workflowTurnBudget - totalTurns;
|
|
60
|
+
if (remainingWorkflowTurns < 1) {
|
|
61
|
+
throw new MaxTurnsExceededError(workflowTurnBudget);
|
|
62
|
+
}
|
|
63
|
+
const perStageTurnCap = clampPositive(config.toolMaxTurns, DEFAULT_STAGE_TURN_CAP);
|
|
64
|
+
const stageTurnBudget = Math.min(perStageTurnCap, remainingWorkflowTurns);
|
|
49
65
|
input.onStageStart?.(step.name);
|
|
50
66
|
await clearArtifactFile(sandbox);
|
|
51
67
|
const priorStages = stageResults.map((r) => r.stage);
|
|
@@ -58,7 +74,10 @@ export async function runWorkflow(input) {
|
|
|
58
74
|
};
|
|
59
75
|
const result = await runWithTools({
|
|
60
76
|
caseEntry,
|
|
61
|
-
config
|
|
77
|
+
config: {
|
|
78
|
+
...config,
|
|
79
|
+
toolMaxTurns: stageTurnBudget
|
|
80
|
+
},
|
|
62
81
|
projectRoot,
|
|
63
82
|
client,
|
|
64
83
|
...(input.tools ? { tools: input.tools } : {}),
|
|
@@ -87,6 +106,7 @@ export async function runWorkflow(input) {
|
|
|
87
106
|
input.onStageEnd?.(step.name, stageResult);
|
|
88
107
|
totalUsageUsd += result.usageUsd;
|
|
89
108
|
totalDurationMs += result.durationMs;
|
|
109
|
+
totalTurns += result.toolUse.turns;
|
|
90
110
|
}
|
|
91
111
|
return {
|
|
92
112
|
caseId: workflow.id,
|
package/dist/gate-evidence.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readDelegationLedger } from "./delegation.js";
|
|
|
7
7
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
8
8
|
import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
|
|
9
9
|
import { readFlowState, writeFlowState } from "./runs.js";
|
|
10
|
+
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
10
11
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
11
12
|
import { FLOW_STAGES } from "./types.js";
|
|
12
13
|
async function currentStageArtifactExists(projectRoot, stage, track) {
|
|
@@ -200,6 +201,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
200
201
|
issues.push(`stale triggered conditional gate "${gateId}" found in stageGateCatalog.triggered for stage "${stage}" (conditional gate DSL removed).`);
|
|
201
202
|
}
|
|
202
203
|
const blockedSet = new Set(catalog.blocked);
|
|
204
|
+
const passedSet = new Set(catalog.passed);
|
|
203
205
|
for (const gateId of catalog.passed) {
|
|
204
206
|
if (!allowedSet.has(gateId)) {
|
|
205
207
|
issues.push(`passed gate "${gateId}" is not defined for stage "${stage}".`);
|
|
@@ -239,6 +241,13 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
239
241
|
if (!verdictConsistency.ok) {
|
|
240
242
|
issues.push(`review verdict inconsistency: ${verdictConsistency.errors.join("; ")}`);
|
|
241
243
|
}
|
|
244
|
+
const reviewCriticalsClaimedResolved = passedSet.has("review_criticals_resolved") || flowState.completedStages.includes("review");
|
|
245
|
+
const unresolvedCriticals = verdictConsistency.openCriticalCount > 0 || verdictConsistency.shipBlockerCount > 0;
|
|
246
|
+
if (reviewCriticalsClaimedResolved && unresolvedCriticals) {
|
|
247
|
+
issues.push(`review criticals gate blocked (review_criticals_resolved): review-army still reports ` +
|
|
248
|
+
`${verdictConsistency.openCriticalCount} open critical(s) and ` +
|
|
249
|
+
`${verdictConsistency.shipBlockerCount} ship blocker(s).`);
|
|
250
|
+
}
|
|
242
251
|
const securityAttestation = await checkReviewSecurityNoChangeAttestation(projectRoot);
|
|
243
252
|
if (!securityAttestation.ok) {
|
|
244
253
|
issues.push(`review security attestation failed: ${securityAttestation.errors.join("; ")}`);
|
|
@@ -309,9 +318,29 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
309
318
|
issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): public surface changes detected (${docsDriftDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
|
|
310
319
|
}
|
|
311
320
|
}
|
|
321
|
+
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
322
|
+
if (await exists(tddLogPath)) {
|
|
323
|
+
try {
|
|
324
|
+
const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
|
|
325
|
+
const parsedCycles = parseTddCycleLog(tddLogRaw);
|
|
326
|
+
const tddOrderValidation = validateTddCycleOrder(parsedCycles, {
|
|
327
|
+
runId: flowState.activeRunId
|
|
328
|
+
});
|
|
329
|
+
if (!tddOrderValidation.ok) {
|
|
330
|
+
const details = [...tddOrderValidation.issues];
|
|
331
|
+
if (tddOrderValidation.openRedSlices.length > 0) {
|
|
332
|
+
details.push(`open red slices: ${tddOrderValidation.openRedSlices.join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
issues.push(`tdd cycle order gate blocked: ${details.join("; ")}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
339
|
+
issues.push(`tdd cycle order gate blocked: unable to read tdd-cycle-log.jsonl (${reason}).`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
312
342
|
}
|
|
313
343
|
}
|
|
314
|
-
const passedSet = new Set(catalog.passed);
|
|
315
344
|
const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
|
|
316
345
|
const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
|
|
317
346
|
const missingTriggeredConditional = [];
|
package/dist/harness-adapters.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
4
|
import { CCLAW_AGENTS, agentMarkdown } from "./content/core-agents.js";
|
|
5
|
+
import { ironLawsAgentsMdBlock } from "./content/iron-laws.js";
|
|
5
6
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
6
7
|
export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
|
|
7
8
|
export const CCLAW_MARKER_END = "<!-- cclaw-end -->";
|
|
@@ -200,6 +201,8 @@ Before responding to a coding request:
|
|
|
200
201
|
2. Use \`/cc\` to start or \`/cc-next\` to continue the flow.
|
|
201
202
|
3. If no stage applies, respond normally.
|
|
202
203
|
|
|
204
|
+
${ironLawsAgentsMdBlock()}
|
|
205
|
+
|
|
203
206
|
### Task Classification (before \`/cc\`)
|
|
204
207
|
|
|
205
208
|
| Class | Examples | Route |
|
package/dist/install.js
CHANGED
|
@@ -23,6 +23,7 @@ import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/a
|
|
|
23
23
|
import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
|
|
24
24
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
25
25
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
26
|
+
import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
|
|
26
27
|
import { sessionStartScript, stopCheckpointScript, runHookDispatcherScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
27
28
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
28
29
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
@@ -255,6 +256,7 @@ async function writeSkills(projectRoot, config) {
|
|
|
255
256
|
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
256
257
|
await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
|
|
257
258
|
await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
|
|
259
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "iron-laws", "SKILL.md"), ironLawsSkillMarkdown());
|
|
258
260
|
await writeFileSafe(runtimePath(projectRoot, "skills", META_SKILL_NAME, "SKILL.md"), usingCclawSkillMarkdown());
|
|
259
261
|
await writeFileSafe(runtimePath(projectRoot, "references", "protocols", "decision.md"), decisionProtocolMarkdown());
|
|
260
262
|
await writeFileSafe(runtimePath(projectRoot, "references", "protocols", "completion.md"), completionProtocolMarkdown());
|
|
@@ -621,7 +623,13 @@ async function writeMergedHookJson(projectRoot, hookFilePath, generatedJson) {
|
|
|
621
623
|
async function writeHooks(projectRoot, config) {
|
|
622
624
|
const harnesses = config.harnesses;
|
|
623
625
|
const hooksDir = runtimePath(projectRoot, "hooks");
|
|
626
|
+
const stateDir = runtimePath(projectRoot, "state");
|
|
624
627
|
await ensureDir(hooksDir);
|
|
628
|
+
await ensureDir(stateDir);
|
|
629
|
+
await writeFileSafe(runtimePath(projectRoot, "state", "iron-laws.json"), `${JSON.stringify(ironLawRuntimeDocument({
|
|
630
|
+
mode: config.ironLaws?.mode,
|
|
631
|
+
strictLaws: config.ironLaws?.strictLaws
|
|
632
|
+
}), null, 2)}\n`);
|
|
625
633
|
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
626
634
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
627
635
|
await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookDispatcherScript());
|
|
@@ -10,6 +10,8 @@ import { getAvailableTransitions, getTransitionGuards, isFlowTrack } from "../fl
|
|
|
10
10
|
import { appendKnowledge } from "../knowledge-store.js";
|
|
11
11
|
import { readFlowState, writeFlowState } from "../runs.js";
|
|
12
12
|
import { FLOW_STAGES } from "../types.js";
|
|
13
|
+
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
14
|
+
import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
|
|
13
15
|
function unique(values) {
|
|
14
16
|
return [...new Set(values)];
|
|
15
17
|
}
|
|
@@ -621,7 +623,7 @@ async function runVerifyCurrentState(projectRoot, args, io) {
|
|
|
621
623
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
622
624
|
const [subcommand, ...tokens] = argv;
|
|
623
625
|
if (!subcommand) {
|
|
624
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state\n");
|
|
626
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate\n");
|
|
625
627
|
return 1;
|
|
626
628
|
}
|
|
627
629
|
try {
|
|
@@ -634,7 +636,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
634
636
|
if (subcommand === "verify-current-state") {
|
|
635
637
|
return await runVerifyCurrentState(projectRoot, parseVerifyCurrentStateArgs(tokens), io);
|
|
636
638
|
}
|
|
637
|
-
|
|
639
|
+
if (subcommand === "knowledge-digest") {
|
|
640
|
+
return await runKnowledgeDigestCommand(projectRoot, tokens, io);
|
|
641
|
+
}
|
|
642
|
+
if (subcommand === "envelope-validate") {
|
|
643
|
+
return await runEnvelopeValidateCommand(projectRoot, tokens, io);
|
|
644
|
+
}
|
|
645
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate\n`);
|
|
638
646
|
return 1;
|
|
639
647
|
}
|
|
640
648
|
catch (err) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { parseSkillEnvelope, validateSkillEnvelope } from "../content/stage-schema.js";
|
|
3
|
+
function parseArgs(tokens) {
|
|
4
|
+
const args = { stdin: false, quiet: false };
|
|
5
|
+
for (const token of tokens) {
|
|
6
|
+
if (token === "--stdin") {
|
|
7
|
+
args.stdin = true;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (token === "--quiet") {
|
|
11
|
+
args.quiet = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (token.startsWith("--json=")) {
|
|
15
|
+
args.json = token.replace("--json=", "");
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (token.startsWith("--file=")) {
|
|
19
|
+
args.file = token.replace("--file=", "");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Unknown flag for envelope-validate: ${token}`);
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
async function readStdin() {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
for await (const chunk of process.stdin) {
|
|
29
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
30
|
+
}
|
|
31
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
export async function runEnvelopeValidateCommand(_projectRoot, tokens, io) {
|
|
34
|
+
const args = parseArgs(tokens);
|
|
35
|
+
let raw = "";
|
|
36
|
+
if (args.json !== undefined) {
|
|
37
|
+
raw = args.json;
|
|
38
|
+
}
|
|
39
|
+
else if (args.file !== undefined) {
|
|
40
|
+
raw = await fs.readFile(args.file, "utf8");
|
|
41
|
+
}
|
|
42
|
+
else if (args.stdin) {
|
|
43
|
+
raw = await readStdin();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new Error("Provide one source: --json=<payload> | --file=<path> | --stdin");
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseSkillEnvelope(raw);
|
|
49
|
+
if (parsed) {
|
|
50
|
+
if (!args.quiet) {
|
|
51
|
+
io.stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
let candidate;
|
|
56
|
+
try {
|
|
57
|
+
candidate = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
io.stderr.write(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const validation = validateSkillEnvelope(candidate);
|
|
64
|
+
io.stderr.write(`Invalid envelope: ${validation.errors.join(" ")}\n`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { selectRelevantLearnings } from "../knowledge-store.js";
|
|
2
|
+
import { FLOW_STAGES } from "../types.js";
|
|
3
|
+
function parseCsv(raw) {
|
|
4
|
+
return raw
|
|
5
|
+
.split(",")
|
|
6
|
+
.map((value) => value.trim())
|
|
7
|
+
.filter((value) => value.length > 0);
|
|
8
|
+
}
|
|
9
|
+
function parseKnowledgeDigestArgs(tokens) {
|
|
10
|
+
const args = {
|
|
11
|
+
diffFiles: [],
|
|
12
|
+
openGates: [],
|
|
13
|
+
limit: 8,
|
|
14
|
+
format: "markdown"
|
|
15
|
+
};
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
if (!token.startsWith("--")) {
|
|
18
|
+
throw new Error(`Unknown positional token for knowledge-digest: ${token}`);
|
|
19
|
+
}
|
|
20
|
+
if (token.startsWith("--stage=")) {
|
|
21
|
+
const value = token.replace("--stage=", "").trim();
|
|
22
|
+
if (!value)
|
|
23
|
+
continue;
|
|
24
|
+
if (!FLOW_STAGES.includes(value)) {
|
|
25
|
+
throw new Error(`--stage must be one of: ${FLOW_STAGES.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
args.stage = value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (token.startsWith("--branch=")) {
|
|
31
|
+
const value = token.replace("--branch=", "").trim();
|
|
32
|
+
if (value.length > 0) {
|
|
33
|
+
args.branch = value;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token.startsWith("--diff-files=")) {
|
|
38
|
+
args.diffFiles.push(...parseCsv(token.replace("--diff-files=", "")));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (token.startsWith("--open-gates=")) {
|
|
42
|
+
args.openGates.push(...parseCsv(token.replace("--open-gates=", "")));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token.startsWith("--limit=")) {
|
|
46
|
+
const raw = token.replace("--limit=", "").trim();
|
|
47
|
+
const value = Number.parseInt(raw, 10);
|
|
48
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
49
|
+
throw new Error("--limit must be a positive integer.");
|
|
50
|
+
}
|
|
51
|
+
args.limit = value;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (token === "--json") {
|
|
55
|
+
args.format = "json";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token === "--markdown") {
|
|
59
|
+
args.format = "markdown";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unknown flag for knowledge-digest: ${token}`);
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
function markdownDigest(rows) {
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return "(no relevant learnings)";
|
|
69
|
+
}
|
|
70
|
+
return rows
|
|
71
|
+
.map((entry) => {
|
|
72
|
+
const stage = entry.stage ?? "global";
|
|
73
|
+
const domain = entry.domain ?? "general";
|
|
74
|
+
return `- [${entry.confidence} | ${stage} | ${domain}] ${entry.trigger} -> ${entry.action}`;
|
|
75
|
+
})
|
|
76
|
+
.join("\n");
|
|
77
|
+
}
|
|
78
|
+
export async function runKnowledgeDigestCommand(projectRoot, tokens, io) {
|
|
79
|
+
const args = parseKnowledgeDigestArgs(tokens);
|
|
80
|
+
const rows = await selectRelevantLearnings(projectRoot, {
|
|
81
|
+
stage: args.stage,
|
|
82
|
+
branch: args.branch,
|
|
83
|
+
diffFiles: args.diffFiles,
|
|
84
|
+
openGates: args.openGates,
|
|
85
|
+
limit: args.limit
|
|
86
|
+
});
|
|
87
|
+
if (args.format === "json") {
|
|
88
|
+
io.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
io.stdout.write(`${markdownDigest(rows)}\n`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
@@ -65,6 +65,13 @@ export interface ReadKnowledgeResult {
|
|
|
65
65
|
entries: KnowledgeEntry[];
|
|
66
66
|
malformedLines: number;
|
|
67
67
|
}
|
|
68
|
+
export interface SelectRelevantLearningsOptions {
|
|
69
|
+
stage?: FlowStage | null;
|
|
70
|
+
branch?: string | null;
|
|
71
|
+
diffFiles?: string[];
|
|
72
|
+
openGates?: string[];
|
|
73
|
+
limit?: number;
|
|
74
|
+
}
|
|
68
75
|
export declare function validateKnowledgeEntry(entry: unknown): {
|
|
69
76
|
ok: boolean;
|
|
70
77
|
errors: string[];
|
|
@@ -72,3 +79,4 @@ export declare function validateKnowledgeEntry(entry: unknown): {
|
|
|
72
79
|
export declare function materializeKnowledgeEntry(seed: KnowledgeSeedEntry, defaults?: AppendKnowledgeDefaults): KnowledgeEntry;
|
|
73
80
|
export declare function readKnowledgeSafely(projectRoot: string, options?: ReadKnowledgeOptions): Promise<ReadKnowledgeResult>;
|
|
74
81
|
export declare function appendKnowledge(projectRoot: string, seeds: KnowledgeSeedEntry[], defaults?: AppendKnowledgeDefaults): Promise<AppendKnowledgeResult>;
|
|
82
|
+
export declare function selectRelevantLearnings(projectRoot: string, options?: SelectRelevantLearningsOptions): Promise<KnowledgeEntry[]>;
|
package/dist/knowledge-store.js
CHANGED
|
@@ -327,3 +327,98 @@ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
|
|
|
327
327
|
appendedEntries
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
|
+
function tokenizeText(value) {
|
|
331
|
+
if (!value)
|
|
332
|
+
return [];
|
|
333
|
+
return value
|
|
334
|
+
.toLowerCase()
|
|
335
|
+
.split(/[^a-z0-9]+/u)
|
|
336
|
+
.map((token) => token.trim())
|
|
337
|
+
.filter((token) => token.length >= 3);
|
|
338
|
+
}
|
|
339
|
+
function uniqueTokens(values) {
|
|
340
|
+
return [...new Set(values)];
|
|
341
|
+
}
|
|
342
|
+
function pathTokens(paths) {
|
|
343
|
+
if (!Array.isArray(paths) || paths.length === 0)
|
|
344
|
+
return [];
|
|
345
|
+
const tokens = [];
|
|
346
|
+
for (const filePath of paths) {
|
|
347
|
+
tokens.push(...tokenizeText(filePath));
|
|
348
|
+
}
|
|
349
|
+
return uniqueTokens(tokens);
|
|
350
|
+
}
|
|
351
|
+
export async function selectRelevantLearnings(projectRoot, options = {}) {
|
|
352
|
+
const { entries } = await readKnowledgeSafely(projectRoot);
|
|
353
|
+
if (entries.length === 0) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
const stage = options.stage ?? null;
|
|
357
|
+
const branchTokens = tokenizeText(options.branch ?? null);
|
|
358
|
+
const diffTokens = pathTokens(options.diffFiles);
|
|
359
|
+
const gateTokens = pathTokens(options.openGates);
|
|
360
|
+
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
|
|
361
|
+
? Math.floor(options.limit)
|
|
362
|
+
: 8;
|
|
363
|
+
const ranked = entries.map((entry, index) => {
|
|
364
|
+
let score = 0;
|
|
365
|
+
if (stage) {
|
|
366
|
+
if (entry.stage === stage) {
|
|
367
|
+
score += 4;
|
|
368
|
+
}
|
|
369
|
+
else if (entry.origin_stage === stage) {
|
|
370
|
+
score += 3;
|
|
371
|
+
}
|
|
372
|
+
else if (entry.stage === null) {
|
|
373
|
+
score += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (entry.confidence === "high")
|
|
377
|
+
score += 2;
|
|
378
|
+
if (entry.confidence === "medium")
|
|
379
|
+
score += 1;
|
|
380
|
+
if (entry.frequency >= 3)
|
|
381
|
+
score += 1;
|
|
382
|
+
if (entry.maturity === "lifted-to-enforcement")
|
|
383
|
+
score -= 1;
|
|
384
|
+
const searchable = [
|
|
385
|
+
...tokenizeText(entry.domain),
|
|
386
|
+
...tokenizeText(entry.trigger),
|
|
387
|
+
...tokenizeText(entry.action),
|
|
388
|
+
...tokenizeText(entry.origin_feature),
|
|
389
|
+
...tokenizeText(entry.project)
|
|
390
|
+
];
|
|
391
|
+
const searchSet = new Set(searchable);
|
|
392
|
+
for (const token of branchTokens) {
|
|
393
|
+
if (searchSet.has(token))
|
|
394
|
+
score += 2;
|
|
395
|
+
}
|
|
396
|
+
for (const token of diffTokens) {
|
|
397
|
+
if (searchSet.has(token))
|
|
398
|
+
score += 2;
|
|
399
|
+
}
|
|
400
|
+
for (const token of gateTokens) {
|
|
401
|
+
if (searchSet.has(token))
|
|
402
|
+
score += 2;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
index,
|
|
406
|
+
score,
|
|
407
|
+
entry
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
ranked.sort((a, b) => {
|
|
411
|
+
if (b.score !== a.score)
|
|
412
|
+
return b.score - a.score;
|
|
413
|
+
const bySeen = Date.parse(b.entry.last_seen_ts) - Date.parse(a.entry.last_seen_ts);
|
|
414
|
+
if (!Number.isNaN(bySeen) && bySeen !== 0)
|
|
415
|
+
return bySeen;
|
|
416
|
+
if (b.entry.frequency !== a.entry.frequency)
|
|
417
|
+
return b.entry.frequency - a.entry.frequency;
|
|
418
|
+
return b.index - a.index;
|
|
419
|
+
});
|
|
420
|
+
return ranked
|
|
421
|
+
.filter((row) => row.score > 0)
|
|
422
|
+
.slice(0, limit)
|
|
423
|
+
.map((row) => row.entry);
|
|
424
|
+
}
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -20,3 +20,10 @@ export declare function parseTddCycleLog(text: string): TddCycleEntry[];
|
|
|
20
20
|
export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
|
|
21
21
|
runId?: string;
|
|
22
22
|
}): TddCycleValidation;
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether the log contains a failing RED record associated with
|
|
25
|
+
* `productionPath` for the active run.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hasFailingTestForPath(entries: TddCycleEntry[], productionPath: string, options?: {
|
|
28
|
+
runId?: string;
|
|
29
|
+
}): boolean;
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -119,3 +119,32 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
119
119
|
sliceCount: bySlice.size
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
+
function normalizePath(value) {
|
|
123
|
+
return value.replace(/\\/gu, "/").toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Checks whether the log contains a failing RED record associated with
|
|
127
|
+
* `productionPath` for the active run.
|
|
128
|
+
*/
|
|
129
|
+
export function hasFailingTestForPath(entries, productionPath, options = {}) {
|
|
130
|
+
const normalizedTarget = normalizePath(productionPath);
|
|
131
|
+
const filtered = options.runId
|
|
132
|
+
? entries.filter((entry) => entry.runId === options.runId)
|
|
133
|
+
: entries;
|
|
134
|
+
for (const entry of filtered) {
|
|
135
|
+
if (entry.phase !== "red")
|
|
136
|
+
continue;
|
|
137
|
+
if (entry.exitCode === undefined || entry.exitCode === 0)
|
|
138
|
+
continue;
|
|
139
|
+
if (!Array.isArray(entry.files) || entry.files.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const hasMatch = entry.files.some((filePath) => {
|
|
142
|
+
const normalized = normalizePath(filePath);
|
|
143
|
+
return normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`);
|
|
144
|
+
});
|
|
145
|
+
if (hasMatch) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|