cclaw-cli 7.4.0 → 7.6.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 +213 -5
- package/dist/artifact-linter/shared.d.ts +1 -0
- package/dist/artifact-linter/shared.js +5 -0
- package/dist/artifact-linter/ship.js +169 -1
- package/dist/artifact-linter/spec.js +33 -1
- package/dist/artifact-linter/tdd.d.ts +5 -0
- package/dist/artifact-linter/tdd.js +202 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +24 -3
- package/dist/content/core-agents.js +15 -0
- package/dist/content/hooks.js +37 -0
- package/dist/content/stage-schema.js +6 -1
- package/dist/content/stages/plan.js +1 -0
- package/dist/content/stages/ship.js +4 -0
- package/dist/content/stages/spec.js +1 -0
- package/dist/content/stages/tdd.js +2 -0
- package/dist/content/templates.js +5 -0
- package/dist/delegation.d.ts +39 -0
- package/dist/delegation.js +66 -1
- package/dist/gate-evidence.js +10 -12
- package/dist/internal/advance-stage/start-flow.js +13 -4
- package/dist/internal/cohesion-contract-stub.js +2 -14
- package/dist/internal/plan-split-waves.js +19 -14
- package/dist/internal/slice-commit.js +161 -7
- package/dist/internal/wave-status.js +6 -4
- package/dist/stack-detection.d.ts +94 -0
- package/dist/stack-detection.js +431 -0
- package/dist/tdd-cycle.js +7 -5
- package/dist/types.d.ts +22 -0
- package/dist/util/slice-id.d.ts +58 -0
- package/dist/util/slice-id.js +89 -0
- package/package.json +1 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
3
5
|
import { loadTddReadySlicePool, readDelegationLedger, readDelegationEvents, selectReadySlices } from "../delegation.js";
|
|
6
|
+
import { resolveArtifactPath as resolveStageArtifactPath } from "../artifact-paths.js";
|
|
7
|
+
import { exists } from "../fs-utils.js";
|
|
4
8
|
import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "../internal/plan-split-waves.js";
|
|
5
|
-
import {
|
|
9
|
+
import { compareSliceIds } from "../util/slice-id.js";
|
|
10
|
+
import { extractAcceptanceCriterionIdsFromMarkdown, extractH2Sections, evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
|
|
6
11
|
const SLICE_SUMMARY_START = "<!-- auto-start: tdd-slice-summary -->";
|
|
7
12
|
const SLICE_SUMMARY_END = "<!-- auto-end: tdd-slice-summary -->";
|
|
8
13
|
const SLICES_INDEX_START = "<!-- auto-start: slices-index -->";
|
|
9
14
|
const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
10
16
|
/**
|
|
11
17
|
* TDD stage linter.
|
|
12
18
|
*
|
|
@@ -351,6 +357,11 @@ export async function lintTddStage(ctx) {
|
|
|
351
357
|
// simply means main-only mode (legacy fallback).
|
|
352
358
|
const slicesDir = path.join(artifactsDir, "tdd-slices");
|
|
353
359
|
const sliceFiles = await listSliceFiles(slicesDir);
|
|
360
|
+
const specAcceptanceIds = await readSpecAcceptanceCriteriaIds(projectRoot, ctx.track);
|
|
361
|
+
const specAcceptanceSet = new Set(specAcceptanceIds);
|
|
362
|
+
const slicesMissingCloses = [];
|
|
363
|
+
const slicesWithUnknownAcs = [];
|
|
364
|
+
let checkedSliceCards = 0;
|
|
354
365
|
for (const sliceFile of sliceFiles) {
|
|
355
366
|
const sliceId = sliceFile.sliceId;
|
|
356
367
|
const requiredForSlice = slicesByEvents.has(sliceId) &&
|
|
@@ -376,6 +387,17 @@ export async function lintTddStage(ctx) {
|
|
|
376
387
|
if (!/^##\s+Learnings\b/imu.test(content)) {
|
|
377
388
|
issues.push("missing `## Learnings` section");
|
|
378
389
|
}
|
|
390
|
+
checkedSliceCards += 1;
|
|
391
|
+
const closesIds = extractSliceCardClosedAcceptanceCriteria(content);
|
|
392
|
+
if (closesIds.length === 0) {
|
|
393
|
+
slicesMissingCloses.push(sliceId);
|
|
394
|
+
}
|
|
395
|
+
else if (specAcceptanceSet.size > 0) {
|
|
396
|
+
const unknown = closesIds.filter((acId) => !specAcceptanceSet.has(acId));
|
|
397
|
+
if (unknown.length > 0) {
|
|
398
|
+
slicesWithUnknownAcs.push(`${sliceId}: ${unknown.join(", ")}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
379
401
|
findings.push({
|
|
380
402
|
section: `tdd_slice_file:${sliceId}`,
|
|
381
403
|
required: requiredForSlice,
|
|
@@ -386,6 +408,34 @@ export async function lintTddStage(ctx) {
|
|
|
386
408
|
: `tdd-slices/${path.basename(sliceFile.absPath)}: ${issues.join(", ")}.`
|
|
387
409
|
});
|
|
388
410
|
}
|
|
411
|
+
const closesRequired = checkedSliceCards > 0;
|
|
412
|
+
const closesGatePassed = !closesRequired
|
|
413
|
+
? true
|
|
414
|
+
: slicesMissingCloses.length === 0 &&
|
|
415
|
+
slicesWithUnknownAcs.length === 0;
|
|
416
|
+
findings.push({
|
|
417
|
+
section: "tdd_slice_closes_ac",
|
|
418
|
+
required: true,
|
|
419
|
+
rule: "Every `tdd-slices/S-<id>.md` card must include `Closes: AC-N` links (comma-separated allowed) that reference real spec AC ids.",
|
|
420
|
+
found: closesGatePassed,
|
|
421
|
+
details: !closesRequired
|
|
422
|
+
? "No `tdd-slices/S-*.md` slice cards found yet; `Closes: AC-N` check is idle."
|
|
423
|
+
: slicesMissingCloses.length > 0
|
|
424
|
+
? `Slice card(s) missing \`Closes: AC-N\`: ${slicesMissingCloses.join(", ")}.`
|
|
425
|
+
: slicesWithUnknownAcs.length > 0
|
|
426
|
+
? `Slice card(s) reference unknown AC ids: ${slicesWithUnknownAcs.join(" | ")}.`
|
|
427
|
+
: specAcceptanceSet.size === 0
|
|
428
|
+
? `All ${checkedSliceCards} slice card(s) include Closes links; spec AC list unavailable for strict ID cross-check.`
|
|
429
|
+
: `All ${checkedSliceCards} slice card(s) include valid Closes links to spec AC ids.`
|
|
430
|
+
});
|
|
431
|
+
const orphanCheck = await evaluateSliceNoOrphanChanges(projectRoot, activeRunEntries);
|
|
432
|
+
findings.push({
|
|
433
|
+
section: "slice_no_orphan_changes",
|
|
434
|
+
required: true,
|
|
435
|
+
rule: "On slice phase=doc, there must be no staged/unstaged changes outside the slice `claimedPaths` (worktree root when present, otherwise project root).",
|
|
436
|
+
found: orphanCheck.ok,
|
|
437
|
+
details: orphanCheck.details
|
|
438
|
+
});
|
|
389
439
|
// Auto-render the slice summary inside `06-tdd.md` between markers.
|
|
390
440
|
// Idempotent — content outside the markers is preserved. Skipped
|
|
391
441
|
// entirely when there is nothing to render, so legacy artifacts (no
|
|
@@ -460,12 +510,162 @@ async function listSliceFiles(slicesDir) {
|
|
|
460
510
|
continue;
|
|
461
511
|
files.push({ sliceId: match[1], absPath: path.join(slicesDir, name) });
|
|
462
512
|
}
|
|
463
|
-
files.sort((a, b) => (a.sliceId
|
|
513
|
+
files.sort((a, b) => compareSliceIds(a.sliceId, b.sliceId));
|
|
464
514
|
return files;
|
|
465
515
|
}
|
|
466
516
|
function escapeForRegex(value) {
|
|
467
517
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
468
518
|
}
|
|
519
|
+
function normalizePathLike(value) {
|
|
520
|
+
const slashes = value.replace(/\\/gu, "/");
|
|
521
|
+
const withoutDot = slashes.replace(/^\.\//u, "");
|
|
522
|
+
return withoutDot.replace(/\/+$/u, "");
|
|
523
|
+
}
|
|
524
|
+
function parsePorcelainPaths(raw) {
|
|
525
|
+
const out = [];
|
|
526
|
+
for (const line of raw.split(/\r?\n/gu)) {
|
|
527
|
+
const trimmed = line.trimEnd();
|
|
528
|
+
if (trimmed.length < 4)
|
|
529
|
+
continue;
|
|
530
|
+
const status = trimmed.slice(0, 2);
|
|
531
|
+
if (status === "??") {
|
|
532
|
+
const p = normalizePathLike(trimmed.slice(3).trim());
|
|
533
|
+
if (p.length > 0)
|
|
534
|
+
out.push(p);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
let p = trimmed.slice(3).trim();
|
|
538
|
+
const renameIdx = p.indexOf(" -> ");
|
|
539
|
+
if (renameIdx >= 0) {
|
|
540
|
+
p = p.slice(renameIdx + 4);
|
|
541
|
+
}
|
|
542
|
+
p = normalizePathLike(p.replace(/^"/u, "").replace(/"$/u, ""));
|
|
543
|
+
if (p.length > 0)
|
|
544
|
+
out.push(p);
|
|
545
|
+
}
|
|
546
|
+
return [...new Set(out)];
|
|
547
|
+
}
|
|
548
|
+
async function gitChangedPaths(cwd) {
|
|
549
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-uall"], { cwd });
|
|
550
|
+
return parsePorcelainPaths(stdout);
|
|
551
|
+
}
|
|
552
|
+
function matchesClaimedPath(changedPath, claimedPaths) {
|
|
553
|
+
const changed = normalizePathLike(changedPath);
|
|
554
|
+
return claimedPaths.some((rawClaimed) => {
|
|
555
|
+
const claimed = normalizePathLike(rawClaimed);
|
|
556
|
+
if (claimed.length === 0)
|
|
557
|
+
return false;
|
|
558
|
+
if (changed === claimed)
|
|
559
|
+
return true;
|
|
560
|
+
return changed.startsWith(`${claimed}/`);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function extractSliceCardClosedAcceptanceCriteria(content) {
|
|
564
|
+
const ids = new Set();
|
|
565
|
+
for (const match of content.matchAll(/^\s*(?:[-*]\s*)?closes\s*:\s*(.+)$/gimu)) {
|
|
566
|
+
const tail = match[1] ?? "";
|
|
567
|
+
for (const id of extractAcceptanceCriterionIdsFromMarkdown(tail)) {
|
|
568
|
+
ids.add(id);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return [...ids];
|
|
572
|
+
}
|
|
573
|
+
async function readSpecAcceptanceCriteriaIds(projectRoot, track) {
|
|
574
|
+
const specArtifact = await resolveStageArtifactPath("spec", {
|
|
575
|
+
projectRoot,
|
|
576
|
+
track,
|
|
577
|
+
intent: "read"
|
|
578
|
+
});
|
|
579
|
+
if (!(await exists(specArtifact.absPath))) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const specRaw = await fs.readFile(specArtifact.absPath, "utf8");
|
|
584
|
+
const specSections = extractH2Sections(specRaw);
|
|
585
|
+
const acceptanceBody = sectionBodyByName(specSections, "Acceptance Criteria") ?? specRaw;
|
|
586
|
+
return extractAcceptanceCriterionIdsFromMarkdown(acceptanceBody);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function resolveClaimedPathsForDocRow(row, allRows) {
|
|
593
|
+
const fromRow = Array.isArray(row.claimedPaths) ? row.claimedPaths : [];
|
|
594
|
+
if (fromRow.length > 0) {
|
|
595
|
+
return [...new Set(fromRow.map((value) => normalizePathLike(value)).filter((value) => value.length > 0))];
|
|
596
|
+
}
|
|
597
|
+
const fromSpan = allRows
|
|
598
|
+
.filter((entry) => entry.spanId === row.spanId &&
|
|
599
|
+
Array.isArray(entry.claimedPaths) &&
|
|
600
|
+
entry.claimedPaths.length > 0)
|
|
601
|
+
.flatMap((entry) => entry.claimedPaths);
|
|
602
|
+
return [...new Set(fromSpan.map((value) => normalizePathLike(value)).filter((value) => value.length > 0))];
|
|
603
|
+
}
|
|
604
|
+
async function resolveWorktreeCwdForDocRow(projectRoot, row, allRows) {
|
|
605
|
+
const candidates = [
|
|
606
|
+
typeof row.worktreePath === "string" ? row.worktreePath.trim() : "",
|
|
607
|
+
...allRows
|
|
608
|
+
.filter((entry) => entry.spanId === row.spanId)
|
|
609
|
+
.map((entry) => (typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : ""))
|
|
610
|
+
].filter((value) => value.length > 0);
|
|
611
|
+
for (const candidateRaw of candidates) {
|
|
612
|
+
const candidateAbs = path.isAbsolute(candidateRaw)
|
|
613
|
+
? candidateRaw
|
|
614
|
+
: path.join(projectRoot, candidateRaw);
|
|
615
|
+
if (await exists(candidateAbs)) {
|
|
616
|
+
return candidateAbs;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return projectRoot;
|
|
620
|
+
}
|
|
621
|
+
export async function evaluateSliceNoOrphanChanges(projectRoot, rows) {
|
|
622
|
+
if (!(await exists(path.join(projectRoot, ".git")))) {
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
details: "No .git directory detected; orphan-change check skipped."
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const docRows = rows.filter((entry) => entry.stage === "tdd" &&
|
|
629
|
+
entry.agent === "slice-builder" &&
|
|
630
|
+
entry.status === "completed" &&
|
|
631
|
+
entry.phase === "doc");
|
|
632
|
+
if (docRows.length === 0) {
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
details: "No completed phase=doc rows found for the active run."
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const missingClaimedPaths = [];
|
|
639
|
+
const driftRows = [];
|
|
640
|
+
for (const row of docRows) {
|
|
641
|
+
const claimedPaths = resolveClaimedPathsForDocRow(row, rows);
|
|
642
|
+
const rowKey = `${row.sliceId ?? "unknown-slice"}@${row.spanId ?? "unknown-span"}`;
|
|
643
|
+
if (claimedPaths.length === 0) {
|
|
644
|
+
missingClaimedPaths.push(rowKey);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const cwd = await resolveWorktreeCwdForDocRow(projectRoot, row, rows);
|
|
648
|
+
const changedPaths = await gitChangedPaths(cwd);
|
|
649
|
+
const driftPaths = changedPaths.filter((changedPath) => !matchesClaimedPath(changedPath, claimedPaths));
|
|
650
|
+
if (driftPaths.length > 0) {
|
|
651
|
+
driftRows.push(`${rowKey}: ${driftPaths.join(", ")}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (missingClaimedPaths.length > 0 || driftRows.length > 0) {
|
|
655
|
+
const parts = [];
|
|
656
|
+
if (missingClaimedPaths.length > 0) {
|
|
657
|
+
parts.push(`doc row(s) missing claimedPaths: ${missingClaimedPaths.join(", ")}`);
|
|
658
|
+
}
|
|
659
|
+
if (driftRows.length > 0) {
|
|
660
|
+
parts.push(`orphan working-tree changes detected: ${driftRows.join(" | ")}`);
|
|
661
|
+
}
|
|
662
|
+
return { ok: false, details: parts.join(". ") };
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
ok: true,
|
|
666
|
+
details: `Checked ${docRows.length} doc row(s); no orphan changes escaped claimedPaths.`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
469
669
|
function groupBySlice(entries) {
|
|
470
670
|
const grouped = new Map();
|
|
471
671
|
for (const entry of entries) {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack, TddCommitMode, TddIsolationMode } from "./types.js";
|
|
1
|
+
import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack, LockfileTwinPolicy, TddCommitMode, TddIsolationMode } from "./types.js";
|
|
2
2
|
export declare const TDD_COMMIT_MODES: readonly ["managed-per-slice", "agent-required", "checkpoint-only", "off"];
|
|
3
3
|
export declare const DEFAULT_TDD_COMMIT_MODE: TddCommitMode;
|
|
4
4
|
export declare const TDD_ISOLATION_MODES: readonly ["worktree", "in-place", "auto"];
|
|
5
5
|
export declare const DEFAULT_TDD_ISOLATION_MODE: TddIsolationMode;
|
|
6
6
|
export declare const DEFAULT_TDD_WORKTREE_ROOT = ".cclaw/worktrees";
|
|
7
|
+
export declare const LOCKFILE_TWIN_POLICIES: readonly ["auto-include", "auto-revert", "strict-fence"];
|
|
8
|
+
export declare const DEFAULT_LOCKFILE_TWIN_POLICY: LockfileTwinPolicy;
|
|
7
9
|
export declare const DEFAULT_TDD_TEST_PATH_PATTERNS: readonly string[];
|
|
8
10
|
export declare const DEFAULT_TDD_TEST_GLOBS: readonly string[];
|
|
9
11
|
export declare const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS: readonly string[];
|
|
@@ -24,6 +26,7 @@ export declare function createDefaultConfig(harnesses?: HarnessId[], _defaultTra
|
|
|
24
26
|
export declare function resolveTddCommitMode(config: Pick<CclawConfig, "tdd"> | null | undefined): TddCommitMode;
|
|
25
27
|
export declare function resolveTddIsolationMode(config: Pick<CclawConfig, "tdd"> | null | undefined): TddIsolationMode;
|
|
26
28
|
export declare function resolveTddWorktreeRoot(config: Pick<CclawConfig, "tdd"> | null | undefined): string;
|
|
29
|
+
export declare function resolveLockfileTwinPolicy(config: Pick<CclawConfig, "tdd"> | null | undefined): LockfileTwinPolicy;
|
|
27
30
|
export declare function detectLanguageRulePacks(_projectRoot: string): Promise<LanguageRulePack[]>;
|
|
28
31
|
export declare function readConfig(projectRoot: string, _options?: ReadConfigOptions): Promise<CclawConfig>;
|
|
29
32
|
export interface WriteConfigOptions {
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,9 @@ export const TDD_ISOLATION_MODES = ["worktree", "in-place", "auto"];
|
|
|
20
20
|
const TDD_ISOLATION_MODE_SET = new Set(TDD_ISOLATION_MODES);
|
|
21
21
|
export const DEFAULT_TDD_ISOLATION_MODE = "worktree";
|
|
22
22
|
export const DEFAULT_TDD_WORKTREE_ROOT = `${RUNTIME_ROOT}/worktrees`;
|
|
23
|
+
export const LOCKFILE_TWIN_POLICIES = ["auto-include", "auto-revert", "strict-fence"];
|
|
24
|
+
const LOCKFILE_TWIN_POLICY_SET = new Set(LOCKFILE_TWIN_POLICIES);
|
|
25
|
+
export const DEFAULT_LOCKFILE_TWIN_POLICY = "auto-include";
|
|
23
26
|
// Kept for runtime modules that use these defaults directly.
|
|
24
27
|
export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
|
|
25
28
|
"**/*.test.*",
|
|
@@ -68,7 +71,8 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, _defaultTrack
|
|
|
68
71
|
tdd: {
|
|
69
72
|
commitMode: DEFAULT_TDD_COMMIT_MODE,
|
|
70
73
|
isolationMode: DEFAULT_TDD_ISOLATION_MODE,
|
|
71
|
-
worktreeRoot: DEFAULT_TDD_WORKTREE_ROOT
|
|
74
|
+
worktreeRoot: DEFAULT_TDD_WORKTREE_ROOT,
|
|
75
|
+
lockfileTwinPolicy: DEFAULT_LOCKFILE_TWIN_POLICY
|
|
72
76
|
}
|
|
73
77
|
};
|
|
74
78
|
}
|
|
@@ -93,6 +97,13 @@ export function resolveTddWorktreeRoot(config) {
|
|
|
93
97
|
}
|
|
94
98
|
return DEFAULT_TDD_WORKTREE_ROOT;
|
|
95
99
|
}
|
|
100
|
+
export function resolveLockfileTwinPolicy(config) {
|
|
101
|
+
const raw = config?.tdd?.lockfileTwinPolicy;
|
|
102
|
+
if (typeof raw === "string" && LOCKFILE_TWIN_POLICY_SET.has(raw)) {
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
return DEFAULT_LOCKFILE_TWIN_POLICY;
|
|
106
|
+
}
|
|
96
107
|
function assertOnlySupportedKeys(parsed, fullPath) {
|
|
97
108
|
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
98
109
|
if (unknownKeys.length === 0)
|
|
@@ -153,6 +164,7 @@ export async function readConfig(projectRoot, _options = {}) {
|
|
|
153
164
|
const rawCommitMode = parsedTdd.commitMode;
|
|
154
165
|
const rawIsolationMode = parsedTdd.isolationMode;
|
|
155
166
|
const rawWorktreeRoot = parsedTdd.worktreeRoot;
|
|
167
|
+
const rawLockfileTwinPolicy = parsedTdd.lockfileTwinPolicy;
|
|
156
168
|
if (rawCommitMode !== undefined &&
|
|
157
169
|
(typeof rawCommitMode !== "string" || !TDD_COMMIT_MODE_SET.has(rawCommitMode))) {
|
|
158
170
|
throw configValidationError(fullPath, `"tdd.commitMode" must be one of: ${TDD_COMMIT_MODES.join(", ")}`);
|
|
@@ -165,6 +177,10 @@ export async function readConfig(projectRoot, _options = {}) {
|
|
|
165
177
|
(typeof rawWorktreeRoot !== "string" || rawWorktreeRoot.trim().length === 0)) {
|
|
166
178
|
throw configValidationError(fullPath, `"tdd.worktreeRoot" must be a non-empty string when provided`);
|
|
167
179
|
}
|
|
180
|
+
if (rawLockfileTwinPolicy !== undefined &&
|
|
181
|
+
(typeof rawLockfileTwinPolicy !== "string" || !LOCKFILE_TWIN_POLICY_SET.has(rawLockfileTwinPolicy))) {
|
|
182
|
+
throw configValidationError(fullPath, `"tdd.lockfileTwinPolicy" must be one of: ${LOCKFILE_TWIN_POLICIES.join(", ")}`);
|
|
183
|
+
}
|
|
168
184
|
const commitMode = typeof rawCommitMode === "string"
|
|
169
185
|
? rawCommitMode
|
|
170
186
|
: DEFAULT_TDD_COMMIT_MODE;
|
|
@@ -174,6 +190,9 @@ export async function readConfig(projectRoot, _options = {}) {
|
|
|
174
190
|
const worktreeRoot = typeof rawWorktreeRoot === "string" && rawWorktreeRoot.trim().length > 0
|
|
175
191
|
? rawWorktreeRoot.trim()
|
|
176
192
|
: DEFAULT_TDD_WORKTREE_ROOT;
|
|
193
|
+
const lockfileTwinPolicy = typeof rawLockfileTwinPolicy === "string"
|
|
194
|
+
? rawLockfileTwinPolicy
|
|
195
|
+
: DEFAULT_LOCKFILE_TWIN_POLICY;
|
|
177
196
|
return {
|
|
178
197
|
version,
|
|
179
198
|
flowVersion,
|
|
@@ -181,7 +200,8 @@ export async function readConfig(projectRoot, _options = {}) {
|
|
|
181
200
|
tdd: {
|
|
182
201
|
commitMode,
|
|
183
202
|
isolationMode,
|
|
184
|
-
worktreeRoot
|
|
203
|
+
worktreeRoot,
|
|
204
|
+
lockfileTwinPolicy
|
|
185
205
|
}
|
|
186
206
|
};
|
|
187
207
|
}
|
|
@@ -193,7 +213,8 @@ export async function writeConfig(projectRoot, config, _options = {}) {
|
|
|
193
213
|
tdd: {
|
|
194
214
|
commitMode: resolveTddCommitMode(config),
|
|
195
215
|
isolationMode: resolveTddIsolationMode(config),
|
|
196
|
-
worktreeRoot: resolveTddWorktreeRoot(config)
|
|
216
|
+
worktreeRoot: resolveTddWorktreeRoot(config),
|
|
217
|
+
lockfileTwinPolicy: resolveLockfileTwinPolicy(config)
|
|
197
218
|
}
|
|
198
219
|
};
|
|
199
220
|
await writeFileSafe(configPath(projectRoot), stringify(serialisable));
|
|
@@ -165,6 +165,21 @@ export function sliceBuilderProtocol() {
|
|
|
165
165
|
"- Honor every `delegation-record`/`delegation-record.mjs` row shape the controller requests so artifact linters keep passing.",
|
|
166
166
|
"- The umbrella `slice-completed` row ties RED/GREEN/REFACTOR/DOC timestamps to your builder span.",
|
|
167
167
|
"",
|
|
168
|
+
"### Event → status flag table (7.6.0 — phase-event status validation)",
|
|
169
|
+
"",
|
|
170
|
+
"Phase-level granularity is only meaningful on terminal outcomes. The dispatch-level ack (no `--phase`) is the controller saying \"I see the dispatch surface back\" — it stays on `--status=acknowledged`. Phase events MUST use `--status=completed` or `--status=failed`. The hook rejects mismatches with `phase_event_requires_completed_or_failed_status` (exit 2) and prints a corrected-command hint.",
|
|
171
|
+
"",
|
|
172
|
+
"| event | --phase | --status |",
|
|
173
|
+
"|---|---|---|",
|
|
174
|
+
"| dispatch ack (controller-side) | (none) | `acknowledged` |",
|
|
175
|
+
"| RED watched-fail captured | `red` | `completed` |",
|
|
176
|
+
"| GREEN test passes | `green` | `completed` (with `--refactor-outcome=inline\\|deferred\\|...`) |",
|
|
177
|
+
"| REFACTOR landed | `refactor` | `completed` |",
|
|
178
|
+
"| DOC card landed (triggers slice-commit) | `doc` | `completed` |",
|
|
179
|
+
"| BLOCKED / unrecoverable | (any phase reached) | `failed` |",
|
|
180
|
+
"",
|
|
181
|
+
"Common slip: recording every phase event with `--status=acknowledged` (e.g. `--phase=doc --status=acknowledged`). The event row is silently dropped from terminal-phase aggregations, `slice-commit.mjs` never fires (it only triggers on `phase=doc status=completed`), and `wave-status` reports the slice as phantom-open. Recovery requires raw backfill commands. The 7.6.0 hook validator forbids this configuration up front.",
|
|
182
|
+
"",
|
|
168
183
|
"### Streaming output contract",
|
|
169
184
|
"- Emit one JSON line to stdout per completed phase: `{\"event\":\"phase-completed\",\"stage\":\"tdd\",\"sliceId\":\"S-<n>\",\"phase\":\"<red|green|refactor|refactor-deferred|doc>\",\"spanId\":\"<span>\",\"runId\":\"<run>\",\"ts\":\"<iso>\"}`.",
|
|
170
185
|
"- For `phase=green` with inline/deferred refactor folding, include `refactorOutcome.mode` in the same JSON line so live controllers can close the slice without waiting for file sync.",
|
package/dist/content/hooks.js
CHANGED
|
@@ -1489,6 +1489,43 @@ async function main() {
|
|
|
1489
1489
|
emitProblems(problems, json, 2);
|
|
1490
1490
|
return;
|
|
1491
1491
|
}
|
|
1492
|
+
// 7.6.0 — phase-event status validation.
|
|
1493
|
+
// \`--phase=<phase>\` carries phase-level granularity (RED/GREEN/REFACTOR/DOC
|
|
1494
|
+
// outcomes). It is only meaningful on terminal statuses
|
|
1495
|
+
// (\`completed\` or \`failed\`). The dispatch-level ack (no phase) keeps
|
|
1496
|
+
// \`--status=acknowledged\`. Refuse acknowledged/launched/scheduled/waived/stale
|
|
1497
|
+
// rows that carry a phase so phantom-open slices cannot be recorded.
|
|
1498
|
+
if (
|
|
1499
|
+
typeof args.phase === "string" &&
|
|
1500
|
+
args.phase.length > 0 &&
|
|
1501
|
+
args.status !== "completed" &&
|
|
1502
|
+
args.status !== "failed"
|
|
1503
|
+
) {
|
|
1504
|
+
const sliceFlag = typeof args.slice === "string" && args.slice.length > 0
|
|
1505
|
+
? "--slice=" + args.slice + " "
|
|
1506
|
+
: "";
|
|
1507
|
+
const spanFlag = typeof args["span-id"] === "string" && args["span-id"].length > 0
|
|
1508
|
+
? "--span-id=" + args["span-id"] + " "
|
|
1509
|
+
: "";
|
|
1510
|
+
const correctedCommandHint =
|
|
1511
|
+
"node .cclaw/hooks/delegation-record.mjs --stage=" + (args.stage || "<stage>") +
|
|
1512
|
+
" --agent=" + (args.agent || "<agent>") +
|
|
1513
|
+
" --mode=" + (args.mode || "mandatory") +
|
|
1514
|
+
" --status=completed --phase=" + args.phase +
|
|
1515
|
+
" " + sliceFlag + spanFlag +
|
|
1516
|
+
'--evidence-ref="<phase outcome>"';
|
|
1517
|
+
emitErrorJson(
|
|
1518
|
+
"phase_event_requires_completed_or_failed_status",
|
|
1519
|
+
{
|
|
1520
|
+
phase: args.phase,
|
|
1521
|
+
status: args.status,
|
|
1522
|
+
spanId: args["span-id"] || "unknown",
|
|
1523
|
+
correctedCommandHint
|
|
1524
|
+
},
|
|
1525
|
+
json
|
|
1526
|
+
);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1492
1529
|
if (args.phase === "refactor-deferred") {
|
|
1493
1530
|
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1494
1531
|
if (rationaleQuality !== "ok") {
|
|
@@ -258,6 +258,7 @@ const REQUIRED_GATE_IDS = {
|
|
|
258
258
|
"design_test_and_perf_defined"
|
|
259
259
|
],
|
|
260
260
|
spec: [
|
|
261
|
+
"spec_ac_ids_present",
|
|
261
262
|
"spec_acceptance_measurable",
|
|
262
263
|
"spec_testability_confirmed",
|
|
263
264
|
"spec_assumptions_surfaced",
|
|
@@ -271,6 +272,7 @@ const REQUIRED_GATE_IDS = {
|
|
|
271
272
|
"plan_execution_posture_recorded",
|
|
272
273
|
"plan_parallel_exec_full_coverage",
|
|
273
274
|
"plan_wave_paths_disjoint",
|
|
275
|
+
"plan_module_introducing_slice_wires_root",
|
|
274
276
|
"plan_wait_for_confirm"
|
|
275
277
|
],
|
|
276
278
|
tdd: (track) => [
|
|
@@ -283,6 +285,8 @@ const REQUIRED_GATE_IDS = {
|
|
|
283
285
|
"tdd_iron_law_acknowledged",
|
|
284
286
|
"tdd_watched_red_observed",
|
|
285
287
|
"tdd_slice_cycle_complete",
|
|
288
|
+
"tdd_slice_closes_ac",
|
|
289
|
+
"slice_no_orphan_changes",
|
|
286
290
|
"tdd_docs_drift_check",
|
|
287
291
|
...(track === "quick" ? [] : ["tdd_traceable_to_plan"])
|
|
288
292
|
],
|
|
@@ -297,6 +301,7 @@ const REQUIRED_GATE_IDS = {
|
|
|
297
301
|
"ship_review_verdict_valid",
|
|
298
302
|
"ship_preflight_passed",
|
|
299
303
|
"ship_rollback_plan_ready",
|
|
304
|
+
"ship_all_acceptance_criteria_have_commits",
|
|
300
305
|
"ship_finalization_executed"
|
|
301
306
|
]
|
|
302
307
|
};
|
|
@@ -341,7 +346,7 @@ const REQUIRED_ARTIFACT_SECTIONS = {
|
|
|
341
346
|
"Verification Ladder"
|
|
342
347
|
],
|
|
343
348
|
review: ["Review Evidence Scope", "Changed-File Coverage", "Layer 1 Verdict", "Review Findings Contract", "Severity Summary", "Final Verdict"],
|
|
344
|
-
ship: ["Preflight Results", "Release Notes", "Rollback Plan", "Finalization"]
|
|
349
|
+
ship: ["Preflight Results", "Release Notes", "Traceability Matrix", "Rollback Plan", "Finalization"]
|
|
345
350
|
};
|
|
346
351
|
function resolveRequiredGateIds(stage, track) {
|
|
347
352
|
const raw = REQUIRED_GATE_IDS[stage];
|
|
@@ -85,6 +85,7 @@ export const PLAN = {
|
|
|
85
85
|
{ id: "plan_execution_posture_recorded", description: "Execution posture is recorded before implementation handoff." },
|
|
86
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
87
|
{ id: "plan_wave_paths_disjoint", description: "Within each authored wave, slice `claimedPaths` remain disjoint so `wave-fanout` can dispatch safely without overlap conflicts." },
|
|
88
|
+
{ id: "plan_module_introducing_slice_wires_root", description: "When a slice introduces a new module file, the stack-adapter's wiring aggregator (Rust `lib.rs`, Python `__init__.py`, Node-TS barrel when present) must appear in the same slice's claim or a transitive predecessor's claim so RED can be expressed." },
|
|
88
89
|
{ id: "plan_wait_for_confirm", description: "Execution blocked until explicit user confirmation." }
|
|
89
90
|
],
|
|
90
91
|
requiredEvidence: [
|
|
@@ -45,6 +45,7 @@ export const SHIP = {
|
|
|
45
45
|
"Merge-base detection (git only) — identify the correct base branch. Run `git merge-base HEAD <base>`. If the base has diverged significantly, flag for rebase-first.",
|
|
46
46
|
"Re-run tests on merged result — if merging locally, run the full test suite AFTER the merge, not just before. Post-merge failures are common.",
|
|
47
47
|
"Generate release notes — summarize what changed, why, and what it affects. Reference spec criteria. Include: breaking changes, new dependencies, migration steps if any.",
|
|
48
|
+
"Assemble acceptance traceability matrix — for each spec AC-N, list mapped slice IDs and at least one managed commit proving closure.",
|
|
48
49
|
"Write rollback plan — trigger conditions (what tells you it is broken), rollback steps (exact commands/git operations), and verification (how to confirm rollback worked).",
|
|
49
50
|
"Load utility skills — `verification-before-completion` for fresh evidence and `finishing-a-development-branch` for finalization workflow.",
|
|
50
51
|
"Monitoring checklist — what should be watched after deploy? Error rates, latency, key business metrics. If no monitoring exists, flag it as a risk.",
|
|
@@ -74,11 +75,13 @@ export const SHIP = {
|
|
|
74
75
|
{ id: "ship_review_verdict_valid", description: "Review verdict is APPROVED or APPROVED_WITH_CONCERNS." },
|
|
75
76
|
{ id: "ship_preflight_passed", description: "Preflight checks passed or exceptions documented and approved." },
|
|
76
77
|
{ id: "ship_rollback_plan_ready", description: "Rollback trigger, steps, and verification are documented." },
|
|
78
|
+
{ id: "ship_all_acceptance_criteria_have_commits", description: "Every spec AC-N has at least one `Closes: AC-N` slice mapping and a managed slice commit in the active run." },
|
|
77
79
|
{ id: "ship_finalization_executed", description: "Selected finalization action was executed and verified." }
|
|
78
80
|
],
|
|
79
81
|
requiredEvidence: [
|
|
80
82
|
"Artifact written to `.cclaw/artifacts/08-ship.md`.",
|
|
81
83
|
"Release notes section is complete.",
|
|
84
|
+
"Traceability Matrix maps each spec AC-N to slice IDs and managed commit evidence.",
|
|
82
85
|
"Rollback section includes trigger conditions, steps, and verification.",
|
|
83
86
|
"Finalization section shows exactly one selected enum token.",
|
|
84
87
|
"Victory Detector result documented: review verdict valid, preflight fresh, rollback ready, finalization enum selected, and execution result present."
|
|
@@ -115,6 +118,7 @@ export const SHIP = {
|
|
|
115
118
|
{ section: "Upstream Handoff", required: false, validationRule: "Summarizes review/tdd decisions, constraints, open questions, and explicit drift before finalization." },
|
|
116
119
|
{ section: "Preflight Results", required: true, validationRule: "Build, test, lint, type-check results captured with fresh output. Exceptions documented if any." },
|
|
117
120
|
{ section: "Release Notes", required: true, validationRule: "What changed, why, impact. References spec criteria. Breaking changes flagged." },
|
|
121
|
+
{ section: "Traceability Matrix", required: true, validationRule: "One row per spec AC-N with mapped slice IDs (`S-<id>`), managed commit evidence (`^S-<id>/` subject), and coverage status." },
|
|
118
122
|
{ section: "Rollback Plan", required: true, validationRule: "Trigger conditions, rollback steps (exact commands), verification steps." },
|
|
119
123
|
{ section: "Monitoring", required: false, validationRule: "If applicable: what metrics/logs to watch post-deploy. Risk note if no monitoring." },
|
|
120
124
|
{ section: "Finalization", required: true, validationRule: "Exactly one finalization enum token selected (FINALIZE_MERGE_LOCAL | FINALIZE_OPEN_PR | FINALIZE_KEEP_BRANCH | FINALIZE_DISCARD_BRANCH | FINALIZE_NO_VCS). Execution result documented. Worktree cleaned if applicable." },
|
|
@@ -68,6 +68,7 @@ export const SPEC = {
|
|
|
68
68
|
"Write spec artifact and request approval."
|
|
69
69
|
],
|
|
70
70
|
requiredGates: [
|
|
71
|
+
{ id: "spec_ac_ids_present", description: "Acceptance Criteria rows include stable `AC-N` identifiers." },
|
|
71
72
|
{ id: "spec_acceptance_measurable", description: "Acceptance criteria are measurable and observable." },
|
|
72
73
|
{ id: "spec_testability_confirmed", description: "Each criterion has a described test method." },
|
|
73
74
|
{ id: "spec_assumptions_surfaced", description: "Assumptions were explicitly reviewed with source/confidence, validation path, and disposition before approval." },
|
|
@@ -76,6 +76,8 @@ export const TDD = {
|
|
|
76
76
|
{ id: "tdd_iron_law_acknowledged", description: "Iron Law acknowledgement is explicit (`Acknowledged: yes`) before implementation proceeds." },
|
|
77
77
|
{ id: "tdd_watched_red_observed", description: "Watched-RED Proof records at least one observed failing test with ISO timestamp evidence." },
|
|
78
78
|
{ id: "tdd_slice_cycle_complete", description: "Vertical Slice Cycle records RED, GREEN, and REFACTOR phases per active slice." },
|
|
79
|
+
{ id: "tdd_slice_closes_ac", description: "Every `tdd-slices/S-<id>.md` card includes valid `Closes: AC-N` links to spec acceptance criteria." },
|
|
80
|
+
{ id: "slice_no_orphan_changes", description: "After `phase=doc`, working tree changes remain inside the slice claimedPaths boundary (worktree root when present)." },
|
|
79
81
|
{ id: "tdd_traceable_to_plan", description: "Change traceability to plan slice is explicit." },
|
|
80
82
|
{ id: "tdd_docs_drift_check", description: "When public API/config/CLI surfaces change, docs drift is addressed via a completed doc-updater pass." }
|
|
81
83
|
],
|
|
@@ -1377,6 +1377,11 @@ ${renderBehaviorAnchorTemplateLine("ship")}
|
|
|
1377
1377
|
## Release Notes
|
|
1378
1378
|
-
|
|
1379
1379
|
|
|
1380
|
+
## Traceability Matrix
|
|
1381
|
+
| AC ID | Slice ID(s) | Managed commit evidence | Coverage status |
|
|
1382
|
+
|---|---|---|---|
|
|
1383
|
+
| AC-1 | S-1 | \`abc1234 S-1/green: ...\` | covered |
|
|
1384
|
+
|
|
1380
1385
|
## Structured PR Body
|
|
1381
1386
|
> Required when selected option is \`OPEN_PR\`. The structure is universal — replace placeholder bullets with concrete content, do not introduce domain-specific subsections.
|
|
1382
1387
|
|
package/dist/delegation.d.ts
CHANGED
|
@@ -381,6 +381,45 @@ export declare class DispatchClaimedPathProtectedError extends Error {
|
|
|
381
381
|
* offending path so the operator can fix the dispatch in one pass.
|
|
382
382
|
*/
|
|
383
383
|
export declare function validateClaimedPathsNotProtected(stamped: DelegationEntry): void;
|
|
384
|
+
/**
|
|
385
|
+
* Thrown by `appendDelegation` (and the inline `delegation-record.mjs`
|
|
386
|
+
* helper) when an event with a non-null `phase` is recorded with
|
|
387
|
+
* `status="acknowledged"`. Phase-level granularity only makes sense on
|
|
388
|
+
* terminal outcomes (`completed` or `failed`); the dispatch-level ACK
|
|
389
|
+
* (no phase) is the controller saying "I see the dispatch surface back".
|
|
390
|
+
*
|
|
391
|
+
* Motivated by hox W-08/S-41: the slice-builder agent recorded all four
|
|
392
|
+
* phase events with `--status=acknowledged`, which the helper silently
|
|
393
|
+
* accepted but `slice-commit.mjs` only fires on `phase=doc status=completed`.
|
|
394
|
+
* `wave-status` then saw the slice as phantom-open even though the
|
|
395
|
+
* worker had finished. Recovery required raw backfill commands.
|
|
396
|
+
*
|
|
397
|
+
* 7.6.0 makes the constraint explicit: pair `--phase=<phase>` with
|
|
398
|
+
* `--status=completed` (or `--status=failed`) and use
|
|
399
|
+
* `--status=acknowledged` only for the dispatch-level ack (no phase).
|
|
400
|
+
*/
|
|
401
|
+
export declare class PhaseEventRequiresTerminalStatusError extends Error {
|
|
402
|
+
readonly phase: string;
|
|
403
|
+
readonly status: DelegationStatus;
|
|
404
|
+
readonly spanId: string;
|
|
405
|
+
readonly correctedCommandHint: string;
|
|
406
|
+
constructor(params: {
|
|
407
|
+
phase: string;
|
|
408
|
+
status: DelegationStatus;
|
|
409
|
+
spanId: string;
|
|
410
|
+
correctedCommandHint: string;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Reject delegation rows where `phase` is set but `status` is not
|
|
415
|
+
* `completed` or `failed`. Acknowledged/launched/scheduled/waived/stale
|
|
416
|
+
* rows must NOT carry a phase — the phase-level lifecycle exists only
|
|
417
|
+
* to record terminal outcomes per phase (RED/GREEN/REFACTOR/DOC).
|
|
418
|
+
*
|
|
419
|
+
* Throws `PhaseEventRequiresTerminalStatusError`; the message includes
|
|
420
|
+
* an actionable corrected-command hint that the controller can paste.
|
|
421
|
+
*/
|
|
422
|
+
export declare function validatePhaseEventStatus(stamped: DelegationEntry): void;
|
|
384
423
|
/**
|
|
385
424
|
* Thrown by `appendDelegation` when a new `scheduled` span would open a
|
|
386
425
|
* second TDD cycle for a slice that already has at least one closed span
|