cclaw-cli 0.5.15 → 0.5.17
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 +6 -0
- package/dist/artifact-linter.d.ts +13 -0
- package/dist/artifact-linter.js +182 -13
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +76 -12
- package/dist/content/agents.js +5 -5
- package/dist/content/examples.js +71 -62
- package/dist/content/stage-schema.js +3 -3
- package/dist/content/subagents.js +1 -1
- package/dist/content/templates.js +6 -21
- package/dist/delegation.d.ts +6 -0
- package/dist/delegation.js +12 -4
- package/dist/doctor.js +37 -1
- package/dist/gate-evidence.d.ts +14 -0
- package/dist/gate-evidence.js +65 -3
- package/dist/runs.d.ts +30 -1
- package/dist/runs.js +164 -10
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -120,6 +120,12 @@ Required repository secret:
|
|
|
120
120
|
└── runs/ # archived feature snapshots (YYYY-MM-DD-feature-name)
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Harness Integration
|
|
124
|
+
|
|
125
|
+
Supported harnesses: `claude`, `cursor`, `opencode`, `codex`. The full
|
|
126
|
+
per-harness install surface, feature matrix, and lifecycle details live in
|
|
127
|
+
[docs/harnesses.md](./docs/harnesses.md).
|
|
128
|
+
|
|
123
129
|
## License
|
|
124
130
|
|
|
125
131
|
[MIT](./LICENSE)
|
|
@@ -18,3 +18,16 @@ export declare function validateReviewArmy(projectRoot: string): Promise<{
|
|
|
18
18
|
valid: boolean;
|
|
19
19
|
errors: string[];
|
|
20
20
|
}>;
|
|
21
|
+
export interface ReviewVerdictConsistencyResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
errors: string[];
|
|
24
|
+
finalVerdict: "APPROVED" | "APPROVED_WITH_CONCERNS" | "BLOCKED" | "UNKNOWN";
|
|
25
|
+
openCriticalCount: number;
|
|
26
|
+
shipBlockerCount: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ensure the narrative verdict in 07-review.md is consistent with the
|
|
30
|
+
* structured review-army reconciliation. A review cannot declare
|
|
31
|
+
* APPROVED while open Critical findings or shipBlockers remain.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
|
package/dist/artifact-linter.js
CHANGED
|
@@ -134,7 +134,61 @@ function extractRequiredKeywords(rule) {
|
|
|
134
134
|
return [];
|
|
135
135
|
return phrases;
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
const VAGUE_AC_ADJECTIVES = [
|
|
138
|
+
"fast",
|
|
139
|
+
"quick",
|
|
140
|
+
"slow",
|
|
141
|
+
"fast enough",
|
|
142
|
+
"quickly",
|
|
143
|
+
"intuitive",
|
|
144
|
+
"robust",
|
|
145
|
+
"reliable",
|
|
146
|
+
"scalable",
|
|
147
|
+
"simple",
|
|
148
|
+
"easy",
|
|
149
|
+
"user-friendly",
|
|
150
|
+
"user friendly",
|
|
151
|
+
"nice",
|
|
152
|
+
"good",
|
|
153
|
+
"clean",
|
|
154
|
+
"secure enough",
|
|
155
|
+
"responsive",
|
|
156
|
+
"efficient",
|
|
157
|
+
"performant",
|
|
158
|
+
"smooth",
|
|
159
|
+
"seamless",
|
|
160
|
+
"modern"
|
|
161
|
+
];
|
|
162
|
+
function isSeparatorRow(line) {
|
|
163
|
+
return /^\|[-:| ]+\|$/u.test(line);
|
|
164
|
+
}
|
|
165
|
+
function getMarkdownTableRows(sectionBody) {
|
|
166
|
+
const lines = sectionBody.split(/\r?\n/).map((line) => line.trim());
|
|
167
|
+
const rows = [];
|
|
168
|
+
let sawSeparator = false;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (!/^\|.*\|$/u.test(line))
|
|
171
|
+
continue;
|
|
172
|
+
if (isSeparatorRow(line)) {
|
|
173
|
+
sawSeparator = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!sawSeparator)
|
|
177
|
+
continue;
|
|
178
|
+
rows.push(parseMarkdownTableRow(line));
|
|
179
|
+
}
|
|
180
|
+
return rows;
|
|
181
|
+
}
|
|
182
|
+
function lineContainsVagueAdjective(text) {
|
|
183
|
+
const lower = text.toLowerCase();
|
|
184
|
+
for (const adjective of VAGUE_AC_ADJECTIVES) {
|
|
185
|
+
const pattern = new RegExp(`(?:^|[^A-Za-z])${adjective.replace(/ /g, "\\s+")}(?:[^A-Za-z]|$)`, "iu");
|
|
186
|
+
if (pattern.test(lower))
|
|
187
|
+
return adjective;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function validateSectionBody(sectionBody, rule, sectionName) {
|
|
138
192
|
const bodyLines = sectionBody.split(/\r?\n/).map((line) => line.trim());
|
|
139
193
|
const meaningful = meaningfulLineCount(sectionBody);
|
|
140
194
|
if (meaningful === 0) {
|
|
@@ -231,6 +285,29 @@ function validateSectionBody(sectionBody, rule) {
|
|
|
231
285
|
};
|
|
232
286
|
}
|
|
233
287
|
}
|
|
288
|
+
if (normalizeHeadingTitle(sectionName).toLowerCase() === "acceptance criteria" &&
|
|
289
|
+
/observable[\s,]*measurable[\s,]+(and )?falsifiable/iu.test(rule)) {
|
|
290
|
+
const rows = getMarkdownTableRows(sectionBody);
|
|
291
|
+
for (const row of rows) {
|
|
292
|
+
const criterionText = row[1] ?? row[0] ?? "";
|
|
293
|
+
const adjective = lineContainsVagueAdjective(criterionText);
|
|
294
|
+
if (adjective) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
details: `Acceptance criterion uses vague adjective "${adjective}" without a measurable predicate: "${criterionText.slice(0, 140)}". Rewrite with a numeric threshold or boolean outcome.`
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const hasDigit = /\d/u.test(criterionText);
|
|
301
|
+
const hasMeasurableVerb = /\b(blocks?|rejects?|returns?|matches?|equals?|emits?|succeeds?|fails?|publishes?|logs?|persists?|reads?|writes?|creates?|deletes?|throws?|contains?|restores?|exceeds?|responds?|warns?|quarantines?|includes?|raises?|passes?|denies|refuses|exits|succeeds|completes|prevents|allows|maps|points|signals|surfaces|records|produces|accepts|requires)\b/iu.test(criterionText);
|
|
302
|
+
const hasMeaningfulText = /[A-Za-z]/u.test(criterionText) && criterionText.trim().length >= 12;
|
|
303
|
+
if (hasMeaningfulText && !hasDigit && !hasMeasurableVerb) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
details: `Acceptance criterion lacks a measurable predicate (no numeric threshold, no observable verb like blocks/returns/publishes/matches): "${criterionText.slice(0, 140)}". Rewrite so the criterion is falsifiable by a single test.`
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
234
311
|
return {
|
|
235
312
|
ok: true,
|
|
236
313
|
details: "Section heading and content satisfy lint heuristics."
|
|
@@ -273,7 +350,7 @@ export async function lintArtifact(projectRoot, stage) {
|
|
|
273
350
|
const body = hasHeading ? sectionBodyByName(sections, v.section) : null;
|
|
274
351
|
const validation = body === null
|
|
275
352
|
? { ok: false, details: `No ## heading matching required section "${v.section}".` }
|
|
276
|
-
: validateSectionBody(body, v.validationRule);
|
|
353
|
+
: validateSectionBody(body, v.validationRule, v.section);
|
|
277
354
|
const found = hasHeading && validation.ok;
|
|
278
355
|
findings.push({
|
|
279
356
|
section: v.section,
|
|
@@ -384,18 +461,19 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
384
461
|
if (!isStringArray(o.reportedBy) || o.reportedBy.length === 0) {
|
|
385
462
|
errors.push(`findings[${i}].reportedBy must be a non-empty string array.`);
|
|
386
463
|
}
|
|
387
|
-
if (o.location
|
|
388
|
-
|
|
389
|
-
|
|
464
|
+
if (o.location === undefined || o.location === null) {
|
|
465
|
+
errors.push(`findings[${i}].location is required and must be an object with file + line.`);
|
|
466
|
+
}
|
|
467
|
+
else if (typeof o.location !== "object" || Array.isArray(o.location)) {
|
|
468
|
+
errors.push(`findings[${i}].location must be an object with file + line.`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
const loc = o.location;
|
|
472
|
+
if (!isNonEmptyString(loc.file)) {
|
|
473
|
+
errors.push(`findings[${i}].location.file must be a non-empty string.`);
|
|
390
474
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (!isNonEmptyString(loc.file)) {
|
|
394
|
-
errors.push(`findings[${i}].location.file must be a non-empty string.`);
|
|
395
|
-
}
|
|
396
|
-
if (!isFiniteNumber(loc.line) || loc.line < 1) {
|
|
397
|
-
errors.push(`findings[${i}].location.line must be a positive number.`);
|
|
398
|
-
}
|
|
475
|
+
if (!isFiniteNumber(loc.line) || loc.line < 1) {
|
|
476
|
+
errors.push(`findings[${i}].location.line must be a positive number.`);
|
|
399
477
|
}
|
|
400
478
|
}
|
|
401
479
|
if (o.recommendation !== undefined && !isNonEmptyString(o.recommendation)) {
|
|
@@ -445,6 +523,21 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
445
523
|
for (const msId of rec.multiSpecialistConfirmed) {
|
|
446
524
|
if (!findingIds.has(msId)) {
|
|
447
525
|
errors.push(`reconciliation.multiSpecialistConfirmed references unknown finding id "${msId}".`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (Array.isArray(root.findings)) {
|
|
529
|
+
const finding = root.findings.find((f) => {
|
|
530
|
+
return f && typeof f === "object" && !Array.isArray(f) && f.id === msId;
|
|
531
|
+
});
|
|
532
|
+
if (finding && typeof finding === "object" && !Array.isArray(finding)) {
|
|
533
|
+
const reportedBy = finding.reportedBy;
|
|
534
|
+
const count = Array.isArray(reportedBy)
|
|
535
|
+
? new Set(reportedBy.filter((v) => typeof v === "string")).size
|
|
536
|
+
: 0;
|
|
537
|
+
if (count < 2) {
|
|
538
|
+
errors.push(`reconciliation.multiSpecialistConfirmed entry "${msId}" must be confirmed by at least 2 distinct reviewers (found ${count}).`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
448
541
|
}
|
|
449
542
|
}
|
|
450
543
|
}
|
|
@@ -474,3 +567,79 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
474
567
|
}
|
|
475
568
|
return { valid: errors.length === 0, errors };
|
|
476
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Ensure the narrative verdict in 07-review.md is consistent with the
|
|
572
|
+
* structured review-army reconciliation. A review cannot declare
|
|
573
|
+
* APPROVED while open Critical findings or shipBlockers remain.
|
|
574
|
+
*/
|
|
575
|
+
export async function checkReviewVerdictConsistency(projectRoot) {
|
|
576
|
+
const errors = [];
|
|
577
|
+
const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
|
|
578
|
+
const armyJsonPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review-army.json");
|
|
579
|
+
let finalVerdict = "UNKNOWN";
|
|
580
|
+
if (await exists(reviewMdPath)) {
|
|
581
|
+
const raw = await fs.readFile(reviewMdPath, "utf8");
|
|
582
|
+
const sections = extractH2Sections(raw);
|
|
583
|
+
const verdictBody = sectionBodyByName(sections, "Final Verdict");
|
|
584
|
+
if (verdictBody) {
|
|
585
|
+
const chosen = [];
|
|
586
|
+
for (const token of ["APPROVED_WITH_CONCERNS", "APPROVED", "BLOCKED"]) {
|
|
587
|
+
const regex = new RegExp(`\\b${token}\\b`, "u");
|
|
588
|
+
if (regex.test(verdictBody)) {
|
|
589
|
+
// APPROVED would match inside APPROVED_WITH_CONCERNS; prefer the longer match first.
|
|
590
|
+
if (token === "APPROVED" && /\bAPPROVED_WITH_CONCERNS\b/u.test(verdictBody))
|
|
591
|
+
continue;
|
|
592
|
+
chosen.push(token);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (chosen.length === 1) {
|
|
596
|
+
finalVerdict = chosen[0];
|
|
597
|
+
}
|
|
598
|
+
else if (chosen.length > 1) {
|
|
599
|
+
errors.push(`Final Verdict section lists multiple verdict tokens (${chosen.join(", ")}). Select exactly one.`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
errors.push('Final Verdict section does not select APPROVED, APPROVED_WITH_CONCERNS, or BLOCKED.');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
errors.push('07-review.md is missing the "## Final Verdict" section.');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
let openCriticalCount = 0;
|
|
610
|
+
let shipBlockerCount = 0;
|
|
611
|
+
if (await exists(armyJsonPath)) {
|
|
612
|
+
try {
|
|
613
|
+
const raw = await fs.readFile(armyJsonPath, "utf8");
|
|
614
|
+
const parsed = JSON.parse(raw);
|
|
615
|
+
const findings = Array.isArray(parsed.findings) ? parsed.findings : [];
|
|
616
|
+
for (const f of findings) {
|
|
617
|
+
if (!f || typeof f !== "object" || Array.isArray(f))
|
|
618
|
+
continue;
|
|
619
|
+
const o = f;
|
|
620
|
+
if (o.severity === "Critical" && o.status === "open") {
|
|
621
|
+
openCriticalCount++;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const rec = parsed.reconciliation && typeof parsed.reconciliation === "object" && !Array.isArray(parsed.reconciliation)
|
|
625
|
+
? parsed.reconciliation
|
|
626
|
+
: null;
|
|
627
|
+
if (rec && Array.isArray(rec.shipBlockers)) {
|
|
628
|
+
shipBlockerCount = rec.shipBlockers.filter((v) => typeof v === "string").length;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// JSON validity is the concern of validateReviewArmy; skip silently here.
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (finalVerdict === "APPROVED" && (openCriticalCount > 0 || shipBlockerCount > 0)) {
|
|
636
|
+
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.`);
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
ok: errors.length === 0,
|
|
640
|
+
errors,
|
|
641
|
+
finalVerdict,
|
|
642
|
+
openCriticalCount,
|
|
643
|
+
shipBlockerCount
|
|
644
|
+
};
|
|
645
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ interface ParsedArgs {
|
|
|
6
6
|
harnesses?: HarnessId[];
|
|
7
7
|
reconcileGates?: boolean;
|
|
8
8
|
archiveName?: string;
|
|
9
|
+
showHelp?: boolean;
|
|
10
|
+
showVersion?: boolean;
|
|
9
11
|
}
|
|
12
|
+
export declare function usage(): string;
|
|
10
13
|
declare function parseHarnesses(raw: string): HarnessId[];
|
|
11
14
|
declare function parseArgs(argv: string[]): ParsedArgs;
|
|
12
15
|
export { parseArgs, parseHarnesses };
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,18 +9,63 @@ import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js
|
|
|
9
9
|
import { error, info } from "./logger.js";
|
|
10
10
|
import { archiveRun } from "./runs.js";
|
|
11
11
|
const INSTALLER_COMMANDS = ["init", "sync", "doctor", "upgrade", "uninstall", "archive"];
|
|
12
|
-
function usage() {
|
|
12
|
+
export function usage() {
|
|
13
13
|
return `cclaw - installer-first flow toolkit
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
|
-
cclaw
|
|
17
|
-
cclaw
|
|
18
|
-
cclaw
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
cclaw
|
|
16
|
+
cclaw <command> [flags]
|
|
17
|
+
cclaw --help | -h
|
|
18
|
+
cclaw --version | -v
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Bootstrap .cclaw runtime, state, and harness shims in this project.
|
|
22
|
+
Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
|
|
23
|
+
sync Regenerate harness shim files from the current .cclaw config (non-destructive).
|
|
24
|
+
doctor Run health checks against the local .cclaw runtime. Exit code 2 on failure.
|
|
25
|
+
Flags: --reconcile-gates Recompute current-stage gate evidence before checks.
|
|
26
|
+
archive Move .cclaw/artifacts into .cclaw/runs/<date>-<slug> and reset flow state.
|
|
27
|
+
Flags: --name=<feature> Feature slug (default: inferred from 00-idea.md).
|
|
28
|
+
upgrade Refresh generated files in .cclaw without modifying user artifacts.
|
|
29
|
+
uninstall Remove .cclaw runtime and the generated harness shim files.
|
|
30
|
+
|
|
31
|
+
Global flags:
|
|
32
|
+
-h, --help Show this help message and exit 0.
|
|
33
|
+
-v, --version Print the cclaw CLI version and exit 0.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
cclaw init --harnesses=claude,cursor
|
|
37
|
+
cclaw doctor --reconcile-gates
|
|
38
|
+
cclaw archive --name=payments-revamp
|
|
39
|
+
|
|
40
|
+
Docs: https://github.com/zuevrs/cclaw
|
|
41
|
+
Issues: https://github.com/zuevrs/cclaw/issues
|
|
22
42
|
`;
|
|
23
43
|
}
|
|
44
|
+
function cliPackageVersion() {
|
|
45
|
+
try {
|
|
46
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.resolve(here, "../package.json"),
|
|
49
|
+
path.resolve(here, "../../package.json")
|
|
50
|
+
];
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(candidate, "utf8");
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (parsed.name === "cclaw-cli" && typeof parsed.version === "string") {
|
|
56
|
+
return parsed.version;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// fall through
|
|
66
|
+
}
|
|
67
|
+
return "unknown";
|
|
68
|
+
}
|
|
24
69
|
function parseHarnesses(raw) {
|
|
25
70
|
const requested = raw
|
|
26
71
|
.split(",")
|
|
@@ -33,11 +78,19 @@ function parseHarnesses(raw) {
|
|
|
33
78
|
return requested;
|
|
34
79
|
}
|
|
35
80
|
function parseArgs(argv) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
81
|
+
const parsed = {};
|
|
82
|
+
const helpFlag = argv.find((arg) => arg === "--help" || arg === "-h");
|
|
83
|
+
if (helpFlag) {
|
|
84
|
+
parsed.showHelp = true;
|
|
85
|
+
}
|
|
86
|
+
const versionFlag = argv.find((arg) => arg === "--version" || arg === "-v");
|
|
87
|
+
if (versionFlag) {
|
|
88
|
+
parsed.showVersion = true;
|
|
89
|
+
}
|
|
90
|
+
const [commandRaw, ...flags] = argv.filter((arg) => arg !== "--help" && arg !== "-h" && arg !== "--version" && arg !== "-v");
|
|
91
|
+
parsed.command = INSTALLER_COMMANDS.includes(commandRaw)
|
|
38
92
|
? commandRaw
|
|
39
93
|
: undefined;
|
|
40
|
-
const parsed = { command };
|
|
41
94
|
for (const flag of flags) {
|
|
42
95
|
if (flag.startsWith("--harnesses=")) {
|
|
43
96
|
parsed.harnesses = parseHarnesses(flag.replace("--harnesses=", ""));
|
|
@@ -54,6 +107,14 @@ function parseArgs(argv) {
|
|
|
54
107
|
return parsed;
|
|
55
108
|
}
|
|
56
109
|
async function runCommand(parsed, ctx) {
|
|
110
|
+
if (parsed.showHelp) {
|
|
111
|
+
ctx.stdout.write(usage());
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (parsed.showVersion) {
|
|
115
|
+
ctx.stdout.write(`cclaw ${cliPackageVersion()}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
57
118
|
const command = parsed.command;
|
|
58
119
|
if (!command) {
|
|
59
120
|
ctx.stderr.write(usage());
|
|
@@ -88,7 +149,10 @@ async function runCommand(parsed, ctx) {
|
|
|
88
149
|
}
|
|
89
150
|
if (command === "archive") {
|
|
90
151
|
const archived = await archiveRun(ctx.cwd, parsed.archiveName);
|
|
91
|
-
|
|
152
|
+
const snapshotSummary = archived.snapshottedStateFiles.length > 0
|
|
153
|
+
? ` Snapshotted ${archived.snapshottedStateFiles.length} state file(s) under ${archived.archivePath}/state and wrote archive-manifest.json.`
|
|
154
|
+
: "";
|
|
155
|
+
info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
|
|
92
156
|
return 0;
|
|
93
157
|
}
|
|
94
158
|
await uninstallCclaw(ctx.cwd);
|
package/dist/content/agents.js
CHANGED
|
@@ -94,10 +94,10 @@ export const CCLAW_AGENTS = [
|
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
name: "security-reviewer",
|
|
97
|
-
description: "
|
|
97
|
+
description: "MANDATORY during every review stage. Even when no auth, crypto, secrets, parsers, or sensitive data paths changed, produce an explicit 'no-change' security attestation. MUST BE USED when trust boundaries move, new external inputs arrive, or LLM/tool output influences privileged actions.",
|
|
98
98
|
tools: ["Read", "Grep", "Glob"],
|
|
99
99
|
model: "balanced",
|
|
100
|
-
activation: "
|
|
100
|
+
activation: "mandatory",
|
|
101
101
|
relatedStages: ["review", "design"],
|
|
102
102
|
body: [
|
|
103
103
|
"You are a **security vulnerability detection** specialist focused on practical exploitability.",
|
|
@@ -216,7 +216,7 @@ export function agentRoutingTable() {
|
|
|
216
216
|
| Brainstorm (start with \`/cc <idea>\`) | planner | — |
|
|
217
217
|
| Scope / Design / Spec / Plan (advance via \`/cc-next\`) | planner | security-reviewer on design, spec-reviewer on spec |
|
|
218
218
|
| TDD (via \`/cc-next\`) | test-author | doc-updater |
|
|
219
|
-
| Review (via \`/cc-next\`) | spec-reviewer, code-reviewer
|
|
219
|
+
| Review (via \`/cc-next\`) | spec-reviewer, code-reviewer, security-reviewer | — |
|
|
220
220
|
| Ship (via \`/cc-next\`) | — | doc-updater |
|
|
221
221
|
`;
|
|
222
222
|
}
|
|
@@ -231,8 +231,8 @@ Cclaw provides specialist agents under \`.cclaw/agents/\` for targeted delegatio
|
|
|
231
231
|
${agentRoutingTable()}
|
|
232
232
|
|
|
233
233
|
**Activation modes:**
|
|
234
|
-
- **Mandatory:** MUST be used when the related stage runs (spec-reviewer, code-reviewer during review)
|
|
235
|
-
- **Proactive:** Should be used automatically when context matches (planner for complex features, security-reviewer
|
|
234
|
+
- **Mandatory:** MUST be used when the related stage runs (spec-reviewer, code-reviewer, and security-reviewer during review; planner during scope and design; test-author during tdd; doc-updater during ship). Even if a change has no trust-boundary impact, security-reviewer produces an explicit no-change attestation.
|
|
235
|
+
- **Proactive:** Should be used automatically when context matches (planner for complex features, security-reviewer escalations outside review, doc-updater on behavior changes)
|
|
236
236
|
- **On-demand:** Invoked only when explicitly requested
|
|
237
237
|
|
|
238
238
|
**Agent files:** \`.cclaw/agents/{name}.md\` — each contains YAML frontmatter with tools and model tier.
|