agileflow 4.0.0-alpha.1 → 4.0.0-alpha.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  All notable changes to `agileflow` v4 are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [4.0.0-alpha.2] — 2026-04-20
7
+
8
+ Curated behavior presets — first hooks ship, but never as a free-for-all.
9
+
10
+ ### Added
11
+
12
+ - **Behavior presets in `agileflow.config.json`** (`behaviors: { loadContext, babysitDefault, damageControl, preCompactState }`). Each preset maps 1:N to hooks declared in plugin manifests via a new `behavior: <key>` field. Disabling a preset excludes its hooks from the generated `.agileflow/hook-manifest.yaml` — the script literally does not run.
13
+ - **Wizard step**: `pickBehaviors()` (`src/cli/wizard/behaviors-picker.js`) — Clack multiselect with all four presets pre-checked. Only shown for IDEs that support hooks (claude-code today; cursor/windsurf/codex skip it).
14
+ - **Six behavior-gated hooks in the `core` plugin**:
15
+ - `context-loader` (SessionStart, gated by `loadContext`) — lean v4 replacement for v3's 79KB welcome banner. Prints stories, dirty files, and recent commits.
16
+ - `babysit-mentor-injector` (SessionStart, gated by `babysitDefault`) — HARD mode mentor injection. Claude defaults to the `/agileflow:babysit` mentor pattern without explicit invocation.
17
+ - `damage-control-bash` / `-edit` / `-write` (PreToolUse, gated by `damageControl`, `skipOnError: false`) — pattern-driven safety net (`damage-control-patterns.yaml`). Blocks `rm -rf /`, `dd to /dev/sda`, fork bombs, writes to `.env`/`.ssh/`, and similar.
18
+ - `pre-compact-state` (PreCompact, gated by `preCompactState`) — dumps active stories, current command, and dirty git state so they survive Claude's compaction summary.
19
+ - **Schema + loader + writer updates**: `behaviors` is now a first-class config section. `mergeConfig` deep-merges across one level so partial user overrides don't wipe defaults.
20
+ - **Aggregator behavior gating**: `buildHookManifest(orderedPlugins, behaviors)` filters hooks where `behaviors[entry.behavior] === false`. Missing keys treated as enabled (preserves intent of partial configs).
21
+ - **15 new tests** covering behaviors filtering in the aggregator, behaviors-picker pure helpers, and the install-time integration test that asserts the manifest reflects the toggle map. Total: **304 passing** (+15 from alpha.1).
22
+
23
+ ### Changed
24
+
25
+ - `installPlugins(options)` accepts a `behaviors` option threaded through to `writeAggregatedManifest`.
26
+ - `agileflow setup` now includes a behaviors step after personalization for hook-capable IDEs.
27
+ - `agileflow update` re-reads `behaviors` from the saved config so manual edits to `agileflow.config.json` propagate without re-running the wizard.
28
+ - `defaultConfig()` ships with all four behaviors enabled. Users opt **out** at the wizard, never opt in.
29
+
30
+ ### Why curated, not free configuration
31
+
32
+ Per user feedback in alpha.1: "I don't want it to be super customizable. Users would be able to customize pretty much anything." Free hook configuration (declare any event + any matcher + any script) is technically possible via the `hooks:` map, but the wizard surface is restricted to four presets. This keeps the install path predictable and prevents the v3-era anti-pattern of every plugin author shipping their own SessionStart welcome banner.
33
+
6
34
  ## [4.0.0-alpha.1] — Unreleased
7
35
 
8
36
  v4 Phase 1 skeleton. Not yet publishable.
@@ -254,6 +282,7 @@ The path from "green test suite" to "user can `npm i agileflow@alpha`" closes he
254
282
  - **Verified `npm pack --dry-run`** from `apps/cli/`: ships 52 files (bin/, src/, content/, README.md, LICENSE, CHANGELOG.md) — 65 KB tarball, 210 KB unpacked. Tests + vitest config + node_modules excluded.
255
283
 
256
284
  **To ship `4.0.0-alpha.1` now**:
285
+
257
286
  ```bash
258
287
  cd apps/cli && npm version 4.0.0-alpha.1 --no-git-tag-version
259
288
  git commit -am "release(v4): 4.0.0-alpha.1"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: babysit-mentor-injector (SessionStart, HARD mode).
4
+ *
5
+ * Pre-loads the babysit mentor pattern into the session by printing
6
+ * its operating rules to stdout. Claude Code includes this in the
7
+ * session context, so Claude defaults to mentor behavior (smart
8
+ * AskUserQuestion at every decision point, plan mode for non-trivial
9
+ * tasks, expert delegation, task tracking, audits) without the user
10
+ * needing to type "walk me through".
11
+ *
12
+ * Disable this hook in agileflow.config.json if you want quick-edit
13
+ * mode by default — the agileflow-babysit-mentor skill still
14
+ * activates on its keyword triggers when needed.
15
+ *
16
+ * Always exits 0.
17
+ */
18
+ process.stdout.write(`## AgileFlow Mentor Mode (default-on)
19
+
20
+ This session has the babysit-mentor pattern enabled. Apply these
21
+ rules unless the user explicitly opts out for a specific request:
22
+
23
+ 1. **Smart AskUserQuestion at every decision point.** When the work
24
+ reaches a meaningful choice (which task, which approach, whether
25
+ to commit), end the response with the AskUserQuestion tool —
26
+ specific options with one marked (Recommended). Never generic
27
+ "Continue?" / "What next?".
28
+
29
+ 2. **Plan mode for non-trivial implementation.** Call EnterPlanMode
30
+ for anything more than a typo/one-liner. Explore 3–5 files,
31
+ write the plan, ExitPlanMode. Skip for trivial fixes.
32
+
33
+ 3. **Delegate complex work to domain experts.** Use the Task tool
34
+ with appropriate subagent_type (database / api / ui / testing /
35
+ security / etc.) for complex single-domain work. Use the
36
+ orchestrator for multi-domain features. Use multi-expert for
37
+ review/analysis questions.
38
+
39
+ 4. **Track progress.** TaskCreate for any task with 3+ steps.
40
+ TaskUpdate as each completes. Don't batch.
41
+
42
+ 5. **Suggest a logic audit after every implementation.** After tests
43
+ pass, present "🔍 Run logic audit on modified files" as
44
+ (Recommended). 5 analyzers catch edge cases tests miss.
45
+
46
+ 6. **Suggest a flow audit when user-flows changed.** Plans for
47
+ non-trivial features must include a "Verify flow integrity"
48
+ step. After tests pass on flow-touching code, suggest
49
+ "🔄 Run flow audit" before commit.
50
+
51
+ To disable mentor mode entirely: edit agileflow.config.json,
52
+ set hooks.babysit-mentor-injector.enabled to false, run
53
+ \`agileflow update\`.
54
+ `);
55
+ process.exit(0);
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: context-loader (SessionStart).
4
+ *
5
+ * Prints a compact project snapshot to stdout so Claude Code includes
6
+ * it in the session prompt. Lean v4 implementation (~200 lines, vs.
7
+ * v3's 79KB welcome banner). Output is pure text — no ANSI colors.
8
+ *
9
+ * Sections (ordered by load-bearing-ness for Claude):
10
+ * 1. Project header (cwd, agileflow version, ide)
11
+ * 2. Active story / epic (from docs/09-agents/status.json if present)
12
+ * 3. Ready stories (top 5 by priority)
13
+ * 4. Git state (branch, dirty files)
14
+ * 5. Recent commits (last 5)
15
+ * 6. Hook health (last 3 entries from hook-execution.jsonl)
16
+ *
17
+ * Fail-open: any read error becomes a one-line "(unavailable)" note.
18
+ * The hook always exits 0; orchestrator-side `skipOnError: true` is
19
+ * defensive belt-and-suspenders.
20
+ */
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+ const { execSync } = require("child_process");
24
+
25
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
26
+ const agileflowDir = path.join(projectDir, ".agileflow");
27
+
28
+ /** Read JSON file, returning null on any error. */
29
+ function readJSON(p) {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(p, "utf8"));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Run a git command; return its stdout trimmed, or null on error. */
38
+ function git(args) {
39
+ try {
40
+ return execSync(`git ${args}`, {
41
+ cwd: projectDir,
42
+ encoding: "utf8",
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ }).trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /** Tail the last N JSONL entries from a file. */
51
+ function tailJSONL(p, n) {
52
+ try {
53
+ const lines = fs.readFileSync(p, "utf8").trim().split("\n");
54
+ return lines
55
+ .slice(-n)
56
+ .map((l) => {
57
+ try {
58
+ return JSON.parse(l);
59
+ } catch {
60
+ return null;
61
+ }
62
+ })
63
+ .filter(Boolean);
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ const lines = [];
70
+ const out = (s = "") => lines.push(s);
71
+
72
+ out("## AgileFlow project context");
73
+ out("");
74
+
75
+ // 1. Project header
76
+ const config = readJSON(path.join(projectDir, "agileflow.config.json")) || {};
77
+ const enabledPlugins = Object.entries(config.plugins || {})
78
+ .filter(([, v]) => v && v.enabled)
79
+ .map(([id]) => id);
80
+ out(`cwd: ${projectDir}`);
81
+ out(`ide: ${(config.ide && config.ide.primary) || "claude-code"}`);
82
+ out(`plugins: ${enabledPlugins.join(", ") || "(none)"}`);
83
+ out(
84
+ `tone: ${(config.personalization && config.personalization.tone) || "concise"}`,
85
+ );
86
+ out("");
87
+
88
+ // 2. Active story / epic
89
+ const status = readJSON(path.join(projectDir, "docs/09-agents/status.json"));
90
+ if (status && status.stories) {
91
+ const inProgress = Object.entries(status.stories)
92
+ .filter(([, s]) => s && s.status === "in_progress")
93
+ .map(([id, s]) => ({ id, ...s }));
94
+ if (inProgress.length) {
95
+ out("## In progress");
96
+ for (const s of inProgress) {
97
+ out(
98
+ ` ${s.id} ${s.title || ""} (owner: ${s.owner || "?"}, est: ${s.estimate || "?"})`,
99
+ );
100
+ }
101
+ out("");
102
+ }
103
+
104
+ // 3. Ready stories — top 5 by priority then estimate
105
+ const ready = Object.entries(status.stories)
106
+ .filter(([, s]) => s && s.status === "ready")
107
+ .map(([id, s]) => ({ id, ...s }))
108
+ .sort((a, b) => {
109
+ const pa = parseInt(String(a.priority || "P9").replace("P", ""), 10);
110
+ const pb = parseInt(String(b.priority || "P9").replace("P", ""), 10);
111
+ if (pa !== pb) return pa - pb;
112
+ return (a.estimate || 99) - (b.estimate || 99);
113
+ })
114
+ .slice(0, 5);
115
+ if (ready.length) {
116
+ out("## Ready (top 5)");
117
+ for (const s of ready) {
118
+ out(
119
+ ` ${s.id} [${s.priority || "P?"} · ${s.estimate || "?"}pts] ${s.title || ""}`,
120
+ );
121
+ }
122
+ out("");
123
+ }
124
+ }
125
+
126
+ // 4. Git state
127
+ const branch = git("rev-parse --abbrev-ref HEAD");
128
+ const dirty = git("status --short");
129
+ if (branch) {
130
+ out("## Git");
131
+ out(` branch: ${branch}`);
132
+ if (dirty) {
133
+ const lines2 = dirty.split("\n").slice(0, 10);
134
+ out(
135
+ ` changes: ${lines2.length} file(s)${dirty.split("\n").length > 10 ? " (showing first 10)" : ""}`,
136
+ );
137
+ for (const l of lines2) out(` ${l}`);
138
+ } else {
139
+ out(" changes: (clean)");
140
+ }
141
+ out("");
142
+ }
143
+
144
+ // 5. Recent commits
145
+ const log = git("log --oneline -5");
146
+ if (log) {
147
+ out("## Recent commits");
148
+ for (const l of log.split("\n")) out(` ${l}`);
149
+ out("");
150
+ }
151
+
152
+ // 6. Hook health
153
+ const hookLog = tailJSONL(
154
+ path.join(agileflowDir, "logs/hook-execution.jsonl"),
155
+ 3,
156
+ );
157
+ if (hookLog.length) {
158
+ out("## Recent hook runs");
159
+ for (const e of hookLog) {
160
+ const status = e.status === "ok" ? "OK" : e.status.toUpperCase();
161
+ out(
162
+ ` ${e.timestamp || "?"} [${status}] ${e.event}/${e.hookId} (${e.durationMs || 0}ms)`,
163
+ );
164
+ }
165
+ out("");
166
+ }
167
+
168
+ process.stdout.write(lines.join("\n") + "\n");
169
+ process.exit(0);
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-bash (PreToolUse, matcher: Bash).
4
+ *
5
+ * Reads damage-control-patterns.yaml and rejects Bash commands that
6
+ * match an `error`-severity pattern of kind `bash`. `warn`-severity
7
+ * patterns log but do not block.
8
+ *
9
+ * Stdin payload (Claude Code-shaped):
10
+ * { tool_name: "Bash", tool_input: { command: "...", description: "..." } }
11
+ *
12
+ * Exit codes:
13
+ * 0 allow (default)
14
+ * 2 block (Claude Code surfaces stderr to the user)
15
+ */
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const yaml = require("js-yaml");
19
+
20
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
21
+ const patternsPath = path.join(
22
+ projectDir,
23
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
24
+ );
25
+
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ for await (const c of process.stdin) chunks.push(c);
29
+ return Buffer.concat(chunks).toString("utf8");
30
+ }
31
+
32
+ async function main() {
33
+ const raw = await readStdin();
34
+ let payload;
35
+ try {
36
+ payload = JSON.parse(raw);
37
+ } catch {
38
+ process.exit(0); // No payload, nothing to gate.
39
+ }
40
+ const command =
41
+ payload &&
42
+ payload.tool_input &&
43
+ typeof payload.tool_input.command === "string"
44
+ ? payload.tool_input.command
45
+ : "";
46
+ if (!command) process.exit(0);
47
+
48
+ let patterns = [];
49
+ try {
50
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
51
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
52
+ } catch {
53
+ process.exit(0); // No patterns file, fail open.
54
+ }
55
+
56
+ for (const p of patterns) {
57
+ if (p.kind !== "bash") continue;
58
+ let re;
59
+ try {
60
+ re = new RegExp(p.regex, "i");
61
+ } catch {
62
+ continue;
63
+ }
64
+ if (re.test(command)) {
65
+ if (p.severity === "error") {
66
+ process.stderr.write(
67
+ `agileflow damage-control: BLOCKED — ${p.reason}\n pattern: ${p.regex}\n command: ${command.slice(0, 200)}\n`,
68
+ );
69
+ process.exit(2);
70
+ }
71
+ // severity: warn — log, do not block
72
+ process.stderr.write(`agileflow damage-control: WARN — ${p.reason}\n`);
73
+ }
74
+ }
75
+ process.exit(0);
76
+ }
77
+
78
+ main().catch(() => process.exit(0));
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-edit (PreToolUse, matcher: Edit).
4
+ *
5
+ * Reads damage-control-patterns.yaml and rejects Edit operations whose
6
+ * file_path matches an `error`-severity pattern of kind `edit`.
7
+ *
8
+ * Stdin payload:
9
+ * { tool_name: "Edit", tool_input: { file_path: "...", old_string, new_string } }
10
+ *
11
+ * Exit codes: 0 allow, 2 block.
12
+ */
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const yaml = require("js-yaml");
16
+
17
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
18
+ const patternsPath = path.join(
19
+ projectDir,
20
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
21
+ );
22
+
23
+ async function readStdin() {
24
+ const chunks = [];
25
+ for await (const c of process.stdin) chunks.push(c);
26
+ return Buffer.concat(chunks).toString("utf8");
27
+ }
28
+
29
+ async function main() {
30
+ const raw = await readStdin();
31
+ let payload;
32
+ try {
33
+ payload = JSON.parse(raw);
34
+ } catch {
35
+ process.exit(0);
36
+ }
37
+ const filePath =
38
+ payload &&
39
+ payload.tool_input &&
40
+ typeof payload.tool_input.file_path === "string"
41
+ ? payload.tool_input.file_path
42
+ : "";
43
+ if (!filePath) process.exit(0);
44
+
45
+ let patterns = [];
46
+ try {
47
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
48
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
49
+ } catch {
50
+ process.exit(0);
51
+ }
52
+
53
+ for (const p of patterns) {
54
+ if (p.kind !== "edit") continue;
55
+ let re;
56
+ try {
57
+ re = new RegExp(p.regex, "i");
58
+ } catch {
59
+ continue;
60
+ }
61
+ if (re.test(filePath)) {
62
+ if (p.severity === "error") {
63
+ process.stderr.write(
64
+ `agileflow damage-control: BLOCKED edit — ${p.reason}\n path: ${filePath}\n`,
65
+ );
66
+ process.exit(2);
67
+ }
68
+ process.stderr.write(
69
+ `agileflow damage-control: WARN — ${p.reason} (path: ${filePath})\n`,
70
+ );
71
+ }
72
+ }
73
+ process.exit(0);
74
+ }
75
+
76
+ main().catch(() => process.exit(0));
@@ -0,0 +1,100 @@
1
+ # Damage control pattern library.
2
+ #
3
+ # The pre-Bash / pre-Edit / pre-Write hooks load this file and reject
4
+ # tool invocations whose payload matches any pattern below. Each entry
5
+ # explains WHY it's blocked so the error message can cite the rule.
6
+ #
7
+ # Pattern syntax:
8
+ # - `regex`: JS RegExp source (used with `new RegExp(pattern, 'i')`)
9
+ # - `kind`: one of bash | edit | write — which hook applies the rule
10
+ # - `severity`: error (block, exit 2) | warn (allow but log warning)
11
+ # - `reason`: human-readable rationale shown in the block message
12
+ #
13
+ # Adding a pattern: keep it surgical. False positives are worse than
14
+ # missed catches because they train users to bypass the guard.
15
+
16
+ patterns:
17
+ # --- Catastrophic filesystem operations ---
18
+ - regex: 'rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+(/|~|\$HOME)\b'
19
+ kind: bash
20
+ severity: error
21
+ reason: rm -rf on / or $HOME is almost certainly a mistake — refuse outright
22
+
23
+ - regex: 'rm\s+-[a-zA-Z]*[rfRF][a-zA-Z]*\s+/\*'
24
+ kind: bash
25
+ severity: error
26
+ reason: rm -rf /* deletes the entire filesystem
27
+
28
+ - regex: '\bdd\s+if=.*\s+of=/dev/(sd|nvme|hd)'
29
+ kind: bash
30
+ severity: error
31
+ reason: dd to a raw block device wipes the disk
32
+
33
+ - regex: '\bmkfs\.\w+\s+/dev/'
34
+ kind: bash
35
+ severity: error
36
+ reason: mkfs on a device formats it — destroys all data
37
+
38
+ - regex: '>\s*/dev/(sd|nvme|hd)\w+'
39
+ kind: bash
40
+ severity: error
41
+ reason: writing directly to a block device corrupts the disk
42
+
43
+ - regex: ':\(\)\{\s*:\|:\&\s*\};:'
44
+ kind: bash
45
+ severity: error
46
+ reason: classic fork bomb
47
+
48
+ # --- Risky git operations ---
49
+ - regex: 'git\s+push\s+.*--force(\s|$)'
50
+ kind: bash
51
+ severity: warn
52
+ reason: force push can rewrite shared history — confirm intent
53
+
54
+ - regex: 'git\s+(reset|checkout)\s+.*--hard'
55
+ kind: bash
56
+ severity: warn
57
+ reason: --hard discards uncommitted work — confirm before running
58
+
59
+ - regex: 'git\s+clean\s+-[a-zA-Z]*[fdx]+'
60
+ kind: bash
61
+ severity: warn
62
+ reason: git clean -fdx removes untracked files including .gitignored — confirm
63
+
64
+ # --- Edit / Write path scope ---
65
+ # The edit/write hooks evaluate the file_path payload, not the command.
66
+ - regex: "^/etc/(passwd|shadow|sudoers)$"
67
+ kind: edit
68
+ severity: error
69
+ reason: editing system credential files would compromise the host
70
+
71
+ - regex: "^/etc/(passwd|shadow|sudoers)$"
72
+ kind: write
73
+ severity: error
74
+ reason: writing to system credential files would compromise the host
75
+
76
+ - regex: '\.ssh/(id_[a-z]+|authorized_keys|known_hosts)$'
77
+ kind: edit
78
+ severity: warn
79
+ reason: editing SSH credentials — confirm this is intentional
80
+
81
+ - regex: '\.ssh/(id_[a-z]+|authorized_keys|known_hosts)$'
82
+ kind: write
83
+ severity: warn
84
+ reason: writing to SSH credentials — confirm this is intentional
85
+
86
+ - regex: '\.env(\.|$)'
87
+ kind: write
88
+ severity: warn
89
+ reason: writing to a .env file — confirm it does not commit secrets
90
+
91
+ # --- npm / pnpm risk ---
92
+ - regex: 'npm\s+publish(\s|$)'
93
+ kind: bash
94
+ severity: warn
95
+ reason: npm publish ships to a public registry — confirm version + auth
96
+
97
+ - regex: 'rm\s+-rf?\s+(node_modules|\.git)\b'
98
+ kind: bash
99
+ severity: warn
100
+ reason: nuking node_modules / .git is recoverable but slow — confirm
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-write (PreToolUse, matcher: Write).
4
+ *
5
+ * Mirror of damage-control-edit.js for the Write tool. Same patterns
6
+ * file (kind: write entries). Blocks writing system credential files,
7
+ * warns on writes to .ssh/ and .env*.
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const yaml = require("js-yaml");
12
+
13
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
+ const patternsPath = path.join(
15
+ projectDir,
16
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
17
+ );
18
+
19
+ async function readStdin() {
20
+ const chunks = [];
21
+ for await (const c of process.stdin) chunks.push(c);
22
+ return Buffer.concat(chunks).toString("utf8");
23
+ }
24
+
25
+ async function main() {
26
+ const raw = await readStdin();
27
+ let payload;
28
+ try {
29
+ payload = JSON.parse(raw);
30
+ } catch {
31
+ process.exit(0);
32
+ }
33
+ const filePath =
34
+ payload &&
35
+ payload.tool_input &&
36
+ typeof payload.tool_input.file_path === "string"
37
+ ? payload.tool_input.file_path
38
+ : "";
39
+ if (!filePath) process.exit(0);
40
+
41
+ let patterns = [];
42
+ try {
43
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
44
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
45
+ } catch {
46
+ process.exit(0);
47
+ }
48
+
49
+ for (const p of patterns) {
50
+ if (p.kind !== "write") continue;
51
+ let re;
52
+ try {
53
+ re = new RegExp(p.regex, "i");
54
+ } catch {
55
+ continue;
56
+ }
57
+ if (re.test(filePath)) {
58
+ if (p.severity === "error") {
59
+ process.stderr.write(
60
+ `agileflow damage-control: BLOCKED write — ${p.reason}\n path: ${filePath}\n`,
61
+ );
62
+ process.exit(2);
63
+ }
64
+ process.stderr.write(
65
+ `agileflow damage-control: WARN — ${p.reason} (path: ${filePath})\n`,
66
+ );
67
+ }
68
+ }
69
+ process.exit(0);
70
+ }
71
+
72
+ main().catch(() => process.exit(0));
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: pre-compact-state (PreCompact).
4
+ *
5
+ * Captures the state Claude needs to know AFTER the compaction summary
6
+ * replaces most of the conversation: the active story, the active
7
+ * command, recent decisions, and the dirty git state. Without this,
8
+ * compaction often loses thread on multi-turn implementation work.
9
+ *
10
+ * Output is plain markdown printed to stdout — Claude Code includes
11
+ * the hook's stdout in the prompt context after compaction.
12
+ *
13
+ * Exits 0 always (must not block compaction).
14
+ */
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const { execSync } = require("child_process");
18
+
19
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
20
+
21
+ function readJSON(p) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(p, "utf8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function git(args) {
30
+ try {
31
+ return execSync(`git ${args}`, {
32
+ cwd: projectDir,
33
+ encoding: "utf8",
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ }).trim();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ const out = [];
42
+ out.push("## Pre-compaction state preservation");
43
+ out.push("");
44
+
45
+ const status = readJSON(path.join(projectDir, "docs/09-agents/status.json"));
46
+ if (status && status.stories) {
47
+ const inProgress = Object.entries(status.stories)
48
+ .filter(([, s]) => s && s.status === "in_progress")
49
+ .map(([id, s]) => `${id} ${s.title || ""}`);
50
+ if (inProgress.length) {
51
+ out.push("Active stories:");
52
+ for (const s of inProgress) out.push(` - ${s}`);
53
+ out.push("");
54
+ }
55
+ }
56
+
57
+ // Active command from session-state if it exists
58
+ const sessionState = readJSON(
59
+ path.join(projectDir, "docs/09-agents/session-state.json"),
60
+ );
61
+ if (sessionState && sessionState.active_command) {
62
+ out.push(
63
+ `Active command: ${sessionState.active_command.name || "(unknown)"}`,
64
+ );
65
+ out.push("");
66
+ }
67
+
68
+ const branch = git("rev-parse --abbrev-ref HEAD");
69
+ const dirty = git("status --short");
70
+ if (branch || dirty) {
71
+ out.push("Git:");
72
+ if (branch) out.push(` branch: ${branch}`);
73
+ if (dirty) {
74
+ const lines = dirty.split("\n").slice(0, 8);
75
+ out.push(` dirty: ${lines.length} file(s)`);
76
+ for (const l of lines) out.push(` ${l}`);
77
+ }
78
+ out.push("");
79
+ }
80
+
81
+ const log = git("log --oneline -3");
82
+ if (log) {
83
+ out.push("Recent commits:");
84
+ for (const l of log.split("\n")) out.push(` ${l}`);
85
+ out.push("");
86
+ }
87
+
88
+ out.push("After compaction, resume from this state.");
89
+ process.stdout.write(out.join("\n") + "\n");
90
+ process.exit(0);