@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,973 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync,
4
+ } from 'node:fs';
5
+ import { dirname, relative, resolve } from 'node:path';
6
+ import { execFileSync, spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { deliveryPaths, atomicJson, isWithin, sha256, now } from '../delivery-documents.mjs';
9
+ import { projectPaths } from '../paths.mjs';
10
+ import { loadProjectConfig, validationStatus } from '../project.mjs';
11
+ import { validateTaskGraph, validateTaskReport } from '../task-graph.mjs';
12
+ import { syncIntentIndex } from './intents.mjs';
13
+ import {
14
+ abandonExecutionLeasesForRun,
15
+ acquireExecutionLease,
16
+ heartbeatExecutionLease,
17
+ releaseExecutionLease,
18
+ } from './execution-leases.mjs';
19
+ import { addActivityEvent } from './work.mjs';
20
+ import {
21
+ AFK_PROVIDERS, buildProviderInvocation, commandAvailable, invokeProvider, providerCommand,
22
+ } from './provider-adapters.mjs';
23
+ import { prepareContextPack } from './context-assembly.mjs';
24
+
25
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
26
+ const workerPath = resolve(moduleDir, '../afk-worker.mjs');
27
+ const runSchema = 'ewai.afk-run/v1';
28
+ const terminalStates = new Set(['completed', 'cancelled']);
29
+
30
+ function git(root, args, options = {}) {
31
+ return execFileSync('git', args, {
32
+ cwd: root,
33
+ encoding: 'utf8',
34
+ stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
35
+ ...options,
36
+ }).trim();
37
+ }
38
+
39
+ function processAlive(pid) {
40
+ if (!Number.isInteger(pid) || pid < 1) return false;
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function pathsFor(projectRoot, runId = '') {
50
+ const root = resolve(projectRoot);
51
+ const afkRoot = resolve(root, '.ewai-pipeline/afk');
52
+ const runRoot = runId ? resolve(afkRoot, 'runs', runId) : '';
53
+ return {
54
+ projectRoot: root,
55
+ afkRoot,
56
+ runsRoot: resolve(afkRoot, 'runs'),
57
+ worktreesRoot: resolve(afkRoot, 'worktrees'),
58
+ runRoot,
59
+ runPath: runRoot ? resolve(runRoot, 'run.json') : '',
60
+ };
61
+ }
62
+
63
+ function requireRun(projectRoot, runId) {
64
+ const paths = pathsFor(projectRoot, runId);
65
+ if (!existsSync(paths.runPath)) throw new Error(`Unknown AFK run: ${runId}`);
66
+ const run = JSON.parse(readFileSync(paths.runPath, 'utf8'));
67
+ if (run.schema !== runSchema) throw new Error(`Unsupported AFK run schema: ${run.schema}`);
68
+ return { paths, run };
69
+ }
70
+
71
+ function saveRun(paths, run) {
72
+ if (existsSync(paths.runPath)) {
73
+ const current = JSON.parse(readFileSync(paths.runPath, 'utf8'));
74
+ if (current.id === run.id && current.desiredState !== 'running' && run.desiredState === 'running') {
75
+ run.desiredState = current.desiredState;
76
+ if (current.desiredState === 'cancelled') run.status = 'cancelled';
77
+ }
78
+ }
79
+ run.updatedAt = now();
80
+ atomicJson(paths.runPath, run);
81
+ return run;
82
+ }
83
+
84
+ function event(projectRoot, run, summary, details = '', eventType = 'progress', requiresHuman = false) {
85
+ try {
86
+ addActivityEvent(projectRoot, run.intentId, {
87
+ phaseKey: 'build',
88
+ summary,
89
+ details,
90
+ eventType,
91
+ requiresHuman,
92
+ });
93
+ } catch {
94
+ // The durable AFK run remains authoritative if the live dashboard is unavailable.
95
+ }
96
+ }
97
+
98
+ function configuredProviders(config) {
99
+ const status = validationStatus(config, 'manual');
100
+ return AFK_PROVIDERS.filter((provider) => {
101
+ const setting = status.providers[provider];
102
+ return setting.state === 'available' && setting.enabled && commandAvailable(providerCommand(provider));
103
+ });
104
+ }
105
+
106
+ function readGraph(projectRoot, slug) {
107
+ const delivery = deliveryPaths(projectRoot, slug);
108
+ return JSON.parse(readFileSync(resolve(delivery.deliveryRoot, 'task-graph.json'), 'utf8'));
109
+ }
110
+
111
+ export function selectExecutableTaskIds(graph, taskCheck, maxParallel = 1) {
112
+ const readyIds = taskCheck.tasks
113
+ .filter((task) => task.status === 'ready' && !task.requiresHuman)
114
+ .map((task) => task.id);
115
+ if (!readyIds.length) return [];
116
+ const firstReady = graph.tasks.find((task) => readyIds.includes(task.id));
117
+ const wave = graph.afk_waves.find((candidate) => Number(candidate.wave) === Number(firstReady.parallel_wave));
118
+ const declaredParallel = new Set(wave?.parallel_tasks ?? []);
119
+ const orderedReady = (wave?.merge_order ?? readyIds).filter((id) => readyIds.includes(id));
120
+ return graph.parallelism?.allowed && orderedReady.every((id) => declaredParallel.has(id))
121
+ ? orderedReady.slice(0, maxParallel)
122
+ : orderedReady.slice(0, 1);
123
+ }
124
+
125
+ function gitDirty(root) {
126
+ return git(root, ['status', '--porcelain=v1', '--untracked-files=all']);
127
+ }
128
+
129
+ function repositoryParentBranch(graph, repositoryName) {
130
+ const configured = graph?.repository_branches?.[repositoryName];
131
+ return typeof configured === 'string'
132
+ ? configured
133
+ : configured?.parent_branch ?? graph?.parent_branch ?? '';
134
+ }
135
+
136
+ function resolveRepositoryTopology(projectRoot, specsRoot, config, graph, failures) {
137
+ const configured = config.repositories ?? [];
138
+ const specsRealRoot = existsSync(specsRoot) ? realpathSync(specsRoot) : resolve(specsRoot);
139
+ const taskRepositories = new Set((graph?.tasks ?? []).map((task) => task.repo));
140
+ const names = new Set();
141
+ const repositories = [];
142
+ for (const repository of configured) {
143
+ if (names.has(repository.name)) {
144
+ failures.push(`Configured repository name is duplicated: ${repository.name}.`);
145
+ continue;
146
+ }
147
+ names.add(repository.name);
148
+ const configuredPath = String(repository.path ?? '').trim();
149
+ const candidate = resolve(projectRoot, configuredPath);
150
+ let root = candidate;
151
+ let currentBranch = '';
152
+ let gitRoot = '';
153
+ const taskCount = (graph?.tasks ?? []).filter((task) => task.repo === repository.name).length;
154
+ const ownsSpecs = (() => {
155
+ try { return isWithin(realpathSync(candidate), specsRealRoot); } catch { return false; }
156
+ })();
157
+ const selected = ownsSpecs || taskRepositories.has(repository.name);
158
+ if (selected) {
159
+ try {
160
+ root = realpathSync(candidate);
161
+ gitRoot = realpathSync(git(root, ['rev-parse', '--show-toplevel']));
162
+ currentBranch = git(root, ['branch', '--show-current']);
163
+ if (gitRoot !== root) failures.push(`Configured repository ${repository.name} must point to its Git root: ${configuredPath}.`);
164
+ if (gitDirty(root)) failures.push(`Configured repository ${repository.name} must be clean before unattended execution starts.`);
165
+ } catch (error) {
166
+ failures.push(`Configured repository ${repository.name} is not an accessible Git working tree at ${configuredPath}: ${error.message}`);
167
+ }
168
+ }
169
+ const parentBranch = selected ? repositoryParentBranch(graph, repository.name) : '';
170
+ if (selected && !parentBranch) failures.push(`No parent branch is defined for repository ${repository.name}.`);
171
+ let branchAction = 'none';
172
+ if (selected && parentBranch) {
173
+ if (!validBranchName(root, parentBranch)) {
174
+ failures.push(`Repository ${repository.name} has invalid AFK parent branch name ${parentBranch}.`);
175
+ } else if (currentBranch !== parentBranch) {
176
+ if (!currentBranch) {
177
+ failures.push(`Repository ${repository.name} is detached; check out a base branch before AFK can prepare ${parentBranch}.`);
178
+ } else if (gitBranchExists(root, parentBranch)) {
179
+ failures.push(`Repository ${repository.name} is on ${currentBranch}, but existing AFK parent branch ${parentBranch} is not checked out.`);
180
+ } else {
181
+ branchAction = 'create';
182
+ }
183
+ }
184
+ }
185
+ repositories.push({
186
+ name: repository.name,
187
+ role: repository.role,
188
+ path: configuredPath,
189
+ root,
190
+ parentBranch,
191
+ currentBranch,
192
+ taskCount,
193
+ selected,
194
+ ownsSpecs,
195
+ branchAction,
196
+ });
197
+ }
198
+ const specsRepositories = repositories.filter((repository) => repository.ownsSpecs);
199
+ const specsRepository = specsRepositories[0];
200
+ if (!specsRepository) failures.push('AFK requires a configured Git repository that contains the selected SPECS root so durable evidence can be versioned.');
201
+ if (specsRepositories.length > 1) failures.push(`SPECS root is ambiguously contained by repositories: ${specsRepositories.map((repository) => repository.name).join(', ')}.`);
202
+ for (const name of taskRepositories) {
203
+ if (!names.has(name)) failures.push(`Task graph references unconfigured repository ${name}.`);
204
+ }
205
+ return {
206
+ kind: repositories.length === 1 ? 'simple' : 'multi-repo',
207
+ specsRepository: specsRepository?.name ?? '',
208
+ repositories,
209
+ };
210
+ }
211
+
212
+ function prepareParentBranches(topology) {
213
+ const created = [];
214
+ try {
215
+ for (const repository of topology.repositories.filter((candidate) => candidate.branchAction === 'create')) {
216
+ git(repository.root, ['switch', '-c', repository.parentBranch]);
217
+ created.push({ repository, previousBranch: repository.currentBranch });
218
+ repository.currentBranch = repository.parentBranch;
219
+ repository.branchAction = 'created';
220
+ }
221
+ } catch (error) {
222
+ for (const item of created.reverse()) {
223
+ try {
224
+ git(item.repository.root, ['switch', item.previousBranch]);
225
+ git(item.repository.root, ['branch', '-D', item.repository.parentBranch]);
226
+ } catch {
227
+ // Leave the exact Git state visible if rollback itself cannot be completed safely.
228
+ }
229
+ }
230
+ throw new Error(`Unable to prepare AFK integration branches: ${error.message}`);
231
+ }
232
+ return topology;
233
+ }
234
+
235
+ export function preflightAfkRun(projectRoot, slug, options = {}) {
236
+ const root = resolve(projectRoot);
237
+ const { config, paths: project } = loadProjectConfig(root);
238
+ const failures = [];
239
+ const existingRun = afkRunStatus(root).find((run) => (
240
+ run.slug === slug && run.id !== options.ignoreRunId && !terminalStates.has(run.status)
241
+ ));
242
+ if (existingRun) failures.push(`AFK run ${existingRun.id} is already ${existingRun.status}; resume, pause, or cancel that durable run instead of starting another.`);
243
+ const delivery = deliveryPaths(root, slug);
244
+ let graph = null;
245
+ let graphCheck = null;
246
+ try {
247
+ graph = readGraph(root, slug);
248
+ graphCheck = validateTaskGraph(delivery.deliveryRoot, { slug });
249
+ if (graphCheck.status !== 'pass') {
250
+ failures.push(`Task graph is invalid: ${graphCheck.errors.map((item) => item.message).join('; ')}`);
251
+ }
252
+ } catch (error) {
253
+ failures.push(`Task graph cannot be loaded: ${error.message}`);
254
+ }
255
+ const topology = resolveRepositoryTopology(root, project.specsRoot, config, graph, failures);
256
+ const selectedRoots = new Set(topology.repositories.filter((repository) => repository.selected).map((repository) => repository.root));
257
+ for (const activeRun of afkRunStatus(root).filter((run) => (
258
+ run.id !== options.ignoreRunId && run.slug !== slug && !terminalStates.has(run.status)
259
+ ))) {
260
+ const overlap = (activeRun.topology?.repositories ?? [])
261
+ .filter((repository) => repository.selected)
262
+ .find((repository) => selectedRoots.has(repository.root));
263
+ if (overlap) {
264
+ failures.push(`AFK run ${activeRun.id} for ${activeRun.slug} already owns repository ${overlap.name}; pause, complete, or cancel it first.`);
265
+ }
266
+ }
267
+ const statePath = delivery.statePath;
268
+ try {
269
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
270
+ const build = state.phases?.find((phase) => phase.id === 'build');
271
+ if (state.currentPhase !== 'build' || build?.status !== 'running') failures.push('The guarded EWAI delivery must already be running Phase 8 Build.');
272
+ if (!state.approvals?.build || !existsSync(resolve(delivery.gatesRoot, 'build/build-approval.json'))) {
273
+ failures.push('Durable human Build approval is missing.');
274
+ }
275
+ } catch (error) {
276
+ failures.push(`Delivery state cannot be verified: ${error.message}`);
277
+ }
278
+ const ready = graphCheck?.tasks?.filter((task) => task.status === 'ready' && !task.requiresHuman).map((task) => task.id) ?? [];
279
+ if (!ready.length && !(graphCheck?.completedTasks?.length === graph?.tasks?.length)) failures.push('No AFK task is ready to execute.');
280
+
281
+ const available = configuredProviders(config);
282
+ const requested = String(options.provider ?? 'auto');
283
+ const providers = requested === 'auto'
284
+ ? available
285
+ : requested.split(',').map((value) => value.trim()).filter(Boolean);
286
+ for (const provider of providers) {
287
+ if (!AFK_PROVIDERS.includes(provider)) failures.push(`Unknown AFK provider: ${provider}.`);
288
+ else if (!available.includes(provider)) failures.push(`${provider} is not both enabled in SPECS/pipeline.yaml and installed in this terminal.`);
289
+ }
290
+ if (!providers.length) failures.push('No configured and installed Claude, Codex, or Antigravity CLI is available.');
291
+ const requestedParallel = Number(options.maxParallel ?? graph?.parallelism?.max_parallel_tasks ?? 1);
292
+ if (!Number.isInteger(requestedParallel) || requestedParallel < 1) failures.push('maxParallel must be a positive integer.');
293
+ const maxParallel = Math.max(1, Math.min(
294
+ requestedParallel || 1,
295
+ graph?.parallelism?.allowed === false ? 1 : Number(graph?.parallelism?.max_parallel_tasks ?? 1),
296
+ ));
297
+ return {
298
+ status: failures.length ? 'blocked' : 'ready',
299
+ failures,
300
+ projectRoot: root,
301
+ specsRoot: project.specsRoot,
302
+ slug,
303
+ branch: topology.repositories.find((repository) => repository.name === topology.specsRepository)?.currentBranch ?? '',
304
+ parentBranch: graph?.parent_branch ?? '',
305
+ topology,
306
+ providers,
307
+ readyTasks: ready,
308
+ maxParallel,
309
+ graphCheck,
310
+ };
311
+ }
312
+
313
+ function spawnWorker(projectRoot, runId) {
314
+ const child = spawn(process.execPath, [workerPath, '--project', projectRoot, '--run', runId], {
315
+ cwd: projectRoot,
316
+ detached: true,
317
+ stdio: 'ignore',
318
+ });
319
+ child.unref();
320
+ return child.pid;
321
+ }
322
+
323
+ export function startAfkRun(projectRoot, slug, options = {}) {
324
+ const root = resolve(projectRoot);
325
+ const lockParent = resolve(root, '.ewai-pipeline/afk/locks');
326
+ const lockPath = resolve(lockParent, `${slug}.lock`);
327
+ mkdirSync(lockParent, { recursive: true });
328
+ try {
329
+ mkdirSync(lockPath);
330
+ } catch {
331
+ throw new Error(`Another AFK start is already being prepared for ${slug}.`);
332
+ }
333
+ try {
334
+ const preflight = preflightAfkRun(root, slug, options);
335
+ if (preflight.status !== 'ready') throw new Error(`AFK preflight blocked: ${preflight.failures.join(' ')}`);
336
+ const indexed = syncIntentIndex(preflight.projectRoot).intents.find((intent) => intent.slug === slug);
337
+ if (!indexed) throw new Error(`Unknown intent slug: ${slug}`);
338
+ prepareParentBranches(preflight.topology);
339
+ const runId = randomUUID();
340
+ const paths = pathsFor(preflight.projectRoot, runId);
341
+ mkdirSync(paths.runRoot, { recursive: true });
342
+ const run = {
343
+ schema: runSchema,
344
+ id: runId,
345
+ projectRoot: preflight.projectRoot,
346
+ specsRoot: preflight.specsRoot,
347
+ slug,
348
+ intentId: indexed.id,
349
+ parentBranch: preflight.parentBranch,
350
+ topology: preflight.topology,
351
+ status: 'starting',
352
+ desiredState: 'running',
353
+ providers: preflight.providers,
354
+ maxParallel: preflight.maxParallel,
355
+ timeoutMs: Number(options.timeoutMs ?? 45 * 60 * 1000),
356
+ createdAt: now(),
357
+ updatedAt: now(),
358
+ pid: null,
359
+ activeWorkers: [],
360
+ tasks: {},
361
+ messages: [],
362
+ };
363
+ saveRun(paths, run);
364
+ event(preflight.projectRoot, run, 'Preparing unattended Build execution.', `${preflight.readyTasks.length} task(s) ready; maximum parallelism ${run.maxParallel}.`);
365
+ if (options.foreground === true) return executeAfkRun(preflight.projectRoot, runId, options.dependencies);
366
+ run.pid = spawnWorker(preflight.projectRoot, runId);
367
+ run.status = 'running';
368
+ saveRun(paths, run);
369
+ return run;
370
+ } finally {
371
+ rmSync(lockPath, { recursive: true, force: true });
372
+ }
373
+ }
374
+
375
+ export function afkRunStatus(projectRoot, runId = '') {
376
+ const root = resolve(projectRoot);
377
+ if (runId) {
378
+ const { run } = requireRun(root, runId);
379
+ return { ...run, alive: processAlive(run.pid) };
380
+ }
381
+ const { runsRoot } = pathsFor(root);
382
+ if (!existsSync(runsRoot)) return [];
383
+ return readdirSync(runsRoot, { withFileTypes: true })
384
+ .filter((entry) => entry.isDirectory() && existsSync(resolve(runsRoot, entry.name, 'run.json')))
385
+ .map((entry) => JSON.parse(readFileSync(resolve(runsRoot, entry.name, 'run.json'), 'utf8')))
386
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
387
+ .map((run) => ({ ...run, alive: processAlive(run.pid) }));
388
+ }
389
+
390
+ export function pauseAfkRun(projectRoot, runId) {
391
+ const { paths, run } = requireRun(projectRoot, runId);
392
+ if (terminalStates.has(run.status)) throw new Error(`AFK run is already ${run.status}.`);
393
+ if (['blocked', 'failed'].includes(run.status)) throw new Error(`AFK run is ${run.status}; resolve the blocker and resume it, or cancel it.`);
394
+ run.desiredState = 'paused';
395
+ run.messages.push({ at: now(), message: 'Safe pause requested; active task wave will finish before pausing.' });
396
+ return saveRun(paths, run);
397
+ }
398
+
399
+ function stopWorkers(run) {
400
+ for (const worker of run.activeWorkers ?? []) {
401
+ if (processAlive(worker.pid)) {
402
+ try { process.kill(worker.pid, 'SIGTERM'); } catch { /* already gone */ }
403
+ }
404
+ }
405
+ }
406
+
407
+ export function cancelAfkRun(projectRoot, runId) {
408
+ const { paths, run } = requireRun(projectRoot, runId);
409
+ if (terminalStates.has(run.status)) return run;
410
+ run.desiredState = 'cancelled';
411
+ run.status = 'cancelled';
412
+ stopWorkers(run);
413
+ abandonExecutionLeasesForRun(paths.projectRoot, run.id, { outcome: 'conductor-cancelled' });
414
+ run.activeWorkers = [];
415
+ event(paths.projectRoot, run, 'Unattended Build execution cancelled.', 'Active task leases were safely abandoned.', 'blocked');
416
+ return saveRun(paths, run);
417
+ }
418
+
419
+ export function resumeAfkRun(projectRoot, runId) {
420
+ const { paths, run } = requireRun(projectRoot, runId);
421
+ if (run.status === 'completed' || run.status === 'cancelled') throw new Error(`AFK run is ${run.status} and cannot resume.`);
422
+ if (processAlive(run.pid)) throw new Error('AFK conductor is already running.');
423
+ const preflight = preflightAfkRun(paths.projectRoot, run.slug, {
424
+ provider: run.providers.join(','),
425
+ maxParallel: run.maxParallel,
426
+ ignoreRunId: run.id,
427
+ });
428
+ if (preflight.status !== 'ready') throw new Error(`AFK recovery preflight blocked: ${preflight.failures.join(' ')}`);
429
+ stopWorkers(run);
430
+ abandonExecutionLeasesForRun(paths.projectRoot, run.id, { outcome: 'conductor-recovered' });
431
+ run.desiredState = 'running';
432
+ run.status = 'running';
433
+ run.topology = preflight.topology;
434
+ run.activeWorkers = [];
435
+ run.pid = spawnWorker(paths.projectRoot, run.id);
436
+ event(paths.projectRoot, run, 'Recovering unattended Build execution.', 'Abandoned leases were released; task evidence will be revalidated before continuing.');
437
+ return saveRun(paths, run);
438
+ }
439
+
440
+ function afkMarkers(task) {
441
+ return [
442
+ task.id,
443
+ task.slice,
444
+ ...(task.claims ?? []),
445
+ ...(task.write_set ?? []),
446
+ ...(task.allowed_commands ?? []),
447
+ ...(task.stop_conditions ?? []),
448
+ task.first_failing_test,
449
+ ...(task.review?.standards_pushed ?? []),
450
+ 'orchestrator_owned',
451
+ ].map((value) => String(value ?? '').trim()).filter(Boolean);
452
+ }
453
+
454
+ export function prepareAfkContext(projectRoot, task, repository, options = {}) {
455
+ const specsRoot = projectPaths(projectRoot).specsRoot;
456
+ const review = options.mode === 'review';
457
+ const implementationCommit = String(options.implementationCommit ?? '').trim();
458
+ const taskContent = JSON.stringify(task);
459
+ const rules = review
460
+ ? `TESTS_FIRST Read tests before implementation. Read cited standards from ${specsRoot}. Review implementation commit ${implementationCommit}. Check the diff, exact task contract, write_set, desired outcome, stop conditions, test evidence, correctness, standards, security and scope. Do not edit files. End with exactly one line: VERDICT: PASS or VERDICT: FAIL.`
461
+ : `You are an EWAI Build worker in an isolated Git worktree. Read standards from ${specsRoot} and tests before implementation. Repository ${repository.name} (${repository.role}). Work only inside write_set. Run only allowed_commands. Stop on every stop_condition. Make the smallest code and test change. Do not create SPECS records, commit, merge, change delivery state or update phase status. AUTHORITY_NONE. Finish with files changed and commands run.`;
462
+ const ruleMarkers = review
463
+ ? ['TESTS_FIRST', implementationCommit, 'VERDICT: PASS or VERDICT: FAIL', 'Do not edit files']
464
+ : ['AUTHORITY_NONE', 'Work only inside write_set', 'Do not create SPECS records', 'Do not create SPECS records, commit, merge, change delivery state or update phase status'];
465
+ return prepareContextPack({
466
+ profile: review ? 'fresh-context-review' : 'build-task',
467
+ focus: `${task.id} ${task.name} ${review ? 'fresh review' : 'implementation'}`,
468
+ budgetTokens: options.budgetTokens,
469
+ repositoryRevision: options.repositoryRevision ?? implementationCommit ?? task.branch,
470
+ deliveryRevision: options.deliveryRevision ?? task.id,
471
+ personaCatalogue: options.personaCatalogue,
472
+ candidates: [
473
+ {
474
+ id: 'task-contract',
475
+ label: 'Exact task contract',
476
+ evidenceClass: 'mandatory',
477
+ priority: 100,
478
+ sourcePath: 'SPECS/6.Build/task-graph.json',
479
+ content: taskContent,
480
+ requiredMarkers: afkMarkers(task),
481
+ selectionReason: 'The worker and reviewer require the complete governed task boundary.',
482
+ },
483
+ {
484
+ id: review ? 'fresh-review-rules' : 'build-worker-rules',
485
+ label: review ? 'Fresh-context review rules' : 'Build worker rules',
486
+ evidenceClass: 'mandatory',
487
+ priority: 95,
488
+ sourcePath: 'SPECS/4.Constraints/standards/lifecycle-hook-safety.md',
489
+ content: rules,
490
+ requiredMarkers: ruleMarkers.filter(Boolean),
491
+ selectionReason: review ? 'Tests-first review and exact verdict are mandatory.' : 'Write, command and delivery authority boundaries are mandatory.',
492
+ },
493
+ ],
494
+ });
495
+ }
496
+
497
+ function taskPrompt(projectRoot, task, repository) {
498
+ const pack = prepareAfkContext(projectRoot, task, repository, { mode: 'implementation' });
499
+ if (pack.status !== 'ready') throw new Error(`AFK Build context is not ready: ${pack.reason}.`);
500
+ return pack.modelContext;
501
+ }
502
+
503
+ function reviewPrompt(projectRoot, task, implementationCommit) {
504
+ const pack = prepareAfkContext(projectRoot, task, { name: task.repo, role: 'review' }, {
505
+ mode: 'review', implementationCommit, repositoryRevision: implementationCommit,
506
+ });
507
+ if (pack.status !== 'ready') throw new Error(`AFK review context is not ready: ${pack.reason}.`);
508
+ return pack.modelContext;
509
+ }
510
+
511
+ function changedPaths(root) {
512
+ const output = execFileSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], { cwd: root });
513
+ const entries = output.toString('utf8').split('\0').filter(Boolean);
514
+ const paths = [];
515
+ for (let index = 0; index < entries.length; index += 1) {
516
+ const entry = entries[index];
517
+ paths.push(entry.slice(3));
518
+ if (entry[0] === 'R' || entry[1] === 'R' || entry[0] === 'C' || entry[1] === 'C') index += 1;
519
+ }
520
+ return paths;
521
+ }
522
+
523
+ function stem(value) {
524
+ return String(value).replaceAll('\\', '/').replace(/\*\*.*$/, '').replace(/\*.*$/, '').replace(/\/$/, '');
525
+ }
526
+
527
+ function allowedPath(candidate, task) {
528
+ const path = stem(candidate);
529
+ return (task.write_set ?? []).some((configured) => {
530
+ const boundary = stem(configured);
531
+ return path === boundary || path.startsWith(`${boundary}/`);
532
+ });
533
+ }
534
+
535
+ function repositoryForTask(run, task) {
536
+ const repository = run.topology?.repositories?.find((candidate) => candidate.name === task.repo);
537
+ if (!repository) throw new Error(`Task ${task.id} references repository ${task.repo}, which is absent from this AFK run topology.`);
538
+ return repository;
539
+ }
540
+
541
+ function gitBranchExists(root, branch) {
542
+ try {
543
+ git(root, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
544
+ return true;
545
+ } catch {
546
+ return false;
547
+ }
548
+ }
549
+
550
+ function validBranchName(root, branch) {
551
+ try {
552
+ git(root, ['check-ref-format', '--branch', branch]);
553
+ return true;
554
+ } catch {
555
+ return false;
556
+ }
557
+ }
558
+
559
+ function branchPrefixCollision(root, branch) {
560
+ const parts = branch.split('/');
561
+ for (let index = 1; index < parts.length; index += 1) {
562
+ if (gitBranchExists(root, parts.slice(0, index).join('/'))) return true;
563
+ }
564
+ try {
565
+ const refs = git(root, ['for-each-ref', '--format=%(refname:short)', `refs/heads/${branch}/`]);
566
+ return Boolean(refs);
567
+ } catch {
568
+ return false;
569
+ }
570
+ }
571
+
572
+ function safeBranchPart(value) {
573
+ return String(value ?? '')
574
+ .toLowerCase()
575
+ .replace(/[^a-z0-9]+/g, '-')
576
+ .replace(/^-+|-+$/g, '')
577
+ .slice(0, 42) || 'task';
578
+ }
579
+
580
+ function taskBranch(root, run, task, attempt) {
581
+ const declared = String(task.branch ?? '').trim();
582
+ if (declared && validBranchName(root, declared) && !gitBranchExists(root, declared) && !branchPrefixCollision(root, declared)) {
583
+ return declared;
584
+ }
585
+ const base = `ewai-afk-${safeBranchPart(run.slug)}-${safeBranchPart(task.id)}-${safeBranchPart(task.name)}`;
586
+ let candidate = attempt > 1 ? `${base}-attempt-${attempt}` : base;
587
+ let suffix = attempt;
588
+ while (gitBranchExists(root, candidate) || branchPrefixCollision(root, candidate)) {
589
+ suffix += 1;
590
+ candidate = `${base}-attempt-${suffix}`;
591
+ }
592
+ return candidate;
593
+ }
594
+
595
+ function taskState(run, task, update = {}) {
596
+ run.tasks[task.id] = { ...(run.tasks[task.id] ?? {}), id: task.id, ...update, updatedAt: now() };
597
+ }
598
+
599
+ function executeCommand(command, cwd, outputPath, timeoutMs) {
600
+ let exitCode = 0;
601
+ let output = '';
602
+ try {
603
+ output = execFileSync('/bin/sh', ['-lc', command], {
604
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs,
605
+ });
606
+ } catch (error) {
607
+ exitCode = Number(error.status ?? 1);
608
+ output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
609
+ }
610
+ mkdirSync(dirname(outputPath), { recursive: true });
611
+ writeFileSync(outputPath, output, 'utf8');
612
+ return { command, exitCode, outputPath };
613
+ }
614
+
615
+ function writeTaskEvidence(projectRoot, run, result, postMerge) {
616
+ const {
617
+ task, implementationCommit, review, reviewer, verifiedCommands, changedFiles, actualBranch, repository,
618
+ } = result;
619
+ const delivery = deliveryPaths(projectRoot, run.slug);
620
+ const evidencePath = resolve(delivery.deliveryRoot, task.evidence_path);
621
+ const reportPath = resolve(delivery.deliveryRoot, task.report_path);
622
+ const reviewRelative = `tasks/${task.id}/evidence/fresh-context-review.md`;
623
+ const reviewPath = resolve(delivery.deliveryRoot, reviewRelative);
624
+ mkdirSync(dirname(reviewPath), { recursive: true });
625
+ writeFileSync(reviewPath, `${review.output.trim()}\n`, 'utf8');
626
+ const commands = verifiedCommands.map((commandResult) => {
627
+ const relativePath = `tasks/${task.id}/evidence/${commandResult.stage}.txt`;
628
+ const destination = resolve(delivery.deliveryRoot, relativePath);
629
+ mkdirSync(dirname(destination), { recursive: true });
630
+ copyFileSync(commandResult.outputPath, destination);
631
+ return {
632
+ stage: commandResult.stage,
633
+ command: commandResult.command,
634
+ ...(commandResult.stage === 'red' ? { expected_failure: task.red_green_refactor.expected_red_failure } : {}),
635
+ exit_code: commandResult.exitCode,
636
+ output_path: relativePath,
637
+ output_sha256: sha256(readFileSync(destination)),
638
+ };
639
+ });
640
+ const green = commands.find((command) => command.stage === 'green');
641
+ const evidence = {
642
+ schema: 'ewai.task-evidence/v1',
643
+ task_id: task.id,
644
+ repository: repository.name,
645
+ task_branch: task.branch,
646
+ branch: actualBranch,
647
+ base_branch: repository.parentBranch,
648
+ commit: implementationCommit,
649
+ changed_files: changedFiles,
650
+ commands,
651
+ review: {
652
+ fresh_context: true,
653
+ tests_reviewed_first: true,
654
+ status: 'pass',
655
+ reviewer,
656
+ evidence_path: reviewRelative,
657
+ evidence_sha256: sha256(readFileSync(reviewPath)),
658
+ },
659
+ completion_checks: (task.completion_evidence ?? []).map((name) => ({
660
+ name,
661
+ status: 'pass',
662
+ evidence_path: green.output_path,
663
+ evidence_sha256: green.output_sha256,
664
+ })),
665
+ post_merge_checks: postMerge,
666
+ };
667
+ mkdirSync(dirname(evidencePath), { recursive: true });
668
+ atomicJson(evidencePath, evidence);
669
+ const report = `# Task Report: ${task.id}
670
+
671
+ ## Summary
672
+ Implemented ${task.name} in repository ${repository.name} on branch \`${actualBranch}\` and integrated it into \`${repository.parentBranch}\`.
673
+
674
+ ## Red-green-refactor evidence
675
+ The declared red command failed as expected; the green and refactor verification commands passed. Exact outputs and hashes are recorded in the evidence sidecar.
676
+
677
+ ## Feedback loops run
678
+ The conductor ran the task's bounded verification commands, a fresh-context review, and ${postMerge.length} post-merge check(s).
679
+
680
+ ## Files changed
681
+ ${changedFiles.map((file) => `- ${file}`).join('\n')}
682
+
683
+ ## Completion evidence
684
+ ${(task.completion_evidence ?? []).map((name) => `- ${name}: pass`).join('\n')}
685
+
686
+ ## Stop conditions
687
+ No declared stop condition occurred.
688
+
689
+ ## Review notes
690
+ - Tests reviewed first: yes
691
+ - Fresh-context review: pass
692
+ - Reviewer: ${reviewer}
693
+
694
+ ## QA notes
695
+ Automated task and integration evidence passed. Human QA remains governed by the later EWAI delivery gate.
696
+ `;
697
+ writeFileSync(reportPath, report, 'utf8');
698
+ }
699
+
700
+ async function implementTask(projectRoot, run, task, provider, dependencies = {}) {
701
+ const invoke = dependencies.invokeProvider ?? invokeProvider;
702
+ const paths = pathsFor(projectRoot, run.id);
703
+ const repository = repositoryForTask(run, task);
704
+ const attempt = Number(run.tasks?.[task.id]?.attempt ?? 0) + 1;
705
+ const worktree = resolve(paths.worktreesRoot, run.id, repository.name, `${task.id}-attempt-${attempt}`);
706
+ const actualBranch = taskBranch(repository.root, run, task, attempt);
707
+ const logRoot = resolve(paths.runRoot, 'tasks', task.id);
708
+ let lease = null;
709
+ try {
710
+ lease = acquireExecutionLease(projectRoot, run.intentId, {
711
+ taskId: task.id,
712
+ ownerId: `ewai-afk:${run.id}:${task.id}`,
713
+ tool: provider,
714
+ runId: run.id,
715
+ durationMs: Math.min(86_400_000, run.timeoutMs + 10 * 60 * 1000),
716
+ });
717
+ taskState(run, task, {
718
+ status: 'preparing', provider, leaseId: lease.id, worktree, attempt,
719
+ repository: repository.name, declaredBranch: task.branch, actualBranch, baseBranch: repository.parentBranch,
720
+ });
721
+ saveRun(paths, run);
722
+ event(projectRoot, run, `Preparing isolated worktree for ${task.id}.`, task.name);
723
+ mkdirSync(dirname(worktree), { recursive: true });
724
+ git(repository.root, ['worktree', 'add', '-b', actualBranch, worktree, repository.parentBranch]);
725
+ const baseHead = git(worktree, ['rev-parse', 'HEAD']);
726
+ const red = executeCommand(
727
+ task.red_green_refactor.red_command,
728
+ worktree,
729
+ resolve(logRoot, 'red.txt'),
730
+ run.timeoutMs,
731
+ );
732
+ red.stage = 'red';
733
+ if (red.exitCode === 0) throw new Error(`${task.id} red command already passes; the task contract is stale and must be reconciled.`);
734
+ taskState(run, task, { status: 'implementing' });
735
+ saveRun(paths, run);
736
+ event(projectRoot, run, `Agent implementing ${task.id}.`, `${provider} · ${task.name}`);
737
+ const invocation = buildProviderInvocation(provider, {
738
+ cwd: worktree,
739
+ prompt: taskPrompt(projectRoot, task, repository),
740
+ task,
741
+ timeoutMs: run.timeoutMs,
742
+ mode: 'implementation',
743
+ });
744
+ invocation.logPath = resolve(logRoot, 'implementation.log');
745
+ const result = await invoke(invocation, {
746
+ onStart(pid) {
747
+ taskState(run, task, { pid });
748
+ run.activeWorkers = [...(run.activeWorkers ?? []).filter((item) => item.taskId !== task.id), { taskId: task.id, pid }];
749
+ saveRun(paths, run);
750
+ },
751
+ });
752
+ run.activeWorkers = (run.activeWorkers ?? []).filter((item) => item.taskId !== task.id);
753
+ if (requireRun(projectRoot, run.id).run.desiredState === 'cancelled') {
754
+ throw new Error('AFK run was cancelled while the task agent was active.');
755
+ }
756
+ if (git(worktree, ['rev-parse', 'HEAD']) !== baseHead) {
757
+ throw new Error(`${task.id} created a commit; commits and merges belong to the conductor.`);
758
+ }
759
+ if (result.timedOut) throw new Error(`${provider} exceeded the ${run.timeoutMs}ms task timeout.`);
760
+ if (result.exitCode !== 0) throw new Error(`${provider} exited ${result.exitCode}; see ${relative(projectRoot, result.logPath)}.`);
761
+ heartbeatExecutionLease(projectRoot, lease.id, {
762
+ token: lease.token,
763
+ durationMs: Math.min(86_400_000, run.timeoutMs + 10 * 60 * 1000),
764
+ });
765
+ const green = executeCommand(
766
+ task.red_green_refactor.green_command,
767
+ worktree,
768
+ resolve(logRoot, 'green.txt'),
769
+ run.timeoutMs,
770
+ );
771
+ green.stage = 'green';
772
+ if (green.exitCode !== 0) throw new Error(`${task.id} green command failed after implementation.`);
773
+ const refactor = executeCommand(
774
+ task.red_green_refactor.green_command,
775
+ worktree,
776
+ resolve(logRoot, 'refactor.txt'),
777
+ run.timeoutMs,
778
+ );
779
+ refactor.stage = 'refactor';
780
+ if (refactor.exitCode !== 0) throw new Error(`${task.id} refactor verification failed.`);
781
+ const changed = changedPaths(worktree);
782
+ if (!changed.length) throw new Error(`${task.id} produced no changes.`);
783
+ const outside = changed.filter((path) => !allowedPath(path, task));
784
+ if (outside.length) throw new Error(`${task.id} changed files outside its write_set: ${outside.join(', ')}.`);
785
+ taskState(run, task, { status: 'checking-scope', changedFiles: changed });
786
+ saveRun(paths, run);
787
+ event(projectRoot, run, `Checking implementation scope for ${task.id}.`, `${changed.length} changed path(s) in ${repository.name}.`);
788
+ git(worktree, ['add', '-A']);
789
+ git(worktree, ['commit', '-m', `feat(${run.slug}): implement ${task.id}`]);
790
+ const implementationCommit = git(worktree, ['rev-parse', 'HEAD']);
791
+
792
+ const reviewer = run.providers.find((candidate) => candidate !== provider) ?? provider;
793
+ const reviewInvocation = buildProviderInvocation(reviewer, {
794
+ cwd: worktree,
795
+ prompt: reviewPrompt(projectRoot, task, implementationCommit),
796
+ task,
797
+ timeoutMs: run.timeoutMs,
798
+ mode: 'review',
799
+ });
800
+ reviewInvocation.logPath = resolve(logRoot, 'review.log');
801
+ const review = await invoke(reviewInvocation, {
802
+ onStart(pid) {
803
+ taskState(run, task, { pid, status: 'reviewing' });
804
+ run.activeWorkers = [{ taskId: task.id, pid }];
805
+ saveRun(paths, run);
806
+ },
807
+ });
808
+ run.activeWorkers = [];
809
+ if (requireRun(projectRoot, run.id).run.desiredState === 'cancelled') {
810
+ throw new Error('AFK run was cancelled during fresh-context review.');
811
+ }
812
+ if (review.timedOut || review.exitCode !== 0 || !/^VERDICT:\s*PASS\s*$/im.test(review.output)) {
813
+ throw new Error(`Fresh-context review did not pass; see ${relative(projectRoot, review.logPath)}.`);
814
+ }
815
+ heartbeatExecutionLease(projectRoot, lease.id, {
816
+ token: lease.token,
817
+ durationMs: Math.min(86_400_000, run.timeoutMs + 10 * 60 * 1000),
818
+ });
819
+ const branchHead = git(worktree, ['rev-parse', 'HEAD']);
820
+ const reviewerIdentity = `${reviewer}/${reviewer === provider ? 'fresh-session' : 'independent-cli'}`;
821
+ taskState(run, task, { status: 'ready-to-integrate', implementationCommit, branchHead, reviewer: reviewerIdentity });
822
+ saveRun(paths, run);
823
+ return {
824
+ task, lease, worktree, branchHead, implementationCommit, review,
825
+ reviewer: reviewerIdentity, verifiedCommands: [red, green, refactor], changedFiles: changed,
826
+ actualBranch, repository,
827
+ };
828
+ } catch (error) {
829
+ const control = requireRun(projectRoot, run.id).run;
830
+ if (control.desiredState === 'cancelled') {
831
+ return { task, cancelled: true, error };
832
+ }
833
+ if (lease) {
834
+ try { releaseExecutionLease(projectRoot, lease.id, { token: lease.token, outcome: 'blocked' }); } catch { /* recovery handles it */ }
835
+ }
836
+ taskState(run, task, { status: 'blocked', error: error.message });
837
+ run.activeWorkers = (run.activeWorkers ?? []).filter((item) => item.taskId !== task.id);
838
+ saveRun(paths, run);
839
+ event(projectRoot, run, `${task.id} needs attention.`, error.message, 'blocked', true);
840
+ return { task, error };
841
+ }
842
+ }
843
+
844
+ function integrateTask(projectRoot, run, result) {
845
+ const { task, lease } = result;
846
+ const repository = result.repository;
847
+ const specsRepository = run.topology.repositories.find((candidate) => candidate.name === run.topology.specsRepository);
848
+ if (!specsRepository) throw new Error('AFK run has no configured SPECS repository.');
849
+ const paths = pathsFor(projectRoot, run.id);
850
+ taskState(run, task, { status: 'integrating' });
851
+ saveRun(paths, run);
852
+ event(projectRoot, run, `Integrating ${task.id}.`, 'The orchestrator is merging sequentially and running post-merge checks.');
853
+ const delivery = deliveryPaths(projectRoot, run.slug);
854
+ const taskEvidenceRoot = resolve(delivery.deliveryRoot, 'tasks', task.id);
855
+ const evidenceBackup = resolve(paths.runRoot, 'backups', task.id);
856
+ const hadTaskEvidence = existsSync(taskEvidenceRoot);
857
+ if (hadTaskEvidence) {
858
+ rmSync(evidenceBackup, { recursive: true, force: true });
859
+ cpSync(taskEvidenceRoot, evidenceBackup, { recursive: true });
860
+ }
861
+ let codeCommitted = false;
862
+ try {
863
+ heartbeatExecutionLease(projectRoot, lease.id, {
864
+ token: lease.token,
865
+ durationMs: Math.min(86_400_000, run.timeoutMs + 10 * 60 * 1000),
866
+ });
867
+ const currentBranch = git(repository.root, ['branch', '--show-current']);
868
+ if (currentBranch !== repository.parentBranch) {
869
+ throw new Error(`Repository ${repository.name} moved to ${currentBranch || '<detached>'}; expected ${repository.parentBranch}.`);
870
+ }
871
+ git(repository.root, ['merge', '--no-ff', '--no-commit', result.actualBranch]);
872
+ const postMerge = [];
873
+ for (const [index, command] of (task.merge?.post_merge_checks ?? []).entries()) {
874
+ const outputPath = resolve(delivery.deliveryRoot, 'tasks', task.id, 'evidence', `post-merge-${index + 1}.txt`);
875
+ const check = executeCommand(command, repository.root, outputPath, run.timeoutMs);
876
+ const evidencePath = relative(delivery.deliveryRoot, outputPath).replaceAll('\\', '/');
877
+ postMerge.push({ command, exit_code: check.exitCode, output_path: evidencePath, output_sha256: sha256(readFileSync(outputPath)) });
878
+ if (check.exitCode !== 0) throw new Error(`Post-merge check failed: ${command}`);
879
+ }
880
+ writeTaskEvidence(projectRoot, run, result, postMerge);
881
+ const validated = validateTaskReport(deliveryPaths(projectRoot, run.slug).deliveryRoot, task);
882
+ if (validated.status !== 'complete') throw new Error(`Merged task evidence is invalid: ${validated.errors.join('; ')}`);
883
+ git(repository.root, ['add', '-A']);
884
+ git(repository.root, ['commit', '-m', `merge(${run.slug}): integrate ${task.id}`]);
885
+ const codeMergeCommit = git(repository.root, ['rev-parse', 'HEAD']);
886
+ codeCommitted = true;
887
+ if (repository.root !== specsRepository.root) {
888
+ git(specsRepository.root, ['add', '-A']);
889
+ git(specsRepository.root, ['commit', '-m', `docs(${run.slug}): record ${task.id} delivery evidence`]);
890
+ }
891
+ const evidenceCommit = git(specsRepository.root, ['rev-parse', 'HEAD']);
892
+ releaseExecutionLease(projectRoot, lease.id, { token: lease.token, outcome: 'completed' });
893
+ taskState(run, task, {
894
+ status: 'completed', mergeCommit: codeMergeCommit, evidenceCommit,
895
+ repository: repository.name, actualBranch: result.actualBranch,
896
+ });
897
+ saveRun(paths, run);
898
+ event(projectRoot, run, `${task.id} integrated and verified.`, `${postMerge.length} post-merge check(s) passed.`);
899
+ rmSync(evidenceBackup, { recursive: true, force: true });
900
+ try { git(repository.root, ['worktree', 'remove', '--force', result.worktree]); } catch { /* retain for diagnosis */ }
901
+ } catch (error) {
902
+ if (!codeCommitted) {
903
+ try { git(repository.root, ['merge', '--abort']); } catch { /* no merge to abort */ }
904
+ rmSync(taskEvidenceRoot, { recursive: true, force: true });
905
+ if (hadTaskEvidence) cpSync(evidenceBackup, taskEvidenceRoot, { recursive: true });
906
+ }
907
+ try { releaseExecutionLease(projectRoot, lease.id, { token: lease.token, outcome: 'blocked' }); } catch { /* recovery handles it */ }
908
+ taskState(run, task, { status: 'blocked', error: error.message });
909
+ saveRun(paths, run);
910
+ event(projectRoot, run, `${task.id} integration blocked.`, error.message, 'blocked', true);
911
+ throw error;
912
+ }
913
+ }
914
+
915
+ export async function executeAfkRun(projectRoot, runId, dependencies = {}) {
916
+ const { paths, run } = requireRun(projectRoot, runId);
917
+ run.pid = process.pid;
918
+ run.status = 'running';
919
+ saveRun(paths, run);
920
+ try {
921
+ while (run.desiredState === 'running') {
922
+ const graph = readGraph(paths.projectRoot, run.slug);
923
+ const check = validateTaskGraph(deliveryPaths(paths.projectRoot, run.slug).deliveryRoot, { slug: run.slug });
924
+ if (check.status !== 'pass') throw new Error(`Task graph became invalid: ${check.errors.map((item) => item.message).join('; ')}`);
925
+ if (check.completedTasks.length === graph.tasks.length) {
926
+ run.status = 'completed';
927
+ run.pid = null;
928
+ event(paths.projectRoot, run, 'All AFK Build tasks are integrated.', 'EWAI Build remains open for its canonical phase gate and subsequent lifecycle stages.');
929
+ return saveRun(paths, run);
930
+ }
931
+ const executableIds = selectExecutableTaskIds(graph, check, run.maxParallel);
932
+ if (!executableIds.length) throw new Error('No executable AFK task remains; a HITL task, dependency, or evidence blocker needs attention.');
933
+ const firstReady = graph.tasks.find((task) => task.id === executableIds[0]);
934
+ const waveDefinition = graph.afk_waves.find((candidate) => Number(candidate.wave) === Number(firstReady.parallel_wave));
935
+ const wave = executableIds.map((id) => graph.tasks.find((task) => task.id === id));
936
+ const results = await Promise.all(wave.map((task, index) => (
937
+ implementTask(paths.projectRoot, run, task, run.providers[index % run.providers.length], dependencies)
938
+ )));
939
+ const control = requireRun(paths.projectRoot, run.id).run;
940
+ run.desiredState = control.desiredState;
941
+ if (run.desiredState === 'cancelled') {
942
+ run.status = 'cancelled';
943
+ run.pid = null;
944
+ run.activeWorkers = [];
945
+ return saveRun(paths, run);
946
+ }
947
+ const failed = results.find((result) => result.error);
948
+ if (failed) throw failed.error;
949
+ const order = waveDefinition?.merge_order ?? wave.map((task) => task.id);
950
+ for (const id of order) {
951
+ const result = results.find((candidate) => candidate.task.id === id);
952
+ if (result) integrateTask(paths.projectRoot, run, result);
953
+ }
954
+ const refreshed = requireRun(paths.projectRoot, run.id).run;
955
+ run.desiredState = refreshed.desiredState;
956
+ if (run.desiredState === 'paused') {
957
+ run.status = 'paused';
958
+ run.pid = null;
959
+ event(paths.projectRoot, run, 'Unattended Build safely paused.', 'The active wave finished and no new task was leased.');
960
+ return saveRun(paths, run);
961
+ }
962
+ }
963
+ return saveRun(paths, run);
964
+ } catch (error) {
965
+ run.status = 'blocked';
966
+ run.pid = null;
967
+ run.messages.push({ at: now(), message: error.message });
968
+ abandonExecutionLeasesForRun(paths.projectRoot, run.id, { outcome: 'conductor-blocked' });
969
+ event(paths.projectRoot, run, 'Unattended Build is blocked.', error.message, 'blocked', true);
970
+ saveRun(paths, run);
971
+ return run;
972
+ }
973
+ }