cclaw-cli 0.48.9 → 0.48.10

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.
@@ -425,16 +425,16 @@ export default function cclawPlugin(ctx) {
425
425
  await refreshBootstrapCache(true);
426
426
  }
427
427
  if (eventType === "session.compacted") {
428
- await runHookScript("pre-compact.sh", eventData ?? {});
428
+ await runHookScript("pre-compact", eventData ?? {});
429
429
  }
430
430
  if (eventType === "session.idle") {
431
- await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
431
+ await runHookScript("stop-checkpoint", { loop_count: 0 });
432
432
  }
433
433
  },
434
434
  "tool.execute.before": async (input, output) => {
435
435
  const payload = normalizeToolPayload(input, output);
436
- const promptOk = await runHookScript("prompt-guard.sh", payload);
437
- const workflowOk = await runHookScript("workflow-guard.sh", payload);
436
+ const promptOk = await runHookScript("prompt-guard", payload);
437
+ const workflowOk = await runHookScript("workflow-guard", payload);
438
438
  if (!promptOk || !workflowOk) {
439
439
  throw new Error(
440
440
  "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
@@ -443,7 +443,7 @@ export default function cclawPlugin(ctx) {
443
443
  },
444
444
  "tool.execute.after": async (input, output) => {
445
445
  const payload = normalizeToolPayload(input, output);
446
- await runHookScript("context-monitor.sh", payload);
446
+ await runHookScript("context-monitor", payload);
447
447
  void refreshBootstrapCache(false);
448
448
  },
449
449
  "experimental.chat.system.transform": (payload) => {
@@ -101,19 +101,15 @@ Shared closeout sequence applied by every stage skill.
101
101
  1. Verify mandatory delegations are completed or explicitly waived.
102
102
  2. Persist stage artifact under \`.cclaw/artifacts/\`.
103
103
  3. Use the canonical helper:
104
- - \`bash .cclaw/hooks/stage-complete.sh <stage>\`
104
+ - \`node .cclaw/hooks/stage-complete.mjs <stage>\`
105
105
  - helper responsibilities: validate mandatory delegations, validate
106
106
  current-stage gate evidence/artifact lint, update
107
107
  \`stageGateCatalog\` + \`guardEvidence\`, and advance \`currentStage\`.
108
- 4. Legacy fallback (only when helper is unavailable): manually edit
109
- \`.cclaw/state/flow-state.json\` to mark passed gates, clear resolved
110
- blocked gates, and update \`guardEvidence\`. This path is legacy and
111
- intentionally noisy in workflow guards.
112
- 5. Run \`npx cclaw doctor\` and resolve failures.
113
- 6. **Capture through-flow learnings** — see the policy below. Knowledge
108
+ 4. Run \`npx cclaw doctor\` and resolve failures.
109
+ 5. **Capture through-flow learnings** see the policy below. Knowledge
114
110
  accrues continuously across stages, not just at retro.
115
- 7. Notify user with stage completion and next action (\`/cc-next\`).
116
- 8. Stop; do not auto-run the next stage unless user asks.
111
+ 6. Notify user with stage completion and next action (\`/cc-next\`).
112
+ 7. Stop; do not auto-run the next stage unless user asks.
117
113
 
118
114
  ## Through-flow knowledge capture
119
115
 
@@ -222,7 +222,7 @@ function completionParametersBlock(schema, track) {
222
222
  - \`gates\`: ${gateList}
223
223
  - \`artifact\`: \`${RUNTIME_ROOT}/artifacts/${schema.artifactFile}\`
224
224
  - \`mandatory delegations\`: ${mandatory}
225
- - \`completion helper\`: \`bash .cclaw/hooks/stage-complete.sh ${schema.stage}\`
225
+ - \`completion helper\`: \`node .cclaw/hooks/stage-complete.mjs ${schema.stage}\`
226
226
  - Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
227
227
  - Record mandatory delegation completion/waiver in \`${RUNTIME_ROOT}/state/delegation-log.json\` with rationale as needed.
228
228
  - Use the completion helper instead of raw \`flow-state.json\` edits (legacy direct edits trigger workflow-guard warnings or strict-mode blocks).
@@ -63,7 +63,7 @@ Before closeout, fill the artifact \`## Learnings\` section (do not write
63
63
  - \`- None this stage.\` when nothing reusable emerged.
64
64
  - Or 1-3 JSON bullets with required keys \`type\`, \`trigger\`, \`action\`,
65
65
  \`confidence\` (optional fields may mirror knowledge.jsonl schema keys).
66
- During \`bash .cclaw/hooks/stage-complete.sh <stage>\`, cclaw validates those
66
+ During \`node .cclaw/hooks/stage-complete.mjs <stage>\`, cclaw validates those
67
67
  bullets, appends unique entries to \`.cclaw/knowledge.jsonl\`, and stamps a
68
68
  harvest marker in the artifact.
69
69
 
@@ -43,7 +43,7 @@ export const DESIGN = {
43
43
  "If a section has no issues, say 'No issues found' and move on.",
44
44
  "Do not skip failure-mode mapping.",
45
45
  "For design baseline approval: present the full baseline. **STOP.** Do NOT proceed until user explicitly approves the design.",
46
- "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh design` (do not hand-edit `.cclaw/state/flow-state.json`).",
46
+ "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `node .cclaw/hooks/stage-complete.mjs design` (do not hand-edit `.cclaw/state/flow-state.json`).",
47
47
  "Take a firm position on every recommendation. Do NOT hedge with 'it depends' or 'you could do either'. State your opinion, then justify it.",
48
48
  "Use pushback patterns for weak framing: if the user says 'it's just a small change', respond with 'small changes to shared interfaces have outsized blast radius — let's map it'. If 'we'll refactor later', respond with 'later never comes — show me the refactor ticket or do it now'.",
49
49
  "When the user's proposed architecture is suboptimal, say so directly. Offer the alternative with concrete trade-offs, do not bury criticism in praise.",
@@ -29,7 +29,7 @@ export const PLAN = {
29
29
  "Map scope Locked Decisions — every D-XX from scope is referenced by at least one plan task (or explicitly marked deferred with reason).",
30
30
  "Run anti-placeholder + anti-scope-reduction scans — block `TODO/TBD/...` and phrasing like `v1`, `for now`, `later` for locked boundaries.",
31
31
  "Define checkpoints — mark points where progress should be validated before continuing.",
32
- "WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then close the stage with `bash .cclaw/hooks/stage-complete.sh plan` and tell user to run `/cc-next`."
32
+ "WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then close the stage with `node .cclaw/hooks/stage-complete.mjs plan` and tell user to run `/cc-next`."
33
33
  ],
34
34
  interactionProtocol: [
35
35
  "Plan in read-only mode relative to implementation.",
@@ -39,7 +39,7 @@ export const PLAN = {
39
39
  "Preserve locked scope boundaries: no silent scope reduction language in task rows.",
40
40
  "Enforce WAIT_FOR_CONFIRM: present the plan summary with options (A) Approve / (B) Revise / (C) Reject.",
41
41
  "**STOP.** Do NOT proceed until user explicitly approves.",
42
- "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh plan` and tell the user to run `/cc-next`."
42
+ "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `node .cclaw/hooks/stage-complete.mjs plan` and tell the user to run `/cc-next`."
43
43
  ],
44
44
  process: [
45
45
  "Build dependency graph and ordered slices.",
@@ -42,7 +42,7 @@ export const SCOPE = {
42
42
  "Once the user accepts or rejects a recommendation, commit fully. Do not re-argue.",
43
43
  "Produce a clean scope summary after all issues are resolved.",
44
44
  "**STOP.** Wait for explicit user approval of scope contract before advancing to design.",
45
- "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh scope` (do not hand-edit `.cclaw/state/flow-state.json`)."
45
+ "**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `node .cclaw/hooks/stage-complete.mjs scope` (do not hand-edit `.cclaw/state/flow-state.json`)."
46
46
  ],
47
47
  process: [
48
48
  "Run premise challenge and existing-solution leverage check.",
package/dist/doctor.js CHANGED
@@ -484,11 +484,11 @@ export async function doctorChecks(projectRoot, options = {}) {
484
484
  : `no deprecated "tddTestGlobs" key detected in ${RUNTIME_ROOT}/config.yaml`
485
485
  });
486
486
  const expectedMode = parsedConfig.promptGuardMode === "strict" ? "strict" : "advisory";
487
- const promptGuardPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "prompt-guard.sh");
487
+ const promptGuardPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
488
488
  let promptGuardModeOk = false;
489
489
  if (await exists(promptGuardPath)) {
490
490
  const promptGuardContent = await fs.readFile(promptGuardPath, "utf8");
491
- promptGuardModeOk = promptGuardContent.includes(`PROMPT_GUARD_MODE="${expectedMode}"`);
491
+ promptGuardModeOk = promptGuardContent.includes(`const DEFAULT_PROMPT_GUARD_MODE = "${expectedMode}"`);
492
492
  }
493
493
  checks.push({
494
494
  name: "hook:prompt_guard:mode",
@@ -496,13 +496,13 @@ export async function doctorChecks(projectRoot, options = {}) {
496
496
  details: `${promptGuardPath} must match promptGuardMode=${expectedMode}`
497
497
  });
498
498
  if (parsedConfig.gitHookGuards === true) {
499
- const runtimePreCommit = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-commit.sh");
500
- const runtimePrePush = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-push.sh");
499
+ const runtimePreCommit = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-commit.mjs");
500
+ const runtimePrePush = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-push.mjs");
501
501
  const runtimeScriptsOk = (await exists(runtimePreCommit)) && (await exists(runtimePrePush));
502
502
  checks.push({
503
503
  name: "git_hooks:managed:runtime_scripts",
504
504
  ok: runtimeScriptsOk,
505
- details: `${RUNTIME_ROOT}/hooks/git/pre-commit.sh and pre-push.sh must exist when gitHookGuards=true`
505
+ details: `${RUNTIME_ROOT}/hooks/git/pre-commit.mjs and pre-push.mjs must exist when gitHookGuards=true`
506
506
  });
507
507
  const gitHooksDir = await resolveGitHooksDir(projectRoot);
508
508
  if (!gitHooksDir) {
@@ -673,15 +673,9 @@ export async function doctorChecks(projectRoot, options = {}) {
673
673
  }
674
674
  // Hook scripts
675
675
  for (const script of [
676
- "_lib.sh",
677
- "session-start.sh",
678
- "stop-checkpoint.sh",
679
- "run-hook.cmd",
680
- "stage-complete.sh",
681
- "pre-compact.sh",
682
- "prompt-guard.sh",
683
- "workflow-guard.sh",
684
- "context-monitor.sh"
676
+ "run-hook.mjs",
677
+ "stage-complete.mjs",
678
+ "opencode-plugin.mjs"
685
679
  ]) {
686
680
  const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
687
681
  const scriptExists = await exists(scriptPath);
@@ -699,10 +693,13 @@ export async function doctorChecks(projectRoot, options = {}) {
699
693
  catch {
700
694
  executable = false;
701
695
  }
696
+ const executableCheckOk = process.platform === "win32" ? true : executable;
702
697
  checks.push({
703
698
  name: `hook:script:${script}:executable`,
704
- ok: executable,
705
- details: `${scriptPath} must be executable`
699
+ ok: executableCheckOk,
700
+ details: process.platform === "win32"
701
+ ? `${scriptPath} executable-bit check skipped on Windows`
702
+ : `${scriptPath} must be executable`
706
703
  });
707
704
  }
708
705
  }
@@ -776,11 +773,11 @@ export async function doctorChecks(projectRoot, options = {}) {
776
773
  const preCommands = collectHookCommands(hooks.PreToolUse);
777
774
  const postCommands = collectHookCommands(hooks.PostToolUse);
778
775
  const stopCommands = collectHookCommands(hooks.Stop);
779
- const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
780
- preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
781
- preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
782
- postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
783
- stopCommands.some((cmd) => cmd.includes("stop-checkpoint.sh"));
776
+ const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start")) &&
777
+ preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
778
+ preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
779
+ postCommands.some((cmd) => cmd.includes("context-monitor")) &&
780
+ stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
784
781
  checks.push({
785
782
  name: "hook:wiring:claude",
786
783
  ok: wiringOk,
@@ -809,11 +806,11 @@ export async function doctorChecks(projectRoot, options = {}) {
809
806
  const preCommands = collectHookCommands(hooks.preToolUse);
810
807
  const postCommands = collectHookCommands(hooks.postToolUse);
811
808
  const stopCommands = collectHookCommands(hooks.stop);
812
- const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start.sh")) &&
813
- preCommands.some((cmd) => cmd.includes("prompt-guard.sh")) &&
814
- preCommands.some((cmd) => cmd.includes("workflow-guard.sh")) &&
815
- postCommands.some((cmd) => cmd.includes("context-monitor.sh")) &&
816
- stopCommands.some((cmd) => cmd.includes("stop-checkpoint.sh"));
809
+ const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start")) &&
810
+ preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
811
+ preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
812
+ postCommands.some((cmd) => cmd.includes("context-monitor")) &&
813
+ stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
817
814
  checks.push({
818
815
  name: "hook:wiring:cursor",
819
816
  ok: wiringOk,
@@ -876,14 +873,14 @@ export async function doctorChecks(projectRoot, options = {}) {
876
873
  const codexPreCmds = collectHookCommands(codexHooks.PreToolUse);
877
874
  const codexPostCmds = collectHookCommands(codexHooks.PostToolUse);
878
875
  const codexStopCmds = collectHookCommands(codexHooks.Stop);
879
- const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start.sh")) &&
880
- codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard.sh")) &&
881
- codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard.sh")) &&
876
+ const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start")) &&
877
+ codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard")) &&
878
+ codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard")) &&
882
879
  codexUserPromptCmds.some((cmd) => cmd.includes("verify-current-state")) &&
883
- codexPreCmds.some((cmd) => cmd.includes("prompt-guard.sh")) &&
884
- codexPreCmds.some((cmd) => cmd.includes("workflow-guard.sh")) &&
885
- codexPostCmds.some((cmd) => cmd.includes("context-monitor.sh")) &&
886
- codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint.sh"));
880
+ codexPreCmds.some((cmd) => cmd.includes("prompt-guard")) &&
881
+ codexPreCmds.some((cmd) => cmd.includes("workflow-guard")) &&
882
+ codexPostCmds.some((cmd) => cmd.includes("context-monitor")) &&
883
+ codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint"));
887
884
  checks.push({
888
885
  name: "hook:wiring:codex",
889
886
  ok: codexWiringOk,
@@ -963,10 +960,10 @@ export async function doctorChecks(projectRoot, options = {}) {
963
960
  content.includes("event: async") &&
964
961
  content.includes('"tool.execute.before"') &&
965
962
  content.includes('"tool.execute.after"') &&
966
- content.includes("prompt-guard.sh") &&
967
- content.includes("workflow-guard.sh") &&
968
- content.includes("context-monitor.sh") &&
969
- content.includes("pre-compact.sh") &&
963
+ content.includes("prompt-guard") &&
964
+ content.includes("workflow-guard") &&
965
+ content.includes("context-monitor") &&
966
+ content.includes("pre-compact") &&
970
967
  content.includes('"session.created"') &&
971
968
  content.includes('"session.idle"') &&
972
969
  content.includes('"session.resumed"') &&
@@ -981,7 +978,7 @@ export async function doctorChecks(projectRoot, options = {}) {
981
978
  content.includes('"tool.execute.after": async');
982
979
  precompactHookOk =
983
980
  content.includes('eventType === "session.compacted"') &&
984
- content.includes('runHookScript("pre-compact.sh"');
981
+ content.includes('runHookScript("pre-compact"');
985
982
  }
986
983
  checks.push({
987
984
  name: "lifecycle:opencode:rehydration_events",
@@ -996,7 +993,7 @@ export async function doctorChecks(projectRoot, options = {}) {
996
993
  checks.push({
997
994
  name: "hook:opencode:precompact_digest",
998
995
  ok: precompactHookOk,
999
- details: `${file} must run pre-compact.sh on session.compacted before bootstrap refresh.`
996
+ details: `${file} must run pre-compact on session.compacted before bootstrap refresh.`
1000
997
  });
1001
998
  const runtimeShape = await opencodePluginRuntimeShapeCheck(projectRoot);
1002
999
  checks.push({
@@ -1011,34 +1008,48 @@ export async function doctorChecks(projectRoot, options = {}) {
1011
1008
  details: registration.details
1012
1009
  });
1013
1010
  }
1014
- const hasBash = await commandAvailable("bash");
1015
1011
  const hasNode = await commandAvailable("node");
1016
1012
  const hasPython = await commandAvailable("python3");
1017
1013
  const hasJq = await commandAvailable("jq");
1018
- checks.push({
1019
- name: "capability:required:bash",
1020
- ok: hasBash,
1021
- details: "bash is required to execute cclaw hook scripts"
1022
- });
1023
1014
  checks.push({
1024
1015
  name: "capability:required:node",
1025
1016
  ok: hasNode,
1026
1017
  details: "node is required for cclaw runtime scripts and CLI wiring"
1027
1018
  });
1028
- checks.push({
1029
- name: "capability:runtime:json_parser",
1030
- ok: hasPython || hasJq,
1031
- details: "at least one of python3 or jq must be available for hook JSON parsing fallbacks"
1032
- });
1033
1019
  checks.push({
1034
1020
  name: "warning:capability:jq",
1035
1021
  ok: true,
1036
- details: hasJq ? "jq available" : "warning: jq not found, python/node fallbacks will be used"
1022
+ details: hasJq
1023
+ ? "jq available (optional)"
1024
+ : "warning: jq not found; Node hook runtime no longer depends on jq"
1037
1025
  });
1038
1026
  checks.push({
1039
1027
  name: "warning:capability:python3",
1040
1028
  ok: true,
1041
- details: hasPython ? "python3 available" : "warning: python3 not found, jq/node paths must stay healthy"
1029
+ details: hasPython
1030
+ ? "python3 available (optional)"
1031
+ : "warning: python3 not found; Node hook runtime no longer depends on python3"
1032
+ });
1033
+ const windowsHookConfigCandidates = [
1034
+ path.join(projectRoot, ".claude/hooks/hooks.json"),
1035
+ path.join(projectRoot, ".cursor/hooks.json"),
1036
+ path.join(projectRoot, ".codex/hooks.json")
1037
+ ];
1038
+ const legacyDispatchFiles = [];
1039
+ for (const candidate of windowsHookConfigCandidates) {
1040
+ if (!(await exists(candidate)))
1041
+ continue;
1042
+ const content = await fs.readFile(candidate, "utf8");
1043
+ if (/run-hook\.cmd|bash\s+\.cclaw\/hooks\//u.test(content)) {
1044
+ legacyDispatchFiles.push(path.relative(projectRoot, candidate));
1045
+ }
1046
+ }
1047
+ checks.push({
1048
+ name: "warning:windows:hook_dispatch_node_only",
1049
+ ok: legacyDispatchFiles.length === 0,
1050
+ details: legacyDispatchFiles.length === 0
1051
+ ? "hook configs use node-dispatched .cclaw/hooks/run-hook.mjs commands"
1052
+ : `warning: legacy shell hook dispatch remains in ${legacyDispatchFiles.join(", ")}`
1042
1053
  });
1043
1054
  // Knowledge store exists (canonical JSONL, no markdown mirror)
1044
1055
  checks.push({
package/dist/install.js CHANGED
@@ -24,8 +24,7 @@ import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rew
24
24
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
25
25
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
26
26
  import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
27
- import { hookLibScript, sessionStartScript, stopCheckpointScript, runHookDispatcherScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
28
- import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
27
+ import { stageCompleteScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
29
28
  import { nodeHookRuntimeScript } from "./content/node-hooks.js";
30
29
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
31
30
  import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
@@ -73,32 +72,121 @@ async function resolveGitHooksDir(projectRoot) {
73
72
  }
74
73
  }
75
74
  function managedGitRuntimeScript(hookName) {
76
- const rangeExpression = hookName === "pre-commit"
77
- ? 'git diff --cached --name-only'
78
- : 'git diff --name-only @{upstream}...HEAD || git diff --name-only HEAD~1...HEAD';
79
- return `#!/usr/bin/env bash
80
- # ${GIT_HOOK_MANAGED_MARKER}: runtime ${hookName}
81
- set -euo pipefail
75
+ return `#!/usr/bin/env node
76
+ // ${GIT_HOOK_MANAGED_MARKER}: runtime ${hookName}
77
+ import fs from "node:fs";
78
+ import path from "node:path";
79
+ import process from "node:process";
80
+ import { spawnSync } from "node:child_process";
81
+
82
+ const HOOK_NAME = ${JSON.stringify(hookName)};
83
+ const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
84
+
85
+ function runGit(args, cwd) {
86
+ const result = spawnSync("git", args, {
87
+ cwd,
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "pipe", "ignore"]
90
+ });
91
+ return {
92
+ status: typeof result.status === "number" ? result.status : 1,
93
+ stdout: typeof result.stdout === "string" ? result.stdout : ""
94
+ };
95
+ }
96
+
97
+ function resolveRepoRoot() {
98
+ const result = runGit(["rev-parse", "--show-toplevel"], process.cwd());
99
+ if (result.status === 0) {
100
+ const root = result.stdout.trim();
101
+ if (root.length > 0) return root;
102
+ }
103
+ return process.cwd();
104
+ }
82
105
 
83
- ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
84
- GUARD_SCRIPT="$ROOT/${RUNTIME_ROOT}/hooks/prompt-guard.sh"
85
- [ -x "$GUARD_SCRIPT" ] || exit 0
106
+ function resolveChangedFiles(root) {
107
+ if (HOOK_NAME === "pre-commit") {
108
+ const result = runGit(["diff", "--cached", "--name-only"], root);
109
+ return result.status === 0 ? result.stdout : "";
110
+ }
111
+ const upstreamResult = runGit(["diff", "--name-only", "@{upstream}...HEAD"], root);
112
+ if (upstreamResult.status === 0) {
113
+ return upstreamResult.stdout;
114
+ }
115
+ const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
116
+ return fallback.status === 0 ? fallback.stdout : "";
117
+ }
118
+
119
+ const root = resolveRepoRoot();
120
+ const runtimeHook = path.join(root, RUNTIME_ROOT, "hooks", "run-hook.mjs");
121
+ if (!fs.existsSync(runtimeHook)) {
122
+ process.exit(0);
123
+ }
124
+
125
+ const changedFiles = resolveChangedFiles(root)
126
+ .split(/\\r?\\n/gu)
127
+ .map((line) => line.trim())
128
+ .filter((line) => line.length > 0);
129
+ if (changedFiles.length === 0) {
130
+ process.exit(0);
131
+ }
86
132
 
87
- FILES=$(${rangeExpression} 2>/dev/null || true)
88
- [ -n "$FILES" ] || exit 0
133
+ const payload = JSON.stringify({
134
+ tool_name: "Write",
135
+ tool_input: {
136
+ path: changedFiles.join("\\n"),
137
+ paths: changedFiles
138
+ }
139
+ });
89
140
 
90
- printf '%s\n' "$FILES" | bash "$GUARD_SCRIPT"
141
+ const result = spawnSync(process.execPath, [runtimeHook, "prompt-guard"], {
142
+ cwd: root,
143
+ env: process.env,
144
+ input: payload,
145
+ encoding: "utf8",
146
+ stdio: ["pipe", "ignore", "inherit"]
147
+ });
148
+ process.exit(typeof result.status === "number" ? result.status : 1);
91
149
  `;
92
150
  }
93
151
  function managedGitRelayHook(hookName) {
94
- return `#!/usr/bin/env bash
95
- # ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
96
- set -euo pipefail
152
+ return `#!/usr/bin/env node
153
+ // ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
154
+ import fs from "node:fs";
155
+ import path from "node:path";
156
+ import process from "node:process";
157
+ import { spawn, spawnSync } from "node:child_process";
158
+
159
+ const RUNTIME_REL_DIR = ${JSON.stringify(GIT_HOOK_RUNTIME_REL_DIR)};
160
+ const HOOK_NAME = ${JSON.stringify(hookName)};
161
+
162
+ function resolveRepoRoot() {
163
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
164
+ cwd: process.cwd(),
165
+ encoding: "utf8",
166
+ stdio: ["ignore", "pipe", "ignore"]
167
+ });
168
+ if (typeof result.status === "number" && result.status === 0) {
169
+ const root = (result.stdout || "").trim();
170
+ if (root.length > 0) return root;
171
+ }
172
+ return process.cwd();
173
+ }
174
+
175
+ const root = resolveRepoRoot();
176
+ const runtimeHook = path.join(root, RUNTIME_REL_DIR, HOOK_NAME + ".mjs");
177
+ if (!fs.existsSync(runtimeHook)) {
178
+ process.exit(0);
179
+ }
97
180
 
98
- ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
99
- RUNTIME_HOOK="$ROOT/${GIT_HOOK_RUNTIME_REL_DIR}/${hookName}.sh"
100
- [ -x "$RUNTIME_HOOK" ] || exit 0
101
- exec bash "$RUNTIME_HOOK" "$@"
181
+ const child = spawn(process.execPath, [runtimeHook, ...process.argv.slice(2)], {
182
+ cwd: root,
183
+ env: process.env,
184
+ stdio: "inherit"
185
+ });
186
+ child.on("error", () => process.exit(1));
187
+ child.on("close", (code, signal) => {
188
+ process.exit(signal ? 1 : typeof code === "number" ? code : 1);
189
+ });
102
190
  `;
103
191
  }
104
192
  async function removeManagedGitHookRelays(projectRoot) {
@@ -141,7 +229,7 @@ async function syncManagedGitHooks(projectRoot, config) {
141
229
  const runtimeGitHooksDir = path.join(projectRoot, GIT_HOOK_RUNTIME_REL_DIR);
142
230
  await ensureDir(runtimeGitHooksDir);
143
231
  for (const hookName of ["pre-commit", "pre-push"]) {
144
- const runtimePathForHook = path.join(runtimeGitHooksDir, `${hookName}.sh`);
232
+ const runtimePathForHook = path.join(runtimeGitHooksDir, `${hookName}.mjs`);
145
233
  await writeFileSafe(runtimePathForHook, managedGitRuntimeScript(hookName));
146
234
  try {
147
235
  await fs.chmod(runtimePathForHook, 0o755);
@@ -631,22 +719,7 @@ async function writeHooks(projectRoot, config) {
631
719
  mode: config.ironLaws?.mode,
632
720
  strictLaws: config.ironLaws?.strictLaws
633
721
  }), null, 2)}\n`);
634
- await writeFileSafe(path.join(hooksDir, "_lib.sh"), hookLibScript());
635
- await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
636
- await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
637
- await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookDispatcherScript());
638
- await writeFileSafe(path.join(hooksDir, "stage-complete.sh"), stageCompleteScript());
639
- await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
640
- await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
641
- strictMode: config.promptGuardMode === "strict"
642
- }));
643
- await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript({
644
- workflowGuardMode: config.strictness ?? "advisory",
645
- tddEnforcementMode: config.tddEnforcement ?? "advisory",
646
- tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
647
- tddProductionPathPatterns: config.tdd?.productionPathPatterns
648
- }));
649
- await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
722
+ await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
650
723
  await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
651
724
  promptGuardMode: config.promptGuardMode ?? config.strictness ?? "advisory",
652
725
  workflowGuardMode: config.strictness ?? "advisory",
@@ -658,15 +731,7 @@ async function writeHooks(projectRoot, config) {
658
731
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
659
732
  try {
660
733
  for (const script of [
661
- "_lib.sh",
662
- "session-start.sh",
663
- "stop-checkpoint.sh",
664
- "run-hook.cmd",
665
- "stage-complete.sh",
666
- "pre-compact.sh",
667
- "prompt-guard.sh",
668
- "workflow-guard.sh",
669
- "context-monitor.sh",
734
+ "stage-complete.mjs",
670
735
  "run-hook.mjs",
671
736
  "opencode-plugin.mjs"
672
737
  ]) {
@@ -1151,7 +1216,16 @@ async function cleanLegacyArtifacts(projectRoot) {
1151
1216
  runtimePath(projectRoot, "observations.jsonl"),
1152
1217
  runtimePath(projectRoot, "hooks", "observe.sh"),
1153
1218
  runtimePath(projectRoot, "hooks", "summarize-observations.sh"),
1154
- runtimePath(projectRoot, "hooks", "summarize-observations.mjs")
1219
+ runtimePath(projectRoot, "hooks", "summarize-observations.mjs"),
1220
+ runtimePath(projectRoot, "hooks", "_lib.sh"),
1221
+ runtimePath(projectRoot, "hooks", "session-start.sh"),
1222
+ runtimePath(projectRoot, "hooks", "stop-checkpoint.sh"),
1223
+ runtimePath(projectRoot, "hooks", "run-hook.cmd"),
1224
+ runtimePath(projectRoot, "hooks", "stage-complete.sh"),
1225
+ runtimePath(projectRoot, "hooks", "pre-compact.sh"),
1226
+ runtimePath(projectRoot, "hooks", "prompt-guard.sh"),
1227
+ runtimePath(projectRoot, "hooks", "workflow-guard.sh"),
1228
+ runtimePath(projectRoot, "hooks", "context-monitor.sh")
1155
1229
  ]) {
1156
1230
  try {
1157
1231
  await fs.rm(legacyRuntimeFile, { force: true });
@@ -1355,13 +1429,10 @@ function stripManagedHookCommands(value) {
1355
1429
  }
1356
1430
  function isManagedRuntimeHookCommand(command) {
1357
1431
  const normalized = command.trim().replace(/\s+/gu, " ");
1358
- if (/(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized) ||
1359
- /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/run-hook\.cmd\s+(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)(?:\.sh)?(?:\s|$)/u.test(normalized) ||
1360
- /(^|\s)(?:node\s+)?(?:"|')?(?:\.\/)?\.cclaw\/hooks\/run-hook\.mjs(?:"|')?\s+(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state)(?:\.sh)?(?:\s|$)/u.test(normalized)) {
1432
+ if (/(^|\s)(?:node\s+)?(?:"|')?(?:\.\/)?\.cclaw\/hooks\/run-hook\.mjs(?:"|')?\s+(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state)(?:\s|$)/u.test(normalized)) {
1361
1433
  return true;
1362
1434
  }
1363
- // Codex UserPromptSubmit non-blocking state nudge:
1364
- // legacy shell and newer Node wrappers call this internal command.
1435
+ // Codex UserPromptSubmit non-blocking state nudge.
1365
1436
  return /internal verify-current-state(?:\s|$)/u.test(normalized);
1366
1437
  }
1367
1438
  async function removeManagedHookEntries(hookFilePath) {
package/dist/policy.js CHANGED
@@ -180,19 +180,19 @@ export async function policyChecks(projectRoot, options = {}) {
180
180
  { file: runtimeFile("contexts/execution.md"), needle: "Context Mode: execution", name: "context_mode:execution" },
181
181
  { file: runtimeFile("contexts/review.md"), needle: "Context Mode: review", name: "context_mode:review" },
182
182
  { file: runtimeFile("contexts/incident.md"), needle: "Context Mode: incident", name: "context_mode:incident" },
183
- { file: runtimeFile("hooks/session-start.sh"), needle: "ACTIVE_RUN=", name: "hooks:session_start:active_run" },
184
- { file: runtimeFile("hooks/session-start.sh"), needle: "checkpoint.json", name: "hooks:session_start:checkpoint_ref" },
185
- { file: runtimeFile("hooks/session-start.sh"), needle: "stage-activity.jsonl", name: "hooks:session_start:activity_ref" },
186
- { file: runtimeFile("hooks/session-start.sh"), needle: "suggestion-memory.json", name: "hooks:session_start:suggestion_memory" },
187
- { file: runtimeFile("hooks/session-start.sh"), needle: "context-warnings.jsonl", name: "hooks:session_start:context_warning_ref" },
188
- { file: runtimeFile("hooks/stop-checkpoint.sh"), needle: "checkpoint.json", name: "hooks:stop:checkpoint_write" },
189
- { file: runtimeFile("hooks/prompt-guard.sh"), needle: "write_to_cclaw_runtime", name: "hooks:guard:risky_write_advisory" },
190
- { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
191
- { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
192
- { file: runtimeFile("hooks/workflow-guard.sh"), needle: "tdd_write_without_open_red", name: "hooks:workflow_guard:tdd_red_first" },
193
- { file: runtimeFile("hooks/context-monitor.sh"), needle: "remaining is", name: "hooks:context:threshold_warning" },
183
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "activeRunId", name: "hooks:session_start:active_run" },
184
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "checkpoint.json", name: "hooks:session_start:checkpoint_ref" },
185
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "stage-activity.jsonl", name: "hooks:session_start:activity_ref" },
186
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "suggestion-memory.json", name: "hooks:session_start:suggestion_memory" },
187
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "context-warnings.jsonl", name: "hooks:session_start:context_warning_ref" },
188
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "checkpoint.json", name: "hooks:stop:checkpoint_write" },
189
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "write_to_cclaw_runtime", name: "hooks:guard:risky_write_advisory" },
190
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
191
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
192
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "tdd_write_without_open_red", name: "hooks:workflow_guard:tdd_red_first" },
193
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "context remaining is", name: "hooks:context:threshold_warning" },
194
194
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" },
195
- { file: runtimeFile("hooks/session-start.sh"), needle: "Knowledge digest", name: "hooks:session_start:knowledge_digest" },
195
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "Knowledge digest", name: "hooks:session_start:knowledge_digest" },
196
196
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "Knowledge digest", name: "hooks:opencode:knowledge_digest" }
197
197
  ];
198
198
  if (activeHarnesses.has("opencode")) {
@@ -203,7 +203,7 @@ export async function policyChecks(projectRoot, options = {}) {
203
203
  });
204
204
  utilitySkillChecks.push({
205
205
  file: ".opencode/plugins/cclaw-plugin.mjs",
206
- needle: "workflow-guard.sh",
206
+ needle: "workflow-guard",
207
207
  name: "hooks:opencode:deployed_workflow_guard"
208
208
  });
209
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.9",
3
+ "version": "0.48.10",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,10 +23,9 @@
23
23
  "test:coverage": "vitest run --coverage",
24
24
  "test:mutation": "stryker run",
25
25
  "smoke:runtime": "npm run build && node scripts/smoke-init.mjs",
26
- "lint:hooks": "npm run build && node scripts/lint-generated-hooks.mjs",
27
26
  "build:harness-docs": "npm run build && node scripts/build-harness-docs.mjs",
28
27
  "build:plugin-manifests": "npm run build && node scripts/build-plugin-manifests.mjs",
29
- "release:check": "npm run build && node scripts/verify-bin-executable.mjs && npm run test && node scripts/lint-generated-hooks.mjs && node scripts/build-plugin-manifests.mjs && npm pack --dry-run && node scripts/smoke-init.mjs",
28
+ "release:check": "npm run build && node scripts/verify-bin-executable.mjs && npm run test && node scripts/build-plugin-manifests.mjs && npm pack --dry-run && node scripts/smoke-init.mjs",
30
29
  "release:bundle": "npm run release:check && npm pack"
31
30
  },
32
31
  "keywords": [