cclaw-cli 0.43.0 → 0.45.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 CHANGED
@@ -140,7 +140,7 @@ Plus harness-specific shims:
140
140
  `cclaw init` writes five keys, on purpose:
141
141
 
142
142
  ```yaml
143
- version: 0.43.0
143
+ version: 0.45.0
144
144
  flowVersion: 1.0.0
145
145
  harnesses:
146
146
  - codex
@@ -1,4 +1,4 @@
1
- import type { FlowStage } from "./types.js";
1
+ import { type FlowStage } from "./types.js";
2
2
  export interface LintFinding {
3
3
  section: string;
4
4
  required: boolean;
@@ -12,6 +12,36 @@ export interface LintResult {
12
12
  passed: boolean;
13
13
  findings: LintFinding[];
14
14
  }
15
+ export declare function extractMarkdownSectionBody(markdown: string, section: string): string | null;
16
+ export type LearningEntryType = "rule" | "pattern" | "lesson" | "compound";
17
+ export type LearningConfidence = "high" | "medium" | "low";
18
+ export type LearningUniversality = "project" | "personal" | "universal";
19
+ export type LearningMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
20
+ export interface LearningSeedEntry {
21
+ type: LearningEntryType;
22
+ trigger: string;
23
+ action: string;
24
+ confidence: LearningConfidence;
25
+ domain?: string | null;
26
+ stage?: FlowStage | null;
27
+ origin_stage?: FlowStage | null;
28
+ origin_feature?: string | null;
29
+ frequency?: number;
30
+ universality?: LearningUniversality;
31
+ maturity?: LearningMaturity;
32
+ created?: string;
33
+ first_seen_ts?: string;
34
+ last_seen_ts?: string;
35
+ project?: string | null;
36
+ }
37
+ export interface LearningsParseResult {
38
+ ok: boolean;
39
+ none: boolean;
40
+ entries: LearningSeedEntry[];
41
+ errors: string[];
42
+ details: string;
43
+ }
44
+ export declare function parseLearningsSection(sectionBody: string): LearningsParseResult;
15
45
  export declare function lintArtifact(projectRoot: string, stage: FlowStage): Promise<LintResult>;
16
46
  export declare function lintAllArtifacts(projectRoot: string): Promise<LintResult[]>;
17
47
  export declare function validateReviewArmy(projectRoot: string): Promise<{
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
4
  import { exists } from "./fs-utils.js";
5
5
  import { orderedStageSchemas, stageSchema } from "./content/stage-schema.js";
6
+ import { FLOW_STAGES } from "./types.js";
6
7
  async function resolveArtifactPath(projectRoot, fileName) {
7
8
  const relPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
8
9
  const absPath = path.join(projectRoot, relPath);
@@ -55,6 +56,9 @@ function sectionBodyByName(sections, section) {
55
56
  }
56
57
  return null;
57
58
  }
59
+ export function extractMarkdownSectionBody(markdown, section) {
60
+ return sectionBodyByName(extractH2Sections(markdown), section);
61
+ }
58
62
  function meaningfulLineCount(sectionBody) {
59
63
  return sectionBody
60
64
  .split(/\r?\n/)
@@ -179,6 +183,211 @@ function getMarkdownTableRows(sectionBody) {
179
183
  }
180
184
  return rows;
181
185
  }
186
+ const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
187
+ const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
188
+ const LEARNING_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
189
+ const LEARNING_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
190
+ const FLOW_STAGE_SET = new Set(FLOW_STAGES);
191
+ const LEARNING_ALLOWED_KEYS = new Set([
192
+ "type",
193
+ "trigger",
194
+ "action",
195
+ "confidence",
196
+ "domain",
197
+ "stage",
198
+ "origin_stage",
199
+ "origin_feature",
200
+ "frequency",
201
+ "universality",
202
+ "maturity",
203
+ "created",
204
+ "first_seen_ts",
205
+ "last_seen_ts",
206
+ "project"
207
+ ]);
208
+ function isIsoUtcTimestamp(value) {
209
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(value);
210
+ }
211
+ function isNullableString(value) {
212
+ return value === null || typeof value === "string";
213
+ }
214
+ function isNullableStage(value) {
215
+ return value === null || (typeof value === "string" && FLOW_STAGE_SET.has(value));
216
+ }
217
+ function parseLearningSeedEntry(raw, index) {
218
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
219
+ return { ok: false, error: `Learnings bullet #${index} must be a JSON object.` };
220
+ }
221
+ const obj = raw;
222
+ for (const key of Object.keys(obj)) {
223
+ if (!LEARNING_ALLOWED_KEYS.has(key)) {
224
+ return {
225
+ ok: false,
226
+ error: `Learnings bullet #${index} includes unknown key "${key}" (allowed keys mirror knowledge JSONL fields).`
227
+ };
228
+ }
229
+ }
230
+ const type = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
231
+ if (!LEARNING_TYPE_SET.has(type)) {
232
+ return {
233
+ ok: false,
234
+ error: `Learnings bullet #${index} must set type to one of: rule, pattern, lesson, compound.`
235
+ };
236
+ }
237
+ const trigger = typeof obj.trigger === "string" ? obj.trigger.trim() : "";
238
+ if (trigger.length === 0) {
239
+ return {
240
+ ok: false,
241
+ error: `Learnings bullet #${index} must include non-empty "trigger".`
242
+ };
243
+ }
244
+ const action = typeof obj.action === "string" ? obj.action.trim() : "";
245
+ if (action.length === 0) {
246
+ return {
247
+ ok: false,
248
+ error: `Learnings bullet #${index} must include non-empty "action".`
249
+ };
250
+ }
251
+ const confidence = typeof obj.confidence === "string" ? obj.confidence.toLowerCase() : "";
252
+ if (!LEARNING_CONFIDENCE_SET.has(confidence)) {
253
+ return {
254
+ ok: false,
255
+ error: `Learnings bullet #${index} must set confidence to high|medium|low.`
256
+ };
257
+ }
258
+ if (obj.domain !== undefined && !isNullableString(obj.domain)) {
259
+ return { ok: false, error: `Learnings bullet #${index} field "domain" must be string or null.` };
260
+ }
261
+ if (obj.stage !== undefined && !isNullableStage(obj.stage)) {
262
+ return {
263
+ ok: false,
264
+ error: `Learnings bullet #${index} field "stage" must be one of ${FLOW_STAGES.join(", ")} or null.`
265
+ };
266
+ }
267
+ if (obj.origin_stage !== undefined && !isNullableStage(obj.origin_stage)) {
268
+ return {
269
+ ok: false,
270
+ error: `Learnings bullet #${index} field "origin_stage" must be one of ${FLOW_STAGES.join(", ")} or null.`
271
+ };
272
+ }
273
+ if (obj.origin_feature !== undefined && !isNullableString(obj.origin_feature)) {
274
+ return { ok: false, error: `Learnings bullet #${index} field "origin_feature" must be string or null.` };
275
+ }
276
+ if (obj.project !== undefined && !isNullableString(obj.project)) {
277
+ return { ok: false, error: `Learnings bullet #${index} field "project" must be string or null.` };
278
+ }
279
+ if (obj.frequency !== undefined &&
280
+ (typeof obj.frequency !== "number" || !Number.isInteger(obj.frequency) || obj.frequency < 1)) {
281
+ return { ok: false, error: `Learnings bullet #${index} field "frequency" must be an integer >= 1.` };
282
+ }
283
+ if (obj.universality !== undefined &&
284
+ (typeof obj.universality !== "string" ||
285
+ !LEARNING_UNIVERSALITY_SET.has(obj.universality))) {
286
+ return {
287
+ ok: false,
288
+ error: `Learnings bullet #${index} field "universality" must be project|personal|universal.`
289
+ };
290
+ }
291
+ if (obj.maturity !== undefined &&
292
+ (typeof obj.maturity !== "string" || !LEARNING_MATURITY_SET.has(obj.maturity))) {
293
+ return {
294
+ ok: false,
295
+ error: `Learnings bullet #${index} field "maturity" must be raw|lifted-to-rule|lifted-to-enforcement.`
296
+ };
297
+ }
298
+ for (const timestampField of ["created", "first_seen_ts", "last_seen_ts"]) {
299
+ const value = obj[timestampField];
300
+ if (value === undefined)
301
+ continue;
302
+ if (typeof value !== "string" || !isIsoUtcTimestamp(value)) {
303
+ return {
304
+ ok: false,
305
+ error: `Learnings bullet #${index} field "${timestampField}" must be ISO UTC (YYYY-MM-DDTHH:MM:SSZ).`
306
+ };
307
+ }
308
+ }
309
+ return {
310
+ ok: true,
311
+ entry: {
312
+ ...obj,
313
+ type: type,
314
+ trigger,
315
+ action,
316
+ confidence: confidence
317
+ }
318
+ };
319
+ }
320
+ export function parseLearningsSection(sectionBody) {
321
+ const lines = sectionBody.split(/\r?\n/).map((line) => line.trim());
322
+ const nonEmpty = lines.filter((line) => line.length > 0);
323
+ const bullets = nonEmpty.filter((line) => /^-\s+\S+/u.test(line));
324
+ if (bullets.length === 0) {
325
+ return {
326
+ ok: false,
327
+ none: false,
328
+ entries: [],
329
+ errors: ["Learnings section must contain bullet entries."],
330
+ details: "Learnings section must contain bullet entries."
331
+ };
332
+ }
333
+ const nonBulletContent = nonEmpty.filter((line) => !/^-\s+\S+/u.test(line));
334
+ if (nonBulletContent.length > 0) {
335
+ return {
336
+ ok: false,
337
+ none: false,
338
+ entries: [],
339
+ errors: ["Learnings section must only contain bullet lines (one bullet per learning)."],
340
+ details: "Learnings section must only contain bullet lines (one bullet per learning)."
341
+ };
342
+ }
343
+ if (bullets.length === 1) {
344
+ const payload = bullets[0].replace(/^-\s+/u, "").trim();
345
+ if (/^none this stage\.?$/iu.test(payload)) {
346
+ return {
347
+ ok: true,
348
+ none: true,
349
+ entries: [],
350
+ errors: [],
351
+ details: "Learnings section explicitly marked as none."
352
+ };
353
+ }
354
+ }
355
+ const entries = [];
356
+ const errors = [];
357
+ for (let i = 0; i < bullets.length; i += 1) {
358
+ const payload = bullets[i].replace(/^-\s+/u, "").trim();
359
+ let parsed;
360
+ try {
361
+ parsed = JSON.parse(payload);
362
+ }
363
+ catch (err) {
364
+ errors.push(`Learnings bullet #${i + 1} must be valid JSON object or "None this stage.": ${err instanceof Error ? err.message : String(err)}`);
365
+ continue;
366
+ }
367
+ const parsedEntry = parseLearningSeedEntry(parsed, i + 1);
368
+ if (!parsedEntry.ok || !parsedEntry.entry) {
369
+ errors.push(parsedEntry.error ?? `Learnings bullet #${i + 1} is invalid.`);
370
+ continue;
371
+ }
372
+ entries.push(parsedEntry.entry);
373
+ }
374
+ if (errors.length > 0) {
375
+ return {
376
+ ok: false,
377
+ none: false,
378
+ entries: [],
379
+ errors,
380
+ details: errors.join(" | ")
381
+ };
382
+ }
383
+ return {
384
+ ok: true,
385
+ none: false,
386
+ entries,
387
+ errors: [],
388
+ details: `Parsed ${entries.length} learning bullet(s) as knowledge-compatible JSON entries.`
389
+ };
390
+ }
182
391
  function lineContainsVagueAdjective(text) {
183
392
  const lower = text.toLowerCase();
184
393
  for (const adjective of VAGUE_AC_ADJECTIVES) {
@@ -453,6 +662,27 @@ export async function lintArtifact(projectRoot, stage) {
453
662
  : validation.details
454
663
  });
455
664
  }
665
+ const learningsBody = sectionBodyByName(sections, "Learnings");
666
+ const requireLearnings = parsedFrontmatter.hasFrontmatter;
667
+ if (learningsBody === null) {
668
+ findings.push({
669
+ section: "Learnings",
670
+ required: requireLearnings,
671
+ rule: "Required for schema-v1 artifacts: include `## Learnings` with bullets of strict JSON objects compatible with knowledge.jsonl schema, or a single `- None this stage.` sentinel.",
672
+ found: false,
673
+ details: "No ## heading matching required section \"Learnings\"."
674
+ });
675
+ }
676
+ else {
677
+ const learnings = parseLearningsSection(learningsBody);
678
+ findings.push({
679
+ section: "Learnings",
680
+ required: requireLearnings,
681
+ rule: "`## Learnings` must contain either a single `- None this stage.` bullet or JSON bullets compatible with knowledge.jsonl fields (type/trigger/action/confidence required).",
682
+ found: learnings.ok,
683
+ details: learnings.details
684
+ });
685
+ }
456
686
  if (stage === "plan") {
457
687
  const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
458
688
  headingPresent(sections, "No-Placeholder Scan") ||
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import type { FlowTrack, HarnessId } from "./types.js";
3
3
  import type { EvalMode } from "./eval/types.js";
4
- type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive" | "eval";
4
+ type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive" | "eval" | "internal";
5
5
  interface ParsedArgs {
6
6
  command?: CommandName;
7
7
  harnesses?: HarnessId[];
@@ -33,6 +33,8 @@ interface ParsedArgs {
33
33
  evalArgs?: string[];
34
34
  evalBackground?: boolean;
35
35
  evalCompareModel?: string;
36
+ /** Hidden plumbing command (`cclaw internal ...`) arguments. */
37
+ internalArgs?: string[];
36
38
  showHelp?: boolean;
37
39
  showVersion?: boolean;
38
40
  }
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import { formatDiffMarkdown, runEvalDiff } from "./eval/diff.js";
24
24
  import { ensureRunDir, generateRunId, isRunAlive, listRuns, readRunStatus, resolveRunId, runLogPath, writeRunStatus } from "./eval/runs.js";
25
25
  import { parseModeInput } from "./eval/mode.js";
26
26
  import { FLOW_STAGES } from "./types.js";
27
+ import { runInternalCommand } from "./internal/advance-stage.js";
27
28
  const INSTALLER_COMMANDS = [
28
29
  "init",
29
30
  "sync",
@@ -31,7 +32,8 @@ const INSTALLER_COMMANDS = [
31
32
  "upgrade",
32
33
  "uninstall",
33
34
  "archive",
34
- "eval"
35
+ "eval",
36
+ "internal"
35
37
  ];
36
38
  export function usage() {
37
39
  return `cclaw - installer-first flow toolkit
@@ -418,6 +420,12 @@ function parseArgs(argv) {
418
420
  parsed.command = INSTALLER_COMMANDS.includes(commandRaw)
419
421
  ? commandRaw
420
422
  : undefined;
423
+ // Hidden maintainer surface for runtime guards/helpers. Keep raw positional
424
+ // args untouched so subcommand-level parsing can evolve independently.
425
+ if (parsed.command === "internal") {
426
+ parsed.internalArgs = [...rest];
427
+ return parsed;
428
+ }
421
429
  // For `eval`, the next non-flag argument is an optional subcommand. Any
422
430
  // subsequent non-flag tokens are captured as evalArgs (consumed by the
423
431
  // subcommand handler). This preserves backwards compat: callers that run
@@ -796,6 +804,9 @@ async function runCommand(parsed, ctx) {
796
804
  if (!command) {
797
805
  return printNoArgsHint(ctx);
798
806
  }
807
+ if (command === "internal") {
808
+ return runInternalCommand(ctx.cwd, parsed.internalArgs ?? [], ctx);
809
+ }
799
810
  if (command === "init") {
800
811
  const resolved = await resolveInitInputs(parsed, ctx);
801
812
  const effectiveTrack = resolved.track;
@@ -232,10 +232,12 @@ Codex CLI has a different shape from Claude/Cursor:
232
232
  - **Tool interception is Bash-only.** Codex's \`PreToolUse\` and
233
233
  \`PostToolUse\` events only fire for the \`Bash\` tool. \`Write\`,
234
234
  \`Edit\`, \`WebSearch\`, and MCP tool calls are **not** gated by hooks.
235
- cclaw partially compensates by also wiring \`UserPromptSubmit\` to
236
- \`prompt-guard.sh\` so the stage routing check fires before the turn
237
- executes, but workflow-guard (TDD red-first, artifact presence) only
238
- fires on Bash turns. See the hook coverage matrix below.
235
+ cclaw partially compensates by wiring \`UserPromptSubmit\` to both
236
+ \`prompt-guard.sh\` and a non-blocking
237
+ \`cclaw internal verify-current-state --quiet\` nudge that emits
238
+ unmet-delegation / missing-evidence warnings before the turn executes.
239
+ This is still a nudge, not a hard block: workflow-guard (TDD red-first,
240
+ artifact presence) only fires on Bash turns. See the hook coverage matrix below.
239
241
  - **Legacy paths.** \`.codex/commands/*\` was never consumed by Codex and
240
242
  is removed on every \`cclaw sync\`. The v0.39.x \`.agents/skills/cclaw-cc*/\`
241
243
  layout is replaced by \`.agents/skills/cc*/\` and the old folders are
@@ -289,6 +291,8 @@ disabled in v0.33 and remains off.
289
291
  - \`/use cc\` — open the \`/cc\` skill and pick a track.
290
292
  - \`/use cc-next\` — advance the flow one stage.
291
293
  - \`/use cc-ops\` — compound / archive / rewind.
294
+ - \`bash .cclaw/hooks/stage-complete.sh <stage>\` — canonical stage closeout helper;
295
+ validates delegations + gate evidence before mutating \`flow-state.json\`.
292
296
  - Typing \`/cc …\` or \`/cc-next …\` in plain text also works: Codex
293
297
  matches the skill descriptions (which spell out these tokens) and
294
298
  auto-loads the right skill body.
@@ -316,6 +320,7 @@ continue to work regardless.
316
320
  |-------------|---------------|----------|
317
321
  | SessionStart rehydration | \`SessionStart\` matcher \`startup|resume\` → \`session-start.sh\` | Full. |
318
322
  | PreToolUse prompt-guard | \`PreToolUse\` matcher \`Bash\` + \`UserPromptSubmit\` → \`prompt-guard.sh\` | Bash tool calls are gated inline; \`UserPromptSubmit\` catches prompts before any tool fires, so non-Bash writes (\`Write\`/\`Edit\`) are still prompt-guarded at the turn boundary. |
323
+ | UserPromptSubmit state nudge | \`UserPromptSubmit\` → \`cclaw internal verify-current-state --quiet\` | Non-blocking warning only. Prints unmet mandatory delegation / gate-evidence counts before the turn; cannot block non-Bash \`Write\`/\`Edit\`. |
319
324
  | PreToolUse workflow-guard | \`PreToolUse\` matcher \`Bash\` → \`workflow-guard.sh\` | Bash-only. For \`Write\`/\`Edit\` calls the agent performs the TDD-order / artifact check in-turn (see the stage skill). |
320
325
  | PostToolUse context-monitor | \`PostToolUse\` matcher \`Bash\` → \`context-monitor.sh\` | Bash-only. Other tool calls get context-monitored at end-of-turn via \`.cclaw/references/protocols/ethos.md\`. |
321
326
  | Stop checkpoint | \`Stop\` → \`stop-checkpoint.sh\` | Full. |
@@ -68,6 +68,11 @@ ${hookRows}
68
68
  - \`tier1\`: full native delegation + structured asks + full hook surface.
69
69
  - \`tier2\`: usable flow with capability gaps; mandatory delegation can require waivers.
70
70
  - \`tier3\`: manual-only fallback; no native automation guarantees.
71
+ - Codex-specific ceiling: \`PreToolUse\` can only intercept \`Bash\`. Direct
72
+ \`Write\`/\`Edit\` to \`.cclaw/state/flow-state.json\` cannot be hard-blocked
73
+ at hook level, so the canonical path is
74
+ \`bash .cclaw/hooks/stage-complete.sh <stage>\` plus the non-blocking
75
+ \`UserPromptSubmit\` state nudge.
71
76
 
72
77
  ## Shared command contract
73
78
 
@@ -11,6 +11,7 @@ export interface HookRuntimeOptions {
11
11
  export declare const RUNTIME_SHELL_DETECT_ROOT = "HARNESS=\"codex\"\nif [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\nelif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\nelif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\nfi\n\nROOT=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\ndone\nif [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\nfi";
12
12
  export declare function sessionStartScript(_options?: HookRuntimeOptions): string;
13
13
  export declare function stopCheckpointScript(): string;
14
+ export declare function stageCompleteScript(): string;
14
15
  export declare function preCompactScript(): string;
15
16
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
16
17
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
@@ -769,6 +769,35 @@ case "$HARNESS" in
769
769
  esac
770
770
  `;
771
771
  }
772
+ export function stageCompleteScript() {
773
+ return `#!/usr/bin/env bash
774
+ # cclaw stage-complete helper — generated by cclaw sync
775
+ # Canonical helper for stage closeout: delegates validation + flow-state
776
+ # mutation to \`cclaw internal advance-stage\`.
777
+ set -euo pipefail
778
+
779
+ ${DETECT_ROOT}
780
+
781
+ if [ "$#" -lt 1 ]; then
782
+ printf 'Usage: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n' >&2
783
+ exit 1
784
+ fi
785
+
786
+ if [ ! -d "$ROOT/${RUNTIME_ROOT}" ]; then
787
+ printf '[cclaw] stage-complete: runtime root not found at %s\\n' "$ROOT/${RUNTIME_ROOT}" >&2
788
+ exit 1
789
+ fi
790
+
791
+ STAGE="$1"
792
+ shift || true
793
+
794
+ if command -v cclaw >/dev/null 2>&1; then
795
+ exec cclaw internal advance-stage "$STAGE" "$@"
796
+ fi
797
+
798
+ exec npx -y cclaw-cli internal advance-stage "$STAGE" "$@"
799
+ `;
800
+ }
772
801
  export function preCompactScript() {
773
802
  return `#!/usr/bin/env bash
774
803
  # cclaw pre-compact hook — generated by cclaw sync
@@ -1109,14 +1138,15 @@ export default function cclawPlugin(ctx) {
1109
1138
  const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
1110
1139
  const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
1111
1140
  try {
1112
- spawnSync("bash", [scriptPath], {
1141
+ const result = spawnSync("bash", [scriptPath], {
1113
1142
  cwd: root,
1114
1143
  timeout: 20000,
1115
1144
  stdio: ["pipe", "ignore", "ignore"],
1116
1145
  input
1117
1146
  });
1147
+ return typeof result.status === "number" ? result.status === 0 : false;
1118
1148
  } catch {
1119
- // advisory-only runtime path
1149
+ return false;
1120
1150
  }
1121
1151
  }
1122
1152
 
@@ -1167,8 +1197,13 @@ export default function cclawPlugin(ctx) {
1167
1197
  }
1168
1198
  if (eventType === "tool.execute.before") {
1169
1199
  const toolPayload = normalizeToolPayload(eventData, undefined);
1170
- await runHookScript("prompt-guard.sh", toolPayload);
1171
- await runHookScript("workflow-guard.sh", toolPayload);
1200
+ const promptOk = await runHookScript("prompt-guard.sh", toolPayload);
1201
+ const workflowOk = await runHookScript("workflow-guard.sh", toolPayload);
1202
+ if (!promptOk || !workflowOk) {
1203
+ throw new Error(
1204
+ "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
1205
+ );
1206
+ }
1172
1207
  }
1173
1208
  if (eventType === "tool.execute.after") {
1174
1209
  const toolPayload = normalizeToolPayload(eventData, undefined);
@@ -1177,8 +1212,13 @@ export default function cclawPlugin(ctx) {
1177
1212
  },
1178
1213
  "tool.execute.before": async (input, output) => {
1179
1214
  const payload = normalizeToolPayload(input, output);
1180
- await runHookScript("prompt-guard.sh", payload);
1181
- await runHookScript("workflow-guard.sh", payload);
1215
+ const promptOk = await runHookScript("prompt-guard.sh", payload);
1216
+ const workflowOk = await runHookScript("workflow-guard.sh", payload);
1217
+ if (!promptOk || !workflowOk) {
1218
+ throw new Error(
1219
+ "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
1220
+ );
1221
+ }
1182
1222
  },
1183
1223
  "tool.execute.after": async (input, output) => {
1184
1224
  const payload = normalizeToolPayload(input, output);
@@ -49,6 +49,20 @@ Use the store to keep durable knowledge that should survive sessions:
49
49
  - **lesson**: non-obvious outcome from a failure or trade-off.
50
50
  - **compound**: post-ship insight about how to make the *next* feature faster (process accelerator, not domain rule).
51
51
 
52
+ ## Continuous capture (stage closeout path)
53
+
54
+ Knowledge capture is now stage-native:
55
+ - Each stage artifact has a \`## Learnings\` section.
56
+ - Allowed payloads:
57
+ - \`- None this stage.\` (explicit no-op)
58
+ - JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (optional keys may mirror the full JSONL schema fields).
59
+ - During \`bash .cclaw/hooks/stage-complete.sh <stage>\`, cclaw:
60
+ 1. validates \`## Learnings\`,
61
+ 2. appends deduped entries to \`${KNOWLEDGE_PATH}\`,
62
+ 3. writes a harvest marker into the artifact.
63
+
64
+ \`/cc-learn\` remains the manual/query surface (search, backfill, curation).
65
+
52
66
  ## HARD-GATE
53
67
 
54
68
  Under \`/cc-learn\`, only modify \`${KNOWLEDGE_PATH}\`, \`${KNOWLEDGE_ARCHIVE_PATH}\`,
@@ -113,6 +127,7 @@ Rules:
113
127
  - Ask for required user-facing fields in order: \`type\`, \`trigger\`, \`action\`, \`confidence\`, \`domain\`, \`stage\`, \`universality\`, \`project\`.
114
128
  - \`confidence\` must be one of \`high\`, \`medium\`, \`low\`. Default to \`medium\` if the user declines to set it.
115
129
  - \`domain\`, \`stage\`, and \`project\` may be explicitly \`null\`.
130
+ - Prefer stage-native \`## Learnings\` capture for new flow work; use \`add\` mainly for backfilling historical lessons or ad-hoc entries outside a stage closeout.
116
131
  - \`origin_stage\` defaults to \`stage\`; \`origin_feature\` defaults to active feature (or \`null\` if unknown).
117
132
  - \`frequency\` starts at \`1\`.
118
133
  - \`maturity\` starts at \`raw\`.
@@ -136,6 +151,11 @@ Manage the project knowledge store. One canonical file, strict JSONL:
136
151
  - \`${KNOWLEDGE_PATH}\` — append-only JSONL, one entry per line.
137
152
  - \`${KNOWLEDGE_ARCHIVE_PATH}\` — soft-archive target written only by curate.
138
153
 
154
+ Stage-native pipeline:
155
+ - During \`stage-complete.sh\`, cclaw harvests \`## Learnings\` from the current
156
+ stage artifact into \`${KNOWLEDGE_PATH}\` automatically.
157
+ - Use \`/cc-learn\` for query, backfill, and curation workflows.
158
+
139
159
  ## HARD-GATE
140
160
 
141
161
  Do not edit source code from this command. Only operate on \`${KNOWLEDGE_PATH}\`,
@@ -44,13 +44,14 @@ This is the only progression command the user needs to drive the entire flow. St
44
44
  5. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
45
45
  6. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
46
46
  7. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
47
- 8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if the agent is **completed** or **waived**.
47
+ 8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
48
+ 9. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
48
49
 
49
50
  ### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
50
51
 
51
52
  → Load **\`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`** and **\`${RUNTIME_ROOT}/commands/<currentStage>.md\`** for the current stage.
52
53
  → Execute that stage's protocol. The stage skill handles the full interaction including STOP points and gate tracking.
53
- When the stage completes, the Stage Completion Protocol in the skill updates \`flow-state.json\` automatically.
54
+ Stage completion must use \`bash .cclaw/hooks/stage-complete.sh <currentStage>\` (canonical), which validates delegations + gate evidence before mutating \`flow-state.json\`.
54
55
 
55
56
  ### Path B: Current stage IS complete (all gates passed, all delegations satisfied)
56
57
 
@@ -152,6 +153,8 @@ For each gate id in \`requiredGates\` for \`currentStage\`:
152
153
  - **Unmet** otherwise.
153
154
 
154
155
  Check \`mandatoryDelegations\` via **\`${delegationPath}\`** — satisfied only if **completed** or **waived**.
156
+ If a mandatory delegation is missing and no waiver exists, **STOP** and ask:
157
+ (A) dispatch now, (B) waive with rationale, (C) cancel stage advance.
155
158
 
156
159
  ### Step 3: Act
157
160
 
@@ -161,7 +164,7 @@ Load the current stage's skill and command contract:
161
164
  - \`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`
162
165
  - \`${RUNTIME_ROOT}/commands/<currentStage>.md\`
163
166
 
164
- Execute the stage protocol. The stage skill handles interaction, STOP points, gate tracking, and the Stage Completion Protocol (updates \`flow-state.json\` when done).
167
+ Execute the stage protocol. The stage skill handles interaction, STOP points, gate tracking, and stage completion via \`bash .cclaw/hooks/stage-complete.sh <stage>\` (canonical flow-state mutation path).
165
168
 
166
169
  **Path B — stage IS complete (all gates met, all delegations done):**
167
170
 
@@ -10,6 +10,7 @@ export interface PromptGuardOptions {
10
10
  }
11
11
  export declare function promptGuardScript(options?: PromptGuardOptions): string;
12
12
  export interface WorkflowGuardOptions {
13
+ workflowGuardMode?: "advisory" | "strict";
13
14
  tddEnforcementMode?: "advisory" | "strict";
14
15
  tddTestGlobs?: string[];
15
16
  }