cclaw-cli 7.0.5 → 7.1.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/tdd.js +142 -100
- package/dist/config.d.ts +4 -1
- package/dist/config.js +44 -5
- package/dist/content/core-agents.js +1 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +116 -0
- package/dist/content/stages/tdd.js +4 -4
- package/dist/delegation.d.ts +55 -0
- package/dist/delegation.js +161 -0
- package/dist/install.js +3 -1
- package/dist/internal/advance-stage.js +15 -3
- package/dist/internal/slice-commit.d.ts +7 -0
- package/dist/internal/slice-commit.js +296 -0
- package/dist/tdd-verification-evidence.js +101 -10
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
|
@@ -1,13 +1,86 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { readConfig, resolveTddCommitMode } from "./config.js";
|
|
5
|
+
import { readDelegationLedger } from "./delegation.js";
|
|
2
6
|
import { exists } from "./fs-utils.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
3
8
|
export const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|npm run test(?::[\w:-]+)?|pnpm test|pnpm [\w:-]*test[\w:-]*|yarn test|yarn [\w:-]*test[\w:-]*|bun test|bun run test(?::[\w:-]+)?|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|\.\/gradlew test|dotnet test)\b/iu;
|
|
4
9
|
export const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
5
10
|
export const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
6
11
|
export const NO_VCS_ATTESTATION_PATTERN = /\b(?:no[-_ ]?vcs|no git|not a git repo|vcs\s*[:=]\s*none)\b/iu;
|
|
7
12
|
export const NO_VCS_HASH_PATTERN = /\b(?:content|artifact)[-_ ]?hash\s*[:=]\s*(?:sha256:)?[0-9a-f]{16,64}\b|\bsha256\s*[:=]\s*[0-9a-f]{16,64}\b/iu;
|
|
13
|
+
function escapeRegex(value) {
|
|
14
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
function hasRefactorCoverage(entries) {
|
|
17
|
+
const phases = new Set(entries
|
|
18
|
+
.filter((e) => e.status === "completed" && typeof e.phase === "string")
|
|
19
|
+
.map((e) => e.phase));
|
|
20
|
+
if (phases.has("refactor") || phases.has("refactor-deferred")) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const greenWithOutcome = entries.find((entry) => entry.status === "completed" &&
|
|
24
|
+
entry.phase === "green" &&
|
|
25
|
+
entry.refactorOutcome &&
|
|
26
|
+
(entry.refactorOutcome.mode === "inline" || entry.refactorOutcome.mode === "deferred"));
|
|
27
|
+
if (!greenWithOutcome?.refactorOutcome)
|
|
28
|
+
return false;
|
|
29
|
+
if (greenWithOutcome.refactorOutcome.mode === "inline")
|
|
30
|
+
return true;
|
|
31
|
+
const rationale = greenWithOutcome.refactorOutcome.rationale;
|
|
32
|
+
if (typeof rationale === "string" && rationale.trim().length > 0)
|
|
33
|
+
return true;
|
|
34
|
+
if (!Array.isArray(greenWithOutcome.evidenceRefs))
|
|
35
|
+
return false;
|
|
36
|
+
return greenWithOutcome.evidenceRefs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
|
|
37
|
+
}
|
|
38
|
+
function collectClosedSlices(entries, runId) {
|
|
39
|
+
const bySlice = new Map();
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.runId !== runId)
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.stage !== "tdd")
|
|
44
|
+
continue;
|
|
45
|
+
if (entry.status !== "completed")
|
|
46
|
+
continue;
|
|
47
|
+
if (typeof entry.sliceId !== "string" || entry.sliceId.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
if (typeof entry.spanId !== "string" || entry.spanId.length === 0)
|
|
50
|
+
continue;
|
|
51
|
+
const bySpan = bySlice.get(entry.sliceId) ?? new Map();
|
|
52
|
+
const rows = bySpan.get(entry.spanId) ?? [];
|
|
53
|
+
rows.push(entry);
|
|
54
|
+
bySpan.set(entry.spanId, rows);
|
|
55
|
+
bySlice.set(entry.sliceId, bySpan);
|
|
56
|
+
}
|
|
57
|
+
const closedSlices = new Set();
|
|
58
|
+
for (const [sliceId, bySpan] of bySlice.entries()) {
|
|
59
|
+
for (const rows of bySpan.values()) {
|
|
60
|
+
const phases = new Set(rows
|
|
61
|
+
.filter((row) => row.status === "completed" && typeof row.phase === "string")
|
|
62
|
+
.map((row) => row.phase));
|
|
63
|
+
const hasRed = phases.has("red");
|
|
64
|
+
const hasGreen = phases.has("green");
|
|
65
|
+
const hasDoc = phases.has("doc");
|
|
66
|
+
if (hasRed && hasGreen && hasDoc && hasRefactorCoverage(rows)) {
|
|
67
|
+
closedSlices.add(sliceId);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...closedSlices].sort();
|
|
73
|
+
}
|
|
74
|
+
async function hasManagedCommitForSlice(projectRoot, sliceId) {
|
|
75
|
+
const grep = `^${escapeRegex(sliceId)}/`;
|
|
76
|
+
const { stdout } = await execFileAsync("git", ["log", "--format=%s%n%b", "--grep", grep], { cwd: projectRoot });
|
|
77
|
+
return stdout.trim().length > 0;
|
|
78
|
+
}
|
|
8
79
|
export async function validateTddVerificationEvidence(projectRoot, evidence, options = {}) {
|
|
9
80
|
const normalized = evidence.trim();
|
|
10
|
-
const
|
|
81
|
+
const config = await readConfig(projectRoot).catch(() => null);
|
|
82
|
+
const commitMode = resolveTddCommitMode(config);
|
|
83
|
+
const mode = commitMode === "off" ? "disabled" : "auto";
|
|
11
84
|
const gitPresent = await exists(path.join(projectRoot, ".git"));
|
|
12
85
|
const issues = [];
|
|
13
86
|
if (options.requireCommand !== false && !TEST_COMMAND_HINT_PATTERN.test(normalized)) {
|
|
@@ -16,16 +89,34 @@ export async function validateTddVerificationEvidence(projectRoot, evidence, opt
|
|
|
16
89
|
if (options.requirePassStatus !== false && !PASS_STATUS_PATTERN.test(normalized)) {
|
|
17
90
|
issues.push("GREEN repair needed: include explicit success status (for example `PASS` or `GREEN`).");
|
|
18
91
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
92
|
+
if (mode !== "disabled" && commitMode === "managed-per-slice" && gitPresent) {
|
|
93
|
+
const ledger = await readDelegationLedger(projectRoot).catch(() => null);
|
|
94
|
+
if (ledger && typeof ledger.runId === "string" && ledger.runId.length > 0) {
|
|
95
|
+
const closedSlices = collectClosedSlices(ledger.entries, ledger.runId);
|
|
96
|
+
const missing = [];
|
|
97
|
+
for (const sliceId of closedSlices) {
|
|
98
|
+
const hasCommit = await hasManagedCommitForSlice(projectRoot, sliceId).catch(() => false);
|
|
99
|
+
if (!hasCommit) {
|
|
100
|
+
missing.push(sliceId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (missing.length > 0) {
|
|
104
|
+
issues.push(`managed-per-slice commit check failed: missing git commit(s) for closed slice(s): ${missing.join(", ")}.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
26
107
|
}
|
|
27
|
-
else if (mode === "auto"
|
|
28
|
-
|
|
108
|
+
else if (mode === "auto") {
|
|
109
|
+
const hasSha = SHA_WITH_LABEL_PATTERN.test(normalized);
|
|
110
|
+
const hasNoVcs = NO_VCS_ATTESTATION_PATTERN.test(normalized);
|
|
111
|
+
if (gitPresent && !hasSha) {
|
|
112
|
+
issues.push("must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).");
|
|
113
|
+
}
|
|
114
|
+
else if (!gitPresent && !hasSha && !hasNoVcs) {
|
|
115
|
+
issues.push("must include either a commit SHA or an explicit no-VCS attestation (for example `no-vcs: project has no .git directory`).");
|
|
116
|
+
}
|
|
117
|
+
else if (!gitPresent && hasNoVcs && !NO_VCS_HASH_PATTERN.test(normalized)) {
|
|
118
|
+
issues.push("NO_VCS_MODE repair needed: include a content/artifact hash for no-VCS TDD evidence (for example `artifact-hash: sha256:<hash>`).");
|
|
119
|
+
}
|
|
29
120
|
}
|
|
30
121
|
return { ok: issues.length === 0, issues, mode, gitPresent };
|
|
31
122
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -161,10 +161,22 @@ export interface ReviewLoopConfig {
|
|
|
161
161
|
externalSecondOpinion?: ReviewLoopExternalSecondOpinionConfig;
|
|
162
162
|
}
|
|
163
163
|
export type VcsMode = "git-with-remote" | "git-local-only" | "none";
|
|
164
|
+
export type TddCommitMode = "managed-per-slice" | "agent-required" | "checkpoint-only" | "off";
|
|
165
|
+
export interface TddConfig {
|
|
166
|
+
/**
|
|
167
|
+
* Commit ownership model for closed TDD slices.
|
|
168
|
+
* - managed-per-slice: cclaw-generated hook performs one commit per closed slice.
|
|
169
|
+
* - agent-required: worker/controller must create the commit outside cclaw.
|
|
170
|
+
* - checkpoint-only: coarse-grained checkpoints are allowed (no per-slice enforcement).
|
|
171
|
+
* - off: skip commit-shape enforcement.
|
|
172
|
+
*/
|
|
173
|
+
commitMode?: TddCommitMode;
|
|
174
|
+
}
|
|
164
175
|
export interface CclawConfig {
|
|
165
176
|
version: string;
|
|
166
177
|
flowVersion: string;
|
|
167
178
|
harnesses: HarnessId[];
|
|
179
|
+
tdd?: TddConfig;
|
|
168
180
|
}
|
|
169
181
|
export interface TransitionRule {
|
|
170
182
|
from: FlowStage;
|