cclaw-cli 0.46.15 → 0.48.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 +3 -1
- package/dist/artifact-linter.d.ts +7 -0
- package/dist/artifact-linter.js +169 -8
- package/dist/config.d.ts +6 -6
- package/dist/config.js +22 -0
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +19 -10
- package/dist/content/contracts.d.ts +1 -1
- package/dist/content/contracts.js +1 -1
- package/dist/content/{harnesses-doc.js → harness-doc.js} +32 -1
- package/dist/content/harness-playbooks.js +4 -4
- package/dist/content/ideate-command.js +19 -19
- package/dist/content/skills.js +2 -2
- package/dist/content/stage-schema.js +54 -15
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/review.js +1 -1
- package/dist/content/stages/ship.js +2 -0
- package/dist/content/stages/tdd.js +8 -4
- package/dist/content/templates.js +4 -3
- package/dist/delegation.js +107 -26
- package/dist/doctor.js +77 -9
- package/dist/flow-state.d.ts +8 -0
- package/dist/flow-state.js +11 -8
- package/dist/gate-evidence.js +26 -2
- package/dist/harness-adapters.d.ts +2 -2
- package/dist/harness-adapters.js +2 -2
- package/dist/install.js +28 -6
- package/dist/internal/advance-stage.js +53 -16
- package/dist/internal/detect-public-api-changes.d.ts +5 -0
- package/dist/internal/detect-public-api-changes.js +45 -0
- package/dist/policy.js +3 -2
- package/dist/retro-gate.js +30 -3
- package/dist/run-persistence.js +16 -5
- package/dist/tdd-cycle.js +19 -1
- package/dist/types.d.ts +6 -1
- package/package.json +4 -1
- /package/dist/content/{harnesses-doc.d.ts → harness-doc.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ inside `/cc-ops` subcommands.
|
|
|
174
174
|
|---|---|
|
|
175
175
|
| **`/cc <idea>`** | Classify the task, discover origin docs (`docs/prd/**`, ADRs, root `PRD.md`, …), sniff the stack, recommend a track, then start the first stage of that track. `/cc` without arguments resumes the current flow. |
|
|
176
176
|
| **`/cc-next`** | The one progression primitive. Reads `flow-state.json`, checks gates + mandatory subagent delegations, and either resumes the current stage or advances to the next. `/cc-next` in a new session is how you **resume**. |
|
|
177
|
-
| **`/cc-ideate`** | Repository improvement
|
|
177
|
+
| **`/cc-ideate`** | Repository improvement ideate mode. Scans for TODOs, flaky tests, oversized modules, docs drift, and recurring knowledge-store lessons, **persists the ranked backlog** to `.cclaw/artifacts/ideate-<date>-<slug>.md`, and ends with a concrete handoff: launch `/cc` on the selected candidate in the same session, save-and-close, or discard. Resume check on next run reuses any ideate artifact younger than 30 days. Never mutates `flow-state.json`. |
|
|
178
178
|
| **`/cc-view`** | Read-only flow visibility. `/cc-view status` (default) shows stage progress, mandatory delegations with their fulfillment mode (isolated / generic-dispatch / role-switch), the ship closeout substate (retro → compound → archive), and the active harness parity row. `/cc-view tree` renders the same picture as a tree with a closeout sub-branch under ship and a per-harness playbook summary. `/cc-view diff` shows stage/gate/closeout/delegation deltas since the last run. Never mutates state (except diff's snapshot baseline). |
|
|
179
179
|
|
|
180
180
|
> Power-user surface: `/cc-ops` is an operational router for manual
|
|
@@ -458,6 +458,8 @@ CCLAW_EVAL_MODEL=glm-5.1 # default
|
|
|
458
458
|
|
|
459
459
|
Full details, corpus format, and the eval contract live in
|
|
460
460
|
[`docs/evals.md`](./docs/evals.md).
|
|
461
|
+
Mutation-testing setup lives in `stryker.config.mjs` and
|
|
462
|
+
`.github/workflows/mutation.yml` (manual + weekly run).
|
|
461
463
|
|
|
462
464
|
---
|
|
463
465
|
|
|
@@ -58,9 +58,16 @@ export interface ReviewVerdictConsistencyResult {
|
|
|
58
58
|
openCriticalCount: number;
|
|
59
59
|
shipBlockerCount: number;
|
|
60
60
|
}
|
|
61
|
+
export interface ReviewSecurityNoChangeAttestationResult {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
errors: string[];
|
|
64
|
+
hasSecurityFinding: boolean;
|
|
65
|
+
hasNoChangeAttestation: boolean;
|
|
66
|
+
}
|
|
61
67
|
/**
|
|
62
68
|
* Ensure the narrative verdict in 07-review.md is consistent with the
|
|
63
69
|
* structured review-army reconciliation. A review cannot declare
|
|
64
70
|
* APPROVED while open Critical findings or shipBlockers remain.
|
|
65
71
|
*/
|
|
66
72
|
export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
|
|
73
|
+
export declare function checkReviewSecurityNoChangeAttestation(projectRoot: string): Promise<ReviewSecurityNoChangeAttestationResult>;
|
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
|
}
|
|
@@ -869,6 +896,49 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
869
896
|
details: learnings.details
|
|
870
897
|
});
|
|
871
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
|
+
}
|
|
872
942
|
if (stage === "plan") {
|
|
873
943
|
const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
|
|
874
944
|
headingPresent(sections, "No-Placeholder Scan") ||
|
|
@@ -914,12 +984,13 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
914
984
|
});
|
|
915
985
|
}
|
|
916
986
|
if (stage === "scope") {
|
|
987
|
+
const lockedDecisionsBody = sectionBodyByName(sections, "Locked Decisions (D-XX)") ?? "";
|
|
917
988
|
const strictScopeGuards = parsedFrontmatter.hasFrontmatter ||
|
|
918
989
|
headingPresent(sections, "Locked Decisions (D-XX)");
|
|
919
990
|
const scopeSections = [
|
|
920
991
|
sectionBodyByName(sections, "In Scope / Out of Scope") ?? "",
|
|
921
992
|
sectionBodyByName(sections, "Scope Summary") ?? "",
|
|
922
|
-
|
|
993
|
+
lockedDecisionsBody
|
|
923
994
|
].join("\n");
|
|
924
995
|
const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
|
|
925
996
|
findings.push({
|
|
@@ -931,6 +1002,45 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
931
1002
|
? "No scope-reduction phrases detected in scope boundary sections."
|
|
932
1003
|
: `Detected scope-reduction phrase(s): ${reductionHits.join(", ")}.`
|
|
933
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
|
+
}
|
|
934
1044
|
}
|
|
935
1045
|
const passed = findings.every((f) => !f.required || f.found);
|
|
936
1046
|
return { stage, file: relFile, passed, findings };
|
|
@@ -1198,6 +1308,14 @@ export async function checkReviewVerdictConsistency(projectRoot) {
|
|
|
1198
1308
|
if (finalVerdict === "APPROVED" && (openCriticalCount > 0 || shipBlockerCount > 0)) {
|
|
1199
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.`);
|
|
1200
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
|
+
}
|
|
1201
1319
|
return {
|
|
1202
1320
|
ok: errors.length === 0,
|
|
1203
1321
|
errors,
|
|
@@ -1206,3 +1324,46 @@ export async function checkReviewVerdictConsistency(projectRoot) {
|
|
|
1206
1324
|
shipBlockerCount
|
|
1207
1325
|
};
|
|
1208
1326
|
}
|
|
1327
|
+
export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
|
|
1328
|
+
const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
|
|
1329
|
+
if (!(await exists(reviewMdPath))) {
|
|
1330
|
+
return {
|
|
1331
|
+
ok: true,
|
|
1332
|
+
errors: [],
|
|
1333
|
+
hasSecurityFinding: false,
|
|
1334
|
+
hasNoChangeAttestation: false
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
const errors = [];
|
|
1338
|
+
const raw = await fs.readFile(reviewMdPath, "utf8");
|
|
1339
|
+
const sections = extractH2Sections(raw);
|
|
1340
|
+
const securityBody = sectionBodyByName(sections, "Layer 2 Security")
|
|
1341
|
+
?? sectionBodyByName(sections, "Layer 2b: Security")
|
|
1342
|
+
?? sectionBodyByName(sections, "Layer 2 Findings");
|
|
1343
|
+
if (!securityBody) {
|
|
1344
|
+
errors.push('07-review.md is missing a Layer 2 security section.');
|
|
1345
|
+
return {
|
|
1346
|
+
ok: false,
|
|
1347
|
+
errors,
|
|
1348
|
+
hasSecurityFinding: false,
|
|
1349
|
+
hasNoChangeAttestation: false
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
const securityTableRowPattern = /^\|\s*[^|\n]+\|\s*[^|\n]+\|\s*security\s*\|\s*[^|\n]+\|\s*[^|\n]+\|/imu;
|
|
1353
|
+
const securityBulletPattern = /^[*-]\s+.*\b(?:security|auth|injection|secret|credential|permission)\b/imu;
|
|
1354
|
+
const hasSecurityFinding = securityTableRowPattern.test(securityBody) || securityBulletPattern.test(securityBody);
|
|
1355
|
+
const attestationMatch = /NO_CHANGE_ATTESTATION\s*:\s*(.*)/iu.exec(securityBody);
|
|
1356
|
+
const hasNoChangeAttestation = Boolean(attestationMatch && attestationMatch[1]?.trim().length > 0);
|
|
1357
|
+
if (attestationMatch && attestationMatch[1]?.trim().length === 0) {
|
|
1358
|
+
errors.push("NO_CHANGE_ATTESTATION must include a non-empty rationale.");
|
|
1359
|
+
}
|
|
1360
|
+
if (!hasSecurityFinding && !hasNoChangeAttestation) {
|
|
1361
|
+
errors.push("Layer 2 security evidence missing: include at least one security finding or `NO_CHANGE_ATTESTATION: <reason>`.");
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
ok: errors.length === 0,
|
|
1365
|
+
errors,
|
|
1366
|
+
hasSecurityFinding,
|
|
1367
|
+
hasNoChangeAttestation
|
|
1368
|
+
};
|
|
1369
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowTrack, HarnessId, LanguageRulePack
|
|
1
|
+
import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack } from "./types.js";
|
|
2
2
|
export declare function configPath(projectRoot: string): string;
|
|
3
3
|
/**
|
|
4
4
|
* Default test-path patterns used by workflow-guard.sh to classify TDD writes.
|
|
@@ -20,7 +20,7 @@ export declare const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
|
|
|
20
20
|
* regardless of whether the user wrote `strictness`, the legacy keys, both,
|
|
21
21
|
* or neither.
|
|
22
22
|
*/
|
|
23
|
-
export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack):
|
|
23
|
+
export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack): CclawConfig;
|
|
24
24
|
/**
|
|
25
25
|
* Probe common project-root manifests to infer which language rule packs the
|
|
26
26
|
* user would reasonably want. Pure-functional best-effort: any filesystem
|
|
@@ -31,9 +31,9 @@ export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrac
|
|
|
31
31
|
* never surprise a user who intentionally cleared the list.
|
|
32
32
|
*/
|
|
33
33
|
export declare function detectLanguageRulePacks(projectRoot: string): Promise<LanguageRulePack[]>;
|
|
34
|
-
export declare function readConfig(projectRoot: string): Promise<
|
|
34
|
+
export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
|
|
35
35
|
/**
|
|
36
|
-
* Fields that live on the populated runtime `
|
|
36
|
+
* Fields that live on the populated runtime `CclawConfig` but are considered
|
|
37
37
|
* "advanced" — we keep them in the in-memory object so downstream callers
|
|
38
38
|
* don't have to branch, but we do **not** write them to `config.yaml` unless
|
|
39
39
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
@@ -43,7 +43,7 @@ type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" |
|
|
|
43
43
|
/**
|
|
44
44
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
45
45
|
*
|
|
46
|
-
* - `"full"` (default): write every field on the `
|
|
46
|
+
* - `"full"` (default): write every field on the `CclawConfig` object that
|
|
47
47
|
* isn't `undefined`. Preserves existing shapes and keeps legacy callers
|
|
48
48
|
* working without migration.
|
|
49
49
|
* - `"minimal"`: write only the user-facing knobs (`MINIMAL_CONFIG_KEYS`)
|
|
@@ -60,7 +60,7 @@ export interface WriteConfigOptions {
|
|
|
60
60
|
mode?: "full" | "minimal";
|
|
61
61
|
advancedKeysPresent?: ReadonlySet<AdvancedConfigKey>;
|
|
62
62
|
}
|
|
63
|
-
export declare function writeConfig(projectRoot: string, config:
|
|
63
|
+
export declare function writeConfig(projectRoot: string, config: CclawConfig, options?: WriteConfigOptions): Promise<void>;
|
|
64
64
|
/**
|
|
65
65
|
* Enumerate which advanced keys are currently set in the on-disk config.
|
|
66
66
|
* Used by `cclaw upgrade` to preserve the user's existing shape — if they
|
package/dist/config.js
CHANGED
|
@@ -44,6 +44,22 @@ const MINIMAL_CONFIG_KEYS = [
|
|
|
44
44
|
];
|
|
45
45
|
const DEFAULT_SLICE_REVIEW_THRESHOLD = 5;
|
|
46
46
|
const DEFAULT_SLICE_REVIEW_TRACKS = ["standard"];
|
|
47
|
+
const emittedConfigWarnings = new Set();
|
|
48
|
+
function emitConfigWarningOnce(code, message) {
|
|
49
|
+
const key = `${code}:${message}`;
|
|
50
|
+
if (emittedConfigWarnings.has(key)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
emittedConfigWarnings.add(key);
|
|
54
|
+
process.emitWarning(message, { code });
|
|
55
|
+
}
|
|
56
|
+
function sameStringArray(a, b) {
|
|
57
|
+
if (!a || !b)
|
|
58
|
+
return false;
|
|
59
|
+
if (a.length !== b.length)
|
|
60
|
+
return false;
|
|
61
|
+
return a.every((value, index) => value === b[index]);
|
|
62
|
+
}
|
|
47
63
|
function configFixExample() {
|
|
48
64
|
return `harnesses:
|
|
49
65
|
- claude
|
|
@@ -244,6 +260,12 @@ export async function readConfig(projectRoot) {
|
|
|
244
260
|
explicitTddTestPathPatterns = validateStringArray(tddRaw.testPathPatterns, "tdd.testPathPatterns", fullPath);
|
|
245
261
|
explicitTddProductionPathPatterns = validateStringArray(tddRaw.productionPathPatterns, "tdd.productionPathPatterns", fullPath);
|
|
246
262
|
}
|
|
263
|
+
if (tddTestGlobsRaw !== undefined &&
|
|
264
|
+
explicitTddTestPathPatterns !== undefined &&
|
|
265
|
+
!sameStringArray(tddTestGlobs, explicitTddTestPathPatterns)) {
|
|
266
|
+
emitConfigWarningOnce("CCLAW_CONFIG_DEPRECATED_TDD_TEST_GLOBS", `[cclaw] Both "tddTestGlobs" (deprecated) and "tdd.testPathPatterns" are set in ${fullPath}. ` +
|
|
267
|
+
`Using "tdd.testPathPatterns".`);
|
|
268
|
+
}
|
|
247
269
|
const resolvedTddTestPathPatterns = [
|
|
248
270
|
...(explicitTddTestPathPatterns ?? tddTestGlobs ?? DEFAULT_TDD_TEST_PATH_PATTERNS)
|
|
249
271
|
];
|
package/dist/constants.d.ts
CHANGED
|
@@ -15,7 +15,16 @@ export declare const EVALS_CONFIG_PATH = ".cclaw/evals/config.yaml";
|
|
|
15
15
|
export declare const EVALS_DIRS: readonly [".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
|
|
16
16
|
export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/worktrees", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks", ".cclaw/custom-skills", ".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
|
|
17
17
|
export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", "# cclaw evals: user-owned, track in git", "!.cclaw/evals/", "!.cclaw/evals/config.yaml", "!.cclaw/evals/corpus/", "!.cclaw/evals/corpus/**", "!.cclaw/evals/rubrics/", "!.cclaw/evals/rubrics/**", "!.cclaw/evals/baselines/", "!.cclaw/evals/baselines/**", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".agents/skills/cc/SKILL.md", ".agents/skills/cc-*/SKILL.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Canonical stage -> skill folder mapping.
|
|
20
|
+
*
|
|
21
|
+
* Intentional divergence from stage ids:
|
|
22
|
+
* - stage ids stay short and flow-oriented (`spec`, `tdd`, `ship`)
|
|
23
|
+
* - skill folders stay descriptive and user-facing for `.cclaw/skills/*`.
|
|
24
|
+
*
|
|
25
|
+
* Keep this map as the single source of truth for generated skill paths.
|
|
26
|
+
*/
|
|
27
|
+
export declare const STAGE_TO_SKILL_FOLDER: Record<FlowStage, string>;
|
|
19
28
|
export declare const UTILITY_COMMANDS: readonly ["learn", "next", "ideate", "view", "status", "tree", "diff", "ops", "feature", "tdd-log", "retro", "compound", "archive", "rewind"];
|
|
20
29
|
export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
|
|
21
30
|
export type UtilityCommand = (typeof UTILITY_COMMANDS)[number];
|
package/dist/constants.js
CHANGED
|
@@ -103,16 +103,25 @@ export const REQUIRED_GITIGNORE_PATTERNS = [
|
|
|
103
103
|
".opencode/plugins/cclaw-plugin.mjs",
|
|
104
104
|
".cursor/rules/cclaw-workflow.mdc"
|
|
105
105
|
];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Canonical stage -> skill folder mapping.
|
|
108
|
+
*
|
|
109
|
+
* Intentional divergence from stage ids:
|
|
110
|
+
* - stage ids stay short and flow-oriented (`spec`, `tdd`, `ship`)
|
|
111
|
+
* - skill folders stay descriptive and user-facing for `.cclaw/skills/*`.
|
|
112
|
+
*
|
|
113
|
+
* Keep this map as the single source of truth for generated skill paths.
|
|
114
|
+
*/
|
|
115
|
+
export const STAGE_TO_SKILL_FOLDER = {
|
|
116
|
+
brainstorm: "brainstorming",
|
|
117
|
+
scope: "scope-shaping",
|
|
118
|
+
design: "engineering-design-lock",
|
|
119
|
+
spec: "specification-authoring",
|
|
120
|
+
plan: "planning-and-task-breakdown",
|
|
121
|
+
tdd: "test-driven-development",
|
|
122
|
+
review: "two-layer-review",
|
|
123
|
+
ship: "shipping-and-handoff"
|
|
124
|
+
};
|
|
116
125
|
export const UTILITY_COMMANDS = [
|
|
117
126
|
"learn",
|
|
118
127
|
"next",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FlowStage } from "../types.js";
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function stageCommandContract(stage: FlowStage): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stageSchema } from "./stage-schema.js";
|
|
2
2
|
import { stageSkillFolder } from "./skills.js";
|
|
3
|
-
export function
|
|
3
|
+
export function stageCommandContract(stage) {
|
|
4
4
|
const schema = stageSchema(stage);
|
|
5
5
|
const skillPath = `.cclaw/skills/${stageSkillFolder(stage)}/SKILL.md`;
|
|
6
6
|
const reads = schema.crossStageTrace.readsFrom;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HARNESS_ADAPTERS, harnessTier } from "../harness-adapters.js";
|
|
2
|
+
import { STAGE_TO_SKILL_FOLDER } from "../constants.js";
|
|
2
3
|
import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./hook-events.js";
|
|
3
4
|
import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./harness-playbooks.js";
|
|
4
5
|
import { HARNESS_TOOL_REFS_DIR } from "./harness-tool-refs.js";
|
|
@@ -23,6 +24,15 @@ function tierDescription(tier) {
|
|
|
23
24
|
}
|
|
24
25
|
export function harnessIntegrationDocMarkdown() {
|
|
25
26
|
const harnesses = Object.keys(HARNESS_ADAPTERS);
|
|
27
|
+
const stageSkillRows = Object.entries(STAGE_TO_SKILL_FOLDER)
|
|
28
|
+
.map(([stage, skillFolder]) => `| \`${stage}\` | \`${skillFolder}\` |`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
const hookCasingRows = [
|
|
31
|
+
"| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |",
|
|
32
|
+
"| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |",
|
|
33
|
+
"| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |",
|
|
34
|
+
"| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |"
|
|
35
|
+
].join("\n");
|
|
26
36
|
const capabilityRows = harnesses
|
|
27
37
|
.map((harness) => {
|
|
28
38
|
const adapter = HARNESS_ADAPTERS[harness];
|
|
@@ -75,6 +85,17 @@ Design-stage research fleet uses the same parity model:
|
|
|
75
85
|
|---|---|---|---|---|
|
|
76
86
|
${hookRows}
|
|
77
87
|
|
|
88
|
+
## Hook event casing
|
|
89
|
+
|
|
90
|
+
Hook keys are intentionally harness-native and must not be normalized:
|
|
91
|
+
|
|
92
|
+
| Harness | ID | Event key casing |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
${hookCasingRows}
|
|
95
|
+
|
|
96
|
+
Use the exact event names from each harness schema. Treating all hooks as one
|
|
97
|
+
shared casing silently breaks generated wiring.
|
|
98
|
+
|
|
78
99
|
## Interpretation
|
|
79
100
|
|
|
80
101
|
- \`tier1\`: full native delegation + structured asks + full hook surface.
|
|
@@ -91,7 +112,7 @@ All harnesses receive the same utility commands:
|
|
|
91
112
|
|
|
92
113
|
- \`/cc\` - flow entry and resume
|
|
93
114
|
- \`/cc-next\` - stage progression
|
|
94
|
-
- \`/cc-ideate\` -
|
|
115
|
+
- \`/cc-ideate\` - ideate mode for ranked repo-improvement backlog
|
|
95
116
|
- \`/cc-view\` - read-only router for status/tree/diff
|
|
96
117
|
- \`/cc-ops\` - operations router for feature/tdd-log/retro/compound/archive/rewind
|
|
97
118
|
|
|
@@ -112,6 +133,16 @@ Operations subcommands:
|
|
|
112
133
|
Stage order remains canonical:
|
|
113
134
|
\`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship\`
|
|
114
135
|
|
|
136
|
+
## Stage -> skill folder mapping
|
|
137
|
+
|
|
138
|
+
| Stage | Skill folder |
|
|
139
|
+
|---|---|
|
|
140
|
+
${stageSkillRows}
|
|
141
|
+
|
|
142
|
+
This map is generated from \`src/constants.ts::STAGE_TO_SKILL_FOLDER\` so
|
|
143
|
+
skill-path naming stays explicit and stable even when stage ids differ from
|
|
144
|
+
folder names.
|
|
145
|
+
|
|
115
146
|
## Install surfaces
|
|
116
147
|
|
|
117
148
|
Always generated:
|
|
@@ -95,9 +95,9 @@ generic dispatcher with a strict role prompt.
|
|
|
95
95
|
## Dispatch pattern
|
|
96
96
|
|
|
97
97
|
1. Pick the mapped \`subagent_type\` from the table above.
|
|
98
|
-
2. Build the \`prompt\` from the cclaw agent
|
|
98
|
+
2. Build the \`prompt\` from the cclaw agent role brief in
|
|
99
99
|
\`.cclaw/agents/<agent>.md\`, prefaced with a single line naming the
|
|
100
|
-
cclaw role (\`You are the cclaw <agent>. Follow the
|
|
100
|
+
cclaw role (\`You are the cclaw <agent>. Follow the role brief below.\`).
|
|
101
101
|
3. Set \`readonly: true\` when the table says yes — Cursor enforces it.
|
|
102
102
|
4. Before dispatch, append a delegation row:
|
|
103
103
|
|
|
@@ -142,7 +142,7 @@ description: "OpenCode has plugin-based dispatch hooks and a native structured-a
|
|
|
142
142
|
**Fallback: role-switch.** OpenCode exposes tool/session event hooks via a
|
|
143
143
|
plugin but does not provide an isolated subagent worker. cclaw closes the
|
|
144
144
|
delegation gate by role-switching inside the same session: the agent
|
|
145
|
-
announces the role, performs the work against the
|
|
145
|
+
announces the role, performs the work against the role brief, and records
|
|
146
146
|
evidence.
|
|
147
147
|
|
|
148
148
|
**Structured ask: native \`question\` tool.** OpenCode ships a first-class
|
|
@@ -168,7 +168,7 @@ artifact decision log. Full mapping:
|
|
|
168
168
|
> Acting as cclaw **<agent>** per \`.cclaw/agents/<agent>.md\`. No other
|
|
169
169
|
> role may be assumed until the delegation row is closed.
|
|
170
170
|
|
|
171
|
-
2. Execute the role's
|
|
171
|
+
2. Execute the role's brief. Do NOT interleave other roles' work.
|
|
172
172
|
3. Write the result into the stage artifact (e.g. TDD work lands in
|
|
173
173
|
\`.cclaw/artifacts/06-tdd.md\`).
|
|
174
174
|
4. Append a delegation row:
|
|
@@ -2,13 +2,13 @@ import { RUNTIME_ROOT } from "../constants.js";
|
|
|
2
2
|
const IDEATE_SKILL_FOLDER = "flow-ideate";
|
|
3
3
|
const IDEATE_SKILL_NAME = "flow-ideate";
|
|
4
4
|
/**
|
|
5
|
-
* Directory + filename convention for
|
|
5
|
+
* Directory + filename convention for ideate artifacts. These are separate
|
|
6
6
|
* from stage artifacts (00-..08-*.md) because `/cc-ideate` runs outside the
|
|
7
7
|
* critical-path flow state machine and must not collide with stage numbering.
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
9
|
+
const IDEATE_ARTIFACT_GLOB = ".cclaw/artifacts/ideate-*.md";
|
|
10
|
+
const IDEATE_ARTIFACT_PATTERN = ".cclaw/artifacts/ideate-<YYYY-MM-DD-slug>.md";
|
|
11
|
+
const IDEATE_RESUME_WINDOW_DAYS = 30;
|
|
12
12
|
/**
|
|
13
13
|
* Structured-ask tool list reused across cclaw skills. Kept inline here (small
|
|
14
14
|
* enough) to avoid cross-module coupling; larger stage skills cite the shared
|
|
@@ -23,25 +23,25 @@ export function ideateCommandContract() {
|
|
|
23
23
|
|
|
24
24
|
## Purpose
|
|
25
25
|
|
|
26
|
-
Repository-improvement
|
|
26
|
+
Repository-improvement ideate mode. Generate a ranked backlog of
|
|
27
27
|
high-value improvements, persist it as an artifact on disk, and end with
|
|
28
28
|
an explicit handoff — either launch \`/cc\` on a chosen candidate in the
|
|
29
29
|
same session, or save/discard the backlog.
|
|
30
30
|
|
|
31
31
|
## HARD-GATE
|
|
32
32
|
|
|
33
|
-
-
|
|
33
|
+
- Ideate mode only. Never mutate \`.cclaw/state/flow-state.json\`.
|
|
34
34
|
- Every recommendation cites evidence from the current repository
|
|
35
35
|
(file path, command output, or knowledge-store entry id).
|
|
36
36
|
- Always write a persisted artifact to
|
|
37
|
-
\`${
|
|
37
|
+
\`${IDEATE_ARTIFACT_PATTERN}\`. Chat-only output is not acceptable —
|
|
38
38
|
the next session must be able to resume.
|
|
39
39
|
- Always end with a structured handoff prompt, not an open question.
|
|
40
40
|
|
|
41
41
|
## Algorithm
|
|
42
42
|
|
|
43
|
-
1. **Resume check.** Glob \`${
|
|
44
|
-
has been modified within the last ${
|
|
43
|
+
1. **Resume check.** Glob \`${IDEATE_ARTIFACT_GLOB}\`. If any artifact
|
|
44
|
+
has been modified within the last ${IDEATE_RESUME_WINDOW_DAYS} days,
|
|
45
45
|
offer the user: continue that backlog, start fresh, or cancel.
|
|
46
46
|
2. **Scan repo signals:**
|
|
47
47
|
- open TODO/FIXME/XXX/HACK notes,
|
|
@@ -54,7 +54,7 @@ same session, or save/discard the backlog.
|
|
|
54
54
|
per candidate.
|
|
55
55
|
4. **Rank by impact/effort**, recommend the top item.
|
|
56
56
|
5. **Write the artifact** at
|
|
57
|
-
\`${
|
|
57
|
+
\`${IDEATE_ARTIFACT_PATTERN}\` using the schema in the skill.
|
|
58
58
|
6. **Present the handoff prompt** with four concrete options — not A/B/C
|
|
59
59
|
letters. Default = "Start /cc on the top recommendation".
|
|
60
60
|
|
|
@@ -66,7 +66,7 @@ same session, or save/discard the backlog.
|
|
|
66
66
|
export function ideateCommandSkillMarkdown() {
|
|
67
67
|
return `---
|
|
68
68
|
name: ${IDEATE_SKILL_NAME}
|
|
69
|
-
description: "Repository
|
|
69
|
+
description: "Repository ideate mode: detect and rank high-leverage improvements, persist a backlog artifact, and hand off to /cc or save/discard."
|
|
70
70
|
---
|
|
71
71
|
|
|
72
72
|
# /cc-ideate
|
|
@@ -75,12 +75,12 @@ description: "Repository ideation mode: detect and rank high-leverage improvemen
|
|
|
75
75
|
|
|
76
76
|
"Using flow-ideate to identify highest-leverage improvements in this
|
|
77
77
|
repository. Will persist a ranked backlog to
|
|
78
|
-
\`${
|
|
78
|
+
\`${IDEATE_ARTIFACT_PATTERN}\` and end with an explicit handoff."
|
|
79
79
|
|
|
80
80
|
## HARD-GATE
|
|
81
81
|
|
|
82
82
|
- Do not start coding in ideate mode.
|
|
83
|
-
- Do not mutate \`.cclaw/state/flow-state.json\` —
|
|
83
|
+
- Do not mutate \`.cclaw/state/flow-state.json\` — ideate mode sits outside
|
|
84
84
|
the critical-path flow.
|
|
85
85
|
- Always produce the artifact file on disk before presenting the handoff.
|
|
86
86
|
- Always end with a structured handoff that names the concrete follow-up
|
|
@@ -91,12 +91,12 @@ repository. Will persist a ranked backlog to
|
|
|
91
91
|
### Phase 0 — Resume check
|
|
92
92
|
|
|
93
93
|
1. Use the harness's file-glob tool (\`Glob\` pattern
|
|
94
|
-
\`${
|
|
95
|
-
2. Filter to files modified within the last ${
|
|
94
|
+
\`${IDEATE_ARTIFACT_GLOB}\` or equivalent \`ls\`/\`find\`).
|
|
95
|
+
2. Filter to files modified within the last ${IDEATE_RESUME_WINDOW_DAYS} days.
|
|
96
96
|
3. If one or more match, present **one** structured ask using the
|
|
97
97
|
harness's native tool (${STRUCTURED_ASK_TOOLS}) with options:
|
|
98
98
|
- **Continue the existing backlog** — read the most-recent
|
|
99
|
-
|
|
99
|
+
ideate-*.md and work from its candidate list; skip re-scanning.
|
|
100
100
|
- **Start a fresh scan** — proceed to Phase 1; the old artifact stays
|
|
101
101
|
on disk for history.
|
|
102
102
|
- **Cancel** — stop; do not scan or write anything.
|
|
@@ -137,10 +137,10 @@ Aim for 5–10 candidates. Do not invent candidates without evidence.
|
|
|
137
137
|
1. Sort by impact/effort ratio; break ties with confidence.
|
|
138
138
|
2. Compute the artifact filename:
|
|
139
139
|
- \`slug\` = first 3–5 words of the top recommendation, lowercase,
|
|
140
|
-
non-alphanumeric collapsed to \`-\`, trimmed. When
|
|
140
|
+
non-alphanumeric collapsed to \`-\`, trimmed. When ideate mode is
|
|
141
141
|
focus-hinted (user passed an argument), use the focus hint instead.
|
|
142
142
|
- \`date\` = today in \`YYYY-MM-DD\` (local time).
|
|
143
|
-
- Path = \`.cclaw/artifacts/
|
|
143
|
+
- Path = \`.cclaw/artifacts/ideate-<date>-<slug>.md\`.
|
|
144
144
|
3. Use the harness's write-file tool (\`Write\`, \`apply_patch\`, or shell
|
|
145
145
|
\`cat <<EOF > path\`) to create the artifact with this schema:
|
|
146
146
|
|
|
@@ -195,7 +195,7 @@ lettered list with the same four labels. Do not invent extra options.
|
|
|
195
195
|
- **Start /cc on I-1** or **different candidate:** announce
|
|
196
196
|
"Handing off to /cc <phrase>" and load the \`using-cclaw\` router
|
|
197
197
|
skill. From there, the normal \`/cc\` classification and stage flow
|
|
198
|
-
takes over. Do not produce a second artifact; the
|
|
198
|
+
takes over. Do not produce a second artifact; the ideate file is
|
|
199
199
|
preserved as the origin document for this run.
|
|
200
200
|
- **Save and close:** reply with the artifact path and stop.
|
|
201
201
|
- **Discard:** delete the artifact file, confirm deletion, stop.
|
package/dist/content/skills.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUNTIME_ROOT } from "../constants.js";
|
|
1
|
+
import { RUNTIME_ROOT, STAGE_TO_SKILL_FOLDER } from "../constants.js";
|
|
2
2
|
import { STAGE_EXAMPLES_REFERENCE_DIR, stageDomainExamples, stageExamples, stageGoodBadExamples } from "./examples.js";
|
|
3
3
|
import { STAGE_COMMON_GUIDANCE_REL_PATH } from "./stage-common-guidance.js";
|
|
4
4
|
import { stageAutoSubagentDispatch, stageSchema } from "./stage-schema.js";
|
|
@@ -295,7 +295,7 @@ After T-3 REFACTOR, before declaring Batch 1 done:
|
|
|
295
295
|
- The same RED failure reappears after a GREEN change → **escalate** per the 3-attempts rule; do not keep patching.
|
|
296
296
|
`;
|
|
297
297
|
export function stageSkillFolder(stage) {
|
|
298
|
-
return
|
|
298
|
+
return STAGE_TO_SKILL_FOLDER[stage];
|
|
299
299
|
}
|
|
300
300
|
export function stageSkillMarkdown(stage, track = "standard") {
|
|
301
301
|
const schema = stageSchema(stage, track);
|