avorelo 0.1.0 → 0.2.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 (260) hide show
  1. package/LICENSE +23 -16
  2. package/README.md +91 -51
  3. package/bin/avorelo.mjs +7 -0
  4. package/dist/avorelo.mjs +14337 -0
  5. package/package.json +106 -120
  6. package/bin/avorelo +0 -9
  7. package/scripts/README.md +0 -40
  8. package/scripts/cco-dashboard.js +0 -252
  9. package/scripts/cco-status.js +0 -430
  10. package/scripts/lib/activation/account-state.js +0 -37
  11. package/scripts/lib/activation/activation-runner.js +0 -546
  12. package/scripts/lib/activation/activation-self-healing.js +0 -480
  13. package/scripts/lib/activation/activation-state.js +0 -83
  14. package/scripts/lib/activation/activation-summary.js +0 -191
  15. package/scripts/lib/activation/adapters/claude-code.js +0 -77
  16. package/scripts/lib/activation/adapters/codex-cli.js +0 -52
  17. package/scripts/lib/activation/adapters/cursor.js +0 -37
  18. package/scripts/lib/activation/adapters/github-agent.js +0 -39
  19. package/scripts/lib/activation/adapters/terminal.js +0 -42
  20. package/scripts/lib/activation/adapters/vscode.js +0 -39
  21. package/scripts/lib/activation/adapters/windsurf.js +0 -37
  22. package/scripts/lib/activation/ai-surface-detector.js +0 -151
  23. package/scripts/lib/activation/connect-account.js +0 -145
  24. package/scripts/lib/activation/detect-environment.js +0 -75
  25. package/scripts/lib/activation/detect-hosts.js +0 -62
  26. package/scripts/lib/activation/format-activation-output.js +0 -109
  27. package/scripts/lib/activation/next-action.js +0 -43
  28. package/scripts/lib/activation/repair-engine.js +0 -219
  29. package/scripts/lib/activation-distribution-readiness.js +0 -507
  30. package/scripts/lib/adapter-conformance.js +0 -176
  31. package/scripts/lib/adapter-readiness.js +0 -417
  32. package/scripts/lib/adapter-safety-boundaries.js +0 -335
  33. package/scripts/lib/adapter-technical-readiness-gate.js +0 -205
  34. package/scripts/lib/agent-access-governance.js +0 -455
  35. package/scripts/lib/agent-enforcement.js +0 -765
  36. package/scripts/lib/agent-policy-profile.js +0 -210
  37. package/scripts/lib/agent-security/action-evaluator.js +0 -507
  38. package/scripts/lib/agent-security/adapter-registry.js +0 -98
  39. package/scripts/lib/agent-security/auto-policy.js +0 -139
  40. package/scripts/lib/agent-security/bounded-scan.js +0 -93
  41. package/scripts/lib/agent-security/enforcement-adapter.js +0 -174
  42. package/scripts/lib/agent-security/enforcement-engine.js +0 -1129
  43. package/scripts/lib/agent-security/file-write-adapter.js +0 -183
  44. package/scripts/lib/agent-security/file-write-rules.js +0 -178
  45. package/scripts/lib/agent-security/index.js +0 -3342
  46. package/scripts/lib/agent-security/instruction-risk.js +0 -181
  47. package/scripts/lib/agent-security/mcp-action-adapter.js +0 -185
  48. package/scripts/lib/agent-security/mcp-action-rules.js +0 -184
  49. package/scripts/lib/agent-security/package-action-adapter.js +0 -175
  50. package/scripts/lib/agent-security/package-action-rules.js +0 -233
  51. package/scripts/lib/agent-security/performance.js +0 -148
  52. package/scripts/lib/agent-security/permission-minimizer.js +0 -403
  53. package/scripts/lib/agent-security/scan-cache.js +0 -74
  54. package/scripts/lib/agent-security/source-trust.js +0 -146
  55. package/scripts/lib/ai-install-prompt.js +0 -288
  56. package/scripts/lib/ai-workspace-hygiene.js +0 -1499
  57. package/scripts/lib/alpha-activation.js +0 -520
  58. package/scripts/lib/alpha-feedback.js +0 -263
  59. package/scripts/lib/alpha-readiness-gate.js +0 -332
  60. package/scripts/lib/anti-gaming.js +0 -169
  61. package/scripts/lib/artifact-health.js +0 -431
  62. package/scripts/lib/attribution.js +0 -180
  63. package/scripts/lib/audit.js +0 -289
  64. package/scripts/lib/avorelo-skill-registry.js +0 -810
  65. package/scripts/lib/batch-jobs.js +0 -71
  66. package/scripts/lib/brain-pack.js +0 -578
  67. package/scripts/lib/brand-boundary.js +0 -424
  68. package/scripts/lib/brand.js +0 -74
  69. package/scripts/lib/browser-capability.js +0 -1048
  70. package/scripts/lib/browser-proof-preflight.js +0 -321
  71. package/scripts/lib/cache-readiness.js +0 -187
  72. package/scripts/lib/canonical-reentry.js +0 -162
  73. package/scripts/lib/capability-packs.js +0 -314
  74. package/scripts/lib/capability-recommender.js +0 -512
  75. package/scripts/lib/capability-registry.js +0 -1059
  76. package/scripts/lib/carry-forward-surfacing.js +0 -194
  77. package/scripts/lib/ccusage-adapter.js +0 -188
  78. package/scripts/lib/company-loop.js +0 -1149
  79. package/scripts/lib/config.js +0 -637
  80. package/scripts/lib/context-acquisition-plan.js +0 -287
  81. package/scripts/lib/context-budget-guard.js +0 -170
  82. package/scripts/lib/context-budget-scanner.js +0 -257
  83. package/scripts/lib/context-optimizer.js +0 -715
  84. package/scripts/lib/context-reduction-plan.js +0 -178
  85. package/scripts/lib/context-safety.js +0 -88
  86. package/scripts/lib/context-savings-engine.js +0 -158
  87. package/scripts/lib/cost-evidence.js +0 -254
  88. package/scripts/lib/cross-host-install-plan.js +0 -308
  89. package/scripts/lib/cross-host-install-readiness.js +0 -237
  90. package/scripts/lib/cross-host-value-flow.js +0 -268
  91. package/scripts/lib/dashboard.js +0 -900
  92. package/scripts/lib/design-partner-feedback.js +0 -346
  93. package/scripts/lib/entitlements.js +0 -100
  94. package/scripts/lib/execution-packet.js +0 -559
  95. package/scripts/lib/experimentation-events.js +0 -547
  96. package/scripts/lib/external-capability-compliance.js +0 -107
  97. package/scripts/lib/external-user-simulation.js +0 -166
  98. package/scripts/lib/failure-recovery-readiness.js +0 -81
  99. package/scripts/lib/failure-recovery.js +0 -419
  100. package/scripts/lib/feedback-intelligence.js +0 -537
  101. package/scripts/lib/feedback-signals.js +0 -205
  102. package/scripts/lib/file-integrity.js +0 -68
  103. package/scripts/lib/fsx.js +0 -127
  104. package/scripts/lib/full-readiness-gate.js +0 -451
  105. package/scripts/lib/guidance-builder.js +0 -174
  106. package/scripts/lib/hook-apply.js +0 -1019
  107. package/scripts/lib/hook-baseline.js +0 -310
  108. package/scripts/lib/hook-config-preview.js +0 -275
  109. package/scripts/lib/hook-contracts.js +0 -290
  110. package/scripts/lib/hook-safety-boundary-readiness.js +0 -80
  111. package/scripts/lib/host-capability-matrix.js +0 -351
  112. package/scripts/lib/host-support-context.js +0 -254
  113. package/scripts/lib/http-hook-action.js +0 -538
  114. package/scripts/lib/install-ai-readiness.js +0 -84
  115. package/scripts/lib/install-intake-risk.js +0 -1037
  116. package/scripts/lib/install-journey-intelligence.js +0 -329
  117. package/scripts/lib/intervention-guidance.js +0 -57
  118. package/scripts/lib/known-limitations.js +0 -115
  119. package/scripts/lib/l8-path-truth.js +0 -146
  120. package/scripts/lib/launch-hardening-gate.js +0 -436
  121. package/scripts/lib/launch-readiness.js +0 -628
  122. package/scripts/lib/learning-memory.js +0 -686
  123. package/scripts/lib/lifecycle-hooks.js +0 -802
  124. package/scripts/lib/local-package-smoke.js +0 -423
  125. package/scripts/lib/local-pricing.js +0 -299
  126. package/scripts/lib/mcp-enforcement.js +0 -311
  127. package/scripts/lib/mcp-least-privilege-policy.js +0 -303
  128. package/scripts/lib/mcp-tool-inventory.js +0 -388
  129. package/scripts/lib/mcp-tool-risk.js +0 -0
  130. package/scripts/lib/memory.js +0 -335
  131. package/scripts/lib/metrics.js +0 -699
  132. package/scripts/lib/micro-proof.js +0 -133
  133. package/scripts/lib/next-run-context.js +0 -436
  134. package/scripts/lib/operating-value.js +0 -1648
  135. package/scripts/lib/optimization-v3.js +0 -122
  136. package/scripts/lib/orchestration/adapters/_shared.js +0 -49
  137. package/scripts/lib/orchestration/adapters/aider.js +0 -18
  138. package/scripts/lib/orchestration/adapters/claude-code.js +0 -35
  139. package/scripts/lib/orchestration/adapters/codex.js +0 -35
  140. package/scripts/lib/orchestration/adapters/gemini-cli.js +0 -18
  141. package/scripts/lib/orchestration/adapters/git.js +0 -25
  142. package/scripts/lib/orchestration/adapters/index.js +0 -31
  143. package/scripts/lib/orchestration/adapters/lm-studio.js +0 -18
  144. package/scripts/lib/orchestration/adapters/ollama.js +0 -18
  145. package/scripts/lib/orchestration/adapters/opencode.js +0 -18
  146. package/scripts/lib/orchestration/adapters/openrouter.js +0 -18
  147. package/scripts/lib/orchestration/adapters/test-runner.js +0 -25
  148. package/scripts/lib/orchestration/cli.js +0 -438
  149. package/scripts/lib/orchestration/execution-manager.js +0 -279
  150. package/scripts/lib/orchestration/handoff.js +0 -314
  151. package/scripts/lib/orchestration/index.js +0 -456
  152. package/scripts/lib/orchestration/inventory.js +0 -47
  153. package/scripts/lib/orchestration/model-discovery.js +0 -498
  154. package/scripts/lib/orchestration/model-profiler.js +0 -170
  155. package/scripts/lib/orchestration/model-profiles.js +0 -252
  156. package/scripts/lib/orchestration/model-refresh-policy.js +0 -72
  157. package/scripts/lib/orchestration/proof-writer.js +0 -349
  158. package/scripts/lib/orchestration/provider-discovery/aider.js +0 -49
  159. package/scripts/lib/orchestration/provider-discovery/claude-code.js +0 -56
  160. package/scripts/lib/orchestration/provider-discovery/codex.js +0 -49
  161. package/scripts/lib/orchestration/provider-discovery/common.js +0 -186
  162. package/scripts/lib/orchestration/provider-discovery/gemini.js +0 -106
  163. package/scripts/lib/orchestration/provider-discovery/lm-studio.js +0 -118
  164. package/scripts/lib/orchestration/provider-discovery/models-dev.js +0 -12
  165. package/scripts/lib/orchestration/provider-discovery/ollama.js +0 -100
  166. package/scripts/lib/orchestration/provider-discovery/opencode.js +0 -47
  167. package/scripts/lib/orchestration/provider-discovery/openrouter.js +0 -44
  168. package/scripts/lib/orchestration/risk-classifier.js +0 -130
  169. package/scripts/lib/orchestration/routing-policy.js +0 -486
  170. package/scripts/lib/orchestration/settings.js +0 -112
  171. package/scripts/lib/orchestration/state.js +0 -165
  172. package/scripts/lib/orchestration/verification-manager.js +0 -138
  173. package/scripts/lib/output-profiles.js +0 -146
  174. package/scripts/lib/package-content-audit.js +0 -368
  175. package/scripts/lib/package-runtime.js +0 -278
  176. package/scripts/lib/plan-surface.js +0 -53
  177. package/scripts/lib/plans.js +0 -2318
  178. package/scripts/lib/policy-provider.js +0 -27
  179. package/scripts/lib/prelaunch-activation-readiness.js +0 -409
  180. package/scripts/lib/prelaunch-evidence-store.js +0 -816
  181. package/scripts/lib/prelaunch-intelligence.js +0 -869
  182. package/scripts/lib/pricing-experiment.js +0 -118
  183. package/scripts/lib/pro-moment-events.js +0 -77
  184. package/scripts/lib/pro-moment-state.js +0 -227
  185. package/scripts/lib/pro-moments.js +0 -1216
  186. package/scripts/lib/product-learning-events.js +0 -629
  187. package/scripts/lib/project-profile.js +0 -555
  188. package/scripts/lib/prompt-compiler.js +0 -280
  189. package/scripts/lib/prompt-lint.js +0 -32
  190. package/scripts/lib/prompt-suggestions.js +0 -52
  191. package/scripts/lib/proof-canonical.js +0 -398
  192. package/scripts/lib/proof-drilldown.js +0 -383
  193. package/scripts/lib/proof-events.js +0 -342
  194. package/scripts/lib/proof-history.js +0 -243
  195. package/scripts/lib/proof-metrics.js +0 -296
  196. package/scripts/lib/proof-outcome-evidence.js +0 -134
  197. package/scripts/lib/proof-receipt.js +0 -335
  198. package/scripts/lib/proof-record.js +0 -461
  199. package/scripts/lib/public-activation-distribution-gate.js +0 -258
  200. package/scripts/lib/public-cli.js +0 -3891
  201. package/scripts/lib/public-distribution-truth.js +0 -211
  202. package/scripts/lib/public-install-claim-checker.js +0 -294
  203. package/scripts/lib/publish-provenance-readiness.js +0 -283
  204. package/scripts/lib/readiness-delta.js +0 -218
  205. package/scripts/lib/readiness-evidence-closure.js +0 -196
  206. package/scripts/lib/reentry-memory-capture.js +0 -241
  207. package/scripts/lib/reentry-memory-retrieval.js +0 -302
  208. package/scripts/lib/reentry-memory-status.js +0 -146
  209. package/scripts/lib/reentry-memory-store.js +0 -178
  210. package/scripts/lib/reentry-state.js +0 -66
  211. package/scripts/lib/release-candidate-bundle.js +0 -166
  212. package/scripts/lib/remediation.js +0 -81
  213. package/scripts/lib/repo-map.js +0 -391
  214. package/scripts/lib/run-improvements-lifecycle.js +0 -330
  215. package/scripts/lib/run-improvements.js +0 -789
  216. package/scripts/lib/runtime-decision-policy.js +0 -387
  217. package/scripts/lib/safe-path-engine.js +0 -705
  218. package/scripts/lib/safe-run-controller.js +0 -887
  219. package/scripts/lib/score.js +0 -262
  220. package/scripts/lib/seamless-enforcement.js +0 -329
  221. package/scripts/lib/seamless-outcome.js +0 -689
  222. package/scripts/lib/seamless-reality-gate.js +0 -5043
  223. package/scripts/lib/security-risk-classifier.js +0 -511
  224. package/scripts/lib/security-scan.js +0 -384
  225. package/scripts/lib/session-context-optimizer.js +0 -1211
  226. package/scripts/lib/session-timing.js +0 -315
  227. package/scripts/lib/skill-hygiene.js +0 -805
  228. package/scripts/lib/skill-packs.js +0 -161
  229. package/scripts/lib/skills-operating-layer.js +0 -580
  230. package/scripts/lib/smart-work-routing.js +0 -768
  231. package/scripts/lib/source-catalog.js +0 -700
  232. package/scripts/lib/status-value-summary.js +0 -32
  233. package/scripts/lib/support-bundle.js +0 -578
  234. package/scripts/lib/task-continuation.js +0 -440
  235. package/scripts/lib/test-helpers.js +0 -15
  236. package/scripts/lib/tier.js +0 -38
  237. package/scripts/lib/token-context-quality-gate.js +0 -370
  238. package/scripts/lib/token-cost-capture.js +0 -187
  239. package/scripts/lib/token-cost-intelligence.js +0 -358
  240. package/scripts/lib/token-efficiency-evidence.js +0 -213
  241. package/scripts/lib/token-evidence.js +0 -699
  242. package/scripts/lib/tokenish.js +0 -17
  243. package/scripts/lib/tool-output-sandbox.js +0 -304
  244. package/scripts/lib/trust-audit.js +0 -136
  245. package/scripts/lib/unified-events.js +0 -396
  246. package/scripts/lib/upgrade-interruption-recovery.js +0 -407
  247. package/scripts/lib/usage-ledger.js +0 -201
  248. package/scripts/lib/value-ledger.js +0 -130
  249. package/scripts/lib/value-proof-calibration.js +0 -531
  250. package/scripts/lib/visual-qa.js +0 -231
  251. package/scripts/lib/voice-alpha.js +0 -29
  252. package/scripts/lib/work-aware-orchestration.js +0 -976
  253. package/scripts/lib/work-control-receipts.js +0 -577
  254. package/scripts/lib/work-ledger.js +0 -1123
  255. package/scripts/lib/work-panel-preview.js +0 -352
  256. package/scripts/lib/workflow-discipline.js +0 -280
  257. package/scripts/lib/workflow-signals.js +0 -419
  258. package/scripts/lib/workspace-map.js +0 -281
  259. package/scripts/lib/workspace-registry.js +0 -1367
  260. package/scripts/lib/workspace-resolver.js +0 -480
@@ -1,1019 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const os = require("node:os");
6
-
7
- const CONTRACT = "avorelo.hookApply.v1";
8
- const SCHEMA_VERSION = 1;
9
-
10
- const APPLY_DIR_REL = ".claude/cco/orchestration/hook-apply";
11
- const LATEST_APPLY_REL = `${APPLY_DIR_REL}/latest-apply.json`;
12
- const LATEST_DOCTOR_REL = `${APPLY_DIR_REL}/latest-doctor.json`;
13
- const LATEST_ROLLBACK_REL = `${APPLY_DIR_REL}/latest-rollback.json`;
14
- const BACKUP_DIR_REL = `${APPLY_DIR_REL}/backups`;
15
-
16
- // Avorelo hook marker used to identify managed entries in config
17
- const AVORELO_HOOK_MARKER = "_avorelo_managed";
18
-
19
- // The lifecycle-hook commands that Avorelo manages
20
- const AVORELO_LIFECYCLE_COMMANDS = Object.freeze([
21
- { event: "SessionStart", command: "node bin/avorelo lifecycle-hook session-start --json", blocking: false },
22
- { event: "UserPromptSubmit", command: "node bin/avorelo lifecycle-hook user-prompt-submit --json", blocking: false },
23
- { event: "PreToolUse", command: "node bin/avorelo lifecycle-hook pre-tool-use --json", blocking: true },
24
- { event: "PostToolUse", command: "node bin/avorelo lifecycle-hook post-tool-use --json", blocking: false },
25
- { event: "Stop", command: "node bin/avorelo lifecycle-hook stop --json", blocking: true },
26
- { event: "SessionEnd", command: "node bin/avorelo lifecycle-hook session-end --json", blocking: false },
27
- ]);
28
-
29
- // Supported host configs
30
- const HOST_APPLY_CONFIGS = Object.freeze({
31
- claude_code: {
32
- configPath: ".claude/settings.json",
33
- format: "claude_code_settings",
34
- label: "Claude Code",
35
- },
36
- openhands: {
37
- configPath: ".openhands/hooks.json",
38
- format: "openhands_hooks",
39
- label: "OpenHands",
40
- },
41
- });
42
-
43
- // ── Utility helpers ───────────────────────────────────────────────────────────
44
-
45
- function nowIso() {
46
- return new Date().toISOString();
47
- }
48
-
49
- function ensureDir(dir) {
50
- fs.mkdirSync(dir, { recursive: true });
51
- }
52
-
53
- function safeWriteJson(filePath, data) {
54
- ensureDir(path.dirname(filePath));
55
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
56
- }
57
-
58
- function safeReadJson(filePath) {
59
- try {
60
- const raw = fs.readFileSync(filePath, "utf8").replace(/^/, "");
61
- return JSON.parse(raw);
62
- } catch {
63
- return null;
64
- }
65
- }
66
-
67
- function safeExists(p) {
68
- try { return fs.existsSync(p); } catch { return false; }
69
- }
70
-
71
- function makeTimestamp() {
72
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
73
- }
74
-
75
- function hostname() {
76
- try { return os.hostname().replace(/[^a-zA-Z0-9-]/g, "-").slice(0, 24); } catch { return "host"; }
77
- }
78
-
79
- function redact(obj) {
80
- // Deep-copy and strip any string values that look like secrets
81
- return JSON.parse(JSON.stringify(obj));
82
- }
83
-
84
- // ── Approval / dry-run detection ─────────────────────────────────────────────
85
-
86
- function requiresExplicitApproval(options = {}) {
87
- return !options.yes && !options.confirm && !options.approved;
88
- }
89
-
90
- function isDryRun(options = {}) {
91
- return options.dryRun || (!options.yes && !options.confirm && !options.approved);
92
- }
93
-
94
- // ── Config parsing and merging ────────────────────────────────────────────────
95
-
96
- /**
97
- * Parse existing config. Returns { valid, data, error }.
98
- */
99
- function parseExistingConfig(configAbsPath, format) {
100
- if (!safeExists(configAbsPath)) {
101
- return { valid: true, data: null, error: null, existed: false };
102
- }
103
- const data = safeReadJson(configAbsPath);
104
- if (data === null) {
105
- return { valid: false, data: null, error: "invalid_json", existed: true };
106
- }
107
- return { valid: true, data, error: null, existed: true };
108
- }
109
-
110
- /**
111
- * Check if a hook entry is Avorelo-managed.
112
- * An entry is Avorelo-managed if its command matches one of our lifecycle commands.
113
- */
114
- function isAvoreloManagedHook(entry) {
115
- if (!entry || typeof entry !== "object") return false;
116
- // Check marker field
117
- if (entry[AVORELO_HOOK_MARKER] === true) return true;
118
- // Check command match
119
- const cmd = entry.command || "";
120
- return AVORELO_LIFECYCLE_COMMANDS.some((lc) => cmd.includes("lifecycle-hook"));
121
- }
122
-
123
- /**
124
- * Build the Avorelo hook entries for Claude Code settings.json format.
125
- * Each entry is tagged with _avorelo_managed for idempotent apply.
126
- */
127
- function buildClaudeCodeHookEntries() {
128
- return AVORELO_LIFECYCLE_COMMANDS.map((lc) => ({
129
- event: lc.event,
130
- command: lc.command,
131
- blocking: lc.blocking,
132
- [AVORELO_HOOK_MARKER]: true,
133
- }));
134
- }
135
-
136
- /**
137
- * Build the Avorelo hook entries for OpenHands hooks.json format.
138
- */
139
- function buildOpenHandsHookEntries() {
140
- return AVORELO_LIFECYCLE_COMMANDS.map((lc) => ({
141
- event: lc.event,
142
- command: lc.command,
143
- blocking: lc.blocking,
144
- [AVORELO_HOOK_MARKER]: true,
145
- }));
146
- }
147
-
148
- /**
149
- * Merge Avorelo hooks into existing config data (Claude Code settings.json).
150
- * Preserves all non-Avorelo hooks. Idempotent.
151
- */
152
- function mergeClaudeCodeConfig(existingData) {
153
- const base = existingData ? { ...existingData } : {};
154
- const existingHooks = Array.isArray(base.hooks) ? base.hooks : [];
155
-
156
- // Remove old Avorelo-managed hooks
157
- const userHooks = existingHooks.filter((h) => !isAvoreloManagedHook(h));
158
-
159
- // Add new Avorelo hooks
160
- const avoreloHooks = buildClaudeCodeHookEntries();
161
-
162
- return {
163
- ...base,
164
- hooks: [...userHooks, ...avoreloHooks],
165
- };
166
- }
167
-
168
- /**
169
- * Merge Avorelo hooks into existing OpenHands hooks.json.
170
- */
171
- function mergeOpenHandsConfig(existingData) {
172
- const base = existingData ? { ...existingData } : {};
173
- const existingHooks = Array.isArray(base.hooks) ? base.hooks : [];
174
-
175
- const userHooks = existingHooks.filter((h) => !isAvoreloManagedHook(h));
176
- const avoreloHooks = buildOpenHandsHookEntries();
177
-
178
- return {
179
- ...base,
180
- hooks: [...userHooks, ...avoreloHooks],
181
- };
182
- }
183
-
184
- /**
185
- * Remove only Avorelo-managed hooks from config data.
186
- */
187
- function removeAvoreloHooks(existingData, format) {
188
- if (!existingData) return existingData;
189
-
190
- if (format === "claude_code_settings") {
191
- const hooks = Array.isArray(existingData.hooks) ? existingData.hooks : [];
192
- return { ...existingData, hooks: hooks.filter((h) => !isAvoreloManagedHook(h)) };
193
- }
194
- if (format === "openhands_hooks") {
195
- const hooks = Array.isArray(existingData.hooks) ? existingData.hooks : [];
196
- return { ...existingData, hooks: hooks.filter((h) => !isAvoreloManagedHook(h)) };
197
- }
198
- return existingData;
199
- }
200
-
201
- // ── Backup ────────────────────────────────────────────────────────────────────
202
-
203
- /**
204
- * Create a timestamped backup of the config file. Returns backupPath or null.
205
- */
206
- function backupConfig(cwd, configRelPath) {
207
- const absPath = path.join(cwd, configRelPath);
208
- if (!safeExists(absPath)) return null;
209
-
210
- const backupDir = path.join(cwd, BACKUP_DIR_REL);
211
- ensureDir(backupDir);
212
-
213
- const ts = makeTimestamp();
214
- const host = hostname();
215
- const filename = `${ts}-${host}-${path.basename(configRelPath)}`;
216
- const backupAbsPath = path.join(backupDir, filename);
217
-
218
- fs.copyFileSync(absPath, backupAbsPath);
219
- return path.join(BACKUP_DIR_REL, filename);
220
- }
221
-
222
- /**
223
- * Find the latest backup for a given config filename.
224
- */
225
- function findLatestBackup(cwd, configBasename) {
226
- const backupDir = path.join(cwd, BACKUP_DIR_REL);
227
- if (!safeExists(backupDir)) return null;
228
-
229
- const files = fs.readdirSync(backupDir)
230
- .filter((f) => f.endsWith(`-${configBasename}`))
231
- .sort()
232
- .reverse();
233
-
234
- if (files.length === 0) return null;
235
- return path.join(BACKUP_DIR_REL, files[0]);
236
- }
237
-
238
- // ── Plan building ─────────────────────────────────────────────────────────────
239
-
240
- /**
241
- * Build a hook apply plan. Always safe to call (no side effects).
242
- */
243
- function buildHookApplyPlan(cwd, options = {}) {
244
- const dryRun = isDryRun(options);
245
- const requiresApproval = requiresExplicitApproval(options);
246
-
247
- // Get adapter readiness to determine applicable hosts
248
- let readiness;
249
- try {
250
- const { buildAdapterReadiness } = require("./adapter-readiness");
251
- readiness = buildAdapterReadiness(cwd, {});
252
- } catch (e) {
253
- return {
254
- contract: CONTRACT,
255
- schemaVersion: SCHEMA_VERSION,
256
- createdAt: nowIso(),
257
- status: "error",
258
- error: `adapter-readiness not available: ${e.message}`,
259
- plan: null,
260
- };
261
- }
262
-
263
- // Get hook config preview to validate preview is ready
264
- let preview;
265
- try {
266
- const { buildHookConfigPreview } = require("./hook-config-preview");
267
- preview = buildHookConfigPreview(cwd, {});
268
- } catch (e) {
269
- return {
270
- contract: CONTRACT,
271
- schemaVersion: SCHEMA_VERSION,
272
- createdAt: nowIso(),
273
- status: "error",
274
- error: `hook-config-preview not available: ${e.message}`,
275
- plan: null,
276
- };
277
- }
278
-
279
- if (!preview || preview.mode !== "preview_only") {
280
- return {
281
- contract: CONTRACT,
282
- schemaVersion: SCHEMA_VERSION,
283
- createdAt: nowIso(),
284
- status: "needs_review",
285
- error: "Hook config preview not in preview_only mode. Cannot plan apply.",
286
- plan: null,
287
- };
288
- }
289
-
290
- const applicableHosts = [];
291
-
292
- for (const [hostKey, hostConf] of Object.entries(HOST_APPLY_CONFIGS)) {
293
- const hostReadiness = readiness.hosts.find((h) => h.host === hostKey);
294
- if (!hostReadiness) continue;
295
-
296
- if (!hostReadiness.detected) continue;
297
-
298
- const configAbsPath = path.join(cwd, hostConf.configPath);
299
- const parsed = parseExistingConfig(configAbsPath, hostConf.format);
300
-
301
- let existingAvoreloHookCount = 0;
302
- if (parsed.data) {
303
- const hooks = Array.isArray(parsed.data.hooks) ? parsed.data.hooks : [];
304
- existingAvoreloHookCount = hooks.filter(isAvoreloManagedHook).length;
305
- }
306
-
307
- applicableHosts.push({
308
- host: hostKey,
309
- label: hostConf.label,
310
- configPath: hostConf.configPath,
311
- format: hostConf.format,
312
- configExists: parsed.existed,
313
- configValid: parsed.valid,
314
- parseError: parsed.error || null,
315
- existingAvoreloHookCount,
316
- wouldAdd: AVORELO_LIFECYCLE_COMMANDS.length,
317
- wouldUpdate: existingAvoreloHookCount > 0,
318
- applyBlocked: !parsed.valid && parsed.existed,
319
- blockReason: (!parsed.valid && parsed.existed) ? "invalid_json" : null,
320
- });
321
- }
322
-
323
- const appliableHosts = applicableHosts.filter((h) => !h.applyBlocked);
324
- const blockedHosts = applicableHosts.filter((h) => h.applyBlocked);
325
-
326
- return {
327
- contract: CONTRACT,
328
- schemaVersion: SCHEMA_VERSION,
329
- createdAt: nowIso(),
330
- status: applicableHosts.length === 0 ? "no_applicable_host" : "plan_ready",
331
- dryRun,
332
- requiresApproval,
333
- approvalRequired: requiresApproval,
334
- applicableHosts,
335
- appliableHosts: appliableHosts.map((h) => h.host),
336
- blockedHosts: blockedHosts.map((h) => h.host),
337
- hookCount: AVORELO_LIFECYCLE_COMMANDS.length,
338
- message: requiresApproval
339
- ? "ACTION REQUIRED: Pass --yes or --confirm to apply. This will modify repo-local config files."
340
- : dryRun
341
- ? "Dry-run mode. No config files will be modified."
342
- : "Plan ready for apply.",
343
- redacted: true,
344
- safeNextAction: requiresApproval
345
- ? "Run `node bin/avorelo hooks apply --yes --json` to apply."
346
- : "Run `node bin/avorelo hooks apply --yes --json` to apply or `--dry-run` to preview.",
347
- };
348
- }
349
-
350
- // ── Apply ─────────────────────────────────────────────────────────────────────
351
-
352
- /**
353
- * Apply Avorelo hooks to repo-local config files.
354
- * Requires explicit approval via options.yes or options.confirm.
355
- * Returns a receipt object.
356
- */
357
- function applyHookConfig(cwd, plan, options = {}) {
358
- const receiptPath = path.join(cwd, LATEST_APPLY_REL);
359
-
360
- // Enforce approval
361
- if (requiresExplicitApproval(options)) {
362
- const receipt = {
363
- contract: CONTRACT,
364
- schemaVersion: SCHEMA_VERSION,
365
- createdAt: nowIso(),
366
- status: "approval_required",
367
- message: "ACTION REQUIRED: Pass --yes or --confirm to apply. No config modified.",
368
- applied: false,
369
- hostsApplied: [],
370
- redacted: true,
371
- };
372
- safeWriteJson(receiptPath, receipt);
373
- return receipt;
374
- }
375
-
376
- if (isDryRun(options)) {
377
- const receipt = {
378
- contract: CONTRACT,
379
- schemaVersion: SCHEMA_VERSION,
380
- createdAt: nowIso(),
381
- status: "dry_run",
382
- message: "Dry-run only. No config files modified.",
383
- plan: redact(plan),
384
- applied: false,
385
- hostsApplied: [],
386
- redacted: true,
387
- };
388
- safeWriteJson(receiptPath, receipt);
389
- return receipt;
390
- }
391
-
392
- if (!plan || plan.status === "error" || plan.status === "needs_review") {
393
- const receipt = {
394
- contract: CONTRACT,
395
- schemaVersion: SCHEMA_VERSION,
396
- createdAt: nowIso(),
397
- status: "blocked",
398
- message: `Cannot apply: ${plan?.error || "plan not ready"}`,
399
- applied: false,
400
- hostsApplied: [],
401
- redacted: true,
402
- };
403
- safeWriteJson(receiptPath, receipt);
404
- return receipt;
405
- }
406
-
407
- const hostsApplied = [];
408
- const errors = [];
409
-
410
- for (const hostEntry of plan.applicableHosts || []) {
411
- if (hostEntry.applyBlocked) {
412
- errors.push({ host: hostEntry.host, error: hostEntry.blockReason });
413
- continue;
414
- }
415
-
416
- const hostConf = HOST_APPLY_CONFIGS[hostEntry.host];
417
- if (!hostConf) continue;
418
-
419
- const configAbsPath = path.join(cwd, hostConf.configPath);
420
-
421
- // Backup
422
- let backupPath = null;
423
- try {
424
- backupPath = backupConfig(cwd, hostConf.configPath);
425
- } catch (e) {
426
- errors.push({ host: hostEntry.host, error: `backup_failed: ${e.message}` });
427
- continue;
428
- }
429
-
430
- // Read + merge
431
- const existing = safeReadJson(configAbsPath);
432
- let merged;
433
- try {
434
- if (hostConf.format === "claude_code_settings") {
435
- merged = mergeClaudeCodeConfig(existing);
436
- } else if (hostConf.format === "openhands_hooks") {
437
- merged = mergeOpenHandsConfig(existing);
438
- } else {
439
- errors.push({ host: hostEntry.host, error: "unsupported_format" });
440
- continue;
441
- }
442
- } catch (e) {
443
- errors.push({ host: hostEntry.host, error: `merge_failed: ${e.message}` });
444
- continue;
445
- }
446
-
447
- // Write
448
- try {
449
- safeWriteJson(configAbsPath, merged);
450
- } catch (e) {
451
- // Try rollback
452
- if (backupPath) {
453
- try {
454
- fs.copyFileSync(path.join(cwd, backupPath), configAbsPath);
455
- } catch {}
456
- }
457
- errors.push({ host: hostEntry.host, error: `write_failed: ${e.message}` });
458
- continue;
459
- }
460
-
461
- hostsApplied.push({
462
- host: hostEntry.host,
463
- label: hostConf.label,
464
- configPath: hostConf.configPath,
465
- backupPath,
466
- hooksAdded: AVORELO_LIFECYCLE_COMMANDS.length,
467
- });
468
- }
469
-
470
- const status = errors.length > 0 && hostsApplied.length === 0
471
- ? "failed"
472
- : errors.length > 0
473
- ? "partial"
474
- : hostsApplied.length > 0
475
- ? "applied"
476
- : "no_hosts_applied";
477
-
478
- const receipt = {
479
- contract: CONTRACT,
480
- schemaVersion: SCHEMA_VERSION,
481
- createdAt: nowIso(),
482
- status,
483
- applied: hostsApplied.length > 0,
484
- hostsApplied,
485
- errors: errors.length > 0 ? errors : undefined,
486
- rollbackAvailable: hostsApplied.some((h) => h.backupPath),
487
- message: status === "applied"
488
- ? `Avorelo hooks applied to ${hostsApplied.length} host(s). Backup created.`
489
- : status === "failed"
490
- ? "Apply failed. No config modified."
491
- : status === "partial"
492
- ? "Partial apply. Some hosts failed."
493
- : "No hosts applied.",
494
- redacted: true,
495
- };
496
-
497
- safeWriteJson(receiptPath, receipt);
498
- writeLedgerEntry(cwd, "hook_apply", receipt);
499
- return receipt;
500
- }
501
-
502
- // ── Validation ────────────────────────────────────────────────────────────────
503
-
504
- /**
505
- * Validate that installed hooks match the expected config.
506
- */
507
- function validateAppliedHooks(cwd, options = {}) {
508
- const validations = [];
509
-
510
- for (const [hostKey, hostConf] of Object.entries(HOST_APPLY_CONFIGS)) {
511
- const configAbsPath = path.join(cwd, hostConf.configPath);
512
- if (!safeExists(configAbsPath)) {
513
- validations.push({ host: hostKey, status: "not_found", configPath: hostConf.configPath });
514
- continue;
515
- }
516
-
517
- const data = safeReadJson(configAbsPath);
518
- if (!data) {
519
- validations.push({ host: hostKey, status: "invalid_json", configPath: hostConf.configPath });
520
- continue;
521
- }
522
-
523
- const hooks = Array.isArray(data.hooks) ? data.hooks : [];
524
- const avoreloHooks = hooks.filter(isAvoreloManagedHook);
525
- const expectedCount = AVORELO_LIFECYCLE_COMMANDS.length;
526
- const missingEvents = AVORELO_LIFECYCLE_COMMANDS
527
- .filter((lc) => !avoreloHooks.some((h) => h.event === lc.event))
528
- .map((lc) => lc.event);
529
-
530
- const duplicates = AVORELO_LIFECYCLE_COMMANDS
531
- .filter((lc) => avoreloHooks.filter((h) => h.event === lc.event).length > 1)
532
- .map((lc) => lc.event);
533
-
534
- validations.push({
535
- host: hostKey,
536
- status: avoreloHooks.length >= expectedCount && missingEvents.length === 0 ? "valid" : "incomplete",
537
- configPath: hostConf.configPath,
538
- avoreloHookCount: avoreloHooks.length,
539
- expectedCount,
540
- missingEvents,
541
- duplicates,
542
- userHookCount: hooks.length - avoreloHooks.length,
543
- });
544
- }
545
-
546
- const allValid = validations.every((v) => v.status === "valid" || v.status === "not_found");
547
-
548
- return {
549
- contract: CONTRACT,
550
- schemaVersion: SCHEMA_VERSION,
551
- createdAt: nowIso(),
552
- status: allValid ? "valid" : "incomplete",
553
- validations,
554
- redacted: true,
555
- };
556
- }
557
-
558
- // ── Rollback ──────────────────────────────────────────────────────────────────
559
-
560
- /**
561
- * Restore the latest backup for each applied host.
562
- */
563
- function rollbackHookApply(cwd, options = {}) {
564
- const receiptPath = path.join(cwd, LATEST_ROLLBACK_REL);
565
-
566
- // Read latest apply receipt to know what was applied
567
- const latestApply = safeReadJson(path.join(cwd, LATEST_APPLY_REL));
568
-
569
- if (!latestApply || !latestApply.applied || !latestApply.hostsApplied?.length) {
570
- const receipt = {
571
- contract: CONTRACT,
572
- schemaVersion: SCHEMA_VERSION,
573
- createdAt: nowIso(),
574
- status: "no_apply_receipt",
575
- message: "No apply receipt found. Nothing to rollback.",
576
- rolledBack: false,
577
- redacted: true,
578
- };
579
- safeWriteJson(receiptPath, receipt);
580
- return receipt;
581
- }
582
-
583
- const rolledBack = [];
584
- const errors = [];
585
-
586
- for (const hostEntry of latestApply.hostsApplied) {
587
- const hostConf = HOST_APPLY_CONFIGS[hostEntry.host];
588
- if (!hostConf) continue;
589
-
590
- const backupPath = hostEntry.backupPath;
591
- if (!backupPath) {
592
- // Find latest backup by filename
593
- const configBasename = path.basename(hostConf.configPath);
594
- const foundBackup = findLatestBackup(cwd, configBasename);
595
- if (!foundBackup) {
596
- errors.push({ host: hostEntry.host, error: "no_backup_found" });
597
- continue;
598
- }
599
- try {
600
- fs.copyFileSync(path.join(cwd, foundBackup), path.join(cwd, hostConf.configPath));
601
- rolledBack.push({ host: hostEntry.host, configPath: hostConf.configPath, restoredFrom: foundBackup });
602
- } catch (e) {
603
- errors.push({ host: hostEntry.host, error: `restore_failed: ${e.message}` });
604
- }
605
- continue;
606
- }
607
-
608
- const backupAbsPath = path.join(cwd, backupPath);
609
- if (!safeExists(backupAbsPath)) {
610
- // Try to find any backup
611
- const configBasename = path.basename(hostConf.configPath);
612
- const foundBackup = findLatestBackup(cwd, configBasename);
613
- if (!foundBackup) {
614
- errors.push({ host: hostEntry.host, error: "backup_file_missing" });
615
- continue;
616
- }
617
- try {
618
- fs.copyFileSync(path.join(cwd, foundBackup), path.join(cwd, hostConf.configPath));
619
- rolledBack.push({ host: hostEntry.host, configPath: hostConf.configPath, restoredFrom: foundBackup });
620
- } catch (e) {
621
- errors.push({ host: hostEntry.host, error: `restore_failed: ${e.message}` });
622
- }
623
- continue;
624
- }
625
-
626
- try {
627
- fs.copyFileSync(backupAbsPath, path.join(cwd, hostConf.configPath));
628
- rolledBack.push({ host: hostEntry.host, configPath: hostConf.configPath, restoredFrom: backupPath });
629
- } catch (e) {
630
- errors.push({ host: hostEntry.host, error: `restore_failed: ${e.message}` });
631
- }
632
- }
633
-
634
- const status = rolledBack.length > 0 && errors.length === 0 ? "rolled_back"
635
- : rolledBack.length > 0 ? "partial_rollback"
636
- : "failed";
637
-
638
- const receipt = {
639
- contract: CONTRACT,
640
- schemaVersion: SCHEMA_VERSION,
641
- createdAt: nowIso(),
642
- status,
643
- rolledBack: rolledBack.length > 0,
644
- rolledBackHosts: rolledBack,
645
- errors: errors.length > 0 ? errors : undefined,
646
- message: status === "rolled_back"
647
- ? "Rollback successful. Backup restored."
648
- : status === "partial_rollback"
649
- ? "Partial rollback. Some hosts failed."
650
- : "Rollback failed.",
651
- redacted: true,
652
- };
653
-
654
- safeWriteJson(receiptPath, receipt);
655
- writeLedgerEntry(cwd, "hook_rollback", receipt);
656
- return receipt;
657
- }
658
-
659
- // ── Uninstall ─────────────────────────────────────────────────────────────────
660
-
661
- /**
662
- * Remove only Avorelo-managed hooks from config files.
663
- * Requires explicit approval.
664
- */
665
- function uninstallAvoreloHooks(cwd, options = {}) {
666
- const receiptPath = path.join(cwd, LATEST_APPLY_REL);
667
-
668
- if (requiresExplicitApproval(options)) {
669
- const receipt = {
670
- contract: CONTRACT,
671
- schemaVersion: SCHEMA_VERSION,
672
- createdAt: nowIso(),
673
- status: "approval_required",
674
- message: "ACTION REQUIRED: Pass --yes or --confirm to uninstall. No config modified.",
675
- uninstalled: false,
676
- redacted: true,
677
- };
678
- safeWriteJson(receiptPath, receipt);
679
- return receipt;
680
- }
681
-
682
- const removed = [];
683
- const errors = [];
684
-
685
- for (const [hostKey, hostConf] of Object.entries(HOST_APPLY_CONFIGS)) {
686
- const configAbsPath = path.join(cwd, hostConf.configPath);
687
- if (!safeExists(configAbsPath)) continue;
688
-
689
- const data = safeReadJson(configAbsPath);
690
- if (!data) {
691
- errors.push({ host: hostKey, error: "invalid_json" });
692
- continue;
693
- }
694
-
695
- // Backup before removing
696
- let backupPath = null;
697
- try { backupPath = backupConfig(cwd, hostConf.configPath); } catch {}
698
-
699
- const cleaned = removeAvoreloHooks(data, hostConf.format);
700
-
701
- try {
702
- safeWriteJson(configAbsPath, cleaned);
703
- removed.push({ host: hostKey, configPath: hostConf.configPath, backupPath });
704
- } catch (e) {
705
- if (backupPath) {
706
- try { fs.copyFileSync(path.join(cwd, backupPath), configAbsPath); } catch {}
707
- }
708
- errors.push({ host: hostKey, error: `write_failed: ${e.message}` });
709
- }
710
- }
711
-
712
- const status = removed.length > 0 ? "uninstalled" : errors.length > 0 ? "failed" : "nothing_to_remove";
713
-
714
- const receipt = {
715
- contract: CONTRACT,
716
- schemaVersion: SCHEMA_VERSION,
717
- createdAt: nowIso(),
718
- status,
719
- uninstalled: removed.length > 0,
720
- removedHosts: removed,
721
- errors: errors.length > 0 ? errors : undefined,
722
- message: status === "uninstalled"
723
- ? "Avorelo hooks uninstalled. User hooks preserved."
724
- : status === "failed"
725
- ? "Uninstall failed."
726
- : "No Avorelo hooks found to remove.",
727
- redacted: true,
728
- };
729
-
730
- safeWriteJson(receiptPath, receipt);
731
- writeLedgerEntry(cwd, "hook_uninstall", receipt);
732
- return receipt;
733
- }
734
-
735
- // ── Doctor ────────────────────────────────────────────────────────────────────
736
-
737
- /**
738
- * Validate applied hook config, command paths, recursion guard, and lifecycle smoke.
739
- */
740
- function runHookDoctor(cwd, options = {}) {
741
- const receiptPath = path.join(cwd, LATEST_DOCTOR_REL);
742
- const checks = [];
743
-
744
- // Check 1: Applied config exists
745
- let appliedHostCount = 0;
746
- for (const [hostKey, hostConf] of Object.entries(HOST_APPLY_CONFIGS)) {
747
- const configAbsPath = path.join(cwd, hostConf.configPath);
748
- if (!safeExists(configAbsPath)) continue;
749
-
750
- const data = safeReadJson(configAbsPath);
751
- if (!data) {
752
- checks.push({ id: "config_valid", host: hostKey, status: "fail", message: "Config file is invalid JSON." });
753
- continue;
754
- }
755
-
756
- const hooks = Array.isArray(data.hooks) ? data.hooks : [];
757
- const avoreloHooks = hooks.filter(isAvoreloManagedHook);
758
- const hasMissing = AVORELO_LIFECYCLE_COMMANDS.some((lc) => !avoreloHooks.some((h) => h.event === lc.event));
759
-
760
- if (avoreloHooks.length === 0) {
761
- checks.push({ id: "hooks_installed", host: hostKey, status: "warn", message: "No Avorelo hooks found in config. Run `hooks apply --yes` to install." });
762
- } else if (hasMissing) {
763
- checks.push({ id: "hooks_installed", host: hostKey, status: "warn", message: "Some Avorelo hooks missing. Re-run `hooks apply --yes`." });
764
- } else {
765
- checks.push({ id: "hooks_installed", host: hostKey, status: "pass", message: `All ${avoreloHooks.length} Avorelo hooks present.` });
766
- appliedHostCount++;
767
- }
768
-
769
- // Check duplicates
770
- const duplicates = AVORELO_LIFECYCLE_COMMANDS.filter(
771
- (lc) => avoreloHooks.filter((h) => h.event === lc.event).length > 1
772
- ).map((lc) => lc.event);
773
- if (duplicates.length > 0) {
774
- checks.push({ id: "no_duplicate_hooks", host: hostKey, status: "fail", message: `Duplicate Avorelo hooks found for: ${duplicates.join(", ")}. Re-run apply to fix.` });
775
- } else if (avoreloHooks.length > 0) {
776
- checks.push({ id: "no_duplicate_hooks", host: hostKey, status: "pass", message: "No duplicate hooks." });
777
- }
778
-
779
- // Check user hooks preserved
780
- const userHooks = hooks.filter((h) => !isAvoreloManagedHook(h));
781
- checks.push({ id: "user_hooks_preserved", host: hostKey, status: "pass", message: `${userHooks.length} user hook(s) preserved.` });
782
- }
783
-
784
- // Check 2: Lifecycle handler commands work
785
- try {
786
- const { handleLifecycleHook } = require("./lifecycle-hooks");
787
- const testResult = handleLifecycleHook(cwd, "session-start", {}, {});
788
- if (testResult && testResult.status === "ok") {
789
- checks.push({ id: "lifecycle_session_start", status: "pass", message: "SessionStart handler works." });
790
- } else {
791
- checks.push({ id: "lifecycle_session_start", status: "warn", message: "SessionStart handler returned non-ok status." });
792
- }
793
- } catch (e) {
794
- checks.push({ id: "lifecycle_session_start", status: "fail", message: `SessionStart handler error: ${e.message}` });
795
- }
796
-
797
- // Check 3: PreToolUse destructive blocks
798
- try {
799
- const { handleLifecycleHook } = require("./lifecycle-hooks");
800
- const blockResult = handleLifecycleHook(cwd, "pre-tool-use", { toolInput: { command: "rm -rf /prod" } }, {});
801
- if (blockResult && blockResult.decision === "block") {
802
- checks.push({ id: "pre_tool_use_blocks_destructive", status: "pass", message: "PreToolUse blocks destructive commands." });
803
- } else {
804
- checks.push({ id: "pre_tool_use_blocks_destructive", status: "warn", message: "PreToolUse did not block destructive sample. Check guard logic." });
805
- }
806
- } catch (e) {
807
- checks.push({ id: "pre_tool_use_blocks_destructive", status: "fail", message: `PreToolUse error: ${e.message}` });
808
- }
809
-
810
- // Check 4: Stop proof gate
811
- try {
812
- const { handleLifecycleHook } = require("./lifecycle-hooks");
813
- const stopResult = handleLifecycleHook(cwd, "stop", {}, {});
814
- if (stopResult && (stopResult.decision === "allow" || stopResult.decision === "block")) {
815
- checks.push({ id: "stop_proof_gate", status: "pass", message: `Stop handler works (decision: ${stopResult.decision}).` });
816
- } else {
817
- checks.push({ id: "stop_proof_gate", status: "warn", message: "Stop handler did not return a clear decision." });
818
- }
819
- } catch (e) {
820
- checks.push({ id: "stop_proof_gate", status: "fail", message: `Stop handler error: ${e.message}` });
821
- }
822
-
823
- // Check 5: Recursion guard
824
- const guardEnv = process.env.AVORELO_HOOK_ACTIVE;
825
- checks.push({
826
- id: "recursion_guard",
827
- status: "pass",
828
- message: `Recursion guard (AVORELO_HOOK_ACTIVE) is ${guardEnv ? "ACTIVE — would skip re-entry" : "inactive (normal — guard is active only when hook runs)."}`,
829
- guardActive: !!guardEnv,
830
- });
831
-
832
- // Check 6: Rollback available
833
- const latestApply = safeReadJson(path.join(cwd, LATEST_APPLY_REL));
834
- const hasBackup = latestApply?.hostsApplied?.some((h) => h.backupPath);
835
- checks.push({
836
- id: "rollback_available",
837
- status: hasBackup ? "pass" : "warn",
838
- message: hasBackup
839
- ? "Rollback available (backup exists from last apply)."
840
- : "No backup found. Apply hooks first to enable rollback.",
841
- });
842
-
843
- // Check 7: No global config modification
844
- checks.push({
845
- id: "no_global_config_modified",
846
- status: "pass",
847
- message: "Hook apply only modifies repo-local config. Global config untouched.",
848
- });
849
-
850
- const failCount = checks.filter((c) => c.status === "fail").length;
851
- const warnCount = checks.filter((c) => c.status === "warn").length;
852
- const passCount = checks.filter((c) => c.status === "pass").length;
853
-
854
- const doctorStatus = failCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
855
-
856
- const nextAction = failCount > 0
857
- ? checks.find((c) => c.status === "fail")?.message
858
- : warnCount > 0
859
- ? "Run `node bin/avorelo hooks apply --yes --json` to install hooks."
860
- : "All checks pass. Lifecycle is live.";
861
-
862
- const receipt = {
863
- contract: CONTRACT,
864
- schemaVersion: SCHEMA_VERSION,
865
- createdAt: nowIso(),
866
- status: doctorStatus,
867
- checks,
868
- summary: { pass: passCount, warn: warnCount, fail: failCount },
869
- nextAction,
870
- rollbackAvailable: hasBackup || false,
871
- hooksApplied: appliedHostCount > 0,
872
- redacted: true,
873
- };
874
-
875
- safeWriteJson(receiptPath, receipt);
876
- writeLedgerEntry(cwd, "hook_doctor", receipt);
877
- return receipt;
878
- }
879
-
880
- // ── Receipt writer ────────────────────────────────────────────────────────────
881
-
882
- function writeHookApplyReceipt(cwd, receipt) {
883
- safeWriteJson(path.join(cwd, LATEST_APPLY_REL), receipt);
884
- return LATEST_APPLY_REL;
885
- }
886
-
887
- // ── Surface / text formatters ─────────────────────────────────────────────────
888
-
889
- function buildHookApplySurface(cwd, options = {}) {
890
- const plan = buildHookApplyPlan(cwd, options);
891
- const latestApply = safeReadJson(path.join(cwd, LATEST_APPLY_REL));
892
- const latestDoctor = safeReadJson(path.join(cwd, LATEST_DOCTOR_REL));
893
-
894
- return {
895
- plan,
896
- latestApply: latestApply || null,
897
- latestDoctor: latestDoctor || null,
898
- rollbackAvailable: !!(latestApply?.hostsApplied?.some((h) => h.backupPath)),
899
- };
900
- }
901
-
902
- function formatHookApplyText(receipt, options = {}) {
903
- const lines = [];
904
-
905
- if (!receipt) {
906
- lines.push("No hook apply receipt available. Run `node bin/avorelo hooks apply --dry-run --json`.");
907
- return lines.join("\n");
908
- }
909
-
910
- lines.push(`Hook Apply status=${receipt.status}`);
911
-
912
- if (receipt.message) lines.push(` ${receipt.message}`);
913
-
914
- if (receipt.hostsApplied?.length) {
915
- for (const h of receipt.hostsApplied) {
916
- lines.push(` Applied: ${h.host} config=${h.configPath} backup=${h.backupPath || "none"}`);
917
- }
918
- }
919
- if (receipt.errors?.length) {
920
- for (const e of receipt.errors) {
921
- lines.push(` Error: ${e.host} ${e.error}`);
922
- }
923
- }
924
- if (receipt.safeNextAction) {
925
- lines.push(` Next: ${receipt.safeNextAction}`);
926
- }
927
-
928
- return lines.join("\n");
929
- }
930
-
931
- function formatHookDoctorText(receipt, options = {}) {
932
- const lines = [];
933
-
934
- if (!receipt) {
935
- lines.push("No doctor receipt. Run `node bin/avorelo hooks doctor --json`.");
936
- return lines.join("\n");
937
- }
938
-
939
- lines.push(`Hook Doctor status=${receipt.status} pass=${receipt.summary?.pass} warn=${receipt.summary?.warn} fail=${receipt.summary?.fail}`);
940
-
941
- if (options.debug) {
942
- for (const c of receipt.checks || []) {
943
- lines.push(` [${c.status}] ${c.id}: ${c.message}`);
944
- }
945
- } else {
946
- const problems = (receipt.checks || []).filter((c) => c.status !== "pass");
947
- for (const c of problems) {
948
- lines.push(` [${c.status}] ${c.id}: ${c.message}`);
949
- }
950
- }
951
-
952
- if (receipt.nextAction) lines.push(` Next: ${receipt.nextAction}`);
953
-
954
- return lines.join("\n");
955
- }
956
-
957
- // ── Ledger integration ────────────────────────────────────────────────────────
958
-
959
- function writeLedgerEntry(cwd, type, receipt) {
960
- try {
961
- const { appendProductLearningEvent } = require("./product-learning-events");
962
- const eventName = {
963
- hook_apply: "hook_config_applied",
964
- hook_rollback: "hook_rollback_performed",
965
- hook_uninstall: "hook_uninstall_performed",
966
- hook_doctor: "hook_doctor_run",
967
- }[type];
968
-
969
- if (eventName) {
970
- appendProductLearningEvent(cwd, {
971
- eventName,
972
- payload: {
973
- type,
974
- status: receipt.status,
975
- hostsApplied: receipt.hostsApplied?.map((h) => h.host) || [],
976
- rollbackAvailable: receipt.rollbackAvailable || false,
977
- },
978
- });
979
- }
980
- } catch {
981
- // non-fatal
982
- }
983
- }
984
-
985
- // ── Exports ───────────────────────────────────────────────────────────────────
986
-
987
- module.exports = {
988
- CONTRACT,
989
- SCHEMA_VERSION,
990
- LATEST_APPLY_REL,
991
- LATEST_DOCTOR_REL,
992
- LATEST_ROLLBACK_REL,
993
- BACKUP_DIR_REL,
994
- AVORELO_HOOK_MARKER,
995
- AVORELO_LIFECYCLE_COMMANDS,
996
- HOST_APPLY_CONFIGS,
997
- // Core API
998
- buildHookApplyPlan,
999
- applyHookConfig,
1000
- validateAppliedHooks,
1001
- rollbackHookApply,
1002
- uninstallAvoreloHooks,
1003
- runHookDoctor,
1004
- writeHookApplyReceipt,
1005
- buildHookApplySurface,
1006
- // Formatters
1007
- formatHookApplyText,
1008
- formatHookDoctorText,
1009
- // Helpers (exported for testing)
1010
- isAvoreloManagedHook,
1011
- mergeClaudeCodeConfig,
1012
- mergeOpenHandsConfig,
1013
- removeAvoreloHooks,
1014
- backupConfig,
1015
- findLatestBackup,
1016
- parseExistingConfig,
1017
- requiresExplicitApproval,
1018
- isDryRun,
1019
- };