cclaw-cli 7.7.1 → 8.1.1
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 +211 -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 +107 -511
- 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 -768
- 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 -134
- 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/harness-prompt.d.ts +26 -0
- package/dist/harness-prompt.js +142 -0
- package/dist/install.d.ts +35 -15
- package/dist/install.js +238 -1347
- 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 -44
- package/dist/execution-topology.js +0 -95
- 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 -74
- package/dist/internal/wave-status.js +0 -506
- 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,887 +1,237 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { RUNTIME_ROOT } from "../constants.js";
|
|
5
|
-
import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
|
|
6
|
-
function resolveCliRuntimeForGeneratedHook() {
|
|
7
|
-
const here = fileURLToPath(import.meta.url);
|
|
8
|
-
const candidates = [
|
|
9
|
-
path.resolve(path.dirname(here), "..", "cli.js"),
|
|
10
|
-
path.resolve(path.dirname(here), "..", "..", "dist", "cli.js")
|
|
11
|
-
];
|
|
12
|
-
for (const candidate of candidates) {
|
|
13
|
-
// Synchronous probe runs only during cclaw-cli init/sync generation.
|
|
14
|
-
if (existsSync(candidate))
|
|
15
|
-
return { entrypoint: candidate, argsPrefix: [] };
|
|
16
|
-
}
|
|
17
|
-
// Vitest exercises init/sync directly from src/ without a compiled dist/.
|
|
18
|
-
// Route that dev-only shape through vite-node so hooks still prove a local runtime.
|
|
19
|
-
if (process.env.VITEST === "true") {
|
|
20
|
-
const sourceCli = path.resolve(path.dirname(here), "..", "cli.ts");
|
|
21
|
-
const viteNode = path.resolve(path.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
|
|
22
|
-
if (existsSync(sourceCli) && existsSync(viteNode)) {
|
|
23
|
-
return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return { entrypoint: null, argsPrefix: [] };
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Node-only hook runtime (single entrypoint).
|
|
30
|
-
*
|
|
31
|
-
* Generated into `.cclaw/hooks/run-hook.mjs` and used by all harnesses to avoid
|
|
32
|
-
* bash/python/jq runtime dependencies.
|
|
33
|
-
*/
|
|
34
|
-
export function nodeHookRuntimeScript(options = {}) {
|
|
35
|
-
void options;
|
|
36
|
-
const defaultHookProfile = "standard";
|
|
37
|
-
const defaultDisabledHooks = [];
|
|
38
|
-
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
39
|
-
return `#!/usr/bin/env node
|
|
40
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
const SESSION_START_HOOK = `#!/usr/bin/env node
|
|
2
|
+
// cclaw session-start: rehydrate flow state and surface active slug.
|
|
41
3
|
import fs from "node:fs/promises";
|
|
42
4
|
import path from "node:path";
|
|
43
|
-
import process from "node:process";
|
|
44
|
-
import { spawn } from "node:child_process";
|
|
45
|
-
|
|
46
|
-
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
47
|
-
const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
|
|
48
|
-
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
49
|
-
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
50
|
-
const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
|
|
51
|
-
const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
|
|
52
|
-
const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
|
|
53
|
-
const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
|
|
54
|
-
"session-start",
|
|
55
|
-
"stop-handoff"
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
59
|
-
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
60
|
-
|
|
61
|
-
function normalizeHookToken(value) {
|
|
62
|
-
return String(value == null ? "" : value).trim().toLowerCase();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseHookProfile(rawValue, fallback = "standard") {
|
|
66
|
-
const normalized = normalizeHookToken(rawValue);
|
|
67
|
-
if (HOOK_PROFILE_VALUES.has(normalized)) return normalized;
|
|
68
|
-
return fallback;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseDisabledHooksCsv(rawValue) {
|
|
72
|
-
const raw = typeof rawValue === "string" ? rawValue : "";
|
|
73
|
-
if (raw.trim().length === 0) return [];
|
|
74
|
-
const out = [];
|
|
75
|
-
for (const token of raw.split(",")) {
|
|
76
|
-
const normalized = normalizeHookToken(token);
|
|
77
|
-
if (normalized.length === 0) continue;
|
|
78
|
-
if (!out.includes(normalized)) out.push(normalized);
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parseInlineYamlList(rawValue) {
|
|
84
|
-
const raw = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
85
|
-
if (!raw.startsWith("[") || !raw.endsWith("]")) return [];
|
|
86
|
-
const inside = raw.slice(1, -1).trim();
|
|
87
|
-
if (inside.length === 0) return [];
|
|
88
|
-
return inside.split(",").map((token) => normalizeHookToken(token.replace(/^['"]|['"]$/g, ""))).filter((token) => token.length > 0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function parseConfigHookProfile(rawYaml) {
|
|
92
|
-
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
93
|
-
return "";
|
|
94
|
-
}
|
|
95
|
-
const match = rawYaml.match(/^\\s*hookProfile\\s*:\\s*([A-Za-z0-9_-]+)\\s*$/m);
|
|
96
|
-
if (!match || typeof match[1] !== "string") return "";
|
|
97
|
-
return parseHookProfile(match[1], "");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function parseConfigDisabledHooks(rawYaml) {
|
|
101
|
-
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
const lines = rawYaml.split(/\\r?\\n/u);
|
|
105
|
-
const out = [];
|
|
106
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
107
|
-
const line = lines[i];
|
|
108
|
-
const inlineMatch = line.match(/^\\s*disabledHooks\\s*:\\s*(\\[[^\\]]*\\])\\s*$/u);
|
|
109
|
-
if (inlineMatch) {
|
|
110
|
-
for (const value of parseInlineYamlList(inlineMatch[1])) {
|
|
111
|
-
if (!out.includes(value)) out.push(value);
|
|
112
|
-
}
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const blockMatch = line.match(/^(\\s*)disabledHooks\\s*:\\s*$/u);
|
|
116
|
-
if (!blockMatch) continue;
|
|
117
|
-
const baseIndent = blockMatch[1] ? blockMatch[1].length : 0;
|
|
118
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
119
|
-
const nextLine = lines[j];
|
|
120
|
-
const indent = (nextLine.match(/^(\\s*)/u)?.[1].length ?? 0);
|
|
121
|
-
const trimmed = nextLine.trim();
|
|
122
|
-
if (trimmed.length === 0) continue;
|
|
123
|
-
if (indent <= baseIndent) break;
|
|
124
|
-
const itemMatch = nextLine.match(/^\\s*-\\s*(.+?)\\s*$/u);
|
|
125
|
-
if (!itemMatch) continue;
|
|
126
|
-
const normalized = normalizeHookToken(itemMatch[1].replace(/^['"]|['"]$/g, ""));
|
|
127
|
-
if (normalized.length === 0) continue;
|
|
128
|
-
if (!out.includes(normalized)) out.push(normalized);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function readConfigHookPolicy(root) {
|
|
135
|
-
const configPath = path.join(root, RUNTIME_ROOT, "config.yaml");
|
|
136
|
-
const raw = await readTextFile(configPath, "");
|
|
137
|
-
const profile = parseConfigHookProfile(raw);
|
|
138
|
-
const disabledHooks = parseConfigDisabledHooks(raw);
|
|
139
|
-
return { profile, disabledHooks };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function resolveHookPolicy(root) {
|
|
143
|
-
const fromConfig = await readConfigHookPolicy(root);
|
|
144
|
-
const configProfile = parseHookProfile(fromConfig.profile, DEFAULT_HOOK_PROFILE);
|
|
145
|
-
const configDisabledHooks = Array.isArray(fromConfig.disabledHooks) && fromConfig.disabledHooks.length > 0
|
|
146
|
-
? fromConfig.disabledHooks
|
|
147
|
-
: DEFAULT_DISABLED_HOOKS;
|
|
148
|
-
|
|
149
|
-
const envProfileRaw = process.env.CCLAW_HOOK_PROFILE;
|
|
150
|
-
const envProfile = parseHookProfile(envProfileRaw, "");
|
|
151
|
-
const profile = envProfile.length > 0 ? envProfile : configProfile;
|
|
152
|
-
|
|
153
|
-
const envDisabledRaw = process.env.CCLAW_DISABLED_HOOKS;
|
|
154
|
-
const envDisabledHooks = parseDisabledHooksCsv(envDisabledRaw);
|
|
155
|
-
const disabledHooks = envDisabledHooks.length > 0 ? envDisabledHooks : configDisabledHooks;
|
|
156
|
-
const disabled = new Set(disabledHooks.map((value) => normalizeHookToken(value)));
|
|
157
|
-
return { profile, disabled };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function hookDisabledByProfile(profile, hookName) {
|
|
161
|
-
if (profile !== "minimal") return false;
|
|
162
|
-
return !MINIMAL_PROFILE_ALLOWED_HOOKS.has(hookName);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isHookDisabled(policy, hookName) {
|
|
166
|
-
if (policy.disabled.has(hookName)) return true;
|
|
167
|
-
return hookDisabledByProfile(policy.profile, hookName);
|
|
168
|
-
}
|
|
169
5
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return value;
|
|
173
|
-
}
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const statePath = path.join(root, ".cclaw", "state", "flow-state.json");
|
|
174
8
|
|
|
175
|
-
function
|
|
176
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
177
|
-
return fallback;
|
|
178
|
-
}
|
|
9
|
+
async function readState() {
|
|
179
10
|
try {
|
|
180
|
-
|
|
181
|
-
return parsed === undefined ? fallback : parsed;
|
|
11
|
+
return JSON.parse(await fs.readFile(statePath, "utf8"));
|
|
182
12
|
} catch {
|
|
183
|
-
return
|
|
13
|
+
return null;
|
|
184
14
|
}
|
|
185
15
|
}
|
|
186
16
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// \`withDirectoryLock\` just enough to keep hook-owned state files
|
|
192
|
-
// atomic and free of interleaved concurrent writes.
|
|
193
|
-
|
|
194
|
-
function hookSleep(ms) {
|
|
195
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function withDirectoryLockInline(lockPath, fn, options = {}) {
|
|
199
|
-
const retries = Number.isFinite(options.retries) ? options.retries : 200;
|
|
200
|
-
const retryDelayMs = Number.isFinite(options.retryDelayMs) ? options.retryDelayMs : 20;
|
|
201
|
-
const staleAfterMs = Number.isFinite(options.staleAfterMs) ? options.staleAfterMs : 60000;
|
|
202
|
-
try {
|
|
203
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
204
|
-
} catch {
|
|
205
|
-
// parent may already exist
|
|
206
|
-
}
|
|
207
|
-
let acquired = false;
|
|
208
|
-
let lastError = null;
|
|
209
|
-
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
210
|
-
try {
|
|
211
|
-
await fs.mkdir(lockPath);
|
|
212
|
-
acquired = true;
|
|
213
|
-
break;
|
|
214
|
-
} catch (error) {
|
|
215
|
-
lastError = error;
|
|
216
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
217
|
-
if (code !== "EEXIST") {
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const stat = await fs.stat(lockPath);
|
|
222
|
-
if (!stat.isDirectory()) {
|
|
223
|
-
throw new Error("Lock path exists but is not a directory: " + lockPath);
|
|
224
|
-
}
|
|
225
|
-
if (Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
226
|
-
await fs.rm(lockPath, { recursive: true, force: true });
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
} catch (statError) {
|
|
230
|
-
if (
|
|
231
|
-
statError instanceof Error &&
|
|
232
|
-
statError.message.startsWith("Lock path exists but is not a directory")
|
|
233
|
-
) {
|
|
234
|
-
throw statError;
|
|
235
|
-
}
|
|
236
|
-
// lock vanished between retries
|
|
237
|
-
}
|
|
238
|
-
await hookSleep(retryDelayMs);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (!acquired) {
|
|
242
|
-
const details = lastError instanceof Error ? lastError.message : String(lastError);
|
|
243
|
-
throw new Error(
|
|
244
|
-
"cclaw hook: failed to acquire lock " + lockPath + " (attempts=" + retries + ", lastError=" + details + ")"
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
return await fn();
|
|
249
|
-
} finally {
|
|
250
|
-
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function writeFileAtomic(filePath, content, options = {}) {
|
|
255
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
256
|
-
const tempPath = path.join(
|
|
257
|
-
path.dirname(filePath),
|
|
258
|
-
"." + path.basename(filePath) + ".tmp-" + process.pid + "-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8)
|
|
259
|
-
);
|
|
260
|
-
await fs.writeFile(tempPath, content, { encoding: "utf8" });
|
|
261
|
-
// Windows' fs.rename can fail transiently with EPERM/EBUSY/EACCES when the
|
|
262
|
-
// destination file is held open by another process (antivirus, indexer,
|
|
263
|
-
// or a sibling hook invocation racing on the same file). Retry with tiny
|
|
264
|
-
// backoff before falling back to copyFile.
|
|
265
|
-
const renameRetryableCodes = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
266
|
-
let attempt = 0;
|
|
267
|
-
const maxAttempts = 6;
|
|
268
|
-
while (true) {
|
|
269
|
-
try {
|
|
270
|
-
await fs.rename(tempPath, filePath);
|
|
271
|
-
if (options.mode !== undefined) {
|
|
272
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
273
|
-
}
|
|
274
|
-
return;
|
|
275
|
-
} catch (error) {
|
|
276
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
277
|
-
if (code === "EXDEV") {
|
|
278
|
-
try {
|
|
279
|
-
await fs.copyFile(tempPath, filePath);
|
|
280
|
-
} finally {
|
|
281
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
282
|
-
}
|
|
283
|
-
if (options.mode !== undefined) {
|
|
284
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (renameRetryableCodes.has(code) && attempt < maxAttempts) {
|
|
289
|
-
attempt += 1;
|
|
290
|
-
await hookSleep(10 * attempt + Math.floor(Math.random() * 10));
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (renameRetryableCodes.has(code)) {
|
|
294
|
-
// Last-resort fallback: copy-then-unlink. Not atomic, but the
|
|
295
|
-
// directory lock around this call already serializes writers.
|
|
296
|
-
try {
|
|
297
|
-
await fs.copyFile(tempPath, filePath);
|
|
298
|
-
if (options.mode !== undefined) {
|
|
299
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
300
|
-
}
|
|
301
|
-
return;
|
|
302
|
-
} finally {
|
|
303
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
17
|
+
const state = await readState();
|
|
18
|
+
if (!state) {
|
|
19
|
+
console.log("[cclaw] no active flow. Use /cc <task> to start.");
|
|
20
|
+
process.exit(0);
|
|
310
21
|
}
|
|
311
22
|
|
|
312
|
-
|
|
313
|
-
|
|
23
|
+
if (state.schemaVersion !== 2) {
|
|
24
|
+
console.error("[cclaw] flow-state schema is from cclaw 7.x. cclaw v8 cannot resume it.");
|
|
25
|
+
console.error("[cclaw] options: 1) finish/abandon the run with cclaw 7.x; 2) delete .cclaw/state/flow-state.json; 3) start a new v8 plan.");
|
|
26
|
+
process.exit(0);
|
|
314
27
|
}
|
|
315
28
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await fs.mkdir(path.dirname(errorsPath), { recursive: true });
|
|
320
|
-
const payload = JSON.stringify({
|
|
321
|
-
ts: new Date().toISOString(),
|
|
322
|
-
stage: typeof stage === "string" ? stage : "unknown",
|
|
323
|
-
detail: typeof detail === "string" ? detail : String(detail)
|
|
324
|
-
});
|
|
325
|
-
await fs.appendFile(errorsPath, payload + "\\n", "utf8");
|
|
326
|
-
} catch {
|
|
327
|
-
// diagnostics must never cascade
|
|
328
|
-
}
|
|
29
|
+
if (!state.currentSlug) {
|
|
30
|
+
console.log("[cclaw] no active slug. Use /cc <task> to start.");
|
|
31
|
+
process.exit(0);
|
|
329
32
|
}
|
|
330
33
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return parsed === undefined ? fallback : parsed;
|
|
340
|
-
} catch (parseErr) {
|
|
341
|
-
// Emit a diagnostic breadcrumb instead of silently returning fallback.
|
|
342
|
-
// The hook must still continue (soft-fail), but the corruption is
|
|
343
|
-
// now visible in \`state/hook-errors.jsonl\` and to \`npx cclaw-cli sync\`.
|
|
344
|
-
if (options.root) {
|
|
345
|
-
await recordHookError(
|
|
346
|
-
options.root,
|
|
347
|
-
options.stage || "read-json",
|
|
348
|
-
"corrupt-json file=" + filePath + " error=" + (parseErr instanceof Error ? parseErr.message : String(parseErr))
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
return fallback;
|
|
352
|
-
}
|
|
353
|
-
} catch {
|
|
354
|
-
return fallback;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
34
|
+
const pending = (state.ac || []).filter((item) => item.status !== "committed").length;
|
|
35
|
+
const total = (state.ac || []).length;
|
|
36
|
+
console.log(\`[cclaw] active: \${state.currentSlug} (stage=\${state.currentStage ?? "n/a"}); AC committed \${total - pending}/\${total}\`);
|
|
37
|
+
`;
|
|
38
|
+
const STOP_HANDOFF_HOOK = `#!/usr/bin/env node
|
|
39
|
+
// cclaw stop-handoff: short reminder when the agent stops mid-flow.
|
|
40
|
+
import fs from "node:fs/promises";
|
|
41
|
+
import path from "node:path";
|
|
357
42
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
await withDirectoryLockInline(lockPathFor(filePath), async () => {
|
|
361
|
-
await writeFileAtomic(filePath, next);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
43
|
+
const root = process.cwd();
|
|
44
|
+
const statePath = path.join(root, ".cclaw", "state", "flow-state.json");
|
|
364
45
|
|
|
365
|
-
async function
|
|
46
|
+
async function readState() {
|
|
366
47
|
try {
|
|
367
|
-
return await fs.readFile(
|
|
48
|
+
return JSON.parse(await fs.readFile(statePath, "utf8"));
|
|
368
49
|
} catch {
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// CLI-compatible knowledge lock. Must match
|
|
374
|
-
// src/knowledge-store.ts::knowledgeLockPath exactly so the hook and the
|
|
375
|
-
// CLI serialize on the same mutex when reading / appending
|
|
376
|
-
// knowledge.jsonl. Drift here re-introduces the race we just closed.
|
|
377
|
-
function knowledgeLockPathInline(root) {
|
|
378
|
-
return path.join(root, RUNTIME_ROOT, "state", ".knowledge.lock");
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function readTextFileLocked(lockPath, filePath, fallback = "") {
|
|
382
|
-
return withDirectoryLockInline(lockPath, async () => {
|
|
383
|
-
try {
|
|
384
|
-
return await fs.readFile(filePath, "utf8");
|
|
385
|
-
} catch {
|
|
386
|
-
return fallback;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async function readStdin() {
|
|
392
|
-
return await new Promise((resolve) => {
|
|
393
|
-
let data = "";
|
|
394
|
-
process.stdin.setEncoding("utf8");
|
|
395
|
-
process.stdin.on("data", (chunk) => {
|
|
396
|
-
data += String(chunk);
|
|
397
|
-
});
|
|
398
|
-
process.stdin.on("end", () => resolve(data));
|
|
399
|
-
process.stdin.on("error", () => resolve(""));
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function detectHarness(env) {
|
|
404
|
-
if (env.CLAUDE_PROJECT_DIR) return "claude";
|
|
405
|
-
if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
|
|
406
|
-
if (env.OPENCODE_PROJECT_DIR || env.OPENCODE_PROJECT_ROOT) return "opencode";
|
|
407
|
-
return "codex";
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async function detectRoot(env) {
|
|
411
|
-
const candidates = [
|
|
412
|
-
env.CCLAW_PROJECT_ROOT,
|
|
413
|
-
env.CLAUDE_PROJECT_DIR,
|
|
414
|
-
env.CURSOR_PROJECT_DIR,
|
|
415
|
-
env.CURSOR_PROJECT_ROOT,
|
|
416
|
-
env.OPENCODE_PROJECT_DIR,
|
|
417
|
-
env.OPENCODE_PROJECT_ROOT,
|
|
418
|
-
process.cwd()
|
|
419
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
420
|
-
for (const candidate of candidates) {
|
|
421
|
-
try {
|
|
422
|
-
const runtimePath = path.join(candidate, RUNTIME_ROOT);
|
|
423
|
-
const stat = await fs.stat(runtimePath);
|
|
424
|
-
if (stat.isDirectory()) return { root: candidate, foundRuntime: true };
|
|
425
|
-
} catch {
|
|
426
|
-
// continue
|
|
427
|
-
}
|
|
50
|
+
return null;
|
|
428
51
|
}
|
|
429
|
-
return { root: candidates[0] || process.cwd(), foundRuntime: false };
|
|
430
52
|
}
|
|
431
53
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
let guard;
|
|
446
|
-
try {
|
|
447
|
-
const guardRaw = await fs.readFile(guardPath, "utf8");
|
|
448
|
-
guard = JSON.parse(guardRaw);
|
|
449
|
-
} catch {
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") {
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
const actual = createHash("sha256").update(raw, "utf8").digest("hex");
|
|
456
|
-
if (actual === guard.sha256) return true;
|
|
457
|
-
const hookLabel = typeof hookName === "string" && hookName.length > 0 ? hookName : "hook";
|
|
458
|
-
process.stderr.write(
|
|
459
|
-
"[cclaw] " + hookLabel + ": flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
|
|
460
|
-
"expected sha: " + guard.sha256 + "\\n" +
|
|
461
|
-
"actual sha: " + actual + "\\n" +
|
|
462
|
-
"last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
|
|
463
|
-
"do not edit flow-state.json by hand. To recover, run:\\n" +
|
|
464
|
-
" cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
|
|
465
|
-
);
|
|
466
|
-
await recordHookError(root, hookLabel, "flow-state guard mismatch actual=" + actual + " expected=" + guard.sha256).catch(() => undefined);
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
54
|
+
const state = await readState();
|
|
55
|
+
if (!state || !state.currentSlug) process.exit(0);
|
|
56
|
+
const pending = (state.ac || []).filter((item) => item.status !== "committed");
|
|
57
|
+
if (pending.length === 0) process.exit(0);
|
|
58
|
+
console.error(\`[cclaw] stopping with \${pending.length} pending AC for \${state.currentSlug}: \${pending.map((item) => item.id).join(", ")}\`);
|
|
59
|
+
`;
|
|
60
|
+
const COMMIT_HELPER_HOOK = `#!/usr/bin/env node
|
|
61
|
+
// cclaw commit-helper: TDD-aware atomic commit per AC phase
|
|
62
|
+
// (RED -> GREEN -> REFACTOR) + AC traceability gate.
|
|
63
|
+
import { execFileSync } from "node:child_process";
|
|
64
|
+
import fs from "node:fs/promises";
|
|
65
|
+
import path from "node:path";
|
|
469
66
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
// Loud-on-corrupt: if flow-state.json exists but fails JSON.parse, log
|
|
473
|
-
// a breadcrumb into state/hook-errors.jsonl before falling back to an
|
|
474
|
-
// empty object. Silent fallbacks used to mask stale CLI+hook drift.
|
|
475
|
-
const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
|
|
476
|
-
const obj = toObject(parsed) || {};
|
|
477
|
-
const summary = summarizeFlowState(obj);
|
|
478
|
-
return {
|
|
479
|
-
filePath: statePath,
|
|
480
|
-
currentStage: summary.stage,
|
|
481
|
-
activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
|
|
482
|
-
completedCount: summary.completed,
|
|
483
|
-
raw: obj
|
|
484
|
-
};
|
|
485
|
-
}
|
|
67
|
+
const root = process.cwd();
|
|
68
|
+
const statePath = path.join(root, ".cclaw", "state", "flow-state.json");
|
|
486
69
|
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const raw = typeof prereadRaw === "string"
|
|
492
|
-
? prereadRaw
|
|
493
|
-
: await readTextFile(knowledgeFile, "");
|
|
494
|
-
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
495
|
-
return {
|
|
496
|
-
digestLines: digest.lines,
|
|
497
|
-
learningsCount: digest.learningsCount
|
|
498
|
-
};
|
|
70
|
+
function arg(name) {
|
|
71
|
+
const prefix = \`--\${name}=\`;
|
|
72
|
+
const found = process.argv.find((value) => value.startsWith(prefix));
|
|
73
|
+
return found ? found.slice(prefix.length) : null;
|
|
499
74
|
}
|
|
500
75
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const stage = currentStage;
|
|
504
|
-
|
|
505
|
-
const parts = [];
|
|
506
|
-
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
507
|
-
const contract = (await readTextFile(contractPath, "")).trim();
|
|
508
|
-
if (contract.length > 0) {
|
|
509
|
-
parts.push(
|
|
510
|
-
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
511
|
-
contract
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const promptName = reviewPromptFileName(stage);
|
|
516
|
-
if (typeof promptName === "string") {
|
|
517
|
-
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
518
|
-
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
519
|
-
if (prompt.length > 0) {
|
|
520
|
-
parts.push(
|
|
521
|
-
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
522
|
-
prompt
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return parts;
|
|
76
|
+
function flag(name) {
|
|
77
|
+
return process.argv.includes(\`--\${name}\`);
|
|
528
78
|
}
|
|
529
79
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
536
|
-
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
537
|
-
// see a partial (mid-write) snapshot. Both the digest and
|
|
538
|
-
// compound-readiness derive from this single read.
|
|
539
|
-
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
540
|
-
const knowledgeRaw = await readTextFileLocked(
|
|
541
|
-
knowledgeLockPathInline(runtime.root),
|
|
542
|
-
knowledgeFilePath,
|
|
543
|
-
""
|
|
544
|
-
);
|
|
545
|
-
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
80
|
+
const acId = arg("ac");
|
|
81
|
+
const phase = arg("phase");
|
|
82
|
+
const message = arg("message") ?? \`cclaw: progress on \${acId ?? "AC"}\`;
|
|
83
|
+
const skipped = flag("skipped");
|
|
546
84
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const earlyLoopLine = "";
|
|
551
|
-
const compoundReadinessLine = "";
|
|
552
|
-
const staleStages = toObject(state.raw.staleStages) || {};
|
|
553
|
-
const staleStageNames = Object.keys(staleStages);
|
|
554
|
-
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
555
|
-
const stageInteractionHint = toObject(interactionHints[state.currentStage]);
|
|
556
|
-
const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
|
|
557
|
-
const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
|
|
558
|
-
? stageInteractionHint.sourceStage
|
|
559
|
-
: "";
|
|
560
|
-
const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
|
|
561
|
-
? stageInteractionHint.recordedAt
|
|
562
|
-
: "";
|
|
563
|
-
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
564
|
-
const ironLawsSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "iron-laws", "SKILL.md");
|
|
565
|
-
const ironLawsContent = (await readTextFile(ironLawsSkillFile, "")).trim();
|
|
566
|
-
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
567
|
-
|
|
568
|
-
const parts = [
|
|
569
|
-
"cclaw loaded. Flow: stage=" +
|
|
570
|
-
state.currentStage +
|
|
571
|
-
" (" +
|
|
572
|
-
String(state.completedCount) +
|
|
573
|
-
"/8 completed, run=" +
|
|
574
|
-
state.activeRunId +
|
|
575
|
-
"). Active artifacts: " +
|
|
576
|
-
activeArtifactsPathLabel(RUNTIME_ROOT) +
|
|
577
|
-
" Learnings: " +
|
|
578
|
-
String(knowledge.learningsCount) +
|
|
579
|
-
" entries."
|
|
580
|
-
];
|
|
581
|
-
if (ralphLoopLine.length > 0) {
|
|
582
|
-
parts.push(ralphLoopLine);
|
|
583
|
-
}
|
|
584
|
-
if (earlyLoopLine.length > 0) {
|
|
585
|
-
parts.push(earlyLoopLine);
|
|
586
|
-
}
|
|
587
|
-
if (compoundReadinessLine.length > 0) {
|
|
588
|
-
parts.push(compoundReadinessLine);
|
|
589
|
-
}
|
|
590
|
-
if (staleStageNames.length > 0) {
|
|
591
|
-
parts.push(
|
|
592
|
-
"Stale stages pending acknowledgement: " +
|
|
593
|
-
staleStageNames.join(", ") +
|
|
594
|
-
" (use npx cclaw-cli internal rewind --ack <stage> after redo)."
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
if (skipQuestionsHintActive) {
|
|
598
|
-
parts.push(
|
|
599
|
-
"Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
|
|
600
|
-
(skipQuestionsSource ? " from " + skipQuestionsSource : "") +
|
|
601
|
-
(skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
|
|
602
|
-
"). Draft with available context unless irreversible/security override checks still require explicit confirmation."
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
if (knowledge.digestLines.length > 0) {
|
|
606
|
-
parts.push(
|
|
607
|
-
"Knowledge digest (top relevant entries):\\n" +
|
|
608
|
-
knowledge.digestLines.join("\\n")
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
if (stageSupportContext.length > 0) {
|
|
612
|
-
parts.push(...stageSupportContext);
|
|
613
|
-
}
|
|
614
|
-
if (metaContent.length > 0) {
|
|
615
|
-
parts.push(metaContent);
|
|
616
|
-
}
|
|
617
|
-
// load iron-laws content into the session-start digest so the
|
|
618
|
-
// non-negotiable workflow constraints are visible from the first turn,
|
|
619
|
-
// not lazily on tool dispatch.
|
|
620
|
-
if (ironLawsContent.length > 0) {
|
|
621
|
-
parts.push(ironLawsContent);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const context = parts.join("\\n");
|
|
625
|
-
if (runtime.harness === "claude" || runtime.harness === "codex") {
|
|
626
|
-
runtime.writeJson({
|
|
627
|
-
hookSpecificOutput: {
|
|
628
|
-
hookEventName: "SessionStart",
|
|
629
|
-
additionalContext: context
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
return 0;
|
|
633
|
-
}
|
|
634
|
-
runtime.writeJson({ additional_context: context });
|
|
635
|
-
return 0;
|
|
85
|
+
if (!acId || !/^AC-\\d+$/.test(acId)) {
|
|
86
|
+
console.error("[commit-helper] usage: commit-helper.mjs --ac=AC-N --phase=red|green|refactor [--skipped] [--message='...']");
|
|
87
|
+
process.exit(2);
|
|
636
88
|
}
|
|
637
89
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
});
|
|
643
|
-
let output = "";
|
|
644
|
-
child.stdout.on("data", (chunk) => {
|
|
645
|
-
output += String(chunk);
|
|
646
|
-
});
|
|
647
|
-
child.on("error", () => resolve("unknown"));
|
|
648
|
-
child.on("close", (code) => {
|
|
649
|
-
if (code !== 0) {
|
|
650
|
-
resolve("unknown");
|
|
651
|
-
} else {
|
|
652
|
-
resolve(output.trim().length > 0 ? "dirty" : "clean");
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
});
|
|
90
|
+
if (!phase || !["red", "green", "refactor"].includes(phase)) {
|
|
91
|
+
console.error("[commit-helper] --phase is required. Allowed: red, green, refactor.");
|
|
92
|
+
console.error("[commit-helper] build is a TDD cycle: every AC needs RED -> GREEN -> REFACTOR.");
|
|
93
|
+
process.exit(2);
|
|
656
94
|
}
|
|
657
95
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (value === true || value === false) return value;
|
|
662
|
-
if (typeof value === "number") return Number.isFinite(value) && value !== 0;
|
|
663
|
-
if (typeof value !== "string") return false;
|
|
664
|
-
const normalized = value.trim().toLowerCase();
|
|
665
|
-
if (normalized.length === 0) return false;
|
|
666
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
96
|
+
if (skipped && phase !== "refactor") {
|
|
97
|
+
console.error("[commit-helper] --skipped is only valid for --phase=refactor.");
|
|
98
|
+
process.exit(2);
|
|
667
99
|
}
|
|
668
100
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
101
|
+
let state;
|
|
102
|
+
try {
|
|
103
|
+
state = JSON.parse(await fs.readFile(statePath, "utf8"));
|
|
104
|
+
} catch {
|
|
105
|
+
console.error("[commit-helper] no flow-state.json. Start a flow with /cc first.");
|
|
106
|
+
process.exit(2);
|
|
673
107
|
}
|
|
674
108
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
679
|
-
.replace(/^-+|-+$/gu, "");
|
|
680
|
-
return normalized.length > 0 ? normalized.slice(0, 96) : "global";
|
|
109
|
+
if (state.schemaVersion !== 2) {
|
|
110
|
+
console.error("[commit-helper] flow-state schema is not v8.");
|
|
111
|
+
process.exit(2);
|
|
681
112
|
}
|
|
682
113
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
asBoolean(input.context_limit) ||
|
|
688
|
-
asBoolean(input.contextLimit) ||
|
|
689
|
-
asBoolean(event.context_limit) ||
|
|
690
|
-
asBoolean(event.contextLimit) ||
|
|
691
|
-
stringTokenHit(input.reason, ["context_limit", "context limit"]) ||
|
|
692
|
-
stringTokenHit(event.reason, ["context_limit", "context limit"]) ||
|
|
693
|
-
stringTokenHit(input.stop_reason, ["context_limit", "context limit"]) ||
|
|
694
|
-
stringTokenHit(event.stop_reason, ["context_limit", "context limit"]);
|
|
695
|
-
const userAbort =
|
|
696
|
-
asBoolean(input.user_abort) ||
|
|
697
|
-
asBoolean(input.userAbort) ||
|
|
698
|
-
asBoolean(input.user_cancelled) ||
|
|
699
|
-
asBoolean(input.userCancelled) ||
|
|
700
|
-
asBoolean(event.user_abort) ||
|
|
701
|
-
asBoolean(event.userAbort) ||
|
|
702
|
-
stringTokenHit(input.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
703
|
-
stringTokenHit(event.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
704
|
-
stringTokenHit(input.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
705
|
-
stringTokenHit(event.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]);
|
|
706
|
-
const stopHookActive =
|
|
707
|
-
asBoolean(input.stop_hook_active) ||
|
|
708
|
-
asBoolean(input.stopHookActive) ||
|
|
709
|
-
asBoolean(event.stop_hook_active) ||
|
|
710
|
-
asBoolean(event.stopHookActive);
|
|
711
|
-
|
|
712
|
-
const sessionKeyCandidate =
|
|
713
|
-
(typeof input.transcript_id === "string" && input.transcript_id) ||
|
|
714
|
-
(typeof input.transcriptId === "string" && input.transcriptId) ||
|
|
715
|
-
(typeof input.session_id === "string" && input.session_id) ||
|
|
716
|
-
(typeof input.sessionId === "string" && input.sessionId) ||
|
|
717
|
-
(typeof session.id === "string" && session.id) ||
|
|
718
|
-
fallbackSessionKey;
|
|
719
|
-
const sessionKey = sanitizeStopSessionKey(sessionKeyCandidate);
|
|
720
|
-
|
|
721
|
-
return {
|
|
722
|
-
contextLimit,
|
|
723
|
-
userAbort,
|
|
724
|
-
stopHookActive,
|
|
725
|
-
sessionKey
|
|
726
|
-
};
|
|
114
|
+
const matching = (state.ac ?? []).find((item) => item.id === acId);
|
|
115
|
+
if (!matching) {
|
|
116
|
+
console.error(\`[commit-helper] AC \${acId} is not declared in plan.md / flow-state.\`);
|
|
117
|
+
process.exit(2);
|
|
727
118
|
}
|
|
728
119
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
732
|
-
const input = toObject(runtime.inputData) || {};
|
|
733
|
-
const loopCount =
|
|
734
|
-
typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
|
|
735
|
-
? Math.trunc(input.loop_count)
|
|
736
|
-
: 0;
|
|
737
|
-
|
|
738
|
-
const dirtyState = await isGitDirty(runtime.root);
|
|
739
|
-
const stopSignals = extractStopSignals(input, "run-" + state.activeRunId);
|
|
740
|
-
const safetyBypassActive = stopSignals.stopHookActive || stopSignals.userAbort || stopSignals.contextLimit;
|
|
741
|
-
if (dirtyState === "dirty" && !safetyBypassActive) {
|
|
742
|
-
const stopBlocksPath = path.join(stateDir, "stop-blocks-" + stopSignals.sessionKey + ".json");
|
|
743
|
-
const prior = toObject(await readJsonFile(stopBlocksPath, {})) || {};
|
|
744
|
-
const priorCount =
|
|
745
|
-
typeof prior.blockCount === "number" && Number.isFinite(prior.blockCount)
|
|
746
|
-
? Math.max(0, Math.trunc(prior.blockCount))
|
|
747
|
-
: 0;
|
|
748
|
-
if (priorCount < STOP_BLOCK_LIMIT_PER_TRANSCRIPT) {
|
|
749
|
-
const nextCount = priorCount + 1;
|
|
750
|
-
await writeJsonFile(stopBlocksPath, {
|
|
751
|
-
schemaVersion: 1,
|
|
752
|
-
sessionKey: stopSignals.sessionKey,
|
|
753
|
-
blockCount: nextCount,
|
|
754
|
-
updatedAt: new Date().toISOString()
|
|
755
|
-
});
|
|
756
|
-
process.stderr.write(
|
|
757
|
-
'[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'
|
|
758
|
-
);
|
|
759
|
-
return 1;
|
|
760
|
-
}
|
|
761
|
-
process.stderr.write(
|
|
762
|
-
'[cclaw] Stop advisory: dirty working tree detected, but block limit reached for this transcript (max 2). Continuing with handoff reminder only.\\n'
|
|
763
|
-
);
|
|
764
|
-
} else if (dirtyState === "dirty" && safetyBypassActive) {
|
|
765
|
-
const reason = stopSignals.stopHookActive
|
|
766
|
-
? "stop_hook_active"
|
|
767
|
-
: stopSignals.userAbort
|
|
768
|
-
? "user_abort"
|
|
769
|
-
: "context_limit";
|
|
770
|
-
process.stderr.write(
|
|
771
|
-
"[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const closeoutObj = toObject(state.raw.closeout) || {};
|
|
776
|
-
const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
|
|
777
|
-
const closeoutContext =
|
|
778
|
-
state.currentStage === "ship" || shipSubstate !== "idle"
|
|
779
|
-
? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
|
|
780
|
-
: "";
|
|
120
|
+
const profile = state.buildProfile ?? "default";
|
|
121
|
+
const phases = matching.phases ?? {};
|
|
781
122
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
")." +
|
|
788
|
-
closeoutContext +
|
|
789
|
-
" Active artifacts stay in " +
|
|
790
|
-
RUNTIME_ROOT +
|
|
791
|
-
"/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.";
|
|
792
|
-
|
|
793
|
-
if (runtime.harness === "cursor") {
|
|
794
|
-
if (loopCount === 0) {
|
|
795
|
-
runtime.writeJson({ followup_message: message });
|
|
796
|
-
} else {
|
|
797
|
-
runtime.writeJson({});
|
|
798
|
-
}
|
|
799
|
-
return 0;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
runtime.writeJson({ systemMessage: message });
|
|
803
|
-
return 0;
|
|
123
|
+
if (phase === "green" && !phases.red && profile !== "bootstrap") {
|
|
124
|
+
console.error(\`[commit-helper] cannot record GREEN for \${acId}: no RED commit on record.\`);
|
|
125
|
+
console.error("[commit-helper] write a failing test first and commit it with --phase=red.");
|
|
126
|
+
console.error("[commit-helper] (override: set buildProfile to 'bootstrap' in flow-state for test-framework bootstrap slugs only.)");
|
|
127
|
+
process.exit(2);
|
|
804
128
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (value === "session-start") return "session-start";
|
|
809
|
-
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
810
|
-
if (value === "stop-checkpoint") return "stop-handoff";
|
|
811
|
-
if (value === "session-rehydrate") return "session-start";
|
|
812
|
-
return "";
|
|
129
|
+
if (phase === "refactor" && (!phases.red || !phases.green)) {
|
|
130
|
+
console.error(\`[commit-helper] cannot record REFACTOR for \${acId}: missing \${!phases.red ? "RED" : "GREEN"} commit.\`);
|
|
131
|
+
process.exit(2);
|
|
813
132
|
}
|
|
814
133
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
process.
|
|
819
|
-
"[cclaw] run-hook: usage: node " +
|
|
820
|
-
RUNTIME_ROOT +
|
|
821
|
-
"/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
|
|
822
|
-
);
|
|
823
|
-
process.exitCode = 1;
|
|
824
|
-
return;
|
|
134
|
+
if (phase === "refactor" && skipped) {
|
|
135
|
+
if (!arg("message") || !arg("message").includes("skipped:")) {
|
|
136
|
+
console.error("[commit-helper] --phase=refactor --skipped requires --message=\\"refactor(AC-N) skipped: <reason>\\".");
|
|
137
|
+
process.exit(2);
|
|
825
138
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
harness,
|
|
840
|
-
root,
|
|
841
|
-
inputRaw,
|
|
842
|
-
inputData,
|
|
843
|
-
writeJson(value) {
|
|
844
|
-
process.stdout.write(JSON.stringify(value) + "\\n");
|
|
845
|
-
}
|
|
139
|
+
const updated = {
|
|
140
|
+
...state,
|
|
141
|
+
ac: state.ac.map((item) => {
|
|
142
|
+
if (item.id !== acId) return item;
|
|
143
|
+
const nextPhases = { ...(item.phases ?? {}), refactor: { skipped: true, reason: arg("message") } };
|
|
144
|
+
const allDone = nextPhases.red && nextPhases.green && nextPhases.refactor;
|
|
145
|
+
return {
|
|
146
|
+
...item,
|
|
147
|
+
phases: nextPhases,
|
|
148
|
+
commit: allDone ? (nextPhases.green.sha ?? item.commit ?? null) : item.commit ?? null,
|
|
149
|
+
status: allDone ? "committed" : "pending"
|
|
150
|
+
};
|
|
151
|
+
})
|
|
846
152
|
};
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
153
|
+
await fs.writeFile(statePath, \`\${JSON.stringify(updated, null, 2)}\\n\`, "utf8");
|
|
154
|
+
console.log(\`[commit-helper] \${acId} phase=refactor skipped (recorded).\`);
|
|
155
|
+
if (updated.ac.find((item) => item.id === acId)?.status === "committed") {
|
|
156
|
+
console.log(\`[commit-helper] \${acId} cycle complete (red, green, refactor=skipped).\`);
|
|
157
|
+
}
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let staged;
|
|
162
|
+
try {
|
|
163
|
+
staged = execFileSync("git", ["diff", "--cached", "--name-only"], { cwd: root, encoding: "utf8" }).trim();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(\`[commit-helper] git not available: \${error.message}\`);
|
|
166
|
+
process.exit(2);
|
|
167
|
+
}
|
|
168
|
+
if (!staged) {
|
|
169
|
+
console.error("[commit-helper] nothing staged. Stage AC-related changes before invoking commit-helper.");
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (phase === "red") {
|
|
174
|
+
const stagedFiles = staged.split("\\n").filter(Boolean);
|
|
175
|
+
const looksLikeProduction = stagedFiles.find((file) => /^src\\//.test(file) || /^lib\\//.test(file) || /^app\\//.test(file));
|
|
176
|
+
if (looksLikeProduction) {
|
|
177
|
+
console.error(\`[commit-helper] RED phase rejects production files: \${looksLikeProduction}\`);
|
|
178
|
+
console.error("[commit-helper] RED commits must contain test files only. Write the failing test first; commit production code under --phase=green.");
|
|
179
|
+
process.exit(2);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const commitMessage = \`\${message}\\n\\nrefs: \${acId} (phase=\${phase})\`;
|
|
184
|
+
execFileSync("git", ["commit", "-m", commitMessage], { cwd: root, stdio: "inherit" });
|
|
185
|
+
|
|
186
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
187
|
+
const updated = {
|
|
188
|
+
...state,
|
|
189
|
+
ac: state.ac.map((item) => {
|
|
190
|
+
if (item.id !== acId) return item;
|
|
191
|
+
const nextPhases = { ...(item.phases ?? {}), [phase]: { sha } };
|
|
192
|
+
const cycleDone = nextPhases.red && nextPhases.green && nextPhases.refactor;
|
|
193
|
+
return {
|
|
194
|
+
...item,
|
|
195
|
+
phases: nextPhases,
|
|
196
|
+
commit: cycleDone ? (nextPhases.green.sha ?? sha) : item.commit ?? null,
|
|
197
|
+
status: cycleDone ? "committed" : "pending"
|
|
198
|
+
};
|
|
199
|
+
})
|
|
200
|
+
};
|
|
201
|
+
await fs.writeFile(statePath, \`\${JSON.stringify(updated, null, 2)}\\n\`, "utf8");
|
|
202
|
+
console.log(\`[commit-helper] \${acId} phase=\${phase} committed as \${sha}\`);
|
|
203
|
+
const after = updated.ac.find((item) => item.id === acId);
|
|
204
|
+
if (after && after.status === "committed") {
|
|
205
|
+
console.log(\`[commit-helper] \${acId} cycle complete (red, green, refactor).\`);
|
|
883
206
|
}
|
|
884
|
-
|
|
885
|
-
void main();
|
|
886
207
|
`;
|
|
887
|
-
|
|
208
|
+
export const SESSION_START_HOOK_SPEC = {
|
|
209
|
+
id: "session-start",
|
|
210
|
+
fileName: "session-start.mjs",
|
|
211
|
+
description: "Rehydrate flow state when a new session begins.",
|
|
212
|
+
events: ["session.start"],
|
|
213
|
+
body: SESSION_START_HOOK,
|
|
214
|
+
defaultEnabled: true
|
|
215
|
+
};
|
|
216
|
+
export const STOP_HANDOFF_HOOK_SPEC = {
|
|
217
|
+
id: "stop-handoff",
|
|
218
|
+
fileName: "stop-handoff.mjs",
|
|
219
|
+
description: "Surface a short handoff message when the agent stops mid-flow.",
|
|
220
|
+
events: ["session.stop"],
|
|
221
|
+
body: STOP_HANDOFF_HOOK,
|
|
222
|
+
defaultEnabled: true
|
|
223
|
+
};
|
|
224
|
+
export const COMMIT_HELPER_HOOK_SPEC = {
|
|
225
|
+
id: "commit-helper",
|
|
226
|
+
fileName: "commit-helper.mjs",
|
|
227
|
+
description: "Atomic commit per AC plus traceability check (AC -> commit SHA).",
|
|
228
|
+
events: [],
|
|
229
|
+
body: COMMIT_HELPER_HOOK,
|
|
230
|
+
defaultEnabled: true
|
|
231
|
+
};
|
|
232
|
+
export const NODE_HOOKS = [
|
|
233
|
+
SESSION_START_HOOK_SPEC,
|
|
234
|
+
STOP_HANDOFF_HOOK_SPEC,
|
|
235
|
+
COMMIT_HELPER_HOOK_SPEC
|
|
236
|
+
];
|
|
237
|
+
export const DEFAULT_HOOK_PROFILE = "minimal";
|