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.
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/content/doctor-references.js +11 -7
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/harness-playbooks.js +7 -7
- package/dist/content/hook-events.js +19 -19
- package/dist/content/hooks.d.ts +0 -16
- package/dist/content/hooks.js +112 -1220
- package/dist/content/learnings.js +2 -2
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +31 -15
- package/dist/content/observe.d.ts +0 -38
- package/dist/content/observe.js +31 -1718
- package/dist/content/opencode-plugin.js +10 -7
- package/dist/content/protocols.js +5 -9
- package/dist/content/skills.js +1 -1
- package/dist/content/stage-common-guidance.js +1 -1
- package/dist/content/stages/design.js +1 -1
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +1 -1
- package/dist/doctor-registry.js +0 -9
- package/dist/doctor.js +58 -66
- package/dist/install.js +134 -54
- package/dist/policy.js +13 -13
- package/package.json +2 -3
|
@@ -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
|
|
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
|
|
437
|
-
const workflowOk = await runHookScript("workflow-guard
|
|
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
|
|
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
|
-
- \`
|
|
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.
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
package/dist/content/skills.js
CHANGED
|
@@ -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\`: \`
|
|
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 \`
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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-registry.js
CHANGED
|
@@ -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", "
|
|
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(`
|
|
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.
|
|
500
|
-
const runtimePrePush = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-push.
|
|
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.
|
|
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
|
-
"
|
|
677
|
-
"
|
|
678
|
-
"
|
|
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:
|
|
705
|
-
details:
|
|
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
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
780
|
-
preCommands.some((cmd) => cmd.includes("prompt-guard
|
|
781
|
-
preCommands.some((cmd) => cmd.includes("workflow-guard
|
|
782
|
-
postCommands.some((cmd) => cmd.includes("context-monitor
|
|
783
|
-
stopCommands.some((cmd) => cmd.includes("stop-checkpoint
|
|
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
|
|
813
|
-
preCommands.some((cmd) => cmd.includes("prompt-guard
|
|
814
|
-
preCommands.some((cmd) => cmd.includes("workflow-guard
|
|
815
|
-
postCommands.some((cmd) => cmd.includes("context-monitor
|
|
816
|
-
stopCommands.some((cmd) => cmd.includes("stop-checkpoint
|
|
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
|
|
880
|
-
codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard
|
|
881
|
-
codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard
|
|
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
|
|
884
|
-
codexPreCmds.some((cmd) => cmd.includes("workflow-guard
|
|
885
|
-
codexPostCmds.some((cmd) => cmd.includes("context-monitor
|
|
886
|
-
codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint
|
|
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
|
|
967
|
-
content.includes("workflow-guard
|
|
968
|
-
content.includes("context-monitor
|
|
969
|
-
content.includes("pre-compact
|
|
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
|
|
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
|
|
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: "
|
|
1030
|
-
ok:
|
|
1031
|
-
details:
|
|
1032
|
-
|
|
1033
|
-
|
|
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 {
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
[
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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}.
|
|
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, "
|
|
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
|
-
"
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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) {
|