cclaw-cli 7.1.1 → 7.3.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/cli.d.ts +1 -0
- package/dist/cli.js +12 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +46 -4
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/content/core-agents.js +2 -1
- package/dist/content/hooks.js +239 -40
- package/dist/content/stage-schema.js +7 -32
- package/dist/delegation.d.ts +5 -0
- 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 +51 -1
- package/dist/internal/advance-stage/start-flow.js +6 -18
- package/dist/internal/advance-stage/verify.js +6 -18
- package/dist/internal/slice-commit.js +179 -10
- package/dist/runtime/run-hook.mjs +1 -0
- package/dist/stack-detection.d.ts +22 -0
- package/dist/stack-detection.js +58 -0
- package/dist/types.d.ts +12 -0
- package/dist/worktree-manager.d.ts +20 -0
- package/dist/worktree-manager.js +108 -0
- package/package.json +1 -1
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"
|
|
@@ -580,6 +614,10 @@ function buildRow(args, status, runId, now, options) {
|
|
|
580
614
|
riskTierRaw === "low" || riskTierRaw === "medium" || riskTierRaw === "high"
|
|
581
615
|
? riskTierRaw
|
|
582
616
|
: undefined;
|
|
617
|
+
const worktreePath =
|
|
618
|
+
typeof args["worktree-path"] === "string" && args["worktree-path"].trim().length > 0
|
|
619
|
+
? args["worktree-path"].trim()
|
|
620
|
+
: undefined;
|
|
583
621
|
return {
|
|
584
622
|
stage: args.stage,
|
|
585
623
|
agent: args.agent,
|
|
@@ -603,6 +641,7 @@ function buildRow(args, status, runId, now, options) {
|
|
|
603
641
|
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
604
642
|
allowParallel: args["allow-parallel"] === true ? true : undefined,
|
|
605
643
|
claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined,
|
|
644
|
+
worktreePath,
|
|
606
645
|
sliceId,
|
|
607
646
|
phase,
|
|
608
647
|
refactorOutcome,
|
|
@@ -911,7 +950,8 @@ async function findLegacyEntry(root, spanId) {
|
|
|
911
950
|
// the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS in
|
|
912
951
|
// src/delegation.ts.
|
|
913
952
|
const VALID_AUDIT_KINDS = new Set([
|
|
914
|
-
"cclaw_integration_overseer_skipped"
|
|
953
|
+
"cclaw_integration_overseer_skipped",
|
|
954
|
+
"cclaw_allow_parallel_auto_flip"
|
|
915
955
|
]);
|
|
916
956
|
|
|
917
957
|
async function runAuditEmit(args, json) {
|
|
@@ -1216,6 +1256,24 @@ async function runSliceCommitIfNeeded(root, row, runId) {
|
|
|
1216
1256
|
"--span-id=" + spanId,
|
|
1217
1257
|
"--run-id=" + runId
|
|
1218
1258
|
];
|
|
1259
|
+
let explicitWorktreePath =
|
|
1260
|
+
typeof row.worktreePath === "string" && row.worktreePath.trim().length > 0
|
|
1261
|
+
? row.worktreePath.trim()
|
|
1262
|
+
: "";
|
|
1263
|
+
if (explicitWorktreePath.length === 0) {
|
|
1264
|
+
const priorLedger = await readDelegationLedgerEntries(root);
|
|
1265
|
+
const priorSpanPath = priorLedger
|
|
1266
|
+
.filter((entry) => entry && entry.spanId === spanId && entry.runId === runId)
|
|
1267
|
+
.map((entry) =>
|
|
1268
|
+
entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
|
|
1269
|
+
.find((value) => value.length > 0);
|
|
1270
|
+
if (priorSpanPath) {
|
|
1271
|
+
explicitWorktreePath = priorSpanPath;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (explicitWorktreePath.length > 0) {
|
|
1275
|
+
helperArgs.push("--worktree-path=" + explicitWorktreePath);
|
|
1276
|
+
}
|
|
1219
1277
|
if (typeof row.taskId === "string" && row.taskId.trim().length > 0) {
|
|
1220
1278
|
helperArgs.push("--task-id=" + row.taskId.trim());
|
|
1221
1279
|
}
|
|
@@ -1285,6 +1343,91 @@ async function runSliceCommitIfNeeded(root, row, runId) {
|
|
|
1285
1343
|
});
|
|
1286
1344
|
}
|
|
1287
1345
|
|
|
1346
|
+
async function runSliceWorktreePrepareIfNeeded(root, row, runId) {
|
|
1347
|
+
if (
|
|
1348
|
+
row.stage !== "tdd" ||
|
|
1349
|
+
row.agent !== "slice-builder" ||
|
|
1350
|
+
row.status !== "scheduled"
|
|
1351
|
+
) {
|
|
1352
|
+
return { ok: true, skipped: true };
|
|
1353
|
+
}
|
|
1354
|
+
const sliceId = typeof row.sliceId === "string" ? row.sliceId.trim() : "";
|
|
1355
|
+
const spanId = typeof row.spanId === "string" ? row.spanId.trim() : "";
|
|
1356
|
+
if (sliceId.length === 0 || spanId.length === 0) {
|
|
1357
|
+
return { ok: true, skipped: true };
|
|
1358
|
+
}
|
|
1359
|
+
const helperPath = path.join(root, RUNTIME_ROOT, "hooks", "slice-commit.mjs");
|
|
1360
|
+
if (!(await exists(helperPath))) {
|
|
1361
|
+
return { ok: true, skipped: true };
|
|
1362
|
+
}
|
|
1363
|
+
const helperArgs = [
|
|
1364
|
+
helperPath,
|
|
1365
|
+
"--json",
|
|
1366
|
+
"--quiet",
|
|
1367
|
+
"--prepare-worktree",
|
|
1368
|
+
"--slice=" + sliceId,
|
|
1369
|
+
"--span-id=" + spanId,
|
|
1370
|
+
"--run-id=" + runId
|
|
1371
|
+
];
|
|
1372
|
+
if (Array.isArray(row.claimedPaths) && row.claimedPaths.length > 0) {
|
|
1373
|
+
helperArgs.push("--claimed-paths=" + row.claimedPaths.join(","));
|
|
1374
|
+
}
|
|
1375
|
+
return await new Promise((resolve) => {
|
|
1376
|
+
const child = spawn(process.execPath, helperArgs, {
|
|
1377
|
+
cwd: root,
|
|
1378
|
+
env: process.env,
|
|
1379
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1380
|
+
});
|
|
1381
|
+
let out = "";
|
|
1382
|
+
let err = "";
|
|
1383
|
+
child.stdout.on("data", (chunk) => {
|
|
1384
|
+
out += String(chunk ?? "");
|
|
1385
|
+
});
|
|
1386
|
+
child.stderr.on("data", (chunk) => {
|
|
1387
|
+
err += String(chunk ?? "");
|
|
1388
|
+
});
|
|
1389
|
+
child.on("error", (error) => {
|
|
1390
|
+
resolve({
|
|
1391
|
+
ok: false,
|
|
1392
|
+
errorCode: "worktree_prepare_failed",
|
|
1393
|
+
details: {
|
|
1394
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
child.on("close", (code) => {
|
|
1399
|
+
let payload = null;
|
|
1400
|
+
const trimmed = out.trim();
|
|
1401
|
+
if (trimmed.length > 0) {
|
|
1402
|
+
try {
|
|
1403
|
+
payload = JSON.parse(trimmed);
|
|
1404
|
+
} catch {
|
|
1405
|
+
payload = null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (code === 0) {
|
|
1409
|
+
resolve({ ok: true, payload });
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const payloadCode =
|
|
1413
|
+
payload && typeof payload === "object" && typeof payload.errorCode === "string"
|
|
1414
|
+
? payload.errorCode
|
|
1415
|
+
: "worktree_prepare_failed";
|
|
1416
|
+
resolve({
|
|
1417
|
+
ok: false,
|
|
1418
|
+
errorCode: payloadCode,
|
|
1419
|
+
details:
|
|
1420
|
+
payload && typeof payload === "object"
|
|
1421
|
+
? payload
|
|
1422
|
+
: {
|
|
1423
|
+
stderr: err.trim(),
|
|
1424
|
+
stdout: out.trim()
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1288
1431
|
async function main() {
|
|
1289
1432
|
const args = parseArgs(process.argv.slice(2));
|
|
1290
1433
|
const json = args.json !== undefined;
|
|
@@ -1347,15 +1490,15 @@ async function main() {
|
|
|
1347
1490
|
return;
|
|
1348
1491
|
}
|
|
1349
1492
|
if (args.phase === "refactor-deferred") {
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
(
|
|
1355
|
-
(
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1493
|
+
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1494
|
+
if (rationaleQuality !== "ok") {
|
|
1495
|
+
if (rationaleQuality === "missing") {
|
|
1496
|
+
problems.push("--phase=refactor-deferred requires --refactor-rationale=<text>");
|
|
1497
|
+
} else if (rationaleQuality === "too-short") {
|
|
1498
|
+
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1499
|
+
} else {
|
|
1500
|
+
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1501
|
+
}
|
|
1359
1502
|
emitProblems(problems, json, 2);
|
|
1360
1503
|
return;
|
|
1361
1504
|
}
|
|
@@ -1375,15 +1518,15 @@ async function main() {
|
|
|
1375
1518
|
return;
|
|
1376
1519
|
}
|
|
1377
1520
|
if (args["refactor-outcome"] === "deferred") {
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
(
|
|
1383
|
-
(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1521
|
+
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1522
|
+
if (rationaleQuality !== "ok") {
|
|
1523
|
+
if (rationaleQuality === "missing") {
|
|
1524
|
+
problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text>");
|
|
1525
|
+
} else if (rationaleQuality === "too-short") {
|
|
1526
|
+
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1527
|
+
} else {
|
|
1528
|
+
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1529
|
+
}
|
|
1387
1530
|
emitProblems(problems, json, 2);
|
|
1388
1531
|
return;
|
|
1389
1532
|
}
|
|
@@ -1398,6 +1541,21 @@ async function main() {
|
|
|
1398
1541
|
emitProblems(problems, json, 2);
|
|
1399
1542
|
return;
|
|
1400
1543
|
}
|
|
1544
|
+
if (args["override-cap"] !== undefined) {
|
|
1545
|
+
const overrideRaw = String(args["override-cap"]).trim();
|
|
1546
|
+
const overrideNum = Number(overrideRaw);
|
|
1547
|
+
if (!Number.isInteger(overrideNum) || overrideNum < 1) {
|
|
1548
|
+
problems.push("--override-cap must be an integer >= 1");
|
|
1549
|
+
emitProblems(problems, json, 2);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const reasonRaw = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
1553
|
+
if (reasonRaw.length === 0) {
|
|
1554
|
+
problems.push("--override-cap requires --reason=<slug>");
|
|
1555
|
+
emitProblems(problems, json, 2);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1401
1559
|
|
|
1402
1560
|
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
|
|
1403
1561
|
for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
@@ -1460,6 +1618,16 @@ async function main() {
|
|
|
1460
1618
|
const status = args.status;
|
|
1461
1619
|
const priorLedger = await readDelegationLedgerEntries(root);
|
|
1462
1620
|
const priorForSpan = priorLedger.filter((e) => e && e.spanId === args["span-id"]);
|
|
1621
|
+
const inheritedWorktreePath = priorForSpan
|
|
1622
|
+
.map((entry) =>
|
|
1623
|
+
entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
|
|
1624
|
+
.find((value) => value.length > 0);
|
|
1625
|
+
if (
|
|
1626
|
+
inheritedWorktreePath &&
|
|
1627
|
+
(typeof args["worktree-path"] !== "string" || args["worktree-path"].trim().length === 0)
|
|
1628
|
+
) {
|
|
1629
|
+
args["worktree-path"] = inheritedWorktreePath;
|
|
1630
|
+
}
|
|
1463
1631
|
const inheritedStartTs = priorForSpan
|
|
1464
1632
|
.map((e) => e.startTs)
|
|
1465
1633
|
.filter((ts) => typeof ts === "string" && ts.length > 0)
|
|
@@ -1479,6 +1647,7 @@ async function main() {
|
|
|
1479
1647
|
const row = buildRow(args, status, runId, now, { spanStartTs });
|
|
1480
1648
|
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
1481
1649
|
const event = { ...clean, event: status, eventTs: now };
|
|
1650
|
+
let autoParallelAuditEvent = null;
|
|
1482
1651
|
|
|
1483
1652
|
const violation = validateMonotonicTimestampsInline(clean, priorLedger);
|
|
1484
1653
|
if (violation) {
|
|
@@ -1487,8 +1656,8 @@ async function main() {
|
|
|
1487
1656
|
}
|
|
1488
1657
|
|
|
1489
1658
|
// 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.
|
|
1659
|
+
// dedup so disjoint claimedPaths can auto-promote to allowParallel,
|
|
1660
|
+
// emit an audit event for the flip, and bypass the duplicate guard.
|
|
1492
1661
|
if (status === "scheduled") {
|
|
1493
1662
|
const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
|
|
1494
1663
|
const activeForRun = computeActiveSubagentsInline(sameRunPrior);
|
|
@@ -1501,6 +1670,18 @@ async function main() {
|
|
|
1501
1670
|
clean.allowParallel = true;
|
|
1502
1671
|
args["allow-parallel"] = true;
|
|
1503
1672
|
event.allowParallel = true;
|
|
1673
|
+
autoParallelAuditEvent = {
|
|
1674
|
+
event: "cclaw_allow_parallel_auto_flip",
|
|
1675
|
+
runId,
|
|
1676
|
+
ts: now,
|
|
1677
|
+
eventTs: now,
|
|
1678
|
+
stage: clean.stage,
|
|
1679
|
+
agent: clean.agent,
|
|
1680
|
+
spanId: clean.spanId,
|
|
1681
|
+
sliceId: clean.sliceId,
|
|
1682
|
+
reason: "disjoint-claimed-paths-auto-flip",
|
|
1683
|
+
claimedPaths: Array.isArray(clean.claimedPaths) ? clean.claimedPaths : []
|
|
1684
|
+
};
|
|
1504
1685
|
}
|
|
1505
1686
|
const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
|
|
1506
1687
|
const override = overrideRaw !== null ? Number(overrideRaw) : null;
|
|
@@ -1509,6 +1690,24 @@ async function main() {
|
|
|
1509
1690
|
emitErrorJson("dispatch_cap", capViolation, json);
|
|
1510
1691
|
return;
|
|
1511
1692
|
}
|
|
1693
|
+
const preparedWorktree = await runSliceWorktreePrepareIfNeeded(root, clean, runId);
|
|
1694
|
+
if (!preparedWorktree.ok) {
|
|
1695
|
+
emitErrorJson(
|
|
1696
|
+
preparedWorktree.errorCode || "worktree_prepare_failed",
|
|
1697
|
+
preparedWorktree.details || {},
|
|
1698
|
+
json
|
|
1699
|
+
);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (
|
|
1703
|
+
preparedWorktree.payload &&
|
|
1704
|
+
typeof preparedWorktree.payload === "object" &&
|
|
1705
|
+
typeof preparedWorktree.payload.worktreePath === "string" &&
|
|
1706
|
+
preparedWorktree.payload.worktreePath.trim().length > 0
|
|
1707
|
+
) {
|
|
1708
|
+
clean.worktreePath = preparedWorktree.payload.worktreePath.trim();
|
|
1709
|
+
event.worktreePath = clean.worktreePath;
|
|
1710
|
+
}
|
|
1512
1711
|
}
|
|
1513
1712
|
const dedupViolation = enforceDispatchDedupInline(clean, priorLedger, args);
|
|
1514
1713
|
if (dedupViolation) {
|
|
@@ -1548,8 +1747,7 @@ async function main() {
|
|
|
1548
1747
|
// 3. green_evidence_too_fresh — completedTs minus ackTs must be
|
|
1549
1748
|
// >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
|
|
1550
1749
|
// Escape hatch for legitimate observational GREENs (cross-slice
|
|
1551
|
-
// handoff, no-op verification):
|
|
1552
|
-
// --green-mode=observational. Both flags are required.
|
|
1750
|
+
// handoff, no-op verification): --green-mode=observational.
|
|
1553
1751
|
if (
|
|
1554
1752
|
clean.stage === "tdd" &&
|
|
1555
1753
|
clean.agent === "slice-builder" &&
|
|
@@ -1559,7 +1757,6 @@ async function main() {
|
|
|
1559
1757
|
const isObservational =
|
|
1560
1758
|
typeof args["green-mode"] === "string" &&
|
|
1561
1759
|
args["green-mode"].trim().toLowerCase() === "observational";
|
|
1562
|
-
const allowFastGreen = args["allow-fast-green"] === true;
|
|
1563
1760
|
const greenEvidenceFirst =
|
|
1564
1761
|
Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
|
|
1565
1762
|
? String(clean.evidenceRefs[0])
|
|
@@ -1586,10 +1783,9 @@ async function main() {
|
|
|
1586
1783
|
// nothing to verify GREEN against (legacy ledger imports, RED
|
|
1587
1784
|
// happened outside cclaw harness, or test fixtures that bypass
|
|
1588
1785
|
// RED). Once a RED row is present, the contract becomes
|
|
1589
|
-
// mandatory unless explicitly waived via --
|
|
1590
|
-
// --green-mode=observational.
|
|
1786
|
+
// mandatory unless explicitly waived via --green-mode=observational.
|
|
1591
1787
|
const hasRedContext = redEvidenceRef !== null;
|
|
1592
|
-
const escapeFastGreen =
|
|
1788
|
+
const escapeFastGreen = isObservational;
|
|
1593
1789
|
|
|
1594
1790
|
if (hasRedContext && !escapeFastGreen) {
|
|
1595
1791
|
// Check 1: RED test name match.
|
|
@@ -1618,7 +1814,7 @@ async function main() {
|
|
|
1618
1814
|
sliceId: clean.sliceId,
|
|
1619
1815
|
greenEvidenceFirst,
|
|
1620
1816
|
remediation:
|
|
1621
|
-
"evidenceRefs[0] on the GREEN row must contain a passing-assertion line
|
|
1817
|
+
"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
1818
|
},
|
|
1623
1819
|
json
|
|
1624
1820
|
);
|
|
@@ -1657,7 +1853,7 @@ async function main() {
|
|
|
1657
1853
|
elapsedMs: elapsed,
|
|
1658
1854
|
minMs,
|
|
1659
1855
|
remediation:
|
|
1660
|
-
"GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --
|
|
1856
|
+
"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
1857
|
},
|
|
1662
1858
|
json
|
|
1663
1859
|
);
|
|
@@ -1687,6 +1883,9 @@ async function main() {
|
|
|
1687
1883
|
}
|
|
1688
1884
|
|
|
1689
1885
|
await persistEntry(root, runId, clean, event);
|
|
1886
|
+
if (autoParallelAuditEvent) {
|
|
1887
|
+
await appendAuditEventInline(root, autoParallelAuditEvent);
|
|
1888
|
+
}
|
|
1690
1889
|
|
|
1691
1890
|
process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
|
|
1692
1891
|
}
|
|
@@ -1695,7 +1894,7 @@ void main();
|
|
|
1695
1894
|
`;
|
|
1696
1895
|
}
|
|
1697
1896
|
export function sliceCommitScript() {
|
|
1698
|
-
return internalHelperScript("slice-commit", "slice-commit", "Usage: node " + RUNTIME_ROOT + "/hooks/slice-commit.mjs --slice=<S-N> --span-id=<span-id> [--task-id=<T-id>] [--title=<text>] [--run-id=<run-id>] [--claimed-paths=<path1,path2,...>] [--claimed-path=<path> ...] [--json] [--quiet]");
|
|
1897
|
+
return internalHelperScript("slice-commit", "slice-commit", "Usage: node " + RUNTIME_ROOT + "/hooks/slice-commit.mjs --slice=<S-N> --span-id=<span-id> [--task-id=<T-id>] [--title=<text>] [--run-id=<run-id>] [--worktree-path=<abs-or-rel-path>] [--prepare-worktree] [--claimed-paths=<path1,path2,...>] [--claimed-path=<path> ...] [--json] [--quiet]");
|
|
1699
1898
|
}
|
|
1700
1899
|
export function runHookCmdScript() {
|
|
1701
1900
|
return `: << 'CMDBLOCK'
|
|
@@ -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
|
}
|
package/dist/delegation.d.ts
CHANGED
|
@@ -142,6 +142,11 @@ export type DelegationEntry = {
|
|
|
142
142
|
* `src/content/hooks.ts::delegationRecordScript`.
|
|
143
143
|
*/
|
|
144
144
|
claimedPaths?: string[];
|
|
145
|
+
/**
|
|
146
|
+
* Absolute path of the isolated git worktree assigned to this span when
|
|
147
|
+
* `tdd.isolationMode=worktree|auto`.
|
|
148
|
+
*/
|
|
149
|
+
worktreePath?: string;
|
|
145
150
|
/**
|
|
146
151
|
* TDD slice identifier, e.g. `"S-1"`. Recorded by the controller when
|
|
147
152
|
* dispatching `slice-builder` so the artifact linter can auto-derive the
|
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
|
@@ -190,7 +190,7 @@ const DEPRECATED_HOOK_FILES = [
|
|
|
190
190
|
"context-monitor.sh"
|
|
191
191
|
];
|
|
192
192
|
const DEPRECATED_RUNTIME_ROOT_FILES = ["learnings.jsonl", "observations.jsonl"];
|
|
193
|
-
const DEPRECATED_RUNTIME_DIRS = ["evals", "
|
|
193
|
+
const DEPRECATED_RUNTIME_DIRS = ["evals", "references", "contexts"];
|
|
194
194
|
async function resolveGitHooksDir(projectRoot) {
|
|
195
195
|
try {
|
|
196
196
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], {
|
|
@@ -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(", ")}.`
|