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.
Files changed (37) hide show
  1. package/dist/artifact-linter.js +32 -0
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.js +44 -5
  4. package/dist/content/hooks.d.ts +2 -2
  5. package/dist/content/hooks.js +293 -89
  6. package/dist/content/ideate-command.js +11 -0
  7. package/dist/content/iron-laws.d.ts +142 -0
  8. package/dist/content/iron-laws.js +191 -0
  9. package/dist/content/meta-skill.js +1 -0
  10. package/dist/content/next-command.js +12 -0
  11. package/dist/content/observe.js +555 -45
  12. package/dist/content/ops-command.js +11 -0
  13. package/dist/content/session-hooks.js +3 -1
  14. package/dist/content/stage-schema.d.ts +16 -0
  15. package/dist/content/stage-schema.js +82 -5
  16. package/dist/content/stages/review.js +4 -4
  17. package/dist/content/stages/tdd.js +7 -7
  18. package/dist/content/start-command.js +12 -0
  19. package/dist/content/subagents.js +26 -0
  20. package/dist/content/templates.js +8 -0
  21. package/dist/content/view-command.js +11 -0
  22. package/dist/doctor.js +6 -2
  23. package/dist/harness-adapters.js +3 -0
  24. package/dist/install.js +11 -1
  25. package/dist/internal/advance-stage.js +14 -2
  26. package/dist/internal/envelope-validate.d.ts +7 -0
  27. package/dist/internal/envelope-validate.js +66 -0
  28. package/dist/internal/knowledge-digest.d.ts +7 -0
  29. package/dist/internal/knowledge-digest.js +93 -0
  30. package/dist/internal/tdd-red-evidence.d.ts +7 -0
  31. package/dist/internal/tdd-red-evidence.js +130 -0
  32. package/dist/knowledge-store.d.ts +8 -0
  33. package/dist/knowledge-store.js +95 -0
  34. package/dist/tdd-cycle.d.ts +7 -0
  35. package/dist/tdd-cycle.js +29 -0
  36. package/dist/types.d.ts +6 -0
  37. 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,7 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export declare function runTddRedEvidenceCommand(projectRoot: string, tokens: string[], io: InternalIo): Promise<number>;
7
+ export {};
@@ -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[]>;
@@ -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
+ }
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.5",
3
+ "version": "0.48.7",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {