@geraldmaron/construct 1.0.12 → 1.0.15

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 (337) hide show
  1. package/README.md +14 -2
  2. package/bin/construct +262 -14
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/commands/build/feature.md +0 -6
  5. package/commands/build/fix.md +0 -6
  6. package/commands/design/access.md +0 -6
  7. package/commands/design/flow.md +0 -6
  8. package/commands/design/ui.md +0 -6
  9. package/commands/measure/experiment.md +0 -6
  10. package/commands/measure/metrics.md +0 -6
  11. package/commands/measure/results.md +0 -6
  12. package/commands/plan/api.md +0 -6
  13. package/commands/plan/challenge.md +0 -6
  14. package/commands/plan/decide.md +0 -6
  15. package/commands/plan/feature.md +0 -7
  16. package/commands/plan/requirements.md +0 -6
  17. package/commands/remember/context.md +0 -6
  18. package/commands/remember/handoff.md +0 -6
  19. package/commands/remember/runbook.md +0 -6
  20. package/commands/review/code.md +0 -6
  21. package/commands/review/quality.md +0 -6
  22. package/commands/review/security.md +0 -6
  23. package/commands/ship/ready.md +0 -6
  24. package/commands/ship/release.md +0 -6
  25. package/commands/ship/status.md +0 -6
  26. package/commands/understand/docs.md +0 -6
  27. package/commands/understand/research.md +0 -6
  28. package/commands/understand/this.md +0 -6
  29. package/commands/understand/why.md +0 -6
  30. package/commands/work/clean.md +0 -6
  31. package/commands/work/drive.md +0 -6
  32. package/commands/work/optimize-prompts.md +0 -6
  33. package/commands/work/parallel-review.md +0 -6
  34. package/lib/auto-docs.mjs +11 -7
  35. package/lib/beads-client.mjs +19 -1
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +54 -10
  39. package/lib/contracts/violation-log.mjs +154 -0
  40. package/lib/dashboard-static.mjs +7 -4
  41. package/lib/doc-stamp.mjs +16 -0
  42. package/lib/docs-verify.mjs +1 -8
  43. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  44. package/lib/document-extract/docling-client.mjs +114 -0
  45. package/lib/document-extract/docling-sidecar.py +122 -0
  46. package/lib/document-extract/whisper-client.mjs +79 -0
  47. package/lib/document-extract.mjs +73 -8
  48. package/lib/document-ingest.mjs +26 -4
  49. package/lib/embed/daemon.mjs +26 -1
  50. package/lib/embed/docs-lifecycle.mjs +19 -0
  51. package/lib/embed/inbox.mjs +85 -2
  52. package/lib/embed/recommendation-store.mjs +29 -0
  53. package/lib/flavors/loader.mjs +1 -1
  54. package/lib/gates-audit.mjs +18 -12
  55. package/lib/handoffs/contract.mjs +2 -2
  56. package/lib/hooks/_lib/input.mjs +52 -0
  57. package/lib/hooks/adaptive-lint.mjs +4 -0
  58. package/lib/hooks/agent-tracker.mjs +59 -15
  59. package/lib/hooks/audit-reads.mjs +83 -42
  60. package/lib/hooks/audit-trail.mjs +28 -18
  61. package/lib/hooks/bash-output-logger.mjs +8 -2
  62. package/lib/hooks/block-no-verify.mjs +4 -0
  63. package/lib/hooks/ci-status-check.mjs +4 -0
  64. package/lib/hooks/comment-lint.mjs +6 -4
  65. package/lib/hooks/config-protection.mjs +4 -0
  66. package/lib/hooks/context-watch.mjs +4 -0
  67. package/lib/hooks/context-window-recovery.mjs +4 -0
  68. package/lib/hooks/dep-audit.mjs +12 -5
  69. package/lib/hooks/doc-coupling-check.mjs +5 -1
  70. package/lib/hooks/edit-accumulator.mjs +25 -10
  71. package/lib/hooks/edit-error-recovery.mjs +4 -0
  72. package/lib/hooks/edit-guard.mjs +4 -0
  73. package/lib/hooks/guard-bash.mjs +4 -0
  74. package/lib/hooks/mcp-audit.mjs +4 -0
  75. package/lib/hooks/mcp-health-check.mjs +4 -0
  76. package/lib/hooks/model-fallback.mjs +7 -11
  77. package/lib/hooks/policy-engine.mjs +4 -0
  78. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  79. package/lib/hooks/post-merge-tracking.mjs +82 -0
  80. package/lib/hooks/pre-compact.mjs +4 -0
  81. package/lib/hooks/pre-push-gate.mjs +84 -231
  82. package/lib/hooks/proactive-activation.mjs +5 -2
  83. package/lib/hooks/readme-age-check.mjs +4 -0
  84. package/lib/hooks/registry-sync.mjs +35 -20
  85. package/lib/hooks/rule-verifier.mjs +3 -0
  86. package/lib/hooks/scan-secrets.mjs +4 -0
  87. package/lib/hooks/session-optimize.mjs +4 -0
  88. package/lib/hooks/session-reflect.mjs +4 -0
  89. package/lib/hooks/session-start.mjs +49 -2
  90. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  91. package/lib/hooks/stop-notify.mjs +13 -2
  92. package/lib/hooks/stop-typecheck.mjs +4 -0
  93. package/lib/hooks/test-watch.mjs +8 -4
  94. package/lib/ingest/chunker.mjs +94 -0
  95. package/lib/ingest/pipeline.mjs +53 -0
  96. package/lib/ingest/store.mjs +82 -0
  97. package/lib/init-unified.mjs +90 -23
  98. package/lib/intake/attribution.mjs +77 -0
  99. package/lib/intake/intake-config.mjs +3 -0
  100. package/lib/intake/manifest.mjs +107 -0
  101. package/lib/intake/poll-lock.mjs +136 -0
  102. package/lib/intake/prepare.mjs +42 -4
  103. package/lib/knowledge/search.mjs +12 -0
  104. package/lib/logging/rotate.mjs +394 -0
  105. package/lib/mcp/server.mjs +142 -1
  106. package/lib/mcp/tools/project.mjs +2 -2
  107. package/lib/mcp/tools/skills.mjs +6 -3
  108. package/lib/mcp/tools/telemetry.mjs +1 -1
  109. package/lib/mcp/tools/workflow.mjs +6 -2
  110. package/lib/observation-store.mjs +14 -5
  111. package/lib/ollama-manager.mjs +32 -28
  112. package/lib/opencode-config.mjs +10 -3
  113. package/lib/orchestration/routing-tables.mjs +176 -0
  114. package/lib/orchestration-policy.mjs +18 -106
  115. package/lib/parity.mjs +48 -7
  116. package/lib/profiles/lifecycle.mjs +19 -1
  117. package/lib/project-init-shared.mjs +11 -5
  118. package/lib/project-root.mjs +104 -0
  119. package/lib/prompt-composer.js +11 -1
  120. package/lib/roles/catalog.mjs +1 -1
  121. package/lib/roles/event-bus.mjs +29 -9
  122. package/lib/roles/router.mjs +8 -7
  123. package/lib/runtime/uv-bootstrap.mjs +129 -0
  124. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  125. package/lib/server/index.mjs +53 -10
  126. package/lib/server/static/index.html +1 -15
  127. package/lib/specialists/postconditions.mjs +1 -1
  128. package/lib/status.mjs +1 -1
  129. package/lib/storage/backup.mjs +2 -2
  130. package/lib/sync/skill-frontmatter.mjs +113 -21
  131. package/lib/telemetry/intent-verifications.mjs +16 -3
  132. package/lib/telemetry/skill-calls.mjs +12 -3
  133. package/lib/tracking-surfaces.mjs +377 -0
  134. package/lib/validators/skills.mjs +86 -54
  135. package/lib/worker/trace.mjs +19 -2
  136. package/package.json +10 -6
  137. package/personas/construct.md +4 -7
  138. package/platforms/claude/settings.template.json +29 -28
  139. package/rules/common/beads-hygiene.md +3 -9
  140. package/rules/common/code-review.md +3 -6
  141. package/rules/common/coding-style.md +3 -6
  142. package/rules/common/comments.md +3 -7
  143. package/rules/common/commit-approval.md +3 -6
  144. package/rules/common/cx-agent-routing.md +3 -7
  145. package/rules/common/cx-skill-routing.md +3 -3
  146. package/rules/common/doc-ownership.md +3 -8
  147. package/rules/common/efficiency.md +3 -8
  148. package/rules/common/framing.md +3 -8
  149. package/rules/common/git-workflow.md +3 -5
  150. package/rules/common/no-fabrication.md +4 -12
  151. package/rules/common/patterns.md +3 -5
  152. package/rules/common/release-gates.md +3 -8
  153. package/rules/common/research.md +3 -8
  154. package/rules/common/review-before-change.md +3 -9
  155. package/rules/common/security.md +3 -6
  156. package/rules/common/skill-composition.md +3 -7
  157. package/rules/common/testing.md +3 -6
  158. package/rules/golang/coding-style.md +1 -5
  159. package/rules/golang/hooks.md +1 -5
  160. package/rules/golang/patterns.md +1 -5
  161. package/rules/golang/security.md +1 -5
  162. package/rules/golang/testing.md +1 -5
  163. package/rules/python/coding-style.md +1 -5
  164. package/rules/python/hooks.md +1 -5
  165. package/rules/python/patterns.md +1 -5
  166. package/rules/python/security.md +1 -5
  167. package/rules/python/testing.md +1 -5
  168. package/rules/swift/coding-style.md +1 -5
  169. package/rules/swift/hooks.md +1 -5
  170. package/rules/swift/patterns.md +1 -5
  171. package/rules/swift/security.md +1 -5
  172. package/rules/swift/testing.md +1 -5
  173. package/rules/typescript/coding-style.md +1 -5
  174. package/rules/typescript/hooks.md +1 -5
  175. package/rules/typescript/patterns.md +1 -5
  176. package/rules/typescript/security.md +1 -5
  177. package/rules/typescript/testing.md +1 -5
  178. package/rules/web/coding-style.md +3 -5
  179. package/rules/web/design-quality.md +3 -5
  180. package/rules/web/hooks.md +3 -5
  181. package/rules/web/patterns.md +3 -5
  182. package/rules/web/performance.md +3 -5
  183. package/rules/web/security.md +3 -5
  184. package/rules/web/testing.md +3 -5
  185. package/scripts/sync-specialists.mjs +269 -75
  186. package/skills/ai/agent-dev.md +4 -5
  187. package/skills/ai/llm-security.md +4 -5
  188. package/skills/ai/ml-ops.md +4 -5
  189. package/skills/ai/orchestration-workflow.md +4 -5
  190. package/skills/ai/prompt-and-eval.md +4 -5
  191. package/skills/ai/prompt-optimizer.md +4 -6
  192. package/skills/ai/rag-system.md +4 -5
  193. package/skills/architecture/api-design.md +4 -5
  194. package/skills/architecture/caching.md +4 -5
  195. package/skills/architecture/cloud-native.md +4 -5
  196. package/skills/architecture/message-queue.md +4 -5
  197. package/skills/architecture/security-arch.md +4 -5
  198. package/skills/compliance/ai-disclosure.md +4 -5
  199. package/skills/compliance/data-privacy.md +4 -5
  200. package/skills/compliance/license-audit.md +4 -5
  201. package/skills/compliance/regulatory-review.md +4 -5
  202. package/skills/development/cpp.md +4 -5
  203. package/skills/development/go.md +4 -5
  204. package/skills/development/java.md +4 -5
  205. package/skills/development/kotlin.md +4 -5
  206. package/skills/development/mobile-crossplatform.md +4 -5
  207. package/skills/development/python.md +4 -5
  208. package/skills/development/rust.md +4 -5
  209. package/skills/development/shell.md +4 -5
  210. package/skills/development/swift.md +4 -5
  211. package/skills/development/typescript.md +4 -5
  212. package/skills/devops/ci-cd.md +4 -5
  213. package/skills/devops/containerization.md +4 -5
  214. package/skills/devops/cost-optimization.md +4 -5
  215. package/skills/devops/data-engineering.md +4 -5
  216. package/skills/devops/database.md +4 -5
  217. package/skills/devops/dependency-management.md +4 -5
  218. package/skills/devops/devsecops.md +4 -5
  219. package/skills/devops/git-workflow.md +4 -5
  220. package/skills/devops/incident-response.md +4 -5
  221. package/skills/devops/monorepo.md +4 -5
  222. package/skills/devops/observability.md +4 -5
  223. package/skills/devops/performance.md +4 -5
  224. package/skills/devops/testing.md +4 -5
  225. package/skills/docs/adr-workflow.md +4 -7
  226. package/skills/docs/backlog-proposal-workflow.md +4 -3
  227. package/skills/docs/customer-profile-workflow.md +4 -3
  228. package/skills/docs/document-ingest-workflow.md +4 -3
  229. package/skills/docs/evidence-ingest-workflow.md +4 -3
  230. package/skills/docs/init-docs.md +4 -5
  231. package/skills/docs/init-project.md +4 -5
  232. package/skills/docs/prd-workflow.md +4 -7
  233. package/skills/docs/prfaq-workflow.md +4 -3
  234. package/skills/docs/product-intelligence-review.md +4 -3
  235. package/skills/docs/product-intelligence-workflow.md +4 -6
  236. package/skills/docs/product-signal-workflow.md +4 -5
  237. package/skills/docs/research-workflow.md +4 -5
  238. package/skills/docs/runbook-workflow.md +4 -5
  239. package/skills/docs/strategy-workflow.md +4 -5
  240. package/skills/exploration/dependency-graph-reading.md +4 -7
  241. package/skills/exploration/repo-map.md +4 -5
  242. package/skills/exploration/tracer-bullet-method.md +4 -7
  243. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  244. package/skills/frameworks/django.md +4 -5
  245. package/skills/frameworks/nextjs.md +4 -5
  246. package/skills/frameworks/react.md +4 -5
  247. package/skills/frameworks/spring-boot.md +4 -5
  248. package/skills/frontend-design/accessibility.md +4 -5
  249. package/skills/frontend-design/component-patterns.md +4 -5
  250. package/skills/frontend-design/engineering.md +4 -5
  251. package/skills/frontend-design/state-management.md +4 -5
  252. package/skills/frontend-design/ui-aesthetics.md +4 -5
  253. package/skills/frontend-design/ux-principles.md +4 -5
  254. package/skills/operating/change-management.md +4 -8
  255. package/skills/operating/incident-response.md +4 -8
  256. package/skills/operating/oncall-rotation.md +4 -7
  257. package/skills/operating/orchestration-reference.md +4 -10
  258. package/skills/quality-gates/review-work.md +4 -5
  259. package/skills/quality-gates/verify-change.md +4 -5
  260. package/skills/quality-gates/verify-module.md +4 -5
  261. package/skills/quality-gates/verify-quality.md +4 -5
  262. package/skills/quality-gates/verify-security.md +4 -5
  263. package/skills/roles/architect.ai-systems.md +6 -9
  264. package/skills/roles/architect.data.md +6 -9
  265. package/skills/roles/architect.enterprise.md +6 -9
  266. package/skills/roles/architect.integration.md +6 -9
  267. package/skills/roles/architect.md +7 -14
  268. package/skills/roles/architect.platform.md +6 -9
  269. package/skills/roles/data-analyst.experiment.md +6 -9
  270. package/skills/roles/data-analyst.md +6 -9
  271. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  272. package/skills/roles/data-analyst.product.md +6 -9
  273. package/skills/roles/data-analyst.telemetry.md +7 -9
  274. package/skills/roles/data-engineer.pipeline.md +6 -9
  275. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  276. package/skills/roles/data-engineer.warehouse.md +6 -9
  277. package/skills/roles/debugger.md +6 -9
  278. package/skills/roles/designer.accessibility.md +6 -9
  279. package/skills/roles/designer.md +7 -9
  280. package/skills/roles/engineer.ai.md +6 -9
  281. package/skills/roles/engineer.data.md +6 -9
  282. package/skills/roles/engineer.md +9 -9
  283. package/skills/roles/engineer.platform.md +6 -9
  284. package/skills/roles/operator.docs.md +6 -9
  285. package/skills/roles/operator.md +9 -9
  286. package/skills/roles/operator.release.md +6 -9
  287. package/skills/roles/operator.sre.md +6 -9
  288. package/skills/roles/orchestrator.md +6 -9
  289. package/skills/roles/product-manager.ai-product.md +6 -9
  290. package/skills/roles/product-manager.business-strategy.md +6 -9
  291. package/skills/roles/product-manager.enterprise.md +6 -9
  292. package/skills/roles/product-manager.growth.md +7 -9
  293. package/skills/roles/product-manager.md +7 -9
  294. package/skills/roles/product-manager.platform.md +6 -9
  295. package/skills/roles/product-manager.product.md +6 -9
  296. package/skills/roles/qa.ai-eval.md +8 -9
  297. package/skills/roles/qa.api-contract.md +7 -9
  298. package/skills/roles/qa.data-pipeline.md +7 -9
  299. package/skills/roles/qa.md +7 -9
  300. package/skills/roles/qa.test-automation.md +6 -9
  301. package/skills/roles/qa.web-ui.md +7 -9
  302. package/skills/roles/researcher.explorer.md +6 -9
  303. package/skills/roles/researcher.md +8 -9
  304. package/skills/roles/researcher.ux.md +6 -9
  305. package/skills/roles/reviewer.devil-advocate.md +6 -9
  306. package/skills/roles/reviewer.evaluator.md +6 -9
  307. package/skills/roles/reviewer.md +9 -9
  308. package/skills/roles/reviewer.trace.md +6 -9
  309. package/skills/roles/security.ai.md +7 -9
  310. package/skills/roles/security.appsec.md +6 -9
  311. package/skills/roles/security.cloud.md +6 -9
  312. package/skills/roles/security.legal-compliance.md +6 -9
  313. package/skills/roles/security.md +7 -9
  314. package/skills/roles/security.privacy.md +7 -9
  315. package/skills/roles/security.supply-chain.md +7 -9
  316. package/skills/security/blue-team.md +4 -5
  317. package/skills/security/code-audit.md +4 -5
  318. package/skills/security/pentest.md +4 -5
  319. package/skills/security/red-team.md +4 -5
  320. package/skills/security/threat-intel.md +4 -5
  321. package/skills/security/vuln-research.md +4 -5
  322. package/skills/strategy/competitive-landscape.md +4 -8
  323. package/skills/strategy/market-research-methods.md +4 -8
  324. package/skills/strategy/narrative-arc.md +4 -8
  325. package/skills/strategy/pricing-positioning.md +4 -8
  326. package/skills/utility/clean-code.md +4 -5
  327. package/specialists/policy-inventory.json +14 -0
  328. package/specialists/registry.json +158 -13
  329. package/lib/hooks/env-check.mjs +0 -83
  330. package/lib/hooks/read-tracker.mjs +0 -61
  331. package/lib/policy/unified-gates.mjs +0 -96
  332. package/lib/server/static/assets/index-ab25c707.js +0 -70
  333. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
  334. package/lib/specialist-contracts-enforce.mjs +0 -158
  335. package/rules/common/agents.md +0 -28
  336. package/rules/common/development-workflow.md +0 -32
  337. package/rules/common/performance.md +0 -55
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/post-merge-tracking.mjs — close beads referenced by a merged PR.
4
+ *
5
+ * Runs as a PostToolUse hook on Bash. Watches for `gh pr merge <N>` (or
6
+ * `gh pr merge --auto`) invocations that exited successfully; parses the
7
+ * PR body via `gh pr view <N> --json body`; extracts every `Refs:` /
8
+ * `Closes:` / `Fixes:` line that names a `construct-XXX` bead; closes each
9
+ * still-open bead with reason `Merged via PR #N (<sha>)`. Idempotent on
10
+ * beads already closed.
11
+ *
12
+ * The PR-body parse is the source of truth — not the merge commit subject
13
+ * — because the bundle PR pattern stores the bead refs in the body's
14
+ * "Beads issue" section and the squash-merge subject often drops them.
15
+ *
16
+ * Best-effort. Exits 0 on any parse or shell failure so the surrounding
17
+ * tool call is never affected. Stderr lines are advisory; the actual
18
+ * outcome is the `bd close` state, which is what the next Stop hook's
19
+ * tracking refresh will read.
20
+ *
21
+ * @lifecycle PostToolUse
22
+ * @matcher Bash
23
+ * @p95ms 3000
24
+ * @maxBlockingScope none (PostToolUse, non-blocking)
25
+ * @exits 0 = pass
26
+ */
27
+
28
+ import { spawnSync } from 'node:child_process';
29
+ import { readHookInput } from './_lib/input.mjs';
30
+ import { logHookFailure } from './_lib/log.mjs';
31
+
32
+ const input = readHookInput();
33
+ const command = input?.tool_input?.command || input?.command || '';
34
+ const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
35
+
36
+ const ghPrMergeMatch = /\bgh\s+pr\s+merge\b(?:\s+(\d+))?/.exec(command);
37
+ if (!ghPrMergeMatch) process.exit(0);
38
+ if (exitCode !== undefined && exitCode !== 0) process.exit(0);
39
+
40
+ const cwd = input?.cwd || process.cwd();
41
+ const prNumber = await resolvePrNumber(ghPrMergeMatch[1], cwd);
42
+ if (!prNumber) process.exit(0);
43
+
44
+ try {
45
+ const { closeBeadsFromPrRefs } = await import('../tracking-surfaces.mjs');
46
+ const mergeCommitSha = readMergeCommitSha(cwd);
47
+ const result = await closeBeadsFromPrRefs({ prNumber, mergeCommitSha, cwd });
48
+ if (result?.closed?.length > 0) {
49
+ process.stderr.write(`[post-merge-tracking] closed beads: ${result.closed.join(', ')}\n`);
50
+ }
51
+ if (result?.errors?.length > 0) {
52
+ process.stderr.write(`[post-merge-tracking] failed to close: ${result.errors.map((e) => e.id).join(', ')}\n`);
53
+ }
54
+ } catch (err) {
55
+ logHookFailure({ hook: 'post-merge-tracking', err, phase: 'close' });
56
+ }
57
+
58
+ process.exit(0);
59
+
60
+ async function resolvePrNumber(captured, cwd) {
61
+ if (captured) return captured;
62
+ // Bare `gh pr merge --auto` / `gh pr merge -s` invocations omit the PR
63
+ // number. Resolve via the most recent merge commit subject, which `gh`
64
+ // standardly tails with `(#N)`.
65
+ try {
66
+ const lastLog = spawnSync('git', ['log', '-1', '--pretty=format:%s'], { cwd, encoding: 'utf8', timeout: 2000 });
67
+ if (lastLog.status !== 0) return null;
68
+ const m = /#(\d+)/.exec(lastLog.stdout);
69
+ return m ? m[1] : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function readMergeCommitSha(cwd) {
76
+ try {
77
+ const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 2000 });
78
+ return result.status === 0 ? result.stdout.trim() : '';
79
+ } catch {
80
+ return '';
81
+ }
82
+ }
@@ -11,6 +11,10 @@
11
11
  *
12
12
  * @p95ms 100
13
13
  * @maxBlockingScope PreCompact
14
+ *
15
+ * @lifecycle PreCompact
16
+ * @matcher *
17
+ * @exits 0 = pass
14
18
  */
15
19
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
16
20
  import { homedir } from 'os';
@@ -1,32 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * pre-push-gate.mjs — PreToolUse / Bash
3
+ * lib/hooks/pre-push-gate.mjs — PreToolUse / Bash
4
4
  *
5
+ * Local pre-push gate, shrunk to what only the developer's machine can know.
5
6
  * Intercepts three command shapes:
6
- * git push refuses claude/* branches, refuses on red remote CI,
7
- * runs project test/build/evals/docs in parallel
7
+ * git push refuses claude/* branches, blocks re-pushing the
8
+ * exact SHA CI just rejected. No test/build/lint
9
+ * CI is the source of truth.
8
10
  * gh pr create lints the --body / --body-file against the PR
9
- * template policy before the PR is opened
10
- * gh pr edit same body lint when --body / --body-file is changed
11
+ * template policy before the PR is opened (CI can't
12
+ * the body doesn't exist until the PR is created).
13
+ * gh pr edit same body lint when --body / --body-file is changed.
11
14
  *
12
- * Template-policy severity is conditional: a hard block (exit 2) when a
13
- * specialist sub-agent is active (CONSTRUCT_AGENT_ID set, or the cx-tracker
14
- * shows a fresh dispatch in the last 10 minutes); a warning otherwise so a
15
- * human driving the session can override editorial nits without ceremony.
16
- * CI still enforces the same rules on the resulting PR.
15
+ * Template-policy severity for PR body lint is conditional: a hard block
16
+ * (exit 2) when a specialist sub-agent is active; a warning otherwise so a
17
+ * human driving the session can override editorial nits. CI still enforces
18
+ * the same rules on the resulting PR.
17
19
  *
18
- * Silent and instant when there's nothing to run.
20
+ * Silent and instant when there's nothing to flag.
19
21
  *
20
- * Bypasses (env vars):
21
- * CONSTRUCT_SKIP_PREPUSH=1 skip the entire push gate
22
- * CONSTRUCT_ALLOW_CLAUDE_PUSH=1 allow pushing a claude/* branch
23
- * CONSTRUCT_SKIP_PR_LINT=1 skip the gh pr body lint
22
+ * No bypass mechanism. If a check fires wrong, the check is wrong — repair
23
+ * the policy or the matcher; do not add a skip env var.
24
24
  *
25
- * @p95ms 30000
25
+ * @lifecycle PreToolUse
26
+ * @matcher Bash
27
+ * @p95ms 5000
26
28
  * @maxBlockingScope PreToolUse
29
+ * @exits 0 = pass | 2 = block tool call
27
30
  */
28
31
  import { readFileSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
29
- import { execSync, spawn, spawnSync } from 'child_process';
32
+ import { execSync, spawnSync } from 'child_process';
30
33
  import { dirname, join, resolve } from 'path';
31
34
  import { tmpdir } from 'os';
32
35
  import { logHookFailure } from './_lib/log.mjs';
@@ -48,7 +51,18 @@ if (!isGitPush && !isGhPrCreate && !isGhPrEdit) { echo(); }
48
51
 
49
52
  const cwd = input?.cwd || process.cwd();
50
53
 
51
- if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1') {
54
+ const PROTECTED_BRANCHES = new Set(['main', 'staging', 'master']);
55
+
56
+ function currentBranch() {
57
+ try {
58
+ return execSync('git branch --show-current', { cwd, timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] })
59
+ .toString().trim();
60
+ } catch { return ''; }
61
+ }
62
+
63
+ // gh pr create / gh pr edit body lint
64
+
65
+ if (isGhPrCreate || isGhPrEdit) {
52
66
  const { body, bodyFile } = extractGhPrBody(command);
53
67
  const hasArgs = body != null || bodyFile != null;
54
68
 
@@ -85,7 +99,6 @@ if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1')
85
99
  ? '[pre-push-gate] PR body fails template policy (specialist agent active — blocking):'
86
100
  : '[pre-push-gate] PR body fails template policy (no specialist agent active — warning only):';
87
101
  process.stderr.write(`${banner}\n${out}\n`);
88
- process.stderr.write(`Override: CONSTRUCT_SKIP_PR_LINT=1 <command>\n`);
89
102
  emitRoleEvent({
90
103
  type: agentActive ? 'push_gate.fail' : 'push_gate.warn',
91
104
  summary: `PR body fails template policy (${severity})`,
@@ -103,36 +116,12 @@ if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1')
103
116
  echo();
104
117
  }
105
118
 
106
- if (process.env.CONSTRUCT_SKIP_PREPUSH === '1') { echo(); }
107
-
108
- function extractGhPrBody(cmd) {
109
- let m = cmd.match(/--body-file[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
110
- if (m) return { bodyFile: m[2] };
111
- m = cmd.match(/(?:^|\s)-F[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
112
- if (m) return { bodyFile: m[2] };
113
- m = cmd.match(/--body[\s=]+"((?:\\.|[^"\\])*)"/s);
114
- if (m) return { body: m[1].replace(/\\(["\\])/g, '$1') };
115
- m = cmd.match(/--body[\s=]+'([^']*)'/s);
116
- if (m) return { body: m[1] };
117
- return {};
118
- }
119
-
120
- function findRepoFile(startDir, relPath) {
121
- let dir = startDir;
122
- for (let i = 0; i < 10; i++) {
123
- const candidate = join(dir, relPath);
124
- if (existsSync(candidate)) return candidate;
125
- const parent = dirname(dir);
126
- if (parent === dir) break;
127
- dir = parent;
128
- }
129
- return null;
130
- }
119
+ // git push: refuse claude/* unconditionally.
131
120
 
132
- if (/\bclaude\//.test(command) && process.env.CONSTRUCT_ALLOW_CLAUDE_PUSH !== '1') {
121
+ if (/\bclaude\//.test(command)) {
133
122
  process.stderr.write(
134
123
  `[pre-push-gate] Refusing to push a claude/* branch to remote (user policy).\n` +
135
- ` Use a non-agent branch name, or override with CONSTRUCT_ALLOW_CLAUDE_PUSH=1 git push ...\n`,
124
+ ` Use a non-agent branch name.\n`,
136
125
  );
137
126
  emitRoleEvent({
138
127
  type: 'push_gate.fail',
@@ -143,208 +132,72 @@ if (/\bclaude\//.test(command) && process.env.CONSTRUCT_ALLOW_CLAUDE_PUSH !== '1
143
132
  process.exit(2);
144
133
  }
145
134
 
135
+ // Prior remote CI red-check: block only when HEAD == the SHA that failed
136
+ // (the developer is about to re-push the exact broken commit). If HEAD has
137
+ // moved past the failed SHA, those new commits may well BE the fix; let
138
+ // them push so CI can re-evaluate.
139
+
146
140
  try {
147
- const branch = execSync('git branch --show-current', { cwd, timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] })
148
- .toString().trim();
149
- if (branch && branch !== 'main' && branch !== 'dev' && branch !== 'master') {
141
+ const branch = currentBranch();
142
+ if (branch && !PROTECTED_BRANCHES.has(branch) && branch !== 'dev') {
150
143
  const json = execSync(
151
- `gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
144
+ `gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url,headSha`,
152
145
  { cwd, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] },
153
146
  ).toString().trim();
154
147
  if (json) {
155
148
  const [run] = JSON.parse(json);
156
149
  if (run?.conclusion === 'failure') {
150
+ const headSha = execSync('git rev-parse HEAD', { cwd, timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] })
151
+ .toString().trim();
152
+ const runSha = run.headSha || '';
153
+ const runUrl = run.url || `gh run view ${run.databaseId}`;
154
+ if (runSha && headSha === runSha) {
155
+ process.stderr.write(
156
+ `[pre-push-gate] HEAD (${headSha.slice(0, 7)}) is the commit that failed CI on '${branch}'.\n` +
157
+ ` Re-pushing the same SHA will fail again. Add a fix commit before pushing.\n` +
158
+ ` Failed run: ${runUrl}\n`,
159
+ );
160
+ emitRoleEvent({
161
+ type: 'push_gate.fail',
162
+ summary: 'remote CI red on current HEAD — push blocked',
163
+ hookInput: input,
164
+ context: { branch, runId: run.databaseId, runUrl: run.url, headSha, runSha },
165
+ });
166
+ process.exit(2);
167
+ }
157
168
  process.stderr.write(
158
- `[pre-push-gate] Last CI run on '${branch}' FAILED fix it before pushing more.\n` +
159
- ` Run: ${run.url || `gh run view ${run.databaseId}`}\n` +
160
- ` Override: CONSTRUCT_SKIP_PREPUSH=1 git push ...\n`,
169
+ `[pre-push-gate] Note: last CI run on '${branch}' (${runSha.slice(0, 7) || 'unknown SHA'}) failed; HEAD (${headSha.slice(0, 7)}) is past it. Allowing push — CI will re-evaluate.\n` +
170
+ ` Prior failure: ${runUrl}\n`,
161
171
  );
162
- emitRoleEvent({
163
- type: 'push_gate.fail',
164
- summary: 'remote CI red — push blocked',
165
- hookInput: input,
166
- context: { branch, runId: run.databaseId, runUrl: run.url },
167
- });
168
- process.exit(2);
169
172
  }
170
173
  }
171
174
  }
172
175
  } catch {}
173
176
 
174
- function findUp(filename, from) {
175
- let dir = from;
176
- for (let i = 0; i < 8; i++) {
177
- const candidate = join(dir, filename);
178
- if (existsSync(candidate)) return { path: candidate, dir };
179
- const parent = dirname(dir);
180
- if (parent === dir) break;
181
- dir = parent;
182
- }
183
- return null;
184
- }
185
-
186
- // ── Node/npm project ──────────────────────────────────────────────────────────
187
- function runNpmGate(projectDir) {
188
- let pkg;
189
- try { pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')); } catch { return null; }
190
-
191
- const scripts = pkg.scripts || {};
192
- const hasTest = !!(scripts.test && !scripts.test.includes('no test specified'));
193
- const hasBuild = !!(scripts.build);
194
- const hasCi = !!(scripts['test:ci'] || scripts['test:run']);
195
-
196
- const testScript = scripts['test:ci'] ? 'test:ci' : scripts['test:run'] ? 'test:run' : 'test';
197
- const runner = existsSync(join(projectDir, 'pnpm-lock.yaml')) ? 'pnpm'
198
- : existsSync(join(projectDir, 'yarn.lock')) ? 'yarn'
199
- : 'npm';
200
-
201
- const jobs = [];
202
- if (hasTest || hasCi) jobs.push({ label: 'tests', cmd: runner, args: ['run', testScript], timeout: 90_000 });
203
- if (hasBuild) jobs.push({ label: 'build', cmd: runner, args: ['run', 'build'], timeout: 120_000 });
204
- // Use the project's package manager for audit so the lockfile format matches.
205
- const auditArgs = runner === 'pnpm'
206
- ? ['audit', '--prod', '--audit-level=high']
207
- : runner === 'yarn'
208
- ? ['npm-audit', '--groups', 'dependencies', '--level', 'high']
209
- : ['audit', '--omit=dev', '--audit-level=high'];
210
- jobs.push({ label: 'audit', cmd: runner, args: auditArgs, timeout: 30_000 });
211
-
212
- if (existsSync(join(projectDir, 'bin/construct'))) {
213
- jobs.push({ label: 'evals', cmd: 'node', args: ['bin/construct', 'evals', 'retrieval'], timeout: 60_000 });
214
- jobs.push({ label: 'docs', cmd: 'node', args: ['bin/construct', 'docs:verify'], timeout: 15_000 });
215
- jobs.push({ label: 'docs drift', cmd: 'node', args: ['bin/construct', 'docs:update', '--check'], timeout: 15_000 });
216
- jobs.push({ label: 'dashboard drift', cmd: 'node', args: ['bin/construct', 'dashboard:sync', '--check'], timeout: 15_000 });
217
- jobs.push({ label: 'comment policy', cmd: 'node', args: ['bin/construct', 'lint:comments'], timeout: 30_000 });
218
- jobs.push({ label: 'agents registry', cmd: 'node', args: ['bin/construct', 'lint:agents'], timeout: 15_000 });
219
- jobs.push({ label: 'contracts schema', cmd: 'node', args: ['bin/construct', 'lint:contracts'], timeout: 15_000 });
220
- }
221
-
222
- if (existsSync(join(projectDir, 'scripts/lint-prose.mjs'))) {
223
- jobs.push({ label: 'prose', cmd: 'node', args: ['scripts/lint-prose.mjs'], timeout: 15_000 });
224
- }
225
- if (existsSync(join(projectDir, 'scripts/lint-profiles.mjs'))) {
226
- jobs.push({ label: 'profiles', cmd: 'node', args: ['scripts/lint-profiles.mjs', '--quiet'], timeout: 15_000 });
227
- }
228
-
229
- return { projectDir, jobs };
230
- }
231
-
232
- // ── Rust/Cargo project ────────────────────────────────────────────────────────
233
- function runCargoGate(projectDir) {
234
- const jobs = [
235
- { label: 'tests', cmd: 'cargo', args: ['test', '--quiet'], timeout: 120_000 },
236
- { label: 'build', cmd: 'cargo', args: ['build', '--quiet'], timeout: 120_000 },
237
- ];
238
- return { projectDir, jobs };
239
- }
240
-
241
- // ── Python project ────────────────────────────────────────────────────────────
242
- function runPythonGate(projectDir) {
243
- const hasPytest = (() => { try { execSync('pytest --version', { stdio: 'pipe', timeout: 3000 }); return true; } catch { return false; } })();
244
- if (!hasPytest) return null;
245
- const jobs = [{ label: 'tests', cmd: 'pytest', args: ['--tb=short', '-q'], timeout: 90_000 }];
246
- return { projectDir, jobs };
247
- }
248
-
249
- // Detect project type
250
- let gate = null;
251
- const npm = findUp('package.json', cwd);
252
- const cargo = findUp('Cargo.toml', cwd);
253
- const pyproj = findUp('pyproject.toml', cwd) || findUp('setup.py', cwd);
254
-
255
- if (npm) gate = runNpmGate(npm.dir);
256
- else if (cargo) gate = runCargoGate(cargo.dir);
257
- else if (pyproj) gate = runPythonGate(pyproj.dir);
177
+ echo();
258
178
 
259
- // Nothing to check
260
- if (!gate || gate.jobs.length === 0) { echo(); }
179
+ // helpers
261
180
 
262
- // Jobs run concurrently. Wall-clock = max(test, build) instead of sum, so the
263
- // gate fits inside the harness's 180s timeout even when both jobs near their
264
- // individual limits.
265
- function runJob(job) {
266
- return new Promise((resolve) => {
267
- let stdoutBuf = '';
268
- let stderrBuf = '';
269
- const child = spawn(job.cmd, job.args, {
270
- cwd: gate.projectDir,
271
- stdio: ['ignore', 'pipe', 'pipe'],
272
- env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
273
- });
274
-
275
- const timer = setTimeout(() => {
276
- try { child.kill('SIGKILL'); } catch { /* already gone */ }
277
- }, job.timeout);
278
-
279
- child.stdout?.on('data', (chunk) => { stdoutBuf += chunk.toString(); });
280
- child.stderr?.on('data', (chunk) => { stderrBuf += chunk.toString(); });
281
- child.on('error', (err) => {
282
- clearTimeout(timer);
283
- resolve({ label: job.label, status: -1, stdout: stdoutBuf, stderr: stderrBuf || err.message, error: err });
284
- });
285
- child.on('close', (code) => {
286
- clearTimeout(timer);
287
- resolve({ label: job.label, status: code ?? -1, stdout: stdoutBuf, stderr: stderrBuf });
288
- });
289
- });
181
+ function extractGhPrBody(cmd) {
182
+ let m = cmd.match(/--body-file[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
183
+ if (m) return { bodyFile: m[2] };
184
+ m = cmd.match(/(?:^|\s)-F[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
185
+ if (m) return { bodyFile: m[2] };
186
+ m = cmd.match(/--body[\s=]+"((?:\\.|[^"\\])*)"/s);
187
+ if (m) return { body: m[1].replace(/\\(["\\])/g, '$1') };
188
+ m = cmd.match(/--body[\s=]+'([^']*)'/s);
189
+ if (m) return { body: m[1] };
190
+ return {};
290
191
  }
291
192
 
292
- const results = await Promise.all(gate.jobs.map(runJob));
293
- const failures = [];
294
-
295
- // Auto-fix docs drift: if docs:update --check fails, run docs:update to regenerate
296
- const docsDriftResult = results.find((r) => r.label === 'docs drift');
297
- if (docsDriftResult && docsDriftResult.status !== 0) {
298
- process.stderr.write('[pre-push-gate] Docs drift detected — auto-running docs:update...\n');
299
- try {
300
- const fix = spawnSync('node', ['bin/construct', 'docs:update'], {
301
- cwd: gate.projectDir,
302
- stdio: ['ignore', 'pipe', 'pipe'],
303
- timeout: 30_000,
304
- });
305
- if (fix.status === 0) {
306
- process.stderr.write('[pre-push-gate] docs:update succeeded — re-checking...\n');
307
- const recheck = spawnSync('node', ['bin/construct', 'docs:update', '--check'], {
308
- cwd: gate.projectDir,
309
- stdio: ['ignore', 'pipe', 'pipe'],
310
- timeout: 15_000,
311
- });
312
- if (recheck.status === 0) {
313
- process.stderr.write('[pre-push-gate] Docs are now clean after auto-fix.\n');
314
- results.splice(results.indexOf(docsDriftResult), 1); // remove from failure candidates
315
- } else {
316
- process.stderr.write(`[pre-push-gate] Auto-fix attempted but docs still drift — manual intervention needed.\n`);
317
- docsDriftResult.stdout = (docsDriftResult.stdout || '') + '\n[auto-fix attempted but still dirty]';
318
- }
319
- } else {
320
- process.stderr.write(`[pre-push-gate] docs:update auto-fix failed (exit ${fix.status}): ${fix.stderr.slice(0, 300)}\n`);
321
- }
322
- } catch (e) {
323
- process.stderr.write(`[pre-push-gate] docs:update auto-fix threw: ${e.message}\n`);
193
+ function findRepoFile(startDir, relPath) {
194
+ let dir = startDir;
195
+ for (let i = 0; i < 10; i++) {
196
+ const candidate = join(dir, relPath);
197
+ if (existsSync(candidate)) return candidate;
198
+ const parent = dirname(dir);
199
+ if (parent === dir) break;
200
+ dir = parent;
324
201
  }
202
+ return null;
325
203
  }
326
-
327
- for (const result of results) {
328
- if (result.status === 0) continue;
329
- const detail = (result.stderr || result.stdout || '').trim();
330
- failures.push({ label: result.label, detail: detail || `exited ${result.status}` });
331
- }
332
-
333
- if (failures.length === 0) { echo(); }
334
-
335
- const summary = failures
336
- .map((f) => `${f.label} failed — ${f.detail}`)
337
- .join('\n');
338
-
339
- process.stderr.write(
340
- `[pre-push-gate] Push blocked — fix these before pushing:\n${summary}\n\nRun the failing checks locally, then push again.\n`
341
- );
342
-
343
- emitRoleEvent({
344
- type: 'push_gate.fail',
345
- summary,
346
- hookInput: input,
347
- context: { failures },
348
- });
349
-
350
- process.exit(2);
@@ -20,9 +20,12 @@
20
20
  *
21
21
  * @p95ms 5
22
22
  * @maxBlockingScope none
23
+ *
24
+ * @unwired (not referenced from platforms/claude/settings.template.json)
25
+ * @exits 0 = pass
23
26
  */
24
27
 
25
- import { EVENT_OWNERSHIP } from '../orchestration-policy.mjs';
28
+ import { ownerForEvent } from '../orchestration/routing-tables.mjs';
26
29
  import { loadProjectConfig } from '../config/project-config.mjs';
27
30
 
28
31
  // Rate limiting: max activations per specialist per hour
@@ -116,7 +119,7 @@ export function getActivationStats(specialist) {
116
119
  * Route an event to the owning specialist.
117
120
  */
118
121
  export function routeEvent(eventType, eventData = {}) {
119
- const owner = EVENT_OWNERSHIP[eventType];
122
+ const owner = ownerForEvent(eventType);
120
123
 
121
124
  if (!owner) {
122
125
  return {
@@ -9,6 +9,10 @@
9
9
  *
10
10
  * @p95ms 1500
11
11
  * @maxBlockingScope none (async, non-blocking)
12
+ *
13
+ * @lifecycle Stop
14
+ * @matcher *
15
+ * @exits 0 = pass
12
16
  */
13
17
 
14
18
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -1,30 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/hooks/registry-sync.mjs — Registry sync hook — reminds to run construct sync after registry changes.
3
+ * lib/hooks/registry-sync.mjs — Registry-change reminder hook.
4
4
  *
5
- * Runs as PostToolUse after edits to specialists/registry.json. Checks if the file was modified and emits a reminder to run construct sync to regenerate platform adapters.
5
+ * PostToolUse on Write|Edit. Fires only when the edited file is
6
+ * specialists/registry.json (the construct repo) or an installed
7
+ * agents/registry.json mirror. Emits a one-line reminder on stderr telling
8
+ * the developer to run `construct sync` when they're ready. Does NOT
9
+ * auto-execute sync — auto-execution during tests (which legitimately
10
+ * mutate registry.json) races test cleanup and regenerates platform state
11
+ * mid-suite. The dev decides when to sync, the same way ordinary code
12
+ * changes don't auto-build.
6
13
  *
7
- * @p95ms 12000
8
- * @maxBlockingScope none (PostToolUse, non-blocking)
14
+ * The reminder is suppressed automatically when CI=true or NODE_ENV=test.
15
+ * Stderr is not used as an interactivity signal: Claude Code captures hook
16
+ * stderr rather than passing it through to a TTY, so a TTY check would
17
+ * silently suppress the reminder in real usage. No skip env var — broaden
18
+ * the detection here if the reminder fires in a context where it shouldn't.
19
+ *
20
+ * @lifecycle PostToolUse
21
+ * @matcher Write|Edit
22
+ * @p95ms 50
23
+ * @maxBlockingScope none
24
+ * @exits 0 = pass
9
25
  */
10
- import { execSync } from 'child_process';
11
26
  import path from 'node:path';
12
- import { logHookFailure } from './_lib/log.mjs';
13
- import { constructDir } from '../paths.mjs';
27
+ import { readHookInput } from './_lib/input.mjs';
28
+
29
+ const input = readHookInput();
30
+ const filePath = input?.tool_input?.file_path || process.env.TOOL_INPUT_FILE_PATH || '';
31
+ const matchesRegistry =
32
+ filePath.endsWith(path.join('agents', 'registry.json')) ||
33
+ filePath.endsWith('/specialists/registry.json');
34
+ if (!matchesRegistry) process.exit(0);
14
35
 
15
- const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
16
- if (!filePath.endsWith(path.join('agents', 'registry.json')) && !filePath.endsWith('/specialists/registry.json')) process.exit(0);
36
+ const isNonInteractive =
37
+ process.env.CI === 'true' ||
38
+ process.env.NODE_ENV === 'test';
17
39
 
18
- const toolkitDir = constructDir();
19
- try {
20
- execSync('node scripts/sync-specialists.mjs', {
21
- cwd: toolkitDir,
22
- stdio: 'pipe',
23
- env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir },
24
- timeout: 12_000,
25
- });
26
- } catch (err) {
27
- logHookFailure({ hook: 'registry-sync', err, phase: 'execute', input: { toolkitDir } });
28
- process.stderr.write(`[registry-sync] Sync failed: ${err.message}\n`);
40
+ if (!isNonInteractive) {
41
+ process.stderr.write(
42
+ `[registry-sync] registry.json changed — run \`construct sync\` to regenerate platform adapters when ready.\n`,
43
+ );
29
44
  }
30
45
  process.exit(0);
@@ -21,6 +21,9 @@
21
21
  *
22
22
  * @p95ms 200
23
23
  * @maxBlockingScope Stop
24
+ *
25
+ * @unwired (not referenced from platforms/claude/settings.template.json)
26
+ * @exits 0 = pass | 2 = block tool call
24
27
  */
25
28
 
26
29
  import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * @p95ms 30
8
8
  * @maxBlockingScope PostToolUse
9
+ *
10
+ * @lifecycle PostToolUse
11
+ * @matcher Write|Edit
12
+ * @exits 0 = pass | 2 = block tool call
9
13
  */
10
14
  import { readFileSync } from 'fs';
11
15
  import { extname } from 'path';
@@ -7,6 +7,10 @@
7
7
  *
8
8
  * @p95ms 300
9
9
  * @maxBlockingScope Stop (non-blocking, async)
10
+ *
11
+ * @lifecycle Stop
12
+ * @matcher *
13
+ * @exits 0 = pass
10
14
  */
11
15
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
12
16
  import { join, dirname } from 'path';
@@ -14,6 +14,10 @@
14
14
  *
15
15
  * @p95ms 300
16
16
  * @maxBlockingScope Stop
17
+ *
18
+ * @lifecycle Stop
19
+ * @matcher *
20
+ * @exits 0 = pass
17
21
  */
18
22
  import { readFileSync, existsSync } from 'node:fs';
19
23
  import { join } from 'node:path';