cclaw-cli 0.48.5 → 0.48.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter.js +32 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +44 -5
- package/dist/content/hooks.d.ts +2 -2
- package/dist/content/hooks.js +293 -89
- package/dist/content/ideate-command.js +11 -0
- package/dist/content/iron-laws.d.ts +142 -0
- package/dist/content/iron-laws.js +191 -0
- package/dist/content/meta-skill.js +1 -0
- package/dist/content/next-command.js +12 -0
- package/dist/content/observe.js +555 -45
- package/dist/content/ops-command.js +11 -0
- package/dist/content/session-hooks.js +3 -1
- package/dist/content/stage-schema.d.ts +16 -0
- package/dist/content/stage-schema.js +82 -5
- package/dist/content/stages/review.js +4 -4
- package/dist/content/stages/tdd.js +7 -7
- package/dist/content/start-command.js +12 -0
- package/dist/content/subagents.js +26 -0
- package/dist/content/templates.js +8 -0
- package/dist/content/view-command.js +11 -0
- package/dist/doctor.js +6 -2
- package/dist/harness-adapters.js +3 -0
- package/dist/install.js +11 -1
- package/dist/internal/advance-stage.js +14 -2
- package/dist/internal/envelope-validate.d.ts +7 -0
- package/dist/internal/envelope-validate.js +66 -0
- package/dist/internal/knowledge-digest.d.ts +7 -0
- package/dist/internal/knowledge-digest.js +93 -0
- package/dist/internal/tdd-red-evidence.d.ts +7 -0
- package/dist/internal/tdd-red-evidence.js +130 -0
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +95 -0
- package/dist/tdd-cycle.d.ts +7 -0
- package/dist/tdd-cycle.js +29 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { selectRelevantLearnings } from "../knowledge-store.js";
|
|
2
|
+
import { FLOW_STAGES } from "../types.js";
|
|
3
|
+
function parseCsv(raw) {
|
|
4
|
+
return raw
|
|
5
|
+
.split(",")
|
|
6
|
+
.map((value) => value.trim())
|
|
7
|
+
.filter((value) => value.length > 0);
|
|
8
|
+
}
|
|
9
|
+
function parseKnowledgeDigestArgs(tokens) {
|
|
10
|
+
const args = {
|
|
11
|
+
diffFiles: [],
|
|
12
|
+
openGates: [],
|
|
13
|
+
limit: 8,
|
|
14
|
+
format: "markdown"
|
|
15
|
+
};
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
if (!token.startsWith("--")) {
|
|
18
|
+
throw new Error(`Unknown positional token for knowledge-digest: ${token}`);
|
|
19
|
+
}
|
|
20
|
+
if (token.startsWith("--stage=")) {
|
|
21
|
+
const value = token.replace("--stage=", "").trim();
|
|
22
|
+
if (!value)
|
|
23
|
+
continue;
|
|
24
|
+
if (!FLOW_STAGES.includes(value)) {
|
|
25
|
+
throw new Error(`--stage must be one of: ${FLOW_STAGES.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
args.stage = value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (token.startsWith("--branch=")) {
|
|
31
|
+
const value = token.replace("--branch=", "").trim();
|
|
32
|
+
if (value.length > 0) {
|
|
33
|
+
args.branch = value;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token.startsWith("--diff-files=")) {
|
|
38
|
+
args.diffFiles.push(...parseCsv(token.replace("--diff-files=", "")));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (token.startsWith("--open-gates=")) {
|
|
42
|
+
args.openGates.push(...parseCsv(token.replace("--open-gates=", "")));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token.startsWith("--limit=")) {
|
|
46
|
+
const raw = token.replace("--limit=", "").trim();
|
|
47
|
+
const value = Number.parseInt(raw, 10);
|
|
48
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
49
|
+
throw new Error("--limit must be a positive integer.");
|
|
50
|
+
}
|
|
51
|
+
args.limit = value;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (token === "--json") {
|
|
55
|
+
args.format = "json";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token === "--markdown") {
|
|
59
|
+
args.format = "markdown";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unknown flag for knowledge-digest: ${token}`);
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
function markdownDigest(rows) {
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return "(no relevant learnings)";
|
|
69
|
+
}
|
|
70
|
+
return rows
|
|
71
|
+
.map((entry) => {
|
|
72
|
+
const stage = entry.stage ?? "global";
|
|
73
|
+
const domain = entry.domain ?? "general";
|
|
74
|
+
return `- [${entry.confidence} | ${stage} | ${domain}] ${entry.trigger} -> ${entry.action}`;
|
|
75
|
+
})
|
|
76
|
+
.join("\n");
|
|
77
|
+
}
|
|
78
|
+
export async function runKnowledgeDigestCommand(projectRoot, tokens, io) {
|
|
79
|
+
const args = parseKnowledgeDigestArgs(tokens);
|
|
80
|
+
const rows = await selectRelevantLearnings(projectRoot, {
|
|
81
|
+
stage: args.stage,
|
|
82
|
+
branch: args.branch,
|
|
83
|
+
diffFiles: args.diffFiles,
|
|
84
|
+
openGates: args.openGates,
|
|
85
|
+
limit: args.limit
|
|
86
|
+
});
|
|
87
|
+
if (args.format === "json") {
|
|
88
|
+
io.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
io.stdout.write(`${markdownDigest(rows)}\n`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
|
+
import { readFlowState } from "../runs.js";
|
|
5
|
+
import { hasFailingTestForPath, parseTddCycleLog } from "../tdd-cycle.js";
|
|
6
|
+
function normalizePath(value) {
|
|
7
|
+
return value.replace(/\\/gu, "/").toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function parseArgs(tokens) {
|
|
10
|
+
const args = { quiet: false };
|
|
11
|
+
for (const token of tokens) {
|
|
12
|
+
if (token === "--quiet") {
|
|
13
|
+
args.quiet = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (token.startsWith("--path=")) {
|
|
17
|
+
const value = token.slice("--path=".length).trim();
|
|
18
|
+
if (!value) {
|
|
19
|
+
throw new Error("--path must not be empty.");
|
|
20
|
+
}
|
|
21
|
+
args.targetPath = value;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (token.startsWith("--run-id=")) {
|
|
25
|
+
const value = token.slice("--run-id=".length).trim();
|
|
26
|
+
if (value) {
|
|
27
|
+
args.runId = value;
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Unknown flag for tdd-red-evidence: ${token}`);
|
|
32
|
+
}
|
|
33
|
+
if (!args.targetPath) {
|
|
34
|
+
throw new Error("Missing required flag: --path=<production-file-path>");
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
function parseAutoEvidence(text) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const rawLine of text.split(/\r?\n/gu)) {
|
|
41
|
+
const line = rawLine.trim();
|
|
42
|
+
if (!line)
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(line);
|
|
46
|
+
const exitCode = parsed.exitCode;
|
|
47
|
+
if (typeof exitCode !== "number" || exitCode === 0)
|
|
48
|
+
continue;
|
|
49
|
+
const runId = typeof parsed.runId === "string" && parsed.runId.length > 0
|
|
50
|
+
? parsed.runId
|
|
51
|
+
: "active";
|
|
52
|
+
const rawPaths = Array.isArray(parsed.paths)
|
|
53
|
+
? parsed.paths
|
|
54
|
+
: typeof parsed.path === "string"
|
|
55
|
+
? [parsed.path]
|
|
56
|
+
: [];
|
|
57
|
+
const paths = rawPaths
|
|
58
|
+
.filter((value) => typeof value === "string")
|
|
59
|
+
.map((value) => value.trim())
|
|
60
|
+
.filter((value) => value.length > 0);
|
|
61
|
+
if (paths.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
out.push({
|
|
64
|
+
runId,
|
|
65
|
+
exitCode,
|
|
66
|
+
paths
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore malformed lines
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
|
|
76
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (options.runId && entry.runId !== options.runId)
|
|
79
|
+
continue;
|
|
80
|
+
for (const filePath of entry.paths) {
|
|
81
|
+
const normalized = normalizePath(filePath);
|
|
82
|
+
if (normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
90
|
+
const args = parseArgs(tokens);
|
|
91
|
+
const flowState = await readFlowState(projectRoot).catch(() => null);
|
|
92
|
+
const effectiveRunId = args.runId ?? flowState?.activeRunId;
|
|
93
|
+
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
94
|
+
const autoEvidencePath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-red-evidence.jsonl");
|
|
95
|
+
let cycleLogHasRed = false;
|
|
96
|
+
let autoEvidenceHasRed = false;
|
|
97
|
+
try {
|
|
98
|
+
const raw = await fs.readFile(tddLogPath, "utf8");
|
|
99
|
+
const entries = parseTddCycleLog(raw);
|
|
100
|
+
cycleLogHasRed = hasFailingTestForPath(entries, args.targetPath, {
|
|
101
|
+
runId: effectiveRunId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
cycleLogHasRed = false;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const raw = await fs.readFile(autoEvidencePath, "utf8");
|
|
109
|
+
const entries = parseAutoEvidence(raw);
|
|
110
|
+
autoEvidenceHasRed = hasFailingAutoEvidenceForPath(entries, args.targetPath, {
|
|
111
|
+
runId: effectiveRunId
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
autoEvidenceHasRed = false;
|
|
116
|
+
}
|
|
117
|
+
const hasRed = cycleLogHasRed || autoEvidenceHasRed;
|
|
118
|
+
if (!args.quiet) {
|
|
119
|
+
io.stdout.write(`${JSON.stringify({
|
|
120
|
+
ok: hasRed,
|
|
121
|
+
path: args.targetPath,
|
|
122
|
+
runId: effectiveRunId ?? null,
|
|
123
|
+
sources: {
|
|
124
|
+
tddCycleLog: cycleLogHasRed,
|
|
125
|
+
autoEvidence: autoEvidenceHasRed
|
|
126
|
+
}
|
|
127
|
+
}, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
return hasRed ? 0 : 2;
|
|
130
|
+
}
|
|
@@ -65,6 +65,13 @@ export interface ReadKnowledgeResult {
|
|
|
65
65
|
entries: KnowledgeEntry[];
|
|
66
66
|
malformedLines: number;
|
|
67
67
|
}
|
|
68
|
+
export interface SelectRelevantLearningsOptions {
|
|
69
|
+
stage?: FlowStage | null;
|
|
70
|
+
branch?: string | null;
|
|
71
|
+
diffFiles?: string[];
|
|
72
|
+
openGates?: string[];
|
|
73
|
+
limit?: number;
|
|
74
|
+
}
|
|
68
75
|
export declare function validateKnowledgeEntry(entry: unknown): {
|
|
69
76
|
ok: boolean;
|
|
70
77
|
errors: string[];
|
|
@@ -72,3 +79,4 @@ export declare function validateKnowledgeEntry(entry: unknown): {
|
|
|
72
79
|
export declare function materializeKnowledgeEntry(seed: KnowledgeSeedEntry, defaults?: AppendKnowledgeDefaults): KnowledgeEntry;
|
|
73
80
|
export declare function readKnowledgeSafely(projectRoot: string, options?: ReadKnowledgeOptions): Promise<ReadKnowledgeResult>;
|
|
74
81
|
export declare function appendKnowledge(projectRoot: string, seeds: KnowledgeSeedEntry[], defaults?: AppendKnowledgeDefaults): Promise<AppendKnowledgeResult>;
|
|
82
|
+
export declare function selectRelevantLearnings(projectRoot: string, options?: SelectRelevantLearningsOptions): Promise<KnowledgeEntry[]>;
|
package/dist/knowledge-store.js
CHANGED
|
@@ -327,3 +327,98 @@ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
|
|
|
327
327
|
appendedEntries
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
|
+
function tokenizeText(value) {
|
|
331
|
+
if (!value)
|
|
332
|
+
return [];
|
|
333
|
+
return value
|
|
334
|
+
.toLowerCase()
|
|
335
|
+
.split(/[^a-z0-9]+/u)
|
|
336
|
+
.map((token) => token.trim())
|
|
337
|
+
.filter((token) => token.length >= 3);
|
|
338
|
+
}
|
|
339
|
+
function uniqueTokens(values) {
|
|
340
|
+
return [...new Set(values)];
|
|
341
|
+
}
|
|
342
|
+
function pathTokens(paths) {
|
|
343
|
+
if (!Array.isArray(paths) || paths.length === 0)
|
|
344
|
+
return [];
|
|
345
|
+
const tokens = [];
|
|
346
|
+
for (const filePath of paths) {
|
|
347
|
+
tokens.push(...tokenizeText(filePath));
|
|
348
|
+
}
|
|
349
|
+
return uniqueTokens(tokens);
|
|
350
|
+
}
|
|
351
|
+
export async function selectRelevantLearnings(projectRoot, options = {}) {
|
|
352
|
+
const { entries } = await readKnowledgeSafely(projectRoot);
|
|
353
|
+
if (entries.length === 0) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
const stage = options.stage ?? null;
|
|
357
|
+
const branchTokens = tokenizeText(options.branch ?? null);
|
|
358
|
+
const diffTokens = pathTokens(options.diffFiles);
|
|
359
|
+
const gateTokens = pathTokens(options.openGates);
|
|
360
|
+
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
|
|
361
|
+
? Math.floor(options.limit)
|
|
362
|
+
: 8;
|
|
363
|
+
const ranked = entries.map((entry, index) => {
|
|
364
|
+
let score = 0;
|
|
365
|
+
if (stage) {
|
|
366
|
+
if (entry.stage === stage) {
|
|
367
|
+
score += 4;
|
|
368
|
+
}
|
|
369
|
+
else if (entry.origin_stage === stage) {
|
|
370
|
+
score += 3;
|
|
371
|
+
}
|
|
372
|
+
else if (entry.stage === null) {
|
|
373
|
+
score += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (entry.confidence === "high")
|
|
377
|
+
score += 2;
|
|
378
|
+
if (entry.confidence === "medium")
|
|
379
|
+
score += 1;
|
|
380
|
+
if (entry.frequency >= 3)
|
|
381
|
+
score += 1;
|
|
382
|
+
if (entry.maturity === "lifted-to-enforcement")
|
|
383
|
+
score -= 1;
|
|
384
|
+
const searchable = [
|
|
385
|
+
...tokenizeText(entry.domain),
|
|
386
|
+
...tokenizeText(entry.trigger),
|
|
387
|
+
...tokenizeText(entry.action),
|
|
388
|
+
...tokenizeText(entry.origin_feature),
|
|
389
|
+
...tokenizeText(entry.project)
|
|
390
|
+
];
|
|
391
|
+
const searchSet = new Set(searchable);
|
|
392
|
+
for (const token of branchTokens) {
|
|
393
|
+
if (searchSet.has(token))
|
|
394
|
+
score += 2;
|
|
395
|
+
}
|
|
396
|
+
for (const token of diffTokens) {
|
|
397
|
+
if (searchSet.has(token))
|
|
398
|
+
score += 2;
|
|
399
|
+
}
|
|
400
|
+
for (const token of gateTokens) {
|
|
401
|
+
if (searchSet.has(token))
|
|
402
|
+
score += 2;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
index,
|
|
406
|
+
score,
|
|
407
|
+
entry
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
ranked.sort((a, b) => {
|
|
411
|
+
if (b.score !== a.score)
|
|
412
|
+
return b.score - a.score;
|
|
413
|
+
const bySeen = Date.parse(b.entry.last_seen_ts) - Date.parse(a.entry.last_seen_ts);
|
|
414
|
+
if (!Number.isNaN(bySeen) && bySeen !== 0)
|
|
415
|
+
return bySeen;
|
|
416
|
+
if (b.entry.frequency !== a.entry.frequency)
|
|
417
|
+
return b.entry.frequency - a.entry.frequency;
|
|
418
|
+
return b.index - a.index;
|
|
419
|
+
});
|
|
420
|
+
return ranked
|
|
421
|
+
.filter((row) => row.score > 0)
|
|
422
|
+
.slice(0, limit)
|
|
423
|
+
.map((row) => row.entry);
|
|
424
|
+
}
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -20,3 +20,10 @@ export declare function parseTddCycleLog(text: string): TddCycleEntry[];
|
|
|
20
20
|
export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
|
|
21
21
|
runId?: string;
|
|
22
22
|
}): TddCycleValidation;
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether the log contains a failing RED record associated with
|
|
25
|
+
* `productionPath` for the active run.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hasFailingTestForPath(entries: TddCycleEntry[], productionPath: string, options?: {
|
|
28
|
+
runId?: string;
|
|
29
|
+
}): boolean;
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -119,3 +119,32 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
119
119
|
sliceCount: bySlice.size
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
+
function normalizePath(value) {
|
|
123
|
+
return value.replace(/\\/gu, "/").toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Checks whether the log contains a failing RED record associated with
|
|
127
|
+
* `productionPath` for the active run.
|
|
128
|
+
*/
|
|
129
|
+
export function hasFailingTestForPath(entries, productionPath, options = {}) {
|
|
130
|
+
const normalizedTarget = normalizePath(productionPath);
|
|
131
|
+
const filtered = options.runId
|
|
132
|
+
? entries.filter((entry) => entry.runId === options.runId)
|
|
133
|
+
: entries;
|
|
134
|
+
for (const entry of filtered) {
|
|
135
|
+
if (entry.phase !== "red")
|
|
136
|
+
continue;
|
|
137
|
+
if (entry.exitCode === undefined || entry.exitCode === 0)
|
|
138
|
+
continue;
|
|
139
|
+
if (!Array.isArray(entry.files) || entry.files.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const hasMatch = entry.files.some((filePath) => {
|
|
142
|
+
const normalized = normalizePath(filePath);
|
|
143
|
+
return normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`);
|
|
144
|
+
});
|
|
145
|
+
if (hasMatch) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -109,6 +109,10 @@ export interface TddPathConfig {
|
|
|
109
109
|
export interface CompoundConfig {
|
|
110
110
|
recurrenceThreshold?: number;
|
|
111
111
|
}
|
|
112
|
+
export interface IronLawsConfig {
|
|
113
|
+
mode?: "advisory" | "strict";
|
|
114
|
+
strictLaws?: string[];
|
|
115
|
+
}
|
|
112
116
|
export interface CclawConfig {
|
|
113
117
|
version: string;
|
|
114
118
|
flowVersion: string;
|
|
@@ -173,6 +177,8 @@ export interface CclawConfig {
|
|
|
173
177
|
* discipline tractable without forcing it on tiny quick-track fixes.
|
|
174
178
|
*/
|
|
175
179
|
sliceReview?: SliceReviewConfig;
|
|
180
|
+
/** Optional per-law strictness controls for hook-enforced iron laws. */
|
|
181
|
+
ironLaws?: IronLawsConfig;
|
|
176
182
|
}
|
|
177
183
|
/**
|
|
178
184
|
* @deprecated Use `CclawConfig` instead.
|