cclaw-cli 0.46.14 → 0.47.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/README.md +16 -11
- package/dist/artifact-linter.d.ts +2 -0
- package/dist/artifact-linter.js +137 -9
- package/dist/config.d.ts +12 -7
- package/dist/config.js +79 -10
- package/dist/content/compound-command.d.ts +5 -2
- package/dist/content/compound-command.js +47 -16
- package/dist/content/contracts.js +1 -1
- package/dist/content/examples.d.ts +1 -0
- package/dist/content/examples.js +13 -0
- package/dist/content/harnesses-doc.js +11 -0
- package/dist/content/learnings.d.ts +2 -1
- package/dist/content/learnings.js +5 -3
- package/dist/content/observe.d.ts +2 -1
- package/dist/content/observe.js +174 -14
- package/dist/content/stage-schema.js +21 -8
- package/dist/content/stages/design.js +0 -2
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.js +19 -4
- package/dist/gate-evidence.js +6 -1
- package/dist/install.d.ts +3 -3
- package/dist/install.js +13 -8
- package/dist/internal/advance-stage.js +53 -16
- package/dist/knowledge-store.d.ts +3 -0
- package/dist/knowledge-store.js +11 -1
- package/dist/retro-gate.js +11 -1
- package/dist/run-persistence.js +11 -1
- package/dist/tdd-cycle.js +19 -1
- package/dist/types.d.ts +30 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,10 +154,11 @@ If cclaw detects a Node / Python / Go project at init time, a sixth
|
|
|
154
154
|
default surface — a new user sees nothing they need to understand yet.
|
|
155
155
|
|
|
156
156
|
Advanced knobs (`promptGuardMode` / `tddEnforcement` per-axis overrides,
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
`tdd.testPathPatterns` / `tdd.productionPathPatterns`,
|
|
158
|
+
`compound.recurrenceThreshold`, `defaultTrack`, `trackHeuristics`,
|
|
159
|
+
`sliceReview`) are **opt-in**: add them by hand when you need them.
|
|
160
|
+
`cclaw upgrade` preserves exactly what you wrote — it never silently
|
|
161
|
+
reintroduces defaults you removed.
|
|
161
162
|
|
|
162
163
|
Full key-by-key reference: [`docs/config.md`](./docs/config.md).
|
|
163
164
|
|
|
@@ -240,7 +241,7 @@ the flow matches the task.
|
|
|
240
241
|
|---|---|---|
|
|
241
242
|
| **quick** | `spec → tdd → review → ship` | `bug`, `hotfix`, `typo`, `rename`, `bump`, `docs only`, one-liners |
|
|
242
243
|
| **medium** | `brainstorm → spec → plan → tdd → review → ship` | `add endpoint`, `add field`, `extend existing`, `wire integration` |
|
|
243
|
-
| **standard** _(default)_ | all 8 stages | `new feature`, `refactor`, `migration`, `platform`, `schema`, `architecture` |
|
|
244
|
+
| **standard** _(default)_ | all 8 stages (+ mandatory design-time parallel research fleet) | `new feature`, `refactor`, `migration`, `platform`, `schema`, `architecture` |
|
|
244
245
|
|
|
245
246
|
**Every track ends with the same auto-closeout chain.** Once ship
|
|
246
247
|
completes, `/cc-next` automatically drives
|
|
@@ -250,10 +251,11 @@ without re-drafting retros or re-asking structured questions. See
|
|
|
250
251
|
[Ship and closeout](#ship-and-closeout--automatic-resumable).
|
|
251
252
|
|
|
252
253
|
Each critical-path stage produces a dated artifact under
|
|
253
|
-
`.cclaw/artifacts/`: `00-idea.md` (seed), `01-brainstorm.md`
|
|
254
|
+
`.cclaw/artifacts/`: `00-idea.md` (seed), `01-brainstorm.md`, `02-scope.md`,
|
|
255
|
+
`02a-research.md` (design research fleet synthesis), `03-design.md` through
|
|
254
256
|
`08-ship.md`. Closeout adds `09-retro.md`; archive then rolls the whole
|
|
255
|
-
bundle into `.cclaw/runs/<YYYY-MM-DD-slug>/` and resets the active flow
|
|
256
|
-
|
|
257
|
+
bundle into `.cclaw/runs/<YYYY-MM-DD-slug>/` and resets the active flow for
|
|
258
|
+
the next feature.
|
|
257
259
|
|
|
258
260
|
### Track heuristics are configurable (advisory)
|
|
259
261
|
|
|
@@ -312,9 +314,12 @@ it into ceremony:
|
|
|
312
314
|
protocol emits typed entries (`rule` / `pattern` / `lesson`) to
|
|
313
315
|
`.cclaw/knowledge.jsonl` as the flow progresses — not only at retro.
|
|
314
316
|
Retro itself adds a `compound` entry, and the automatic compound pass
|
|
315
|
-
after ship promotes recurring entries
|
|
316
|
-
rules/protocols/skills
|
|
317
|
-
|
|
317
|
+
after ship promotes recurring entries into first-class
|
|
318
|
+
rules/protocols/skills (base threshold from
|
|
319
|
+
`compound.recurrenceThreshold`, temporarily lowered to 2 for repositories
|
|
320
|
+
with <5 archived runs, plus a critical-severity single-hit override) so
|
|
321
|
+
the **next** run is easier. Strict JSONL schema keeps the whole thing
|
|
322
|
+
machine-queryable.
|
|
318
323
|
- **Automatic integrity checks.** Runtime health is verified on every
|
|
319
324
|
stage transition — no command you need to remember to run.
|
|
320
325
|
|
|
@@ -15,6 +15,7 @@ export interface LintResult {
|
|
|
15
15
|
export declare function extractMarkdownSectionBody(markdown: string, section: string): string | null;
|
|
16
16
|
export type LearningEntryType = "rule" | "pattern" | "lesson" | "compound";
|
|
17
17
|
export type LearningConfidence = "high" | "medium" | "low";
|
|
18
|
+
export type LearningSeverity = "critical" | "important" | "suggestion";
|
|
18
19
|
export type LearningUniversality = "project" | "personal" | "universal";
|
|
19
20
|
export type LearningMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
|
|
20
21
|
export type LearningSource = "stage" | "retro" | "compound" | "ideate" | "manual";
|
|
@@ -23,6 +24,7 @@ export interface LearningSeedEntry {
|
|
|
23
24
|
trigger: string;
|
|
24
25
|
action: string;
|
|
25
26
|
confidence: LearningConfidence;
|
|
27
|
+
severity?: LearningSeverity;
|
|
26
28
|
domain?: string | null;
|
|
27
29
|
stage?: FlowStage | null;
|
|
28
30
|
origin_stage?: FlowStage | null;
|
package/dist/artifact-linter.js
CHANGED
|
@@ -12,25 +12,52 @@ async function resolveArtifactPath(projectRoot, fileName) {
|
|
|
12
12
|
function normalizeHeadingTitle(title) {
|
|
13
13
|
return title.trim().replace(/\s+/g, " ");
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Collect H2 sections and body content (`## Section Name`).
|
|
17
|
+
*
|
|
18
|
+
* - Ignores lines that live inside fenced code blocks (``` / ~~~) so a
|
|
19
|
+
* commented `## Approaches` inside an example doesn't open a phantom
|
|
20
|
+
* section and swallow real content.
|
|
21
|
+
* - When the same heading appears more than once at the top level we
|
|
22
|
+
* concatenate the bodies rather than silently overwriting the earlier
|
|
23
|
+
* occurrence. This keeps lint rules honest when authors split a section
|
|
24
|
+
* into multiple passes.
|
|
25
|
+
*/
|
|
16
26
|
function extractH2Sections(markdown) {
|
|
17
27
|
const sections = new Map();
|
|
18
28
|
const lines = markdown.split(/\r?\n/);
|
|
19
29
|
let currentHeading = null;
|
|
20
30
|
let buffer = [];
|
|
31
|
+
let fenced = null;
|
|
21
32
|
const flush = () => {
|
|
22
33
|
if (currentHeading === null)
|
|
23
34
|
return;
|
|
24
|
-
sections.
|
|
35
|
+
const existing = sections.get(currentHeading);
|
|
36
|
+
const body = buffer.join("\n");
|
|
37
|
+
sections.set(currentHeading, existing === undefined ? body : `${existing}\n${body}`);
|
|
25
38
|
};
|
|
26
39
|
for (const line of lines) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
const fenceMatch = /^(```|~~~)/u.exec(line);
|
|
41
|
+
if (fenceMatch) {
|
|
42
|
+
if (fenced === null) {
|
|
43
|
+
fenced = fenceMatch[1] ?? null;
|
|
44
|
+
}
|
|
45
|
+
else if (line.startsWith(fenced)) {
|
|
46
|
+
fenced = null;
|
|
47
|
+
}
|
|
48
|
+
if (currentHeading !== null)
|
|
49
|
+
buffer.push(line);
|
|
32
50
|
continue;
|
|
33
51
|
}
|
|
52
|
+
if (fenced === null) {
|
|
53
|
+
const match = /^##\s+(.+)$/u.exec(line);
|
|
54
|
+
if (match) {
|
|
55
|
+
flush();
|
|
56
|
+
currentHeading = normalizeHeadingTitle(match[1] ?? "");
|
|
57
|
+
buffer = [];
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
34
61
|
if (currentHeading !== null) {
|
|
35
62
|
buffer.push(line);
|
|
36
63
|
}
|
|
@@ -299,6 +326,7 @@ function validateVerificationLadder(sectionBody) {
|
|
|
299
326
|
}
|
|
300
327
|
const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
301
328
|
const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
329
|
+
const LEARNING_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
|
302
330
|
const LEARNING_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
|
|
303
331
|
const LEARNING_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
|
|
304
332
|
const LEARNING_SOURCE_SET = new Set([
|
|
@@ -314,6 +342,7 @@ const LEARNING_ALLOWED_KEYS = new Set([
|
|
|
314
342
|
"trigger",
|
|
315
343
|
"action",
|
|
316
344
|
"confidence",
|
|
345
|
+
"severity",
|
|
317
346
|
"domain",
|
|
318
347
|
"stage",
|
|
319
348
|
"origin_stage",
|
|
@@ -377,6 +406,13 @@ function parseLearningSeedEntry(raw, index) {
|
|
|
377
406
|
error: `Learnings bullet #${index} must set confidence to high|medium|low.`
|
|
378
407
|
};
|
|
379
408
|
}
|
|
409
|
+
const severity = typeof obj.severity === "string" ? obj.severity.toLowerCase() : undefined;
|
|
410
|
+
if (severity !== undefined && !LEARNING_SEVERITY_SET.has(severity)) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
error: `Learnings bullet #${index} field "severity" must be critical|important|suggestion.`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
380
416
|
if (obj.domain !== undefined && !isNullableString(obj.domain)) {
|
|
381
417
|
return { ok: false, error: `Learnings bullet #${index} field "domain" must be string or null.` };
|
|
382
418
|
}
|
|
@@ -443,7 +479,8 @@ function parseLearningSeedEntry(raw, index) {
|
|
|
443
479
|
type: type,
|
|
444
480
|
trigger,
|
|
445
481
|
action,
|
|
446
|
-
confidence: confidence
|
|
482
|
+
confidence: confidence,
|
|
483
|
+
...(severity ? { severity: severity } : {})
|
|
447
484
|
}
|
|
448
485
|
};
|
|
449
486
|
}
|
|
@@ -859,6 +896,49 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
859
896
|
details: learnings.details
|
|
860
897
|
});
|
|
861
898
|
}
|
|
899
|
+
if (stage === "brainstorm") {
|
|
900
|
+
// Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
|
|
901
|
+
// APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
|
|
902
|
+
// prose-only — nothing failed when the Selected Direction section
|
|
903
|
+
// omitted an approval marker, or when the Approaches table collapsed
|
|
904
|
+
// to a single row (defeating the "2-3 distinct approaches" gate).
|
|
905
|
+
const approachesBody = sectionBodyByName(sections, "Approaches");
|
|
906
|
+
if (approachesBody !== null) {
|
|
907
|
+
const tableRows = approachesBody
|
|
908
|
+
.split(/\r?\n/u)
|
|
909
|
+
.map((line) => line.trim())
|
|
910
|
+
.filter((line) => line.startsWith("|"))
|
|
911
|
+
.filter((line) => !/^\|\s*[-: |]+\|\s*$/u.test(line))
|
|
912
|
+
.filter((line) => !/^\|\s*approach\b/iu.test(line));
|
|
913
|
+
const bulletRows = approachesBody
|
|
914
|
+
.split(/\r?\n/u)
|
|
915
|
+
.map((line) => line.trim())
|
|
916
|
+
.filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
|
|
917
|
+
const rowCount = Math.max(tableRows.length, bulletRows.length);
|
|
918
|
+
findings.push({
|
|
919
|
+
section: "Distinct Approaches Enforcement",
|
|
920
|
+
required: true,
|
|
921
|
+
rule: "Approaches section must document at least 2 distinct approaches so the Iron Law comparison is meaningful.",
|
|
922
|
+
found: rowCount >= 2,
|
|
923
|
+
details: rowCount >= 2
|
|
924
|
+
? `Detected ${rowCount} approach row(s).`
|
|
925
|
+
: `Detected ${rowCount} approach row(s); at least 2 required.`
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const directionBody = sectionBodyByName(sections, "Selected Direction");
|
|
929
|
+
if (directionBody !== null) {
|
|
930
|
+
const approvalMarker = /\bapprov(?:ed|al)\b/iu.test(directionBody);
|
|
931
|
+
findings.push({
|
|
932
|
+
section: "Direction Approval Marker",
|
|
933
|
+
required: true,
|
|
934
|
+
rule: "Selected Direction section must state an explicit approval marker (for example `Approval: approved` or `Approved by: user`).",
|
|
935
|
+
found: approvalMarker,
|
|
936
|
+
details: approvalMarker
|
|
937
|
+
? "Approval marker present in Selected Direction."
|
|
938
|
+
: "No explicit `approved`/`approval` marker found in Selected Direction."
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
862
942
|
if (stage === "plan") {
|
|
863
943
|
const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
|
|
864
944
|
headingPresent(sections, "No-Placeholder Scan") ||
|
|
@@ -904,12 +984,13 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
904
984
|
});
|
|
905
985
|
}
|
|
906
986
|
if (stage === "scope") {
|
|
987
|
+
const lockedDecisionsBody = sectionBodyByName(sections, "Locked Decisions (D-XX)") ?? "";
|
|
907
988
|
const strictScopeGuards = parsedFrontmatter.hasFrontmatter ||
|
|
908
989
|
headingPresent(sections, "Locked Decisions (D-XX)");
|
|
909
990
|
const scopeSections = [
|
|
910
991
|
sectionBodyByName(sections, "In Scope / Out of Scope") ?? "",
|
|
911
992
|
sectionBodyByName(sections, "Scope Summary") ?? "",
|
|
912
|
-
|
|
993
|
+
lockedDecisionsBody
|
|
913
994
|
].join("\n");
|
|
914
995
|
const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
|
|
915
996
|
findings.push({
|
|
@@ -921,6 +1002,45 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
921
1002
|
? "No scope-reduction phrases detected in scope boundary sections."
|
|
922
1003
|
: `Detected scope-reduction phrase(s): ${reductionHits.join(", ")}.`
|
|
923
1004
|
});
|
|
1005
|
+
// When the Locked Decisions section is present we must enforce the
|
|
1006
|
+
// D-XX ID contract at runtime (previously this was prose-only in the
|
|
1007
|
+
// artifactValidation rule). Empty body, missing IDs, and duplicate
|
|
1008
|
+
// IDs all fail the lint; absence of the section remains advisory so
|
|
1009
|
+
// scope stays optional for small/quick tracks.
|
|
1010
|
+
if (headingPresent(sections, "Locked Decisions (D-XX)")) {
|
|
1011
|
+
const decisionIds = extractDecisionIds(lockedDecisionsBody);
|
|
1012
|
+
const bulletLines = lockedDecisionsBody
|
|
1013
|
+
.split(/\r?\n/u)
|
|
1014
|
+
.map((line) => line.trim())
|
|
1015
|
+
.filter((line) => /^(?:[-*]|\|)\s+\S/u.test(line));
|
|
1016
|
+
const orphanBullets = bulletLines.filter((line) => !/\bD-\d+\b/u.test(line));
|
|
1017
|
+
const duplicateIds = (() => {
|
|
1018
|
+
const all = lockedDecisionsBody.match(/\bD-\d+\b/gu) ?? [];
|
|
1019
|
+
const counts = new Map();
|
|
1020
|
+
for (const id of all)
|
|
1021
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
1022
|
+
return [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
|
|
1023
|
+
})();
|
|
1024
|
+
const issues = [];
|
|
1025
|
+
if (decisionIds.length === 0 && bulletLines.length === 0) {
|
|
1026
|
+
issues.push("section is empty");
|
|
1027
|
+
}
|
|
1028
|
+
if (orphanBullets.length > 0) {
|
|
1029
|
+
issues.push(`${orphanBullets.length} bullet(s) missing a D-XX ID`);
|
|
1030
|
+
}
|
|
1031
|
+
if (duplicateIds.length > 0) {
|
|
1032
|
+
issues.push(`duplicate IDs: ${duplicateIds.join(", ")}`);
|
|
1033
|
+
}
|
|
1034
|
+
findings.push({
|
|
1035
|
+
section: "Locked Decisions ID Integrity",
|
|
1036
|
+
required: true,
|
|
1037
|
+
rule: "Locked Decisions section must list each decision with a unique stable D-XX ID.",
|
|
1038
|
+
found: issues.length === 0,
|
|
1039
|
+
details: issues.length === 0
|
|
1040
|
+
? `${decisionIds.length} decision ID(s) recorded with no duplicates.`
|
|
1041
|
+
: issues.join("; ")
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
924
1044
|
}
|
|
925
1045
|
const passed = findings.every((f) => !f.required || f.found);
|
|
926
1046
|
return { stage, file: relFile, passed, findings };
|
|
@@ -1188,6 +1308,14 @@ export async function checkReviewVerdictConsistency(projectRoot) {
|
|
|
1188
1308
|
if (finalVerdict === "APPROVED" && (openCriticalCount > 0 || shipBlockerCount > 0)) {
|
|
1189
1309
|
errors.push(`Final Verdict is APPROVED but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Use BLOCKED or APPROVED_WITH_CONCERNS.`);
|
|
1190
1310
|
}
|
|
1311
|
+
// APPROVED_WITH_CONCERNS is intended for Important/Suggestion findings
|
|
1312
|
+
// the author has accepted. An *open* Critical finding or an active
|
|
1313
|
+
// shipBlocker must route through BLOCKED (review_verdict_blocked gate)
|
|
1314
|
+
// rather than pass as a concession — previously this slipped through.
|
|
1315
|
+
if (finalVerdict === "APPROVED_WITH_CONCERNS" &&
|
|
1316
|
+
(openCriticalCount > 0 || shipBlockerCount > 0)) {
|
|
1317
|
+
errors.push(`Final Verdict is APPROVED_WITH_CONCERNS but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Resolve them or use BLOCKED.`);
|
|
1318
|
+
}
|
|
1191
1319
|
return {
|
|
1192
1320
|
ok: errors.length === 0,
|
|
1193
1321
|
errors,
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type { FlowTrack, HarnessId, LanguageRulePack, VibyConfig } from "./types.js";
|
|
2
2
|
export declare function configPath(projectRoot: string): string;
|
|
3
3
|
/**
|
|
4
|
-
* Default test-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
* Default test-path patterns used by workflow-guard.sh to classify TDD writes.
|
|
5
|
+
*
|
|
6
|
+
* Scope is intentionally narrow and language-agnostic; users can extend this
|
|
7
|
+
* list in config when their repository uses different conventions.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_TDD_TEST_PATH_PATTERNS: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Legacy alias kept for backwards compatibility with `tddTestGlobs`.
|
|
12
|
+
* Prefer `tdd.testPathPatterns` in new configurations.
|
|
10
13
|
*/
|
|
11
14
|
export declare const DEFAULT_TDD_TEST_GLOBS: readonly string[];
|
|
15
|
+
export declare const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS: readonly string[];
|
|
16
|
+
export declare const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
|
|
12
17
|
/**
|
|
13
18
|
* Populated runtime view of config values that downstream callers (install,
|
|
14
19
|
* observe, doctor) consume. Always has the derived guard modes populated,
|
|
@@ -34,7 +39,7 @@ export declare function readConfig(projectRoot: string): Promise<VibyConfig>;
|
|
|
34
39
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
35
40
|
* only knobs a new user would meaningfully flip show up.
|
|
36
41
|
*/
|
|
37
|
-
type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview";
|
|
42
|
+
type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview";
|
|
38
43
|
/**
|
|
39
44
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
40
45
|
*
|
package/dist/config.js
CHANGED
|
@@ -19,6 +19,8 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
19
19
|
"promptGuardMode",
|
|
20
20
|
"tddEnforcement",
|
|
21
21
|
"tddTestGlobs",
|
|
22
|
+
"tdd",
|
|
23
|
+
"compound",
|
|
22
24
|
"gitHookGuards",
|
|
23
25
|
"defaultTrack",
|
|
24
26
|
"languageRulePacks",
|
|
@@ -74,18 +76,23 @@ export function configPath(projectRoot) {
|
|
|
74
76
|
return path.join(projectRoot, CONFIG_PATH);
|
|
75
77
|
}
|
|
76
78
|
/**
|
|
77
|
-
* Default test-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* guard script, even though the field is no longer written to the default
|
|
82
|
-
* `config.yaml` template.
|
|
79
|
+
* Default test-path patterns used by workflow-guard.sh to classify TDD writes.
|
|
80
|
+
*
|
|
81
|
+
* Scope is intentionally narrow and language-agnostic; users can extend this
|
|
82
|
+
* list in config when their repository uses different conventions.
|
|
83
83
|
*/
|
|
84
|
-
export const
|
|
84
|
+
export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
|
|
85
85
|
"**/*.test.*",
|
|
86
|
-
"
|
|
87
|
-
"**/
|
|
86
|
+
"**/tests/**",
|
|
87
|
+
"**/__tests__/**"
|
|
88
88
|
];
|
|
89
|
+
/**
|
|
90
|
+
* Legacy alias kept for backwards compatibility with `tddTestGlobs`.
|
|
91
|
+
* Prefer `tdd.testPathPatterns` in new configurations.
|
|
92
|
+
*/
|
|
93
|
+
export const DEFAULT_TDD_TEST_GLOBS = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
|
|
94
|
+
export const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = [];
|
|
95
|
+
export const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
|
|
89
96
|
/**
|
|
90
97
|
* Populated runtime view of config values that downstream callers (install,
|
|
91
98
|
* observe, doctor) consume. Always has the derived guard modes populated,
|
|
@@ -93,6 +100,8 @@ export const DEFAULT_TDD_TEST_GLOBS = [
|
|
|
93
100
|
* or neither.
|
|
94
101
|
*/
|
|
95
102
|
export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack = "standard") {
|
|
103
|
+
const tddTestPathPatterns = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
|
|
104
|
+
const tddProductionPathPatterns = [...DEFAULT_TDD_PRODUCTION_PATH_PATTERNS];
|
|
96
105
|
return {
|
|
97
106
|
version: CCLAW_VERSION,
|
|
98
107
|
flowVersion: FLOW_VERSION,
|
|
@@ -100,7 +109,14 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
100
109
|
strictness: "advisory",
|
|
101
110
|
promptGuardMode: "advisory",
|
|
102
111
|
tddEnforcement: "advisory",
|
|
103
|
-
tddTestGlobs: [...
|
|
112
|
+
tddTestGlobs: [...tddTestPathPatterns],
|
|
113
|
+
tdd: {
|
|
114
|
+
testPathPatterns: tddTestPathPatterns,
|
|
115
|
+
productionPathPatterns: tddProductionPathPatterns
|
|
116
|
+
},
|
|
117
|
+
compound: {
|
|
118
|
+
recurrenceThreshold: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD
|
|
119
|
+
},
|
|
104
120
|
gitHookGuards: false,
|
|
105
121
|
defaultTrack,
|
|
106
122
|
languageRulePacks: []
|
|
@@ -213,6 +229,48 @@ export async function readConfig(projectRoot) {
|
|
|
213
229
|
const tddTestGlobsRaw = parsed.tddTestGlobs;
|
|
214
230
|
const tddTestGlobs = validateStringArray(tddTestGlobsRaw, "tddTestGlobs", fullPath)
|
|
215
231
|
?? [...DEFAULT_TDD_TEST_GLOBS];
|
|
232
|
+
const hasTddField = Object.prototype.hasOwnProperty.call(parsed, "tdd");
|
|
233
|
+
const tddRaw = parsed.tdd;
|
|
234
|
+
let explicitTddTestPathPatterns;
|
|
235
|
+
let explicitTddProductionPathPatterns;
|
|
236
|
+
if (hasTddField) {
|
|
237
|
+
if (!isRecord(tddRaw)) {
|
|
238
|
+
throw configValidationError(fullPath, `"tdd" must be an object`);
|
|
239
|
+
}
|
|
240
|
+
const unknownTddKeys = Object.keys(tddRaw).filter((key) => key !== "testPathPatterns" && key !== "productionPathPatterns");
|
|
241
|
+
if (unknownTddKeys.length > 0) {
|
|
242
|
+
throw configValidationError(fullPath, `"tdd" has unknown key(s): ${unknownTddKeys.join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
explicitTddTestPathPatterns = validateStringArray(tddRaw.testPathPatterns, "tdd.testPathPatterns", fullPath);
|
|
245
|
+
explicitTddProductionPathPatterns = validateStringArray(tddRaw.productionPathPatterns, "tdd.productionPathPatterns", fullPath);
|
|
246
|
+
}
|
|
247
|
+
const resolvedTddTestPathPatterns = [
|
|
248
|
+
...(explicitTddTestPathPatterns ?? tddTestGlobs ?? DEFAULT_TDD_TEST_PATH_PATTERNS)
|
|
249
|
+
];
|
|
250
|
+
const resolvedTddProductionPathPatterns = [
|
|
251
|
+
...(explicitTddProductionPathPatterns ?? DEFAULT_TDD_PRODUCTION_PATH_PATTERNS)
|
|
252
|
+
];
|
|
253
|
+
const hasCompoundField = Object.prototype.hasOwnProperty.call(parsed, "compound");
|
|
254
|
+
const compoundRaw = parsed.compound;
|
|
255
|
+
let compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
256
|
+
if (hasCompoundField) {
|
|
257
|
+
if (!isRecord(compoundRaw)) {
|
|
258
|
+
throw configValidationError(fullPath, `"compound" must be an object`);
|
|
259
|
+
}
|
|
260
|
+
const unknownCompoundKeys = Object.keys(compoundRaw).filter((key) => key !== "recurrenceThreshold");
|
|
261
|
+
if (unknownCompoundKeys.length > 0) {
|
|
262
|
+
throw configValidationError(fullPath, `"compound" has unknown key(s): ${unknownCompoundKeys.join(", ")}`);
|
|
263
|
+
}
|
|
264
|
+
if (compoundRaw.recurrenceThreshold !== undefined &&
|
|
265
|
+
(typeof compoundRaw.recurrenceThreshold !== "number" ||
|
|
266
|
+
!Number.isInteger(compoundRaw.recurrenceThreshold) ||
|
|
267
|
+
compoundRaw.recurrenceThreshold < 1)) {
|
|
268
|
+
throw configValidationError(fullPath, `"compound.recurrenceThreshold" must be a positive integer`);
|
|
269
|
+
}
|
|
270
|
+
if (typeof compoundRaw.recurrenceThreshold === "number") {
|
|
271
|
+
compoundRecurrenceThreshold = compoundRaw.recurrenceThreshold;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
216
274
|
const gitHookGuardsRaw = parsed.gitHookGuards;
|
|
217
275
|
if (Object.prototype.hasOwnProperty.call(parsed, "gitHookGuards") &&
|
|
218
276
|
typeof gitHookGuardsRaw !== "boolean") {
|
|
@@ -327,6 +385,13 @@ export async function readConfig(projectRoot) {
|
|
|
327
385
|
promptGuardMode,
|
|
328
386
|
tddEnforcement,
|
|
329
387
|
tddTestGlobs,
|
|
388
|
+
tdd: {
|
|
389
|
+
testPathPatterns: resolvedTddTestPathPatterns,
|
|
390
|
+
productionPathPatterns: resolvedTddProductionPathPatterns
|
|
391
|
+
},
|
|
392
|
+
compound: {
|
|
393
|
+
recurrenceThreshold: compoundRecurrenceThreshold
|
|
394
|
+
},
|
|
330
395
|
gitHookGuards,
|
|
331
396
|
defaultTrack,
|
|
332
397
|
languageRulePacks,
|
|
@@ -349,6 +414,8 @@ function buildSerializableConfig(config, options = {}) {
|
|
|
349
414
|
"promptGuardMode",
|
|
350
415
|
"tddEnforcement",
|
|
351
416
|
"tddTestGlobs",
|
|
417
|
+
"tdd",
|
|
418
|
+
"compound",
|
|
352
419
|
"gitHookGuards",
|
|
353
420
|
"defaultTrack",
|
|
354
421
|
"languageRulePacks",
|
|
@@ -402,6 +469,8 @@ export async function detectAdvancedKeys(projectRoot) {
|
|
|
402
469
|
"promptGuardMode",
|
|
403
470
|
"tddEnforcement",
|
|
404
471
|
"tddTestGlobs",
|
|
472
|
+
"tdd",
|
|
473
|
+
"compound",
|
|
405
474
|
"defaultTrack",
|
|
406
475
|
"languageRulePacks",
|
|
407
476
|
"trackHeuristics",
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export interface CompoundCommandOptions {
|
|
2
|
+
recurrenceThreshold?: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function compoundCommandContract(options?: CompoundCommandOptions): string;
|
|
5
|
+
export declare function compoundCommandSkillMarkdown(options?: CompoundCommandOptions): string;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
2
|
const COMPOUND_SKILL_FOLDER = "flow-compound";
|
|
3
3
|
const COMPOUND_SKILL_NAME = "flow-compound";
|
|
4
|
-
|
|
4
|
+
const DEFAULT_RECURRENCE_THRESHOLD = 3;
|
|
5
|
+
const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = 5;
|
|
6
|
+
const SMALL_PROJECT_RECURRENCE_THRESHOLD = 2;
|
|
7
|
+
function resolveRecurrenceThreshold(options) {
|
|
8
|
+
const threshold = options.recurrenceThreshold;
|
|
9
|
+
if (typeof threshold === "number" && Number.isInteger(threshold) && threshold >= 1) {
|
|
10
|
+
return threshold;
|
|
11
|
+
}
|
|
12
|
+
return DEFAULT_RECURRENCE_THRESHOLD;
|
|
13
|
+
}
|
|
14
|
+
export function compoundCommandContract(options = {}) {
|
|
15
|
+
const recurrenceThreshold = resolveRecurrenceThreshold(options);
|
|
5
16
|
return `# /cc-ops compound
|
|
6
17
|
|
|
7
18
|
## Purpose
|
|
@@ -29,39 +40,48 @@ the user can approve individual lifts, accept-all, or skip.
|
|
|
29
40
|
|
|
30
41
|
1. Read \`${RUNTIME_ROOT}/knowledge.jsonl\` (strict JSONL, one entry per line).
|
|
31
42
|
2. Cluster entries by \`trigger\` + \`action\` similarity.
|
|
32
|
-
3.
|
|
33
|
-
|
|
43
|
+
3. Resolve recurrence policy:
|
|
44
|
+
- base threshold = \`${recurrenceThreshold}\` (from \`config.compound.recurrenceThreshold\`),
|
|
45
|
+
- count archived runs under \`${RUNTIME_ROOT}/runs/\`,
|
|
46
|
+
- if archived run count is < ${SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD}, use
|
|
47
|
+
effective threshold = \`min(base threshold, ${SMALL_PROJECT_RECURRENCE_THRESHOLD})\` for this pass.
|
|
48
|
+
4. Filter candidates that satisfy at least one trigger:
|
|
49
|
+
- recurrence count >= effective threshold, or
|
|
50
|
+
- any knowledge entry in the cluster has \`severity: "critical"\`
|
|
51
|
+
(critical override, recurrence can be 1).
|
|
52
|
+
5. If **no candidates** exist:
|
|
34
53
|
- set \`closeout.compoundCompletedAt = <ISO>\`,
|
|
35
54
|
- set \`closeout.compoundPromoted = 0\`,
|
|
36
55
|
- set \`closeout.shipSubstate = "ready_to_archive"\`,
|
|
37
56
|
- emit \`compound: no candidates | next: /cc-next\` and stop.
|
|
38
|
-
|
|
57
|
+
6. **Drift check** each surviving candidate before presenting it (see
|
|
39
58
|
"Drift check" section in the skill): confirm the lift target file is
|
|
40
59
|
current, spot-check the repo for contradictions, demote stale clusters
|
|
41
60
|
into a new superseding entry instead of a lift.
|
|
42
|
-
|
|
61
|
+
7. Otherwise, present **one** structured ask via the harness's native ask
|
|
43
62
|
tool (\`AskUserQuestion\` / \`AskQuestion\` / \`question\` /
|
|
44
63
|
\`request_user_input\`; plain-text lettered list as fallback) summarising
|
|
45
64
|
all candidates at once:
|
|
46
65
|
- \`apply-all\` (default) — apply every listed lift,
|
|
47
66
|
- \`apply-selected\` — prompt per-candidate,
|
|
48
67
|
- \`skip\` — record a skip reason and advance without changes.
|
|
49
|
-
|
|
68
|
+
8. Apply approved lifts to the target file(s). Each lift also appends a
|
|
50
69
|
\`type: "compound"\` entry back to \`${RUNTIME_ROOT}/knowledge.jsonl\`
|
|
51
70
|
summarising what was lifted.
|
|
52
|
-
|
|
71
|
+
9. Update flow-state:
|
|
53
72
|
- \`closeout.compoundCompletedAt = <ISO>\`,
|
|
54
73
|
- \`closeout.compoundPromoted = <count>\`,
|
|
55
74
|
- \`closeout.compoundSkipped = true\` if user picked skip,
|
|
56
75
|
- \`closeout.shipSubstate = "ready_to_archive"\`.
|
|
57
|
-
|
|
76
|
+
10. Emit one-line summary: \`compound: promoted=<N> skipped=<bool> | next: /cc-next\`.
|
|
58
77
|
|
|
59
78
|
## Primary skill
|
|
60
79
|
|
|
61
80
|
**${RUNTIME_ROOT}/skills/${COMPOUND_SKILL_FOLDER}/SKILL.md**
|
|
62
81
|
`;
|
|
63
82
|
}
|
|
64
|
-
export function compoundCommandSkillMarkdown() {
|
|
83
|
+
export function compoundCommandSkillMarkdown(options = {}) {
|
|
84
|
+
const recurrenceThreshold = resolveRecurrenceThreshold(options);
|
|
65
85
|
return `---
|
|
66
86
|
name: ${COMPOUND_SKILL_NAME}
|
|
67
87
|
description: "Lift repeated learnings into durable rules/protocols/skills. Auto-triggered after retro accept."
|
|
@@ -83,13 +103,20 @@ empty pass is allowed and must advance \`closeout.shipSubstate\` to
|
|
|
83
103
|
|
|
84
104
|
1. Parse \`.cclaw/knowledge.jsonl\` and group repeated lessons by
|
|
85
105
|
trigger+action similarity.
|
|
86
|
-
2.
|
|
87
|
-
|
|
106
|
+
2. Resolve recurrence policy:
|
|
107
|
+
- base threshold = \`${recurrenceThreshold}\` from \`config.compound.recurrenceThreshold\`,
|
|
108
|
+
- count archived runs under \`.cclaw/runs/\`,
|
|
109
|
+
- if archived run count is < ${SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD}, use
|
|
110
|
+
effective threshold = \`min(base threshold, ${SMALL_PROJECT_RECURRENCE_THRESHOLD})\` for this pass.
|
|
111
|
+
3. Keep only candidates that meet at least one trigger:
|
|
112
|
+
- recurrence >= effective threshold and actionable lift path, or
|
|
113
|
+
- a cluster entry with \`severity: critical\` (critical override, recurrence can be 1).
|
|
114
|
+
4. If none qualify, record an empty pass:
|
|
88
115
|
- \`closeout.compoundCompletedAt = <ISO>\`,
|
|
89
116
|
- \`closeout.compoundPromoted = 0\`,
|
|
90
117
|
- \`closeout.shipSubstate = "ready_to_archive"\`,
|
|
91
118
|
- announce \`compound: no candidates\` and stop.
|
|
92
|
-
|
|
119
|
+
5. **Drift check — run before presenting any candidate.** Knowledge lines
|
|
93
120
|
are append-only, so textual repetition alone does not prove the rule is
|
|
94
121
|
still true. For every cluster that survives the recurrence filter:
|
|
95
122
|
|
|
@@ -111,13 +138,17 @@ empty pass is allowed and must advance \`closeout.shipSubstate\` to
|
|
|
111
138
|
- **Cite line IDs.** Every surviving candidate must list the concrete
|
|
112
139
|
knowledge line indices (1-based) that back it, not just a
|
|
113
140
|
summary string. This is what makes the lift auditable.
|
|
141
|
+
- **Include qualification reason.** Mark each candidate as
|
|
142
|
+
\`recurrence\` or \`critical_override\` so reviewers can see why it passed
|
|
143
|
+
the filter.
|
|
114
144
|
- Optionally invoke the \`knowledge-curation\` utility skill's
|
|
115
145
|
stale/duplicate/supersede heuristics if you want a second pass.
|
|
116
146
|
|
|
117
|
-
|
|
147
|
+
6. Otherwise, render each candidate as:
|
|
118
148
|
|
|
119
149
|
\`\`\`
|
|
120
150
|
Candidate: <short title>
|
|
151
|
+
Qualification: <recurrence|critical_override>
|
|
121
152
|
Evidence: <knowledge line-ids>
|
|
122
153
|
Freshness: <newest last_seen_ts among evidence lines>
|
|
123
154
|
Lift target: <rule/protocol/skill file>
|
|
@@ -125,17 +156,17 @@ Change type: <add/update/remove>
|
|
|
125
156
|
Expected benefit: <what regressions this prevents>
|
|
126
157
|
\`\`\`
|
|
127
158
|
|
|
128
|
-
|
|
159
|
+
7. Present **one** structured question with three options:
|
|
129
160
|
- \`apply-all\` (default) — apply every candidate,
|
|
130
161
|
- \`apply-selected\` — prompt per-candidate approval next,
|
|
131
162
|
- \`skip\` — record a skip reason and advance.
|
|
132
163
|
|
|
133
|
-
|
|
164
|
+
8. For approved candidates:
|
|
134
165
|
- edit the target file(s) with the lift,
|
|
135
166
|
- append a \`type: "compound"\` entry to \`.cclaw/knowledge.jsonl\`
|
|
136
167
|
describing what was promoted, including the source line IDs.
|
|
137
168
|
|
|
138
|
-
|
|
169
|
+
9. Update flow-state \`closeout\`:
|
|
139
170
|
- \`compoundCompletedAt\`,
|
|
140
171
|
- \`compoundPromoted\` (count),
|
|
141
172
|
- \`compoundSkipped\` (boolean) + \`compoundSkipReason\` when applicable,
|
|
@@ -34,7 +34,7 @@ ${schema.hardGate}
|
|
|
34
34
|
2. Resolve active artifact root: \`.cclaw/artifacts/\`.
|
|
35
35
|
3. Load required upstream artifacts for this stage:
|
|
36
36
|
${hydrationLines}
|
|
37
|
-
4. Stream \`.cclaw/knowledge.jsonl\` and apply relevant JSON-line entries (strict schema: type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project).
|
|
37
|
+
4. Stream \`.cclaw/knowledge.jsonl\` and apply relevant JSON-line entries (strict schema: type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project; optional: source, severity).
|
|
38
38
|
5. Write stage output to ${writeStepPaths}.
|
|
39
39
|
6. Do NOT copy artifacts into \`.cclaw/runs/\`; archival is handled by \`/cc-ops archive\` (agent-facing wrapper over archive runtime).
|
|
40
40
|
|
|
@@ -16,4 +16,5 @@ export declare function stageExamplesReferenceMarkdown(stage: FlowStage): string
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function stageExamples(stage: FlowStage): string;
|
|
18
18
|
export type ExampleDomain = "web" | "cli" | "library" | "data-pipeline";
|
|
19
|
+
export declare const RESEARCH_FLEET_USAGE_EXAMPLE: string;
|
|
19
20
|
export declare function stageDomainExamples(stage: FlowStage): string;
|