cclaw-cli 0.51.18 → 0.51.21
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 +89 -6
- package/dist/content/examples.js +1 -0
- package/dist/content/hook-events.js +1 -5
- package/dist/content/node-hooks.js +2 -13
- package/dist/content/observe.js +2 -4
- package/dist/content/opencode-plugin.js +5 -6
- package/dist/content/stage-schema.d.ts +0 -1
- package/dist/content/stage-schema.js +1 -5
- package/dist/content/start-command.js +1 -1
- package/dist/content/subagent-context-skills.d.ts +4 -0
- package/dist/content/subagent-context-skills.js +122 -0
- package/dist/content/templates.js +1 -1
- package/dist/delegation.d.ts +0 -1
- package/dist/delegation.js +1 -3
- package/dist/doctor.js +1 -3
- package/dist/install.js +28 -3
- package/dist/run-persistence.js +6 -3
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -485,15 +485,39 @@ const SCOPE_MODE_SHORT_TOKEN_REGEX = /\b(?:hold(?:[\s_-]?scope)?|selective(?:[\s
|
|
|
485
485
|
// not the wording of the rationale.
|
|
486
486
|
const NEXT_STAGE_HANDOFF_REGEX = /(?:`(?:design|spec)`|\bdesign\b|\bspec\b|next[-\s_]stage|next stage|handoff|hand[-\s]off)/iu;
|
|
487
487
|
function hasCanonicalScopeMode(body) {
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
// Strict: a Mode: / Selected mode: line that picks exactly ONE canonical mode
|
|
489
|
+
// is the strongest signal. The template scaffolding contains all four mode
|
|
490
|
+
// tokens inside an instructional `(one of ...)` placeholder; we ignore that
|
|
491
|
+
// line so authors who never replace the scaffolding still fail validation.
|
|
490
492
|
for (const match of body.matchAll(new RegExp(SCOPE_MODE_LINE_REGEX, "giu"))) {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
+
const raw = (match[1] ?? "").trim();
|
|
494
|
+
const sanitized = raw.replace(/\(.*?\)/gu, "").trim();
|
|
495
|
+
if (sanitized.length === 0)
|
|
496
|
+
continue;
|
|
497
|
+
if (countCanonicalModeMentions(sanitized) === 1)
|
|
498
|
+
return true;
|
|
499
|
+
if (countCanonicalModeMentions(sanitized) === 0 && SCOPE_MODE_SHORT_TOKEN_REGEX.test(sanitized))
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
// Fallback: any line outside an instructional `(one of ...)` placeholder
|
|
503
|
+
// names exactly one mode. Block lines that list multiple modes (the
|
|
504
|
+
// unfilled template) or are wrapped in an instructional parenthetical.
|
|
505
|
+
for (const rawLine of body.split(/\r?\n/u)) {
|
|
506
|
+
const line = rawLine.trim();
|
|
507
|
+
if (line.length === 0)
|
|
508
|
+
continue;
|
|
509
|
+
if (/\(\s*one\s+of\b/iu.test(line))
|
|
510
|
+
continue;
|
|
511
|
+
const sanitized = line.replace(/\(.*?\)/gu, "");
|
|
512
|
+
if (countCanonicalModeMentions(sanitized) === 1)
|
|
493
513
|
return true;
|
|
494
514
|
}
|
|
495
515
|
return false;
|
|
496
516
|
}
|
|
517
|
+
function countCanonicalModeMentions(text) {
|
|
518
|
+
const matches = text.match(new RegExp(SCOPE_MODE_FULL_REGEX, "giu"));
|
|
519
|
+
return matches ? matches.length : 0;
|
|
520
|
+
}
|
|
497
521
|
function validatePremiseChallenge(sectionBody) {
|
|
498
522
|
// gstack-style premise challenge requires a real Q/A structure (table or
|
|
499
523
|
// list), not free-form prose. The validation is *structural* only — we do
|
|
@@ -1051,10 +1075,12 @@ function validateTddGreenEvidence(sectionBody) {
|
|
|
1051
1075
|
};
|
|
1052
1076
|
}
|
|
1053
1077
|
function validateVerificationLadder(sectionBody) {
|
|
1054
|
-
|
|
1078
|
+
const hasTextLine = /highest tier reached/iu.test(sectionBody);
|
|
1079
|
+
const hasCanonicalTable = hasVerificationLadderTableRow(sectionBody);
|
|
1080
|
+
if (!hasTextLine && !hasCanonicalTable) {
|
|
1055
1081
|
return {
|
|
1056
1082
|
ok: false,
|
|
1057
|
-
details: "Verification Ladder must include a 'Highest tier reached' line."
|
|
1083
|
+
details: "Verification Ladder must include either a 'Highest tier reached' line or a canonical table row (Slice | Tier reached | Evidence) with non-empty tier and evidence."
|
|
1058
1084
|
};
|
|
1059
1085
|
}
|
|
1060
1086
|
if (!/\b(static|command|behavioral|human)\b/iu.test(sectionBody)) {
|
|
@@ -1074,6 +1100,49 @@ function validateVerificationLadder(sectionBody) {
|
|
|
1074
1100
|
details: "Verification Ladder includes tier + evidence fields."
|
|
1075
1101
|
};
|
|
1076
1102
|
}
|
|
1103
|
+
function hasVerificationLadderTableRow(sectionBody) {
|
|
1104
|
+
const lines = sectionBody.split(/\r?\n/u);
|
|
1105
|
+
let sawHeader = false;
|
|
1106
|
+
let sawSeparator = false;
|
|
1107
|
+
for (const line of lines) {
|
|
1108
|
+
const trimmed = line.trim();
|
|
1109
|
+
if (!trimmed.startsWith("|")) {
|
|
1110
|
+
sawHeader = false;
|
|
1111
|
+
sawSeparator = false;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const cells = trimmed
|
|
1115
|
+
.replace(/^\|/u, "")
|
|
1116
|
+
.replace(/\|$/u, "")
|
|
1117
|
+
.split("|")
|
|
1118
|
+
.map((cell) => cell.trim());
|
|
1119
|
+
if (!sawHeader) {
|
|
1120
|
+
const lowered = cells.map((cell) => cell.toLowerCase());
|
|
1121
|
+
const hasTierColumn = lowered.some((cell) => /tier(?:\s+reached)?/u.test(cell));
|
|
1122
|
+
const hasEvidenceColumn = lowered.some((cell) => cell.includes("evidence"));
|
|
1123
|
+
if (hasTierColumn && hasEvidenceColumn) {
|
|
1124
|
+
sawHeader = true;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (!sawSeparator) {
|
|
1130
|
+
if (cells.every((cell) => /^[:\-\s]+$/u.test(cell))) {
|
|
1131
|
+
sawSeparator = true;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
sawHeader = false;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (cells.length >= 2 && cells.some((cell) => /\b(static|command|behavioral|human)\b/iu.test(cell))) {
|
|
1138
|
+
const evidenceCellHasContent = cells.some((cell) => cell.length > 0 && !/^\s*$/u.test(cell) && !/^[:\-\s]+$/u.test(cell));
|
|
1139
|
+
if (evidenceCellHasContent) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1077
1146
|
const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
1078
1147
|
const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
1079
1148
|
const LEARNING_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
|
@@ -1786,6 +1855,20 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
1786
1855
|
? "Selected Direction is traceable to prior user reaction."
|
|
1787
1856
|
: "Selected Direction is not traceable to user reaction. Add `## Approach Reaction` before it, or mention the user's reaction/concerns in the rationale."
|
|
1788
1857
|
});
|
|
1858
|
+
// Track-aware handoff: standard track goes to `scope`; medium track
|
|
1859
|
+
// goes directly to `spec`; the quick track skips brainstorm entirely.
|
|
1860
|
+
// We accept either canonical successor token plus a generic
|
|
1861
|
+
// `next-stage` / `handoff` phrase to preserve i18n flexibility.
|
|
1862
|
+
const handoffTrace = /(?:`(?:scope|spec)`|\bscope\b|\bspec\b|next[-\s_]stage|next stage|\bhandoff\b|hand[-\s]off)/iu.test(directionBody);
|
|
1863
|
+
findings.push({
|
|
1864
|
+
section: "Direction Next-Stage Handoff",
|
|
1865
|
+
required: true,
|
|
1866
|
+
rule: "Selected Direction must record the track-aware next-stage handoff (mention `scope` for standard, `spec` for medium, or include a `Next-stage handoff:` line).",
|
|
1867
|
+
found: handoffTrace,
|
|
1868
|
+
details: handoffTrace
|
|
1869
|
+
? "Selected Direction names the next-stage handoff."
|
|
1870
|
+
: "Selected Direction is missing a next-stage handoff token. Mention `scope` (standard) or `spec` (medium), or add a `Next-stage handoff:` line so downstream stages can trace the contract."
|
|
1871
|
+
});
|
|
1789
1872
|
}
|
|
1790
1873
|
}
|
|
1791
1874
|
const shortCircuitBody = brainstormShortCircuitBody;
|
package/dist/content/examples.js
CHANGED
|
@@ -48,6 +48,7 @@ const STAGE_EXAMPLES = {
|
|
|
48
48
|
- **Approach:** A — Reusable validation module
|
|
49
49
|
- **Rationale:** based on user reaction favoring fast delivery and lower complexity, shared TS module gives consistent behavior in CI/local, avoids script duplication, and stays within the no-new-dependency constraint.
|
|
50
50
|
- **Approval:** approved
|
|
51
|
+
- **Next-stage handoff:** \`scope\` — carry the locked stack constraints and the validator module boundary forward.
|
|
51
52
|
|
|
52
53
|
## Design
|
|
53
54
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { semanticEventCoverage } from "./hook-manifest.js";
|
|
2
2
|
export { HOOK_SEMANTIC_EVENTS } from "./hook-manifest.js";
|
|
3
|
-
function isManifestHarness(value) {
|
|
4
|
-
return HOOK_MANIFEST_HARNESSES.includes(value);
|
|
5
|
-
}
|
|
6
3
|
/**
|
|
7
4
|
* OpenCode is covered by the inline plugin (`opencode-plugin.ts`), not
|
|
8
5
|
* by the generated `run-hook.mjs` dispatcher. We keep its semantic
|
|
@@ -28,4 +25,3 @@ export const HOOK_EVENTS_BY_HARNESS = Object.freeze({
|
|
|
28
25
|
codex: semanticEventCoverage("codex"),
|
|
29
26
|
opencode: OPENCODE_SEMANTIC_COVERAGE
|
|
30
27
|
});
|
|
31
|
-
void isManifestHarness;
|
|
@@ -265,15 +265,6 @@ async function writeJsonFile(filePath, value) {
|
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
async function fileExists(filePath) {
|
|
269
|
-
try {
|
|
270
|
-
await fs.stat(filePath);
|
|
271
|
-
return true;
|
|
272
|
-
} catch {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
268
|
async function readTextFile(filePath, fallback = "") {
|
|
278
269
|
try {
|
|
279
270
|
return await fs.readFile(filePath, "utf8");
|
|
@@ -625,7 +616,7 @@ function isCodeLikePath(rawPath) {
|
|
|
625
616
|
}
|
|
626
617
|
|
|
627
618
|
function isMutatingTool(toolLower) {
|
|
628
|
-
return /^(write|edit|multiedit|multi_edit|delete|applypatch|apply_patch)$/u.test(toolLower);
|
|
619
|
+
return /^(write|edit|multiedit|multi_edit|delete|applypatch|apply_patch|notebookedit|notebook_edit)$/u.test(toolLower);
|
|
629
620
|
}
|
|
630
621
|
|
|
631
622
|
function isExecutionOrMutatingTool(toolLower) {
|
|
@@ -871,8 +862,6 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
|
871
862
|
const action = typeof row.action === "string" ? row.action : "action";
|
|
872
863
|
return "- [" + confidence + " • " + stage + " • " + domain + "] " + trigger + " -> " + action;
|
|
873
864
|
});
|
|
874
|
-
const body =
|
|
875
|
-
relevant.length > 0 ? relevant.join("\\n") : "(no matching entries for current stage)";
|
|
876
865
|
return {
|
|
877
866
|
digestLines: relevant,
|
|
878
867
|
learningsCount
|
|
@@ -1164,7 +1153,7 @@ async function handlePromptGuard(runtime) {
|
|
|
1164
1153
|
const payloadLower = toLower(payloadText);
|
|
1165
1154
|
const reasons = [];
|
|
1166
1155
|
|
|
1167
|
-
if (/^(write|edit|multiedit|multi_edit|delete|applypatch|runcommand|shell|terminal|execcommand)$/u.test(toolLower)) {
|
|
1156
|
+
if (/^(write|edit|multiedit|multi_edit|delete|applypatch|notebookedit|runcommand|shell|terminal|execcommand)$/u.test(toolLower)) {
|
|
1168
1157
|
// Artifacts, runs, and knowledge writes are part of normal stage flow.
|
|
1169
1158
|
// Guard only managed internals that should be mutated via installer/CLI.
|
|
1170
1159
|
if (/\\.cclaw\\/(state|hooks|skills|commands|agents)/u.test(payloadLower)) {
|
package/dist/content/observe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { groupBindingsByEvent } from "./hook-manifest.js";
|
|
3
3
|
function hookDispatcherCommand(hookName) {
|
|
4
4
|
// Dispatch through the polyglot .cmd wrapper so Windows harnesses can run
|
|
5
5
|
// hooks even when command execution happens under CMD-style shells.
|
|
@@ -78,9 +78,7 @@ export function codexHooksJsonWithObservation() {
|
|
|
78
78
|
* manifest without importing the private generator helpers.
|
|
79
79
|
*/
|
|
80
80
|
export function hookManifestSnapshot() {
|
|
81
|
-
return (
|
|
82
|
-
? ["claude", "cursor", "codex"]
|
|
83
|
-
: ["claude", "cursor", "codex"]).map((harness) => ({
|
|
81
|
+
return ["claude", "cursor", "codex"].map((harness) => ({
|
|
84
82
|
harness,
|
|
85
83
|
events: groupBindingsByEvent(harness)
|
|
86
84
|
}));
|
|
@@ -463,11 +463,10 @@ export default function cclawPlugin(ctx) {
|
|
|
463
463
|
* \`DEFAULT_STRICTNESS = advisory\`, so the plugin can no longer
|
|
464
464
|
* accidentally be the stricter half of a mismatched pair.
|
|
465
465
|
*/
|
|
466
|
-
function readConfigStrictness() {
|
|
466
|
+
async function readConfigStrictness() {
|
|
467
467
|
try {
|
|
468
468
|
if (!existsSync(configPath)) return "";
|
|
469
|
-
const
|
|
470
|
-
const raw = readFileSync(configPath, "utf8");
|
|
469
|
+
const raw = await readFileText(configPath);
|
|
471
470
|
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
472
471
|
const match = raw.match(/^\\s*strictness\\s*:\\s*([A-Za-z0-9_-]+)/m);
|
|
473
472
|
return match && typeof match[1] === "string" ? match[1].trim().toLowerCase() : "";
|
|
@@ -476,7 +475,7 @@ export default function cclawPlugin(ctx) {
|
|
|
476
475
|
}
|
|
477
476
|
}
|
|
478
477
|
|
|
479
|
-
function resolveStrictness() {
|
|
478
|
+
async function resolveStrictness() {
|
|
480
479
|
const envRaw = typeof process.env.CCLAW_STRICTNESS === "string"
|
|
481
480
|
? process.env.CCLAW_STRICTNESS.trim().toLowerCase()
|
|
482
481
|
: "";
|
|
@@ -484,7 +483,7 @@ export default function cclawPlugin(ctx) {
|
|
|
484
483
|
if (envRaw === "advisory" || envRaw === "off" || envRaw === "disabled" || envRaw === "none") {
|
|
485
484
|
return "advisory";
|
|
486
485
|
}
|
|
487
|
-
const fileRaw = readConfigStrictness();
|
|
486
|
+
const fileRaw = await readConfigStrictness();
|
|
488
487
|
if (fileRaw === "strict") return "strict";
|
|
489
488
|
return "advisory";
|
|
490
489
|
}
|
|
@@ -683,7 +682,7 @@ export default function cclawPlugin(ctx) {
|
|
|
683
682
|
);
|
|
684
683
|
return;
|
|
685
684
|
}
|
|
686
|
-
const strictness = resolveStrictness();
|
|
685
|
+
const strictness = await resolveStrictness();
|
|
687
686
|
if (strictness !== "strict") {
|
|
688
687
|
// Advisory mode (the default) — every guard refusal is a hint,
|
|
689
688
|
// not a hard stop. Users report the "failure" as a log line
|
|
@@ -47,7 +47,6 @@ export declare function stageSchema(stage: FlowStage, track?: FlowTrack): StageS
|
|
|
47
47
|
export declare function orderedStageSchemas(track?: FlowTrack): StageSchema[];
|
|
48
48
|
export declare function stageGateIds(stage: FlowStage, track?: FlowTrack): string[];
|
|
49
49
|
export declare function stageRecommendedGateIds(stage: FlowStage, track?: FlowTrack): string[];
|
|
50
|
-
export declare function nextCclawCommand(stage: FlowStage): string;
|
|
51
50
|
export declare function buildTransitionRules(): TransitionRule[];
|
|
52
51
|
export declare function stagePolicyNeedles(stage: FlowStage, track?: FlowTrack): string[];
|
|
53
52
|
export declare function stageTrackRenderContext(track?: FlowTrack): import("./track-render-context.js").TrackRenderContext;
|
|
@@ -258,7 +258,7 @@ const REQUIRED_ARTIFACT_SECTIONS = {
|
|
|
258
258
|
"Deployment & Rollout",
|
|
259
259
|
"Completion Dashboard"
|
|
260
260
|
],
|
|
261
|
-
spec: ["Acceptance Criteria", "Edge Cases", "Assumptions Before Finalization", "
|
|
261
|
+
spec: ["Acceptance Criteria", "Edge Cases", "Assumptions Before Finalization", "Acceptance Mapping", "Approval"],
|
|
262
262
|
plan: ["Task List", "Dependency Batches", "Acceptance Mapping", "Execution Posture", "WAIT_FOR_CONFIRM"],
|
|
263
263
|
tdd: ["Test Discovery", "System-Wide Impact Check", "RED Evidence", "GREEN Evidence", "REFACTOR Notes", "Traceability", "Verification Ladder"],
|
|
264
264
|
review: ["Layer 1 Verdict", "Review Findings Contract", "Severity Summary", "Final Verdict"],
|
|
@@ -577,10 +577,6 @@ export function stageRecommendedGateIds(stage, track = "standard") {
|
|
|
577
577
|
.filter((gate) => gate.tier === "recommended")
|
|
578
578
|
.map((gate) => gate.id);
|
|
579
579
|
}
|
|
580
|
-
export function nextCclawCommand(stage) {
|
|
581
|
-
const next = stageSchema(stage).next;
|
|
582
|
-
return next === "done" ? "none" : `/cc-${next}`;
|
|
583
|
-
}
|
|
584
580
|
export function buildTransitionRules() {
|
|
585
581
|
const rules = [];
|
|
586
582
|
const seen = new Set();
|
|
@@ -92,7 +92,7 @@ ${conversationLanguagePolicyMarkdown()}
|
|
|
92
92
|
12. Load the **first-stage skill for the chosen track** and its command file:
|
|
93
93
|
- quick → \`.cclaw/skills/specification-authoring/SKILL.md\`
|
|
94
94
|
- medium/standard → \`.cclaw/skills/brainstorming/SKILL.md\`
|
|
95
|
-
- trivial fast-path →
|
|
95
|
+
- trivial fast-path → spec skill per Phase 0 decision.
|
|
96
96
|
13. Execute that stage with the prompt + Phase 1/Phase 2 + seed context as initial input.
|
|
97
97
|
|
|
98
98
|
### Reclassification on discovery
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function skillFrontmatter(name, description) {
|
|
2
|
+
return [
|
|
3
|
+
"---",
|
|
4
|
+
`name: ${name}`,
|
|
5
|
+
`description: ${JSON.stringify(description)}`,
|
|
6
|
+
"---",
|
|
7
|
+
""
|
|
8
|
+
].join("\n");
|
|
9
|
+
}
|
|
10
|
+
function tddCycleEvidenceSkill() {
|
|
11
|
+
return `${skillFrontmatter("tdd-cycle-evidence", "Evidence contract for the mandatory test-author delegation during RED/GREEN/REFACTOR.")}# TDD Cycle Evidence
|
|
12
|
+
|
|
13
|
+
Use with the \`test-author\` delegation in the \`tdd\` stage.
|
|
14
|
+
|
|
15
|
+
## Required Output
|
|
16
|
+
|
|
17
|
+
- RED evidence: failing test command, failing assertion/error, and why it fails for the intended reason.
|
|
18
|
+
- GREEN evidence: implementation summary plus relevant passing command.
|
|
19
|
+
- REFACTOR evidence: changed/unchanged behavior statement plus full-suite or highest available verification command.
|
|
20
|
+
- Trace refs: plan task ID, acceptance criterion ID, and touched test files.
|
|
21
|
+
|
|
22
|
+
## Guardrails
|
|
23
|
+
|
|
24
|
+
- No production code before RED evidence exists.
|
|
25
|
+
- If a RED test cannot be expressed, stop and route back to design/spec with the blocker.
|
|
26
|
+
- Record command output summaries, not just "tests passed".
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
function reviewSpecPassSkill() {
|
|
30
|
+
return `${skillFrontmatter("review-spec-pass", "Spec compliance pass for the mandatory reviewer delegation during review.")}# Review Spec Pass
|
|
31
|
+
|
|
32
|
+
Use with the \`reviewer\` delegation in the \`review\` stage before broader code-quality findings.
|
|
33
|
+
|
|
34
|
+
## Required Output
|
|
35
|
+
|
|
36
|
+
- For each acceptance criterion: PASS / PARTIAL / FAIL.
|
|
37
|
+
- Evidence refs grounded in files, tests, artifacts, or command output.
|
|
38
|
+
- Any mismatch between scope/design/spec/plan and implementation.
|
|
39
|
+
- Explicit list of Critical/Important blockers before ship.
|
|
40
|
+
|
|
41
|
+
## Guardrails
|
|
42
|
+
|
|
43
|
+
- Do not trust implementer summaries; verify by reading artifacts/code.
|
|
44
|
+
- Keep spec compliance separate from style suggestions.
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
function securityAuditSkill() {
|
|
48
|
+
return `${skillFrontmatter("security-audit", "Mandatory security sweep contract for the security-reviewer delegation.")}# Security Audit
|
|
49
|
+
|
|
50
|
+
Use with the \`security-reviewer\` delegation in the \`review\` stage.
|
|
51
|
+
|
|
52
|
+
## Required Output
|
|
53
|
+
|
|
54
|
+
- Trust-boundary map: auth/authz, input validation, secrets, filesystem/network/process access, third-party calls.
|
|
55
|
+
- Findings with severity, exploitability, affected file/path, and concrete mitigation.
|
|
56
|
+
- NO_CHANGE_ATTESTATION when no security-relevant surface moved, with evidence for why.
|
|
57
|
+
|
|
58
|
+
## Guardrails
|
|
59
|
+
|
|
60
|
+
- Pattern-scan the diff and touched modules before attesting no change.
|
|
61
|
+
- Security is mandatory in review even for small diffs.
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
function adversarialReviewSkill() {
|
|
65
|
+
return `${skillFrontmatter("adversarial-review", "Second-opinion reviewer lens for high-risk review scenarios.")}# Adversarial Review
|
|
66
|
+
|
|
67
|
+
Use only when the review dispatch trigger says risk justifies a second opinion.
|
|
68
|
+
|
|
69
|
+
## Required Output
|
|
70
|
+
|
|
71
|
+
- Attack the implementation assumptions, not the author.
|
|
72
|
+
- Look for hidden coupling, rollback gaps, data loss, race conditions, and untested edge cases.
|
|
73
|
+
- Mark each finding as confirmed, disproven, or needs-human-decision.
|
|
74
|
+
|
|
75
|
+
## Guardrails
|
|
76
|
+
|
|
77
|
+
- Do not duplicate the mandatory reviewer pass.
|
|
78
|
+
- If no additional risk is found, say so explicitly and cite what was checked.
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
function receivingCodeReviewSkill() {
|
|
82
|
+
return `${skillFrontmatter("receiving-code-review", "Workflow for triaging external reviewer, bot, or CI feedback during review.")}# Receiving Code Review
|
|
83
|
+
|
|
84
|
+
Use when external comments, bot findings, or CI annotations appear after the initial review pass.
|
|
85
|
+
|
|
86
|
+
## Required Output
|
|
87
|
+
|
|
88
|
+
- Queue every feedback item with source, severity, requested change, and evidence.
|
|
89
|
+
- Disposition: accepted, rejected-with-evidence, accepted-risk, duplicate, or needs-user-decision.
|
|
90
|
+
- Mirror the queue into the review artifact so unresolved feedback cannot disappear.
|
|
91
|
+
|
|
92
|
+
## Guardrails
|
|
93
|
+
|
|
94
|
+
- Do not silently dismiss bot/CI feedback.
|
|
95
|
+
- Re-run relevant checks after accepted fixes.
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
function stackAwareReviewSkill() {
|
|
99
|
+
return `${skillFrontmatter("stack-aware-review", "Language/runtime-specific review lens selected from detected repo signals.")}# Stack-Aware Review
|
|
100
|
+
|
|
101
|
+
Use after the default reviewer/security-reviewer passes when repo signals identify a relevant stack.
|
|
102
|
+
|
|
103
|
+
## Required Output
|
|
104
|
+
|
|
105
|
+
- Detected stack signal and why this lens applies.
|
|
106
|
+
- Stack-specific risks checked: package/build/test config, type/runtime boundaries, framework conventions, and deployment assumptions.
|
|
107
|
+
- Findings with evidence and whether they affect ship readiness.
|
|
108
|
+
|
|
109
|
+
## Guardrails
|
|
110
|
+
|
|
111
|
+
- Do not run every stack lens unconditionally.
|
|
112
|
+
- Keep the default general reviewer pass intact; this is additive context, not a replacement.
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
export const SUBAGENT_CONTEXT_SKILLS = {
|
|
116
|
+
"tdd-cycle-evidence": tddCycleEvidenceSkill(),
|
|
117
|
+
"review-spec-pass": reviewSpecPassSkill(),
|
|
118
|
+
"security-audit": securityAuditSkill(),
|
|
119
|
+
"adversarial-review": adversarialReviewSkill(),
|
|
120
|
+
"receiving-code-review": receivingCodeReviewSkill(),
|
|
121
|
+
"stack-aware-review": stackAwareReviewSkill()
|
|
122
|
+
};
|
package/dist/delegation.d.ts
CHANGED
|
@@ -93,7 +93,6 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
|
|
|
93
93
|
satisfied: boolean;
|
|
94
94
|
missing: string[];
|
|
95
95
|
waived: string[];
|
|
96
|
-
autoWaived: string[];
|
|
97
96
|
staleIgnored: string[];
|
|
98
97
|
/** Delegation rows missing required evidence under a role-switch fallback. */
|
|
99
98
|
missingEvidence: string[];
|
package/dist/delegation.js
CHANGED
|
@@ -268,7 +268,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
268
268
|
.map((e) => `${e.agent}(runId=${e.runId ?? "unknown"})`);
|
|
269
269
|
const missing = [];
|
|
270
270
|
const waived = [];
|
|
271
|
-
const autoWaived = [];
|
|
272
271
|
const missingEvidence = [];
|
|
273
272
|
const config = await readConfig(projectRoot).catch(() => null);
|
|
274
273
|
const harnesses = config?.harnesses ?? [];
|
|
@@ -277,7 +276,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
277
276
|
for (const agent of mandatory) {
|
|
278
277
|
const rows = forRun.filter((e) => e.agent === agent);
|
|
279
278
|
const completedRows = rows.filter((e) => e.status === "completed");
|
|
280
|
-
const waivedRows = rows.filter((e) => e.status === "waived");
|
|
279
|
+
const waivedRows = rows.filter((e) => e.status === "waived" && e.mode === "mandatory");
|
|
281
280
|
const hasCompleted = completedRows.length >= 1;
|
|
282
281
|
const hasWaived = waivedRows.length > 0;
|
|
283
282
|
const ok = hasWaived || hasCompleted;
|
|
@@ -301,7 +300,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
301
300
|
satisfied: missing.length === 0 && missingEvidence.length === 0,
|
|
302
301
|
missing,
|
|
303
302
|
waived,
|
|
304
|
-
autoWaived,
|
|
305
303
|
staleIgnored,
|
|
306
304
|
missingEvidence,
|
|
307
305
|
expectedMode
|
package/dist/doctor.js
CHANGED
|
@@ -1461,9 +1461,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1461
1461
|
name: "warning:delegation:waived",
|
|
1462
1462
|
ok: true,
|
|
1463
1463
|
details: delegation.waived.length > 0
|
|
1464
|
-
? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}
|
|
1465
|
-
? ` (auto-waived due to harness limitation: ${delegation.autoWaived.join(", ")})`
|
|
1466
|
-
: ""}`
|
|
1464
|
+
? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
|
|
1467
1465
|
: "no waived mandatory delegations for current stage"
|
|
1468
1466
|
});
|
|
1469
1467
|
checks.push({
|
package/dist/install.js
CHANGED
|
@@ -21,6 +21,7 @@ import { REVIEW_PROMPTS } from "./content/review-prompts.js";
|
|
|
21
21
|
import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
22
22
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GENERATORS, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
|
|
23
23
|
import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
|
|
24
|
+
import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
|
|
24
25
|
import { createInitialFlowState } from "./flow-state.js";
|
|
25
26
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
26
27
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
@@ -70,12 +71,9 @@ const DEPRECATED_UTILITY_SKILL_FOLDERS = [
|
|
|
70
71
|
"source-driven-development",
|
|
71
72
|
"frontend-accessibility",
|
|
72
73
|
"landscape-check",
|
|
73
|
-
"adversarial-review",
|
|
74
|
-
"security-audit",
|
|
75
74
|
"knowledge-curation",
|
|
76
75
|
"retrospective",
|
|
77
76
|
"document-review",
|
|
78
|
-
"receiving-code-review",
|
|
79
77
|
"flow-status",
|
|
80
78
|
"flow-tree",
|
|
81
79
|
"flow-diff"
|
|
@@ -387,18 +385,45 @@ async function writeSkills(projectRoot, config) {
|
|
|
387
385
|
for (const [fileName, markdown] of Object.entries(REVIEW_PROMPTS)) {
|
|
388
386
|
await writeFileSafe(runtimePath(projectRoot, "skills", "review-prompts", fileName), markdown);
|
|
389
387
|
}
|
|
388
|
+
for (const [folderName, markdown] of Object.entries(SUBAGENT_CONTEXT_SKILLS)) {
|
|
389
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", folderName, "SKILL.md"), markdown);
|
|
390
|
+
}
|
|
390
391
|
// Language rule packs live under .cclaw/rules/lang/<pack>.md. They are opt-in:
|
|
391
392
|
// only the packs listed in config.languageRulePacks are materialised. Any
|
|
392
393
|
// legacy per-language skill folders from v0.7.0 (.cclaw/skills/language-*)
|
|
393
394
|
// are cleaned up below so the new rules/lang layout is the only truth.
|
|
394
395
|
const enabledPacks = config?.languageRulePacks ?? [];
|
|
396
|
+
const enabledPackFileNames = new Set();
|
|
395
397
|
for (const pack of enabledPacks) {
|
|
396
398
|
const fileName = LANGUAGE_RULE_PACK_FILES[pack];
|
|
397
399
|
const generator = LANGUAGE_RULE_PACK_GENERATORS[pack];
|
|
398
400
|
if (!fileName || !generator)
|
|
399
401
|
continue;
|
|
402
|
+
enabledPackFileNames.add(fileName);
|
|
400
403
|
await writeFileSafe(runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR, fileName), generator());
|
|
401
404
|
}
|
|
405
|
+
// Strict idempotence: once a pack is removed from config, its generated
|
|
406
|
+
// file under .cclaw/rules/lang/ must disappear on the next sync. Without
|
|
407
|
+
// this loop the directory accumulates a superset of every pack ever
|
|
408
|
+
// enabled, which silently keeps stale guidance alive.
|
|
409
|
+
const langDir = runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR);
|
|
410
|
+
if (await exists(langDir)) {
|
|
411
|
+
const knownPackFileNames = new Set(Object.values(LANGUAGE_RULE_PACK_FILES));
|
|
412
|
+
let entries = [];
|
|
413
|
+
try {
|
|
414
|
+
entries = await fs.readdir(langDir);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
entries = [];
|
|
418
|
+
}
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
if (!knownPackFileNames.has(entry))
|
|
421
|
+
continue;
|
|
422
|
+
if (enabledPackFileNames.has(entry))
|
|
423
|
+
continue;
|
|
424
|
+
await fs.rm(path.join(langDir, entry), { force: true });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
402
427
|
for (const legacyFolder of LEGACY_LANGUAGE_RULE_PACK_FOLDERS) {
|
|
403
428
|
const legacyPath = runtimePath(projectRoot, "skills", legacyFolder);
|
|
404
429
|
if (await exists(legacyPath)) {
|
package/dist/run-persistence.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import {
|
|
4
|
+
import { nextStage, createInitialCloseoutState, createInitialFlowState, FLOW_STATE_SCHEMA_VERSION, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
|
|
5
5
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
6
6
|
import { FLOW_STAGES } from "./types.js";
|
|
7
7
|
export class InvalidStageTransitionError extends Error {
|
|
@@ -38,8 +38,11 @@ function validateFlowTransition(prev, next) {
|
|
|
38
38
|
if (prev.currentStage === next.currentStage) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const naturalForward = nextStage(prev.currentStage, prev.track);
|
|
42
|
+
const isNaturalForward = naturalForward === next.currentStage;
|
|
43
|
+
const isReviewRewind = prev.currentStage === "review" && next.currentStage === "tdd";
|
|
44
|
+
if (!isNaturalForward && !isReviewRewind) {
|
|
45
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}" for track "${prev.track}". Use /cc-next to advance stages or archive the run to reset.`);
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
function flowStatePath(projectRoot) {
|