cclaw-cli 0.48.9 → 0.48.11

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.
@@ -409,6 +409,12 @@ export default function cclawPlugin(ctx) {
409
409
  : typeof payload;
410
410
  console.error("[cclaw] opencode unknown event payload keys: " + keys);
411
411
  }
412
+ // session.compacted must run pre-compact BEFORE refreshing the bootstrap
413
+ // cache, otherwise the injected system prompt still shows the pre-compact
414
+ // digest/state until the next lifecycle event.
415
+ if (eventType === "session.compacted") {
416
+ await runHookScript("pre-compact", eventData ?? {});
417
+ }
412
418
  if (
413
419
  eventType === "session.created" ||
414
420
  eventType === "session.resumed" ||
@@ -424,17 +430,14 @@ export default function cclawPlugin(ctx) {
424
430
  // until the next compaction or restart.
425
431
  await refreshBootstrapCache(true);
426
432
  }
427
- if (eventType === "session.compacted") {
428
- await runHookScript("pre-compact.sh", eventData ?? {});
429
- }
430
433
  if (eventType === "session.idle") {
431
- await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
434
+ await runHookScript("stop-checkpoint", { loop_count: 0 });
432
435
  }
433
436
  },
434
437
  "tool.execute.before": async (input, output) => {
435
438
  const payload = normalizeToolPayload(input, output);
436
- const promptOk = await runHookScript("prompt-guard.sh", payload);
437
- const workflowOk = await runHookScript("workflow-guard.sh", payload);
439
+ const promptOk = await runHookScript("prompt-guard", payload);
440
+ const workflowOk = await runHookScript("workflow-guard", payload);
438
441
  if (!promptOk || !workflowOk) {
439
442
  throw new Error(
440
443
  "cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
@@ -443,7 +446,7 @@ export default function cclawPlugin(ctx) {
443
446
  },
444
447
  "tool.execute.after": async (input, output) => {
445
448
  const payload = normalizeToolPayload(input, output);
446
- await runHookScript("context-monitor.sh", payload);
449
+ await runHookScript("context-monitor", payload);
447
450
  void refreshBootstrapCache(false);
448
451
  },
449
452
  "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.",
@@ -30,15 +30,6 @@ const RULES = [
30
30
  docRef: ref("runtime-layout.md")
31
31
  }
32
32
  },
33
- {
34
- test: /^capability:runtime:json_parser$/,
35
- metadata: {
36
- severity: "warning",
37
- summary: "Optional JSON fallback parser availability.",
38
- fix: "Install at least one of `python3` or `jq` for resilient fallback parsing.",
39
- docRef: ref("tooling-capabilities.md")
40
- }
41
- },
42
33
  {
43
34
  test: /^capability:required:/,
44
35
  metadata: {
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
  }
@@ -746,12 +743,9 @@ export async function doctorChecks(projectRoot, options = {}) {
746
743
  }
747
744
  }
748
745
  }
749
- // OpenCode plugin source + deployed path
750
- checks.push({
751
- name: "hook:opencode_plugin_source",
752
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs")),
753
- details: `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs`
754
- });
746
+ // OpenCode plugin deployed path. (Presence of the source under
747
+ // `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs` is already asserted by the
748
+ // generic `hook:script:opencode-plugin.mjs` check above; avoid a duplicate.)
755
749
  const opencodeEnabled = configuredHarnesses.includes("opencode");
756
750
  const opencodeDeployed = await exists(path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs"));
757
751
  checks.push({
@@ -776,11 +770,11 @@ export async function doctorChecks(projectRoot, options = {}) {
776
770
  const preCommands = collectHookCommands(hooks.PreToolUse);
777
771
  const postCommands = collectHookCommands(hooks.PostToolUse);
778
772
  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"));
773
+ const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start")) &&
774
+ preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
775
+ preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
776
+ postCommands.some((cmd) => cmd.includes("context-monitor")) &&
777
+ stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
784
778
  checks.push({
785
779
  name: "hook:wiring:claude",
786
780
  ok: wiringOk,
@@ -809,11 +803,11 @@ export async function doctorChecks(projectRoot, options = {}) {
809
803
  const preCommands = collectHookCommands(hooks.preToolUse);
810
804
  const postCommands = collectHookCommands(hooks.postToolUse);
811
805
  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"));
806
+ const wiringOk = sessionCommands.some((cmd) => cmd.includes("session-start")) &&
807
+ preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
808
+ preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
809
+ postCommands.some((cmd) => cmd.includes("context-monitor")) &&
810
+ stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
817
811
  checks.push({
818
812
  name: "hook:wiring:cursor",
819
813
  ok: wiringOk,
@@ -876,14 +870,14 @@ export async function doctorChecks(projectRoot, options = {}) {
876
870
  const codexPreCmds = collectHookCommands(codexHooks.PreToolUse);
877
871
  const codexPostCmds = collectHookCommands(codexHooks.PostToolUse);
878
872
  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")) &&
873
+ const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start")) &&
874
+ codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard")) &&
875
+ codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard")) &&
882
876
  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"));
877
+ codexPreCmds.some((cmd) => cmd.includes("prompt-guard")) &&
878
+ codexPreCmds.some((cmd) => cmd.includes("workflow-guard")) &&
879
+ codexPostCmds.some((cmd) => cmd.includes("context-monitor")) &&
880
+ codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint"));
887
881
  checks.push({
888
882
  name: "hook:wiring:codex",
889
883
  ok: codexWiringOk,
@@ -963,10 +957,10 @@ export async function doctorChecks(projectRoot, options = {}) {
963
957
  content.includes("event: async") &&
964
958
  content.includes('"tool.execute.before"') &&
965
959
  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") &&
960
+ content.includes("prompt-guard") &&
961
+ content.includes("workflow-guard") &&
962
+ content.includes("context-monitor") &&
963
+ content.includes("pre-compact") &&
970
964
  content.includes('"session.created"') &&
971
965
  content.includes('"session.idle"') &&
972
966
  content.includes('"session.resumed"') &&
@@ -981,7 +975,7 @@ export async function doctorChecks(projectRoot, options = {}) {
981
975
  content.includes('"tool.execute.after": async');
982
976
  precompactHookOk =
983
977
  content.includes('eventType === "session.compacted"') &&
984
- content.includes('runHookScript("pre-compact.sh"');
978
+ content.includes('runHookScript("pre-compact"');
985
979
  }
986
980
  checks.push({
987
981
  name: "lifecycle:opencode:rehydration_events",
@@ -996,7 +990,7 @@ export async function doctorChecks(projectRoot, options = {}) {
996
990
  checks.push({
997
991
  name: "hook:opencode:precompact_digest",
998
992
  ok: precompactHookOk,
999
- details: `${file} must run pre-compact.sh on session.compacted before bootstrap refresh.`
993
+ details: `${file} must run pre-compact on session.compacted before bootstrap refresh.`
1000
994
  });
1001
995
  const runtimeShape = await opencodePluginRuntimeShapeCheck(projectRoot);
1002
996
  checks.push({
@@ -1011,34 +1005,32 @@ export async function doctorChecks(projectRoot, options = {}) {
1011
1005
  details: registration.details
1012
1006
  });
1013
1007
  }
1014
- const hasBash = await commandAvailable("bash");
1015
1008
  const hasNode = await commandAvailable("node");
1016
- const hasPython = await commandAvailable("python3");
1017
- 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
1009
  checks.push({
1024
1010
  name: "capability:required:node",
1025
1011
  ok: hasNode,
1026
1012
  details: "node is required for cclaw runtime scripts and CLI wiring"
1027
1013
  });
1014
+ const windowsHookConfigCandidates = [
1015
+ path.join(projectRoot, ".claude/hooks/hooks.json"),
1016
+ path.join(projectRoot, ".cursor/hooks.json"),
1017
+ path.join(projectRoot, ".codex/hooks.json")
1018
+ ];
1019
+ const legacyDispatchFiles = [];
1020
+ for (const candidate of windowsHookConfigCandidates) {
1021
+ if (!(await exists(candidate)))
1022
+ continue;
1023
+ const content = await fs.readFile(candidate, "utf8");
1024
+ if (/run-hook\.cmd|bash\s+\.cclaw\/hooks\//u.test(content)) {
1025
+ legacyDispatchFiles.push(path.relative(projectRoot, candidate));
1026
+ }
1027
+ }
1028
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
- checks.push({
1034
- name: "warning:capability:jq",
1035
- ok: true,
1036
- details: hasJq ? "jq available" : "warning: jq not found, python/node fallbacks will be used"
1037
- });
1038
- checks.push({
1039
- name: "warning:capability:python3",
1040
- ok: true,
1041
- details: hasPython ? "python3 available" : "warning: python3 not found, jq/node paths must stay healthy"
1029
+ name: "warning:windows:hook_dispatch_node_only",
1030
+ ok: legacyDispatchFiles.length === 0,
1031
+ details: legacyDispatchFiles.length === 0
1032
+ ? "hook configs use node-dispatched .cclaw/hooks/run-hook.mjs commands"
1033
+ : `warning: legacy shell hook dispatch remains in ${legacyDispatchFiles.join(", ")}`
1042
1034
  });
1043
1035
  // Knowledge store exists (canonical JSONL, no markdown mirror)
1044
1036
  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,126 @@ 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
+ // cclaw git relay is installed but the runtime entrypoint is missing —
123
+ // warn visibly (without blocking the commit) so the drift is noticed.
124
+ process.stderr.write(
125
+ "[cclaw] " + HOOK_NAME + ": " + runtimeHook + " not found; run \`cclaw sync\` to reinstall\\n"
126
+ );
127
+ process.exit(0);
128
+ }
129
+
130
+ const changedFiles = resolveChangedFiles(root)
131
+ .split(/\\r?\\n/gu)
132
+ .map((line) => line.trim())
133
+ .filter((line) => line.length > 0);
134
+ if (changedFiles.length === 0) {
135
+ process.exit(0);
136
+ }
86
137
 
87
- FILES=$(${rangeExpression} 2>/dev/null || true)
88
- [ -n "$FILES" ] || exit 0
138
+ const payload = JSON.stringify({
139
+ tool_name: "Write",
140
+ tool_input: {
141
+ path: changedFiles.join("\\n"),
142
+ paths: changedFiles
143
+ }
144
+ });
89
145
 
90
- printf '%s\n' "$FILES" | bash "$GUARD_SCRIPT"
146
+ const result = spawnSync(process.execPath, [runtimeHook, "prompt-guard"], {
147
+ cwd: root,
148
+ env: process.env,
149
+ input: payload,
150
+ encoding: "utf8",
151
+ stdio: ["pipe", "ignore", "inherit"]
152
+ });
153
+ process.exit(typeof result.status === "number" ? result.status : 1);
91
154
  `;
92
155
  }
93
156
  function managedGitRelayHook(hookName) {
94
- return `#!/usr/bin/env bash
95
- # ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
96
- set -euo pipefail
157
+ return `#!/usr/bin/env node
158
+ // ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
159
+ import fs from "node:fs";
160
+ import path from "node:path";
161
+ import process from "node:process";
162
+ import { spawn, spawnSync } from "node:child_process";
163
+
164
+ const RUNTIME_REL_DIR = ${JSON.stringify(GIT_HOOK_RUNTIME_REL_DIR)};
165
+ const HOOK_NAME = ${JSON.stringify(hookName)};
166
+
167
+ function resolveRepoRoot() {
168
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
169
+ cwd: process.cwd(),
170
+ encoding: "utf8",
171
+ stdio: ["ignore", "pipe", "ignore"]
172
+ });
173
+ if (typeof result.status === "number" && result.status === 0) {
174
+ const root = (result.stdout || "").trim();
175
+ if (root.length > 0) return root;
176
+ }
177
+ return process.cwd();
178
+ }
179
+
180
+ const root = resolveRepoRoot();
181
+ const runtimeHook = path.join(root, RUNTIME_REL_DIR, HOOK_NAME + ".mjs");
182
+ if (!fs.existsSync(runtimeHook)) {
183
+ process.exit(0);
184
+ }
97
185
 
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" "$@"
186
+ const child = spawn(process.execPath, [runtimeHook, ...process.argv.slice(2)], {
187
+ cwd: root,
188
+ env: process.env,
189
+ stdio: "inherit"
190
+ });
191
+ child.on("error", () => process.exit(1));
192
+ child.on("close", (code, signal) => {
193
+ process.exit(signal ? 1 : typeof code === "number" ? code : 1);
194
+ });
102
195
  `;
103
196
  }
104
197
  async function removeManagedGitHookRelays(projectRoot) {
@@ -141,7 +234,7 @@ async function syncManagedGitHooks(projectRoot, config) {
141
234
  const runtimeGitHooksDir = path.join(projectRoot, GIT_HOOK_RUNTIME_REL_DIR);
142
235
  await ensureDir(runtimeGitHooksDir);
143
236
  for (const hookName of ["pre-commit", "pre-push"]) {
144
- const runtimePathForHook = path.join(runtimeGitHooksDir, `${hookName}.sh`);
237
+ const runtimePathForHook = path.join(runtimeGitHooksDir, `${hookName}.mjs`);
145
238
  await writeFileSafe(runtimePathForHook, managedGitRuntimeScript(hookName));
146
239
  try {
147
240
  await fs.chmod(runtimePathForHook, 0o755);
@@ -631,22 +724,7 @@ async function writeHooks(projectRoot, config) {
631
724
  mode: config.ironLaws?.mode,
632
725
  strictLaws: config.ironLaws?.strictLaws
633
726
  }), 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());
727
+ await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
650
728
  await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
651
729
  promptGuardMode: config.promptGuardMode ?? config.strictness ?? "advisory",
652
730
  workflowGuardMode: config.strictness ?? "advisory",
@@ -658,15 +736,7 @@ async function writeHooks(projectRoot, config) {
658
736
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
659
737
  try {
660
738
  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",
739
+ "stage-complete.mjs",
670
740
  "run-hook.mjs",
671
741
  "opencode-plugin.mjs"
672
742
  ]) {
@@ -1151,7 +1221,16 @@ async function cleanLegacyArtifacts(projectRoot) {
1151
1221
  runtimePath(projectRoot, "observations.jsonl"),
1152
1222
  runtimePath(projectRoot, "hooks", "observe.sh"),
1153
1223
  runtimePath(projectRoot, "hooks", "summarize-observations.sh"),
1154
- runtimePath(projectRoot, "hooks", "summarize-observations.mjs")
1224
+ runtimePath(projectRoot, "hooks", "summarize-observations.mjs"),
1225
+ runtimePath(projectRoot, "hooks", "_lib.sh"),
1226
+ runtimePath(projectRoot, "hooks", "session-start.sh"),
1227
+ runtimePath(projectRoot, "hooks", "stop-checkpoint.sh"),
1228
+ runtimePath(projectRoot, "hooks", "run-hook.cmd"),
1229
+ runtimePath(projectRoot, "hooks", "stage-complete.sh"),
1230
+ runtimePath(projectRoot, "hooks", "pre-compact.sh"),
1231
+ runtimePath(projectRoot, "hooks", "prompt-guard.sh"),
1232
+ runtimePath(projectRoot, "hooks", "workflow-guard.sh"),
1233
+ runtimePath(projectRoot, "hooks", "context-monitor.sh")
1155
1234
  ]) {
1156
1235
  try {
1157
1236
  await fs.rm(legacyRuntimeFile, { force: true });
@@ -1354,14 +1433,15 @@ function stripManagedHookCommands(value) {
1354
1433
  return { updated: root, changed: true };
1355
1434
  }
1356
1435
  function isManagedRuntimeHookCommand(command) {
1357
- 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)) {
1436
+ // Normalize whitespace and collapse any Windows-style backslash path
1437
+ // separators to forward slashes so user-edited hook configs on Windows
1438
+ // (e.g. `node .cclaw\hooks\run-hook.mjs ...`) still round-trip through
1439
+ // sync without being duplicated alongside freshly generated entries.
1440
+ const normalized = command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
1441
+ 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
1442
  return true;
1362
1443
  }
1363
- // Codex UserPromptSubmit non-blocking state nudge:
1364
- // legacy shell and newer Node wrappers call this internal command.
1444
+ // Codex UserPromptSubmit non-blocking state nudge.
1365
1445
  return /internal verify-current-state(?:\s|$)/u.test(normalized);
1366
1446
  }
1367
1447
  async function removeManagedHookEntries(hookFilePath) {