continuous-improvement 3.12.3 → 3.15.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.
Files changed (127) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +31 -1
  3. package/LICENSE +21 -21
  4. package/QUICKSTART.md +1 -1
  5. package/README.md +25 -5
  6. package/bin/analyze.sh +161 -161
  7. package/bin/backfill.mjs +19 -19
  8. package/bin/check-doc-runtime-claims.mjs +0 -0
  9. package/bin/check-docs-substrings.mjs +0 -0
  10. package/bin/check-everything-mirror.mjs +0 -0
  11. package/bin/check-routing-targets.mjs +0 -0
  12. package/bin/check-scripts-citation-drift.mjs +0 -0
  13. package/bin/check-skill-count.mjs +32 -1
  14. package/bin/check-skill-law-tag.mjs +0 -0
  15. package/bin/check-skill-mirror.mjs +0 -0
  16. package/bin/check-skill-tiers.mjs +0 -0
  17. package/bin/check-test-imports-only.mjs +1 -1
  18. package/bin/check-third-party-shape.mjs +0 -0
  19. package/bin/check-tool-count.mjs +0 -0
  20. package/bin/companion-preference-status.mjs +0 -0
  21. package/bin/gateguard-clear.mjs +0 -0
  22. package/bin/generate-plugin-manifests.mjs +2 -1
  23. package/bin/harvest-friction.mjs +0 -0
  24. package/bin/hook-stats.mjs +21 -21
  25. package/bin/install.mjs +74 -1
  26. package/bin/lint-transcript.mjs +0 -0
  27. package/bin/mcp-server.mjs +65 -8
  28. package/bin/observe.mjs +0 -0
  29. package/bin/plan-pack.mjs +77 -0
  30. package/bin/pre-commit-block-strays.sh +0 -0
  31. package/bin/refresh-third-party.mjs +0 -0
  32. package/bin/unified-cli.mjs +55 -410
  33. package/commands/continuous-improvement.md +115 -115
  34. package/commands/dashboard.md +56 -56
  35. package/commands/model-forward.md +13 -0
  36. package/commands/production-readiness-review.md +53 -0
  37. package/commands/seven-laws.md +16 -16
  38. package/commands/ship.md +57 -0
  39. package/commands/workspace-surface-audit.md +77 -77
  40. package/hooks/companion-preference.mjs +31 -19
  41. package/hooks/gateguard.mjs +38 -16
  42. package/hooks/goal-drift-stop.mjs +0 -0
  43. package/hooks/hook-pack.mjs +110 -0
  44. package/hooks/observe.sh +0 -0
  45. package/hooks/recall-briefing.mjs +0 -0
  46. package/hooks/route-prompt.mjs +0 -0
  47. package/hooks/session.sh +106 -106
  48. package/hooks/three-section-close.mjs +0 -0
  49. package/instinct-packs/go.json +58 -58
  50. package/instinct-packs/meta.json +16 -16
  51. package/instinct-packs/python.json +58 -58
  52. package/instinct-packs/react.json +58 -58
  53. package/lib/cli-anything.mjs +0 -0
  54. package/lib/gateguard-state.mjs +62 -13
  55. package/lib/goal-drift-gate.mjs +0 -0
  56. package/lib/goal-state.mjs +8 -4
  57. package/lib/hook-pack-gate.mjs +65 -0
  58. package/lib/install-targets.mjs +121 -0
  59. package/lib/observe-event.mjs +0 -0
  60. package/lib/plan-review-packet.mjs +96 -0
  61. package/lib/plugin-metadata.mjs +23 -2
  62. package/lib/pm-marketplace.mjs +0 -0
  63. package/lib/recall-briefing.mjs +0 -0
  64. package/lib/recall-index.mjs +0 -0
  65. package/lib/resolve-home-dir.mjs +0 -0
  66. package/lib/skill-distill.mjs +141 -0
  67. package/lib/skill-tiers.mjs +0 -0
  68. package/llms.txt +2 -2
  69. package/package.json +4 -3
  70. package/plugins/beginner.json +2 -2
  71. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  72. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  73. package/plugins/continuous-improvement/LICENSE +21 -21
  74. package/plugins/continuous-improvement/README.md +1 -1
  75. package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
  76. package/plugins/continuous-improvement/bin/gateguard-clear.mjs +0 -0
  77. package/plugins/continuous-improvement/bin/mcp-server.mjs +65 -8
  78. package/plugins/continuous-improvement/bin/observe.mjs +0 -0
  79. package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
  80. package/plugins/continuous-improvement/commands/dashboard.md +56 -56
  81. package/plugins/continuous-improvement/commands/model-forward.md +13 -0
  82. package/plugins/continuous-improvement/commands/production-readiness-review.md +53 -0
  83. package/plugins/continuous-improvement/commands/seven-laws.md +16 -16
  84. package/plugins/continuous-improvement/commands/ship.md +57 -0
  85. package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
  86. package/plugins/continuous-improvement/hooks/companion-preference.mjs +31 -19
  87. package/plugins/continuous-improvement/hooks/gateguard.mjs +38 -16
  88. package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +0 -0
  89. package/plugins/continuous-improvement/hooks/hook-pack.mjs +110 -0
  90. package/plugins/continuous-improvement/hooks/hooks.json +10 -0
  91. package/plugins/continuous-improvement/hooks/observe.sh +0 -0
  92. package/plugins/continuous-improvement/hooks/recall-briefing.mjs +0 -0
  93. package/plugins/continuous-improvement/hooks/route-prompt.mjs +0 -0
  94. package/plugins/continuous-improvement/hooks/session.sh +106 -106
  95. package/plugins/continuous-improvement/hooks/three-section-close.mjs +0 -0
  96. package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
  97. package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
  98. package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
  99. package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
  100. package/plugins/continuous-improvement/lib/gateguard-state.mjs +62 -13
  101. package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +0 -0
  102. package/plugins/continuous-improvement/lib/goal-state.mjs +8 -4
  103. package/plugins/continuous-improvement/lib/hook-pack-gate.mjs +65 -0
  104. package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
  105. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +23 -2
  106. package/plugins/continuous-improvement/lib/recall-briefing.mjs +0 -0
  107. package/plugins/continuous-improvement/lib/recall-index.mjs +0 -0
  108. package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
  109. package/plugins/continuous-improvement/lib/skill-distill.mjs +141 -0
  110. package/plugins/continuous-improvement/skills/README.md +1 -1
  111. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +4 -4
  112. package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +1 -1
  113. package/plugins/continuous-improvement/skills/handoff/SKILL.md +0 -1
  114. package/plugins/continuous-improvement/skills/model-forward/SKILL.md +44 -0
  115. package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
  116. package/plugins/expert.json +5 -1
  117. package/skills/README.md +4 -2
  118. package/skills/gateguard.md +4 -4
  119. package/skills/goal-monitor.md +1 -1
  120. package/skills/handoff.md +0 -1
  121. package/skills/model-forward.md +44 -0
  122. package/skills/tdd-workflow.md +411 -411
  123. package/lib/compound-engineering.mjs +0 -831
  124. package/lib/pm-skills.mjs +0 -1274
  125. package/lib/unified-plugin.mjs +0 -924
  126. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +0 -108
  127. package/skills/para-memory-files.md +0 -108
@@ -1,77 +1,77 @@
1
- ---
2
- name: workspace-surface-audit
3
- description: "Audit workspace capabilities and recommend continuous-improvement-native skills, hooks, and workflows"
4
- ---
5
-
6
- # /workspace-surface-audit
7
-
8
- Audit the active repo, MCP servers, plugins, connectors, env surfaces, and harness setup. Returns a 5-section report with specific next moves.
9
-
10
- ## Usage
11
-
12
- ```
13
- /workspace-surface-audit
14
- ```
15
-
16
- ## What It Checks
17
-
18
- 1. **Repo surface** — `package.json`, lockfiles, framework config, `.mcp.json`, `.claude/settings*.json`, `AGENTS.md`
19
- 2. **Environment surface** — `.env*` files (key names only, no secrets)
20
- 3. **Connected tools** — installed plugins, MCP servers, LSPs, app integrations
21
- 4. **continuous-improvement surface** — existing skills, commands, hooks, agents
22
-
23
- ## Output Format
24
-
25
- ```
26
- === Workspace Surface Audit ===
27
-
28
- ## Current Surface
29
- - [What is usable right now]
30
-
31
- ## Parity
32
- - [Where continuous-improvement matches or exceeds benchmarks]
33
-
34
- ## Primitive-Only Gaps
35
- - [Tools exist, but continuous-improvement lacks clean operator skills]
36
-
37
- ## Missing Integrations
38
- - [Capabilities not available yet]
39
-
40
- ## Top 3-5 Next Moves
41
- 1. [Concrete continuous-improvement-native addition]
42
- 2. [Ordered by impact]
43
- ```
44
-
45
- ## Rules
46
-
47
- - **Never print secrets** — only provider names, capability names, file paths
48
- - **Prefer continuous-improvement** over generic "install another plugin" advice
49
- - **Organize by workflows**, not API brands
50
- - **Specific recommendations** — concrete enough to implement without another discovery pass
51
-
52
- ## Example Output
53
-
54
- ```
55
- ## Current Surface
56
- - MCP: filesystem, github, playwright
57
- - Plugins: beginner, expert (continuous-improvement)
58
- - Env: STRIPE_API_KEY, FAL_KEY configured
59
- - Framework: Next.js 15, React 19, TypeScript
60
-
61
- ## Parity
62
- - Continuous improvement: covered (native)
63
- - Testing discipline: covered (TDD workflows)
64
-
65
- ## Primitive-Only Gaps
66
- - Stripe connected but no billing-operator skill
67
- - GitHub MCP available but no issue-triage workflow
68
-
69
- ## Missing Integrations
70
- - No Linear connector detected
71
- - No Slack webhook configured
72
-
73
- ## Top 3-5 Next Moves
74
- 1. Create billing-operator skill (Stripe integration)
75
- 2. Create github-ops workflow for issue triage
76
- 3. Add Linear connector skill for project management
77
- ```
1
+ ---
2
+ name: workspace-surface-audit
3
+ description: "Audit workspace capabilities and recommend continuous-improvement-native skills, hooks, and workflows"
4
+ ---
5
+
6
+ # /workspace-surface-audit
7
+
8
+ Audit the active repo, MCP servers, plugins, connectors, env surfaces, and harness setup. Returns a 5-section report with specific next moves.
9
+
10
+ ## Usage
11
+
12
+ ```
13
+ /workspace-surface-audit
14
+ ```
15
+
16
+ ## What It Checks
17
+
18
+ 1. **Repo surface** — `package.json`, lockfiles, framework config, `.mcp.json`, `.claude/settings*.json`, `AGENTS.md`
19
+ 2. **Environment surface** — `.env*` files (key names only, no secrets)
20
+ 3. **Connected tools** — installed plugins, MCP servers, LSPs, app integrations
21
+ 4. **continuous-improvement surface** — existing skills, commands, hooks, agents
22
+
23
+ ## Output Format
24
+
25
+ ```
26
+ === Workspace Surface Audit ===
27
+
28
+ ## Current Surface
29
+ - [What is usable right now]
30
+
31
+ ## Parity
32
+ - [Where continuous-improvement matches or exceeds benchmarks]
33
+
34
+ ## Primitive-Only Gaps
35
+ - [Tools exist, but continuous-improvement lacks clean operator skills]
36
+
37
+ ## Missing Integrations
38
+ - [Capabilities not available yet]
39
+
40
+ ## Top 3-5 Next Moves
41
+ 1. [Concrete continuous-improvement-native addition]
42
+ 2. [Ordered by impact]
43
+ ```
44
+
45
+ ## Rules
46
+
47
+ - **Never print secrets** — only provider names, capability names, file paths
48
+ - **Prefer continuous-improvement** over generic "install another plugin" advice
49
+ - **Organize by workflows**, not API brands
50
+ - **Specific recommendations** — concrete enough to implement without another discovery pass
51
+
52
+ ## Example Output
53
+
54
+ ```
55
+ ## Current Surface
56
+ - MCP: filesystem, github, playwright
57
+ - Plugins: beginner, expert (continuous-improvement)
58
+ - Env: STRIPE_API_KEY, FAL_KEY configured
59
+ - Framework: Next.js 15, React 19, TypeScript
60
+
61
+ ## Parity
62
+ - Continuous improvement: covered (native)
63
+ - Testing discipline: covered (TDD workflows)
64
+
65
+ ## Primitive-Only Gaps
66
+ - Stripe connected but no billing-operator skill
67
+ - GitHub MCP available but no issue-triage workflow
68
+
69
+ ## Missing Integrations
70
+ - No Linear connector detected
71
+ - No Slack webhook configured
72
+
73
+ ## Top 3-5 Next Moves
74
+ 1. Create billing-operator skill (Stripe integration)
75
+ 2. Create github-ops workflow for issue triage
76
+ 3. Add Linear connector skill for project management
77
+ ```
@@ -12,11 +12,17 @@
12
12
  * (when installed) or the install hint (when not).
13
13
  *
14
14
  * Fail-open: any unexpected error reading stdin / parsing settings / probing
15
- * the filesystem → emit { decision: "allow" } and exit 0. The hook never
16
- * blocks on its own bugs.
15
+ * the filesystem → exit 0 with empty stdout (allow). The hook never blocks
16
+ * on its own bugs.
17
17
  *
18
18
  * Stdin : JSON { tool_name, tool_input }
19
- * Stdout : JSON { decision: "allow" | "block", reason?: string }
19
+ * Stdout : empty on allow (no output = no opinion, the call proceeds); on
20
+ * deny, the documented PreToolUse shape:
21
+ * { hookSpecificOutput: { hookEventName: "PreToolUse",
22
+ * permissionDecision: "deny", permissionDecisionReason } }
23
+ * A bare { decision: "allow" } is not schema-valid for PreToolUse
24
+ * (the deprecated enum is "approve" | "block") and surfaces as a
25
+ * "Hook JSON output validation failed" error on every tool call.
20
26
  * Exit : 0 always — decision lives in stdout, never in the exit code.
21
27
  *
22
28
  * OVERRIDES map must stay row-aligned with skills/superpowers.md
@@ -50,8 +56,20 @@ const OVERRIDES = {
50
56
  plugin: "oh-my-claudecode",
51
57
  },
52
58
  };
53
- function emitAndExit(d) {
54
- process.stdout.write(`${JSON.stringify(d)}\n`);
59
+ // Allow = empty stdout + exit 0. Emitting JSON here is wrong: PreToolUse has
60
+ // no { decision: "allow" } shape, and the invalid output surfaces as a
61
+ // per-tool-call "Hook JSON output validation failed" error in the client.
62
+ function emitAllowAndExit() {
63
+ process.exit(0);
64
+ }
65
+ function emitDenyAndExit(reason) {
66
+ process.stdout.write(`${JSON.stringify({
67
+ hookSpecificOutput: {
68
+ hookEventName: "PreToolUse",
69
+ permissionDecision: "deny",
70
+ permissionDecisionReason: reason,
71
+ },
72
+ })}\n`);
55
73
  process.exit(0);
56
74
  }
57
75
  function readStdin() {
@@ -137,19 +155,19 @@ function main() {
137
155
  payload = JSON.parse(raw);
138
156
  }
139
157
  catch {
140
- emitAndExit({ decision: "allow" });
158
+ emitAllowAndExit();
141
159
  }
142
160
  if (payload.tool_name !== "Skill") {
143
- emitAndExit({ decision: "allow" });
161
+ emitAllowAndExit();
144
162
  }
145
163
  const skill = payload.tool_input?.skill;
146
164
  if (typeof skill !== "string" || skill.length === 0) {
147
- emitAndExit({ decision: "allow" });
165
+ emitAllowAndExit();
148
166
  }
149
167
  const normalized = normalizeSkill(skill);
150
168
  const override = OVERRIDES[normalized];
151
169
  if (!override) {
152
- emitAndExit({ decision: "allow" });
170
+ emitAllowAndExit();
153
171
  }
154
172
  const home = resolveHome();
155
173
  const mode = readMode(home);
@@ -166,25 +184,19 @@ function main() {
166
184
  // Shadow row: what companions-first would have done. This is the data
167
185
  // set that earns a future default-flip decision.
168
186
  writeTelemetry(home, { ...baseEvent, mode, action: "observation" });
169
- emitAndExit({ decision: "allow" });
187
+ emitAllowAndExit();
170
188
  }
171
189
  if (mode === "companions-first") {
172
190
  process.stderr.write(`[continuous-improvement] companion_preference=companions-first → prefer \`${override.companion}\` over \`ci:${normalized}\`.\n`);
173
191
  writeTelemetry(home, { ...baseEvent, mode, action: "advisory" });
174
- emitAndExit({ decision: "allow" });
192
+ emitAllowAndExit();
175
193
  }
176
194
  // strict-companions: always block; reason depends on install state.
177
195
  if (installed) {
178
196
  writeTelemetry(home, { ...baseEvent, mode, action: "block" });
179
- emitAndExit({
180
- decision: "block",
181
- reason: `companion_preference=strict-companions: route to \`${override.companion}\` instead of \`ci:${normalized}\`. The CI fallback is suppressed by your setting.`,
182
- });
197
+ emitDenyAndExit(`companion_preference=strict-companions: route to \`${override.companion}\` instead of \`ci:${normalized}\`. The CI fallback is suppressed by your setting.`);
183
198
  }
184
199
  writeTelemetry(home, { ...baseEvent, mode, action: "block-not-installed" });
185
- emitAndExit({
186
- decision: "block",
187
- reason: `companion_preference=strict-companions: companion plugin \`${override.plugin}\` is not installed. Install with \`/plugin install ${override.plugin}@continuous-improvement\` or relax the setting to \`companions-first\` or \`ci-first\` in ~/.claude/settings.json.`,
188
- });
200
+ emitDenyAndExit(`companion_preference=strict-companions: companion plugin \`${override.plugin}\` is not installed. Install with \`/plugin install ${override.plugin}@continuous-improvement\` or relax the setting to \`companions-first\` or \`ci-first\` in ~/.claude/settings.json.`);
189
201
  }
190
202
  main();
@@ -3,7 +3,13 @@
3
3
  * Runtime PreToolUse gateguard hook.
4
4
  *
5
5
  * Stdin : JSON { tool_name, tool_input }
6
- * Stdout : JSON { decision: "allow" | "block", reason?: string }
6
+ * Stdout : empty on allow (no output = no opinion, the call proceeds); on
7
+ * deny, the documented PreToolUse shape:
8
+ * { hookSpecificOutput: { hookEventName: "PreToolUse",
9
+ * permissionDecision: "deny", permissionDecisionReason } }
10
+ * A bare { decision: "allow" } is not schema-valid for PreToolUse
11
+ * (the deprecated enum is "approve" | "block") and surfaces as a
12
+ * "Hook JSON output validation failed" error on every tool call.
7
13
  * Exit : 0 always (decision is in stdout, fail-open on parse error).
8
14
  *
9
15
  * Three-stage gate per skills/gateguard.md:
@@ -132,7 +138,7 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
132
138
  "",
133
139
  "Then clear the gate and retry the same call. Either route works:",
134
140
  ` A. Bash (always works — run verbatim): node ${quotePath(CLEAR_CLI_PATH)} --state ${quotePath(stateFilePath)} ${quoted.join(" ")}`,
135
- ` B. MCP tool (when the continuous-improvement server is connected): ci_gateguard_clear {file_paths: [${quoted.join(", ")}]}`,
141
+ ` B. MCP tool (when the continuous-improvement server is connected): ci_gateguard_clear {file_paths: [${quoted.join(", ")}], state_path: ${quotePath(stateFilePath)}}`,
136
142
  " Both canonicalize paths — drive-letter case and separators don't matter.",
137
143
  " (Harnesses that forward unknown tool params may instead retry the call with",
138
144
  " `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
@@ -151,13 +157,26 @@ function buildDestructiveBashReason(command) {
151
157
  }
152
158
  function buildCapReachedReason() {
153
159
  return [
154
- `Gateguard session clearance cap reached (${MAX_CLEARED_FILES} distinct files).`,
155
- "Start a new Claude Code session to reset the gate. The cap exists to bound",
156
- "stuck-loop or rogue-agent clearance from compounding within a single session.",
160
+ `Gateguard clearance cap reached (${MAX_CLEARED_FILES} distinct files this session).`,
161
+ "The cap is per-session: start a new Claude Code session to reset it, or wait",
162
+ "for the state file to self-heal. It bounds stuck-loop or rogue-agent clearance",
163
+ "from compounding within one session without affecting your other sessions.",
157
164
  ].join("\n");
158
165
  }
159
- function emit(decision) {
160
- process.stdout.write(`${JSON.stringify(decision)}\n`);
166
+ // Allow = empty stdout + exit 0. Emitting JSON here is wrong: PreToolUse has
167
+ // no { decision: "allow" } shape, and the invalid output surfaces as a
168
+ // per-tool-call "Hook JSON output validation failed" error in the client.
169
+ function emitAllow() {
170
+ process.exit(0);
171
+ }
172
+ function emitDeny(reason) {
173
+ process.stdout.write(`${JSON.stringify({
174
+ hookSpecificOutput: {
175
+ hookEventName: "PreToolUse",
176
+ permissionDecision: "deny",
177
+ permissionDecisionReason: reason,
178
+ },
179
+ })}\n`);
161
180
  process.exit(0);
162
181
  }
163
182
  function main() {
@@ -166,7 +185,7 @@ function main() {
166
185
  raw = readFileSync(0, "utf8");
167
186
  }
168
187
  catch {
169
- emit({ decision: "allow" });
188
+ emitAllow();
170
189
  return;
171
190
  }
172
191
  let payload;
@@ -174,23 +193,26 @@ function main() {
174
193
  payload = JSON.parse(raw);
175
194
  }
176
195
  catch {
177
- emit({ decision: "allow" }); // fail-open
196
+ emitAllow(); // fail-open
178
197
  return;
179
198
  }
180
199
  const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
181
200
  const toolInput = payload.tool_input ?? {};
182
201
  const gate = classifyTool(toolName, toolInput);
183
202
  if (gate === "allow") {
184
- emit({ decision: "allow" });
203
+ emitAllow();
185
204
  return;
186
205
  }
187
206
  if (gate === "destructive-bash") {
188
207
  const cmd = typeof toolInput.command === "string" ? toolInput.command : "";
189
- emit({ decision: "block", reason: buildDestructiveBashReason(cmd) });
208
+ emitDeny(buildDestructiveBashReason(cmd));
190
209
  return;
191
210
  }
192
- // mutating-file
193
- const sessionDir = resolveSessionDir();
211
+ // mutating-file — scope state to THIS session so the clearance cap never
212
+ // bleeds across concurrent same-day sessions. session_id is the standard hook
213
+ // stdin field (see recall-briefing / observe-event); absent → legacy dir.
214
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : undefined;
215
+ const sessionDir = resolveSessionDir(sessionId);
194
216
  const stateFilePath = join(sessionDir, "gateguard-session.json");
195
217
  const state = loadState(sessionDir);
196
218
  const filePaths = extractFilePaths(toolInput);
@@ -199,12 +221,12 @@ function main() {
199
221
  const alreadyCleared = filePaths.length > 0 && filePaths.every((path) => isFileCleared(state, path));
200
222
  const newFileCount = countDistinctNewFilePaths(filePaths, state);
201
223
  if (!factsFlagged && !alreadyCleared) {
202
- emit({ decision: "block", reason: buildMutatingFileReason(toolName, filePaths.length > 0 ? filePaths : [filePath], stateFilePath) });
224
+ emitDeny(buildMutatingFileReason(toolName, filePaths.length > 0 ? filePaths : [filePath], stateFilePath));
203
225
  return;
204
226
  }
205
227
  if (factsFlagged && !alreadyCleared) {
206
228
  if (isCapReached(state) || Object.keys(state.cleared_files).length + newFileCount > MAX_CLEARED_FILES) {
207
- emit({ decision: "block", reason: buildCapReachedReason() });
229
+ emitDeny(buildCapReachedReason());
208
230
  return;
209
231
  }
210
232
  let nextState = state;
@@ -214,6 +236,6 @@ function main() {
214
236
  if (filePaths.length > 0)
215
237
  saveState(sessionDir, nextState);
216
238
  }
217
- emit({ decision: "allow" });
239
+ emitAllow();
218
240
  }
219
241
  main();
File without changes
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runtime PreToolUse enforcing-hook pack (v1).
4
+ *
5
+ * Stdin : JSON { tool_name, tool_input: { command } }
6
+ * Stdout : empty on allow / warn (warn prints to stderr); on block, the
7
+ * documented PreToolUse shape:
8
+ * { hookSpecificOutput: { hookEventName: "PreToolUse",
9
+ * permissionDecision: "deny", permissionDecisionReason } }
10
+ * A bare { decision: "allow" } is not schema-valid for PreToolUse.
11
+ * Exit : 0 always (decision is in stdout; fail-open on any error).
12
+ *
13
+ * Two gates over Bash git commands (pure logic in ../lib/hook-pack-gate.mjs):
14
+ * - push-to-main : a direct `git push` to main/master/release-*
15
+ * - commit-size : a `git commit` staging more than COMMIT_FILE_LIMIT files
16
+ *
17
+ * Mode via CLAUDE_CI_HOOKPACK_GATE: "warn" (default) | "block" | "off".
18
+ * warn : print a one-line notice to stderr; never blocks.
19
+ * block : emit the PreToolUse deny shape.
20
+ * off : no-op.
21
+ *
22
+ * Only `git push` / `git commit` commands spawn a git subprocess; every other
23
+ * Bash command and non-Bash tool returns immediately. No network. Fail-open.
24
+ */
25
+ import { execFileSync } from "node:child_process";
26
+ import { readFileSync } from "node:fs";
27
+ import { COMMIT_FILE_LIMIT, classifyCommand, decide, evaluateCommitSize, evaluatePush, parseMode, } from "../lib/hook-pack-gate.mjs";
28
+ function readStdinSync() {
29
+ try {
30
+ return readFileSync(0, "utf8");
31
+ }
32
+ catch {
33
+ return "";
34
+ }
35
+ }
36
+ function safeJsonParse(text) {
37
+ try {
38
+ return JSON.parse(text);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ function gitCurrentBranch() {
45
+ try {
46
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
47
+ encoding: "utf8",
48
+ stdio: ["ignore", "pipe", "ignore"],
49
+ }).trim();
50
+ return branch || null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function gitStagedCount() {
57
+ try {
58
+ const out = execFileSync("git", ["diff", "--cached", "--name-only"], {
59
+ encoding: "utf8",
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ });
62
+ return out.split(/\r?\n/).filter(Boolean).length;
63
+ }
64
+ catch {
65
+ return 0;
66
+ }
67
+ }
68
+ const ALLOW = { action: "allow", reason: "" };
69
+ function compute() {
70
+ const mode = parseMode(process.env.CLAUDE_CI_HOOKPACK_GATE);
71
+ if (mode === "off")
72
+ return ALLOW;
73
+ const payload = safeJsonParse(readStdinSync());
74
+ if (!payload || typeof payload !== "object")
75
+ return ALLOW;
76
+ const obj = payload;
77
+ if (obj.tool_name !== "Bash")
78
+ return ALLOW;
79
+ const input = (obj.tool_input && typeof obj.tool_input === "object"
80
+ ? obj.tool_input
81
+ : {});
82
+ const command = typeof input.command === "string" ? input.command : "";
83
+ const kind = classifyCommand(command);
84
+ if (kind === "push") {
85
+ const res = evaluatePush(command, gitCurrentBranch());
86
+ return decide(mode, res.gated, `Direct push to protected branch "${res.branch}" — open a feature branch + PR instead.`);
87
+ }
88
+ if (kind === "commit") {
89
+ const res = evaluateCommitSize(command, gitStagedCount());
90
+ return decide(mode, res.gated, `This commit stages more than ${COMMIT_FILE_LIMIT} files — exceeds the one-concern limit. Split by layer, or set CLAUDE_CI_HOOKPACK_GATE=off for genuinely generated bulk.`);
91
+ }
92
+ return ALLOW;
93
+ }
94
+ function main() {
95
+ const decision = compute();
96
+ if (decision.action === "block") {
97
+ process.stdout.write(JSON.stringify({
98
+ hookSpecificOutput: {
99
+ hookEventName: "PreToolUse",
100
+ permissionDecision: "deny",
101
+ permissionDecisionReason: decision.reason,
102
+ },
103
+ }));
104
+ }
105
+ else if (decision.action === "warn") {
106
+ process.stderr.write(`[hook-pack] ${decision.reason}\n`);
107
+ }
108
+ process.exit(0);
109
+ }
110
+ main();
package/hooks/observe.sh CHANGED
File without changes
File without changes
File without changes