cclaw-cli 0.5.16 → 0.6.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.
@@ -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>;
@@ -134,7 +134,61 @@ function extractRequiredKeywords(rule) {
134
134
  return [];
135
135
  return phrases;
136
136
  }
137
- function validateSectionBody(sectionBody, rule) {
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 !== undefined) {
388
- if (o.location === null || typeof o.location !== "object" || Array.isArray(o.location)) {
389
- errors.push(`findings[${i}].location must be an object when present.`);
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
- else {
392
- const loc = o.location;
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
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import type { HarnessId } from "./types.js";
2
+ import type { FlowTrack, HarnessId } from "./types.js";
3
3
  type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive";
4
4
  interface ParsedArgs {
5
5
  command?: CommandName;
6
6
  harnesses?: HarnessId[];
7
+ track?: FlowTrack;
7
8
  reconcileGates?: boolean;
8
9
  archiveName?: string;
9
10
  showHelp?: boolean;
@@ -11,5 +12,6 @@ interface ParsedArgs {
11
12
  }
12
13
  export declare function usage(): string;
13
14
  declare function parseHarnesses(raw: string): HarnessId[];
15
+ declare function parseTrack(raw: string): FlowTrack;
14
16
  declare function parseArgs(argv: string[]): ParsedArgs;
15
- export { parseArgs, parseHarnesses };
17
+ export { parseArgs, parseHarnesses, parseTrack };
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ 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";
6
- import { HARNESS_IDS } from "./types.js";
6
+ import { FLOW_TRACKS, HARNESS_IDS } from "./types.js";
7
7
  import { doctorChecks, doctorSucceeded } from "./doctor.js";
8
8
  import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js";
9
9
  import { error, info } from "./logger.js";
@@ -20,6 +20,7 @@ Usage:
20
20
  Commands:
21
21
  init Bootstrap .cclaw runtime, state, and harness shims in this project.
22
22
  Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
23
+ --track=<id> Flow track for new runs (standard | quick). Default: standard.
23
24
  sync Regenerate harness shim files from the current .cclaw config (non-destructive).
24
25
  doctor Run health checks against the local .cclaw runtime. Exit code 2 on failure.
25
26
  Flags: --reconcile-gates Recompute current-stage gate evidence before checks.
@@ -77,6 +78,13 @@ function parseHarnesses(raw) {
77
78
  }
78
79
  return requested;
79
80
  }
81
+ function parseTrack(raw) {
82
+ const trimmed = raw.trim();
83
+ if (!FLOW_TRACKS.includes(trimmed)) {
84
+ throw new Error(`Unknown track: ${trimmed}. Supported: ${FLOW_TRACKS.join(", ")}`);
85
+ }
86
+ return trimmed;
87
+ }
80
88
  function parseArgs(argv) {
81
89
  const parsed = {};
82
90
  const helpFlag = argv.find((arg) => arg === "--help" || arg === "-h");
@@ -96,6 +104,10 @@ function parseArgs(argv) {
96
104
  parsed.harnesses = parseHarnesses(flag.replace("--harnesses=", ""));
97
105
  continue;
98
106
  }
107
+ if (flag.startsWith("--track=")) {
108
+ parsed.track = parseTrack(flag.replace("--track=", ""));
109
+ continue;
110
+ }
99
111
  if (flag === "--reconcile-gates") {
100
112
  parsed.reconcileGates = true;
101
113
  continue;
@@ -123,9 +135,11 @@ async function runCommand(parsed, ctx) {
123
135
  if (command === "init") {
124
136
  await initCclaw({
125
137
  projectRoot: ctx.cwd,
126
- harnesses: parsed.harnesses
138
+ harnesses: parsed.harnesses,
139
+ track: parsed.track
127
140
  });
128
- info(ctx, "Initialized .cclaw runtime and generated harness shims");
141
+ const trackNote = parsed.track ? ` (track: ${parsed.track})` : "";
142
+ info(ctx, `Initialized .cclaw runtime and generated harness shims${trackNote}`);
129
143
  return 0;
130
144
  }
131
145
  if (command === "sync") {
@@ -190,4 +204,4 @@ function isDirectExecution() {
190
204
  if (isDirectExecution()) {
191
205
  void main();
192
206
  }
193
- export { parseArgs, parseHarnesses };
207
+ export { parseArgs, parseHarnesses, parseTrack };
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { HarnessId, VibyConfig } from "./types.js";
1
+ import type { FlowTrack, HarnessId, VibyConfig } from "./types.js";
2
2
  export declare function configPath(projectRoot: string): string;
3
- export declare function createDefaultConfig(harnesses?: HarnessId[]): VibyConfig;
3
+ export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack): VibyConfig;
4
4
  export declare function readConfig(projectRoot: string): Promise<VibyConfig>;
5
5
  export declare function writeConfig(projectRoot: string, config: VibyConfig): Promise<void>;
package/dist/config.js CHANGED
@@ -3,17 +3,20 @@ import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
5
5
  import { exists, writeFileSafe } from "./fs-utils.js";
6
- import { HARNESS_IDS } from "./types.js";
6
+ import { FLOW_TRACKS, HARNESS_IDS } from "./types.js";
7
7
  const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
8
8
  const HARNESS_ID_SET = new Set(HARNESS_IDS);
9
+ const FLOW_TRACK_SET = new Set(FLOW_TRACKS);
9
10
  const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
11
+ const SUPPORTED_TRACKS_TEXT = FLOW_TRACKS.join(", ");
10
12
  const ALLOWED_CONFIG_KEYS = new Set([
11
13
  "version",
12
14
  "flowVersion",
13
15
  "harnesses",
14
16
  "autoAdvance",
15
17
  "promptGuardMode",
16
- "gitHookGuards"
18
+ "gitHookGuards",
19
+ "defaultTrack"
17
20
  ]);
18
21
  function configFixExample() {
19
22
  return `harnesses:
@@ -23,20 +26,22 @@ function configFixExample() {
23
26
  function configValidationError(configFilePath, reason) {
24
27
  return new Error(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
25
28
  `Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
29
+ `Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
26
30
  `Example config:\n${configFixExample()}\n` +
27
31
  `After fixing, run: cclaw sync`);
28
32
  }
29
33
  export function configPath(projectRoot) {
30
34
  return path.join(projectRoot, CONFIG_PATH);
31
35
  }
32
- export function createDefaultConfig(harnesses = DEFAULT_HARNESSES) {
36
+ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack = "standard") {
33
37
  return {
34
38
  version: CCLAW_VERSION,
35
39
  flowVersion: FLOW_VERSION,
36
40
  harnesses,
37
41
  autoAdvance: false,
38
42
  promptGuardMode: "advisory",
39
- gitHookGuards: false
43
+ gitHookGuards: false,
44
+ defaultTrack
40
45
  };
41
46
  }
42
47
  export async function readConfig(projectRoot) {
@@ -89,13 +94,22 @@ export async function readConfig(projectRoot) {
89
94
  throw configValidationError(fullPath, `"gitHookGuards" must be a boolean`);
90
95
  }
91
96
  const gitHookGuards = typeof gitHookGuardsRaw === "boolean" ? gitHookGuardsRaw : false;
97
+ const defaultTrackRaw = parsed.defaultTrack;
98
+ if (Object.prototype.hasOwnProperty.call(parsed, "defaultTrack") &&
99
+ (typeof defaultTrackRaw !== "string" || !FLOW_TRACK_SET.has(defaultTrackRaw))) {
100
+ throw configValidationError(fullPath, `"defaultTrack" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
101
+ }
102
+ const defaultTrack = typeof defaultTrackRaw === "string" && FLOW_TRACK_SET.has(defaultTrackRaw)
103
+ ? defaultTrackRaw
104
+ : "standard";
92
105
  return {
93
106
  version: parsed.version ?? CCLAW_VERSION,
94
107
  flowVersion: parsed.flowVersion ?? FLOW_VERSION,
95
108
  harnesses,
96
109
  autoAdvance,
97
110
  promptGuardMode,
98
- gitHookGuards
111
+ gitHookGuards,
112
+ defaultTrack
99
113
  };
100
114
  }
101
115
  export async function writeConfig(projectRoot, config) {
@@ -4,9 +4,9 @@ export declare const RUNTIME_ROOT = ".cclaw";
4
4
  export declare const CCLAW_VERSION = "0.1.1";
5
5
  export declare const FLOW_VERSION = "1.0.0";
6
6
  export declare const DEFAULT_HARNESSES: HarnessId[];
7
- export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks"];
7
+ export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks", ".cclaw/custom-skills"];
8
8
  export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".codex/commands/cc-*.md", ".codex/commands/cc.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
9
9
  export declare const COMMAND_FILE_ORDER: FlowStage[];
10
- export declare const UTILITY_COMMANDS: readonly ["learn", "next"];
10
+ export declare const UTILITY_COMMANDS: readonly ["learn", "next", "status"];
11
11
  export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
12
12
  export type UtilityCommand = (typeof UTILITY_COMMANDS)[number];
package/dist/constants.js CHANGED
@@ -20,7 +20,8 @@ export const REQUIRED_DIRS = [
20
20
  `${RUNTIME_ROOT}/rules`,
21
21
  `${RUNTIME_ROOT}/adapters`,
22
22
  `${RUNTIME_ROOT}/agents`,
23
- `${RUNTIME_ROOT}/hooks`
23
+ `${RUNTIME_ROOT}/hooks`,
24
+ `${RUNTIME_ROOT}/custom-skills`
24
25
  ];
25
26
  export const REQUIRED_GITIGNORE_PATTERNS = [
26
27
  "# cclaw generated artifacts",
@@ -49,7 +50,7 @@ export const COMMAND_FILE_ORDER = [
49
50
  "review",
50
51
  "ship"
51
52
  ];
52
- export const UTILITY_COMMANDS = ["learn", "next"];
53
+ export const UTILITY_COMMANDS = ["learn", "next", "status"];
53
54
  export const SUBAGENT_SKILL_FOLDERS = [
54
55
  "subagent-dev",
55
56
  "parallel-dispatch"
@@ -94,10 +94,10 @@ export const CCLAW_AGENTS = [
94
94
  },
95
95
  {
96
96
  name: "security-reviewer",
97
- description: "PROACTIVE after auth, crypto, secrets, parsers, or sensitive data paths change. MUST BE USED when trust boundaries move, new external inputs arrive, or LLM/tool output influences privileged actions.",
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: "proactive",
100
+ activation: "mandatory",
101
101
  relatedStages: ["review", "design"],
102
102
  body: [
103
103
  "You are a **security vulnerability detection** specialist focused on practical exploitability.",