@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,170 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { loadProjectConfig } from '../project.mjs';
4
+ import { readPolicyBaselineStatus } from '../organisation-policies.mjs';
5
+ import {
6
+ confirmPolicyFacts, evaluatePolicyDesign, readPolicyEvaluationStatus,
7
+ recordPolicyException, recordPolicyReview,
8
+ } from '../policy-design-gates.mjs';
9
+ import { POLICY_DESIGN_AUTHORITY_NOTICE } from '../policy-gate-integration.mjs';
10
+ import { listRuntimeIntents, readRuntimeIntent } from './intents.mjs';
11
+ import { selectPolicyPersonas } from './persona-engagement.mjs';
12
+ import { publishLifecycleEventSafely } from './lifecycle-hooks.mjs';
13
+
14
+ const actions = Object.freeze({
15
+ facts: { event: 'ewai.policy.facts.confirmed', allowed: ['intentReference', 'expectedRevision', 'expectedProposalDigest', 'confirmed', 'authority', 'confirmedBy', 'decisions'] },
16
+ evaluation: { event: 'ewai.policy.evaluation.recorded', allowed: ['intentReference', 'expectedIntentRevision', 'expectedPolicyDigest', 'expectedFactsDigest'] },
17
+ review: { event: 'ewai.policy.review.recorded', allowed: ['intentReference', 'evaluationDigest', 'ruleId', 'authority', 'reviewedBy', 'reviewerRole', 'decision', 'rationale', 'evidence', 'controls'] },
18
+ exception: { event: 'ewai.policy.exception.recorded', allowed: ['intentReference', 'evaluationDigest', 'ruleId', 'authority', 'approvedBy', 'owner', 'reviewerRole', 'scope', 'rationale', 'compensatingControls', 'evidence', 'expiresAt'] },
19
+ });
20
+
21
+ export { POLICY_DESIGN_AUTHORITY_NOTICE };
22
+
23
+ function exact(input, allowed, label) {
24
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error(`${label} must be an object`);
25
+ const unsupported = Object.keys(input).filter((key) => !allowed.includes(key));
26
+ if (unsupported.length) throw new Error(`${label} contains unsupported field(s): ${unsupported.join(', ')}`);
27
+ return input;
28
+ }
29
+
30
+ function safeReference(value) {
31
+ const text = String(value ?? '').trim();
32
+ const segments = text.split(/[\\/]+/);
33
+ return isAbsolute(text) || text.includes('/private/') || text.includes('\\') || segments.includes('..')
34
+ ? '[redacted-local-reference]'
35
+ : text.slice(0, 500);
36
+ }
37
+
38
+ function safeFacts(record) {
39
+ if (!record) return null;
40
+ return {
41
+ revision: record.revision, intent: record.intent, policyDigest: record.policyDigest,
42
+ proposalDigest: record.proposalDigest, factsDigest: record.factsDigest,
43
+ facts: (record.facts ?? []).map((fact) => ({
44
+ id: fact.id, dimension: fact.dimension, values: fact.values, rationale: fact.rationale,
45
+ provenance: { ...fact.provenance, reference: safeReference(fact.provenance?.reference), ...(fact.provenance?.persona ? { persona: fact.provenance.persona } : {}) },
46
+ })),
47
+ confirmation: record.confirmation,
48
+ authority: record.authority,
49
+ };
50
+ }
51
+
52
+ function factsRecord(projectRoot, reference) {
53
+ const { paths } = loadProjectConfig(projectRoot);
54
+ const path = resolve(paths.specsRoot, '3.Evidence/policy/facts', `${reference}.json`);
55
+ if (!existsSync(path)) return null;
56
+ try { return safeFacts(JSON.parse(readFileSync(path, 'utf8'))); } catch { return null; }
57
+ }
58
+
59
+ function evaluationRecord(projectRoot, reference, expectedDigest) {
60
+ if (!expectedDigest) return null;
61
+ const { paths } = loadProjectConfig(projectRoot);
62
+ const path = resolve(paths.specsRoot, '3.Evidence/policy/evaluations', `${reference}.json`);
63
+ if (!existsSync(path)) return null;
64
+ try {
65
+ const record = JSON.parse(readFileSync(path, 'utf8'));
66
+ return record.schema === 'ewai.policy-design-evaluation/v1' && record.evaluationDigest === expectedDigest
67
+ ? record
68
+ : null;
69
+ } catch { return null; }
70
+ }
71
+
72
+ function authority() {
73
+ return { advisory: true, designTimeOnly: true, productionEnforcement: false, productionCodeExecution: false, buildApproved: false, manualQaApproved: false, complianceCertified: false, releaseApproved: false, residualRiskAccepted: false };
74
+ }
75
+
76
+ function chosenIntent(projectRoot, reference = '') {
77
+ if (reference) return readRuntimeIntent(projectRoot, reference);
78
+ return listRuntimeIntents(projectRoot)[0] ?? null;
79
+ }
80
+
81
+ export function readPolicyWorkspace(projectRoot, options = {}) {
82
+ if (!['business', 'technical'].includes(options.mode ?? 'business')) throw new Error('Policy workspace mode must be business or technical');
83
+ const intent = chosenIntent(projectRoot, String(options.intentReference ?? ''));
84
+ const baseline = readPolicyBaselineStatus(projectRoot);
85
+ const reference = intent?.id ?? '';
86
+ const policyStatus = reference && baseline.status !== 'not-configured'
87
+ ? readPolicyEvaluationStatus(projectRoot, reference, options)
88
+ : { schema: 'ewai.policy-evaluation-status/v1', status: baseline.status, blocking: false, controls: [] };
89
+ const evaluation = reference ? evaluationRecord(projectRoot, reference, policyStatus.evaluationDigest) : null;
90
+ const policy = {
91
+ ...policyStatus,
92
+ matchedRules: evaluation?.matchedRules ?? [],
93
+ requiredReviews: evaluation?.requiredReviews ?? [],
94
+ unmatchedPolicies: evaluation?.unmatchedPolicies ?? [],
95
+ };
96
+ const facts = reference ? factsRecord(projectRoot, reference) : null;
97
+ const dimensions = [...new Set((facts?.facts ?? []).map(({ dimension }) => dimension))];
98
+ const engagement = selectPolicyPersonas({ stage: options.stage ?? 'facts', dimensions, context: { intent: reference, status: policy.status } }, options.personas ?? []);
99
+ const activePersonas = engagement.activePersonas.map(({ id, name, tier, category, description, matchedSignals, engagementReason }) => ({ id, name, tier, category, description, matchedSignals, engagementReason, advisory: true }));
100
+ const status = policy.status ?? baseline.status;
101
+ const blocking = baseline.status === 'not-configured' ? false : policy.blocking !== false;
102
+ const shared = { intentReference: reference, status, blocking };
103
+ return {
104
+ schema: 'ewai.policy-workspace/v1', ...shared,
105
+ baseline: { status: baseline.status, enabled: baseline.enabled, blocking: baseline.blocking, effectiveDigest: baseline.effectiveDigest ?? null, policyCount: baseline.policyCount ?? 0, ruleCount: baseline.ruleCount ?? 0, approvedBy: baseline.approvedBy ?? null, approvedAt: baseline.approvedAt ?? null },
106
+ facts, policy, activePersonas, personaAvailability: { baseline: engagement.baseline, premium: engagement.premium },
107
+ views: {
108
+ business: { ...shared, outcome: status, nextActions: blocking ? ['Resolve the current policy evidence and named decisions.'] : [], controls: (policy.controls ?? []).map(({ ruleId, id, title, description }) => ({ ruleId, id, title, description })) },
109
+ technical: { ...shared, policyDigest: policy.policyDigest ?? baseline.effectiveDigest ?? null, factsDigest: policy.factsDigest ?? null, evaluationDigest: policy.evaluationDigest ?? null, matchedRules: policy.matchedRules ?? [], controls: policy.controls ?? [] },
110
+ },
111
+ notices: [POLICY_DESIGN_AUTHORITY_NOTICE], authority: authority(),
112
+ };
113
+ }
114
+
115
+ export function policyLifecycleEvent(projectRoot, kind, record, options = {}) {
116
+ const definition = actions[kind];
117
+ if (!definition) throw new Error(`Unsupported policy lifecycle event: ${kind}`);
118
+ if (!record || typeof record !== 'object') throw new Error('Policy lifecycle event requires a persisted policy record');
119
+ const reference = record.intentReference ?? record.intent?.reference;
120
+ const revision = record.evaluationDigest ?? record.factsDigest ?? record.decisionDigest;
121
+ if (!reference || !revision) throw new Error('Policy lifecycle event requires a persisted record identity');
122
+ const status = readPolicyEvaluationStatus(projectRoot, reference);
123
+ const persisted = kind === 'facts'
124
+ ? factsRecord(projectRoot, reference)?.factsDigest === record.factsDigest
125
+ : kind === 'evaluation'
126
+ ? status.evaluationDigest === record.evaluationDigest
127
+ : kind === 'review'
128
+ ? status.reviews?.some(({ decisionDigest }) => decisionDigest === record.decisionDigest)
129
+ : status.exceptions?.some(({ decisionDigest }) => decisionDigest === record.decisionDigest);
130
+ if (!persisted) throw new Error('Policy lifecycle event requires the exact persisted policy record');
131
+ const personas = options.personas ?? [];
132
+ return publishLifecycleEventSafely(projectRoot, definition.event, {
133
+ sourceKey: `policy:${kind}:${reference}:${revision}`,
134
+ sourceRevision: revision,
135
+ scope: { intent: reference },
136
+ facts: {
137
+ status: kind === 'evaluation' ? record.governingOutcome : kind === 'review' ? record.decision : kind === 'exception' ? 'approved' : 'confirmed',
138
+ intentReference: reference,
139
+ evidenceDigest: revision,
140
+ ...(record.ruleId ? { ruleId: record.ruleId } : {}),
141
+ },
142
+ personas,
143
+ streamId: `intent:${reference}`,
144
+ });
145
+ }
146
+
147
+ function resultAfter(projectRoot, kind, record, options) {
148
+ const workspace = readPolicyWorkspace(projectRoot, { intentReference: record.intentReference ?? record.intent?.reference, personas: options.personas, stage: kind });
149
+ return { record, lifecycle: policyLifecycleEvent(projectRoot, kind, record, { personas: workspace.activePersonas }), workspace };
150
+ }
151
+
152
+ export function confirmPolicyFactsAction(projectRoot, input, options = {}) {
153
+ exact(input, actions.facts.allowed, 'Policy fact confirmation request');
154
+ return resultAfter(projectRoot, 'facts', confirmPolicyFacts(projectRoot, input, options), options);
155
+ }
156
+
157
+ export function evaluatePolicyDesignAction(projectRoot, input, options = {}) {
158
+ exact(input, actions.evaluation.allowed, 'Policy evaluation request');
159
+ return resultAfter(projectRoot, 'evaluation', evaluatePolicyDesign(projectRoot, input, options), options);
160
+ }
161
+
162
+ export function recordPolicyReviewAction(projectRoot, input, options = {}) {
163
+ exact(input, actions.review.allowed, 'Policy review request');
164
+ return resultAfter(projectRoot, 'review', recordPolicyReview(projectRoot, input, options), options);
165
+ }
166
+
167
+ export function recordPolicyExceptionAction(projectRoot, input, options = {}) {
168
+ exact(input, actions.exception.allowed, 'Policy exception request');
169
+ return resultAfter(projectRoot, 'exception', recordPolicyException(projectRoot, input, options), options);
170
+ }
@@ -0,0 +1,235 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve } from 'node:path';
3
+ import { atomicJson } from '../delivery-documents.mjs';
4
+ import { projectPaths } from '../paths.mjs';
5
+ import {
6
+ comparePrototypeReviews,
7
+ preparePrototypeCycleReview,
8
+ preparePrototypePlanReview,
9
+ prototypeFindingId,
10
+ readPrototypeReviewStatus,
11
+ recordPrototypeCycleReview,
12
+ recordPrototypePlanReview,
13
+ } from '../prototype-iterations.mjs';
14
+ import { runtimePaths } from './paths.mjs';
15
+ import { selectPrototypeReviewPersonas } from './persona-engagement.mjs';
16
+
17
+ const WORKSPACE_SCHEMA = 'ewai.prototype-iteration-workspace/v1';
18
+ const slugPattern = /^[a-z0-9][a-z0-9._-]{0,79}$/;
19
+
20
+ function fail(message, statusCode = 409) {
21
+ const error = new Error(`Prototype iteration workspace: ${message}`);
22
+ error.statusCode = statusCode;
23
+ throw error;
24
+ }
25
+
26
+ function object(value, label) {
27
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be an object`, 400);
28
+ return value;
29
+ }
30
+
31
+ function knownFields(value, allowed, label) {
32
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${label} has unknown field ${key}`, 400);
33
+ }
34
+
35
+ function safeSlug(value) {
36
+ const slug = String(value ?? '').trim();
37
+ if (!slugPattern.test(slug)) fail('delivery slug must be a bounded lowercase identifier', 400);
38
+ return slug;
39
+ }
40
+
41
+ function readJson(path, label) {
42
+ if (!existsSync(path)) fail(`${label} has not been prepared`);
43
+ if (lstatSync(path).isSymbolicLink()) fail(`${label} may not be a symbolic link`);
44
+ try {
45
+ return JSON.parse(readFileSync(path, 'utf8'));
46
+ } catch (error) {
47
+ fail(`${label} is not valid JSON: ${error.message}`);
48
+ }
49
+ }
50
+
51
+ function resolveAssessmentReferences(submission) {
52
+ const request = structuredClone(object(submission, 'review submission'));
53
+ if (!Array.isArray(request.assessments)) return request;
54
+ request.assessments = request.assessments.map((assessment, index) => {
55
+ const item = object(assessment, `assessment ${index + 1}`);
56
+ const allowed = ['findingId', 'findingRef', 'disposition', 'rationale', 'modification', 'owner'];
57
+ knownFields(item, allowed, `assessment ${index + 1}`);
58
+ const findingId = item.findingId || (item.findingRef ? prototypeFindingId(item.findingRef) : '');
59
+ if (!findingId) fail(`assessment ${index + 1} requires findingId or findingRef`, 400);
60
+ const { findingRef: _findingRef, ...rest } = item;
61
+ return { ...rest, findingId };
62
+ });
63
+ return request;
64
+ }
65
+
66
+ function preparationFiles(projectRoot, slug) {
67
+ const folder = runtimePaths(projectRoot).prototypeIterationRoot(slug);
68
+ if (!existsSync(folder)) return [];
69
+ if (lstatSync(folder).isSymbolicLink()) fail('prototype preparation folder may not be a symbolic link');
70
+ return readdirSync(folder, { withFileTypes: true })
71
+ .filter((entry) => entry.isFile() && /^cycle-[1-3]-preparation\.json$/.test(entry.name))
72
+ .map((entry) => readJson(resolve(folder, entry.name), entry.name));
73
+ }
74
+
75
+ function validatePrototypeEntry(projectRoot, slug, entryPath) {
76
+ const project = projectPaths(projectRoot);
77
+ const deliveryRoot = resolve(project.buildRoot, slug);
78
+ const prototypeRoot = resolve(deliveryRoot, 'ui-design-assets/prototypes');
79
+ const candidate = resolve(deliveryRoot, String(entryPath ?? ''));
80
+ const rel = relative(prototypeRoot, candidate);
81
+ if (!entryPath || isAbsolute(String(entryPath)) || !rel || rel.startsWith('..') || isAbsolute(rel)) {
82
+ fail('prototype entry path must identify a file inside ui-design-assets/prototypes', 400);
83
+ }
84
+ if (!existsSync(candidate) || !lstatSync(candidate).isFile()) fail(`prototype entry path does not exist: ${entryPath}`, 400);
85
+ if (lstatSync(candidate).isSymbolicLink()) fail('prototype entry path may not be a symbolic link', 400);
86
+ }
87
+
88
+ function catalogue(options) {
89
+ return Array.isArray(options.personas) ? options.personas : Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
90
+ }
91
+
92
+ export function preparePrototypePlanWorkspace(projectRoot, deliverySlug, input = {}, options = {}) {
93
+ const slug = safeSlug(deliverySlug);
94
+ const request = object(input, 'plan prepare request');
95
+ knownFields(request, ['intentDigest', 'designSystem', 'plan', 'signals', 'predecessorDigest'], 'plan prepare request');
96
+ const engagement = selectPrototypeReviewPersonas({
97
+ stage: 'plan',
98
+ signals: request.signals ?? [],
99
+ context: { deliverySlug: slug, plan: request.plan },
100
+ }, catalogue(options));
101
+ const preparation = preparePrototypePlanReview({
102
+ schema: 'ewai.prototype-plan-input/v1',
103
+ deliverySlug: slug,
104
+ intentDigest: request.intentDigest,
105
+ designSystem: request.designSystem,
106
+ plan: request.plan,
107
+ engagement,
108
+ predecessorDigest: request.predecessorDigest ?? '',
109
+ }, { now: options.now });
110
+ atomicJson(runtimePaths(projectRoot).prototypePlanPreparationPath(slug), preparation);
111
+ return {
112
+ schema: WORKSPACE_SCHEMA,
113
+ status: 'plan-review-prepared',
114
+ deliverySlug: slug,
115
+ preparation,
116
+ activePersonas: preparation.engagement.activePersonas,
117
+ personaAvailability: preparation.engagement.availability,
118
+ nextAction: 'record-plan-review',
119
+ authority: preparation.authority,
120
+ };
121
+ }
122
+
123
+ export function recordPrototypePlanWorkspaceReview(projectRoot, deliverySlug, input, options = {}) {
124
+ const slug = safeSlug(deliverySlug);
125
+ const preparation = readJson(runtimePaths(projectRoot).prototypePlanPreparationPath(slug), 'prototype plan review');
126
+ if (preparation.deliverySlug !== slug) fail('prepared plan belongs to a different delivery');
127
+ return recordPrototypePlanReview(projectRoot, preparation, resolveAssessmentReferences(input), options);
128
+ }
129
+
130
+ export function preparePrototypeCycleWorkspace(projectRoot, deliverySlug, input = {}, options = {}) {
131
+ const slug = safeSlug(deliverySlug);
132
+ const request = object(input, 'cycle prepare request');
133
+ knownFields(request, [
134
+ 'planReviewDigest', 'cycleNumber', 'maxCycles', 'predecessorDigest', 'prototype',
135
+ 'evidenceChannels', 'signals',
136
+ ], 'cycle prepare request');
137
+ const status = readPrototypeReviewStatus(projectRoot, slug);
138
+ const plan = status.planReviews.find(({ contentDigest }) => contentDigest === request.planReviewDigest);
139
+ if (!plan) fail('cycle preparation requires a recorded plan review digest');
140
+ const cycleNumber = Number(request.cycleNumber);
141
+ const predecessorDigest = String(request.predecessorDigest ?? '');
142
+ if (cycleNumber > 1) {
143
+ const predecessor = status.cycleReviews.find(({ contentDigest }) => contentDigest === predecessorDigest);
144
+ if (!predecessor) fail('cycle predecessor digest must identify a recorded earlier cycle');
145
+ if (predecessor.cycleNumber !== cycleNumber - 1) fail('cycle predecessor must be the immediately previous cycle');
146
+ if (status.currentCycleReview?.contentDigest !== predecessorDigest) fail('cycle predecessor must be the current cycle review');
147
+ } else if (status.cycleReviews.length) {
148
+ fail('cycle one cannot be prepared after recorded design cycles already exist');
149
+ }
150
+ validatePrototypeEntry(projectRoot, slug, request.prototype?.entryPath);
151
+ const engagement = selectPrototypeReviewPersonas({
152
+ stage: 'design',
153
+ signals: request.signals ?? [],
154
+ context: {
155
+ deliverySlug: slug,
156
+ prototype: request.prototype,
157
+ evidenceChannels: request.evidenceChannels,
158
+ planReviewDigest: request.planReviewDigest,
159
+ },
160
+ }, catalogue(options));
161
+ const preparation = preparePrototypeCycleReview({
162
+ schema: 'ewai.prototype-cycle-input/v1',
163
+ deliverySlug: slug,
164
+ planReviewDigest: request.planReviewDigest,
165
+ cycleNumber,
166
+ maxCycles: request.maxCycles ?? 2,
167
+ predecessorDigest,
168
+ prototype: request.prototype,
169
+ evidenceChannels: request.evidenceChannels,
170
+ engagement,
171
+ }, { now: options.now });
172
+ atomicJson(runtimePaths(projectRoot).prototypeCyclePreparationPath(slug, preparation.cycleNumber), preparation);
173
+ return {
174
+ schema: WORKSPACE_SCHEMA,
175
+ status: 'design-review-prepared',
176
+ deliverySlug: slug,
177
+ preparation,
178
+ activePersonas: preparation.engagement.activePersonas,
179
+ personaAvailability: preparation.engagement.availability,
180
+ nextAction: 'record-design-review',
181
+ authority: preparation.authority,
182
+ };
183
+ }
184
+
185
+ export function recordPrototypeCycleWorkspaceReview(projectRoot, deliverySlug, input, options = {}) {
186
+ const slug = safeSlug(deliverySlug);
187
+ const expected = String(input?.expectedPreparationDigest ?? '');
188
+ const preparation = preparationFiles(projectRoot, slug).find(({ preparationDigest }) => preparationDigest === expected);
189
+ if (!preparation) fail('no prepared design cycle matches the expected preparation digest');
190
+ if (preparation.deliverySlug !== slug) fail('prepared design cycle belongs to a different delivery');
191
+ return recordPrototypeCycleReview(projectRoot, preparation, resolveAssessmentReferences(input), options);
192
+ }
193
+
194
+ export function readPrototypeIterationWorkspace(projectRoot, deliverySlug, options = {}) {
195
+ const slug = safeSlug(deliverySlug);
196
+ const status = readPrototypeReviewStatus(projectRoot, slug);
197
+ const planPath = runtimePaths(projectRoot).prototypePlanPreparationPath(slug);
198
+ const planPreparation = existsSync(planPath) ? readJson(planPath, 'prototype plan review') : null;
199
+ const cyclePreparations = preparationFiles(projectRoot, slug).sort((left, right) => left.cycleNumber - right.cycleNumber);
200
+ const currentEngagement = status.currentCycleReview?.engagement
201
+ ?? cyclePreparations.at(-1)?.engagement
202
+ ?? status.currentPlanReview?.engagement
203
+ ?? planPreparation?.engagement
204
+ ?? null;
205
+ const availability = currentEngagement?.availability
206
+ ?? selectPrototypeReviewPersonas({ stage: 'plan' }, catalogue(options)).availability;
207
+ return {
208
+ schema: WORKSPACE_SCHEMA,
209
+ status: status.currentCycleReview?.nextAction
210
+ ?? status.currentPlanReview?.nextAction
211
+ ?? (planPreparation ? 'plan-review-prepared' : 'not-prepared'),
212
+ deliverySlug: slug,
213
+ planPreparation,
214
+ cyclePreparations,
215
+ planReviews: status.planReviews,
216
+ cycleReviews: status.cycleReviews,
217
+ currentPlanReview: status.currentPlanReview,
218
+ currentCycleReview: status.currentCycleReview,
219
+ activePersonas: currentEngagement?.activePersonas ?? [],
220
+ personaAvailability: availability,
221
+ authority: status.authority,
222
+ };
223
+ }
224
+
225
+ export function comparePrototypeWorkspaceReviews(projectRoot, deliverySlug, input = {}) {
226
+ const slug = safeSlug(deliverySlug);
227
+ const request = object(input, 'comparison request');
228
+ knownFields(request, ['leftDigest', 'rightDigest'], 'comparison request');
229
+ const status = readPrototypeReviewStatus(projectRoot, slug);
230
+ const reviews = [...status.planReviews, ...status.cycleReviews];
231
+ const left = reviews.find(({ contentDigest }) => contentDigest === request.leftDigest);
232
+ const right = reviews.find(({ contentDigest }) => contentDigest === request.rightDigest);
233
+ if (!left || !right) fail('comparison digests must identify recorded prototype reviews', 404);
234
+ return comparePrototypeReviews(left, right);
235
+ }
@@ -0,0 +1,163 @@
1
+ import { accessSync, constants, createWriteStream, mkdirSync } from 'node:fs';
2
+ import { delimiter, isAbsolute, resolve } from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { CONTEXT_ESTIMATE_METHOD, estimateContextTokens } from './context-assembly.mjs';
5
+
6
+ export const AFK_PROVIDERS = Object.freeze(['codex', 'claude', 'antigravity']);
7
+
8
+ export function measureProviderPrompt(prompt) {
9
+ const measurement = estimateContextTokens(prompt);
10
+ return {
11
+ bytes: measurement.bytes,
12
+ estimatedTokens: measurement.estimatedTokens,
13
+ estimateMethod: CONTEXT_ESTIMATE_METHOD,
14
+ };
15
+ }
16
+
17
+ function nonNegativeInteger(value) {
18
+ return Number.isInteger(value) && value >= 0 ? value : null;
19
+ }
20
+
21
+ export function normaliseProviderUsage(provider, usage) {
22
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return null;
23
+ const inputTokens = nonNegativeInteger(usage.inputTokens ?? usage.input_tokens);
24
+ const cachedInputTokens = nonNegativeInteger(
25
+ usage.cachedInputTokens ?? usage.cached_input_tokens ?? usage.cache_read_input_tokens,
26
+ );
27
+ const outputTokens = nonNegativeInteger(usage.outputTokens ?? usage.output_tokens);
28
+ if (inputTokens === null || outputTokens === null) return null;
29
+ return {
30
+ schema: 'ewai.provider-usage/v1',
31
+ provider: String(provider ?? ''),
32
+ inputTokens,
33
+ cachedInputTokens: cachedInputTokens ?? 0,
34
+ outputTokens,
35
+ source: 'provider-reported',
36
+ };
37
+ }
38
+
39
+ export function providerCommand(provider) {
40
+ return provider === 'antigravity' ? 'agy' : provider;
41
+ }
42
+
43
+ export function commandAvailable(command, env = process.env) {
44
+ const candidates = isAbsolute(command)
45
+ ? [command]
46
+ : String(env.PATH ?? '').split(delimiter).filter(Boolean).map((entry) => resolve(entry, command));
47
+ return candidates.some((candidate) => {
48
+ try {
49
+ accessSync(candidate, constants.X_OK);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ });
55
+ }
56
+
57
+ function claudeTools(task, mode) {
58
+ if (mode === 'review') return ['Read', 'Glob', 'Grep'];
59
+ const commands = (task?.allowed_commands ?? []).map((command) => `Bash(${command})`);
60
+ return ['Read', 'Edit', 'Write', 'Glob', 'Grep', ...commands];
61
+ }
62
+
63
+ export function buildProviderInvocation(provider, input = {}) {
64
+ if (!AFK_PROVIDERS.includes(provider)) throw new Error(`Unsupported AFK provider: ${provider}`);
65
+ const mode = input.mode === 'review' ? 'review' : 'implementation';
66
+ const cwd = resolve(input.cwd);
67
+ const model = String(input.model ?? '').trim();
68
+ const invocation = {
69
+ provider,
70
+ command: providerCommand(provider),
71
+ cwd,
72
+ prompt: String(input.prompt ?? ''),
73
+ timeoutMs: Number(input.timeoutMs ?? 45 * 60 * 1000),
74
+ mode,
75
+ task: input.task ?? null,
76
+ promptMeasurement: measureProviderPrompt(input.prompt ?? ''),
77
+ providerUsage: normaliseProviderUsage(provider, input.providerUsage),
78
+ args: [],
79
+ };
80
+
81
+ if (provider === 'codex') {
82
+ invocation.args = ['--ask-for-approval', 'never', 'exec', '--color', 'never', '--sandbox', mode === 'review' ? 'read-only' : 'workspace-write', '-C', cwd];
83
+ if (mode === 'implementation') invocation.args.splice(3, 0, '--json');
84
+ if (model) invocation.args.push('--model', model);
85
+ invocation.args.push('-');
86
+ } else if (provider === 'claude') {
87
+ invocation.args = [
88
+ '-p',
89
+ '--output-format', mode === 'review' ? 'text' : 'stream-json',
90
+ '--permission-mode', mode === 'review' ? 'plan' : 'dontAsk',
91
+ '--no-session-persistence',
92
+ '--allowedTools', claudeTools(input.task, mode).join(','),
93
+ ];
94
+ if (model) invocation.args.push('--model', model);
95
+ if (Number.isFinite(Number(input.maxBudgetUsd)) && Number(input.maxBudgetUsd) > 0) {
96
+ invocation.args.push('--max-budget-usd', String(input.maxBudgetUsd));
97
+ }
98
+ } else {
99
+ invocation.args = [
100
+ '--print', invocation.prompt,
101
+ '--output-format', mode === 'review' ? 'text' : 'stream-json',
102
+ '--mode', mode === 'review' ? 'plan' : 'accept-edits',
103
+ '--sandbox',
104
+ ];
105
+ if (mode === 'implementation') invocation.args.push('--dangerously-skip-permissions');
106
+ if (model) invocation.args.push('--model', model);
107
+ }
108
+ return invocation;
109
+ }
110
+
111
+ export function invokeProvider(invocation, options = {}) {
112
+ mkdirSync(resolve(invocation.logPath, '..'), { recursive: true });
113
+ const output = createWriteStream(invocation.logPath, { flags: 'a' });
114
+ const startedAt = Date.now();
115
+ const maxCapture = Number(options.maxCaptureBytes ?? 1024 * 1024);
116
+
117
+ return new Promise((resolvePromise, reject) => {
118
+ const child = spawn(invocation.command, invocation.args, {
119
+ cwd: invocation.cwd,
120
+ env: options.env ?? process.env,
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ shell: false,
123
+ });
124
+ options.onStart?.(child.pid);
125
+ let captured = '';
126
+ let timedOut = false;
127
+ const capture = (chunk) => {
128
+ output.write(chunk);
129
+ captured = `${captured}${chunk.toString('utf8')}`.slice(-maxCapture);
130
+ };
131
+ child.stdout.on('data', capture);
132
+ child.stderr.on('data', capture);
133
+ child.once('error', (error) => {
134
+ output.end();
135
+ reject(error);
136
+ });
137
+ const timeout = setTimeout(() => {
138
+ timedOut = true;
139
+ child.kill('SIGTERM');
140
+ setTimeout(() => child.kill('SIGKILL'), 5_000).unref();
141
+ }, invocation.timeoutMs);
142
+ timeout.unref();
143
+ child.once('close', (exitCode, signal) => {
144
+ clearTimeout(timeout);
145
+ output.end();
146
+ resolvePromise({
147
+ provider: invocation.provider,
148
+ exitCode: exitCode ?? -1,
149
+ signal: signal ?? '',
150
+ timedOut,
151
+ durationMs: Date.now() - startedAt,
152
+ logPath: invocation.logPath,
153
+ output: captured,
154
+ promptMeasurement: invocation.promptMeasurement ?? measureProviderPrompt(invocation.prompt),
155
+ providerUsage: normaliseProviderUsage(
156
+ invocation.provider,
157
+ options.providerUsage ?? invocation.providerUsage,
158
+ ),
159
+ });
160
+ });
161
+ child.stdin.end(invocation.prompt);
162
+ });
163
+ }