@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,544 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { relative, resolve } from 'node:path';
3
+ import { atomicJson, atomicText, isWithin, now, sha256 } from './delivery-documents.mjs';
4
+
5
+ const taskIdPattern = /^T-\d{3,}$/;
6
+ const terminalCentralFiles = new Set([
7
+ 'tracker.md',
8
+ 'context-packet.md',
9
+ 'delivery-checklist.md',
10
+ 'delivery-state.json',
11
+ ]);
12
+
13
+ function text(value) {
14
+ return String(value ?? '').trim();
15
+ }
16
+
17
+ function list(value) {
18
+ return Array.isArray(value) ? value.map(text).filter(Boolean) : [];
19
+ }
20
+
21
+ function pathStem(value) {
22
+ return text(value)
23
+ .replaceAll('\\', '/')
24
+ .replace(/\*\*.*$/, '')
25
+ .replace(/\*.*$/, '')
26
+ .replace(/\/$/, '');
27
+ }
28
+
29
+ function pathsOverlap(left, right) {
30
+ const a = pathStem(left);
31
+ const b = pathStem(right);
32
+ return Boolean(a && b && (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)));
33
+ }
34
+
35
+ function isCentralWrite(path) {
36
+ const normalized = text(path).replaceAll('\\', '/');
37
+ const name = normalized.split('/').at(-1);
38
+ return terminalCentralFiles.has(name)
39
+ || /\/gates\/[^/]+\/(?:gate-ledger\.json|phase-gate-check\.md)$/.test(`/${normalized}`);
40
+ }
41
+
42
+ function section(content, heading) {
43
+ const lines = content.split(/\r?\n/);
44
+ const start = lines.findIndex((line) => line.trim().toLowerCase() === `## ${heading}`.toLowerCase());
45
+ if (start < 0) return '';
46
+ const end = lines.findIndex((line, index) => index > start && line.startsWith('## '));
47
+ return lines.slice(start + 1, end < 0 ? lines.length : end).join('\n').trim();
48
+ }
49
+
50
+ function isRealWithin(root, path) {
51
+ return isWithin(realpathSync(root), realpathSync(path));
52
+ }
53
+
54
+ function readJsonWithin(root, configuredPath, label, errors) {
55
+ const value = text(configuredPath);
56
+ const absolute = resolve(root, value);
57
+ if (!value || !isWithin(root, absolute)) {
58
+ errors.push({ code: 'task-graph-source-path', message: `${label} must be a path inside the delivery root.`, evidence: value || label });
59
+ return null;
60
+ }
61
+ if (!existsSync(absolute)) {
62
+ errors.push({ code: 'task-graph-source-missing', message: `${label} does not exist: ${value}.`, evidence: value });
63
+ return null;
64
+ }
65
+ if (!statSync(absolute).isFile() || !isRealWithin(root, absolute)) {
66
+ errors.push({ code: 'task-graph-source-path', message: `${label} must be a real file inside the delivery root.`, evidence: value });
67
+ return null;
68
+ }
69
+ try {
70
+ return JSON.parse(readFileSync(absolute, 'utf8'));
71
+ } catch (error) {
72
+ errors.push({ code: 'task-graph-source-invalid', message: `${label} is not valid JSON: ${error.message}`, evidence: value });
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function recordsFrom(document, keys) {
78
+ for (const key of keys) {
79
+ if (Array.isArray(document?.[key])) return document[key];
80
+ }
81
+ return [];
82
+ }
83
+
84
+ function recordId(record) {
85
+ return text(record?.id ?? record?.claim_id ?? record?.slice_id);
86
+ }
87
+
88
+ function actionableClaim(record) {
89
+ return !['defer', 'deferred', 'decision_required', 'decision-required', 'out_of_scope', 'out-of-scope']
90
+ .includes(text(record?.type ?? record?.status).toLowerCase());
91
+ }
92
+
93
+ function pathAllowed(path, writeSet) {
94
+ const candidate = pathStem(path);
95
+ return writeSet.some((allowed) => {
96
+ const boundary = pathStem(allowed);
97
+ return Boolean(candidate && boundary && (candidate === boundary || candidate.startsWith(`${boundary}/`)));
98
+ });
99
+ }
100
+
101
+ function evidenceFile(root, configuredPath, expectedHash, label, errors) {
102
+ const absolute = resolve(root, text(configuredPath));
103
+ if (!text(configuredPath) || !isWithin(root, absolute) || !existsSync(absolute)
104
+ || !statSync(absolute).isFile() || !isRealWithin(root, absolute)) {
105
+ errors.push(`${label} is missing or outside the delivery root`);
106
+ return;
107
+ }
108
+ if (!text(expectedHash) || sha256(readFileSync(absolute)) !== expectedHash) {
109
+ errors.push(`${label} hash is missing or stale`);
110
+ }
111
+ }
112
+
113
+ export function validateTaskEvidence(deliveryRoot, task) {
114
+ const root = resolve(deliveryRoot);
115
+ const configured = text(task?.evidence_path);
116
+ const path = resolve(root, configured);
117
+ const errors = [];
118
+ if (!configured || !isWithin(root, path)) {
119
+ errors.push('evidence_path is missing or escapes the delivery root');
120
+ } else if (!existsSync(path)) {
121
+ return { status: 'missing', path: configured, errors: ['structured task evidence does not exist'] };
122
+ } else if (!statSync(path).isFile() || !isRealWithin(root, path)) {
123
+ errors.push('structured task evidence must be a real file inside the delivery root');
124
+ } else {
125
+ let evidence = null;
126
+ try {
127
+ evidence = JSON.parse(readFileSync(path, 'utf8'));
128
+ } catch (error) {
129
+ errors.push(`structured task evidence is not valid JSON: ${error.message}`);
130
+ }
131
+ if (evidence) {
132
+ if (evidence.schema !== 'ewai.task-evidence/v1') errors.push('schema must be ewai.task-evidence/v1');
133
+ if (evidence.task_id !== task.id) errors.push(`task_id must be ${task.id}`);
134
+ if (text(evidence.task_branch)) {
135
+ if (evidence.task_branch !== task.branch) errors.push(`task_branch must be ${task.branch}`);
136
+ if (!text(evidence.branch)) errors.push('branch is required');
137
+ } else if (evidence.branch !== task.branch) {
138
+ errors.push(`branch must be ${task.branch}`);
139
+ }
140
+ if (text(evidence.repository) && evidence.repository !== task.repo) errors.push(`repository must be ${task.repo}`);
141
+ if (!text(evidence.commit)) errors.push('commit is required');
142
+ const changed = list(evidence.changed_files);
143
+ if (!changed.length) errors.push('changed_files must contain at least one file');
144
+ for (const file of changed) {
145
+ if (!pathAllowed(file, list(task.write_set))) errors.push(`changed file is outside the task write_set: ${file}`);
146
+ }
147
+ const commands = Array.isArray(evidence.commands) ? evidence.commands : [];
148
+ const red = commands.find((item) => item.stage === 'red');
149
+ const green = commands.find((item) => item.stage === 'green');
150
+ const refactor = commands.find((item) => item.stage === 'refactor');
151
+ if (!red || red.command !== task.red_green_refactor?.red_command || !Number.isInteger(red.exit_code) || red.exit_code === 0) {
152
+ errors.push('red command must match the task contract and capture a non-zero exit');
153
+ }
154
+ if (red && red.expected_failure !== task.red_green_refactor?.expected_red_failure) {
155
+ errors.push('red expected_failure must match the task contract');
156
+ }
157
+ if (!green || green.command !== task.red_green_refactor?.green_command || green.exit_code !== 0) {
158
+ errors.push('green command must match the task contract and capture exit 0');
159
+ }
160
+ if (!refactor || refactor.exit_code !== 0 || !text(refactor.command)) {
161
+ errors.push('refactor verification must capture a command with exit 0');
162
+ }
163
+ for (const command of commands) {
164
+ if (!list(task.allowed_commands).includes(text(command.command))) {
165
+ errors.push(`evidence contains a command outside allowed_commands: ${command.command ?? '<missing>'}`);
166
+ }
167
+ evidenceFile(root, command.output_path, command.output_sha256, `${command.stage || 'command'} output`, errors);
168
+ }
169
+ const review = evidence.review ?? {};
170
+ if (review.fresh_context !== true) errors.push('review.fresh_context must be true');
171
+ if (review.tests_reviewed_first !== true) errors.push('review.tests_reviewed_first must be true');
172
+ if (review.status !== 'pass') errors.push('review.status must be pass');
173
+ if (!text(review.reviewer)) errors.push('review.reviewer is required');
174
+ evidenceFile(root, review.evidence_path, review.evidence_sha256, 'review evidence', errors);
175
+ const checks = Array.isArray(evidence.completion_checks) ? evidence.completion_checks : [];
176
+ const checksByName = new Map(checks.map((check) => [text(check.name), check]));
177
+ for (const planned of list(task.completion_evidence)) {
178
+ const check = checksByName.get(planned);
179
+ if (!check || check.status !== 'pass') errors.push(`planned completion check has no passing evidence: ${planned}`);
180
+ else evidenceFile(root, check.evidence_path, check.evidence_sha256, `completion check ${planned}`, errors);
181
+ }
182
+ }
183
+ }
184
+ return { status: errors.length ? 'incomplete' : 'complete', path: configured, errors };
185
+ }
186
+
187
+ export function validateTaskReport(deliveryRoot, task) {
188
+ const root = resolve(deliveryRoot);
189
+ const reportPath = resolve(root, text(task?.report_path));
190
+ const errors = [];
191
+ if (!text(task?.report_path) || !isWithin(root, reportPath)) {
192
+ errors.push('report_path is missing or escapes the delivery root');
193
+ } else if (!existsSync(reportPath)) {
194
+ return { status: 'missing', path: relative(root, reportPath).replaceAll('\\', '/'), errors: ['task report does not exist'] };
195
+ } else if (!statSync(reportPath).isFile() || !isRealWithin(root, reportPath)) {
196
+ errors.push('task report must be a real file inside the delivery root');
197
+ } else {
198
+ let content = '';
199
+ let readFailed = false;
200
+ try {
201
+ content = readFileSync(reportPath, 'utf8');
202
+ } catch (error) {
203
+ errors.push(`task report cannot be read: ${error.message}`);
204
+ readFailed = true;
205
+ }
206
+ if (!readFailed) {
207
+ if (!new RegExp(`^# Task Report: ${text(task.id)}\\s*$`, 'm').test(content)) {
208
+ errors.push(`title must identify ${text(task.id)}`);
209
+ }
210
+ const headings = [
211
+ 'Summary', 'Red-green-refactor evidence', 'Feedback loops run', 'Files changed',
212
+ 'Completion evidence', 'Stop conditions', 'Review notes', 'QA notes',
213
+ ];
214
+ for (const heading of headings) {
215
+ if (!section(content, heading)) errors.push(`${heading} section is missing or empty`);
216
+ }
217
+ if (/\{[^}\n]+\}/.test(content)) errors.push('template placeholders remain');
218
+ const completion = section(content, 'Completion evidence');
219
+ if (!/\bpass(?:ed)?\b/i.test(completion)) errors.push('completion evidence has no passing result');
220
+ if (/\b(?:fail|failed)\b/i.test(completion)) errors.push('completion evidence contains a failing result');
221
+ const review = section(content, 'Review notes');
222
+ if (!/Tests reviewed first:\s*yes\b/i.test(review)) errors.push('tests-first review is not confirmed');
223
+ if (!/Fresh-context review:\s*pass\b/i.test(review)) errors.push('fresh-context review has not passed');
224
+ }
225
+ }
226
+ const evidence = validateTaskEvidence(root, task);
227
+ if (evidence.status !== 'complete') errors.push(...evidence.errors);
228
+ return {
229
+ status: errors.length ? 'incomplete' : 'complete',
230
+ path: text(task?.report_path),
231
+ errors,
232
+ evidence,
233
+ };
234
+ }
235
+
236
+ function taskIncomplete(task) {
237
+ const scalar = [
238
+ 'id', 'slice', 'name', 'issue_kind', 'classification', 'repo', 'branch',
239
+ 'user_visible_outcome', 'report_path', 'evidence_path',
240
+ ];
241
+ const arrays = [
242
+ 'depends_on', 'claims', 'read_set', 'write_set', 'allowed_commands',
243
+ 'stop_conditions', 'completion_evidence',
244
+ ];
245
+ const missing = scalar.filter((key) => !text(task?.[key]));
246
+ missing.push(...arrays.filter((key) => !Array.isArray(task?.[key])));
247
+ if (!Number.isFinite(Number(task?.priority))) missing.push('priority');
248
+ if (!Number.isInteger(Number(task?.parallel_wave))) missing.push('parallel_wave');
249
+ for (const key of ['module_interface_contract', 'red_green_refactor', 'ralph_loop', 'review', 'merge']) {
250
+ if (!task?.[key] || typeof task[key] !== 'object' || Array.isArray(task[key])) missing.push(key);
251
+ }
252
+ const contract = task?.module_interface_contract ?? {};
253
+ for (const key of ['module', 'test_boundary']) if (!text(contract[key])) missing.push(`module_interface_contract.${key}`);
254
+ for (const key of ['interface', 'behaviour_owned', 'caller_contract']) {
255
+ if (!list(contract[key]).length) missing.push(`module_interface_contract.${key}`);
256
+ }
257
+ const cycle = task?.red_green_refactor ?? {};
258
+ for (const key of ['red_command', 'expected_red_failure', 'green_command', 'refactor_boundary']) {
259
+ if (!text(cycle[key])) missing.push(`red_green_refactor.${key}`);
260
+ }
261
+ if (task?.review?.fresh_context_required !== true) missing.push('review.fresh_context_required');
262
+ if (task?.review?.review_tests_first !== true) missing.push('review.review_tests_first');
263
+ if (!list(task?.review?.standards_pushed).length) missing.push('review.standards_pushed');
264
+ if (task?.merge?.orchestrator_owned !== true) missing.push('merge.orchestrator_owned');
265
+ if (!list(task?.merge?.post_merge_checks).length) missing.push('merge.post_merge_checks');
266
+ return missing;
267
+ }
268
+
269
+ function cycleIn(tasksById) {
270
+ const visiting = new Set();
271
+ const visited = new Set();
272
+ const visit = (id, trail = []) => {
273
+ if (visiting.has(id)) return [...trail, id];
274
+ if (visited.has(id)) return null;
275
+ visiting.add(id);
276
+ for (const dependency of list(tasksById.get(id)?.depends_on)) {
277
+ if (!tasksById.has(dependency)) continue;
278
+ const cycle = visit(dependency, [...trail, id]);
279
+ if (cycle) return cycle;
280
+ }
281
+ visiting.delete(id);
282
+ visited.add(id);
283
+ return null;
284
+ };
285
+ for (const id of tasksById.keys()) {
286
+ const cycle = visit(id);
287
+ if (cycle) return cycle;
288
+ }
289
+ return null;
290
+ }
291
+
292
+ function taskReadiness(tasks, completedIds = new Set(), reports = new Map()) {
293
+ const executableWave = tasks
294
+ .filter((task) => !completedIds.has(task.id) && list(task.depends_on).every((id) => completedIds.has(id)))
295
+ .reduce((lowest, task) => Math.min(lowest, Number(task.parallel_wave)), Number.POSITIVE_INFINITY);
296
+ return tasks.map((task) => {
297
+ const dependencies = list(task.depends_on);
298
+ const waitingFor = dependencies.filter((id) => !completedIds.has(id));
299
+ const completed = completedIds.has(task.id);
300
+ const requiresHuman = text(task.classification).toUpperCase() === 'HITL';
301
+ const status = completed
302
+ ? 'completed'
303
+ : waitingFor.length
304
+ ? 'blocked-dependency'
305
+ : Number(task.parallel_wave) > executableWave
306
+ ? 'blocked-wave'
307
+ : requiresHuman ? 'awaiting-human' : 'ready';
308
+ return { id: task.id, status, waitingFor, requiresHuman, report: reports.get(task.id) ?? null };
309
+ });
310
+ }
311
+
312
+ export function validateTaskGraph(deliveryRoot, options = {}) {
313
+ const root = resolve(deliveryRoot);
314
+ const path = resolve(root, 'task-graph.json');
315
+ const errors = [];
316
+ const warnings = [];
317
+ const add = (code, message, evidence = '') => errors.push({ code, message, evidence });
318
+ let graph = null;
319
+
320
+ if (!existsSync(path)) {
321
+ add('task-graph-missing', 'task-graph.json does not exist.', relative(root, path));
322
+ } else {
323
+ try {
324
+ graph = JSON.parse(readFileSync(path, 'utf8'));
325
+ } catch (error) {
326
+ add('task-graph-invalid-json', `task-graph.json is not valid JSON: ${error.message}`, 'task-graph.json');
327
+ }
328
+ }
329
+
330
+ if (graph) {
331
+ if (graph.schema_version !== 1) add('task-graph-schema-version', 'schema_version must be 1.', 'schema_version');
332
+ if (options.slug && graph.slug !== options.slug) add('task-graph-slug', `slug must be ${options.slug}.`, 'slug');
333
+ if (!['final', 'ready', 'pass'].includes(text(graph.status).toLowerCase())) {
334
+ add('task-graph-status', 'status must be final, ready, or pass.', 'status');
335
+ }
336
+ if (!text(graph.parent_branch)) add('task-graph-parent-branch', 'parent_branch is required.', 'parent_branch');
337
+ if (graph.repository_branches !== undefined) {
338
+ if (!graph.repository_branches || typeof graph.repository_branches !== 'object' || Array.isArray(graph.repository_branches)) {
339
+ add('task-graph-repository-branches', 'repository_branches must be an object keyed by configured repository name.', 'repository_branches');
340
+ } else {
341
+ for (const [repository, configured] of Object.entries(graph.repository_branches)) {
342
+ const branch = typeof configured === 'string' ? configured : configured?.parent_branch;
343
+ if (!text(repository) || !text(branch)) {
344
+ add('task-graph-repository-branches', 'Every repository_branches entry requires a parent_branch.', `repository_branches.${repository}`);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ for (const key of ['claim_ledger', 'plan_contract', 'destination']) {
350
+ if (!text(graph.source?.[key])) add('task-graph-source', `source.${key} is required.`, `source.${key}`);
351
+ }
352
+ const claimLedger = readJsonWithin(root, graph.source?.claim_ledger, 'source.claim_ledger', errors);
353
+ const planContract = readJsonWithin(root, graph.source?.plan_contract, 'source.plan_contract', errors);
354
+ const destinationPath = resolve(root, text(graph.source?.destination));
355
+ if (text(graph.source?.destination) && (!isWithin(root, destinationPath) || !existsSync(destinationPath))) {
356
+ add('task-graph-source-missing', 'source.destination must be an existing file inside the delivery root.', graph.source.destination);
357
+ }
358
+ if (!graph.parallelism || typeof graph.parallelism !== 'object') {
359
+ add('task-graph-parallelism', 'parallelism is required.', 'parallelism');
360
+ } else {
361
+ if (typeof graph.parallelism.allowed !== 'boolean') add('task-graph-parallelism', 'parallelism.allowed must be boolean.', 'parallelism.allowed');
362
+ if (!Number.isInteger(graph.parallelism.max_parallel_tasks) || graph.parallelism.max_parallel_tasks < 1) {
363
+ add('task-graph-parallelism', 'parallelism.max_parallel_tasks must be a positive integer.', 'parallelism.max_parallel_tasks');
364
+ }
365
+ if (!text(graph.parallelism.rationale)) add('task-graph-parallelism', 'parallelism.rationale is required.', 'parallelism.rationale');
366
+ }
367
+
368
+ const tasks = Array.isArray(graph.tasks) ? graph.tasks : [];
369
+ const waves = Array.isArray(graph.afk_waves) ? graph.afk_waves : [];
370
+ if (!tasks.length) add('task-graph-empty', 'tasks must contain at least one task.', 'tasks');
371
+ if (!waves.length) add('task-graph-waves', 'afk_waves must contain at least one execution wave.', 'afk_waves');
372
+
373
+ const ids = tasks.map((task) => text(task?.id)).filter(Boolean);
374
+ const tasksById = new Map(tasks.map((task) => [text(task?.id), task]));
375
+ if (new Set(ids).size !== ids.length) add('task-id-duplicate', 'Task IDs must be unique.', 'tasks[].id');
376
+ const branches = tasks.map((task) => text(task?.branch)).filter(Boolean);
377
+ if (new Set(branches).size !== branches.length) add('task-branch-duplicate', 'Task branches must be unique.', 'tasks[].branch');
378
+
379
+ const claimOwners = new Map();
380
+ for (const task of tasks) {
381
+ const id = text(task?.id) || '<missing-id>';
382
+ const missing = taskIncomplete(task);
383
+ if (missing.length) add('task-incomplete', `${id} is missing required fields: ${missing.join(', ')}.`, id);
384
+ for (const command of [task?.red_green_refactor?.red_command, task?.red_green_refactor?.green_command].map(text).filter(Boolean)) {
385
+ if (!list(task?.allowed_commands).includes(command)) {
386
+ add('task-command-not-allowed', `${id} red/green command is absent from allowed_commands: ${command}.`, id);
387
+ }
388
+ }
389
+ if (text(task?.id) && !taskIdPattern.test(task.id)) add('task-id-format', `${id} must match T-###.`, id);
390
+ if (!['AFK', 'HITL'].includes(text(task?.classification).toUpperCase())) {
391
+ add('task-classification', `${id} classification must be AFK or HITL.`, id);
392
+ }
393
+ for (const dependency of list(task?.depends_on)) {
394
+ if (!tasksById.has(dependency)) add('task-dependency-missing', `${id} depends on missing task ${dependency}.`, id);
395
+ if (dependency === id) add('task-dependency-self', `${id} cannot depend on itself.`, id);
396
+ }
397
+ for (const claim of list(task?.claims)) {
398
+ if (claimOwners.has(claim)) add('task-claim-duplicate', `${claim} is owned by both ${claimOwners.get(claim)} and ${id}.`, claim);
399
+ else claimOwners.set(claim, id);
400
+ }
401
+ for (const write of list(task?.write_set)) {
402
+ if (isCentralWrite(write) && task?.central_writes_allowed !== true) {
403
+ add('central-write-owned-by-worker', `${id} cannot own central delivery file ${write}.`, id);
404
+ }
405
+ }
406
+ if (text(task?.classification).toUpperCase() === 'AFK') {
407
+ if (!text(task.first_failing_test) && !text(task.no_first_test_rationale)) {
408
+ add('afk-first-test-missing', `${id} requires a first failing test or rationale.`, id);
409
+ }
410
+ if (!list(task.write_set).length) add('afk-write-set-missing', `${id} requires a write_set.`, id);
411
+ if (!list(task.stop_conditions).length) add('afk-stop-conditions-missing', `${id} requires stop conditions.`, id);
412
+ if (!list(task.completion_evidence).length) add('afk-completion-evidence-missing', `${id} requires completion evidence.`, id);
413
+ }
414
+ if (task?.ralph_loop?.allowed === true) {
415
+ if (!Number.isInteger(task.ralph_loop.max_iterations) || task.ralph_loop.max_iterations < 1) {
416
+ add('ralph-loop-unbounded', `${id} Ralph loop requires a finite positive max_iterations.`, id);
417
+ }
418
+ if (!text(task.ralph_loop.completion_promise)) add('ralph-loop-promise-missing', `${id} Ralph loop requires a completion promise.`, id);
419
+ }
420
+ if (task?.merge?.orchestrator_owned !== true) add('merge-not-orchestrator-owned', `${id} merge must be orchestrator-owned.`, id);
421
+ if (task?.review?.fresh_context_required !== true) add('fresh-review-required', `${id} requires fresh-context review.`, id);
422
+ }
423
+
424
+ const ledgerClaims = recordsFrom(claimLedger, ['implementation_claims', 'claims']);
425
+ const actionableClaims = new Set(ledgerClaims.filter(actionableClaim).map(recordId).filter(Boolean));
426
+ if (!ledgerClaims.length) add('claim-ledger-empty', 'The Claim Ledger must contain implementation claims.', graph.source?.claim_ledger);
427
+ for (const claim of claimOwners.keys()) {
428
+ if (!actionableClaims.has(claim)) add('task-claim-unknown', `${claim} is not an actionable Claim Ledger claim.`, claim);
429
+ }
430
+ for (const claim of actionableClaims) {
431
+ if (!claimOwners.has(claim)) add('task-claim-unowned', `${claim} is actionable but has no task owner.`, claim);
432
+ }
433
+
434
+ const ledgerSlices = new Set(recordsFrom(claimLedger, ['slices', 'vertical_slices']).map(recordId).filter(Boolean));
435
+ const planSlices = new Set(recordsFrom(planContract, ['vertical_slices', 'slices']).map(recordId).filter(Boolean));
436
+ if (!ledgerSlices.size) add('claim-ledger-slices-empty', 'The Claim Ledger must identify delivery slices.', graph.source?.claim_ledger);
437
+ if (!planSlices.size) add('plan-contract-slices-empty', 'The Plan Contract must identify vertical slices.', graph.source?.plan_contract);
438
+ for (const task of tasks) {
439
+ if (['orchestration', 'migration-checkpoint'].includes(text(task.issue_kind).toLowerCase())) continue;
440
+ if (!ledgerSlices.has(text(task.slice))) add('task-slice-not-in-ledger', `${task.id} slice ${task.slice} is absent from the Claim Ledger.`, task.id);
441
+ if (!planSlices.has(text(task.slice))) add('task-slice-not-in-plan', `${task.id} slice ${task.slice} is absent from the Plan Contract.`, task.id);
442
+ }
443
+
444
+ const cycle = cycleIn(tasksById);
445
+ if (cycle) add('task-dependency-cycle', `Task dependency cycle detected: ${cycle.join(' -> ')}.`, cycle.join(' -> '));
446
+
447
+ const seenWaves = new Set();
448
+ for (const wave of waves) {
449
+ if (!Number.isInteger(wave?.wave) || wave.wave < 1) add('wave-id-invalid', 'Every wave needs a positive integer wave number.', 'afk_waves');
450
+ if (seenWaves.has(wave?.wave)) add('wave-id-duplicate', `Wave ${wave.wave} is duplicated.`, `wave ${wave.wave}`);
451
+ seenWaves.add(wave?.wave);
452
+ const ready = list(wave?.ready_tasks);
453
+ const parallel = list(wave?.parallel_tasks);
454
+ const mergeOrder = list(wave?.merge_order);
455
+ for (const id of new Set([...ready, ...parallel, ...mergeOrder])) {
456
+ const task = tasksById.get(id);
457
+ if (!task) add('wave-task-missing', `Wave ${wave.wave} references missing task ${id}.`, `wave ${wave.wave}`);
458
+ else if (Number(task.parallel_wave) !== wave.wave) add('wave-task-mismatch', `${id} is assigned to wave ${task.parallel_wave}, not ${wave.wave}.`, id);
459
+ }
460
+ if (new Set(mergeOrder).size !== mergeOrder.length) add('wave-merge-order-duplicate', `Wave ${wave.wave} merge_order contains duplicates.`, `wave ${wave.wave}`);
461
+ for (const id of parallel) {
462
+ if (text(tasksById.get(id)?.classification).toUpperCase() !== 'AFK') {
463
+ add('parallel-task-not-afk', `${id} cannot run in parallel because it is not AFK.`, id);
464
+ }
465
+ }
466
+ for (let left = 0; left < parallel.length; left += 1) {
467
+ for (let right = left + 1; right < parallel.length; right += 1) {
468
+ const a = tasksById.get(parallel[left]);
469
+ const b = tasksById.get(parallel[right]);
470
+ for (const aPath of list(a?.write_set)) {
471
+ for (const bPath of list(b?.write_set)) {
472
+ if (pathsOverlap(aPath, bPath)) {
473
+ add('parallel-write-conflict', `${a.id} and ${b.id} overlap at ${aPath} / ${bPath}.`, `wave ${wave.wave}`);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+ }
480
+ for (const task of tasks) {
481
+ if (!seenWaves.has(Number(task.parallel_wave))) add('task-wave-missing', `${task.id} references missing wave ${task.parallel_wave}.`, task.id);
482
+ for (const dependency of list(task.depends_on)) {
483
+ const target = tasksById.get(dependency);
484
+ if (target && Number(target.parallel_wave) >= Number(task.parallel_wave)) {
485
+ add('task-dependency-wave-order', `${task.id} must run after dependency ${dependency}.`, task.id);
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ const tasks = Array.isArray(graph?.tasks) ? graph.tasks : [];
492
+ const reports = new Map(tasks.map((task) => [task.id, validateTaskReport(root, task)]));
493
+ if (options.requireAllTasksCompleted === true) {
494
+ for (const task of tasks) {
495
+ const report = reports.get(task.id);
496
+ if (report?.status !== 'complete') {
497
+ add(
498
+ 'task-report-incomplete',
499
+ `${task.id} cannot pass this gate because its task report is ${report?.status ?? 'missing'}: ${report?.errors?.join('; ') || 'completion evidence is absent'}.`,
500
+ report?.path ?? task.report_path ?? task.id,
501
+ );
502
+ }
503
+ }
504
+ }
505
+ const completedIds = new Set(options.completedTaskIds ?? tasks
506
+ .filter((task) => reports.get(task.id)?.status === 'complete')
507
+ .map((task) => task.id));
508
+ const readiness = taskReadiness(tasks, completedIds, reports);
509
+ return {
510
+ schema: 'ewai.task-graph-check/v1',
511
+ status: errors.length ? 'fail' : 'pass',
512
+ slug: graph?.slug ?? options.slug ?? '',
513
+ graphPath: relative(root, path).replaceAll('\\', '/'),
514
+ checkedAt: now(),
515
+ errors,
516
+ warnings,
517
+ tasks: readiness,
518
+ readyTasks: readiness.filter((task) => task.status === 'ready').map((task) => task.id),
519
+ completedTasks: readiness.filter((task) => task.status === 'completed').map((task) => task.id),
520
+ };
521
+ }
522
+
523
+ export function renderTaskGraphCheck(result) {
524
+ const findings = result.errors.length
525
+ ? result.errors.map((error) => `- **${error.code}:** ${error.message}`).join('\n')
526
+ : '- No task-graph safety failures found.';
527
+ const rows = result.tasks.map((task) => (
528
+ `| ${task.id} | ${task.status} | ${task.waitingFor.join(', ') || '—'} | ${task.requiresHuman ? 'yes' : 'no'} |`
529
+ )).join('\n');
530
+ return `# Task Graph Check — ${result.slug}\n\n**Verdict:** ${result.status.toUpperCase()}\n**Checked:** ${result.checkedAt}\n\n## Findings\n\n${findings}\n\n## Derived task readiness\n\n| Task | State | Waiting for | Human required |\n|---|---|---|---|\n${rows || '| — | — | — | — |'}\n`;
531
+ }
532
+
533
+ export function writeTaskGraphCheck(deliveryRoot, phase = 'pattern-validation', options = {}) {
534
+ const root = resolve(deliveryRoot);
535
+ const result = validateTaskGraph(root, {
536
+ ...options,
537
+ requireAllTasksCompleted: options.requireAllTasksCompleted ?? ['build', 'delivery'].includes(phase),
538
+ });
539
+ const gateRoot = resolve(root, 'gates', phase);
540
+ if (!isWithin(root, gateRoot)) throw new Error(`Unsafe task-graph gate path: ${gateRoot}`);
541
+ atomicJson(resolve(gateRoot, 'task-graph-check.json'), result);
542
+ atomicText(resolve(gateRoot, 'task-graph-check.md'), renderTaskGraphCheck(result));
543
+ return result;
544
+ }