cclaw-cli 7.1.0 → 7.2.0
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/plan.js +187 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +12 -0
- package/dist/content/core-agents.js +1 -1
- package/dist/content/hooks.js +102 -39
- package/dist/content/skills.js +2 -2
- package/dist/content/stage-schema.js +8 -32
- package/dist/content/stages/plan.js +3 -0
- package/dist/content/start-command.js +2 -2
- package/dist/delegation.js +1 -0
- package/dist/flow-state.d.ts +1 -1
- package/dist/install.d.ts +1 -0
- package/dist/install.js +50 -0
- package/dist/internal/advance-stage/start-flow.js +6 -18
- package/dist/internal/advance-stage/verify.js +6 -18
- package/dist/internal/wave-status.d.ts +1 -1
- package/dist/internal/wave-status.js +99 -2
- package/dist/stack-detection.d.ts +22 -0
- package/dist/stack-detection.js +58 -0
- package/package.json +1 -1
|
@@ -8,6 +8,14 @@ import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits, parseImpleme
|
|
|
8
8
|
const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
|
|
9
9
|
const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
|
|
10
10
|
const TASK_ID_PATTERN = /\bT-\d{3}[a-z]?(?:\.\d{1,3})?\b/giu;
|
|
11
|
+
const PLAN_LANE_WHITELIST = new Set([
|
|
12
|
+
"production",
|
|
13
|
+
"test",
|
|
14
|
+
"docs",
|
|
15
|
+
"infra",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"migration"
|
|
18
|
+
]);
|
|
11
19
|
/**
|
|
12
20
|
* Extract every distinct T-NNN[a-z]?(.NNN)? id from a markdown body.
|
|
13
21
|
*
|
|
@@ -36,6 +44,102 @@ function extractParallelExecManagedBody(planMarkdown) {
|
|
|
36
44
|
}
|
|
37
45
|
return planMarkdown.slice(startIdx + PARALLEL_EXEC_MANAGED_START.length, endIdx);
|
|
38
46
|
}
|
|
47
|
+
function normalizePathToken(raw) {
|
|
48
|
+
return raw.trim().replace(/^`|`$/gu, "").replace(/^\.\/+/u, "");
|
|
49
|
+
}
|
|
50
|
+
function parsePipeRow(trimmedLine) {
|
|
51
|
+
const inner = trimmedLine.replace(/^\|/u, "").replace(/\|\s*$/u, "");
|
|
52
|
+
return inner.split("|").map((cell) => cell.trim());
|
|
53
|
+
}
|
|
54
|
+
function headerIndexByName(cells) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
for (let i = 0; i < cells.length; i += 1) {
|
|
57
|
+
const key = cells[i].toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
58
|
+
if (key.length > 0 && !map.has(key)) {
|
|
59
|
+
map.set(key, i);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
function parseParallelWaveTableMetadata(planMarkdown) {
|
|
65
|
+
const body = extractParallelExecManagedBody(planMarkdown);
|
|
66
|
+
if (body.trim().length === 0)
|
|
67
|
+
return [];
|
|
68
|
+
const lines = body.split(/\r?\n/u);
|
|
69
|
+
const out = [];
|
|
70
|
+
let current = null;
|
|
71
|
+
let headerIdx = null;
|
|
72
|
+
const flush = () => {
|
|
73
|
+
if (current)
|
|
74
|
+
out.push(current);
|
|
75
|
+
};
|
|
76
|
+
for (const rawLine of lines) {
|
|
77
|
+
const trimmed = rawLine.trim();
|
|
78
|
+
const waveMatch = /^###\s+Wave\s+(?:W-)?(\d+)\b/iu.exec(trimmed);
|
|
79
|
+
if (waveMatch) {
|
|
80
|
+
flush();
|
|
81
|
+
current = {
|
|
82
|
+
waveId: `W-${waveMatch[1].padStart(2, "0")}`,
|
|
83
|
+
rows: [],
|
|
84
|
+
notes: []
|
|
85
|
+
};
|
|
86
|
+
headerIdx = null;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!current)
|
|
90
|
+
continue;
|
|
91
|
+
current.notes.push(trimmed);
|
|
92
|
+
if (!trimmed.startsWith("|"))
|
|
93
|
+
continue;
|
|
94
|
+
const cells = parsePipeRow(trimmed);
|
|
95
|
+
if (cells.length === 0)
|
|
96
|
+
continue;
|
|
97
|
+
const first = cells[0].toLowerCase();
|
|
98
|
+
const isHeader = first === "sliceid" || first === "slice id";
|
|
99
|
+
if (isHeader) {
|
|
100
|
+
headerIdx = headerIndexByName(cells);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const sliceCell = cells[0];
|
|
107
|
+
if (!/^S-\d+$/iu.test(sliceCell))
|
|
108
|
+
continue;
|
|
109
|
+
const idx = headerIdx ?? new Map();
|
|
110
|
+
const unitIdx = idx.get("unit") ?? idx.get("taskid") ?? 1;
|
|
111
|
+
const pathsIdx = idx.get("claimedpaths");
|
|
112
|
+
const parallelizableIdx = idx.get("parallelizable");
|
|
113
|
+
const laneIdx = idx.get("lane");
|
|
114
|
+
const rawPaths = pathsIdx !== undefined ? (cells[pathsIdx] ?? "") : "";
|
|
115
|
+
const claimedPaths = rawPaths.length === 0
|
|
116
|
+
? []
|
|
117
|
+
: rawPaths
|
|
118
|
+
.split(",")
|
|
119
|
+
.map((p) => normalizePathToken(p))
|
|
120
|
+
.filter((p) => p.length > 0);
|
|
121
|
+
const rawParallel = parallelizableIdx !== undefined ? (cells[parallelizableIdx] ?? "").toLowerCase() : "";
|
|
122
|
+
let parallelizable = null;
|
|
123
|
+
if (rawParallel === "true" || rawParallel === "yes")
|
|
124
|
+
parallelizable = true;
|
|
125
|
+
if (rawParallel === "false" || rawParallel === "no")
|
|
126
|
+
parallelizable = false;
|
|
127
|
+
const laneRaw = laneIdx !== undefined ? (cells[laneIdx] ?? "").trim().toLowerCase() : "";
|
|
128
|
+
current.rows.push({
|
|
129
|
+
sliceId: sliceCell.toUpperCase(),
|
|
130
|
+
unit: (cells[unitIdx] ?? "").trim(),
|
|
131
|
+
claimedPaths,
|
|
132
|
+
parallelizable,
|
|
133
|
+
lane: laneRaw.length > 0 ? laneRaw : null
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
flush();
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function waveHasSequentialModeHint(wave) {
|
|
140
|
+
const noteText = wave.notes.join("\n").toLowerCase();
|
|
141
|
+
return /mode\s*:\s*sequential/iu.test(noteText) || /\bsequential\b/iu.test(noteText) || /\bserial\b/iu.test(noteText);
|
|
142
|
+
}
|
|
39
143
|
export async function lintPlanStage(ctx) {
|
|
40
144
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
41
145
|
evaluateInvestigationTrace(ctx, "Implementation Units");
|
|
@@ -308,7 +412,8 @@ export async function lintPlanStage(ctx) {
|
|
|
308
412
|
: "Parallel-ready units detected or plan is single-unit."
|
|
309
413
|
});
|
|
310
414
|
}
|
|
311
|
-
// plan_parallel_exec_full_coverage
|
|
415
|
+
// plan_parallel_exec_full_coverage + atomic wave metadata checks.
|
|
416
|
+
// Every T-NNN task listed in the
|
|
312
417
|
// plan's Task List must be assigned to a slice inside the
|
|
313
418
|
// <!-- parallel-exec-managed-start --> block. Without this, TDD
|
|
314
419
|
// cannot fan out work the plan never authored as waves; the previous
|
|
@@ -355,5 +460,86 @@ export async function lintPlanStage(ctx) {
|
|
|
355
460
|
? `Parallel Execution Plan covers all ${authoredTaskIds.size} authored task id(s); ${deferredIds.size} task id(s) are explicitly deferred.`
|
|
356
461
|
: `Uncovered task id(s) — author waves for: ${uncovered.slice(0, 25).join(", ")}${uncovered.length > 25 ? `, … (${uncovered.length - 25} more)` : ""}. Either add slices for them inside <!-- parallel-exec-managed-start --> or move them under \`## Deferred Tasks\` with a reason.`
|
|
357
462
|
});
|
|
463
|
+
const waveMeta = parseParallelWaveTableMetadata(raw);
|
|
464
|
+
const pathConflicts = [];
|
|
465
|
+
for (const wave of waveMeta) {
|
|
466
|
+
const rows = wave.rows;
|
|
467
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
468
|
+
for (let j = i + 1; j < rows.length; j += 1) {
|
|
469
|
+
const left = rows[i];
|
|
470
|
+
const right = rows[j];
|
|
471
|
+
const rightPathSet = new Set(right.claimedPaths);
|
|
472
|
+
const overlap = left.claimedPaths.filter((p) => rightPathSet.has(p));
|
|
473
|
+
if (overlap.length === 0)
|
|
474
|
+
continue;
|
|
475
|
+
pathConflicts.push(`${wave.waveId} ${left.sliceId}<->${right.sliceId} overlap: ${overlap.join(", ")}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
findings.push({
|
|
480
|
+
section: "plan_wave_paths_disjoint",
|
|
481
|
+
required: taskListPresent,
|
|
482
|
+
rule: "Slices within the same wave must keep `claimedPaths` disjoint so TDD can safely fan out parallel slice-builders.",
|
|
483
|
+
found: taskListPresent && blockPresent && pathConflicts.length === 0,
|
|
484
|
+
details: !taskListPresent
|
|
485
|
+
? "Task List section is empty or missing T-NNN ids; disjoint-path wave check skipped."
|
|
486
|
+
: !blockPresent
|
|
487
|
+
? "`<!-- parallel-exec-managed-start -->` block is missing or empty; cannot validate wave path disjointness."
|
|
488
|
+
: pathConflicts.length === 0
|
|
489
|
+
? "All parsed same-wave slice rows have disjoint claimedPaths."
|
|
490
|
+
: `Overlapping claimedPaths detected: ${pathConflicts.slice(0, 12).join(" | ")}${pathConflicts.length > 12 ? ` | … (${pathConflicts.length - 12} more)` : ""}.`
|
|
491
|
+
});
|
|
492
|
+
const invalidLanes = [];
|
|
493
|
+
for (const wave of waveMeta) {
|
|
494
|
+
for (const row of wave.rows) {
|
|
495
|
+
if (!row.lane)
|
|
496
|
+
continue;
|
|
497
|
+
if (!PLAN_LANE_WHITELIST.has(row.lane)) {
|
|
498
|
+
invalidLanes.push(`${wave.waveId}/${row.sliceId}:${row.lane}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
findings.push({
|
|
503
|
+
section: "plan_lane_meaningful",
|
|
504
|
+
required: false,
|
|
505
|
+
rule: "When a lane is declared, it must be one of: production, test, docs, infra, scaffold, migration.",
|
|
506
|
+
found: invalidLanes.length === 0,
|
|
507
|
+
details: invalidLanes.length === 0
|
|
508
|
+
? "All declared lane values are either omitted or in the approved lane whitelist."
|
|
509
|
+
: `Invalid lane value(s): ${invalidLanes.join(", ")}. Remove lane or use a whitelisted value.`
|
|
510
|
+
});
|
|
511
|
+
const inconsistentParallelizable = [];
|
|
512
|
+
for (const wave of waveMeta) {
|
|
513
|
+
const hasSerialSlice = wave.rows.some((row) => row.parallelizable === false);
|
|
514
|
+
if (!hasSerialSlice)
|
|
515
|
+
continue;
|
|
516
|
+
if (!waveHasSequentialModeHint(wave)) {
|
|
517
|
+
const serialSlices = wave.rows
|
|
518
|
+
.filter((row) => row.parallelizable === false)
|
|
519
|
+
.map((row) => row.sliceId)
|
|
520
|
+
.join(", ");
|
|
521
|
+
inconsistentParallelizable.push(`${wave.waveId} [${serialSlices}]`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
findings.push({
|
|
525
|
+
section: "plan_parallelizable_consistency",
|
|
526
|
+
required: false,
|
|
527
|
+
rule: "Waves containing `parallelizable: false` slices should be explicitly marked sequential in wave notes/mode.",
|
|
528
|
+
found: inconsistentParallelizable.length === 0,
|
|
529
|
+
details: inconsistentParallelizable.length === 0
|
|
530
|
+
? "No serial slices were found outside a sequentially-labeled wave context."
|
|
531
|
+
: `Serial slice(s) found without sequential wave mode hints in: ${inconsistentParallelizable.join(", ")}. Add a wave mode/note indicating sequential execution.`
|
|
532
|
+
});
|
|
533
|
+
const mermaidBlocks = raw.match(/```mermaid[\s\S]*?```/giu) ?? [];
|
|
534
|
+
const hasParallelExecMermaid = mermaidBlocks.some((block) => /(flowchart|gantt)/iu.test(block) && /\bW-\d+\b/iu.test(block) && /\bS-\d+\b/iu.test(block));
|
|
535
|
+
findings.push({
|
|
536
|
+
section: "plan_parallel_exec_mermaid_present",
|
|
537
|
+
required: false,
|
|
538
|
+
rule: "Plan should include a mermaid flowchart/gantt for parallel waves and slice dependencies to make fanout shape visually reviewable.",
|
|
539
|
+
found: hasParallelExecMermaid,
|
|
540
|
+
details: hasParallelExecMermaid
|
|
541
|
+
? "Mermaid visualization for parallel execution waves is present."
|
|
542
|
+
: "No mermaid parallel-execution visualization found (advisory). Add a ` ```mermaid ` flowchart or gantt with W-* and S-* nodes."
|
|
543
|
+
});
|
|
358
544
|
}
|
|
359
545
|
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -40,6 +40,7 @@ Commands:
|
|
|
40
40
|
sync Reconcile generated runtime files with the current config.
|
|
41
41
|
Flags: --harnesses=<list> Update configured harnesses before syncing.
|
|
42
42
|
--interactive Pick harnesses from a numbered TTY menu.
|
|
43
|
+
--check Verify managed hook files are byte-identical to canonical generators.
|
|
43
44
|
upgrade Refresh generated files in .cclaw. Preserves your config.yaml.
|
|
44
45
|
archive Archive the active run and reset flow state for the next run.
|
|
45
46
|
Flags: --name=<slug> Override archive folder suffix.
|
|
@@ -57,6 +58,7 @@ Examples:
|
|
|
57
58
|
npx cclaw-cli
|
|
58
59
|
npx cclaw-cli init --harnesses=claude,cursor --no-interactive
|
|
59
60
|
npx cclaw-cli sync --interactive
|
|
61
|
+
npx cclaw-cli sync --check
|
|
60
62
|
npx cclaw-cli archive --name=my-run
|
|
61
63
|
npx cclaw-cli archive --disposition=cancelled --reason="deprioritized"
|
|
62
64
|
npx cclaw-cli upgrade
|
|
@@ -334,6 +336,7 @@ function parseArgs(argv) {
|
|
|
334
336
|
return flag.startsWith("--harnesses=") ||
|
|
335
337
|
(parsed.command === "init" && flag.startsWith("--track=")) ||
|
|
336
338
|
(parsed.command === "init" && flag.startsWith("--profile=")) ||
|
|
339
|
+
(parsed.command === "sync" && flag === "--check") ||
|
|
337
340
|
flag === "--interactive" ||
|
|
338
341
|
flag === "--no-interactive" ||
|
|
339
342
|
(parsed.command === "init" && flag === "--dry-run");
|
|
@@ -374,6 +377,10 @@ function parseArgs(argv) {
|
|
|
374
377
|
parsed.dryRun = true;
|
|
375
378
|
continue;
|
|
376
379
|
}
|
|
380
|
+
if (flag === "--check") {
|
|
381
|
+
parsed.syncCheck = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
377
384
|
if (flag.startsWith("--name=")) {
|
|
378
385
|
parsed.archiveName = flag.replace("--name=", "").trim();
|
|
379
386
|
continue;
|
|
@@ -446,6 +453,11 @@ async function runCommand(parsed, ctx) {
|
|
|
446
453
|
return 0;
|
|
447
454
|
}
|
|
448
455
|
if (command === "sync") {
|
|
456
|
+
if (parsed.syncCheck === true) {
|
|
457
|
+
await syncCclaw(ctx.cwd, { check: true });
|
|
458
|
+
info(ctx, "Managed hook drift check passed (no sync required).");
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
449
461
|
const resolved = await resolveSyncInputs(parsed, ctx);
|
|
450
462
|
await syncCclaw(ctx.cwd, { harnesses: resolved.harnesses });
|
|
451
463
|
const harnessNote = resolved.harnesses ? ` (${resolved.harnesses.join(", ")})` : "";
|
|
@@ -97,7 +97,7 @@ node .cclaw/hooks/delegation-record.mjs \\
|
|
|
97
97
|
--json
|
|
98
98
|
\`\`\`
|
|
99
99
|
|
|
100
|
-
Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. **GREEN evidence freshness** (slice-builder): the FIRST \`--evidence-ref\` MUST (1) reference the same test the matching \`phase=red\` row cited (basename/stem substring; reject \`green_evidence_red_test_mismatch\`), (2) include a recognized passing-runner line such as \`=> N passed; 0 failed\`, \`N passed in 0.42s\`, or \`ok pkg 0.12s\` (reject \`green_evidence_passing_assertion_missing\`), AND (3) be captured AFTER \`ackTs\` of this span — \`completedTs - ackTs\` must be ≥ \`flow-state.json::tddGreenMinElapsedMs\` (default 4000ms; reject \`green_evidence_too_fresh\`). Escape clause for legitimate observational GREEN: pass
|
|
100
|
+
Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. **GREEN evidence freshness** (slice-builder): the FIRST \`--evidence-ref\` MUST (1) reference the same test the matching \`phase=red\` row cited (basename/stem substring; reject \`green_evidence_red_test_mismatch\`), (2) include a recognized passing-runner line such as \`=> N passed; 0 failed\`, \`N passed in 0.42s\`, or \`ok pkg 0.12s\` (reject \`green_evidence_passing_assertion_missing\`), AND (3) be captured AFTER \`ackTs\` of this span — \`completedTs - ackTs\` must be ≥ \`flow-state.json::tddGreenMinElapsedMs\` (default 4000ms; reject \`green_evidence_too_fresh\`). Escape clause for legitimate observational GREEN: pass \`--green-mode=observational\`. \`--ack-ts\` and \`--completed-ts\` must be monotonic on the span (\`startTs ≤ launchedTs ≤ ackTs ≤ completedTs\`); the helper rejects out-of-order writes with \`delegation_timestamp_non_monotonic\`. If the helper rejects with \`dispatch_active_span_collision\` against a stale span, surface the conflicting \`spanId\` to the parent — do NOT silently retry with \`--allow-parallel\`.`;
|
|
101
101
|
}
|
|
102
102
|
function formatReturnSchema(schema) {
|
|
103
103
|
const lines = [
|
package/dist/content/hooks.js
CHANGED
|
@@ -334,19 +334,20 @@ function extractRedTestNameInline(redEvidenceRef) {
|
|
|
334
334
|
return trimmed;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
// Match canonical runner pass lines:
|
|
338
|
-
//
|
|
339
|
-
// pytest: "===== N passed in 0.42s ====="
|
|
340
|
-
// go test: "ok pkg 0.123s"
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// runner-specific patterns.
|
|
337
|
+
// Match canonical runner pass lines using language-agnostic examples:
|
|
338
|
+
// Node/TS (vitest/jest): "=> N passed; 0 failed" or "Tests: N passed"
|
|
339
|
+
// Python (pytest): "===== N passed in 0.42s ====="
|
|
340
|
+
// Go (go test): "ok pkg 0.123s"
|
|
341
|
+
// Rust (cargo test): "test result: ok. N passed; 0 failed"
|
|
342
|
+
// Java/JVM (maven/surefire): "Tests run: N, Failures: 0, Errors: 0"
|
|
343
|
+
// We accept a generic "passed/failed" shape plus runner-specific patterns.
|
|
344
344
|
const GREEN_PASS_PATTERNS = [
|
|
345
345
|
/=>\\s*\\d+\\s+passed/iu,
|
|
346
346
|
/\\b\\d+\\s+passed[;,]\\s*0\\s+failed\\b/iu,
|
|
347
347
|
/\\btest\\s+result:\\s*ok\\b/iu,
|
|
348
348
|
/\\b\\d+\\s+passed\\s+in\\s+\\d+(?:\\.\\d+)?\\s*s\\b/iu,
|
|
349
|
-
/^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu
|
|
349
|
+
/^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu,
|
|
350
|
+
/tests\\s+run\\s*:\\s*\\d+\\s*,\\s*failures\\s*:\\s*0\\s*,\\s*errors\\s*:\\s*0/iu
|
|
350
351
|
];
|
|
351
352
|
|
|
352
353
|
function matchesPassingAssertionInline(value) {
|
|
@@ -373,6 +374,16 @@ async function readDelegationEvents(root) {
|
|
|
373
374
|
}
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
async function appendAuditEventInline(root, payload) {
|
|
378
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
379
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
380
|
+
await fs.appendFile(
|
|
381
|
+
path.join(stateDir, "delegation-events.jsonl"),
|
|
382
|
+
JSON.stringify(payload) + "\\n",
|
|
383
|
+
{ encoding: "utf8", mode: 0o600 }
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
376
387
|
function hasPriorAck(events, args, runId) {
|
|
377
388
|
return events.some((event) =>
|
|
378
389
|
event.runId === runId &&
|
|
@@ -388,7 +399,7 @@ function hasPriorAck(events, args, runId) {
|
|
|
388
399
|
function usage() {
|
|
389
400
|
process.stderr.write([
|
|
390
401
|
"Usage:",
|
|
391
|
-
" node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--json]",
|
|
402
|
+
" node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--reason=<slug>] [--json]",
|
|
392
403
|
" node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
|
|
393
404
|
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
|
|
394
405
|
" node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped [--audit-reason=\\\"<comma-separated reasons>\\\"] [--slice-ids=\\\"S-1,S-2\\\"] [--json] # non-delegation audit row",
|
|
@@ -406,11 +417,12 @@ function usage() {
|
|
|
406
417
|
"TDD parallel scheduler:",
|
|
407
418
|
" --paths=<a,b,c> repo-relative paths the slice-builder will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
|
|
408
419
|
" --override-cap=<int> raise the slice worker fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_BUILDERS overrides globally)",
|
|
420
|
+
" --reason=<slug> required with --override-cap so cap bypasses are auditable (e.g. red-checkpoint-retry)",
|
|
409
421
|
"",
|
|
410
422
|
"TDD slice phase tagging:",
|
|
411
423
|
" --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
|
|
412
424
|
" --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
|
|
413
|
-
" --refactor-rationale=<t> required
|
|
425
|
+
" --refactor-rationale=<t> required for deferred refactor paths; must be >=80 chars and mention slice + task context (e.g. S-12 / T-103).",
|
|
414
426
|
" --refactor-outcome=<m> one of inline|deferred. Folds REFACTOR into the phase=green event so a single row can close RED→GREEN→REFACTOR. Pair --refactor-outcome=deferred with --refactor-rationale.",
|
|
415
427
|
" --risk-tier=<t> one of low|medium|high. high triggers integration-overseer in conditional mode.",
|
|
416
428
|
""
|
|
@@ -505,6 +517,28 @@ function normalizeEvidenceRefs(args) {
|
|
|
505
517
|
return [];
|
|
506
518
|
}
|
|
507
519
|
|
|
520
|
+
function validateDeferredRationaleInline(rationaleRaw, args) {
|
|
521
|
+
const rationale = typeof rationaleRaw === "string" ? rationaleRaw.trim() : "";
|
|
522
|
+
if (rationale.length === 0) {
|
|
523
|
+
return "missing";
|
|
524
|
+
}
|
|
525
|
+
if (rationale.length < 80) {
|
|
526
|
+
return "too-short";
|
|
527
|
+
}
|
|
528
|
+
const lower = rationale.toLowerCase();
|
|
529
|
+
const sliceRaw = typeof args.slice === "string" ? args.slice.trim().toLowerCase() : "";
|
|
530
|
+
const hasSliceMention =
|
|
531
|
+
(sliceRaw.length > 0 && lower.includes(sliceRaw)) ||
|
|
532
|
+
/\\bs-\\d+\\b/iu.test(rationale);
|
|
533
|
+
const hasTaskMention =
|
|
534
|
+
/\\bt-\\d{3}[a-z]?(?:\\.\\d{1,3})?\\b/iu.test(rationale) ||
|
|
535
|
+
/\\btask\\b/iu.test(rationale);
|
|
536
|
+
if (!hasSliceMention || !hasTaskMention) {
|
|
537
|
+
return "missing-context";
|
|
538
|
+
}
|
|
539
|
+
return "ok";
|
|
540
|
+
}
|
|
541
|
+
|
|
508
542
|
function buildRow(args, status, runId, now, options) {
|
|
509
543
|
const fulfillmentMode = args["dispatch-surface"] === "role-switch"
|
|
510
544
|
? "role-switch"
|
|
@@ -911,7 +945,8 @@ async function findLegacyEntry(root, spanId) {
|
|
|
911
945
|
// the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS in
|
|
912
946
|
// src/delegation.ts.
|
|
913
947
|
const VALID_AUDIT_KINDS = new Set([
|
|
914
|
-
"cclaw_integration_overseer_skipped"
|
|
948
|
+
"cclaw_integration_overseer_skipped",
|
|
949
|
+
"cclaw_allow_parallel_auto_flip"
|
|
915
950
|
]);
|
|
916
951
|
|
|
917
952
|
async function runAuditEmit(args, json) {
|
|
@@ -1347,15 +1382,15 @@ async function main() {
|
|
|
1347
1382
|
return;
|
|
1348
1383
|
}
|
|
1349
1384
|
if (args.phase === "refactor-deferred") {
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
(
|
|
1355
|
-
(
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1385
|
+
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1386
|
+
if (rationaleQuality !== "ok") {
|
|
1387
|
+
if (rationaleQuality === "missing") {
|
|
1388
|
+
problems.push("--phase=refactor-deferred requires --refactor-rationale=<text>");
|
|
1389
|
+
} else if (rationaleQuality === "too-short") {
|
|
1390
|
+
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1391
|
+
} else {
|
|
1392
|
+
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1393
|
+
}
|
|
1359
1394
|
emitProblems(problems, json, 2);
|
|
1360
1395
|
return;
|
|
1361
1396
|
}
|
|
@@ -1375,15 +1410,15 @@ async function main() {
|
|
|
1375
1410
|
return;
|
|
1376
1411
|
}
|
|
1377
1412
|
if (args["refactor-outcome"] === "deferred") {
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
(
|
|
1383
|
-
(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1413
|
+
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1414
|
+
if (rationaleQuality !== "ok") {
|
|
1415
|
+
if (rationaleQuality === "missing") {
|
|
1416
|
+
problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text>");
|
|
1417
|
+
} else if (rationaleQuality === "too-short") {
|
|
1418
|
+
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1419
|
+
} else {
|
|
1420
|
+
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1421
|
+
}
|
|
1387
1422
|
emitProblems(problems, json, 2);
|
|
1388
1423
|
return;
|
|
1389
1424
|
}
|
|
@@ -1398,6 +1433,21 @@ async function main() {
|
|
|
1398
1433
|
emitProblems(problems, json, 2);
|
|
1399
1434
|
return;
|
|
1400
1435
|
}
|
|
1436
|
+
if (args["override-cap"] !== undefined) {
|
|
1437
|
+
const overrideRaw = String(args["override-cap"]).trim();
|
|
1438
|
+
const overrideNum = Number(overrideRaw);
|
|
1439
|
+
if (!Number.isInteger(overrideNum) || overrideNum < 1) {
|
|
1440
|
+
problems.push("--override-cap must be an integer >= 1");
|
|
1441
|
+
emitProblems(problems, json, 2);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const reasonRaw = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
1445
|
+
if (reasonRaw.length === 0) {
|
|
1446
|
+
problems.push("--override-cap requires --reason=<slug>");
|
|
1447
|
+
emitProblems(problems, json, 2);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1401
1451
|
|
|
1402
1452
|
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
|
|
1403
1453
|
for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
@@ -1479,6 +1529,7 @@ async function main() {
|
|
|
1479
1529
|
const row = buildRow(args, status, runId, now, { spanStartTs });
|
|
1480
1530
|
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
1481
1531
|
const event = { ...clean, event: status, eventTs: now };
|
|
1532
|
+
let autoParallelAuditEvent = null;
|
|
1482
1533
|
|
|
1483
1534
|
const violation = validateMonotonicTimestampsInline(clean, priorLedger);
|
|
1484
1535
|
if (violation) {
|
|
@@ -1487,8 +1538,8 @@ async function main() {
|
|
|
1487
1538
|
}
|
|
1488
1539
|
|
|
1489
1540
|
// File-overlap scheduler + fan-out cap. Run before the dispatch
|
|
1490
|
-
// dedup so disjoint claimedPaths can auto-promote to allowParallel
|
|
1491
|
-
// bypass the duplicate guard.
|
|
1541
|
+
// dedup so disjoint claimedPaths can auto-promote to allowParallel,
|
|
1542
|
+
// emit an audit event for the flip, and bypass the duplicate guard.
|
|
1492
1543
|
if (status === "scheduled") {
|
|
1493
1544
|
const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
|
|
1494
1545
|
const activeForRun = computeActiveSubagentsInline(sameRunPrior);
|
|
@@ -1501,6 +1552,18 @@ async function main() {
|
|
|
1501
1552
|
clean.allowParallel = true;
|
|
1502
1553
|
args["allow-parallel"] = true;
|
|
1503
1554
|
event.allowParallel = true;
|
|
1555
|
+
autoParallelAuditEvent = {
|
|
1556
|
+
event: "cclaw_allow_parallel_auto_flip",
|
|
1557
|
+
runId,
|
|
1558
|
+
ts: now,
|
|
1559
|
+
eventTs: now,
|
|
1560
|
+
stage: clean.stage,
|
|
1561
|
+
agent: clean.agent,
|
|
1562
|
+
spanId: clean.spanId,
|
|
1563
|
+
sliceId: clean.sliceId,
|
|
1564
|
+
reason: "disjoint-claimed-paths-auto-flip",
|
|
1565
|
+
claimedPaths: Array.isArray(clean.claimedPaths) ? clean.claimedPaths : []
|
|
1566
|
+
};
|
|
1504
1567
|
}
|
|
1505
1568
|
const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
|
|
1506
1569
|
const override = overrideRaw !== null ? Number(overrideRaw) : null;
|
|
@@ -1548,8 +1611,7 @@ async function main() {
|
|
|
1548
1611
|
// 3. green_evidence_too_fresh — completedTs minus ackTs must be
|
|
1549
1612
|
// >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
|
|
1550
1613
|
// Escape hatch for legitimate observational GREENs (cross-slice
|
|
1551
|
-
// handoff, no-op verification):
|
|
1552
|
-
// --green-mode=observational. Both flags are required.
|
|
1614
|
+
// handoff, no-op verification): --green-mode=observational.
|
|
1553
1615
|
if (
|
|
1554
1616
|
clean.stage === "tdd" &&
|
|
1555
1617
|
clean.agent === "slice-builder" &&
|
|
@@ -1559,7 +1621,6 @@ async function main() {
|
|
|
1559
1621
|
const isObservational =
|
|
1560
1622
|
typeof args["green-mode"] === "string" &&
|
|
1561
1623
|
args["green-mode"].trim().toLowerCase() === "observational";
|
|
1562
|
-
const allowFastGreen = args["allow-fast-green"] === true;
|
|
1563
1624
|
const greenEvidenceFirst =
|
|
1564
1625
|
Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
|
|
1565
1626
|
? String(clean.evidenceRefs[0])
|
|
@@ -1586,10 +1647,9 @@ async function main() {
|
|
|
1586
1647
|
// nothing to verify GREEN against (legacy ledger imports, RED
|
|
1587
1648
|
// happened outside cclaw harness, or test fixtures that bypass
|
|
1588
1649
|
// RED). Once a RED row is present, the contract becomes
|
|
1589
|
-
// mandatory unless explicitly waived via --
|
|
1590
|
-
// --green-mode=observational.
|
|
1650
|
+
// mandatory unless explicitly waived via --green-mode=observational.
|
|
1591
1651
|
const hasRedContext = redEvidenceRef !== null;
|
|
1592
|
-
const escapeFastGreen =
|
|
1652
|
+
const escapeFastGreen = isObservational;
|
|
1593
1653
|
|
|
1594
1654
|
if (hasRedContext && !escapeFastGreen) {
|
|
1595
1655
|
// Check 1: RED test name match.
|
|
@@ -1618,7 +1678,7 @@ async function main() {
|
|
|
1618
1678
|
sliceId: clean.sliceId,
|
|
1619
1679
|
greenEvidenceFirst,
|
|
1620
1680
|
remediation:
|
|
1621
|
-
"evidenceRefs[0] on the GREEN row must contain a passing-assertion line
|
|
1681
|
+
"evidenceRefs[0] on the GREEN row must contain a passing-assertion line (language-agnostic examples: Node/Vitest \\"=> N passed; 0 failed\\", Python/Pytest \\"N passed in 0.42s\\", Go \\"ok pkg 0.12s\\", Rust \\"test result: ok\\", Java/Maven \\"Tests run: N, Failures: 0, Errors: 0\\"). Re-run the test and paste a fresh runner line."
|
|
1622
1682
|
},
|
|
1623
1683
|
json
|
|
1624
1684
|
);
|
|
@@ -1657,7 +1717,7 @@ async function main() {
|
|
|
1657
1717
|
elapsedMs: elapsed,
|
|
1658
1718
|
minMs,
|
|
1659
1719
|
remediation:
|
|
1660
|
-
"GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --
|
|
1720
|
+
"GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --green-mode=observational for legitimate no-op verification spans."
|
|
1661
1721
|
},
|
|
1662
1722
|
json
|
|
1663
1723
|
);
|
|
@@ -1687,6 +1747,9 @@ async function main() {
|
|
|
1687
1747
|
}
|
|
1688
1748
|
|
|
1689
1749
|
await persistEntry(root, runId, clean, event);
|
|
1750
|
+
if (autoParallelAuditEvent) {
|
|
1751
|
+
await appendAuditEventInline(root, autoParallelAuditEvent);
|
|
1752
|
+
}
|
|
1690
1753
|
|
|
1691
1754
|
process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
|
|
1692
1755
|
}
|
package/dist/content/skills.js
CHANGED
|
@@ -188,14 +188,14 @@ export function tddTopOfSkillBlock(stage) {
|
|
|
188
188
|
**Step 1 — Wave status (always first):**
|
|
189
189
|
\`node .cclaw/cli.mjs internal wave-status --json\`
|
|
190
190
|
|
|
191
|
-
The output names: \`waves[]\` (closed/open), \`nextDispatch.waveId\`, \`nextDispatch.mode\` (\`wave-fanout
|
|
191
|
+
The output names: \`waves[]\` (closed/open), \`nextDispatch.waveId\`, \`nextDispatch.mode\` (\`wave-fanout\`, \`single-slice\`, or \`blocked\`), \`nextDispatch.readyToDispatch\` (slice ids), and \`nextDispatch.pathConflicts\` (overlapping \`claimedPaths\` between members).
|
|
192
192
|
|
|
193
193
|
**Step 2 — Decide automatically (no user question when paths disjoint):**
|
|
194
194
|
|
|
195
195
|
| \`mode\` | \`pathConflicts\` | Action |
|
|
196
196
|
|------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
|
|
197
197
|
| \`wave-fanout\` | \`[]\` | **Fan out the entire wave in one tool batch.** Emit one \`Task\` per ready slice in a single controller message. Do NOT ask the user. |
|
|
198
|
-
| \`
|
|
198
|
+
| \`blocked\` | non-empty | Issue exactly one AskQuestion (resolve overlap, split/serialize, or adjust claimedPaths), then re-run \`wave-status\`. |
|
|
199
199
|
| \`single-slice\` | — | One \`Task\` for the next ready slice. |
|
|
200
200
|
|
|
201
201
|
**Step 3 — Dispatch protocol per slice:** in the SAME controller message that issues the \`Task\` call:
|
|
@@ -4,6 +4,7 @@ import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stag
|
|
|
4
4
|
import { stagePolicyNeedlesFromMetadata } from "./stages/_lint-metadata/index.js";
|
|
5
5
|
import { tddStageForTrack } from "./stages/tdd.js";
|
|
6
6
|
import { trackRenderContext } from "./track-render-context.js";
|
|
7
|
+
import { STACK_REVIEW_ROUTE_PROFILES } from "../stack-detection.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// NOTE: The former QUESTION_FORMAT_SPEC / ERROR_BUDGET_SPEC exports were
|
|
9
10
|
// hoisted into `src/content/meta-skill.ts` (Shared Decision + Tool-Use
|
|
@@ -31,38 +32,12 @@ const COMPLEXITY_TIER_ORDER = {
|
|
|
31
32
|
standard: 1,
|
|
32
33
|
deep: 2
|
|
33
34
|
};
|
|
34
|
-
const REVIEW_STACK_AWARE_ROUTES =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
stack: "Python",
|
|
43
|
-
agent: "reviewer",
|
|
44
|
-
signals: ["pyproject.toml", "requirements.txt"],
|
|
45
|
-
focus: "packaging, virtualenv assumptions, typing, pytest or unittest evidence"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
stack: "Ruby/Rails",
|
|
49
|
-
agent: "reviewer",
|
|
50
|
-
signals: ["Gemfile", "config/"],
|
|
51
|
-
focus: "Rails conventions, migrations, routes/controllers, RSpec or Minitest evidence"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
stack: "Go",
|
|
55
|
-
agent: "reviewer",
|
|
56
|
-
signals: ["go.mod"],
|
|
57
|
-
focus: "interfaces, concurrency, error handling, go test coverage"
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
stack: "Rust",
|
|
61
|
-
agent: "reviewer",
|
|
62
|
-
signals: ["Cargo.toml"],
|
|
63
|
-
focus: "ownership, error/result handling, feature flags, cargo test coverage"
|
|
64
|
-
}
|
|
65
|
-
];
|
|
35
|
+
const REVIEW_STACK_AWARE_ROUTES = STACK_REVIEW_ROUTE_PROFILES.map((profile) => ({
|
|
36
|
+
stack: profile.stack,
|
|
37
|
+
agent: "reviewer",
|
|
38
|
+
signals: [...profile.reviewSignals],
|
|
39
|
+
focus: profile.focus
|
|
40
|
+
}));
|
|
66
41
|
function stackAwareRoutesForStage(stage) {
|
|
67
42
|
return stage === "review" ? reviewStackAwareRoutes() : [];
|
|
68
43
|
}
|
|
@@ -295,6 +270,7 @@ const REQUIRED_GATE_IDS = {
|
|
|
295
270
|
"plan_acceptance_mapped",
|
|
296
271
|
"plan_execution_posture_recorded",
|
|
297
272
|
"plan_parallel_exec_full_coverage",
|
|
273
|
+
"plan_wave_paths_disjoint",
|
|
298
274
|
"plan_wait_for_confirm"
|
|
299
275
|
],
|
|
300
276
|
tdd: (track) => [
|
|
@@ -52,6 +52,7 @@ export const PLAN = {
|
|
|
52
52
|
"Define validation points — mark where progress must be checked before continuing, with concrete command and expected evidence.",
|
|
53
53
|
"Define execution posture — record whether execution should be sequential, dependency-batched, parallel-safe, or blocked; include risk triggers and RED/GREEN/REFACTOR checkpoint/commit expectations when the repo workflow supports them. This fulfills the `plan_execution_posture_recorded` gate.",
|
|
54
54
|
"**Author the FULL Parallel Execution Plan.** Inside the `<!-- parallel-exec-managed-start -->` block, enumerate ALL waves W-02..W-N covering EVERY T-NNN task in `## Task List` — no `we'll author waves later`, `next batch only`, or open-ended Backlog handwave is acceptable. Each task gets a slice with `sliceId | taskId | dependsOn | claimedPaths | parallelizable | riskTier | lane`. Spike rows (`S-N`) and tasks marked `deferred` in an explicit `Deferred:` column may be omitted, but every other T-NNN must be claimed. This fulfills the `plan_parallel_exec_full_coverage` gate. The TDD stage downstream is a pure consumer of these waves — if the plan does not author them, TDD cannot fan out that work.",
|
|
55
|
+
"After authoring/refreshing the managed parallel-exec block, render a Mermaid `flowchart` or `gantt` covering waves (`W-*`) and slice dependencies (`S-*`) so parallelism and fan-in boundaries are visually auditable.",
|
|
55
56
|
"WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then close the stage with `node .cclaw/hooks/stage-complete.mjs plan` and tell user to run `/cc`."
|
|
56
57
|
],
|
|
57
58
|
interactionProtocol: [
|
|
@@ -59,6 +60,7 @@ export const PLAN = {
|
|
|
59
60
|
"Split work into small vertical slices (target 2-5 minute tasks).",
|
|
60
61
|
"Publish explicit dependency batches with entry and exit checks for each batch.",
|
|
61
62
|
"Expose execution posture: sequential vs batch/parallel, stop conditions, and checkpoint cadence for the TDD handoff.",
|
|
63
|
+
"Keep same-wave `claimedPaths` disjoint; if overlap exists, split waves or serialize explicitly before handoff.",
|
|
62
64
|
"Attach exact verification command/manual step and expected evidence to every task.",
|
|
63
65
|
"Preserve locked scope boundaries: no silent scope reduction language in task rows.",
|
|
64
66
|
"Enforce WAIT_FOR_CONFIRM: present the plan summary with options (A) Approve / (B) Revise / (C) Reject.",
|
|
@@ -82,6 +84,7 @@ export const PLAN = {
|
|
|
82
84
|
{ id: "plan_acceptance_mapped", description: "Each task maps to a spec acceptance criterion." },
|
|
83
85
|
{ id: "plan_execution_posture_recorded", description: "Execution posture is recorded before implementation handoff." },
|
|
84
86
|
{ id: "plan_parallel_exec_full_coverage", description: "Every T-NNN task in `## Task List` (other than spikes/explicitly-deferred) is assigned to at least one slice inside the `<!-- parallel-exec-managed-start -->` block; TDD cannot fan out work that the plan never authored as waves." },
|
|
87
|
+
{ id: "plan_wave_paths_disjoint", description: "Within each authored wave, slice `claimedPaths` remain disjoint so `wave-fanout` can dispatch safely without overlap conflicts." },
|
|
85
88
|
{ id: "plan_wait_for_confirm", description: "Execution blocked until explicit user confirmation." }
|
|
86
89
|
],
|
|
87
90
|
requiredEvidence: [
|
|
@@ -119,7 +119,7 @@ If during any stage the agent discovers evidence that contradicts the initial Ph
|
|
|
119
119
|
|
|
120
120
|
**The controller never edits production code in TDD.** When \`mode: wave-fanout\` and \`pathConflicts: []\`, fan out the entire wave in a SINGLE controller message: one harness \`Task(subagent_type=…, description="slice-builder S-<id>", prompt=<full slice context>)\` call per ready slice, **side by side in the same tool batch**. Each \`slice-builder\` span owns the full RED → GREEN → REFACTOR → DOC cycle for its slice and emits its own \`delegation-record --phase=red|green|refactor|refactor-deferred|doc\` rows. RED-before-GREEN is enforced per-slice by the linter.
|
|
121
121
|
|
|
122
|
-
When \`mode:
|
|
122
|
+
When \`mode: blocked\` with \`pathConflicts\`, surface exactly one AskQuestion that lets the user resolve the overlap (drop / split / serialize). When \`mode: single-slice\`, dispatch one \`Task\` for the next ready slice.
|
|
123
123
|
|
|
124
124
|
6. **Auto-advance after stage-complete:** when \`stage-complete\` returns \`ok\` with a new \`currentStage\`, immediately load the next stage skill and continue without waiting for the user to retype \`/cc\`. Announce \`Stage <prev> complete → entering <next>. Continuing.\` and proceed.
|
|
125
125
|
|
|
@@ -214,7 +214,7 @@ Progress the tracked flow only when one exists:
|
|
|
214
214
|
2. If missing, guide the user to run \`npx cclaw-cli init\` and stop.
|
|
215
215
|
3. If it is only a fresh init placeholder (\`completedStages: []\`, no passed gates, and no \`${RUNTIME_ROOT}/artifacts/00-idea.md\`), stop and ask for \`/cc <prompt>\` to start a tracked run. Do not silently create a brainstorm run.
|
|
216
216
|
4. Check gates for \`currentStage\`.
|
|
217
|
-
5. **TDD:** When \`currentStage\` is \`tdd\`, run \`wave-status --json\`, then reconcile the managed **Parallel Execution Plan** in \`05-plan.md\` with \`wave-plans/wave-NN.md\`. **The controller never edits production code in TDD.** When \`mode: wave-fanout\` and \`pathConflicts: []\`, fan out the wave in a SINGLE controller message — one \`Task\` per ready slice, side by side. Each \`slice-builder\` span owns its full RED → GREEN → REFACTOR → DOC cycle. Mirror plan \`dependsOn\` ordering between waves.
|
|
217
|
+
5. **TDD:** When \`currentStage\` is \`tdd\`, run \`wave-status --json\`, then reconcile the managed **Parallel Execution Plan** in \`05-plan.md\` with \`wave-plans/wave-NN.md\`. **The controller never edits production code in TDD.** When \`mode: wave-fanout\` and \`pathConflicts: []\`, fan out the wave in a SINGLE controller message — one \`Task\` per ready slice, side by side. If \`mode: blocked\`, resolve overlaps first. Each \`slice-builder\` span owns its full RED → GREEN → REFACTOR → DOC cycle. Mirror plan \`dependsOn\` ordering between waves.
|
|
218
218
|
6. **Wave resume:** Parallelize unfinished members; never restart completed lanes. Integration-overseer follows \`integrationCheckRequired\`; when skipped, emit \`cclaw_integration_overseer_skipped\` per the hook contract.
|
|
219
219
|
7. If incomplete → load current stage skill and execute.
|
|
220
220
|
8. If complete → advance to next stage and execute. **Auto-advance:** when \`stage-complete\` returns \`ok\`, immediately load the next stage skill and continue without waiting for the user to retype \`/cc\`.
|
package/dist/delegation.js
CHANGED
package/dist/flow-state.d.ts
CHANGED
|
@@ -124,7 +124,7 @@ export interface FlowState {
|
|
|
124
124
|
* Minimum elapsed milliseconds between `acknowledged` and `completed`
|
|
125
125
|
* for a `slice-builder --phase green` row. The hook helper rejects
|
|
126
126
|
* fast-greens (`completedTs - ackTs < this`) with `green_evidence_too_fresh`
|
|
127
|
-
* unless the dispatch carries `--
|
|
127
|
+
* unless the dispatch carries `--green-mode=observational`.
|
|
128
128
|
*
|
|
129
129
|
* Default 4000ms when omitted (see `effectiveTddGreenMinElapsedMs`).
|
|
130
130
|
* Operators tuning the floor for very fast suites may set it lower
|
package/dist/install.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface InitOptions {
|
|
|
6
6
|
}
|
|
7
7
|
export interface SyncOptions {
|
|
8
8
|
harnesses?: HarnessId[];
|
|
9
|
+
check?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function initCclaw(options: InitOptions): Promise<void>;
|
|
11
12
|
export declare function syncCclaw(projectRoot: string, options?: SyncOptions): Promise<void>;
|
package/dist/install.js
CHANGED
|
@@ -750,6 +750,49 @@ async function writeHooks(projectRoot, config) {
|
|
|
750
750
|
// OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
+
async function canonicalHookScripts() {
|
|
754
|
+
const hookRuntimeOptions = {};
|
|
755
|
+
const bundledHookRuntime = await readBundledRunHookRuntimeScript(hookRuntimeOptions);
|
|
756
|
+
return {
|
|
757
|
+
"stage-complete.mjs": stageCompleteScript(),
|
|
758
|
+
"start-flow.mjs": startFlowScript(),
|
|
759
|
+
"cancel-run.mjs": cancelRunScript(),
|
|
760
|
+
"run-hook.mjs": bundledHookRuntime ?? nodeHookRuntimeScript(hookRuntimeOptions),
|
|
761
|
+
"run-hook.cmd": runHookCmdScript(),
|
|
762
|
+
"delegation-record.mjs": delegationRecordScript(),
|
|
763
|
+
"slice-commit.mjs": sliceCommitScript(),
|
|
764
|
+
"opencode-plugin.mjs": opencodePluginJs()
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async function checkManagedHookDrift(projectRoot) {
|
|
768
|
+
const hooksDir = runtimePath(projectRoot, "hooks");
|
|
769
|
+
const canonical = await canonicalHookScripts();
|
|
770
|
+
const findings = [];
|
|
771
|
+
for (const [fileName, expectedSource] of Object.entries(canonical)) {
|
|
772
|
+
const targetPath = path.join(hooksDir, fileName);
|
|
773
|
+
let actual;
|
|
774
|
+
try {
|
|
775
|
+
actual = await fs.readFile(targetPath);
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
findings.push({ file: fileName, reason: "missing" });
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const expected = Buffer.from(expectedSource, "utf8");
|
|
782
|
+
if (!actual.equals(expected)) {
|
|
783
|
+
findings.push({ file: fileName, reason: "content_mismatch" });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return findings;
|
|
787
|
+
}
|
|
788
|
+
function formatManagedHookDriftError(findings) {
|
|
789
|
+
const details = findings
|
|
790
|
+
.map((finding) => `- .cclaw/hooks/${finding.file}: ${finding.reason === "missing" ? "missing" : "content differs from canonical renderer"}`)
|
|
791
|
+
.join("\n");
|
|
792
|
+
return ("[sync --check] Managed hook drift detected.\n" +
|
|
793
|
+
`${details}\n` +
|
|
794
|
+
"Re-run `npx cclaw-cli sync` to rewrite managed hooks.");
|
|
795
|
+
}
|
|
753
796
|
async function ensureKnowledgeStore(projectRoot) {
|
|
754
797
|
const storePath = runtimePath(projectRoot, "knowledge.jsonl");
|
|
755
798
|
if (!(await exists(storePath))) {
|
|
@@ -1064,6 +1107,13 @@ export async function syncCclaw(projectRoot, options = {}) {
|
|
|
1064
1107
|
if (options.harnesses !== undefined && options.harnesses.length === 0) {
|
|
1065
1108
|
throw new Error("Select at least one harness.");
|
|
1066
1109
|
}
|
|
1110
|
+
if (options.check === true) {
|
|
1111
|
+
const drift = await checkManagedHookDrift(projectRoot);
|
|
1112
|
+
if (drift.length > 0) {
|
|
1113
|
+
throw new Error(formatManagedHookDriftError(drift));
|
|
1114
|
+
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1067
1117
|
const configExists = await exists(configPath(projectRoot));
|
|
1068
1118
|
let config = await readConfig(projectRoot);
|
|
1069
1119
|
if (!configExists) {
|
|
@@ -4,6 +4,7 @@ import { RUNTIME_ROOT } from "../../constants.js";
|
|
|
4
4
|
import { createInitialFlowState } from "../../flow-state.js";
|
|
5
5
|
import { readFlowState, writeFlowState } from "../../runs.js";
|
|
6
6
|
import { listExistingFiles, listFilesUnder, pathExists } from "./helpers.js";
|
|
7
|
+
import { STACK_DISCOVERY_DIR_MARKERS, STACK_DISCOVERY_MARKERS } from "../../stack-detection.js";
|
|
7
8
|
import { TRACK_STAGES } from "../../types.js";
|
|
8
9
|
import { buildValidationReport } from "./advance.js";
|
|
9
10
|
import { carriedCompletedStageCatalog, completedStageClosureEvidenceIssues, firstIncompleteStageForTrack } from "./verify.js";
|
|
@@ -91,24 +92,11 @@ export async function discoverStartFlowContext(projectRoot) {
|
|
|
91
92
|
lines.push(originFiles.length > 0
|
|
92
93
|
? `- Origin docs scanned: found ${originFiles.join(", ")}.`
|
|
93
94
|
: "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
|
|
94
|
-
const stackMarkers = await listExistingFiles(projectRoot, [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
".python-version",
|
|
100
|
-
"go.mod",
|
|
101
|
-
"Cargo.toml",
|
|
102
|
-
"pom.xml",
|
|
103
|
-
"build.gradle",
|
|
104
|
-
"build.gradle.kts",
|
|
105
|
-
"Dockerfile",
|
|
106
|
-
"docker-compose.yml",
|
|
107
|
-
"docker-compose.yaml",
|
|
108
|
-
".gitlab-ci.yml"
|
|
109
|
-
]);
|
|
110
|
-
if (await pathExists(projectRoot, ".github/workflows")) {
|
|
111
|
-
stackMarkers.push(".github/workflows/");
|
|
95
|
+
const stackMarkers = await listExistingFiles(projectRoot, [...STACK_DISCOVERY_MARKERS]);
|
|
96
|
+
for (const markerDir of STACK_DISCOVERY_DIR_MARKERS) {
|
|
97
|
+
if (await pathExists(projectRoot, markerDir)) {
|
|
98
|
+
stackMarkers.push(`${markerDir}/`);
|
|
99
|
+
}
|
|
112
100
|
}
|
|
113
101
|
lines.push(stackMarkers.length > 0
|
|
114
102
|
? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
|
|
@@ -3,6 +3,7 @@ import { RUNTIME_ROOT } from "../../constants.js";
|
|
|
3
3
|
import { stageSchema } from "../../content/stage-schema.js";
|
|
4
4
|
import { readFlowState } from "../../runs.js";
|
|
5
5
|
import { TRACK_STAGES } from "../../types.js";
|
|
6
|
+
import { STACK_DISCOVERY_DIR_MARKERS, STACK_DISCOVERY_MARKERS } from "../../stack-detection.js";
|
|
6
7
|
import { coerceCandidateFlowState } from "./flow-state-coercion.js";
|
|
7
8
|
import { buildValidationReport } from "./advance.js";
|
|
8
9
|
import fs from "node:fs/promises";
|
|
@@ -171,24 +172,11 @@ export async function discoverStartFlowContext(projectRoot) {
|
|
|
171
172
|
lines.push(originFiles.length > 0
|
|
172
173
|
? `- Origin docs scanned: found ${originFiles.join(", ")}.`
|
|
173
174
|
: "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
|
|
174
|
-
const stackMarkers = await listExistingFiles(projectRoot, [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
".python-version",
|
|
180
|
-
"go.mod",
|
|
181
|
-
"Cargo.toml",
|
|
182
|
-
"pom.xml",
|
|
183
|
-
"build.gradle",
|
|
184
|
-
"build.gradle.kts",
|
|
185
|
-
"Dockerfile",
|
|
186
|
-
"docker-compose.yml",
|
|
187
|
-
"docker-compose.yaml",
|
|
188
|
-
".gitlab-ci.yml"
|
|
189
|
-
]);
|
|
190
|
-
if (await pathExists(projectRoot, ".github/workflows")) {
|
|
191
|
-
stackMarkers.push(".github/workflows/");
|
|
175
|
+
const stackMarkers = await listExistingFiles(projectRoot, [...STACK_DISCOVERY_MARKERS]);
|
|
176
|
+
for (const markerDir of STACK_DISCOVERY_DIR_MARKERS) {
|
|
177
|
+
if (await pathExists(projectRoot, markerDir)) {
|
|
178
|
+
stackMarkers.push(`${markerDir}/`);
|
|
179
|
+
}
|
|
192
180
|
}
|
|
193
181
|
lines.push(stackMarkers.length > 0
|
|
194
182
|
? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
|
|
@@ -16,7 +16,7 @@ export interface WaveStatusNextDispatch {
|
|
|
16
16
|
waveId: string | null;
|
|
17
17
|
readyToDispatch: string[];
|
|
18
18
|
pathConflicts: string[];
|
|
19
|
-
mode: "single-slice" | "wave-fanout" | "none";
|
|
19
|
+
mode: "single-slice" | "wave-fanout" | "blocked" | "none";
|
|
20
20
|
}
|
|
21
21
|
export interface WaveStatusReport {
|
|
22
22
|
activeRunId: string;
|
|
@@ -4,6 +4,8 @@ import { RUNTIME_ROOT } from "../constants.js";
|
|
|
4
4
|
import { readDelegationEvents, readDelegationLedger } from "../delegation.js";
|
|
5
5
|
import { readFlowState } from "../runs.js";
|
|
6
6
|
import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./plan-split-waves.js";
|
|
7
|
+
const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
|
|
8
|
+
const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
|
|
7
9
|
function parseArgs(tokens) {
|
|
8
10
|
const args = { format: "json" };
|
|
9
11
|
for (const token of tokens) {
|
|
@@ -33,6 +35,92 @@ function classifyWaveStatus(total, closedCount) {
|
|
|
33
35
|
return "closed";
|
|
34
36
|
return "partial";
|
|
35
37
|
}
|
|
38
|
+
function parsePipeRow(trimmedLine) {
|
|
39
|
+
const inner = trimmedLine.replace(/^\|/u, "").replace(/\|\s*$/u, "");
|
|
40
|
+
return inner.split("|").map((cell) => cell.trim());
|
|
41
|
+
}
|
|
42
|
+
function normalizePathToken(raw) {
|
|
43
|
+
return raw.trim().replace(/^`|`$/gu, "").replace(/^\.\/+/u, "");
|
|
44
|
+
}
|
|
45
|
+
function parseManagedWaveClaimedPaths(planMarkdown) {
|
|
46
|
+
const out = new Map();
|
|
47
|
+
const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
|
|
48
|
+
const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
|
|
49
|
+
if (startIdx < 0 || endIdx <= startIdx)
|
|
50
|
+
return out;
|
|
51
|
+
const body = planMarkdown.slice(startIdx + PARALLEL_EXEC_MANAGED_START.length, endIdx);
|
|
52
|
+
const lines = body.split(/\r?\n/u);
|
|
53
|
+
let currentWaveId = null;
|
|
54
|
+
let headerIdx = new Map();
|
|
55
|
+
for (const rawLine of lines) {
|
|
56
|
+
const trimmed = rawLine.trim();
|
|
57
|
+
const waveMatch = /^###\s+Wave\s+(?:W-)?(\d+)\b/iu.exec(trimmed);
|
|
58
|
+
if (waveMatch) {
|
|
59
|
+
currentWaveId = `W-${waveMatch[1].padStart(2, "0")}`;
|
|
60
|
+
if (!out.has(currentWaveId)) {
|
|
61
|
+
out.set(currentWaveId, new Map());
|
|
62
|
+
}
|
|
63
|
+
headerIdx = new Map();
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!currentWaveId || !trimmed.startsWith("|"))
|
|
67
|
+
continue;
|
|
68
|
+
const cells = parsePipeRow(trimmed);
|
|
69
|
+
if (cells.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const first = cells[0].toLowerCase();
|
|
72
|
+
if (first === "sliceid" || first === "slice id") {
|
|
73
|
+
headerIdx = new Map();
|
|
74
|
+
for (let i = 0; i < cells.length; i += 1) {
|
|
75
|
+
const key = cells[i].toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
76
|
+
if (key.length > 0 && !headerIdx.has(key)) {
|
|
77
|
+
headerIdx.set(key, i);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const sliceId = cells[0].trim().toUpperCase();
|
|
86
|
+
if (!/^S-\d+$/u.test(sliceId))
|
|
87
|
+
continue;
|
|
88
|
+
const pathsIdx = headerIdx.get("claimedpaths");
|
|
89
|
+
const rawPaths = pathsIdx !== undefined ? (cells[pathsIdx] ?? "") : "";
|
|
90
|
+
const claimedPaths = rawPaths.length === 0
|
|
91
|
+
? []
|
|
92
|
+
: rawPaths
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((p) => normalizePathToken(p))
|
|
95
|
+
.filter((p) => p.length > 0);
|
|
96
|
+
out.get(currentWaveId).set(sliceId, claimedPaths);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function detectPathConflicts(readySlices, bySlice) {
|
|
101
|
+
const conflicts = new Set();
|
|
102
|
+
const ordered = [...readySlices].sort();
|
|
103
|
+
for (let i = 0; i < ordered.length; i += 1) {
|
|
104
|
+
const leftSlice = ordered[i];
|
|
105
|
+
const leftPaths = bySlice.get(leftSlice) ?? [];
|
|
106
|
+
if (leftPaths.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const leftSet = new Set(leftPaths);
|
|
109
|
+
for (let j = i + 1; j < ordered.length; j += 1) {
|
|
110
|
+
const rightSlice = ordered[j];
|
|
111
|
+
const rightPaths = bySlice.get(rightSlice) ?? [];
|
|
112
|
+
if (rightPaths.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
for (const pathToken of rightPaths) {
|
|
115
|
+
if (!leftSet.has(pathToken))
|
|
116
|
+
continue;
|
|
117
|
+
conflicts.add(`${leftSlice}:${pathToken}`);
|
|
118
|
+
conflicts.add(`${rightSlice}:${pathToken}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [...conflicts].sort();
|
|
123
|
+
}
|
|
36
124
|
const TERMINAL_PHASES = new Set([
|
|
37
125
|
"refactor",
|
|
38
126
|
"refactor-deferred",
|
|
@@ -202,11 +290,20 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
202
290
|
}
|
|
203
291
|
else {
|
|
204
292
|
const readyToDispatch = [...firstOpenWave.readyMembers].sort();
|
|
293
|
+
const claimedPathsByWave = parseManagedWaveClaimedPaths(planRaw);
|
|
294
|
+
const conflicts = detectPathConflicts(readyToDispatch, claimedPathsByWave.get(firstOpenWave.waveId) ?? new Map());
|
|
295
|
+
const mode = conflicts.length > 0
|
|
296
|
+
? "blocked"
|
|
297
|
+
: readyToDispatch.length > 1
|
|
298
|
+
? "wave-fanout"
|
|
299
|
+
: readyToDispatch.length === 1
|
|
300
|
+
? "single-slice"
|
|
301
|
+
: "none";
|
|
205
302
|
nextDispatch = {
|
|
206
303
|
waveId: firstOpenWave.waveId,
|
|
207
304
|
readyToDispatch,
|
|
208
|
-
pathConflicts:
|
|
209
|
-
mode
|
|
305
|
+
pathConflicts: conflicts,
|
|
306
|
+
mode
|
|
210
307
|
};
|
|
211
308
|
}
|
|
212
309
|
return {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface StackReviewRouteProfile {
|
|
2
|
+
stack: string;
|
|
3
|
+
/**
|
|
4
|
+
* Signals shown in review routing documentation/skills.
|
|
5
|
+
* These are human-facing pointers, not strict parsers.
|
|
6
|
+
*/
|
|
7
|
+
reviewSignals: string[];
|
|
8
|
+
/** Root-level markers used by start-flow context discovery. */
|
|
9
|
+
discoveryMarkers: string[];
|
|
10
|
+
focus: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const STACK_REVIEW_ROUTE_PROFILES: readonly StackReviewRouteProfile[];
|
|
13
|
+
/**
|
|
14
|
+
* Unified root-marker list used by start-flow context discovery.
|
|
15
|
+
* Keep this in one place so stage skill routing and start-flow scanning
|
|
16
|
+
* evolve together.
|
|
17
|
+
*/
|
|
18
|
+
export declare const STACK_DISCOVERY_MARKERS: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Directory markers (checked with pathExists) for stack discovery.
|
|
21
|
+
*/
|
|
22
|
+
export declare const STACK_DISCOVERY_DIR_MARKERS: readonly string[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const STACK_REVIEW_ROUTE_PROFILES = [
|
|
2
|
+
{
|
|
3
|
+
stack: "TypeScript/JavaScript",
|
|
4
|
+
reviewSignals: ["package.json", "tsconfig.json"],
|
|
5
|
+
discoveryMarkers: ["package.json", "tsconfig.json", "jsconfig.json"],
|
|
6
|
+
focus: "type safety, package scripts, build/test config, dependency boundaries"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
stack: "Python",
|
|
10
|
+
reviewSignals: ["pyproject.toml", "requirements.txt"],
|
|
11
|
+
discoveryMarkers: ["pyproject.toml", "requirements.txt", "requirements-dev.txt", ".python-version"],
|
|
12
|
+
focus: "packaging, virtualenv assumptions, typing, pytest or unittest evidence"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
stack: "Ruby/Rails",
|
|
16
|
+
reviewSignals: ["Gemfile", "config/"],
|
|
17
|
+
discoveryMarkers: ["Gemfile"],
|
|
18
|
+
focus: "Rails conventions, migrations, routes/controllers, RSpec or Minitest evidence"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
stack: "Go",
|
|
22
|
+
reviewSignals: ["go.mod"],
|
|
23
|
+
discoveryMarkers: ["go.mod"],
|
|
24
|
+
focus: "interfaces, concurrency, error handling, go test coverage"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
stack: "Rust",
|
|
28
|
+
reviewSignals: ["Cargo.toml"],
|
|
29
|
+
discoveryMarkers: ["Cargo.toml"],
|
|
30
|
+
focus: "ownership, error/result handling, feature flags, cargo test coverage"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
const EXTRA_DISCOVERY_MARKERS = [
|
|
34
|
+
"pom.xml",
|
|
35
|
+
"build.gradle",
|
|
36
|
+
"build.gradle.kts",
|
|
37
|
+
"Dockerfile",
|
|
38
|
+
"docker-compose.yml",
|
|
39
|
+
"docker-compose.yaml",
|
|
40
|
+
".gitlab-ci.yml"
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Unified root-marker list used by start-flow context discovery.
|
|
44
|
+
* Keep this in one place so stage skill routing and start-flow scanning
|
|
45
|
+
* evolve together.
|
|
46
|
+
*/
|
|
47
|
+
export const STACK_DISCOVERY_MARKERS = [
|
|
48
|
+
...new Set([
|
|
49
|
+
...STACK_REVIEW_ROUTE_PROFILES.flatMap((profile) => profile.discoveryMarkers),
|
|
50
|
+
...EXTRA_DISCOVERY_MARKERS
|
|
51
|
+
])
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Directory markers (checked with pathExists) for stack discovery.
|
|
55
|
+
*/
|
|
56
|
+
export const STACK_DISCOVERY_DIR_MARKERS = [
|
|
57
|
+
".github/workflows"
|
|
58
|
+
];
|