@geraldmaron/construct 1.0.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 (352) hide show
  1. package/.env.example +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +344 -0
  4. package/agents/prompts/cx-accessibility.md +29 -0
  5. package/agents/prompts/cx-ai-engineer.md +31 -0
  6. package/agents/prompts/cx-architect.md +40 -0
  7. package/agents/prompts/cx-business-strategist.md +25 -0
  8. package/agents/prompts/cx-data-analyst.md +30 -0
  9. package/agents/prompts/cx-data-engineer.md +32 -0
  10. package/agents/prompts/cx-debugger.md +27 -0
  11. package/agents/prompts/cx-designer.md +33 -0
  12. package/agents/prompts/cx-devil-advocate.md +36 -0
  13. package/agents/prompts/cx-docs-keeper.md +50 -0
  14. package/agents/prompts/cx-engineer.md +36 -0
  15. package/agents/prompts/cx-evaluator.md +26 -0
  16. package/agents/prompts/cx-explorer.md +38 -0
  17. package/agents/prompts/cx-legal-compliance.md +25 -0
  18. package/agents/prompts/cx-operations.md +28 -0
  19. package/agents/prompts/cx-orchestrator.md +30 -0
  20. package/agents/prompts/cx-platform-engineer.md +25 -0
  21. package/agents/prompts/cx-product-manager.md +30 -0
  22. package/agents/prompts/cx-qa.md +42 -0
  23. package/agents/prompts/cx-rd-lead.md +25 -0
  24. package/agents/prompts/cx-release-manager.md +31 -0
  25. package/agents/prompts/cx-researcher.md +31 -0
  26. package/agents/prompts/cx-reviewer.md +36 -0
  27. package/agents/prompts/cx-security.md +37 -0
  28. package/agents/prompts/cx-sre.md +26 -0
  29. package/agents/prompts/cx-test-automation.md +35 -0
  30. package/agents/prompts/cx-trace-reviewer.md +76 -0
  31. package/agents/prompts/cx-ux-researcher.md +30 -0
  32. package/agents/registry.json +879 -0
  33. package/agents/teams.json +94 -0
  34. package/bin/construct +613 -0
  35. package/commands/build/feature.md +21 -0
  36. package/commands/build/fix.md +20 -0
  37. package/commands/design/access.md +19 -0
  38. package/commands/design/flow.md +16 -0
  39. package/commands/design/ui.md +18 -0
  40. package/commands/measure/experiment.md +18 -0
  41. package/commands/measure/metrics.md +18 -0
  42. package/commands/measure/results.md +18 -0
  43. package/commands/plan/api.md +19 -0
  44. package/commands/plan/challenge.md +18 -0
  45. package/commands/plan/decide.md +20 -0
  46. package/commands/plan/feature.md +49 -0
  47. package/commands/plan/requirements.md +24 -0
  48. package/commands/remember/context.md +26 -0
  49. package/commands/remember/handoff.md +19 -0
  50. package/commands/remember/runbook.md +20 -0
  51. package/commands/review/code.md +21 -0
  52. package/commands/review/quality.md +19 -0
  53. package/commands/review/security.md +16 -0
  54. package/commands/ship/ready.md +19 -0
  55. package/commands/ship/release.md +19 -0
  56. package/commands/ship/status.md +18 -0
  57. package/commands/understand/docs.md +22 -0
  58. package/commands/understand/research.md +22 -0
  59. package/commands/understand/this.md +23 -0
  60. package/commands/understand/why.md +18 -0
  61. package/commands/work/clean.md +32 -0
  62. package/commands/work/drive.md +69 -0
  63. package/commands/work/optimize-prompts.md +46 -0
  64. package/commands/work/parallel-review.md +40 -0
  65. package/db/migrations/001_init.sql +38 -0
  66. package/langfuse/docker-compose.yml +82 -0
  67. package/lib/audit-skills.mjs +118 -0
  68. package/lib/auto-docs.mjs +293 -0
  69. package/lib/cli-commands.mjs +409 -0
  70. package/lib/codex-config.mjs +114 -0
  71. package/lib/comment-lint.mjs +191 -0
  72. package/lib/completions.mjs +170 -0
  73. package/lib/context-state.mjs +99 -0
  74. package/lib/cost.mjs +213 -0
  75. package/lib/distill.mjs +403 -0
  76. package/lib/efficiency.mjs +139 -0
  77. package/lib/env-config.mjs +53 -0
  78. package/lib/eval-harness.mjs +59 -0
  79. package/lib/features.mjs +209 -0
  80. package/lib/headhunt.mjs +597 -0
  81. package/lib/hooks/adaptive-lint.mjs +112 -0
  82. package/lib/hooks/agent-tracker.mjs +50 -0
  83. package/lib/hooks/bootstrap-guard.mjs +90 -0
  84. package/lib/hooks/comment-lint.mjs +26 -0
  85. package/lib/hooks/config-protection.mjs +52 -0
  86. package/lib/hooks/console-warn.mjs +43 -0
  87. package/lib/hooks/context-window-recovery.mjs +90 -0
  88. package/lib/hooks/continuation-enforcer.mjs +72 -0
  89. package/lib/hooks/dep-audit.mjs +155 -0
  90. package/lib/hooks/drive-guard.mjs +89 -0
  91. package/lib/hooks/edit-accumulator.mjs +36 -0
  92. package/lib/hooks/edit-error-recovery.mjs +46 -0
  93. package/lib/hooks/edit-guard.mjs +109 -0
  94. package/lib/hooks/env-check.mjs +80 -0
  95. package/lib/hooks/guard-bash.mjs +83 -0
  96. package/lib/hooks/mcp-audit.mjs +57 -0
  97. package/lib/hooks/mcp-health-check.mjs +51 -0
  98. package/lib/hooks/mcp-task-scope.mjs +47 -0
  99. package/lib/hooks/model-fallback.mjs +105 -0
  100. package/lib/hooks/pre-compact.mjs +212 -0
  101. package/lib/hooks/pre-push-gate.mjs +129 -0
  102. package/lib/hooks/read-tracker.mjs +129 -0
  103. package/lib/hooks/registry-sync.mjs +23 -0
  104. package/lib/hooks/scan-secrets.mjs +76 -0
  105. package/lib/hooks/session-start.mjs +95 -0
  106. package/lib/hooks/stop-notify.mjs +191 -0
  107. package/lib/hooks/stop-typecheck.mjs +83 -0
  108. package/lib/hooks/task-completed-guard.mjs +43 -0
  109. package/lib/hooks/teammate-idle-guard.mjs +54 -0
  110. package/lib/hooks/workflow-guard.mjs +62 -0
  111. package/lib/host-capabilities.mjs +123 -0
  112. package/lib/init-docs.mjs +382 -0
  113. package/lib/mcp/server.mjs +1123 -0
  114. package/lib/mcp-catalog.json +243 -0
  115. package/lib/mcp-manager.mjs +433 -0
  116. package/lib/mcp-platform-config.mjs +104 -0
  117. package/lib/model-router.mjs +810 -0
  118. package/lib/opencode-config.mjs +44 -0
  119. package/lib/opencode-runtime-plugin.mjs +909 -0
  120. package/lib/opencode-telemetry.mjs +433 -0
  121. package/lib/orchestration-policy.mjs +258 -0
  122. package/lib/prompt-composer.mjs +196 -0
  123. package/lib/prompt-metadata.mjs +68 -0
  124. package/lib/review.mjs +429 -0
  125. package/lib/role-preload.mjs +52 -0
  126. package/lib/schemas/decision.json +50 -0
  127. package/lib/schemas/implementation.json +51 -0
  128. package/lib/schemas/review-report.json +32 -0
  129. package/lib/schemas/test-report.json +43 -0
  130. package/lib/server/index.mjs +334 -0
  131. package/lib/server/static/app.js +841 -0
  132. package/lib/server/static/index.html +820 -0
  133. package/lib/service-manager.mjs +225 -0
  134. package/lib/setup.mjs +319 -0
  135. package/lib/status.mjs +707 -0
  136. package/lib/storage/backend.mjs +44 -0
  137. package/lib/storage/embeddings.mjs +70 -0
  138. package/lib/storage/hybrid-query.mjs +184 -0
  139. package/lib/storage/sql-store.mjs +55 -0
  140. package/lib/storage/state-source.mjs +101 -0
  141. package/lib/storage/sync.mjs +164 -0
  142. package/lib/storage/vector-store.mjs +59 -0
  143. package/lib/telemetry/backends/langfuse.mjs +58 -0
  144. package/lib/telemetry/backends/noop.mjs +16 -0
  145. package/lib/telemetry/langfuse-ingest.mjs +108 -0
  146. package/lib/telemetry/langfuse-model-sync.mjs +270 -0
  147. package/lib/telemetry/team-rollup.mjs +143 -0
  148. package/lib/toolkit-env.mjs +30 -0
  149. package/lib/validator.mjs +181 -0
  150. package/lib/workflow-state.mjs +794 -0
  151. package/package.json +53 -0
  152. package/personas/construct.md +94 -0
  153. package/platforms/claude/CLAUDE.md +30 -0
  154. package/platforms/claude/settings.template.json +472 -0
  155. package/platforms/opencode/config.template.json +65 -0
  156. package/platforms/opencode/plugins/construct-fallback.js +5 -0
  157. package/platforms/opencode/sync-config.mjs +69 -0
  158. package/rules/common/agents.md +55 -0
  159. package/rules/common/code-review.md +129 -0
  160. package/rules/common/coding-style.md +95 -0
  161. package/rules/common/comments.md +139 -0
  162. package/rules/common/cx-agent-routing.md +61 -0
  163. package/rules/common/cx-skill-routing.md +11 -0
  164. package/rules/common/development-workflow.md +49 -0
  165. package/rules/common/git-workflow.md +29 -0
  166. package/rules/common/hooks.md +35 -0
  167. package/rules/common/patterns.md +36 -0
  168. package/rules/common/performance.md +71 -0
  169. package/rules/common/security.md +34 -0
  170. package/rules/common/testing.md +62 -0
  171. package/rules/golang/coding-style.md +37 -0
  172. package/rules/golang/hooks.md +22 -0
  173. package/rules/golang/patterns.md +50 -0
  174. package/rules/golang/security.md +39 -0
  175. package/rules/golang/testing.md +36 -0
  176. package/rules/python/coding-style.md +47 -0
  177. package/rules/python/hooks.md +24 -0
  178. package/rules/python/patterns.md +44 -0
  179. package/rules/python/security.md +35 -0
  180. package/rules/python/testing.md +43 -0
  181. package/rules/swift/coding-style.md +52 -0
  182. package/rules/swift/hooks.md +25 -0
  183. package/rules/swift/patterns.md +71 -0
  184. package/rules/swift/security.md +38 -0
  185. package/rules/swift/testing.md +50 -0
  186. package/rules/typescript/coding-style.md +204 -0
  187. package/rules/typescript/hooks.md +27 -0
  188. package/rules/typescript/patterns.md +57 -0
  189. package/rules/typescript/security.md +33 -0
  190. package/rules/typescript/testing.md +23 -0
  191. package/rules/web/coding-style.md +101 -0
  192. package/rules/web/design-quality.md +68 -0
  193. package/rules/web/hooks.md +125 -0
  194. package/rules/web/patterns.md +84 -0
  195. package/rules/web/performance.md +69 -0
  196. package/rules/web/security.md +62 -0
  197. package/rules/web/testing.md +60 -0
  198. package/rules/zh/README.md +113 -0
  199. package/rules/zh/agents.md +55 -0
  200. package/rules/zh/code-review.md +129 -0
  201. package/rules/zh/coding-style.md +53 -0
  202. package/rules/zh/development-workflow.md +49 -0
  203. package/rules/zh/git-workflow.md +29 -0
  204. package/rules/zh/hooks.md +35 -0
  205. package/rules/zh/patterns.md +36 -0
  206. package/rules/zh/performance.md +60 -0
  207. package/rules/zh/security.md +34 -0
  208. package/rules/zh/testing.md +34 -0
  209. package/skills/ai/agent-dev.md +102 -0
  210. package/skills/ai/llm-security.md +105 -0
  211. package/skills/ai/ml-ops.md +169 -0
  212. package/skills/ai/orchestration-workflow.md +87 -0
  213. package/skills/ai/prompt-and-eval.md +114 -0
  214. package/skills/ai/prompt-optimizer.md +114 -0
  215. package/skills/ai/rag-system.md +104 -0
  216. package/skills/architecture/api-design.md +100 -0
  217. package/skills/architecture/caching.md +101 -0
  218. package/skills/architecture/cloud-native.md +97 -0
  219. package/skills/architecture/message-queue.md +100 -0
  220. package/skills/architecture/security-arch.md +105 -0
  221. package/skills/development/cpp.md +127 -0
  222. package/skills/development/go.md +129 -0
  223. package/skills/development/java.md +134 -0
  224. package/skills/development/kotlin.md +140 -0
  225. package/skills/development/mobile-crossplatform.md +144 -0
  226. package/skills/development/python.md +109 -0
  227. package/skills/development/rust.md +127 -0
  228. package/skills/development/shell.md +127 -0
  229. package/skills/development/swift.md +129 -0
  230. package/skills/development/typescript.md +129 -0
  231. package/skills/devops/ci-cd.md +108 -0
  232. package/skills/devops/containerization.md +106 -0
  233. package/skills/devops/cost-optimization.md +105 -0
  234. package/skills/devops/data-engineering.md +181 -0
  235. package/skills/devops/database.md +95 -0
  236. package/skills/devops/dependency-management.md +91 -0
  237. package/skills/devops/devsecops.md +96 -0
  238. package/skills/devops/git-workflow.md +100 -0
  239. package/skills/devops/incident-response.md +167 -0
  240. package/skills/devops/monorepo.md +87 -0
  241. package/skills/devops/observability.md +103 -0
  242. package/skills/devops/performance.md +104 -0
  243. package/skills/devops/testing.md +104 -0
  244. package/skills/docs/adr-workflow.md +32 -0
  245. package/skills/docs/backlog-proposal-workflow.md +19 -0
  246. package/skills/docs/customer-profile-workflow.md +22 -0
  247. package/skills/docs/evidence-ingest-workflow.md +23 -0
  248. package/skills/docs/init-docs.md +160 -0
  249. package/skills/docs/init-project.md +50 -0
  250. package/skills/docs/prd-workflow.md +57 -0
  251. package/skills/docs/prfaq-workflow.md +25 -0
  252. package/skills/docs/product-intelligence-review.md +22 -0
  253. package/skills/docs/product-intelligence-workflow.md +62 -0
  254. package/skills/docs/product-signal-workflow.md +27 -0
  255. package/skills/docs/research-workflow.md +28 -0
  256. package/skills/docs/runbook-workflow.md +24 -0
  257. package/skills/exploration/repo-map.md +297 -0
  258. package/skills/frameworks/django.md +159 -0
  259. package/skills/frameworks/nextjs.md +148 -0
  260. package/skills/frameworks/react.md +132 -0
  261. package/skills/frameworks/spring-boot.md +165 -0
  262. package/skills/frontend-design/accessibility.md +153 -0
  263. package/skills/frontend-design/component-patterns.md +111 -0
  264. package/skills/frontend-design/engineering.md +122 -0
  265. package/skills/frontend-design/state-management.md +118 -0
  266. package/skills/frontend-design/ui-aesthetics.md +102 -0
  267. package/skills/frontend-design/ux-principles.md +126 -0
  268. package/skills/quality-gates/review-work.md +90 -0
  269. package/skills/quality-gates/verify-change.md +94 -0
  270. package/skills/quality-gates/verify-module.md +96 -0
  271. package/skills/quality-gates/verify-quality.md +93 -0
  272. package/skills/quality-gates/verify-security.md +95 -0
  273. package/skills/roles/architect.ai-systems.md +37 -0
  274. package/skills/roles/architect.data.md +37 -0
  275. package/skills/roles/architect.enterprise.md +37 -0
  276. package/skills/roles/architect.integration.md +37 -0
  277. package/skills/roles/architect.md +68 -0
  278. package/skills/roles/architect.platform.md +37 -0
  279. package/skills/roles/data-analyst.experiment.md +37 -0
  280. package/skills/roles/data-analyst.md +68 -0
  281. package/skills/roles/data-analyst.product-intelligence.md +37 -0
  282. package/skills/roles/data-analyst.product.md +37 -0
  283. package/skills/roles/data-analyst.telemetry.md +37 -0
  284. package/skills/roles/data-engineer.pipeline.md +37 -0
  285. package/skills/roles/data-engineer.vector-retrieval.md +37 -0
  286. package/skills/roles/data-engineer.warehouse.md +37 -0
  287. package/skills/roles/debugger.md +68 -0
  288. package/skills/roles/designer.accessibility.md +43 -0
  289. package/skills/roles/designer.md +68 -0
  290. package/skills/roles/engineer.ai.md +49 -0
  291. package/skills/roles/engineer.data.md +49 -0
  292. package/skills/roles/engineer.md +85 -0
  293. package/skills/roles/engineer.platform.md +49 -0
  294. package/skills/roles/operator.docs.md +43 -0
  295. package/skills/roles/operator.md +68 -0
  296. package/skills/roles/operator.release.md +43 -0
  297. package/skills/roles/operator.sre.md +43 -0
  298. package/skills/roles/orchestrator.md +66 -0
  299. package/skills/roles/product-manager.ai-product.md +37 -0
  300. package/skills/roles/product-manager.business-strategy.md +43 -0
  301. package/skills/roles/product-manager.enterprise.md +37 -0
  302. package/skills/roles/product-manager.growth.md +37 -0
  303. package/skills/roles/product-manager.md +67 -0
  304. package/skills/roles/product-manager.platform.md +37 -0
  305. package/skills/roles/product-manager.product.md +37 -0
  306. package/skills/roles/qa.ai-eval.md +37 -0
  307. package/skills/roles/qa.api-contract.md +37 -0
  308. package/skills/roles/qa.data-pipeline.md +37 -0
  309. package/skills/roles/qa.md +68 -0
  310. package/skills/roles/qa.test-automation.md +49 -0
  311. package/skills/roles/qa.web-ui.md +37 -0
  312. package/skills/roles/researcher.explorer.md +43 -0
  313. package/skills/roles/researcher.md +68 -0
  314. package/skills/roles/researcher.ux.md +43 -0
  315. package/skills/roles/reviewer.devil-advocate.md +43 -0
  316. package/skills/roles/reviewer.evaluator.md +43 -0
  317. package/skills/roles/reviewer.md +68 -0
  318. package/skills/roles/reviewer.trace.md +43 -0
  319. package/skills/roles/security.ai.md +37 -0
  320. package/skills/roles/security.appsec.md +37 -0
  321. package/skills/roles/security.cloud.md +37 -0
  322. package/skills/roles/security.legal-compliance.md +49 -0
  323. package/skills/roles/security.md +68 -0
  324. package/skills/roles/security.privacy.md +37 -0
  325. package/skills/roles/security.supply-chain.md +37 -0
  326. package/skills/routing.md +139 -0
  327. package/skills/security/blue-team.md +83 -0
  328. package/skills/security/code-audit.md +94 -0
  329. package/skills/security/pentest.md +84 -0
  330. package/skills/security/red-team.md +81 -0
  331. package/skills/security/threat-intel.md +89 -0
  332. package/skills/security/vuln-research.md +87 -0
  333. package/skills/utility/clean-code.md +165 -0
  334. package/sync-agents.mjs +899 -0
  335. package/templates/docs/adr.md +27 -0
  336. package/templates/docs/backlog-proposal.md +26 -0
  337. package/templates/docs/customer-profile.md +34 -0
  338. package/templates/docs/evidence-brief.md +37 -0
  339. package/templates/docs/incident-report.md +46 -0
  340. package/templates/docs/memo.md +27 -0
  341. package/templates/docs/meta-prd.md +56 -0
  342. package/templates/docs/one-pager.md +24 -0
  343. package/templates/docs/prd-business.md +61 -0
  344. package/templates/docs/prd-platform.md +62 -0
  345. package/templates/docs/prd.md +47 -0
  346. package/templates/docs/prfaq.md +31 -0
  347. package/templates/docs/product-intelligence-report.md +31 -0
  348. package/templates/docs/research-brief.md +29 -0
  349. package/templates/docs/rfc-platform.md +60 -0
  350. package/templates/docs/rfc.md +46 -0
  351. package/templates/docs/runbook.md +33 -0
  352. package/templates/docs/signal-brief.md +25 -0
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/registry-sync.mjs — Registry sync hook — reminds to run construct sync after registry changes.
4
+ *
5
+ * Runs as PostToolUse after edits to agents/registry.json. Checks if the file was modified and emits a reminder to run construct sync to regenerate platform adapters.
6
+ */
7
+ import { execSync } from 'child_process';
8
+
9
+ const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
10
+ if (!filePath.endsWith('/agents/registry.json')) process.exit(0);
11
+
12
+ const toolkitDir = process.env.CX_TOOLKIT_DIR || `${process.env.HOME}/.construct`;
13
+ try {
14
+ execSync('node sync-agents.mjs', {
15
+ cwd: toolkitDir,
16
+ stdio: 'pipe',
17
+ env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir },
18
+ timeout: 12_000,
19
+ });
20
+ } catch (e) {
21
+ process.stderr.write(`[registry-sync] Sync failed: ${e.message}\n`);
22
+ }
23
+ process.exit(0);
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/scan-secrets.mjs — Scan secrets hook — detects potential secrets in files before they are committed.
4
+ *
5
+ * Runs as PostToolUse after Edit/Write. Scans the modified file content against known secret patterns (API keys, tokens, passwords) and blocks (exit 2) on matches.
6
+ */
7
+ import { readFileSync } from 'fs';
8
+ import { extname } from 'path';
9
+
10
+ const SCAN_EXTENSIONS = new Set([
11
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
12
+ '.py', '.go', '.rs', '.rb', '.php', '.java', '.kt', '.swift',
13
+ '.sh', '.bash', '.zsh',
14
+ '.env', '.yaml', '.yml', '.toml', '.json', '.tf', '.tfvars', '.config',
15
+ ]);
16
+
17
+ const PLACEHOLDER_PATTERNS = [
18
+ /\.\.\./,
19
+ /YOUR_KEY/i,
20
+ /<[^>]+>/,
21
+ /^sk-\.\.\./,
22
+ /^pk-lf-\.\.\./,
23
+ /__[A-Z_]+__/,
24
+ ];
25
+
26
+ const SECRET_PATTERNS = [
27
+ { name: 'Anthropic API key', pattern: /ANTHROPIC_API_KEY\s*=\s*(sk-ant-[a-zA-Z0-9\-_]{20,})/i },
28
+ { name: 'OpenAI API key', pattern: /OPENAI_API_KEY\s*=\s*(sk-[a-zA-Z0-9]{40,})/i },
29
+ { name: 'OpenRouter key', pattern: /(sk-or-v1-[a-zA-Z0-9]{40,})/ },
30
+ { name: 'AWS access key', pattern: /(AKIA[0-9A-Z]{16})/ },
31
+ { name: 'Private key (PEM)', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
32
+ { name: 'GitHub personal access token', pattern: /(ghp_[a-zA-Z0-9]{36})/ },
33
+ { name: 'GitHub Actions token', pattern: /(ghs_[a-zA-Z0-9]{36})/ },
34
+ { name: 'Database URL with credentials', pattern: /DATABASE_URL\s*=\s*(postgresql:\/\/[^@]+:[^@]+@)/i },
35
+ ];
36
+
37
+ function isPlaceholder(value) {
38
+ return PLACEHOLDER_PATTERNS.some((p) => p.test(value));
39
+ }
40
+
41
+ const filePath = process.env.TOOL_INPUT_FILE_PATH;
42
+
43
+ if (!filePath) process.exit(0);
44
+
45
+ const ext = extname(filePath).toLowerCase();
46
+ if (!SCAN_EXTENSIONS.has(ext)) process.exit(0);
47
+
48
+ let content;
49
+ try {
50
+ content = readFileSync(filePath, 'utf8');
51
+ } catch {
52
+ process.exit(0);
53
+ }
54
+
55
+ const lines = content.split('\n');
56
+ const findings = [];
57
+
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ for (const { name, pattern } of SECRET_PATTERNS) {
61
+ const match = pattern.exec(line);
62
+ if (!match) continue;
63
+ const captured = match[1] ?? match[0];
64
+ if (isPlaceholder(captured)) continue;
65
+ findings.push({ name, line: i + 1, value: captured.slice(0, 20) + '...' });
66
+ }
67
+ }
68
+
69
+ if (findings.length === 0) process.exit(0);
70
+
71
+ process.stderr.write('[scan-secrets] BLOCKED: Secret(s) detected in ' + filePath + '\n');
72
+ for (const { name, line, value } of findings) {
73
+ process.stderr.write(` Line ${line}: ${name} — matched value starts with: ${value}\n`);
74
+ }
75
+ process.stderr.write('Remove or rotate the secret before writing this file.\n');
76
+ process.exit(2);
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/session-start.mjs — Session start hook — emits resumable project context at the start of each session.
4
+ *
5
+ * Runs at session start. Reads .cx/context.json, workflow state, git status, and efficiency log to produce a structured resume message. Non-blocking — always exits 0.
6
+ */
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { execSync } from 'child_process';
9
+ import { homedir } from 'os';
10
+ import { dirname, join, resolve } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { loadWorkflow, summarizeWorkflow, alignmentFindings } from '../workflow-state.mjs';
13
+ import { buildCompactEfficiencyDigest, readEfficiencyLog } from '../efficiency.mjs';
14
+ import { readContextState, contextSummaryLine } from '../context-state.mjs';
15
+
16
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
17
+ const CONSTRUCT_BIN = resolve(MODULE_DIR, '..', '..', 'bin', 'construct');
18
+
19
+ let input = {};
20
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
21
+
22
+ const cwd = input?.cwd || process.cwd();
23
+ const projectName = cwd.split('/').pop() || 'project';
24
+ const date = new Date().toISOString().slice(0, 10);
25
+
26
+ // Load context — project-local first, global fallback
27
+ const localCtx = join(cwd, '.cx', 'context.md');
28
+ const globalCtx = join(homedir(), '.cx', 'context.md');
29
+ let context = '';
30
+ const localState = readContextState(cwd);
31
+ const globalState = localState ? null : readContextState(homedir());
32
+ const stateContext = localState || globalState;
33
+ if (stateContext?.markdown) {
34
+ const raw = String(stateContext.markdown);
35
+ context = raw.length > 1800 ? `${raw.slice(0, 400)}\n…\n${raw.slice(-1400)}` : raw;
36
+ } else if (existsSync(localCtx)) {
37
+ try { const raw = readFileSync(localCtx, 'utf8'); context = raw.length > 1800 ? `${raw.slice(0, 400)}\n…\n${raw.slice(-1400)}` : raw; } catch { /* best effort */ }
38
+ } else if (existsSync(globalCtx)) {
39
+ try { const raw = readFileSync(globalCtx, 'utf8'); context = raw.length > 1800 ? `${raw.slice(0, 400)}\n…\n${raw.slice(-1400)}` : raw; } catch { /* best effort */ }
40
+ }
41
+ const contextSummary = contextSummaryLine(stateContext);
42
+
43
+ // Git status
44
+ let uncommitted = 0;
45
+ let recentCommits = '';
46
+ try {
47
+ const status = execSync(`git -C "${cwd}" status --short 2>/dev/null`, { timeout: 5000 }).toString();
48
+ uncommitted = status.split('\n').filter(l => l.trim()).length;
49
+ } catch { /* not a git repo or git unavailable */ }
50
+
51
+ try {
52
+ recentCommits = execSync(`git -C "${cwd}" log --oneline -3 2>/dev/null`, { timeout: 5000 }).toString().trim();
53
+ } catch { /* best effort */ }
54
+
55
+ // Pending typecheck warning
56
+ const tcPath = join(homedir(), '.cx', 'pending-typecheck.txt');
57
+ let pendingNote = '';
58
+ try {
59
+ const pending = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
60
+ if (pending.length > 0) pendingNote = '\nNote: TypeScript was not checked last session.';
61
+ } catch { /* best effort */ }
62
+
63
+ const statusLine = uncommitted > 0
64
+ ? `Current: ${uncommitted} uncommitted file${uncommitted !== 1 ? 's' : ''}`
65
+ : 'Current: clean working tree';
66
+
67
+ const recentLine = recentCommits
68
+ ? `Recent: ${recentCommits.split('\n').join(' · ')}`
69
+ : '';
70
+
71
+ const header = `## Resuming — ${projectName} · ${date}\nNote: You are currently working on the **${projectName}** project.`;
72
+ const footer = [statusLine, recentLine].filter(Boolean).join(' · ');
73
+
74
+ let workflowNote = '';
75
+ try {
76
+ const workflow = loadWorkflow(cwd);
77
+ if (workflow) {
78
+ const findings = alignmentFindings(workflow);
79
+ const high = findings.filter((finding) => finding.severity === 'HIGH').length;
80
+ const medium = findings.filter((finding) => finding.severity === 'MEDIUM').length;
81
+ workflowNote = `\n## Active Construct Workflow\n${summarizeWorkflow(workflow)}\nAlignment: ${high ? `${high} high issue${high === 1 ? '' : 's'}` : 'no high issues'}${medium ? `, ${medium} warning${medium === 1 ? '' : 's'}` : ''}\n`;
82
+ }
83
+ } catch {
84
+ // best effort only
85
+ }
86
+
87
+ const body = context || '## Fresh start — no prior context found.\n';
88
+ const efficiency = buildCompactEfficiencyDigest(readEfficiencyLog(homedir()));
89
+ const efficiencyNote = efficiency?.compact
90
+ ? `\n## Session efficiency\n${efficiency.compact}\n`
91
+ : '';
92
+ const stateNote = contextSummary ? `\n## Context digest\n${contextSummary}\n` : '';
93
+ const memoryDirective = `\n## Required first actions\nBefore responding: call memory_search("${projectName}") to retrieve prior session context and user preferences. Apply any relevant results immediately. If context is degraded, compact before broad rereads.\n`;
94
+ process.stdout.write(`${header}\n${body}${stateNote}${workflowNote}${efficiencyNote}${memoryDirective}\n${footer}${pendingNote}\n`);
95
+ process.exit(0);
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/stop-notify.mjs — Stop notify hook — emits a session summary notification when Claude stops.
4
+ *
5
+ * Runs as a Stop hook. Summarizes the session work, active workflow state, and efficiency signals into a final message written to stdout for the user.
6
+ */
7
+ import { readFileSync, writeFileSync, existsSync, appendFileSync, statSync } from 'fs';
8
+ import { execSync } from 'child_process';
9
+ import { homedir } from 'os';
10
+ import { join } from 'path';
11
+ import { loadWorkflow, summarizeWorkflow, alignmentFindings } from '../workflow-state.mjs';
12
+
13
+ function readLastTranscriptUsage(transcriptPath) {
14
+ try {
15
+ if (!transcriptPath || !existsSync(transcriptPath)) return null;
16
+ const size = statSync(transcriptPath).size;
17
+ const readSize = Math.min(size, 256 * 1024);
18
+ const content = readFileSync(transcriptPath, 'utf8');
19
+ const tail = content.slice(-readSize);
20
+ const lines = tail.split(/\r?\n/).filter(Boolean);
21
+ for (let i = lines.length - 1; i >= 0; i--) {
22
+ try {
23
+ const obj = JSON.parse(lines[i]);
24
+ const usage = obj?.message?.usage || obj?.usage;
25
+ if (usage && (usage.input_tokens || usage.output_tokens)) return usage;
26
+ } catch { /* skip */ }
27
+ }
28
+ } catch { /* best effort */ }
29
+ return null;
30
+ }
31
+
32
+ function resolveAttribution(workflow) {
33
+ try {
34
+ if (!workflow) return { agent: 'construct' };
35
+ const active = (workflow.tasks || []).find((t) => t.status === 'in-progress' || t.status === 'in_progress');
36
+ if (active?.owner) return { agent: active.owner, taskKey: active.key };
37
+ return { agent: 'construct' };
38
+ } catch {
39
+ return { agent: 'construct' };
40
+ }
41
+ }
42
+
43
+ // Stop hooks must echo stdin to stdout
44
+ let raw = '';
45
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
46
+ if (raw) process.stdout.write(raw);
47
+
48
+ const home = homedir();
49
+ const tsResultPath = join(home, '.cx', 'ts-result.txt');
50
+ const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
51
+ const countPath = join(home, '.cx', 'files-changed-count.txt');
52
+ const costLogPath = join(home, '.cx', 'session-cost.jsonl');
53
+ const lastAgentPath = join(home, '.cx', 'last-agent.json');
54
+
55
+ // Read TS result
56
+ let tsResult = 'unchecked';
57
+ try { tsResult = existsSync(tsResultPath) ? readFileSync(tsResultPath, 'utf8').trim() : 'unchecked'; } catch { /* best effort */ }
58
+
59
+ // Read and clear warn flags
60
+ let warnings = [];
61
+ try {
62
+ if (existsSync(warnFlagsPath)) {
63
+ warnings = readFileSync(warnFlagsPath, 'utf8').split('\n').filter(Boolean);
64
+ writeFileSync(warnFlagsPath, '');
65
+ }
66
+ } catch { /* best effort */ }
67
+
68
+ // Read and clear files-changed count
69
+ let fileCount = 0;
70
+ try {
71
+ if (existsSync(countPath)) {
72
+ fileCount = parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) || 0;
73
+ writeFileSync(countPath, '0');
74
+ }
75
+ } catch { /* best effort */ }
76
+
77
+ // Estimate cost from stdin usage tokens using Sonnet 4.5/4.6 defaults.
78
+ let costNote = '';
79
+ try {
80
+ const payload = raw ? JSON.parse(raw) : {};
81
+ let usage = payload?.usage || payload?.stop_hook_active?.usage || null;
82
+ const transcriptPath = payload?.transcript_path || payload?.transcriptPath || process.env.CLAUDE_TRANSCRIPT_PATH;
83
+ if (!usage || (!usage.input_tokens && !usage.output_tokens)) {
84
+ const fromTranscript = readLastTranscriptUsage(transcriptPath);
85
+ if (fromTranscript) usage = fromTranscript;
86
+ }
87
+ usage = usage || {};
88
+ const inputTokens = usage?.input_tokens || 0;
89
+ const outputTokens = usage?.output_tokens || 0;
90
+ const cacheReadInputTokens = usage?.cache_read_input_tokens || 0;
91
+ const cacheCreation5mInputTokens = usage?.cache_creation?.ephemeral_5m_input_tokens || usage?.cache_creation_5m_input_tokens || 0;
92
+ const cacheCreation1hInputTokens = usage?.cache_creation?.ephemeral_1h_input_tokens || usage?.cache_creation_1h_input_tokens || 0;
93
+ const explicitCacheCreationInputTokens = usage?.cache_creation_input_tokens || 0;
94
+ const cacheCreationInputTokens = explicitCacheCreationInputTokens || cacheCreation5mInputTokens + cacheCreation1hInputTokens;
95
+ if (inputTokens || outputTokens) {
96
+ const cost = (
97
+ inputTokens * 3 +
98
+ cacheCreation5mInputTokens * 3.75 +
99
+ cacheCreation1hInputTokens * 6 +
100
+ Math.max(0, cacheCreationInputTokens - cacheCreation5mInputTokens - cacheCreation1hInputTokens) * 3.75 +
101
+ cacheReadInputTokens * 0.3 +
102
+ outputTokens * 15
103
+ ) / 1_000_000;
104
+ costNote = `~${cost.toFixed(2)} this response`;
105
+
106
+ // Read agent attribution recorded by agent-tracker hook (best effort, cleared after read)
107
+ let agentName = null;
108
+ let taskKey = null;
109
+ try {
110
+ if (existsSync(lastAgentPath)) {
111
+ const lastAgent = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
112
+ agentName = lastAgent.agent || null;
113
+ taskKey = lastAgent.taskKey || null;
114
+ writeFileSync(lastAgentPath, '');
115
+ }
116
+ } catch { /* best effort */ }
117
+
118
+ // Fallback attribution: active workflow task owner, else 'construct'
119
+ if (!agentName) {
120
+ try {
121
+ const wf = loadWorkflow(process.cwd());
122
+ const resolved = resolveAttribution(wf);
123
+ agentName = resolved.agent;
124
+ if (!taskKey && resolved.taskKey) taskKey = resolved.taskKey;
125
+ } catch { agentName = 'construct'; }
126
+ }
127
+
128
+ try {
129
+ const entry = { ts: new Date().toISOString(), input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: cost };
130
+ if (cacheReadInputTokens) entry.cache_read_input_tokens = cacheReadInputTokens;
131
+ if (cacheCreationInputTokens) entry.cache_creation_input_tokens = cacheCreationInputTokens;
132
+ if (cacheCreation5mInputTokens) entry.cache_creation_5m_input_tokens = cacheCreation5mInputTokens;
133
+ if (cacheCreation1hInputTokens) entry.cache_creation_1h_input_tokens = cacheCreation1hInputTokens;
134
+ if (agentName) entry.agent = agentName;
135
+ if (taskKey) entry.task_key = taskKey;
136
+ appendFileSync(costLogPath, JSON.stringify(entry) + '\n');
137
+ } catch { /* best effort */ }
138
+ }
139
+ } catch { /* payload parse failure */ }
140
+
141
+ // Executive Summary from Workflow
142
+ let execSummary = '';
143
+ try {
144
+ const workflow = loadWorkflow(process.cwd());
145
+ if (workflow) {
146
+ const findings = alignmentFindings(workflow);
147
+ const high = findings.filter(f => f.severity === 'HIGH').length;
148
+ const phase = workflow.phase.toUpperCase();
149
+ execSummary = `${phase}: ${workflow.status}${high > 0 ? ` (${high} blocks)` : ''}`;
150
+ }
151
+ } catch { /* best effort */ }
152
+
153
+ // Compose message parts
154
+ const parts = [];
155
+ if (execSummary) parts.push(execSummary);
156
+ if (fileCount > 0) parts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''} updated`);
157
+ if (tsResult === 'pass') parts.push('TS OK');
158
+ else if (tsResult !== 'unchecked') parts.push(`TS: ${tsResult}`);
159
+ if (costNote) parts.push(costNote);
160
+
161
+ const hasWarnings = warnings.length > 0;
162
+ const title = hasWarnings ? 'Construct — Review needed' : 'Construct';
163
+ const body = [
164
+ ...warnings.slice(0, 3),
165
+ parts.join(' · '),
166
+ ].filter(Boolean).join('\n');
167
+
168
+ // Fire notification
169
+ const platform = process.platform;
170
+ try {
171
+ if (platform === 'darwin') {
172
+ const escaped = body.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
173
+ const titleEsc = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
174
+ execSync(`osascript -e 'display notification "${escaped}" with title "${titleEsc}"'`, {
175
+ timeout: 5000,
176
+ stdio: 'pipe',
177
+ });
178
+ } else if (platform === 'linux') {
179
+ execSync(`notify-send "${title}" "${body.replace(/"/g, '\\"')}"`, {
180
+ timeout: 5000,
181
+ stdio: 'pipe',
182
+ });
183
+ } else {
184
+ process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
185
+ }
186
+ } catch {
187
+ // Fallback to stderr if notification fails
188
+ process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
189
+ }
190
+
191
+ process.exit(0);
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/stop-typecheck.mjs — Stop typecheck hook — runs TypeScript type-check at session end and records result.
4
+ *
5
+ * Runs as a Stop hook. Invokes tsc --noEmit on the project and records pass/fail to ~/.cx/pending-typecheck.txt for the next session-start hook to surface.
6
+ */
7
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
8
+ import { execSync } from 'child_process';
9
+ import { homedir } from 'os';
10
+ import { join, dirname } from 'path';
11
+
12
+ // Stop hooks must echo stdin to stdout
13
+ let raw = '';
14
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
15
+ if (raw) process.stdout.write(raw);
16
+
17
+ const home = homedir();
18
+ const tcPath = join(home, '.cx', 'pending-typecheck.txt');
19
+ const tsResultPath = join(home, '.cx', 'ts-result.txt');
20
+ const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
21
+
22
+ // Read pending files
23
+ const pending = existsSync(tcPath)
24
+ ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean)
25
+ : [];
26
+
27
+ if (pending.length === 0) {
28
+ try { writeFileSync(tsResultPath, 'pass'); } catch { /* best effort */ }
29
+ process.exit(0);
30
+ }
31
+
32
+ // Clear pending list immediately (don't re-run on next session if this run crashes)
33
+ try { writeFileSync(tcPath, ''); } catch { /* best effort */ }
34
+
35
+ // Find nearest tsconfig.json by walking up from first pending file's directory
36
+ function findTsConfig(startDir) {
37
+ let dir = startDir;
38
+ for (let i = 0; i < 10; i++) {
39
+ const candidate = join(dir, 'tsconfig.json');
40
+ if (existsSync(candidate)) return candidate;
41
+ const parent = dirname(dir);
42
+ if (parent === dir) break;
43
+ dir = parent;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ const startDir = dirname(pending[0]);
49
+ const tsconfig = findTsConfig(startDir);
50
+
51
+ if (!tsconfig) {
52
+ try { writeFileSync(tsResultPath, 'pass'); } catch { /* best effort */ }
53
+ process.exit(0);
54
+ }
55
+
56
+ const projectDir = dirname(tsconfig);
57
+
58
+ try {
59
+ execSync('npx tsc --noEmit --pretty false', {
60
+ cwd: projectDir,
61
+ stdio: 'pipe',
62
+ timeout: 90_000,
63
+ });
64
+ try { writeFileSync(tsResultPath, 'pass'); } catch { /* best effort */ }
65
+ } catch (e) {
66
+ const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');
67
+ // Count error lines (lines ending with "error TS####:")
68
+ const errorLines = output.split('\n').filter(l => /error TS\d+:/.test(l));
69
+ const count = errorLines.length || 1;
70
+
71
+ try { writeFileSync(tsResultPath, `${count} error${count !== 1 ? 's' : ''}`); } catch { /* best effort */ }
72
+
73
+ try {
74
+ const existing = existsSync(warnFlagsPath) ? readFileSync(warnFlagsPath, 'utf8') : '';
75
+ writeFileSync(warnFlagsPath, existing + `TypeScript: ${count} error${count !== 1 ? 's' : ''}\n`);
76
+ } catch { /* best effort */ }
77
+
78
+ process.stderr.write(
79
+ `[stop-typecheck] TypeScript found ${count} error${count !== 1 ? 's' : ''} that need attention. The code runs but won't compile cleanly. Review before releasing.\n`
80
+ );
81
+ }
82
+
83
+ process.exit(0);
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/task-completed-guard.mjs — Task completed guard hook — enforces verification evidence before marking tasks done.
4
+ *
5
+ * Runs as PreToolUse on workflow_update_task. Checks that implement-phase tasks include verification evidence before allowing status to be set to done. Exits 2 to block.
6
+ */
7
+ import { loadWorkflow, validateWorkflowState } from '../workflow-state.mjs';
8
+
9
+ function readInput() {
10
+ try {
11
+ const chunks = [];
12
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
13
+ return new Promise((resolve) => {
14
+ process.stdin.on('end', () => {
15
+ try {
16
+ const raw = Buffer.concat(chunks).toString('utf8');
17
+ resolve(raw ? JSON.parse(raw) : {});
18
+ } catch {
19
+ resolve({});
20
+ }
21
+ });
22
+ });
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ const input = await readInput();
29
+ const cwd = input.cwd || input.workspace_dir || process.cwd();
30
+ const workflow = loadWorkflow(cwd);
31
+
32
+ if (!workflow) process.exit(0);
33
+
34
+ const result = validateWorkflowState(workflow);
35
+ if (result.valid) process.exit(0);
36
+
37
+ process.stderr.write([
38
+ '[construct] Task completion blocked by workflow validation.',
39
+ ...result.errors.map((error) => `- ${error}`),
40
+ 'Update .cx/workflow.json with owner, acceptance criteria, dependencies, and verification before marking the task complete.',
41
+ '',
42
+ ].join('\n'));
43
+ process.exit(2);
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/teammate-idle-guard.mjs — Teammate idle guard hook — warns when a spawned agent has been idle too long.
4
+ *
5
+ * Runs as PostToolUse after Agent tool calls. Tracks agent start times and emits a warning if an agent appears idle or unresponsive beyond the idle threshold.
6
+ */
7
+ import { loadWorkflow } from '../workflow-state.mjs';
8
+
9
+ async function readInput() {
10
+ const chunks = [];
11
+ for await (const chunk of process.stdin) chunks.push(chunk);
12
+ try {
13
+ const raw = Buffer.concat(chunks).toString('utf8');
14
+ return raw ? JSON.parse(raw) : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function namesFromInput(input) {
21
+ return [
22
+ input.teammate,
23
+ input.teammate_name,
24
+ input.agent,
25
+ input.agent_name,
26
+ input.name,
27
+ ].filter((value) => typeof value === 'string' && value.trim()).map((value) => value.trim());
28
+ }
29
+
30
+ const input = await readInput();
31
+ const cwd = input.cwd || input.workspace_dir || process.cwd();
32
+ const names = namesFromInput(input);
33
+ const workflow = loadWorkflow(cwd);
34
+
35
+ if (!workflow || names.length === 0) process.exit(0);
36
+
37
+ const active = (workflow.tasks || []).find((task) =>
38
+ names.includes(task.owner)
39
+ && task.status === 'in-progress'
40
+ && (!Array.isArray(task.notes) || task.notes.length === 0)
41
+ && (!Array.isArray(task.verification) || task.verification.length === 0)
42
+ );
43
+
44
+ if (!active) process.exit(0);
45
+
46
+ process.stderr.write([
47
+ `[construct] ${names[0]} is going idle with active task ${active.key} but no workflow update.`,
48
+ 'Before going idle, update .cx/workflow.json with one of:',
49
+ '- status=done plus verification evidence',
50
+ '- status=blocked with the blocker',
51
+ '- status=blocked_needs_user and a NEEDS_MAIN_INPUT packet for the primary persona',
52
+ '',
53
+ ].join('\n'));
54
+ process.exit(2);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/workflow-guard.mjs — Workflow guard hook — enforces that significant work flows through the workflow state.
4
+ *
5
+ * Runs as PreToolUse on implement tasks. Checks that the active workflow task is set before allowing non-trivial Bash or Edit operations. Warns without blocking.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { loadWorkflow, alignmentFindings, summarizeWorkflow } from "../workflow-state.mjs";
9
+
10
+ let input = {};
11
+ try {
12
+ input = JSON.parse(readFileSync(0, "utf8"));
13
+ } catch {
14
+ process.exit(0);
15
+ }
16
+
17
+ const text = [
18
+ input?.prompt,
19
+ input?.message,
20
+ input?.transcript,
21
+ input?.last_message,
22
+ input?.assistant_message
23
+ ].filter(Boolean).join("\n").toLowerCase();
24
+
25
+ if (/\b(stop|pause|halt|enough|abort|cancel)\b/.test(text)) {
26
+ process.exit(0);
27
+ }
28
+
29
+ const cwd = input?.cwd || process.cwd();
30
+ const workflow = loadWorkflow(cwd);
31
+ if (!workflow || workflow.status !== "in-progress") {
32
+ process.exit(0);
33
+ }
34
+
35
+ const tasks = workflow.tasks || [];
36
+ const openTasks = tasks.filter((task) => !["done", "skipped"].includes(task.status));
37
+ const findings = alignmentFindings(workflow).filter((finding) => finding.severity === "HIGH");
38
+
39
+ if (openTasks.length === 0 && findings.length === 0) {
40
+ process.exit(0);
41
+ }
42
+
43
+ const next = tasks.find((task) => task.key === workflow.currentTaskKey) || openTasks[0];
44
+ const lines = [
45
+ "Construct workflow is still active.",
46
+ summarizeWorkflow(workflow)
47
+ ];
48
+
49
+ if (next) {
50
+ lines.push(`Next task: ${next.key} ${next.title} -> ${next.owner} [${next.status}]`);
51
+ }
52
+
53
+ if (findings.length > 0) {
54
+ lines.push(`Alignment blockers: ${findings.length}`);
55
+ for (const finding of findings.slice(0, 3)) {
56
+ lines.push(`- ${finding.task ? `${finding.task}: ` : ""}${finding.issue}`);
57
+ }
58
+ }
59
+
60
+ lines.push("Continue the current workflow, update .cx/workflow.json, or explicitly say stop/pause to end.");
61
+ console.error(lines.join("\n"));
62
+ process.exit(2);