@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,794 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/workflow-state.mjs — <one-line purpose>
4
+ *
5
+ * <2–6 line summary.>
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { execSync } from "node:child_process";
10
+ import { routeRequest } from "./orchestration-policy.mjs";
11
+
12
+ const PHASES = ["research", "plan", "implement", "validate", "operate"];
13
+ // Back-compat stub: pre-overhaul workflow.json files used Matrix-themed phase
14
+ // names. Kept solely to migrate existing state on load. Do not use these names
15
+ // in new code — write only the canonical PHASES values above.
16
+ const LEGACY_PHASE_ALIASES = {
17
+ oracle: "research",
18
+ morpheus: "plan",
19
+ neo: "implement",
20
+ trinity: "validate",
21
+ operator: "operate"
22
+ };
23
+ const VALID_STATUS = new Set(["todo", "in-progress", "blocked", "blocked_needs_user", "blocked_needs_executive", "done", "skipped"]);
24
+
25
+ /**
26
+ * High-gate checkpoints that require explicit Executive/Customer sign-off.
27
+ * Work cannot transition out of these phases without an 'executive-approved' status.
28
+ */
29
+ export const EXECUTIVE_GATES = {
30
+ plan: "The plan requires executive approval before implementation.",
31
+ validate: "Final validation requires executive sign-off before release."
32
+ };
33
+
34
+ function normalizePhase(value, fallback = null) {
35
+ const normalized = LEGACY_PHASE_ALIASES[value] || value;
36
+ return PHASES.includes(normalized) ? normalized : fallback;
37
+ }
38
+
39
+ function normalizeWorkflow(workflow) {
40
+ if (!workflow || typeof workflow !== "object") return workflow;
41
+ const normalizedPhase = normalizePhase(workflow.phase, workflow.phase);
42
+ const normalizedPhases = {};
43
+
44
+ for (const [key, entry] of Object.entries(workflow.phases || {})) {
45
+ const phaseKey = normalizePhase(key, key);
46
+ normalizedPhases[phaseKey] = {
47
+ ...normalizedPhases[phaseKey],
48
+ ...entry,
49
+ owner: normalizePhase(entry?.owner, entry?.owner ?? phaseKey)
50
+ };
51
+ }
52
+
53
+ workflow.phase = normalizedPhase;
54
+ workflow.phases = normalizedPhases;
55
+ workflow.tasks = (workflow.tasks || []).map((task) => ({
56
+ ...task,
57
+ phase: normalizePhase(task.phase, task.phase)
58
+ }));
59
+ return workflow;
60
+ }
61
+
62
+ function now() {
63
+ return new Date().toISOString();
64
+ }
65
+
66
+ function slugify(value) {
67
+ return String(value || "workflow")
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9]+/g, "-")
70
+ .replace(/^-+|-+$/g, "")
71
+ .slice(0, 64) || "workflow";
72
+ }
73
+
74
+ function projectName(root) {
75
+ try {
76
+ const remote = execSync("git remote get-url origin", { cwd: root, timeout: 3000, stdio: ["ignore", "pipe", "ignore"] })
77
+ .toString()
78
+ .trim();
79
+ const match = remote.match(/[:/]([^/]+\/[^/.]+)(?:\.git)?$/);
80
+ if (match) return match[1];
81
+ } catch {
82
+ // best effort
83
+ }
84
+ return path.basename(root);
85
+ }
86
+
87
+ export function workflowPath(root = process.cwd()) {
88
+ return path.join(root, ".cx", "workflow.json");
89
+ }
90
+
91
+ export function defaultWorkflow(root = process.cwd(), title = "Untitled workflow", specRef = null) {
92
+ const id = `${new Date().toISOString().slice(0, 10)}-${slugify(title)}`;
93
+ return {
94
+ version: 1,
95
+ project: projectName(root),
96
+ id,
97
+ title,
98
+ specRef: specRef || null,
99
+ status: "in-progress",
100
+ phase: "plan",
101
+ currentTaskKey: null,
102
+ updatedAt: now(),
103
+ phases: {
104
+ research: { owner: "research", status: "todo", summary: "Explore the problem and gather evidence." },
105
+ plan: { owner: "plan", status: "in-progress", summary: "Define and challenge the approach." },
106
+ implement: { owner: "implement", status: "todo", summary: "Build the approved solution." },
107
+ validate: { owner: "validate", status: "todo", summary: "Verify correctness, security, accessibility, and tests." },
108
+ operate: { owner: "operate", status: "todo", summary: "Run, release, deploy, or operationalize when needed." }
109
+ },
110
+ tasks: [],
111
+ decisions: [],
112
+ handoffs: [],
113
+ alignment: {
114
+ acceptanceCriteriaRequired: true,
115
+ ownerRequired: true,
116
+ readFirstRequired: true,
117
+ doNotChangeRequired: true,
118
+ verificationRequiredBeforeDone: true
119
+ }
120
+ };
121
+ }
122
+
123
+ export function loadWorkflow(root = process.cwd()) {
124
+ const file = workflowPath(root);
125
+ if (!fs.existsSync(file)) return null;
126
+ return normalizeWorkflow(JSON.parse(fs.readFileSync(file, "utf8")));
127
+ }
128
+
129
+ export function saveWorkflow(workflow, root = process.cwd()) {
130
+ const file = workflowPath(root);
131
+ fs.mkdirSync(path.dirname(file), { recursive: true });
132
+ const normalized = {
133
+ ...normalizeWorkflow(structuredClone(workflow)),
134
+ updatedAt: now()
135
+ };
136
+ fs.writeFileSync(file, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
137
+ return normalized;
138
+ }
139
+
140
+ export function createNeedsMainInputPacket({
141
+ taskKey,
142
+ worker,
143
+ blocker,
144
+ question,
145
+ safeDefault,
146
+ context = [],
147
+ }) {
148
+ if (!taskKey) throw new Error("NEEDS_MAIN_INPUT requires taskKey");
149
+ if (!worker) throw new Error("NEEDS_MAIN_INPUT requires worker");
150
+ if (!blocker) throw new Error("NEEDS_MAIN_INPUT requires blocker");
151
+ if (!question) throw new Error("NEEDS_MAIN_INPUT requires question");
152
+ return {
153
+ type: "NEEDS_MAIN_INPUT",
154
+ taskKey,
155
+ worker,
156
+ blocker,
157
+ question,
158
+ safeDefault: safeDefault || null,
159
+ context: Array.isArray(context) ? context : [String(context)],
160
+ createdAt: now(),
161
+ };
162
+ }
163
+
164
+ export function initWorkflow(root = process.cwd(), title = "Untitled workflow", specRef = null) {
165
+ const existing = loadWorkflow(root);
166
+ if (existing) return { workflow: existing, created: false };
167
+ return { workflow: saveWorkflow(defaultWorkflow(root, title, specRef), root), created: true };
168
+ }
169
+
170
+ function nextTaskKey(workflow) {
171
+ const used = new Set((workflow.tasks || []).map((task) => task.key));
172
+ let n = 1;
173
+ while (used.has(`todo:${n}`)) n += 1;
174
+ return `todo:${n}`;
175
+ }
176
+
177
+ export function addTask(root, options) {
178
+ const workflow = loadWorkflow(root) || defaultWorkflow(root, options.workflowTitle || "Untitled workflow");
179
+ const phase = normalizePhase(options.phase || workflow.phase || "implement");
180
+ if (!phase) throw new Error(`Invalid phase: ${options.phase || workflow.phase}`);
181
+ const task = {
182
+ key: options.key || nextTaskKey(workflow),
183
+ title: options.title || "Untitled task",
184
+ phase,
185
+ owner: options.owner || phase,
186
+ status: options.status || "todo",
187
+ dependsOn: options.dependsOn || [],
188
+ files: options.files || [],
189
+ readFirst: options.readFirst || [],
190
+ doNotChange: options.doNotChange || [],
191
+ acceptanceCriteria: options.acceptanceCriteria || [],
192
+ verification: options.verification || [],
193
+ overlays: options.overlays || [],
194
+ challengeRequired: Boolean(options.challengeRequired),
195
+ challengeStatus: options.challengeStatus || null,
196
+ tokenBudget: options.tokenBudget || null,
197
+ tokensUsed: null,
198
+ notes: [],
199
+ createdAt: now(),
200
+ updatedAt: now()
201
+ };
202
+ if (!VALID_STATUS.has(task.status)) throw new Error(`Invalid status: ${task.status}`);
203
+ const existing = workflow.tasks || [];
204
+ const normalizedTitle = task.title.trim().toLowerCase();
205
+ const duplicate = existing.find((t) => {
206
+ if (t.key === task.key) return true;
207
+ if (t.status === "done" || t.status === "skipped") return false;
208
+ return (t.title || "").trim().toLowerCase() === normalizedTitle;
209
+ });
210
+ if (duplicate) {
211
+ if (!workflow.currentTaskKey) workflow.currentTaskKey = duplicate.key;
212
+ return saveWorkflow(workflow, root);
213
+ }
214
+ workflow.tasks = [...existing, task];
215
+ if (!workflow.currentTaskKey) workflow.currentTaskKey = task.key;
216
+ return saveWorkflow(workflow, root);
217
+ }
218
+
219
+ export function buildTaskPacketFromIntent(request, options = {}) {
220
+ const route = routeRequest({ request: String(request || "") });
221
+ if (!route) return null;
222
+ if (route.track === "immediate") return null;
223
+
224
+ const defaults = {
225
+ title: options.title || request || "User-requested task",
226
+ phase: options.phase || (route.intent === "research" ? "research" : route.intent === "fix" ? "implement" : route.intent === "evaluation" ? "validate" : route.intent === "implementation" ? "implement" : "implement"),
227
+ owner: options.owner || (route.workCategory === "analysis" ? "cx-architect" : route.workCategory === "writing" ? "cx-docs-keeper" : route.workCategory === "visual" ? "cx-designer" : route.workCategory === "deep" ? "cx-engineer" : "cx-engineer"),
228
+ status: "todo",
229
+ dependsOn: options.dependsOn || [],
230
+ files: options.files || [],
231
+ readFirst: options.readFirst || [],
232
+ doNotChange: options.doNotChange || [],
233
+ acceptanceCriteria: options.acceptanceCriteria || ["Task derived from user intent and tracked in workflow state"],
234
+ verification: options.verification || [],
235
+ overlays: options.overlays || [],
236
+ challengeRequired: Boolean(options.challengeRequired ?? (route.track === "orchestrated")),
237
+ challengeStatus: options.challengeStatus || null,
238
+ source: {
239
+ kind: "user-intent",
240
+ intent: route.intent,
241
+ workCategory: route.workCategory,
242
+ track: route.track,
243
+ specialists: route.specialists || [],
244
+ dispatchPlan: route.dispatchPlan || null,
245
+ request: String(request || ""),
246
+ },
247
+ };
248
+
249
+ return { ...defaults, ...options, source: { ...defaults.source, ...(options.source || {}) } };
250
+ }
251
+
252
+ export function addTaskFromIntent(root, request, options = {}) {
253
+ const packet = buildTaskPacketFromIntent(request, options);
254
+ if (!packet) return null;
255
+ return addTask(root, packet);
256
+ }
257
+
258
+ function parseCommaSeparated(value) {
259
+ if (!value) return [];
260
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
261
+ }
262
+
263
+ function slugifyTitle(title, usedKeys) {
264
+ const base = slugify(title);
265
+ if (!usedKeys.has(base)) return base;
266
+ let n = 2;
267
+ while (usedKeys.has(`${base}-${n}`)) n += 1;
268
+ return `${base}-${n}`;
269
+ }
270
+
271
+ function parseRichSections(markdown, options) {
272
+ const defaultPhase = normalizePhase(options.phase || "implement");
273
+ const defaultOwner = options.owner || "cx-engineer";
274
+ const defaultReadFirst = options.readFirst || [];
275
+ const defaultDoNotChange = options.doNotChange || [];
276
+ const defaultAcceptanceCriteria = options.acceptanceCriteria || [];
277
+
278
+ const lines = String(markdown || "").split("\n");
279
+ const sectionStarts = [];
280
+ for (let i = 0; i < lines.length; i++) {
281
+ if (/^###\s+T\d+\s*[—-]/.test(lines[i]) || /^###\s+T\d+$/.test(lines[i])) {
282
+ sectionStarts.push(i);
283
+ }
284
+ }
285
+ if (sectionStarts.length === 0) return null;
286
+
287
+ const usedKeys = new Set();
288
+ const tasks = [];
289
+
290
+ for (let si = 0; si < sectionStarts.length; si++) {
291
+ const start = sectionStarts[si];
292
+ const end = si + 1 < sectionStarts.length ? sectionStarts[si + 1] : lines.length;
293
+ const sectionLines = lines.slice(start, end);
294
+
295
+ const headerMatch = sectionLines[0].match(/^###\s+T\d+\s*[—-]\s*(.+)$/) || sectionLines[0].match(/^###\s+(T\d+)$/);
296
+ const title = headerMatch ? headerMatch[1].trim() : sectionLines[0].replace(/^###\s*/, "").trim();
297
+
298
+ let owner = defaultOwner;
299
+ let phase = defaultPhase;
300
+ let files = [];
301
+ let dependsOn = [];
302
+ let readFirst = defaultReadFirst.slice();
303
+ let doNotChange = defaultDoNotChange.slice();
304
+ let acceptanceCriteria = [];
305
+ let inAcceptanceCriteria = false;
306
+
307
+ for (let li = 1; li < sectionLines.length; li++) {
308
+ const line = sectionLines[li];
309
+ const trimmed = line.trim();
310
+ const field = trimmed.replace(/^[-*]\s+/, "");
311
+
312
+ if (/^\*\*Owner\*\*\s*:/.test(field)) {
313
+ owner = field.replace(/^\*\*Owner\*\*\s*:\s*/, "").trim() || defaultOwner;
314
+ inAcceptanceCriteria = false;
315
+ continue;
316
+ }
317
+ if (/^\*\*Phase\*\*\s*:/.test(field)) {
318
+ phase = normalizePhase(field.replace(/^\*\*Phase\*\*\s*:\s*/, "").trim()) || defaultPhase;
319
+ inAcceptanceCriteria = false;
320
+ continue;
321
+ }
322
+ if (/^\*\*Files\*\*\s*:/.test(field)) {
323
+ files = parseCommaSeparated(field.replace(/^\*\*Files\*\*\s*:\s*/, ""));
324
+ inAcceptanceCriteria = false;
325
+ continue;
326
+ }
327
+ if (/^\*\*Depends on\*\*\s*:/.test(field)) {
328
+ const raw = field.replace(/^\*\*Depends on\*\*\s*:\s*/, "").trim();
329
+ dependsOn = raw.toLowerCase() === "(none)" ? [] : parseCommaSeparated(raw);
330
+ inAcceptanceCriteria = false;
331
+ continue;
332
+ }
333
+ if (/^\*\*Read first\*\*\s*:/.test(field)) {
334
+ readFirst = parseCommaSeparated(field.replace(/^\*\*Read first\*\*\s*:\s*/, ""));
335
+ inAcceptanceCriteria = false;
336
+ continue;
337
+ }
338
+ if (/^\*\*Do not change\*\*\s*:/.test(field)) {
339
+ doNotChange = parseCommaSeparated(field.replace(/^\*\*Do not change\*\*\s*:\s*/, ""));
340
+ inAcceptanceCriteria = false;
341
+ continue;
342
+ }
343
+ if (/^\*\*Acceptance criteria\*\*\s*:/.test(field)) {
344
+ inAcceptanceCriteria = true;
345
+ continue;
346
+ }
347
+ if (inAcceptanceCriteria && /^[-*]\s+/.test(trimmed)) {
348
+ acceptanceCriteria.push(trimmed.replace(/^[-*]\s+/, "").trim());
349
+ continue;
350
+ }
351
+ if (inAcceptanceCriteria && trimmed && !/^[-*]/.test(trimmed) && !/^\*\*/.test(trimmed)) {
352
+ inAcceptanceCriteria = false;
353
+ }
354
+ }
355
+
356
+ if (acceptanceCriteria.length === 0) {
357
+ acceptanceCriteria = defaultAcceptanceCriteria.length
358
+ ? defaultAcceptanceCriteria
359
+ : [`Complete: ${title}`];
360
+ } else if (defaultAcceptanceCriteria.length) {
361
+ acceptanceCriteria = [...acceptanceCriteria, ...defaultAcceptanceCriteria];
362
+ }
363
+
364
+ const key = slugifyTitle(title, usedKeys);
365
+ usedKeys.add(key);
366
+
367
+ tasks.push({ title, phase, owner, files, dependsOn, readFirst, doNotChange, acceptanceCriteria });
368
+ }
369
+ return tasks;
370
+ }
371
+
372
+ export function extractTasksFromPlan(markdown, options = {}) {
373
+ const rich = parseRichSections(markdown, options);
374
+ if (rich !== null) return rich;
375
+
376
+ const phase = normalizePhase(options.phase || "implement");
377
+ const owner = options.owner || "cx-engineer";
378
+ const readFirst = options.readFirst || [];
379
+ const doNotChange = options.doNotChange || [];
380
+ const acceptanceCriteria = options.acceptanceCriteria || [];
381
+ return String(markdown || "")
382
+ .split("\n")
383
+ .map((line) => line.trim())
384
+ .map((line) => {
385
+ const checkbox = /^[-*]\s+\[[ xX]\]\s+(.+)$/.exec(line);
386
+ if (checkbox) return checkbox[1].trim();
387
+ const numbered = /^\d+\.\s+(.+)$/.exec(line);
388
+ if (numbered) return numbered[1].trim();
389
+ return null;
390
+ })
391
+ .filter(Boolean)
392
+ .map((title) => ({
393
+ title,
394
+ phase,
395
+ owner,
396
+ readFirst,
397
+ doNotChange,
398
+ acceptanceCriteria: acceptanceCriteria.length ? acceptanceCriteria : [`Complete: ${title}`],
399
+ }));
400
+ }
401
+
402
+ export function addTasksFromPlan(root, markdown, options = {}) {
403
+ const tasks = extractTasksFromPlan(markdown, options);
404
+ let workflow = loadWorkflow(root) || saveWorkflow(defaultWorkflow(root, options.workflowTitle || "Imported plan", options.specRef || null), root);
405
+ if (options.specRef && !workflow.specRef) {
406
+ workflow.specRef = options.specRef;
407
+ workflow = saveWorkflow(workflow, root);
408
+ }
409
+ if (options.phase && PHASES.includes(options.phase)) {
410
+ workflow = transitionPhase(root, options.phase);
411
+ }
412
+ for (const task of tasks) {
413
+ workflow = addTask(root, task);
414
+ }
415
+ return { workflow, count: tasks.length };
416
+ }
417
+
418
+ export function updateTask(root, key, patch) {
419
+ const workflow = loadWorkflow(root);
420
+ if (!workflow) throw new Error("No .cx/workflow.json found. Run `construct workflow init` first.");
421
+ const cleanPatch = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
422
+ let found = false;
423
+ workflow.tasks = (workflow.tasks || []).map((task) => {
424
+ if (task.key !== key) return task;
425
+ found = true;
426
+ const notes = cleanPatch.note
427
+ ? [...(task.notes || []), { at: now(), note: cleanPatch.note }]
428
+ : task.notes || [];
429
+ return {
430
+ ...task,
431
+ ...cleanPatch,
432
+ notes,
433
+ updatedAt: now()
434
+ };
435
+ });
436
+ if (!found) throw new Error(`Task not found: ${key}`);
437
+ if (cleanPatch.status && !VALID_STATUS.has(cleanPatch.status)) throw new Error(`Invalid status: ${cleanPatch.status}`);
438
+ if (cleanPatch.status === "done") {
439
+ const task = workflow.tasks.find((t) => t.key === key);
440
+ if (task && task.phase === "implement" && workflow.alignment?.verificationRequiredBeforeDone !== false) {
441
+ const mergedVerification = cleanPatch.verification ?? task.verification ?? [];
442
+ if (mergedVerification.length === 0) {
443
+ throw new Error(
444
+ `Cannot mark implement-phase task "${key}" as done without verification evidence. ` +
445
+ `Add cx-reviewer and cx-qa results to task.verification before marking done.`
446
+ );
447
+ }
448
+ }
449
+ }
450
+ if (cleanPatch.status === "in-progress" || cleanPatch.status === "blocked_needs_user") workflow.currentTaskKey = key;
451
+ if (cleanPatch.status === "done" && workflow.currentTaskKey === key) {
452
+ workflow.currentTaskKey = (workflow.tasks || []).find((task) => task.status !== "done" && task.status !== "skipped")?.key || null;
453
+ }
454
+ return saveWorkflow(workflow, root);
455
+ }
456
+
457
+ export function transitionPhase(root, phase, status = "in-progress") {
458
+ phase = normalizePhase(phase);
459
+ if (!phase) throw new Error(`Invalid phase: ${phase}`);
460
+ const workflow = loadWorkflow(root);
461
+ if (!workflow) throw new Error("No .cx/workflow.json found. Run `construct workflow init` first.");
462
+ workflow.phase = phase;
463
+ workflow.phases = workflow.phases || {};
464
+ for (const p of PHASES) {
465
+ workflow.phases[p] = workflow.phases[p] || { owner: p, status: "todo" };
466
+ }
467
+ workflow.phases[phase] = { ...workflow.phases[phase], status };
468
+ return saveWorkflow(workflow, root);
469
+ }
470
+
471
+ export function alignmentFindings(workflow) {
472
+ if (!workflow) {
473
+ return [{
474
+ severity: "HIGH",
475
+ issue: "No .cx/workflow.json found",
476
+ fix: "Run `construct workflow init \"<title>\"` at the project root."
477
+ }];
478
+ }
479
+ const findings = [];
480
+ const tasks = workflow.tasks || [];
481
+ const current = tasks.find((task) => task.key === workflow.currentTaskKey);
482
+ if (current && current.phase !== workflow.phase) {
483
+ findings.push({
484
+ severity: "HIGH",
485
+ task: current.key,
486
+ issue: `Current task phase (${current.phase}) does not match workflow phase (${workflow.phase})`,
487
+ fix: `Run \`construct workflow phase ${current.phase}\` or move the task to the active phase.`
488
+ });
489
+ }
490
+ if (workflow.status === "in-progress" && tasks.length === 0) {
491
+ findings.push({
492
+ severity: "MEDIUM",
493
+ issue: "Workflow has no tasks",
494
+ fix: "Add scoped tasks with owners, read-first files, protected files, and acceptance criteria."
495
+ });
496
+ }
497
+ for (const task of tasks) {
498
+ if (!task.owner) findings.push({ severity: "HIGH", task: task.key, issue: "Task has no owner", fix: "Set owner to a persona or cx-specialist." });
499
+ if (!Array.isArray(task.acceptanceCriteria) || task.acceptanceCriteria.length === 0) {
500
+ findings.push({ severity: "HIGH", task: task.key, issue: "Task has no acceptance criteria", fix: "Add binary pass/fail acceptance criteria." });
501
+ }
502
+ if (!Array.isArray(task.readFirst) || task.readFirst.length === 0) {
503
+ findings.push({ severity: "MEDIUM", task: task.key, issue: "Task has no readFirst list", fix: "Add files, docs, or memory queries to inspect before work." });
504
+ }
505
+ if (!Array.isArray(task.doNotChange) || task.doNotChange.length === 0) {
506
+ findings.push({ severity: "MEDIUM", task: task.key, issue: "Task has no doNotChange list", fix: "Add explicit drift boundaries." });
507
+ }
508
+ if (task.status === "done" && (!Array.isArray(task.verification) || task.verification.length === 0)) {
509
+ findings.push({ severity: "HIGH", task: task.key, issue: "Done task has no verification evidence", fix: "Record commands, checks, or review evidence before marking done." });
510
+ }
511
+ for (const dep of task.dependsOn || []) {
512
+ const depTask = tasks.find((candidate) => candidate.key === dep);
513
+ if (!depTask) findings.push({ severity: "HIGH", task: task.key, issue: `Unknown dependency ${dep}`, fix: "Remove or correct dependsOn." });
514
+ if (depTask && task.status === "in-progress" && !["done", "skipped"].includes(depTask.status)) {
515
+ findings.push({ severity: "HIGH", task: task.key, issue: `Started before dependency ${dep} completed`, fix: "Finish dependency first or revise task graph." });
516
+ }
517
+ }
518
+ }
519
+ return findings;
520
+ }
521
+
522
+ export function validateWorkflowState(workflow) {
523
+ const errors = [];
524
+ if (!workflow || typeof workflow !== "object") {
525
+ return { valid: false, errors: ["workflow: must be an object"] };
526
+ }
527
+ if (workflow.version !== 1) errors.push("workflow.version: must be 1");
528
+ if (!workflow.id || typeof workflow.id !== "string") errors.push("workflow.id: must be a non-empty string");
529
+ if (!workflow.title || typeof workflow.title !== "string") errors.push("workflow.title: must be a non-empty string");
530
+ if (!normalizePhase(workflow.phase)) errors.push(`workflow.phase: invalid phase '${workflow.phase}'`);
531
+ if (!["in-progress", "blocked", "done", "skipped"].includes(workflow.status)) {
532
+ errors.push(`workflow.status: invalid status '${workflow.status}'`);
533
+ }
534
+ if (!Array.isArray(workflow.tasks)) errors.push("workflow.tasks: must be an array");
535
+
536
+ const tasks = Array.isArray(workflow.tasks) ? workflow.tasks : [];
537
+ const keys = new Set();
538
+ for (const task of tasks) {
539
+ if (!task.key || typeof task.key !== "string") errors.push("task: key must be a non-empty string");
540
+ else if (keys.has(task.key)) errors.push(`${task.key}: duplicate task key`);
541
+ else keys.add(task.key);
542
+
543
+ if (!task.title || typeof task.title !== "string") errors.push(`${task.key || "task"}: title must be a non-empty string`);
544
+ if (!normalizePhase(task.phase)) errors.push(`${task.key || "task"}: invalid phase '${task.phase}'`);
545
+ if (!task.owner || typeof task.owner !== "string") errors.push(`${task.key || "task"}: owner must be a non-empty string`);
546
+ if (!VALID_STATUS.has(task.status)) errors.push(`${task.key || "task"}: invalid status '${task.status}'`);
547
+ if (!Array.isArray(task.dependsOn)) errors.push(`${task.key || "task"}: dependsOn must be an array`);
548
+ if (!Array.isArray(task.readFirst)) errors.push(`${task.key || "task"}: readFirst must be an array`);
549
+ if (!Array.isArray(task.doNotChange)) errors.push(`${task.key || "task"}: doNotChange must be an array`);
550
+ if (!Array.isArray(task.acceptanceCriteria)) errors.push(`${task.key || "task"}: acceptanceCriteria must be an array`);
551
+ if (!Array.isArray(task.verification)) errors.push(`${task.key || "task"}: verification must be an array`);
552
+ if (task.overlays !== undefined && !Array.isArray(task.overlays)) errors.push(`${task.key || "task"}: overlays must be an array`);
553
+ }
554
+
555
+ for (const finding of alignmentFindings(workflow)) {
556
+ if (finding.severity === "HIGH") errors.push(`${finding.task ? `${finding.task}: ` : ""}${finding.issue}`);
557
+ }
558
+
559
+ return { valid: errors.length === 0, errors };
560
+ }
561
+
562
+ export function summarizeWorkflow(workflow) {
563
+ if (!workflow) return "No workflow state found.";
564
+ const tasks = workflow.tasks || [];
565
+ const done = tasks.filter((task) => task.status === "done" || task.status === "skipped").length;
566
+ const blocked = tasks.filter((task) => task.status === "blocked").length;
567
+ const current = tasks.find((task) => task.key === workflow.currentTaskKey);
568
+ const lines = [
569
+ `${workflow.title} (${workflow.id})`,
570
+ `Status: ${workflow.status} | Phase: ${workflow.phase} | Tasks: ${done}/${tasks.length} complete${blocked ? ` | Blocked: ${blocked}` : ""}`
571
+ ];
572
+ if (current) lines.push(`Current: ${current.key} ${current.title} -> ${current.owner} [${current.status}]`);
573
+ return lines.join("\n");
574
+ }
575
+
576
+ export function inspectWorkflowHealth(workflow, { cwd = process.cwd() } = {}) {
577
+ const findings = alignmentFindings(workflow);
578
+ const activeTask = workflow?.tasks?.find((task) => task.key === workflow?.currentTaskKey) || null;
579
+ const highSeverityCount = findings.filter((finding) => finding.severity === 'HIGH').length;
580
+ const alignmentStatus = workflow
581
+ ? findings.length === 0
582
+ ? 'pass'
583
+ : highSeverityCount > 0 ? 'fail' : 'warn'
584
+ : 'missing';
585
+
586
+ return {
587
+ cwd,
588
+ exists: Boolean(workflow),
589
+ phase: workflow?.phase ?? null,
590
+ lifecycleStatus: workflow?.status ?? null,
591
+ currentTaskKey: workflow?.currentTaskKey ?? null,
592
+ summary: summarizeWorkflow(workflow),
593
+ activeTask: activeTask
594
+ ? {
595
+ key: activeTask.key ?? null,
596
+ title: activeTask.title ?? null,
597
+ phase: activeTask.phase ?? null,
598
+ owner: activeTask.owner ?? null,
599
+ status: activeTask.status ?? null,
600
+ }
601
+ : {
602
+ key: null,
603
+ title: null,
604
+ phase: null,
605
+ owner: null,
606
+ status: null,
607
+ },
608
+ alignment: {
609
+ status: alignmentStatus,
610
+ findings,
611
+ findingCount: findings.length,
612
+ highSeverityCount,
613
+ },
614
+ };
615
+ }
616
+
617
+ function parseOptions(args) {
618
+ const result = { _: [] };
619
+ for (const arg of args) {
620
+ if (!arg.startsWith("--")) {
621
+ result._.push(arg);
622
+ continue;
623
+ }
624
+ const [key, raw = "true"] = arg.slice(2).split("=");
625
+ result[key] = raw;
626
+ }
627
+ return result;
628
+ }
629
+
630
+ function splitList(value) {
631
+ if (!value) return [];
632
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
633
+ }
634
+
635
+ function printStatus(root) {
636
+ const workflow = loadWorkflow(root);
637
+ console.log(summarizeWorkflow(workflow));
638
+ if (!workflow) return;
639
+ for (const task of workflow.tasks || []) {
640
+ console.log(` ${task.key.padEnd(8)} ${task.status.padEnd(11)} ${task.phase.padEnd(8)} ${task.owner.padEnd(18)} ${task.title}`);
641
+ }
642
+ }
643
+
644
+ function printAlign(root) {
645
+ const workflow = loadWorkflow(root);
646
+ const findings = alignmentFindings(workflow);
647
+ if (workflow) console.log(summarizeWorkflow(workflow));
648
+ if (findings.length === 0) {
649
+ console.log("Alignment: PASS");
650
+ return;
651
+ }
652
+ console.log(`Alignment: ${findings.some((f) => f.severity === "HIGH") ? "FAIL" : "WARN"}`);
653
+ for (const finding of findings) {
654
+ const prefix = finding.task ? `${finding.severity} ${finding.task}` : finding.severity;
655
+ console.log(` ${prefix}: ${finding.issue}`);
656
+ console.log(` fix: ${finding.fix}`);
657
+ }
658
+ }
659
+
660
+ export function approveWorkflow(root = process.cwd(), note = "Approved by Executive") {
661
+ const workflow = loadWorkflow(root);
662
+ if (!workflow) throw new Error("Workflow not found");
663
+
664
+ workflow.status = "in-progress";
665
+ workflow.updatedAt = now();
666
+
667
+ const phase = workflow.phase;
668
+ if (workflow.phases[phase]) {
669
+ workflow.phases[phase].status = "executive-approved";
670
+ if (!workflow.phases[phase].notes) workflow.phases[phase].notes = [];
671
+ workflow.phases[phase].notes.push({ date: now(), text: note });
672
+ }
673
+
674
+ saveWorkflow(root, workflow);
675
+ return workflow;
676
+ }
677
+
678
+ export function approveTask(root = process.cwd(), key, note = "Approved by Executive") {
679
+ const workflow = loadWorkflow(root);
680
+ if (!workflow) throw new Error("Workflow not found");
681
+
682
+ const task = workflow.tasks.find(t => t.key === key);
683
+ if (!task) throw new Error(`Task ${key} not found`);
684
+
685
+ task.status = "todo";
686
+ if (!task.notes) task.notes = [];
687
+ task.notes.push({ date: now(), text: note });
688
+
689
+ workflow.updatedAt = now();
690
+ saveWorkflow(root, workflow);
691
+ return workflow;
692
+ }
693
+ export function runWorkflowCli(argv = process.argv.slice(2), root = process.cwd()) {
694
+ const [command = "status", ...rest] = argv;
695
+ const options = parseOptions(rest);
696
+ if (command === "init") {
697
+ const title = options._.join(" ") || options.title || "Untitled workflow";
698
+ const { workflow, created } = initWorkflow(root, title);
699
+ console.log(`${created ? "Created" : "Existing"} .cx/workflow.json`);
700
+ console.log(summarizeWorkflow(workflow));
701
+ return;
702
+ }
703
+ if (command === "status") {
704
+ printStatus(root);
705
+ return;
706
+ }
707
+ if (command === "add") {
708
+ const title = options.title || options._.join(" ");
709
+ if (!title) throw new Error("Usage: construct workflow add --title=\"...\" [--phase=implement] [--owner=cx-engineer]");
710
+ const workflow = addTask(root, {
711
+ title,
712
+ phase: options.phase,
713
+ owner: options.owner,
714
+ files: splitList(options.files),
715
+ readFirst: splitList(options.readFirst),
716
+ doNotChange: splitList(options.doNotChange),
717
+ acceptanceCriteria: splitList(options.acceptance),
718
+ verification: splitList(options.verification),
719
+ dependsOn: splitList(options.dependsOn),
720
+ tokenBudget: options.tokenBudget ? Number(options.tokenBudget) : undefined
721
+ });
722
+ console.log("Task added.");
723
+ console.log(summarizeWorkflow(workflow));
724
+ return;
725
+ }
726
+ if (command === "from-plan") {
727
+ const file = options._[0] || options.file;
728
+ if (!file) throw new Error("Usage: construct workflow from-plan plan.md [--phase=implement] [--owner=cx-engineer]");
729
+ const markdown = fs.readFileSync(path.resolve(root, file), "utf8");
730
+ const { workflow, count } = addTasksFromPlan(root, markdown, {
731
+ phase: options.phase,
732
+ owner: options.owner,
733
+ readFirst: splitList(options.readFirst),
734
+ doNotChange: splitList(options.doNotChange),
735
+ acceptanceCriteria: splitList(options.acceptance),
736
+ workflowTitle: options.title,
737
+ });
738
+ console.log(`Imported ${count} task${count === 1 ? "" : "s"} from ${file}.`);
739
+ console.log(summarizeWorkflow(workflow));
740
+ return;
741
+ }
742
+ if (command === "task") {
743
+ const key = options.task || options.key || options._[0];
744
+ if (!key) throw new Error("Usage: construct workflow task todo:1 --status=in-progress [--note=\"...\"]");
745
+ const workflow = updateTask(root, key, {
746
+ status: options.status,
747
+ owner: options.owner,
748
+ phase: options.phase,
749
+ note: options.note,
750
+ verification: options.verification ? splitList(options.verification) : undefined,
751
+ overlays: options.overlays ? splitList(options.overlays) : undefined,
752
+ challengeRequired: options.challengeRequired !== undefined ? options.challengeRequired === "true" : undefined,
753
+ challengeStatus: options.challengeStatus !== undefined ? options.challengeStatus : undefined,
754
+ });
755
+ console.log("Task updated.");
756
+ console.log(summarizeWorkflow(workflow));
757
+ return;
758
+ }
759
+ if (command === "phase") {
760
+ const phase = options.phase || options._[0];
761
+ if (!phase) throw new Error("Usage: construct workflow phase implement [--status=in-progress]");
762
+ const workflow = transitionPhase(root, phase, options.status || "in-progress");
763
+ console.log("Phase updated.");
764
+ console.log(summarizeWorkflow(workflow));
765
+ return;
766
+ }
767
+ if (command === "align") {
768
+ printAlign(root);
769
+ return;
770
+ }
771
+ if (command === "approve") {
772
+ const workflow = approveWorkflow(root, options.note);
773
+ console.log("Workflow approved by executive.");
774
+ console.log(summarizeWorkflow(workflow));
775
+ return;
776
+ }
777
+ if (command === "approve-task") {
778
+ const key = options.task || options.key || options._[0];
779
+ const workflow = approveTask(root, key, options.note);
780
+ console.log(`Task ${key} approved by executive.`);
781
+ console.log(summarizeWorkflow(workflow));
782
+ return;
783
+ }
784
+ throw new Error(`Unknown workflow subcommand: ${command}`);
785
+ }
786
+
787
+ if (import.meta.url === `file://${process.argv[1]}`) {
788
+ try {
789
+ runWorkflowCli(process.argv.slice(2), process.cwd());
790
+ } catch (error) {
791
+ console.error(error.message);
792
+ process.exit(1);
793
+ }
794
+ }