cclaw-cli 7.7.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -134
- package/dist/artifact-frontmatter.d.ts +51 -0
- package/dist/artifact-frontmatter.js +131 -0
- package/dist/artifact-paths.d.ts +7 -27
- package/dist/artifact-paths.js +20 -249
- package/dist/cancel.d.ts +16 -0
- package/dist/cancel.js +66 -0
- package/dist/cli.d.ts +2 -27
- package/dist/cli.js +90 -508
- package/dist/compound.d.ts +26 -0
- package/dist/compound.js +96 -0
- package/dist/config.d.ts +14 -51
- package/dist/config.js +23 -359
- package/dist/constants.d.ts +11 -18
- package/dist/constants.js +19 -106
- package/dist/content/antipatterns.d.ts +1 -0
- package/dist/content/antipatterns.js +109 -0
- package/dist/content/artifact-templates.d.ts +10 -0
- package/dist/content/artifact-templates.js +550 -0
- package/dist/content/cancel-command.d.ts +2 -2
- package/dist/content/cancel-command.js +25 -17
- package/dist/content/core-agents.d.ts +9 -233
- package/dist/content/core-agents.js +39 -766
- package/dist/content/decision-protocol.d.ts +1 -12
- package/dist/content/decision-protocol.js +27 -20
- package/dist/content/examples.d.ts +8 -42
- package/dist/content/examples.js +293 -425
- package/dist/content/idea-command.d.ts +2 -0
- package/dist/content/idea-command.js +38 -0
- package/dist/content/iron-laws.d.ts +4 -138
- package/dist/content/iron-laws.js +18 -197
- package/dist/content/meta-skill.d.ts +1 -3
- package/dist/content/meta-skill.js +57 -132
- package/dist/content/node-hooks.d.ts +12 -8
- package/dist/content/node-hooks.js +188 -838
- package/dist/content/recovery.d.ts +8 -0
- package/dist/content/recovery.js +179 -0
- package/dist/content/reference-patterns.d.ts +4 -13
- package/dist/content/reference-patterns.js +260 -389
- package/dist/content/research-playbooks.d.ts +8 -8
- package/dist/content/research-playbooks.js +108 -121
- package/dist/content/review-loop.d.ts +6 -192
- package/dist/content/review-loop.js +29 -731
- package/dist/content/skills.d.ts +8 -38
- package/dist/content/skills.js +681 -732
- package/dist/content/specialist-prompts/architect.d.ts +1 -0
- package/dist/content/specialist-prompts/architect.js +225 -0
- package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
- package/dist/content/specialist-prompts/brainstormer.js +168 -0
- package/dist/content/specialist-prompts/index.d.ts +2 -0
- package/dist/content/specialist-prompts/index.js +14 -0
- package/dist/content/specialist-prompts/planner.d.ts +1 -0
- package/dist/content/specialist-prompts/planner.js +182 -0
- package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/reviewer.js +193 -0
- package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/security-reviewer.js +133 -0
- package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
- package/dist/content/specialist-prompts/slice-builder.js +232 -0
- package/dist/content/stage-playbooks.d.ts +8 -0
- package/dist/content/stage-playbooks.js +404 -0
- package/dist/content/start-command.d.ts +2 -12
- package/dist/content/start-command.js +221 -207
- package/dist/flow-state.d.ts +21 -178
- package/dist/flow-state.js +67 -170
- package/dist/fs-utils.d.ts +6 -26
- package/dist/fs-utils.js +29 -162
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.js +51 -34
- package/dist/harness-detect.d.ts +10 -0
- package/dist/harness-detect.js +29 -0
- package/dist/install.d.ts +27 -15
- package/dist/install.js +230 -1342
- package/dist/knowledge-store.d.ts +19 -163
- package/dist/knowledge-store.js +56 -590
- package/dist/logger.d.ts +8 -3
- package/dist/logger.js +13 -4
- package/dist/orchestrator-routing.d.ts +29 -0
- package/dist/orchestrator-routing.js +156 -0
- package/dist/run-persistence.d.ts +7 -118
- package/dist/run-persistence.js +29 -845
- package/dist/runtime/run-hook.entry.d.ts +1 -3
- package/dist/runtime/run-hook.entry.js +19 -4
- package/dist/runtime/run-hook.mjs +13 -1024
- package/dist/types.d.ts +25 -261
- package/dist/types.js +8 -36
- package/package.json +6 -3
- package/dist/artifact-linter/brainstorm.d.ts +0 -2
- package/dist/artifact-linter/brainstorm.js +0 -353
- package/dist/artifact-linter/design.d.ts +0 -18
- package/dist/artifact-linter/design.js +0 -444
- package/dist/artifact-linter/findings-dedup.d.ts +0 -56
- package/dist/artifact-linter/findings-dedup.js +0 -232
- package/dist/artifact-linter/plan.d.ts +0 -2
- package/dist/artifact-linter/plan.js +0 -826
- package/dist/artifact-linter/review-army.d.ts +0 -49
- package/dist/artifact-linter/review-army.js +0 -520
- package/dist/artifact-linter/review.d.ts +0 -2
- package/dist/artifact-linter/review.js +0 -113
- package/dist/artifact-linter/scope.d.ts +0 -2
- package/dist/artifact-linter/scope.js +0 -158
- package/dist/artifact-linter/shared.d.ts +0 -637
- package/dist/artifact-linter/shared.js +0 -2163
- package/dist/artifact-linter/ship.d.ts +0 -2
- package/dist/artifact-linter/ship.js +0 -250
- package/dist/artifact-linter/spec.d.ts +0 -2
- package/dist/artifact-linter/spec.js +0 -176
- package/dist/artifact-linter/tdd.d.ts +0 -118
- package/dist/artifact-linter/tdd.js +0 -1404
- package/dist/artifact-linter.d.ts +0 -15
- package/dist/artifact-linter.js +0 -517
- package/dist/codex-feature-flag.d.ts +0 -58
- package/dist/codex-feature-flag.js +0 -193
- package/dist/content/closeout-guidance.d.ts +0 -14
- package/dist/content/closeout-guidance.js +0 -44
- package/dist/content/diff-command.d.ts +0 -1
- package/dist/content/diff-command.js +0 -43
- package/dist/content/harness-doc.d.ts +0 -1
- package/dist/content/harness-doc.js +0 -65
- package/dist/content/hook-events.d.ts +0 -9
- package/dist/content/hook-events.js +0 -23
- package/dist/content/hook-manifest.d.ts +0 -81
- package/dist/content/hook-manifest.js +0 -156
- package/dist/content/hooks.d.ts +0 -11
- package/dist/content/hooks.js +0 -1972
- package/dist/content/idea.d.ts +0 -60
- package/dist/content/idea.js +0 -416
- package/dist/content/language-policy.d.ts +0 -2
- package/dist/content/language-policy.js +0 -13
- package/dist/content/learnings.d.ts +0 -6
- package/dist/content/learnings.js +0 -141
- package/dist/content/observe.d.ts +0 -19
- package/dist/content/observe.js +0 -86
- package/dist/content/opencode-plugin.d.ts +0 -1
- package/dist/content/opencode-plugin.js +0 -635
- package/dist/content/review-prompts.d.ts +0 -1
- package/dist/content/review-prompts.js +0 -104
- package/dist/content/runtime-shared-snippets.d.ts +0 -8
- package/dist/content/runtime-shared-snippets.js +0 -80
- package/dist/content/session-hooks.d.ts +0 -7
- package/dist/content/session-hooks.js +0 -107
- package/dist/content/skills-elicitation.d.ts +0 -1
- package/dist/content/skills-elicitation.js +0 -167
- package/dist/content/stage-command.d.ts +0 -2
- package/dist/content/stage-command.js +0 -17
- package/dist/content/stage-schema.d.ts +0 -117
- package/dist/content/stage-schema.js +0 -955
- package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
- package/dist/content/stages/_lint-metadata/index.js +0 -97
- package/dist/content/stages/brainstorm.d.ts +0 -2
- package/dist/content/stages/brainstorm.js +0 -184
- package/dist/content/stages/design.d.ts +0 -2
- package/dist/content/stages/design.js +0 -288
- package/dist/content/stages/index.d.ts +0 -8
- package/dist/content/stages/index.js +0 -11
- package/dist/content/stages/plan.d.ts +0 -2
- package/dist/content/stages/plan.js +0 -191
- package/dist/content/stages/review.d.ts +0 -2
- package/dist/content/stages/review.js +0 -240
- package/dist/content/stages/schema-types.d.ts +0 -203
- package/dist/content/stages/schema-types.js +0 -1
- package/dist/content/stages/scope.d.ts +0 -2
- package/dist/content/stages/scope.js +0 -254
- package/dist/content/stages/ship.d.ts +0 -2
- package/dist/content/stages/ship.js +0 -159
- package/dist/content/stages/spec.d.ts +0 -2
- package/dist/content/stages/spec.js +0 -170
- package/dist/content/stages/tdd.d.ts +0 -4
- package/dist/content/stages/tdd.js +0 -273
- package/dist/content/state-contracts.d.ts +0 -1
- package/dist/content/state-contracts.js +0 -63
- package/dist/content/status-command.d.ts +0 -4
- package/dist/content/status-command.js +0 -109
- package/dist/content/subagent-context-skills.d.ts +0 -4
- package/dist/content/subagent-context-skills.js +0 -279
- package/dist/content/subagents.d.ts +0 -3
- package/dist/content/subagents.js +0 -997
- package/dist/content/templates.d.ts +0 -26
- package/dist/content/templates.js +0 -1692
- package/dist/content/track-render-context.d.ts +0 -18
- package/dist/content/track-render-context.js +0 -53
- package/dist/content/tree-command.d.ts +0 -1
- package/dist/content/tree-command.js +0 -64
- package/dist/content/utility-skills.d.ts +0 -30
- package/dist/content/utility-skills.js +0 -160
- package/dist/content/view-command.d.ts +0 -2
- package/dist/content/view-command.js +0 -92
- package/dist/delegation.d.ts +0 -649
- package/dist/delegation.js +0 -1539
- package/dist/early-loop.d.ts +0 -70
- package/dist/early-loop.js +0 -302
- package/dist/execution-topology.d.ts +0 -36
- package/dist/execution-topology.js +0 -73
- package/dist/gate-evidence.d.ts +0 -85
- package/dist/gate-evidence.js +0 -631
- package/dist/harness-adapters.d.ts +0 -151
- package/dist/harness-adapters.js +0 -756
- package/dist/harness-selection.d.ts +0 -31
- package/dist/harness-selection.js +0 -214
- package/dist/hook-schema.d.ts +0 -6
- package/dist/hook-schema.js +0 -114
- package/dist/hook-schemas/claude-hooks.v1.json +0 -10
- package/dist/hook-schemas/codex-hooks.v1.json +0 -10
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
- package/dist/init-detect.d.ts +0 -2
- package/dist/init-detect.js +0 -50
- package/dist/internal/advance-stage/advance.d.ts +0 -89
- package/dist/internal/advance-stage/advance.js +0 -655
- package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
- package/dist/internal/advance-stage/cancel-run.js +0 -19
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
- package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
- package/dist/internal/advance-stage/helpers.d.ts +0 -14
- package/dist/internal/advance-stage/helpers.js +0 -145
- package/dist/internal/advance-stage/hook.d.ts +0 -8
- package/dist/internal/advance-stage/hook.js +0 -40
- package/dist/internal/advance-stage/parsers.d.ts +0 -72
- package/dist/internal/advance-stage/parsers.js +0 -357
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
- package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
- package/dist/internal/advance-stage/review-loop.d.ts +0 -16
- package/dist/internal/advance-stage/review-loop.js +0 -199
- package/dist/internal/advance-stage/rewind.d.ts +0 -14
- package/dist/internal/advance-stage/rewind.js +0 -108
- package/dist/internal/advance-stage/start-flow.d.ts +0 -13
- package/dist/internal/advance-stage/start-flow.js +0 -241
- package/dist/internal/advance-stage/verify.d.ts +0 -21
- package/dist/internal/advance-stage/verify.js +0 -185
- package/dist/internal/advance-stage.d.ts +0 -7
- package/dist/internal/advance-stage.js +0 -138
- package/dist/internal/cohesion-contract-stub.d.ts +0 -24
- package/dist/internal/cohesion-contract-stub.js +0 -148
- package/dist/internal/compound-readiness.d.ts +0 -23
- package/dist/internal/compound-readiness.js +0 -102
- package/dist/internal/detect-public-api-changes.d.ts +0 -5
- package/dist/internal/detect-public-api-changes.js +0 -45
- package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
- package/dist/internal/detect-supply-chain-changes.js +0 -138
- package/dist/internal/early-loop-status.d.ts +0 -7
- package/dist/internal/early-loop-status.js +0 -93
- package/dist/internal/envelope-validate.d.ts +0 -7
- package/dist/internal/envelope-validate.js +0 -66
- package/dist/internal/flow-state-repair.d.ts +0 -20
- package/dist/internal/flow-state-repair.js +0 -104
- package/dist/internal/plan-split-waves.d.ts +0 -190
- package/dist/internal/plan-split-waves.js +0 -764
- package/dist/internal/runtime-integrity.d.ts +0 -7
- package/dist/internal/runtime-integrity.js +0 -268
- package/dist/internal/slice-commit.d.ts +0 -7
- package/dist/internal/slice-commit.js +0 -619
- package/dist/internal/tdd-loop-status.d.ts +0 -14
- package/dist/internal/tdd-loop-status.js +0 -68
- package/dist/internal/tdd-red-evidence.d.ts +0 -7
- package/dist/internal/tdd-red-evidence.js +0 -153
- package/dist/internal/waiver-grant.d.ts +0 -62
- package/dist/internal/waiver-grant.js +0 -294
- package/dist/internal/wave-status.d.ts +0 -63
- package/dist/internal/wave-status.js +0 -450
- package/dist/managed-resources.d.ts +0 -53
- package/dist/managed-resources.js +0 -313
- package/dist/policy.d.ts +0 -10
- package/dist/policy.js +0 -167
- package/dist/retro-gate.d.ts +0 -9
- package/dist/retro-gate.js +0 -47
- package/dist/run-archive.d.ts +0 -61
- package/dist/run-archive.js +0 -391
- package/dist/runs.d.ts +0 -2
- package/dist/runs.js +0 -2
- package/dist/stack-detection.d.ts +0 -116
- package/dist/stack-detection.js +0 -489
- package/dist/streaming/event-stream.d.ts +0 -31
- package/dist/streaming/event-stream.js +0 -114
- package/dist/tdd-cycle.d.ts +0 -107
- package/dist/tdd-cycle.js +0 -289
- package/dist/tdd-verification-evidence.d.ts +0 -17
- package/dist/tdd-verification-evidence.js +0 -122
- package/dist/track-heuristics.d.ts +0 -27
- package/dist/track-heuristics.js +0 -154
- package/dist/util/slice-id.d.ts +0 -58
- package/dist/util/slice-id.js +0 -89
- package/dist/worktree-manager.d.ts +0 -20
- package/dist/worktree-manager.js +0 -108
|
@@ -1,1030 +1,19 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import path2 from "node:path";
|
|
4
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
|
-
|
|
6
|
-
// src/constants.ts
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
var RUNTIME_ROOT = ".cclaw";
|
|
11
|
-
function readPackageVersion() {
|
|
12
|
-
try {
|
|
13
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const candidates = [
|
|
15
|
-
path.resolve(here, "../package.json"),
|
|
16
|
-
path.resolve(here, "../../package.json")
|
|
17
|
-
];
|
|
18
|
-
for (const candidate of candidates) {
|
|
19
|
-
try {
|
|
20
|
-
const raw = readFileSync(candidate, "utf8");
|
|
21
|
-
const parsed = JSON.parse(raw);
|
|
22
|
-
if (parsed.name === "cclaw-cli" && typeof parsed.version === "string") {
|
|
23
|
-
return parsed.version;
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
}
|
|
31
|
-
return "0.0.0-dev";
|
|
32
|
-
}
|
|
33
|
-
var CCLAW_VERSION = readPackageVersion();
|
|
34
|
-
var REQUIRED_DIRS = [
|
|
35
|
-
RUNTIME_ROOT,
|
|
36
|
-
`${RUNTIME_ROOT}/commands`,
|
|
37
|
-
`${RUNTIME_ROOT}/skills`,
|
|
38
|
-
`${RUNTIME_ROOT}/templates`,
|
|
39
|
-
`${RUNTIME_ROOT}/templates/state-contracts`,
|
|
40
|
-
`${RUNTIME_ROOT}/artifacts`,
|
|
41
|
-
`${RUNTIME_ROOT}/wave-plans`,
|
|
42
|
-
`${RUNTIME_ROOT}/archive`,
|
|
43
|
-
`${RUNTIME_ROOT}/worktrees`,
|
|
44
|
-
`${RUNTIME_ROOT}/state`,
|
|
45
|
-
`${RUNTIME_ROOT}/rules`,
|
|
46
|
-
`${RUNTIME_ROOT}/agents`,
|
|
47
|
-
`${RUNTIME_ROOT}/hooks`,
|
|
48
|
-
`${RUNTIME_ROOT}/skills/review-prompts`
|
|
49
|
-
];
|
|
50
|
-
var REQUIRED_GITIGNORE_PATTERNS = [
|
|
51
|
-
"# cclaw generated artifacts",
|
|
52
|
-
`${RUNTIME_ROOT}/`,
|
|
53
|
-
".claude/commands/cc-*.md",
|
|
54
|
-
".claude/commands/cc.md",
|
|
55
|
-
".cursor/commands/cc-*.md",
|
|
56
|
-
".cursor/commands/cc.md",
|
|
57
|
-
".opencode/commands/cc-*.md",
|
|
58
|
-
".opencode/commands/cc.md",
|
|
59
|
-
// Codex uses skill-kind shims under `.agents/skills/cc*/` since
|
|
60
|
-
// Codex shim layout (renamed from the older `cclaw-cc*` layout).
|
|
61
|
-
// `cclaw sync` and `cclaw uninstall` both auto-remove the legacy
|
|
62
|
-
// `cclaw-cc*` directories.
|
|
63
|
-
".agents/skills/cc/SKILL.md",
|
|
64
|
-
".agents/skills/cc-*/SKILL.md",
|
|
65
|
-
".claude/hooks/hooks.json",
|
|
66
|
-
".cursor/hooks.json",
|
|
67
|
-
".codex/hooks.json",
|
|
68
|
-
".opencode/plugins/cclaw-plugin.mjs",
|
|
69
|
-
".cursor/rules/cclaw-workflow.mdc"
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
// src/content/runtime-shared-snippets.ts
|
|
73
|
-
var SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS = `
|
|
74
|
-
function summarizeFlowState(rawState) {
|
|
75
|
-
const state =
|
|
76
|
-
rawState && typeof rawState === "object" && !Array.isArray(rawState)
|
|
77
|
-
? rawState
|
|
78
|
-
: {};
|
|
79
|
-
return {
|
|
80
|
-
stage: typeof state.currentStage === "string" ? state.currentStage : "none",
|
|
81
|
-
completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
|
|
82
|
-
activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function parseKnowledgeDigest(rawKnowledge, currentStage, maxRows = 6) {
|
|
87
|
-
const text = typeof rawKnowledge === "string" ? rawKnowledge : "";
|
|
88
|
-
if (text.trim().length === 0) {
|
|
89
|
-
return { learningsCount: 0, lines: [] };
|
|
90
|
-
}
|
|
91
|
-
const rows = text
|
|
92
|
-
.split(/\\r?\\n/gu)
|
|
93
|
-
.map((line) => line.trim())
|
|
94
|
-
.filter((line) => line.length > 0);
|
|
95
|
-
let learningsCount = 0;
|
|
96
|
-
const parsedRows = [];
|
|
97
|
-
for (const line of rows) {
|
|
98
|
-
if (line.startsWith("{")) learningsCount += 1;
|
|
99
|
-
try {
|
|
100
|
-
const parsed = JSON.parse(line);
|
|
101
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
102
|
-
parsedRows.push(parsed);
|
|
103
|
-
} catch {
|
|
104
|
-
// ignore malformed knowledge line in digest
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const lines = parsedRows
|
|
108
|
-
.filter((row) => {
|
|
109
|
-
const stage = typeof row.stage === "string" ? row.stage : null;
|
|
110
|
-
return stage === null || stage === currentStage;
|
|
111
|
-
})
|
|
112
|
-
.slice(-maxRows)
|
|
113
|
-
.reverse()
|
|
114
|
-
.map((row) => {
|
|
115
|
-
const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
|
|
116
|
-
const stage = typeof row.stage === "string" ? row.stage : "global";
|
|
117
|
-
const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
|
|
118
|
-
const action = typeof row.action === "string" ? row.action : "action";
|
|
119
|
-
return "- [" + confidence + " \u2022 " + stage + "] " + trigger + " -> " + action;
|
|
120
|
-
});
|
|
121
|
-
return { learningsCount, lines };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function activeArtifactsPathLabel(runtimeRoot) {
|
|
125
|
-
return String(runtimeRoot || ".cclaw") + "/artifacts/";
|
|
126
|
-
}
|
|
127
|
-
`;
|
|
128
|
-
var SHARED_STAGE_SUPPORT_SNIPPETS = `
|
|
129
|
-
const STAGE_IDS = ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
|
|
130
|
-
const REVIEW_PROMPT_BY_STAGE = {
|
|
131
|
-
brainstorm: "brainstorm-self-review.md",
|
|
132
|
-
scope: "scope-ceo-review.md",
|
|
133
|
-
design: "design-eng-review.md"
|
|
134
|
-
};
|
|
135
|
-
const REVIEW_PROMPT_FILES = Object.values(REVIEW_PROMPT_BY_STAGE);
|
|
136
|
-
|
|
137
|
-
function isKnownStageId(stage) {
|
|
138
|
-
return typeof stage === "string" && STAGE_IDS.includes(stage);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function reviewPromptFileName(stage) {
|
|
142
|
-
if (!isKnownStageId(stage)) return null;
|
|
143
|
-
const name = REVIEW_PROMPT_BY_STAGE[stage];
|
|
144
|
-
return typeof name === "string" ? name : null;
|
|
145
|
-
}
|
|
146
|
-
`;
|
|
147
|
-
|
|
148
|
-
// src/content/node-hooks.ts
|
|
149
|
-
function resolveCliRuntimeForGeneratedHook() {
|
|
150
|
-
const here = fileURLToPath2(import.meta.url);
|
|
151
|
-
const candidates = [
|
|
152
|
-
path2.resolve(path2.dirname(here), "..", "cli.js"),
|
|
153
|
-
path2.resolve(path2.dirname(here), "..", "..", "dist", "cli.js")
|
|
154
|
-
];
|
|
155
|
-
for (const candidate of candidates) {
|
|
156
|
-
if (existsSync(candidate)) return { entrypoint: candidate, argsPrefix: [] };
|
|
157
|
-
}
|
|
158
|
-
if (process.env.VITEST === "true") {
|
|
159
|
-
const sourceCli = path2.resolve(path2.dirname(here), "..", "cli.ts");
|
|
160
|
-
const viteNode = path2.resolve(path2.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
|
|
161
|
-
if (existsSync(sourceCli) && existsSync(viteNode)) {
|
|
162
|
-
return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { entrypoint: null, argsPrefix: [] };
|
|
166
|
-
}
|
|
167
|
-
function nodeHookRuntimeScript(options = {}) {
|
|
168
|
-
void options;
|
|
169
|
-
const defaultHookProfile = "standard";
|
|
170
|
-
const defaultDisabledHooks = [];
|
|
171
|
-
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
172
|
-
return `#!/usr/bin/env node
|
|
173
|
-
import { createHash } from "node:crypto";
|
|
174
|
-
import fs from "node:fs/promises";
|
|
1
|
+
// src/runtime/run-hook.entry.ts
|
|
175
2
|
import path from "node:path";
|
|
176
|
-
import
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
182
|
-
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
183
|
-
const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
|
|
184
|
-
const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
|
|
185
|
-
const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
|
|
186
|
-
const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
|
|
187
|
-
"session-start",
|
|
188
|
-
"stop-handoff"
|
|
189
|
-
]);
|
|
190
|
-
|
|
191
|
-
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
192
|
-
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
193
|
-
|
|
194
|
-
function normalizeHookToken(value) {
|
|
195
|
-
return String(value == null ? "" : value).trim().toLowerCase();
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
async function runHookByName(projectRoot, hookFile) {
|
|
5
|
+
const hookPath = path.join(projectRoot, ".cclaw", "hooks", hookFile);
|
|
6
|
+
const url = pathToFileURL(hookPath).href;
|
|
7
|
+
await import(url);
|
|
196
8
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function parseDisabledHooksCsv(rawValue) {
|
|
205
|
-
const raw = typeof rawValue === "string" ? rawValue : "";
|
|
206
|
-
if (raw.trim().length === 0) return [];
|
|
207
|
-
const out = [];
|
|
208
|
-
for (const token of raw.split(",")) {
|
|
209
|
-
const normalized = normalizeHookToken(token);
|
|
210
|
-
if (normalized.length === 0) continue;
|
|
211
|
-
if (!out.includes(normalized)) out.push(normalized);
|
|
9
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
10
|
+
const [, , hookFile] = process.argv;
|
|
11
|
+
if (!hookFile) {
|
|
12
|
+
process.stderr.write("usage: run-hook.entry.js <hook-file.mjs>\n");
|
|
13
|
+
process.exit(2);
|
|
212
14
|
}
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function parseInlineYamlList(rawValue) {
|
|
217
|
-
const raw = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
218
|
-
if (!raw.startsWith("[") || !raw.endsWith("]")) return [];
|
|
219
|
-
const inside = raw.slice(1, -1).trim();
|
|
220
|
-
if (inside.length === 0) return [];
|
|
221
|
-
return inside.split(",").map((token) => normalizeHookToken(token.replace(/^['"]|['"]$/g, ""))).filter((token) => token.length > 0);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function parseConfigHookProfile(rawYaml) {
|
|
225
|
-
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
226
|
-
return "";
|
|
227
|
-
}
|
|
228
|
-
const match = rawYaml.match(/^\\s*hookProfile\\s*:\\s*([A-Za-z0-9_-]+)\\s*$/m);
|
|
229
|
-
if (!match || typeof match[1] !== "string") return "";
|
|
230
|
-
return parseHookProfile(match[1], "");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function parseConfigDisabledHooks(rawYaml) {
|
|
234
|
-
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
const lines = rawYaml.split(/\\r?\\n/u);
|
|
238
|
-
const out = [];
|
|
239
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
240
|
-
const line = lines[i];
|
|
241
|
-
const inlineMatch = line.match(/^\\s*disabledHooks\\s*:\\s*(\\[[^\\]]*\\])\\s*$/u);
|
|
242
|
-
if (inlineMatch) {
|
|
243
|
-
for (const value of parseInlineYamlList(inlineMatch[1])) {
|
|
244
|
-
if (!out.includes(value)) out.push(value);
|
|
245
|
-
}
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const blockMatch = line.match(/^(\\s*)disabledHooks\\s*:\\s*$/u);
|
|
249
|
-
if (!blockMatch) continue;
|
|
250
|
-
const baseIndent = blockMatch[1] ? blockMatch[1].length : 0;
|
|
251
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
252
|
-
const nextLine = lines[j];
|
|
253
|
-
const indent = (nextLine.match(/^(\\s*)/u)?.[1].length ?? 0);
|
|
254
|
-
const trimmed = nextLine.trim();
|
|
255
|
-
if (trimmed.length === 0) continue;
|
|
256
|
-
if (indent <= baseIndent) break;
|
|
257
|
-
const itemMatch = nextLine.match(/^\\s*-\\s*(.+?)\\s*$/u);
|
|
258
|
-
if (!itemMatch) continue;
|
|
259
|
-
const normalized = normalizeHookToken(itemMatch[1].replace(/^['"]|['"]$/g, ""));
|
|
260
|
-
if (normalized.length === 0) continue;
|
|
261
|
-
if (!out.includes(normalized)) out.push(normalized);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return out;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function readConfigHookPolicy(root) {
|
|
268
|
-
const configPath = path.join(root, RUNTIME_ROOT, "config.yaml");
|
|
269
|
-
const raw = await readTextFile(configPath, "");
|
|
270
|
-
const profile = parseConfigHookProfile(raw);
|
|
271
|
-
const disabledHooks = parseConfigDisabledHooks(raw);
|
|
272
|
-
return { profile, disabledHooks };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function resolveHookPolicy(root) {
|
|
276
|
-
const fromConfig = await readConfigHookPolicy(root);
|
|
277
|
-
const configProfile = parseHookProfile(fromConfig.profile, DEFAULT_HOOK_PROFILE);
|
|
278
|
-
const configDisabledHooks = Array.isArray(fromConfig.disabledHooks) && fromConfig.disabledHooks.length > 0
|
|
279
|
-
? fromConfig.disabledHooks
|
|
280
|
-
: DEFAULT_DISABLED_HOOKS;
|
|
281
|
-
|
|
282
|
-
const envProfileRaw = process.env.CCLAW_HOOK_PROFILE;
|
|
283
|
-
const envProfile = parseHookProfile(envProfileRaw, "");
|
|
284
|
-
const profile = envProfile.length > 0 ? envProfile : configProfile;
|
|
285
|
-
|
|
286
|
-
const envDisabledRaw = process.env.CCLAW_DISABLED_HOOKS;
|
|
287
|
-
const envDisabledHooks = parseDisabledHooksCsv(envDisabledRaw);
|
|
288
|
-
const disabledHooks = envDisabledHooks.length > 0 ? envDisabledHooks : configDisabledHooks;
|
|
289
|
-
const disabled = new Set(disabledHooks.map((value) => normalizeHookToken(value)));
|
|
290
|
-
return { profile, disabled };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function hookDisabledByProfile(profile, hookName) {
|
|
294
|
-
if (profile !== "minimal") return false;
|
|
295
|
-
return !MINIMAL_PROFILE_ALLOWED_HOOKS.has(hookName);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isHookDisabled(policy, hookName) {
|
|
299
|
-
if (policy.disabled.has(hookName)) return true;
|
|
300
|
-
return hookDisabledByProfile(policy.profile, hookName);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function toObject(value) {
|
|
304
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
305
|
-
return value;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function safeParseJson(raw, fallback = {}) {
|
|
309
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
310
|
-
return fallback;
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
const parsed = JSON.parse(raw);
|
|
314
|
-
return parsed === undefined ? fallback : parsed;
|
|
315
|
-
} catch {
|
|
316
|
-
return fallback;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// === atomic/locked state I/O =========================================
|
|
321
|
-
//
|
|
322
|
-
// The generated hook script runs OUTSIDE the cclaw CLI process, so it
|
|
323
|
-
// cannot import \`fs-utils.ts\`. These helpers mirror \`writeFileSafe\` and
|
|
324
|
-
// \`withDirectoryLock\` just enough to keep hook-owned state files
|
|
325
|
-
// atomic and free of interleaved concurrent writes.
|
|
326
|
-
|
|
327
|
-
function hookSleep(ms) {
|
|
328
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function withDirectoryLockInline(lockPath, fn, options = {}) {
|
|
332
|
-
const retries = Number.isFinite(options.retries) ? options.retries : 200;
|
|
333
|
-
const retryDelayMs = Number.isFinite(options.retryDelayMs) ? options.retryDelayMs : 20;
|
|
334
|
-
const staleAfterMs = Number.isFinite(options.staleAfterMs) ? options.staleAfterMs : 60000;
|
|
335
|
-
try {
|
|
336
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
337
|
-
} catch {
|
|
338
|
-
// parent may already exist
|
|
339
|
-
}
|
|
340
|
-
let acquired = false;
|
|
341
|
-
let lastError = null;
|
|
342
|
-
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
343
|
-
try {
|
|
344
|
-
await fs.mkdir(lockPath);
|
|
345
|
-
acquired = true;
|
|
346
|
-
break;
|
|
347
|
-
} catch (error) {
|
|
348
|
-
lastError = error;
|
|
349
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
350
|
-
if (code !== "EEXIST") {
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
353
|
-
try {
|
|
354
|
-
const stat = await fs.stat(lockPath);
|
|
355
|
-
if (!stat.isDirectory()) {
|
|
356
|
-
throw new Error("Lock path exists but is not a directory: " + lockPath);
|
|
357
|
-
}
|
|
358
|
-
if (Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
359
|
-
await fs.rm(lockPath, { recursive: true, force: true });
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
} catch (statError) {
|
|
363
|
-
if (
|
|
364
|
-
statError instanceof Error &&
|
|
365
|
-
statError.message.startsWith("Lock path exists but is not a directory")
|
|
366
|
-
) {
|
|
367
|
-
throw statError;
|
|
368
|
-
}
|
|
369
|
-
// lock vanished between retries
|
|
370
|
-
}
|
|
371
|
-
await hookSleep(retryDelayMs);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (!acquired) {
|
|
375
|
-
const details = lastError instanceof Error ? lastError.message : String(lastError);
|
|
376
|
-
throw new Error(
|
|
377
|
-
"cclaw hook: failed to acquire lock " + lockPath + " (attempts=" + retries + ", lastError=" + details + ")"
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
try {
|
|
381
|
-
return await fn();
|
|
382
|
-
} finally {
|
|
383
|
-
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function writeFileAtomic(filePath, content, options = {}) {
|
|
388
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
389
|
-
const tempPath = path.join(
|
|
390
|
-
path.dirname(filePath),
|
|
391
|
-
"." + path.basename(filePath) + ".tmp-" + process.pid + "-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8)
|
|
392
|
-
);
|
|
393
|
-
await fs.writeFile(tempPath, content, { encoding: "utf8" });
|
|
394
|
-
// Windows' fs.rename can fail transiently with EPERM/EBUSY/EACCES when the
|
|
395
|
-
// destination file is held open by another process (antivirus, indexer,
|
|
396
|
-
// or a sibling hook invocation racing on the same file). Retry with tiny
|
|
397
|
-
// backoff before falling back to copyFile.
|
|
398
|
-
const renameRetryableCodes = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
399
|
-
let attempt = 0;
|
|
400
|
-
const maxAttempts = 6;
|
|
401
|
-
while (true) {
|
|
402
|
-
try {
|
|
403
|
-
await fs.rename(tempPath, filePath);
|
|
404
|
-
if (options.mode !== undefined) {
|
|
405
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
406
|
-
}
|
|
407
|
-
return;
|
|
408
|
-
} catch (error) {
|
|
409
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
410
|
-
if (code === "EXDEV") {
|
|
411
|
-
try {
|
|
412
|
-
await fs.copyFile(tempPath, filePath);
|
|
413
|
-
} finally {
|
|
414
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
415
|
-
}
|
|
416
|
-
if (options.mode !== undefined) {
|
|
417
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
418
|
-
}
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (renameRetryableCodes.has(code) && attempt < maxAttempts) {
|
|
422
|
-
attempt += 1;
|
|
423
|
-
await hookSleep(10 * attempt + Math.floor(Math.random() * 10));
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
if (renameRetryableCodes.has(code)) {
|
|
427
|
-
// Last-resort fallback: copy-then-unlink. Not atomic, but the
|
|
428
|
-
// directory lock around this call already serializes writers.
|
|
429
|
-
try {
|
|
430
|
-
await fs.copyFile(tempPath, filePath);
|
|
431
|
-
if (options.mode !== undefined) {
|
|
432
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
435
|
-
} finally {
|
|
436
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function lockPathFor(filePath) {
|
|
446
|
-
return filePath + ".lock";
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async function recordHookError(root, stage, detail) {
|
|
450
|
-
try {
|
|
451
|
-
const errorsPath = path.join(root, RUNTIME_ROOT, "state", "hook-errors.jsonl");
|
|
452
|
-
await fs.mkdir(path.dirname(errorsPath), { recursive: true });
|
|
453
|
-
const payload = JSON.stringify({
|
|
454
|
-
ts: new Date().toISOString(),
|
|
455
|
-
stage: typeof stage === "string" ? stage : "unknown",
|
|
456
|
-
detail: typeof detail === "string" ? detail : String(detail)
|
|
457
|
-
});
|
|
458
|
-
await fs.appendFile(errorsPath, payload + "\\n", "utf8");
|
|
459
|
-
} catch {
|
|
460
|
-
// diagnostics must never cascade
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function readJsonFile(filePath, fallback = {}, options = {}) {
|
|
465
|
-
try {
|
|
466
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
467
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
468
|
-
return fallback;
|
|
469
|
-
}
|
|
470
|
-
try {
|
|
471
|
-
const parsed = JSON.parse(raw);
|
|
472
|
-
return parsed === undefined ? fallback : parsed;
|
|
473
|
-
} catch (parseErr) {
|
|
474
|
-
// Emit a diagnostic breadcrumb instead of silently returning fallback.
|
|
475
|
-
// The hook must still continue (soft-fail), but the corruption is
|
|
476
|
-
// now visible in \`state/hook-errors.jsonl\` and to \`npx cclaw-cli sync\`.
|
|
477
|
-
if (options.root) {
|
|
478
|
-
await recordHookError(
|
|
479
|
-
options.root,
|
|
480
|
-
options.stage || "read-json",
|
|
481
|
-
"corrupt-json file=" + filePath + " error=" + (parseErr instanceof Error ? parseErr.message : String(parseErr))
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
return fallback;
|
|
485
|
-
}
|
|
486
|
-
} catch {
|
|
487
|
-
return fallback;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function writeJsonFile(filePath, value) {
|
|
492
|
-
const next = JSON.stringify(value, null, 2) + "\\n";
|
|
493
|
-
await withDirectoryLockInline(lockPathFor(filePath), async () => {
|
|
494
|
-
await writeFileAtomic(filePath, next);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async function readTextFile(filePath, fallback = "") {
|
|
499
|
-
try {
|
|
500
|
-
return await fs.readFile(filePath, "utf8");
|
|
501
|
-
} catch {
|
|
502
|
-
return fallback;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// CLI-compatible knowledge lock. Must match
|
|
507
|
-
// src/knowledge-store.ts::knowledgeLockPath exactly so the hook and the
|
|
508
|
-
// CLI serialize on the same mutex when reading / appending
|
|
509
|
-
// knowledge.jsonl. Drift here re-introduces the race we just closed.
|
|
510
|
-
function knowledgeLockPathInline(root) {
|
|
511
|
-
return path.join(root, RUNTIME_ROOT, "state", ".knowledge.lock");
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async function readTextFileLocked(lockPath, filePath, fallback = "") {
|
|
515
|
-
return withDirectoryLockInline(lockPath, async () => {
|
|
516
|
-
try {
|
|
517
|
-
return await fs.readFile(filePath, "utf8");
|
|
518
|
-
} catch {
|
|
519
|
-
return fallback;
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async function readStdin() {
|
|
525
|
-
return await new Promise((resolve) => {
|
|
526
|
-
let data = "";
|
|
527
|
-
process.stdin.setEncoding("utf8");
|
|
528
|
-
process.stdin.on("data", (chunk) => {
|
|
529
|
-
data += String(chunk);
|
|
530
|
-
});
|
|
531
|
-
process.stdin.on("end", () => resolve(data));
|
|
532
|
-
process.stdin.on("error", () => resolve(""));
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function detectHarness(env) {
|
|
537
|
-
if (env.CLAUDE_PROJECT_DIR) return "claude";
|
|
538
|
-
if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
|
|
539
|
-
if (env.OPENCODE_PROJECT_DIR || env.OPENCODE_PROJECT_ROOT) return "opencode";
|
|
540
|
-
return "codex";
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
async function detectRoot(env) {
|
|
544
|
-
const candidates = [
|
|
545
|
-
env.CCLAW_PROJECT_ROOT,
|
|
546
|
-
env.CLAUDE_PROJECT_DIR,
|
|
547
|
-
env.CURSOR_PROJECT_DIR,
|
|
548
|
-
env.CURSOR_PROJECT_ROOT,
|
|
549
|
-
env.OPENCODE_PROJECT_DIR,
|
|
550
|
-
env.OPENCODE_PROJECT_ROOT,
|
|
551
|
-
process.cwd()
|
|
552
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
553
|
-
for (const candidate of candidates) {
|
|
554
|
-
try {
|
|
555
|
-
const runtimePath = path.join(candidate, RUNTIME_ROOT);
|
|
556
|
-
const stat = await fs.stat(runtimePath);
|
|
557
|
-
if (stat.isDirectory()) return { root: candidate, foundRuntime: true };
|
|
558
|
-
} catch {
|
|
559
|
-
// continue
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return { root: candidates[0] || process.cwd(), foundRuntime: false };
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function normalizeText(value) {
|
|
566
|
-
return String(value || "").replace(/\\s+/gu, " ").trim();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function verifyFlowStateGuardInline(root, hookName) {
|
|
570
|
-
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
571
|
-
const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
|
|
572
|
-
let raw;
|
|
573
|
-
try {
|
|
574
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
575
|
-
} catch {
|
|
576
|
-
return true;
|
|
577
|
-
}
|
|
578
|
-
let guard;
|
|
579
|
-
try {
|
|
580
|
-
const guardRaw = await fs.readFile(guardPath, "utf8");
|
|
581
|
-
guard = JSON.parse(guardRaw);
|
|
582
|
-
} catch {
|
|
583
|
-
return true;
|
|
584
|
-
}
|
|
585
|
-
if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") {
|
|
586
|
-
return true;
|
|
587
|
-
}
|
|
588
|
-
const actual = createHash("sha256").update(raw, "utf8").digest("hex");
|
|
589
|
-
if (actual === guard.sha256) return true;
|
|
590
|
-
const hookLabel = typeof hookName === "string" && hookName.length > 0 ? hookName : "hook";
|
|
591
|
-
process.stderr.write(
|
|
592
|
-
"[cclaw] " + hookLabel + ": flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
|
|
593
|
-
"expected sha: " + guard.sha256 + "\\n" +
|
|
594
|
-
"actual sha: " + actual + "\\n" +
|
|
595
|
-
"last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
|
|
596
|
-
"do not edit flow-state.json by hand. To recover, run:\\n" +
|
|
597
|
-
" cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
|
|
598
|
-
);
|
|
599
|
-
await recordHookError(root, hookLabel, "flow-state guard mismatch actual=" + actual + " expected=" + guard.sha256).catch(() => undefined);
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function readFlowState(root) {
|
|
604
|
-
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
605
|
-
// Loud-on-corrupt: if flow-state.json exists but fails JSON.parse, log
|
|
606
|
-
// a breadcrumb into state/hook-errors.jsonl before falling back to an
|
|
607
|
-
// empty object. Silent fallbacks used to mask stale CLI+hook drift.
|
|
608
|
-
const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
|
|
609
|
-
const obj = toObject(parsed) || {};
|
|
610
|
-
const summary = summarizeFlowState(obj);
|
|
611
|
-
return {
|
|
612
|
-
filePath: statePath,
|
|
613
|
-
currentStage: summary.stage,
|
|
614
|
-
activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
|
|
615
|
-
completedCount: summary.completed,
|
|
616
|
-
raw: obj
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
621
|
-
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
622
|
-
// Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
|
|
623
|
-
// Falls back to a local read if nothing is passed in.
|
|
624
|
-
const raw = typeof prereadRaw === "string"
|
|
625
|
-
? prereadRaw
|
|
626
|
-
: await readTextFile(knowledgeFile, "");
|
|
627
|
-
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
628
|
-
return {
|
|
629
|
-
digestLines: digest.lines,
|
|
630
|
-
learningsCount: digest.learningsCount
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async function readStageSupportContext(root, currentStage) {
|
|
635
|
-
if (!isKnownStageId(currentStage)) return [];
|
|
636
|
-
const stage = currentStage;
|
|
637
|
-
|
|
638
|
-
const parts = [];
|
|
639
|
-
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
640
|
-
const contract = (await readTextFile(contractPath, "")).trim();
|
|
641
|
-
if (contract.length > 0) {
|
|
642
|
-
parts.push(
|
|
643
|
-
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
644
|
-
contract
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const promptName = reviewPromptFileName(stage);
|
|
649
|
-
if (typeof promptName === "string") {
|
|
650
|
-
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
651
|
-
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
652
|
-
if (prompt.length > 0) {
|
|
653
|
-
parts.push(
|
|
654
|
-
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
655
|
-
prompt
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return parts;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async function handleSessionStart(runtime) {
|
|
664
|
-
const state = await readFlowState(runtime.root);
|
|
665
|
-
const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
669
|
-
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
670
|
-
// see a partial (mid-write) snapshot. Both the digest and
|
|
671
|
-
// compound-readiness derive from this single read.
|
|
672
|
-
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
673
|
-
const knowledgeRaw = await readTextFileLocked(
|
|
674
|
-
knowledgeLockPathInline(runtime.root),
|
|
675
|
-
knowledgeFilePath,
|
|
676
|
-
""
|
|
677
|
-
);
|
|
678
|
-
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
679
|
-
|
|
680
|
-
// honest-core: session-start no longer runs background helper
|
|
681
|
-
// pipelines or digest caches. It rehydrates flow + knowledge only.
|
|
682
|
-
const ralphLoopLine = "";
|
|
683
|
-
const earlyLoopLine = "";
|
|
684
|
-
const compoundReadinessLine = "";
|
|
685
|
-
const staleStages = toObject(state.raw.staleStages) || {};
|
|
686
|
-
const staleStageNames = Object.keys(staleStages);
|
|
687
|
-
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
688
|
-
const stageInteractionHint = toObject(interactionHints[state.currentStage]);
|
|
689
|
-
const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
|
|
690
|
-
const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
|
|
691
|
-
? stageInteractionHint.sourceStage
|
|
692
|
-
: "";
|
|
693
|
-
const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
|
|
694
|
-
? stageInteractionHint.recordedAt
|
|
695
|
-
: "";
|
|
696
|
-
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
697
|
-
const ironLawsSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "iron-laws", "SKILL.md");
|
|
698
|
-
const ironLawsContent = (await readTextFile(ironLawsSkillFile, "")).trim();
|
|
699
|
-
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
700
|
-
|
|
701
|
-
const parts = [
|
|
702
|
-
"cclaw loaded. Flow: stage=" +
|
|
703
|
-
state.currentStage +
|
|
704
|
-
" (" +
|
|
705
|
-
String(state.completedCount) +
|
|
706
|
-
"/8 completed, run=" +
|
|
707
|
-
state.activeRunId +
|
|
708
|
-
"). Active artifacts: " +
|
|
709
|
-
activeArtifactsPathLabel(RUNTIME_ROOT) +
|
|
710
|
-
" Learnings: " +
|
|
711
|
-
String(knowledge.learningsCount) +
|
|
712
|
-
" entries."
|
|
713
|
-
];
|
|
714
|
-
if (ralphLoopLine.length > 0) {
|
|
715
|
-
parts.push(ralphLoopLine);
|
|
716
|
-
}
|
|
717
|
-
if (earlyLoopLine.length > 0) {
|
|
718
|
-
parts.push(earlyLoopLine);
|
|
719
|
-
}
|
|
720
|
-
if (compoundReadinessLine.length > 0) {
|
|
721
|
-
parts.push(compoundReadinessLine);
|
|
722
|
-
}
|
|
723
|
-
if (staleStageNames.length > 0) {
|
|
724
|
-
parts.push(
|
|
725
|
-
"Stale stages pending acknowledgement: " +
|
|
726
|
-
staleStageNames.join(", ") +
|
|
727
|
-
" (use npx cclaw-cli internal rewind --ack <stage> after redo)."
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
if (skipQuestionsHintActive) {
|
|
731
|
-
parts.push(
|
|
732
|
-
"Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
|
|
733
|
-
(skipQuestionsSource ? " from " + skipQuestionsSource : "") +
|
|
734
|
-
(skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
|
|
735
|
-
"). Draft with available context unless irreversible/security override checks still require explicit confirmation."
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
if (knowledge.digestLines.length > 0) {
|
|
739
|
-
parts.push(
|
|
740
|
-
"Knowledge digest (top relevant entries):\\n" +
|
|
741
|
-
knowledge.digestLines.join("\\n")
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
if (stageSupportContext.length > 0) {
|
|
745
|
-
parts.push(...stageSupportContext);
|
|
746
|
-
}
|
|
747
|
-
if (metaContent.length > 0) {
|
|
748
|
-
parts.push(metaContent);
|
|
749
|
-
}
|
|
750
|
-
// load iron-laws content into the session-start digest so the
|
|
751
|
-
// non-negotiable workflow constraints are visible from the first turn,
|
|
752
|
-
// not lazily on tool dispatch.
|
|
753
|
-
if (ironLawsContent.length > 0) {
|
|
754
|
-
parts.push(ironLawsContent);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const context = parts.join("\\n");
|
|
758
|
-
if (runtime.harness === "claude" || runtime.harness === "codex") {
|
|
759
|
-
runtime.writeJson({
|
|
760
|
-
hookSpecificOutput: {
|
|
761
|
-
hookEventName: "SessionStart",
|
|
762
|
-
additionalContext: context
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
return 0;
|
|
766
|
-
}
|
|
767
|
-
runtime.writeJson({ additional_context: context });
|
|
768
|
-
return 0;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
async function isGitDirty(root) {
|
|
772
|
-
return await new Promise((resolve) => {
|
|
773
|
-
const child = spawn("git", ["-C", root, "status", "--porcelain"], {
|
|
774
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
775
|
-
});
|
|
776
|
-
let output = "";
|
|
777
|
-
child.stdout.on("data", (chunk) => {
|
|
778
|
-
output += String(chunk);
|
|
779
|
-
});
|
|
780
|
-
child.on("error", () => resolve("unknown"));
|
|
781
|
-
child.on("close", (code) => {
|
|
782
|
-
if (code !== 0) {
|
|
783
|
-
resolve("unknown");
|
|
784
|
-
} else {
|
|
785
|
-
resolve(output.trim().length > 0 ? "dirty" : "clean");
|
|
786
|
-
}
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const STOP_BLOCK_LIMIT_PER_TRANSCRIPT = 2;
|
|
792
|
-
|
|
793
|
-
function asBoolean(value) {
|
|
794
|
-
if (value === true || value === false) return value;
|
|
795
|
-
if (typeof value === "number") return Number.isFinite(value) && value !== 0;
|
|
796
|
-
if (typeof value !== "string") return false;
|
|
797
|
-
const normalized = value.trim().toLowerCase();
|
|
798
|
-
if (normalized.length === 0) return false;
|
|
799
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function stringTokenHit(value, tokens) {
|
|
803
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
804
|
-
if (normalized.length === 0) return false;
|
|
805
|
-
return tokens.some((token) => normalized.includes(token));
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function sanitizeStopSessionKey(raw) {
|
|
809
|
-
const normalized = normalizeText(raw)
|
|
810
|
-
.toLowerCase()
|
|
811
|
-
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
812
|
-
.replace(/^-+|-+$/gu, "");
|
|
813
|
-
return normalized.length > 0 ? normalized.slice(0, 96) : "global";
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function extractStopSignals(input, fallbackSessionKey) {
|
|
817
|
-
const event = toObject(input.event) || {};
|
|
818
|
-
const session = toObject(input.session) || {};
|
|
819
|
-
const contextLimit =
|
|
820
|
-
asBoolean(input.context_limit) ||
|
|
821
|
-
asBoolean(input.contextLimit) ||
|
|
822
|
-
asBoolean(event.context_limit) ||
|
|
823
|
-
asBoolean(event.contextLimit) ||
|
|
824
|
-
stringTokenHit(input.reason, ["context_limit", "context limit"]) ||
|
|
825
|
-
stringTokenHit(event.reason, ["context_limit", "context limit"]) ||
|
|
826
|
-
stringTokenHit(input.stop_reason, ["context_limit", "context limit"]) ||
|
|
827
|
-
stringTokenHit(event.stop_reason, ["context_limit", "context limit"]);
|
|
828
|
-
const userAbort =
|
|
829
|
-
asBoolean(input.user_abort) ||
|
|
830
|
-
asBoolean(input.userAbort) ||
|
|
831
|
-
asBoolean(input.user_cancelled) ||
|
|
832
|
-
asBoolean(input.userCancelled) ||
|
|
833
|
-
asBoolean(event.user_abort) ||
|
|
834
|
-
asBoolean(event.userAbort) ||
|
|
835
|
-
stringTokenHit(input.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
836
|
-
stringTokenHit(event.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
837
|
-
stringTokenHit(input.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
838
|
-
stringTokenHit(event.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]);
|
|
839
|
-
const stopHookActive =
|
|
840
|
-
asBoolean(input.stop_hook_active) ||
|
|
841
|
-
asBoolean(input.stopHookActive) ||
|
|
842
|
-
asBoolean(event.stop_hook_active) ||
|
|
843
|
-
asBoolean(event.stopHookActive);
|
|
844
|
-
|
|
845
|
-
const sessionKeyCandidate =
|
|
846
|
-
(typeof input.transcript_id === "string" && input.transcript_id) ||
|
|
847
|
-
(typeof input.transcriptId === "string" && input.transcriptId) ||
|
|
848
|
-
(typeof input.session_id === "string" && input.session_id) ||
|
|
849
|
-
(typeof input.sessionId === "string" && input.sessionId) ||
|
|
850
|
-
(typeof session.id === "string" && session.id) ||
|
|
851
|
-
fallbackSessionKey;
|
|
852
|
-
const sessionKey = sanitizeStopSessionKey(sessionKeyCandidate);
|
|
853
|
-
|
|
854
|
-
return {
|
|
855
|
-
contextLimit,
|
|
856
|
-
userAbort,
|
|
857
|
-
stopHookActive,
|
|
858
|
-
sessionKey
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
async function handleStopHandoff(runtime) {
|
|
863
|
-
const state = await readFlowState(runtime.root);
|
|
864
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
865
|
-
const input = toObject(runtime.inputData) || {};
|
|
866
|
-
const loopCount =
|
|
867
|
-
typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
|
|
868
|
-
? Math.trunc(input.loop_count)
|
|
869
|
-
: 0;
|
|
870
|
-
|
|
871
|
-
const dirtyState = await isGitDirty(runtime.root);
|
|
872
|
-
const stopSignals = extractStopSignals(input, "run-" + state.activeRunId);
|
|
873
|
-
const safetyBypassActive = stopSignals.stopHookActive || stopSignals.userAbort || stopSignals.contextLimit;
|
|
874
|
-
if (dirtyState === "dirty" && !safetyBypassActive) {
|
|
875
|
-
const stopBlocksPath = path.join(stateDir, "stop-blocks-" + stopSignals.sessionKey + ".json");
|
|
876
|
-
const prior = toObject(await readJsonFile(stopBlocksPath, {})) || {};
|
|
877
|
-
const priorCount =
|
|
878
|
-
typeof prior.blockCount === "number" && Number.isFinite(prior.blockCount)
|
|
879
|
-
? Math.max(0, Math.trunc(prior.blockCount))
|
|
880
|
-
: 0;
|
|
881
|
-
if (priorCount < STOP_BLOCK_LIMIT_PER_TRANSCRIPT) {
|
|
882
|
-
const nextCount = priorCount + 1;
|
|
883
|
-
await writeJsonFile(stopBlocksPath, {
|
|
884
|
-
schemaVersion: 1,
|
|
885
|
-
sessionKey: stopSignals.sessionKey,
|
|
886
|
-
blockCount: nextCount,
|
|
887
|
-
updatedAt: new Date().toISOString()
|
|
888
|
-
});
|
|
889
|
-
process.stderr.write(
|
|
890
|
-
'[cclaw] Stop blocked by iron law "stop-clean-or-handoff": working tree is dirty. Commit/revert changes or record blockers in the current artifact before ending the session.\\n'
|
|
891
|
-
);
|
|
892
|
-
return 1;
|
|
893
|
-
}
|
|
894
|
-
process.stderr.write(
|
|
895
|
-
'[cclaw] Stop advisory: dirty working tree detected, but block limit reached for this transcript (max 2). Continuing with handoff reminder only.\\n'
|
|
896
|
-
);
|
|
897
|
-
} else if (dirtyState === "dirty" && safetyBypassActive) {
|
|
898
|
-
const reason = stopSignals.stopHookActive
|
|
899
|
-
? "stop_hook_active"
|
|
900
|
-
: stopSignals.userAbort
|
|
901
|
-
? "user_abort"
|
|
902
|
-
: "context_limit";
|
|
903
|
-
process.stderr.write(
|
|
904
|
-
"[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const closeoutObj = toObject(state.raw.closeout) || {};
|
|
909
|
-
const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
|
|
910
|
-
const closeoutContext =
|
|
911
|
-
state.currentStage === "ship" || shipSubstate !== "idle"
|
|
912
|
-
? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
|
|
913
|
-
: "";
|
|
914
|
-
|
|
915
|
-
const message =
|
|
916
|
-
"Cclaw: session ending (stage=" +
|
|
917
|
-
state.currentStage +
|
|
918
|
-
", run=" +
|
|
919
|
-
state.activeRunId +
|
|
920
|
-
")." +
|
|
921
|
-
closeoutContext +
|
|
922
|
-
" Active artifacts stay in " +
|
|
923
|
-
RUNTIME_ROOT +
|
|
924
|
-
"/artifacts until archive. Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match current intent, (3) if you discovered a non-obvious rule/pattern during stage work, add it to the current artifact ## Learnings section so stage-complete can harvest it, (4) commit or revert pending changes.";
|
|
925
|
-
|
|
926
|
-
if (runtime.harness === "cursor") {
|
|
927
|
-
if (loopCount === 0) {
|
|
928
|
-
runtime.writeJson({ followup_message: message });
|
|
929
|
-
} else {
|
|
930
|
-
runtime.writeJson({});
|
|
931
|
-
}
|
|
932
|
-
return 0;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
runtime.writeJson({ systemMessage: message });
|
|
936
|
-
return 0;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function normalizeHookName(rawName) {
|
|
940
|
-
const value = normalizeText(rawName).toLowerCase();
|
|
941
|
-
if (value === "session-start") return "session-start";
|
|
942
|
-
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
943
|
-
if (value === "stop-checkpoint") return "stop-handoff";
|
|
944
|
-
if (value === "session-rehydrate") return "session-start";
|
|
945
|
-
return "";
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
async function main() {
|
|
949
|
-
const hookName = normalizeHookName(process.argv[2] || "");
|
|
950
|
-
if (!hookName) {
|
|
951
|
-
process.stderr.write(
|
|
952
|
-
"[cclaw] run-hook: usage: node " +
|
|
953
|
-
RUNTIME_ROOT +
|
|
954
|
-
"/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
|
|
955
|
-
);
|
|
956
|
-
process.exitCode = 1;
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const harness = detectHarness(process.env);
|
|
961
|
-
const { root, foundRuntime } = await detectRoot(process.env);
|
|
962
|
-
if (!foundRuntime) {
|
|
963
|
-
// No .cclaw/ runtime in any candidate root \u2014 this directory is not
|
|
964
|
-
// initialized for cclaw. Exit 0 silently so hooks never block harnesses
|
|
965
|
-
// that run in unrelated repos; users initialize with \`cclaw init\`.
|
|
966
|
-
process.exitCode = 0;
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const inputRaw = await readStdin();
|
|
970
|
-
const inputData = safeParseJson(inputRaw, {});
|
|
971
|
-
const runtime = {
|
|
972
|
-
harness,
|
|
973
|
-
root,
|
|
974
|
-
inputRaw,
|
|
975
|
-
inputData,
|
|
976
|
-
writeJson(value) {
|
|
977
|
-
process.stdout.write(JSON.stringify(value) + "\\n");
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
try {
|
|
982
|
-
const policy = await resolveHookPolicy(runtime.root);
|
|
983
|
-
if (isHookDisabled(policy, hookName)) {
|
|
984
|
-
// Honor CCLAW_HOOK_PROFILE / CCLAW_DISABLED_HOOKS / config disabledHooks.
|
|
985
|
-
// Disabled hooks must exit 0 quietly so harnesses keep running.
|
|
986
|
-
process.exitCode = 0;
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
if (hookName === "session-start" || hookName === "stop-handoff") {
|
|
990
|
-
const guardOk = await verifyFlowStateGuardInline(runtime.root, hookName);
|
|
991
|
-
if (!guardOk) {
|
|
992
|
-
process.exitCode = 2;
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
if (hookName === "session-start") {
|
|
997
|
-
process.exitCode = await handleSessionStart(runtime);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (hookName === "stop-handoff") {
|
|
1001
|
-
process.exitCode = await handleStopHandoff(runtime);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
|
|
1005
|
-
process.exitCode = 1;
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
process.stderr.write(
|
|
1008
|
-
"[cclaw] run-hook: " +
|
|
1009
|
-
hookName +
|
|
1010
|
-
" failed: " +
|
|
1011
|
-
(error instanceof Error ? error.message : String(error)) +
|
|
1012
|
-
"\\n"
|
|
1013
|
-
);
|
|
1014
|
-
process.exitCode = 1;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
void main();
|
|
1019
|
-
`;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// src/runtime/run-hook.entry.ts
|
|
1023
|
-
function buildRunHookRuntimeScript(options = {}) {
|
|
1024
|
-
return nodeHookRuntimeScript(options);
|
|
15
|
+
await runHookByName(process.cwd(), hookFile);
|
|
1025
16
|
}
|
|
1026
|
-
var run_hook_entry_default = buildRunHookRuntimeScript;
|
|
1027
17
|
export {
|
|
1028
|
-
|
|
1029
|
-
run_hook_entry_default as default
|
|
18
|
+
runHookByName
|
|
1030
19
|
};
|