@hybridlabor-api/aos 4.6.6 → 4.7.1

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 (503) hide show
  1. package/.agents/agents.md +9 -3
  2. package/.agents/nodes.json +7 -2
  3. package/.agents/state.schema.json +26 -1
  4. package/.agents/vendor-manifest.json +56 -0
  5. package/.agents/workflows/startcycle-graph.md +2 -2
  6. package/.claude/agents/architect.md +7 -2
  7. package/.claude/agents/security-reviewer.md +1 -1
  8. package/.claude/agents/techlead.md +2 -0
  9. package/.claude/hooks/memb-inject.mjs +257 -135
  10. package/.claude/hooks/trail-relay.mjs +18 -0
  11. package/.claude/settings.json +70 -0
  12. package/.claude/workflows/startcycle-dispatch.mjs +203 -17
  13. package/.opencode/agents/architect.md +7 -2
  14. package/.opencode/agents/techlead.md +2 -0
  15. package/.opencode/plugins/bdb-aos.js +75 -0
  16. package/THIRD_PARTY_NOTICES.md +57 -1
  17. package/bin/aos-doctor.mjs +366 -0
  18. package/bin/aos-store.mjs +167 -0
  19. package/bin/aos-uninstall.mjs +55 -1
  20. package/docs/AUDIT_BRIEF_ECC_STORE.md +122 -0
  21. package/docs/PLAN_AOS_STORE_PHASE1.md +190 -0
  22. package/docs/handover/agenttrail-for-ao.md +58 -0
  23. package/docs/handover/deja-for-ao.md +51 -0
  24. package/installer.js +182 -38
  25. package/lib/aos-archify-contract.mjs +277 -0
  26. package/lib/ecc-store-index.json +1 -0
  27. package/mcp_config.json +8 -0
  28. package/mcps/mcsc/README.md +24 -0
  29. package/mcps/mcsc/package-lock.json +1234 -0
  30. package/mcps/mcsc/package.json +14 -0
  31. package/mcps/mcsc/packages/core/bin/mcsc.js +162 -0
  32. package/mcps/mcsc/packages/core/rulebook.default.yaml +49 -0
  33. package/mcps/mcsc/packages/core/schema/rulebook.schema.json +56 -0
  34. package/mcps/mcsc/packages/core/src/adapters/agy.js +107 -0
  35. package/mcps/mcsc/packages/core/src/adapters/codex.js +179 -0
  36. package/mcps/mcsc/packages/core/src/adapters/native.js +14 -0
  37. package/mcps/mcsc/packages/core/src/adapters/opencode.js +183 -0
  38. package/mcps/mcsc/packages/core/src/adapters/types.js +21 -0
  39. package/mcps/mcsc/packages/core/src/capabilities/buildOfferTable.js +34 -0
  40. package/mcps/mcsc/packages/core/src/capabilities/registry.js +99 -0
  41. package/mcps/mcsc/packages/core/src/capabilities/types.js +24 -0
  42. package/mcps/mcsc/packages/core/src/detect/cache.js +47 -0
  43. package/mcps/mcsc/packages/core/src/detect/inventory.js +73 -0
  44. package/mcps/mcsc/packages/core/src/detect/probes.js +158 -0
  45. package/mcps/mcsc/packages/core/src/detect/types.js +26 -0
  46. package/mcps/mcsc/packages/core/src/rulebook/load.js +53 -0
  47. package/mcps/mcsc/packages/core/src/rulebook/resolve.js +227 -0
  48. package/mcps/mcsc/packages/core/src/rulebook/rulebook.types.js +89 -0
  49. package/mcps/mcsc/packages/core/src/trail.js +17 -0
  50. package/mcps/mcsc/packages/mcp/server.js +302 -0
  51. package/mcps/mcsc/server.js +2 -0
  52. package/package.json +8 -2
  53. package/scripts/build-ecc-store-index.mjs +147 -0
  54. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  55. package/skills/basic/godmode-ui-ux/SKILL.md +2 -2
  56. package/skills/basic/godmode-ui-ux/references/anti-slop-rules.md +44 -40
  57. package/skills/basic/godmode-ui-ux/references/taste-archetypes.md +49 -0
  58. package/skills/basic/startcycle/SKILL.md +7 -0
  59. package/skills/basic/startcycle-graph/SKILL.md +25 -0
  60. package/skills/basic/startcycle-graph-user/SKILL.md +3 -0
  61. package/skills/basic/teamwork-preview/SKILL.md +2 -0
  62. package/skills/bdbrainstorm/SKILL.md +1 -0
  63. package/skills/global_config/agenttrail/LICENSE +21 -0
  64. package/skills/global_config/agenttrail/SKILL.md +48 -0
  65. package/skills/global_config/agenttrail/bin/agenttrail.mjs +1113 -0
  66. package/skills/global_config/agenttrail/public/index.html +662 -0
  67. package/skills/global_config/aos-project-init/SKILL.md +25 -5
  68. package/skills/global_config/aos-setup/SKILL.md +34 -1
  69. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +115 -1
  70. package/skills/global_config/archify/LICENSE +22 -0
  71. package/skills/global_config/archify/SKILL.md +140 -0
  72. package/skills/global_config/archify/THIRD_PARTY_NOTICES.md +69 -0
  73. package/skills/global_config/archify/assets/JetBrainsMono-OFL.txt +93 -0
  74. package/skills/global_config/archify/assets/template.html +14937 -0
  75. package/skills/global_config/archify/bin/archify.mjs +2139 -0
  76. package/skills/global_config/archify/bin/open-artifact.mjs +86 -0
  77. package/skills/global_config/archify/bin/preview.mjs +653 -0
  78. package/skills/global_config/archify/bin/visual-check.mjs +829 -0
  79. package/skills/global_config/archify/brand-marks/README.md +31 -0
  80. package/skills/global_config/archify/brand-marks/catalog.json +131 -0
  81. package/skills/global_config/archify/delta/architecture-delta.mjs +1232 -0
  82. package/skills/global_config/archify/examples/agent-run.lifecycle.json +60 -0
  83. package/skills/global_config/archify/examples/agent-tool-call.workflow.json +94 -0
  84. package/skills/global_config/archify/examples/async-job-roundtrip.sequence.json +61 -0
  85. package/skills/global_config/archify/examples/brand-aware-delivery.architecture.json +47 -0
  86. package/skills/global_config/archify/examples/cache-miss-request.sequence.json +82 -0
  87. package/skills/global_config/archify/examples/checkout-platform.base.architecture.json +31 -0
  88. package/skills/global_config/archify/examples/checkout-platform.head.architecture.json +31 -0
  89. package/skills/global_config/archify/examples/dataflow-product-analytics.html +15048 -0
  90. package/skills/global_config/archify/examples/deployment-release.lifecycle.json +49 -0
  91. package/skills/global_config/archify/examples/event-stream.dataflow.json +57 -0
  92. package/skills/global_config/archify/examples/incident-response.workflow.json +64 -0
  93. package/skills/global_config/archify/examples/lifecycle-agent-run.html +14983 -0
  94. package/skills/global_config/archify/examples/product-analytics.dataflow.json +76 -0
  95. package/skills/global_config/archify/examples/production-deployment.architecture.json +71 -0
  96. package/skills/global_config/archify/examples/release-delivery.workflow.json +62 -0
  97. package/skills/global_config/archify/examples/sequence-cache-miss-request.html +15063 -0
  98. package/skills/global_config/archify/examples/web-app-rendered.html +15012 -0
  99. package/skills/global_config/archify/examples/web-app.architecture.json +46 -0
  100. package/skills/global_config/archify/examples/workflow-agent-tool-call-rendered.html +15054 -0
  101. package/skills/global_config/archify/migrations/workflow-v2.mjs +279 -0
  102. package/skills/global_config/archify/package.json +39 -0
  103. package/skills/global_config/archify/recipes/scenarios.mjs +391 -0
  104. package/skills/global_config/archify/references/authoring-contract.md +247 -0
  105. package/skills/global_config/archify/references/brand-marks.md +65 -0
  106. package/skills/global_config/archify/references/delivery-contract.md +138 -0
  107. package/skills/global_config/archify/references/viewer-runtime.md +45 -0
  108. package/skills/global_config/archify/renderers/architecture/grid.mjs +62 -0
  109. package/skills/global_config/archify/renderers/architecture/render-architecture.mjs +1078 -0
  110. package/skills/global_config/archify/renderers/dataflow/README.md +104 -0
  111. package/skills/global_config/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  112. package/skills/global_config/archify/renderers/lifecycle/README.md +115 -0
  113. package/skills/global_config/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  114. package/skills/global_config/archify/renderers/sequence/README.md +114 -0
  115. package/skills/global_config/archify/renderers/sequence/render-sequence.mjs +464 -0
  116. package/skills/global_config/archify/renderers/shared/brand-marks.mjs +563 -0
  117. package/skills/global_config/archify/renderers/shared/cli.mjs +218 -0
  118. package/skills/global_config/archify/renderers/shared/desktop-readability.mjs +26 -0
  119. package/skills/global_config/archify/renderers/shared/diagnostics.mjs +160 -0
  120. package/skills/global_config/archify/renderers/shared/engineering-profiles.mjs +157 -0
  121. package/skills/global_config/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  122. package/skills/global_config/archify/renderers/shared/generated-validators.mjs +13 -0
  123. package/skills/global_config/archify/renderers/shared/geometry.mjs +1423 -0
  124. package/skills/global_config/archify/renderers/shared/i18n.mjs +595 -0
  125. package/skills/global_config/archify/renderers/shared/layout-report.mjs +40 -0
  126. package/skills/global_config/archify/renderers/shared/legend.mjs +217 -0
  127. package/skills/global_config/archify/renderers/shared/output-path.mjs +340 -0
  128. package/skills/global_config/archify/renderers/shared/repository-evidence.mjs +239 -0
  129. package/skills/global_config/archify/renderers/shared/repository-location.mjs +58 -0
  130. package/skills/global_config/archify/renderers/shared/text-fit.mjs +49 -0
  131. package/skills/global_config/archify/renderers/shared/utils.mjs +232 -0
  132. package/skills/global_config/archify/renderers/shared/validator.mjs +86 -0
  133. package/skills/global_config/archify/renderers/workflow/README.md +223 -0
  134. package/skills/global_config/archify/renderers/workflow/render-workflow.mjs +35 -0
  135. package/skills/global_config/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  136. package/skills/global_config/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  137. package/skills/global_config/archify/schemas/README.md +211 -0
  138. package/skills/global_config/archify/schemas/architecture.schema.json +178 -0
  139. package/skills/global_config/archify/schemas/common.schema.json +115 -0
  140. package/skills/global_config/archify/schemas/dataflow.schema.json +243 -0
  141. package/skills/global_config/archify/schemas/lifecycle.schema.json +266 -0
  142. package/skills/global_config/archify/schemas/sequence.schema.json +223 -0
  143. package/skills/global_config/archify/schemas/workflow.schema.json +428 -0
  144. package/skills/global_config/archify/scripts/check-render-output.mjs +916 -0
  145. package/skills/global_config/archify/scripts/generate-brand-marks.mjs +141 -0
  146. package/skills/global_config/archify/scripts/generate-validators.mjs +66 -0
  147. package/skills/global_config/archify/scripts/render-examples.mjs +26 -0
  148. package/skills/global_config/archify/skill-release.json +10 -0
  149. package/skills/global_config/ask-tim/SKILL.md +10 -0
  150. package/skills/global_config/bdb-security-audit/SKILL.md +109 -0
  151. package/skills/global_config/bdb-security-audit/references/audit-checklists.md +172 -0
  152. package/skills/global_config/bdb-security-audit/references/skylos-guide.md +139 -0
  153. package/skills/global_config/bdb-security-audit/references/xalgorix-bridge.md +110 -0
  154. package/skills/global_config/bdbdesignpro/SKILL.md +4 -4
  155. package/skills/global_config/bdbdesignpro/references/bdb-motion-tokens.md +52 -31
  156. package/skills/global_config/bdbresilience/SKILL.md +10 -0
  157. package/skills/global_config/brag/SKILL.md +179 -0
  158. package/skills/global_config/brag/assets/music/README.md +20 -0
  159. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.json +3846 -0
  160. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.md +35 -0
  161. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.json +1826 -0
  162. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.md +35 -0
  163. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.json +2266 -0
  164. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.md +35 -0
  165. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.json +2806 -0
  166. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.md +35 -0
  167. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.json +2796 -0
  168. package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.md +35 -0
  169. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-1-by-ende-dot-app.mp3 +0 -0
  170. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-10-by-ende-dot-app.mp3 +0 -0
  171. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-11-by-ende-dot-app.mp3 +0 -0
  172. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-12-by-ende-dot-app.mp3 +0 -0
  173. package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-9-by-ende-dot-app.mp3 +0 -0
  174. package/skills/global_config/brag/assets/sfx/casino/card-fan-1.ogg +0 -0
  175. package/skills/global_config/brag/assets/sfx/casino/card-fan-2.ogg +0 -0
  176. package/skills/global_config/brag/assets/sfx/casino/card-place-1.ogg +0 -0
  177. package/skills/global_config/brag/assets/sfx/casino/card-place-2.ogg +0 -0
  178. package/skills/global_config/brag/assets/sfx/casino/card-place-3.ogg +0 -0
  179. package/skills/global_config/brag/assets/sfx/casino/card-place-4.ogg +0 -0
  180. package/skills/global_config/brag/assets/sfx/casino/card-shove-1.ogg +0 -0
  181. package/skills/global_config/brag/assets/sfx/casino/card-shove-2.ogg +0 -0
  182. package/skills/global_config/brag/assets/sfx/casino/card-shove-3.ogg +0 -0
  183. package/skills/global_config/brag/assets/sfx/casino/card-shove-4.ogg +0 -0
  184. package/skills/global_config/brag/assets/sfx/casino/card-shuffle.ogg +0 -0
  185. package/skills/global_config/brag/assets/sfx/casino/card-slide-1.ogg +0 -0
  186. package/skills/global_config/brag/assets/sfx/casino/card-slide-2.ogg +0 -0
  187. package/skills/global_config/brag/assets/sfx/casino/card-slide-3.ogg +0 -0
  188. package/skills/global_config/brag/assets/sfx/casino/card-slide-4.ogg +0 -0
  189. package/skills/global_config/brag/assets/sfx/casino/card-slide-5.ogg +0 -0
  190. package/skills/global_config/brag/assets/sfx/casino/card-slide-6.ogg +0 -0
  191. package/skills/global_config/brag/assets/sfx/casino/card-slide-7.ogg +0 -0
  192. package/skills/global_config/brag/assets/sfx/casino/card-slide-8.ogg +0 -0
  193. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-1.ogg +0 -0
  194. package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-2.ogg +0 -0
  195. package/skills/global_config/brag/assets/sfx/casino/chip-lay-1.ogg +0 -0
  196. package/skills/global_config/brag/assets/sfx/casino/chip-lay-2.ogg +0 -0
  197. package/skills/global_config/brag/assets/sfx/casino/chip-lay-3.ogg +0 -0
  198. package/skills/global_config/brag/assets/sfx/casino/chips-collide-1.ogg +0 -0
  199. package/skills/global_config/brag/assets/sfx/casino/chips-collide-2.ogg +0 -0
  200. package/skills/global_config/brag/assets/sfx/casino/chips-collide-3.ogg +0 -0
  201. package/skills/global_config/brag/assets/sfx/casino/chips-collide-4.ogg +0 -0
  202. package/skills/global_config/brag/assets/sfx/casino/chips-handle-1.ogg +0 -0
  203. package/skills/global_config/brag/assets/sfx/casino/chips-handle-2.ogg +0 -0
  204. package/skills/global_config/brag/assets/sfx/casino/chips-handle-3.ogg +0 -0
  205. package/skills/global_config/brag/assets/sfx/casino/chips-handle-4.ogg +0 -0
  206. package/skills/global_config/brag/assets/sfx/casino/chips-handle-6.ogg +0 -0
  207. package/skills/global_config/brag/assets/sfx/casino/chips-stack-1.ogg +0 -0
  208. package/skills/global_config/brag/assets/sfx/casino/chips-stack-2.ogg +0 -0
  209. package/skills/global_config/brag/assets/sfx/casino/chips-stack-3.ogg +0 -0
  210. package/skills/global_config/brag/assets/sfx/casino/chips-stack-4.ogg +0 -0
  211. package/skills/global_config/brag/assets/sfx/casino/chips-stack-5.ogg +0 -0
  212. package/skills/global_config/brag/assets/sfx/casino/chips-stack-6.ogg +0 -0
  213. package/skills/global_config/brag/assets/sfx/casino/dice-grab-1.ogg +0 -0
  214. package/skills/global_config/brag/assets/sfx/casino/dice-grab-2.ogg +0 -0
  215. package/skills/global_config/brag/assets/sfx/casino/dice-shake-1.ogg +0 -0
  216. package/skills/global_config/brag/assets/sfx/casino/dice-shake-2.ogg +0 -0
  217. package/skills/global_config/brag/assets/sfx/casino/dice-shake-3.ogg +0 -0
  218. package/skills/global_config/brag/assets/sfx/casino/dice-throw-1.ogg +0 -0
  219. package/skills/global_config/brag/assets/sfx/casino/dice-throw-2.ogg +0 -0
  220. package/skills/global_config/brag/assets/sfx/casino/dice-throw-3.ogg +0 -0
  221. package/skills/global_config/brag/assets/sfx/casino/die-throw-1.ogg +0 -0
  222. package/skills/global_config/brag/assets/sfx/casino/die-throw-2.ogg +0 -0
  223. package/skills/global_config/brag/assets/sfx/casino/die-throw-3.ogg +0 -0
  224. package/skills/global_config/brag/assets/sfx/casino/die-throw-4.ogg +0 -0
  225. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_000.ogg +0 -0
  226. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_003.ogg +0 -0
  227. package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_004.ogg +0 -0
  228. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_000.ogg +0 -0
  229. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_001.ogg +0 -0
  230. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_002.ogg +0 -0
  231. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_003.ogg +0 -0
  232. package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_004.ogg +0 -0
  233. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_000.ogg +0 -0
  234. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_001.ogg +0 -0
  235. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_002.ogg +0 -0
  236. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_003.ogg +0 -0
  237. package/skills/global_config/brag/assets/sfx/impact/footstep_grass_004.ogg +0 -0
  238. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_000.ogg +0 -0
  239. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_001.ogg +0 -0
  240. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_002.ogg +0 -0
  241. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_003.ogg +0 -0
  242. package/skills/global_config/brag/assets/sfx/impact/footstep_snow_004.ogg +0 -0
  243. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_000.ogg +0 -0
  244. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_001.ogg +0 -0
  245. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_002.ogg +0 -0
  246. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_003.ogg +0 -0
  247. package/skills/global_config/brag/assets/sfx/impact/footstep_wood_004.ogg +0 -0
  248. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_000.ogg +0 -0
  249. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_003.ogg +0 -0
  250. package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_004.ogg +0 -0
  251. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_000.ogg +0 -0
  252. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_001.ogg +0 -0
  253. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_002.ogg +0 -0
  254. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_003.ogg +0 -0
  255. package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_004.ogg +0 -0
  256. package/skills/global_config/brag/assets/sfx/impact/impactGlass_heavy_002.ogg +0 -0
  257. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_001.ogg +0 -0
  258. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_002.ogg +0 -0
  259. package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_003.ogg +0 -0
  260. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_000.ogg +0 -0
  261. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_002.ogg +0 -0
  262. package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_004.ogg +0 -0
  263. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_000.ogg +0 -0
  264. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_002.ogg +0 -0
  265. package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_004.ogg +0 -0
  266. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_002.ogg +0 -0
  267. package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_003.ogg +0 -0
  268. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_000.ogg +0 -0
  269. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_001.ogg +0 -0
  270. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_002.ogg +0 -0
  271. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_003.ogg +0 -0
  272. package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_004.ogg +0 -0
  273. package/skills/global_config/brag/assets/sfx/impact/impactMining_001.ogg +0 -0
  274. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_000.ogg +0 -0
  275. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_001.ogg +0 -0
  276. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_002.ogg +0 -0
  277. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_003.ogg +0 -0
  278. package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_004.ogg +0 -0
  279. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_000.ogg +0 -0
  280. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_001.ogg +0 -0
  281. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_002.ogg +0 -0
  282. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_003.ogg +0 -0
  283. package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_004.ogg +0 -0
  284. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_000.ogg +0 -0
  285. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_001.ogg +0 -0
  286. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_002.ogg +0 -0
  287. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_003.ogg +0 -0
  288. package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_004.ogg +0 -0
  289. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_000.ogg +0 -0
  290. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_001.ogg +0 -0
  291. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_002.ogg +0 -0
  292. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_003.ogg +0 -0
  293. package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_004.ogg +0 -0
  294. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_000.ogg +0 -0
  295. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_001.ogg +0 -0
  296. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_002.ogg +0 -0
  297. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_003.ogg +0 -0
  298. package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_004.ogg +0 -0
  299. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_000.ogg +0 -0
  300. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_001.ogg +0 -0
  301. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_002.ogg +0 -0
  302. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_003.ogg +0 -0
  303. package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_004.ogg +0 -0
  304. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_000.ogg +0 -0
  305. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_001.ogg +0 -0
  306. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_002.ogg +0 -0
  307. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_003.ogg +0 -0
  308. package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_004.ogg +0 -0
  309. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_000.ogg +0 -0
  310. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_001.ogg +0 -0
  311. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_002.ogg +0 -0
  312. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_003.ogg +0 -0
  313. package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_004.ogg +0 -0
  314. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_000.ogg +0 -0
  315. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_001.ogg +0 -0
  316. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_002.ogg +0 -0
  317. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_003.ogg +0 -0
  318. package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_004.ogg +0 -0
  319. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_000.ogg +0 -0
  320. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_001.ogg +0 -0
  321. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_002.ogg +0 -0
  322. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_003.ogg +0 -0
  323. package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_004.ogg +0 -0
  324. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_000.ogg +0 -0
  325. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_001.ogg +0 -0
  326. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_002.ogg +0 -0
  327. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_003.ogg +0 -0
  328. package/skills/global_config/brag/assets/sfx/impact/impactWood_light_004.ogg +0 -0
  329. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_000.ogg +0 -0
  330. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_001.ogg +0 -0
  331. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_002.ogg +0 -0
  332. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_003.ogg +0 -0
  333. package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_004.ogg +0 -0
  334. package/skills/global_config/brag/assets/sfx/interface/bong_001.ogg +0 -0
  335. package/skills/global_config/brag/assets/sfx/interface/click_001.ogg +0 -0
  336. package/skills/global_config/brag/assets/sfx/interface/click_002.ogg +0 -0
  337. package/skills/global_config/brag/assets/sfx/interface/click_003.ogg +0 -0
  338. package/skills/global_config/brag/assets/sfx/interface/click_004.ogg +0 -0
  339. package/skills/global_config/brag/assets/sfx/interface/click_005.ogg +0 -0
  340. package/skills/global_config/brag/assets/sfx/interface/drop_001.ogg +0 -0
  341. package/skills/global_config/brag/assets/sfx/interface/drop_002.ogg +0 -0
  342. package/skills/global_config/brag/assets/sfx/interface/drop_003.ogg +0 -0
  343. package/skills/global_config/brag/assets/sfx/interface/error_005.ogg +0 -0
  344. package/skills/global_config/brag/assets/sfx/interface/error_006.ogg +0 -0
  345. package/skills/global_config/brag/assets/sfx/interface/glitch_002.ogg +0 -0
  346. package/skills/global_config/brag/assets/sfx/interface/glitch_004.ogg +0 -0
  347. package/skills/global_config/brag/assets/sfx/interface/select_008.ogg +0 -0
  348. package/skills/global_config/brag/assets/sfx/interface/switch_001.ogg +0 -0
  349. package/skills/global_config/brag/assets/sfx/interface/switch_002.ogg +0 -0
  350. package/skills/global_config/brag/assets/sfx/interface/switch_004.ogg +0 -0
  351. package/skills/global_config/brag/assets/sfx/interface/switch_005.ogg +0 -0
  352. package/skills/global_config/brag/assets/sfx/interface/switch_006.ogg +0 -0
  353. package/skills/global_config/brag/assets/sfx/interface/switch_007.ogg +0 -0
  354. package/skills/global_config/brag/assets/sfx/keyboard/keypress-001.wav +0 -0
  355. package/skills/global_config/brag/assets/sfx/keyboard/keypress-002.wav +0 -0
  356. package/skills/global_config/brag/assets/sfx/keyboard/keypress-003.wav +0 -0
  357. package/skills/global_config/brag/assets/sfx/keyboard/keypress-004.wav +0 -0
  358. package/skills/global_config/brag/assets/sfx/keyboard/keypress-005.wav +0 -0
  359. package/skills/global_config/brag/assets/sfx/keyboard/keypress-006.wav +0 -0
  360. package/skills/global_config/brag/assets/sfx/keyboard/keypress-007.wav +0 -0
  361. package/skills/global_config/brag/assets/sfx/keyboard/keypress-008.wav +0 -0
  362. package/skills/global_config/brag/assets/sfx/keyboard/keypress-009.wav +0 -0
  363. package/skills/global_config/brag/assets/sfx/keyboard/keypress-010.wav +0 -0
  364. package/skills/global_config/brag/assets/sfx/keyboard/keypress-011.wav +0 -0
  365. package/skills/global_config/brag/assets/sfx/keyboard/keypress-012.wav +0 -0
  366. package/skills/global_config/brag/assets/sfx/keyboard/keypress-013.wav +0 -0
  367. package/skills/global_config/brag/assets/sfx/keyboard/keypress-014.wav +0 -0
  368. package/skills/global_config/brag/assets/sfx/keyboard/keypress-015.wav +0 -0
  369. package/skills/global_config/brag/assets/sfx/keyboard/keypress-016.wav +0 -0
  370. package/skills/global_config/brag/assets/sfx/keyboard/keypress-017.wav +0 -0
  371. package/skills/global_config/brag/assets/sfx/keyboard/keypress-018.wav +0 -0
  372. package/skills/global_config/brag/assets/sfx/keyboard/keypress-019.wav +0 -0
  373. package/skills/global_config/brag/assets/sfx/keyboard/keypress-020.wav +0 -0
  374. package/skills/global_config/brag/assets/sfx/keyboard/keypress-021.wav +0 -0
  375. package/skills/global_config/brag/assets/sfx/keyboard/keypress-022.wav +0 -0
  376. package/skills/global_config/brag/assets/sfx/keyboard/keypress-023.wav +0 -0
  377. package/skills/global_config/brag/assets/sfx/keyboard/keypress-024.wav +0 -0
  378. package/skills/global_config/brag/assets/sfx/keyboard/keypress-025.wav +0 -0
  379. package/skills/global_config/brag/assets/sfx/keyboard/keypress-026.wav +0 -0
  380. package/skills/global_config/brag/assets/sfx/keyboard/keypress-027.wav +0 -0
  381. package/skills/global_config/brag/assets/sfx/keyboard/keypress-028.wav +0 -0
  382. package/skills/global_config/brag/assets/sfx/keyboard/keypress-029.wav +0 -0
  383. package/skills/global_config/brag/assets/sfx/keyboard/keypress-030.wav +0 -0
  384. package/skills/global_config/brag/assets/sfx/keyboard/keypress-031.wav +0 -0
  385. package/skills/global_config/brag/assets/sfx/keyboard/keypress-032.wav +0 -0
  386. package/skills/global_config/brag/assets/sfx/sfx-analysis.json +15584 -0
  387. package/skills/global_config/brag/assets/sfx/sfx-analysis.md +75 -0
  388. package/skills/global_config/brag/assets/sfx/ui/click1.ogg +0 -0
  389. package/skills/global_config/brag/assets/sfx/ui/click2.ogg +0 -0
  390. package/skills/global_config/brag/assets/sfx/ui/click3.ogg +0 -0
  391. package/skills/global_config/brag/assets/sfx/ui/click4.ogg +0 -0
  392. package/skills/global_config/brag/assets/sfx/ui/click5.ogg +0 -0
  393. package/skills/global_config/brag/assets/sfx/ui/mouseclick1.ogg +0 -0
  394. package/skills/global_config/brag/assets/sfx/ui/rollover1.ogg +0 -0
  395. package/skills/global_config/brag/assets/sfx/ui/rollover2.ogg +0 -0
  396. package/skills/global_config/brag/assets/sfx/ui/rollover4.ogg +0 -0
  397. package/skills/global_config/brag/assets/sfx/ui/rollover5.ogg +0 -0
  398. package/skills/global_config/brag/assets/sfx/ui/switch1.ogg +0 -0
  399. package/skills/global_config/brag/assets/sfx/ui/switch10.ogg +0 -0
  400. package/skills/global_config/brag/assets/sfx/ui/switch11.ogg +0 -0
  401. package/skills/global_config/brag/assets/sfx/ui/switch12.ogg +0 -0
  402. package/skills/global_config/brag/assets/sfx/ui/switch13.ogg +0 -0
  403. package/skills/global_config/brag/assets/sfx/ui/switch14.ogg +0 -0
  404. package/skills/global_config/brag/assets/sfx/ui/switch15.ogg +0 -0
  405. package/skills/global_config/brag/assets/sfx/ui/switch16.ogg +0 -0
  406. package/skills/global_config/brag/assets/sfx/ui/switch17.ogg +0 -0
  407. package/skills/global_config/brag/assets/sfx/ui/switch18.ogg +0 -0
  408. package/skills/global_config/brag/assets/sfx/ui/switch19.ogg +0 -0
  409. package/skills/global_config/brag/assets/sfx/ui/switch2.ogg +0 -0
  410. package/skills/global_config/brag/assets/sfx/ui/switch20.ogg +0 -0
  411. package/skills/global_config/brag/assets/sfx/ui/switch21.ogg +0 -0
  412. package/skills/global_config/brag/assets/sfx/ui/switch22.ogg +0 -0
  413. package/skills/global_config/brag/assets/sfx/ui/switch23.ogg +0 -0
  414. package/skills/global_config/brag/assets/sfx/ui/switch24.ogg +0 -0
  415. package/skills/global_config/brag/assets/sfx/ui/switch25.ogg +0 -0
  416. package/skills/global_config/brag/assets/sfx/ui/switch26.ogg +0 -0
  417. package/skills/global_config/brag/assets/sfx/ui/switch27.ogg +0 -0
  418. package/skills/global_config/brag/assets/sfx/ui/switch28.ogg +0 -0
  419. package/skills/global_config/brag/assets/sfx/ui/switch29.ogg +0 -0
  420. package/skills/global_config/brag/assets/sfx/ui/switch3.ogg +0 -0
  421. package/skills/global_config/brag/assets/sfx/ui/switch30.ogg +0 -0
  422. package/skills/global_config/brag/assets/sfx/ui/switch31.ogg +0 -0
  423. package/skills/global_config/brag/assets/sfx/ui/switch32.ogg +0 -0
  424. package/skills/global_config/brag/assets/sfx/ui/switch33.ogg +0 -0
  425. package/skills/global_config/brag/assets/sfx/ui/switch34.ogg +0 -0
  426. package/skills/global_config/brag/assets/sfx/ui/switch35.ogg +0 -0
  427. package/skills/global_config/brag/assets/sfx/ui/switch36.ogg +0 -0
  428. package/skills/global_config/brag/assets/sfx/ui/switch37.ogg +0 -0
  429. package/skills/global_config/brag/assets/sfx/ui/switch38.ogg +0 -0
  430. package/skills/global_config/brag/assets/sfx/ui/switch4.ogg +0 -0
  431. package/skills/global_config/brag/assets/sfx/ui/switch5.ogg +0 -0
  432. package/skills/global_config/brag/assets/sfx/ui/switch6.ogg +0 -0
  433. package/skills/global_config/brag/assets/sfx/ui/switch7.ogg +0 -0
  434. package/skills/global_config/brag/assets/sfx/ui/switch8.ogg +0 -0
  435. package/skills/global_config/brag/assets/sfx/ui/switch9.ogg +0 -0
  436. package/skills/global_config/brag/references/audio.md +307 -0
  437. package/skills/global_config/brag/references/step-1-inspect.md +133 -0
  438. package/skills/global_config/brag/references/step-2-plan.md +234 -0
  439. package/skills/global_config/brag/references/step-3-compose.md +210 -0
  440. package/skills/global_config/brag/references/step-4-deliver.md +168 -0
  441. package/skills/global_config/brag/references/tones.md +231 -0
  442. package/skills/global_config/brag/scripts/analyze_music_cues.py +309 -0
  443. package/skills/global_config/brag/scripts/pyproject.toml +14 -0
  444. package/skills/global_config/brag/scripts/uv.lock +819 -0
  445. package/skills/global_config/deja-memory/SKILL.md +42 -0
  446. package/skills/global_config/editable-design/LICENSE +202 -0
  447. package/skills/global_config/editable-design/README.md +34 -0
  448. package/skills/global_config/editable-design/SKILL.md +630 -0
  449. package/skills/global_config/editable-design/THIRD_PARTY_NOTICES.md +10 -0
  450. package/skills/global_config/editable-design/agents/openai.yaml +4 -0
  451. package/skills/global_config/editable-design/assets/editor/layer-editor.css +262 -0
  452. package/skills/global_config/editable-design/assets/editor/layer-editor.js +1929 -0
  453. package/skills/global_config/editable-design/assets/poster-starter/README.md +92 -0
  454. package/skills/global_config/editable-design/assets/poster-starter/assets/.gitkeep +0 -0
  455. package/skills/global_config/editable-design/assets/poster-starter/index.html +91 -0
  456. package/skills/global_config/editable-design/assets/poster-starter/poster.json +6 -0
  457. package/skills/global_config/editable-design/assets/poster-starter/reference/.gitkeep +1 -0
  458. package/skills/global_config/editable-design/assets/replay/icons/assets.png +0 -0
  459. package/skills/global_config/editable-design/assets/replay/icons/html.png +0 -0
  460. package/skills/global_config/editable-design/assets/replay/icons/input.png +0 -0
  461. package/skills/global_config/editable-design/assets/replay/icons/layers.png +0 -0
  462. package/skills/global_config/editable-design/assets/replay/icons/plan.png +0 -0
  463. package/skills/global_config/editable-design/assets/replay/icons/reference.png +0 -0
  464. package/skills/global_config/editable-design/assets/replay/icons/review.png +0 -0
  465. package/skills/global_config/editable-design/assets/replay/index.html +59 -0
  466. package/skills/global_config/editable-design/assets/replay/replay-motion.js +410 -0
  467. package/skills/global_config/editable-design/assets/replay/replay.css +380 -0
  468. package/skills/global_config/editable-design/assets/replay/replay.js +374 -0
  469. package/skills/global_config/editable-design/font-kit/package-lock.json +115 -0
  470. package/skills/global_config/editable-design/font-kit/package.json +19 -0
  471. package/skills/global_config/editable-design/pack.sh +20 -0
  472. package/skills/global_config/editable-design/references/asset-architecture.md +72 -0
  473. package/skills/global_config/editable-design/references/editor-pitfalls.md +70 -0
  474. package/skills/global_config/editable-design/references/editor-runtime.md +131 -0
  475. package/skills/global_config/editable-design/references/font-system.md +89 -0
  476. package/skills/global_config/editable-design/references/imagery.md +145 -0
  477. package/skills/global_config/editable-design/references/layout-typography.md +116 -0
  478. package/skills/global_config/editable-design/references/replay-contract.md +169 -0
  479. package/skills/global_config/editable-design/scripts/_browser.mjs +247 -0
  480. package/skills/global_config/editable-design/scripts/bake.mjs +87 -0
  481. package/skills/global_config/editable-design/scripts/build-replay.mjs +316 -0
  482. package/skills/global_config/editable-design/scripts/check-contract.mjs +293 -0
  483. package/skills/global_config/editable-design/scripts/check-fonts.sh +252 -0
  484. package/skills/global_config/editable-design/scripts/check-poster.sh +70 -0
  485. package/skills/global_config/editable-design/scripts/doctor.sh +78 -0
  486. package/skills/global_config/editable-design/scripts/explode.mjs +115 -0
  487. package/skills/global_config/editable-design/scripts/font-kit.mjs +187 -0
  488. package/skills/global_config/editable-design/scripts/import-asset.sh +23 -0
  489. package/skills/global_config/editable-design/scripts/init-poster.sh +64 -0
  490. package/skills/global_config/editable-design/scripts/install-font-kit.sh +13 -0
  491. package/skills/global_config/editable-design/scripts/package-lock.json +334 -0
  492. package/skills/global_config/editable-design/scripts/package.json +14 -0
  493. package/skills/global_config/editable-design/scripts/remove-chroma-key.py +201 -0
  494. package/skills/global_config/editable-design/scripts/render-poster.sh +202 -0
  495. package/skills/global_config/editable-design/scripts/trace.sh +159 -0
  496. package/skills/global_config/editable-design/scripts/verify-replay.mjs +97 -0
  497. package/skills/global_config/editable-design/scripts/verify.mjs +782 -0
  498. package/skills/global_config/editable-design/scripts/wire-editor.mjs +104 -0
  499. package/skills/global_config/editable-design/tests/fixtures/small-explode/index.html +86 -0
  500. package/skills/global_config/mcsc/SKILL.md +60 -0
  501. package/skills/global_config/plan-canvas/SKILL.md +8 -0
  502. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +31 -6
  503. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +4 -2
@@ -0,0 +1,1113 @@
1
+ #!/usr/bin/env node
2
+ // agenttrail v0 — codebase-grounded daemon.
3
+ // Spine: fs watcher on the repo + PLAN.md convention. No hooks required
4
+ // (hooks become an optional fidelity adapter later).
5
+ import http from 'node:http'
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import os from 'node:os'
10
+ import crypto from 'node:crypto'
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+
14
+ // ---------- CLI ----------
15
+ const argv = process.argv.slice(2)
16
+ let cmd = null
17
+ let repo = process.cwd()
18
+ let port = 5330
19
+ const spawningPaths = new Set()
20
+ let openBrowser = process.stdout.isTTY ? true : false
21
+ let noOpen = false
22
+ let hooksOnly = false
23
+ const MAX_BODY_BYTES = 64 * 1024
24
+ const MAX_CONTROL_BODY_BYTES = 4 * 1024
25
+ let assumeYes = false
26
+ let removeFlag = false
27
+ let printFlag = false
28
+ // AOS patch: --plan <path> and --agent <name> flags
29
+ let planArg = null
30
+ let agentArg = null
31
+ // AOS patch: ask engine — `ask "<question>" [--timeout 10s|30m|2h|N]` blocks until the map answers
32
+ let askWords = null
33
+ let askTimeoutArg = null
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i]
36
+ if (a === 'init') cmd = 'init'
37
+ else if (a === 'hook') cmd = 'hook'
38
+ // AOS patch: ask engine CLI — asks the human a question via the map
39
+ else if (a === 'ask') cmd = 'ask'
40
+ else if (a === '--port') port = parseInt(argv[++i], 10)
41
+ else if (a === '--open') openBrowser = true
42
+ else if (a === '--no-open') { noOpen = true; openBrowser = false }
43
+ else if (a === '--yes' || a === '-y') assumeYes = true
44
+ else if (a === 'up') cmd = 'up'
45
+ else if (a === 'autostart') cmd = 'autostart'
46
+ else if (a === '--remove') removeFlag = true
47
+ else if (a === '--print') printFlag = true
48
+ else if (a === '--hooks-only') hooksOnly = true
49
+ // AOS patch: --plan <path> and --agent <name> flags
50
+ else if (a === '--plan') planArg = argv[++i]
51
+ else if (a === '--agent') agentArg = argv[++i]
52
+ // AOS patch: --timeout for the ask engine (10s|30m|2h|<minutes>, default 30m)
53
+ else if (a === '--timeout') askTimeoutArg = argv[++i]
54
+ // AOS patch: in ask mode the remaining positional words form the question, not the repo
55
+ else if (cmd === 'ask') (askWords = askWords || []).push(a)
56
+ else repo = path.resolve(a)
57
+ }
58
+ // AOS patch: --plan <path> overrides the default PLAN.md location
59
+ const planPath = planArg ? path.resolve(repo, planArg) : path.join(repo, 'PLAN.md')
60
+ const atDir = path.join(repo, '.agenttrail')
61
+
62
+ function realDirectory(value) {
63
+ if (typeof value !== 'string' || value.length === 0) return null
64
+ try {
65
+ const resolved = fs.realpathSync(path.resolve(value))
66
+ return fs.statSync(resolved).isDirectory() ? resolved : null
67
+ } catch {
68
+ return null
69
+ }
70
+ }
71
+
72
+ function isWithin(root, candidate) {
73
+ const relativePath = path.relative(root, candidate)
74
+ return relativePath === '' || (relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath))
75
+ }
76
+
77
+ function allowedSpawnDirectory(value) {
78
+ const target = realDirectory(value)
79
+ if (!target) return null
80
+ const roots = [repo]
81
+ for (const board of Array.isArray(boards) ? boards : []) if (board && board.repoPath) roots.push(board.repoPath)
82
+ try {
83
+ for (const file of fs.readdirSync(path.join(os.homedir(), '.agenttrail'))) {
84
+ if (!file.endsWith('.json')) continue
85
+ try {
86
+ const state = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.agenttrail', file), 'utf8'))
87
+ if (state && typeof state.repoPath === 'string') roots.push(state.repoPath)
88
+ } catch {}
89
+ }
90
+ } catch {}
91
+ for (const root of roots) {
92
+ const realRoot = realDirectory(root)
93
+ if (realRoot && isWithin(realRoot, target)) return target
94
+ }
95
+ return null
96
+ }
97
+
98
+ async function askYesNo(q) {
99
+ if (assumeYes || !process.stdin.isTTY) return true
100
+ const rl = (await import('node:readline')).createInterface({ input: process.stdin, output: process.stdout })
101
+ const ans = await new Promise(r => rl.question(q, r))
102
+ rl.close()
103
+ return !/^n/i.test(ans.trim())
104
+ }
105
+ function copyToClipboard(text) {
106
+ try {
107
+ const cp = require('node:child_process')
108
+ } catch {}
109
+ return import('node:child_process').then(cp => new Promise(res => {
110
+ const cmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip'
111
+ const args = process.platform === 'darwin' ? [] : ['-selection', 'clipboard']
112
+ const p = cp.spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'], shell: false })
113
+ p.on('error', () => res(false)); p.on('close', () => res(true))
114
+ p.stdin.end(text)
115
+ })).catch(() => false)
116
+ }
117
+ if (cmd === 'init') { hooksOnly ? installHooks() : await init(); process.exit(0) }
118
+ if (cmd === 'up') { await upAll(); process.exit(0) }
119
+ if (cmd === 'autostart') { autostart(); process.exit(0) }
120
+ if (cmd === 'hook') { await relayHook(); process.exit(0) }
121
+ // AOS patch: ask engine CLI — blocks until the map answers; exit 0 = go, 1 = deny/timeout
122
+ if (cmd === 'ask') { process.exit(await askFlow()) }
123
+
124
+ // reads a claude code hook payload on stdin and fans it out to local daemons;
125
+ // each daemon keeps only events whose cwd lives inside its repo. Never blocks
126
+ // the agent: 400ms cap, failures are silent.
127
+ async function relayHook() {
128
+ const chunks = []
129
+ let size = 0
130
+ try {
131
+ for await (const chunk of process.stdin) {
132
+ const buffer = Buffer.from(chunk)
133
+ size += buffer.length
134
+ if (size > MAX_BODY_BYTES) return
135
+ chunks.push(buffer)
136
+ }
137
+ } catch {}
138
+ const raw = Buffer.concat(chunks).toString('utf8')
139
+ if (!raw) return
140
+ // AOS patch: --agent <name> tags the hook event with the calling harness
141
+ let body = raw
142
+ if (agentArg) {
143
+ try {
144
+ const ev = JSON.parse(raw)
145
+ ev.agent = agentArg
146
+ body = JSON.stringify(ev)
147
+ } catch {}
148
+ }
149
+ const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] : Array.from({ length: 15 }, (_, i) => 5330 + i)
150
+ await Promise.allSettled(ports.map(p => fetch(`http://127.0.0.1:${p}/hook`, {
151
+ method: 'POST', body, signal: AbortSignal.timeout(400),
152
+ }).catch(() => {})))
153
+ // SessionStart: ask daemons for a staleness nudge — stdout lands in the agent's context
154
+ try {
155
+ const ev = JSON.parse(raw)
156
+ if (ev.hook_event_name === 'SessionStart' && ev.cwd) {
157
+ for (const p of ports) {
158
+ try {
159
+ const r = await fetch(`http://127.0.0.1:${p}/nudge?cwd=${encodeURIComponent(ev.cwd)}`, { signal: AbortSignal.timeout(400) })
160
+ const t = (await r.text()).trim()
161
+ // AOS patch: only Claude prints the nudge — other harnesses may treat hook stdout as structured output
162
+ if (t && (!agentArg || agentArg === 'claude')) { console.log(t); break }
163
+ } catch {}
164
+ }
165
+ }
166
+ } catch {}
167
+ }
168
+
169
+ // ---------- AOS patch: ask engine CLI ----------
170
+ // `agenttrail ask "<question>" [--timeout 10s|30m|2h|<minutes>] [--agent <name>]` blocks
171
+ // until the map answers. Exit 0 = go, 1 = deny/timeout. Pure question relay: this runs
172
+ // nothing itself and never touches a session transcript — go-gate keeps sole authority
173
+ // over guarded commands (git push / npm publish / npm version / recursive rm).
174
+ function parseTimeoutArg(v) {
175
+ const m = String(v || '').trim().match(/^(\d+)(s|m|h)?$/i)
176
+ if (!m) return 30 * 60e3 // default 30 minutes
177
+ const n = parseInt(m[1], 10)
178
+ const u = (m[2] || 'm').toLowerCase()
179
+ return Math.min(n * (u === 's' ? 1e3 : u === 'h' ? 3600e3 : 60e3), 24 * 3600e3)
180
+ }
181
+ // find this repo's daemon by probing GET /whoami on 127.0.0.1:5330-5344 and matching
182
+ // repoPath — same discovery as bootDedup; AGENTTRAIL_PORT wins when set. Both sides are
183
+ // symlink-normalized so a daemon started via /tmp matches a cwd of /private/tmp (macOS).
184
+ async function findDaemon() {
185
+ const norm = p => { try { return fs.realpathSync(p) } catch { return path.resolve(p) } }
186
+ const here = norm(repo)
187
+ const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] : Array.from({ length: 15 }, (_, i) => 5330 + i)
188
+ const hits = await Promise.all(ports.map(p =>
189
+ fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(400) }).then(r => r.json()).then(w => (w && norm(w.repoPath) === here) ? p : null).catch(() => null)))
190
+ return hits.find(Boolean) || null
191
+ }
192
+ async function askFlow() {
193
+ const question = (askWords || []).join(' ').trim()
194
+ if (!question) { console.error('usage: aos-trail ask "<question>" [--timeout 10s|30m|2h|<minutes>] [--agent <name>]'); return 1 }
195
+ const daemonPort = await findDaemon()
196
+ if (!daemonPort) { console.error('no agenttrail daemon running for this repo — start: aos-trail . --plan <plan.md>'); return 1 }
197
+ const base = `http://127.0.0.1:${daemonPort}`
198
+ const timeoutMs = parseTimeoutArg(askTimeoutArg)
199
+ const created = await fetch(`${base}/ask`, {
200
+ method: 'POST',
201
+ headers: { 'content-type': 'application/json' },
202
+ body: JSON.stringify({ question, timeoutMs, askedBy: agentArg || 'agent' }),
203
+ signal: AbortSignal.timeout(5000),
204
+ }).then(r => r.json()).catch(() => null)
205
+ if (!created || !created.ok) { console.error('could not register the question with the daemon'); return 1 }
206
+ console.log(`waiting for your answer on the map: http://localhost:${daemonPort}`)
207
+ const deadline = Date.now() + timeoutMs + 5000 // grace past the daemon's own expiry flip
208
+ while (Date.now() < deadline) {
209
+ await new Promise(r => setTimeout(r, 1000))
210
+ const st = await fetch(`${base}/ask/${created.id}`, { signal: AbortSignal.timeout(2000) }).then(r => r.json()).catch(() => null)
211
+ if (st && st.ask && st.ask.status !== 'pending') return st.ask.status === 'go' ? 0 : 1
212
+ }
213
+ return 1 // deny by default: timeout, daemon gone, or answer never came
214
+ }
215
+
216
+ // ---------- PLAN.md parser (convention v2: components, owner-first names) ----------
217
+ // `## Plain-language name {#id}` = a component of the system, not a phase.
218
+ // `tech:` under a component or task carries the engineer phrasing.
219
+ // `needs: [id]` = directed sequencing edge; `links: [id]` = undirected coupling.
220
+ const NODE_RE = /^##\s+(.+?)\s*\{#([a-z0-9][a-z0-9-]*)\}\s*$/i
221
+ const TASK_RE = /^\s*[-*]\s+\[( |x|~|!)\]\s+(.+?)\s*\{#([a-z0-9][a-z0-9-]*)\}\s*$/i
222
+ const NEEDS_RE = /^needs:\s*\[([^\]]*)\]\s*$/i
223
+ const LINKS_RE = /^links:\s*\[([^\]]*)\]\s*$/i
224
+ const FILES_RE = /^files:\s*\[([^\]]*)\]\s*$/i
225
+ const TECH_RE = /^\s*tech:\s*(.+?)\s*$/i
226
+ const BY_RE = /^\s*by:\s*(.+?)\s*$/i
227
+ const FROM_RE = /^\s*(?:from|horizon):\s*(agent|roadmap|now|backlog)\s*$/i
228
+ const KIND_RE = /^\s*kind:\s*([a-z][a-z-]*)\s*$/i
229
+ const URL_RE = /^\s*url:\s*(\S+)\s*$/i
230
+ const DECISIONS_RE = /^##\s+decisions\s*$/i
231
+ const idList = s => s.split(',').map(x => x.trim()).filter(Boolean)
232
+
233
+ function parsePlan(text) {
234
+ const nodes = [] // ordered
235
+ const decisions = []
236
+ let title = ''
237
+ let curComponent = null
238
+ let lastNode = null // tech: lines attach to the most recent component or task
239
+ let inDecisions = false
240
+ for (const raw of text.split('\n')) {
241
+ const line = raw.trimEnd()
242
+ if (!title && /^#\s+/.test(line)) { title = line.replace(/^#\s+/, ''); continue }
243
+ if (DECISIONS_RE.test(line)) { inDecisions = true; curComponent = null; lastNode = null; continue }
244
+ let m
245
+ if ((m = line.match(NODE_RE))) {
246
+ inDecisions = false
247
+ curComponent = { id: m[2], title: m[1], level: 'component', parent: null, needs: [], links: [], files: [], tech: '', by: '', kind: '', url: '', status: 'pending' }
248
+ lastNode = curComponent
249
+ nodes.push(curComponent)
250
+ continue
251
+ }
252
+ if (inDecisions) {
253
+ if (/^\s*[-*]\s+/.test(line)) decisions.push(line.replace(/^\s*[-*]\s+/, ''))
254
+ continue
255
+ }
256
+ if ((m = line.match(TASK_RE))) {
257
+ const status = m[1] === 'x' ? 'done' : m[1] === '~' ? 'active' : m[1] === '!' ? 'blocked' : 'pending'
258
+ lastNode = { id: m[3], title: m[2], level: 'task', parent: curComponent ? curComponent.id : null, needs: [], links: [], tech: '', by: '', src: '', status }
259
+ nodes.push(lastNode)
260
+ continue
261
+ }
262
+ if ((m = line.match(TECH_RE))) { if (lastNode) lastNode.tech = m[1]; continue }
263
+ if ((m = line.match(KIND_RE))) { if (curComponent) curComponent.kind = m[1].toLowerCase(); continue }
264
+ if ((m = line.match(URL_RE))) { if (curComponent) curComponent.url = m[1]; continue }
265
+ if ((m = line.match(BY_RE))) { if (lastNode) lastNode.by = m[1]; continue }
266
+ if ((m = line.match(FROM_RE))) { if (lastNode) { const v = m[1].toLowerCase(); lastNode.src = v === 'now' ? 'agent' : v === 'backlog' ? 'roadmap' : v } continue }
267
+ if ((m = line.match(NEEDS_RE))) { if (curComponent) curComponent.needs = idList(m[1]); continue }
268
+ if ((m = line.match(LINKS_RE))) { if (curComponent) curComponent.links = idList(m[1]); continue }
269
+ if ((m = line.match(FILES_RE))) { if (curComponent) curComponent.files = idList(m[1]); continue }
270
+ }
271
+ // derive component status from its tasks — blocked wins (it demands attention)
272
+ for (const c of nodes.filter(n => n.level === 'component')) {
273
+ const kids = nodes.filter(n => n.parent === c.id)
274
+ if (kids.some(k => k.status === 'blocked')) c.status = 'blocked'
275
+ else if (kids.some(k => k.status === 'active')) c.status = 'active'
276
+ else if (kids.length && kids.every(k => k.status === 'done')) c.status = 'done'
277
+ }
278
+ return { nodes, decisions, title }
279
+ }
280
+
281
+ // ---------- live state ----------
282
+ const session = { id: Math.random().toString(36).slice(2, 10), project: path.basename(repo), startedAt: new Date().toISOString() }
283
+ let planText = safeRead(planPath)
284
+ let parsed = parsePlan(planText)
285
+ let activity = null // { file, at } — most recent non-plan repo write
286
+ let recentActivity = [] // last N writes, newest first — feeds the live-view drill-down
287
+ let planMtime = statMtime(planPath)
288
+ const clients = new Set()
289
+
290
+ // ---------- observed activity per component (files: globs) ----------
291
+ // Declared status ([~]/[x]) says what the agent claims; this says what the
292
+ // filesystem shows. A component whose files are being written is live no
293
+ // matter what its checkbox says — that is how "revising done work" stays visible.
294
+ const compTouched = {} // component id -> last matching write ts
295
+ const compRecent = {} // component id -> [{file, at}] newest first — feeds capsule work lines
296
+ const fileHeat = {} // file -> last touch ts, capped
297
+ function heatFile(file, at) {
298
+ fileHeat[file] = at
299
+ const keys = Object.keys(fileHeat)
300
+ if (keys.length > 600) { keys.sort((a, b) => fileHeat[a] - fileHeat[b]); for (const k of keys.slice(0, 100)) delete fileHeat[k] }
301
+ }
302
+ function globToRe(g) {
303
+ if (!/[*?]/.test(g)) return new RegExp('^' + g.replace(/[.+^${}()|[\]\\]/g, '\\$&') + '(/|$)')
304
+ const esc = g.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\u0000/g, '.*')
305
+ return new RegExp('^' + esc + '$')
306
+ }
307
+ let compMatchers = []
308
+ function rebuildMatchers() {
309
+ compMatchers = parsed.nodes.filter(n => n.level === 'component' && n.files.length)
310
+ .map(c => ({ id: c.id, res: c.files.map(globToRe) }))
311
+ }
312
+ function touchComponents(file, at) {
313
+ for (const m of compMatchers) if (m.res.some(re => re.test(file))) {
314
+ compTouched[m.id] = at
315
+ const arr = compRecent[m.id] || (compRecent[m.id] = [])
316
+ if (arr[0] && arr[0].file === file) arr[0] = { file, at, n: (arr[0].n || 1) + 1 }
317
+ else arr.unshift({ file, at, n: 1 })
318
+ if (arr.length > 6) arr.length = 6
319
+ }
320
+ }
321
+
322
+ // ---------- live runs (claude code hooks adapter) ----------
323
+ // The run is the foreground: what the agent is doing right now — current
324
+ // todo, streaming tool line, recent calls — pinned to a component on the map.
325
+ const runs = {} // session id -> run
326
+ const handoffs = [] // {c, from, to, at} — one session picks up where another stopped
327
+ // AOS patch: `done` distinguishes a finished run (SessionEnd) from one waiting for its user (Stop)
328
+ function runFor(id, cwd) {
329
+ return runs[id] || (runs[id] = { id, agent: 'claude', cwd, startedAt: Date.now(), lastEventAt: Date.now(), todos: [], currentTool: null, recentTools: [], componentId: null, ended: false, done: false, waiting: null })
330
+ }
331
+ function toolDetail(input = {}) {
332
+ const p = input.file_path || input.notebook_path
333
+ const d = input.command || (p ? (relToRepo(p) ?? p) : '') || input.description || input.pattern || input.url || input.query || input.prompt || ''
334
+ return String(d).replace(/\s+/g, ' ').slice(0, 90)
335
+ }
336
+ function relToRepo(p) {
337
+ if (!p) return null
338
+ const r = path.resolve(String(p))
339
+ return r === repo ? '' : r.startsWith(repo + path.sep) ? r.slice(repo.length + 1) : null
340
+ }
341
+ // AOS patch: native plan/todo tools across harnesses, normalized to the
342
+ // {content, status} shape the frontend's run-todo rendering already expects.
343
+ // - Claude: `TodoWrite`, input {todos:[{content,status}]}
344
+ // - OpenCode: its real tool id is `todowrite` (verified live via `opencode
345
+ // run --format json`), but mcsc's opencode.js adapter title-cases the raw
346
+ // tool name before it reaches this hook (`part.tool.charAt(0).toUpperCase()
347
+ // + part.tool.slice(1)`), so it arrives here as `Todowrite`. Same input
348
+ // shape as Claude's, plus an extra `priority` field we ignore.
349
+ // - Codex: `update_plan`, input {explanation?, plan:[{step,status}]}. NOT
350
+ // verified live (Codex CLI's `--json` stream errored on auth in the
351
+ // environment this was built in, see report.md) — mapped on Codex's
352
+ // publicly documented tool contract; re-verify once Codex is authenticated.
353
+ function normalizeTodos(toolName, input) {
354
+ if (!input) return null
355
+ if ((toolName === 'TodoWrite' || toolName === 'Todowrite') && Array.isArray(input.todos)) {
356
+ return input.todos.map(t => ({ content: t.content, status: t.status }))
357
+ }
358
+ if (toolName === 'update_plan' && Array.isArray(input.plan)) {
359
+ return input.plan.map(t => ({ content: t.step, status: t.status }))
360
+ }
361
+ return null
362
+ }
363
+ function handleHookEvent(ev) {
364
+ const cwd = ev.cwd || ''
365
+ if (!(cwd === repo || cwd.startsWith(repo + path.sep))) return false
366
+ const run = runFor(ev.session_id || 'session', cwd)
367
+ if (ev.agent) run.agent = String(ev.agent).slice(0, 24).toLowerCase()
368
+ run.lastEventAt = Date.now()
369
+ stateDirty = true
370
+ const kind = ev.hook_event_name
371
+ if (kind === 'SessionStart') { run.ended = false; run.done = false } // AOS patch: a new session starts neither ended nor done
372
+ else if (kind === 'Stop' || kind === 'SessionEnd') { run.ended = true; run.currentTool = null; run.done = kind === 'SessionEnd' } // AOS patch: SessionEnd = finished, Stop = waiting for you
373
+ else if (kind === 'Notification') { // AOS patch: UNVERIFIED event name — see decisions; unknown events still return false below, so a wrong name changes nothing
374
+ run.waiting = String(ev.message || '').slice(0, 120)
375
+ run.ended = false
376
+ run.done = false
377
+ }
378
+ else if (kind === 'PreToolUse') {
379
+ run.ended = false
380
+ run.done = false // AOS patch: an ended run that resumes is no longer done
381
+ run.waiting = null // AOS patch: a resumed run is no longer waiting on the human
382
+ run.currentTool = { name: ev.tool_name, detail: toolDetail(ev.tool_input), at: Date.now() }
383
+ if (ev.tool_name === 'Task' && ev.tool_input) {
384
+ const name = String(ev.tool_input.description || ev.tool_input.subagent_type || 'sub-agent').slice(0, 60)
385
+ ;(run.subagents = run.subagents || []).push({ name, startedAt: Date.now(), ended: false })
386
+ if (run.subagents.length > 12) run.subagents.shift()
387
+ }
388
+ } else if (kind === 'SubagentStop') {
389
+ const sa = (run.subagents || []).find(x => !x.ended)
390
+ if (sa) { sa.ended = true; sa.endedAt = Date.now() }
391
+ } else if (kind === 'PostToolUse') {
392
+ run.ended = false
393
+ run.done = false // AOS patch: keep done in sync when a run resumes
394
+ run.waiting = null // AOS patch: a resumed run is no longer waiting on the human
395
+ const started = run.currentTool && run.currentTool.name === ev.tool_name ? run.currentTool.at : Date.now()
396
+ run.recentTools.unshift({ name: ev.tool_name, detail: toolDetail(ev.tool_input), at: Date.now(), ms: Date.now() - started })
397
+ if (run.recentTools.length > 8) run.recentTools.length = 8
398
+ run.currentTool = null
399
+ const todos = normalizeTodos(ev.tool_name, ev.tool_input)
400
+ if (todos) run.todos = todos
401
+ const rel = relToRepo(ev.tool_input && (ev.tool_input.file_path || ev.tool_input.notebook_path))
402
+ if (rel) {
403
+ heatFile(rel, Date.now())
404
+ touchComponents(rel, Date.now())
405
+ activity = { file: rel, at: Date.now() }
406
+ for (const m of compMatchers) if (m.res.some(re => re.test(rel))) {
407
+ if (run.componentId !== m.id) {
408
+ (run.path = run.path || []).push({ c: m.id, at: Date.now() })
409
+ const prev = Object.values(runs).find(r2 => r2.id !== run.id && r2.ended && r2.componentId === m.id && Date.now() - r2.lastEventAt < 10 * 60e3)
410
+ if (prev) { handoffs.push({ c: m.id, from: prev.agent, to: run.agent, at: Date.now() }); if (handoffs.length > 10) handoffs.shift() }
411
+ }
412
+ run.componentId = m.id
413
+ if (run.path && run.path.length > 20) run.path.shift()
414
+ }
415
+ }
416
+ } else return false
417
+ return true
418
+ }
419
+ let cycles = [] // saved history: [{start,end,sessions:[{agent,componentId,tools}]}]
420
+ function archiveRun(r) {
421
+ const start = r.startedAt, end = r.lastEventAt
422
+ let c = cycles.find(c => start <= c.end + 30 * 60e3 && end >= c.start - 30 * 60e3)
423
+ if (!c) { c = { start, end, sessions: [] }; cycles.push(c) }
424
+ c.start = Math.min(c.start, start); c.end = Math.max(c.end, end)
425
+ c.sessions.push({ agent: r.agent, componentId: r.componentId, tools: (r.recentTools || []).length, todosDone: (r.todos || []).filter(t => t.status === 'completed').length })
426
+ cycles.sort((a, b) => b.start - a.start)
427
+ if (cycles.length > 30) cycles.length = 30
428
+ stateDirty = true
429
+ }
430
+ function liveRuns() {
431
+ const now = Date.now()
432
+ for (const [id, r] of Object.entries(runs)) {
433
+ if (!r.ended && now - r.lastEventAt > 15 * 60e3) { r.ended = true; r.currentTool = null } // missed Stop
434
+ if (now - r.lastEventAt > 2 * 3600e3) { archiveRun(r); delete runs[id] } // 2h trail → saved cycle record
435
+ }
436
+ return Object.values(runs).sort((a, b) => a.startedAt - b.startedAt)
437
+ }
438
+
439
+ // ---------- observed-state persistence (survives daemon restarts) ----------
440
+ // Lives under ~/.agenttrail keyed by repo path — never touches the repo itself.
441
+ const stateFile = path.join(os.homedir(), '.agenttrail', crypto.createHash('sha1').update(repo).digest('hex').slice(0, 12) + '.json')
442
+ let stateDirty = false
443
+ // AOS patch: ask engine state — questions awaiting the human + the timestamped decision log
444
+ let asks = [] // {id, question, askedBy, askedAt, expiryAt, status:'pending'|'go'|'deny', decidedAt, by, expired}
445
+ let decisions = [] // [{question, decision, by:'you'|'timeout', at}] — newest last, capped
446
+ function loadState() {
447
+ try {
448
+ const st = JSON.parse(fs.readFileSync(stateFile, 'utf8'))
449
+ activity = st.activity || null
450
+ recentActivity = st.recentActivity || []
451
+ Object.assign(runs, st.runs || {})
452
+ cycles = st.cycles || []
453
+ asks = Array.isArray(st.asks) ? st.asks : [] // AOS patch: ask engine survives restarts
454
+ decisions = Array.isArray(st.decisions) ? st.decisions : [] // AOS patch: decision log survives restarts
455
+ Object.assign(compTouched, st.compTouched || {})
456
+ Object.assign(compRecent, st.compRecent || {})
457
+ Object.assign(fileHeat, st.fileHeat || {})
458
+ } catch {}
459
+ }
460
+ function saveState() {
461
+ if (!stateDirty) return
462
+ stateDirty = false
463
+ try {
464
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true })
465
+ fs.writeFileSync(stateFile, JSON.stringify({ repoPath: repo, port, activity, recentActivity, compTouched, compRecent, runs, fileHeat, cycles, asks, decisions }))
466
+ } catch {}
467
+ }
468
+ loadState()
469
+ stateDirty = true // boot save: registers this repo for `agenttrail up`
470
+ setInterval(saveState, 15000).unref()
471
+ for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { stateDirty = true; saveState(); process.exit(0) })
472
+
473
+ // AOS patch: ask engine — a pending question past its deadline auto-denies and is logged
474
+ // like a human decision, with by:'timeout'; runs on every read and on a 1s interval tick
475
+ function expireAsks() {
476
+ const now = Date.now()
477
+ let changed = false
478
+ for (const a of asks) {
479
+ if (a.status === 'pending' && now >= a.expiryAt) {
480
+ a.status = 'deny'; a.expired = true; a.decidedAt = now; a.by = 'timeout'
481
+ decisions.push({ question: a.question, decision: 'deny', by: 'timeout', at: new Date().toISOString() })
482
+ if (decisions.length > 100) decisions.splice(0, decisions.length - 100)
483
+ changed = true
484
+ }
485
+ }
486
+ if (changed) { stateDirty = true; broadcastTick() }
487
+ }
488
+ setInterval(expireAsks, 1000).unref()
489
+
490
+ // ---------- repo tree (vs-code-style explorer, folders first) ----------
491
+ const IGNORE = /(^|\/)(\.git|node_modules|\.agenttrail|dist|build|\.next|__pycache__|\.venv|\.build|\.pytest_cache|\.ruff_cache|\.cache|\.DS_Store)(\/|$)/
492
+ // editor/tool atomic-write droppings — not real activity targets
493
+ const TMP_FILE = /(\.tmp(\.|$)|~$|\.swp$|\.swx$|(^|\/)\.#|(^|\/)#.+#$|\.DS_Store$)/
494
+
495
+ function buildTree(rootDir, budgetN = 4000, perDir = 250) {
496
+ treeTruncated = false
497
+ const root = []
498
+ const queue = [{ dir: rootDir, rel: '', depth: 0, out: root }]
499
+ let budget = budgetN
500
+ while (queue.length) {
501
+ const { dir, rel, depth, out } = queue.shift()
502
+ if (depth > 8) continue
503
+ let entries = []
504
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { continue }
505
+ let taken = 0
506
+ for (const e of entries) {
507
+ const r = rel ? rel + '/' + e.name : e.name
508
+ if (IGNORE.test(r) || TMP_FILE.test(r)) continue
509
+ if (taken >= perDir || budget <= 0) { treeTruncated = true; break }
510
+ taken++; budget--
511
+ if (e.isDirectory()) {
512
+ const node = { name: e.name, path: r, dir: true, children: [] }
513
+ out.push(node)
514
+ queue.push({ dir: path.join(dir, e.name), rel: r, depth: depth + 1, out: node.children })
515
+ } else if (e.isFile()) out.push({ name: e.name, path: r, dir: false })
516
+ }
517
+ out.sort((a, b) => (b.dir - a.dir) || a.name.localeCompare(b.name))
518
+ }
519
+ return root
520
+ }
521
+ let treeTruncated = false
522
+ let tree = buildTree(repo)
523
+ let treeDirty = false
524
+ let lastTreeSent = Date.now()
525
+ rebuildMatchers()
526
+
527
+ function safeRead(p) { try { return fs.readFileSync(p, 'utf8') } catch { return '' } }
528
+ function statMtime(p) { try { return fs.statSync(p).mtimeMs } catch { return null } }
529
+
530
+ function compFilesFor(id) {
531
+ const m = compMatchers.find(x => x.id === id)
532
+ if (!m) return []
533
+ const out = []
534
+ const walk = t => { for (const n of t) { if (n.dir) walk(n.children || []); else if (m.res.some(re => re.test(n.path))) out.push(n.path) } }
535
+ walk(tree)
536
+ out.sort((a, b) => (fileHeat[b] || 0) - (fileHeat[a] || 0) || a.length - b.length || a.localeCompare(b))
537
+ return out.slice(0, 24)
538
+ }
539
+ function hotFiles() {
540
+ const now = Date.now(), out = {}
541
+ for (const [f, at] of Object.entries(fileHeat)) if (now - at < 60000) out[f] = at
542
+ return out
543
+ }
544
+ function lintPlan() {
545
+ const out = []
546
+ const comps = parsed.nodes.filter(n => n.level === 'component')
547
+ const tasks = parsed.nodes.filter(n => n.level === 'task')
548
+ if (!planText.length || (comps.length === 1 && tasks.length <= 1)) return out
549
+ const ids = new Set()
550
+ for (const n of parsed.nodes) { if (ids.has(n.id)) out.push(`duplicate id {#${n.id}}`); ids.add(n.id) }
551
+ const compIds = new Set(comps.map(c => c.id))
552
+ for (const c of comps) {
553
+ for (const e of [...c.needs, ...c.links]) if (!compIds.has(e)) out.push(`${c.id}: edge to unknown component "${e}"`)
554
+ if (!c.files.length) out.push(`${c.id}: no files: globs — the live layer can't pin work to it`)
555
+ }
556
+ for (const t of tasks) if (t.status === 'done' && !t.tech) out.push(`[x] "${t.title}" cites no evidence (tech: line)`)
557
+ if (comps.length > 9) out.push(`${comps.length} components — over the 5–9 governor, consider merging`)
558
+ return out.slice(0, 12)
559
+ }
560
+ let hooksInstalledCache = { at: 0, val: false }
561
+ function hooksInstalled() {
562
+ if (Date.now() - hooksInstalledCache.at < 30000) return hooksInstalledCache.val
563
+ const val = safeRead(path.join(repo, '.claude', 'settings.local.json')).includes('agenttrail')
564
+ hooksInstalledCache = { at: Date.now(), val }
565
+ return val
566
+ }
567
+ function planStaleness() {
568
+ // code moving while the plan sits untouched — the drift that makes boards look dead
569
+ if (!activity || !planMtime) return { stale: false }
570
+ const gap = activity.at - planMtime
571
+ const fresh = Date.now() - activity.at < 15 * 60e3
572
+ return gap > 20 * 60e3 && fresh ? { stale: true, minutes: Math.round(gap / 60e3) } : { stale: false }
573
+ }
574
+ // AOS patch: when no plan file exists yet, synthesize one synthetic
575
+ // component card per live run that has native todos (any harness — see
576
+ // normalizeTodos above) so the board renders instead of the empty
577
+ // "Waiting for the plan" state. A real plan file always wins outright: this
578
+ // only runs when planText is empty, and never mixes with parsed.nodes.
579
+ function syntheticPlanNodes() {
580
+ if (planText.length) return []
581
+ const out = []
582
+ for (const r of liveRuns()) {
583
+ if (!r.todos || !r.todos.length) continue
584
+ const id = 'live-' + r.id.replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 20)
585
+ const done = r.todos.filter(t => t.status === 'completed').length
586
+ const status = r.todos.some(t => t.status === 'in_progress') ? 'active' : done === r.todos.length ? 'done' : 'pending'
587
+ out.push({
588
+ id, title: `${r.agent}'s live todos`, level: 'component', parent: null,
589
+ needs: [], links: [], files: [], tech: '', by: r.agent, kind: '', url: '',
590
+ status, synthetic: true,
591
+ touchedAt: r.lastEventAt, recent: [], filesList: [],
592
+ })
593
+ }
594
+ return out
595
+ }
596
+ function model() {
597
+ if (treeDirty) { tree = buildTree(repo); treeDirty = false }
598
+ // AOS patch: include planFile (relative path) and aosPlan (boolean) for AOS UI customization
599
+ const planFile = planArg ? path.relative(repo, planPath).split(path.sep).join('/') : undefined
600
+ const aosPlan = Boolean(planArg)
601
+ const realPlanNodes = parsed.nodes.map(n => n.level === 'component' ? { ...n, touchedAt: compTouched[n.id] || null, recent: compRecent[n.id] || [], filesList: compFilesFor(n.id) } : n)
602
+ const planNodes = realPlanNodes.length ? realPlanNodes : syntheticPlanNodes()
603
+ return {
604
+ boards, port,
605
+ runs: liveRuns(),
606
+ session, plan: planNodes, tree,
607
+ planTitle: parsed.title,
608
+ hasPlan: planText.length > 0, treeTruncated,
609
+ activity, recentActivity, planMtime, handoffs, asks, hotFiles: hotFiles(), cycles, // AOS patch: asks ride along so the open map updates live
610
+ planStale: planStaleness(), lints: lintPlan(), hooksInstalled: hooksInstalled(),
611
+ planFile, aosPlan,
612
+ now: Date.now(),
613
+ }
614
+ }
615
+
616
+ function send(obj) {
617
+ const data = `data: ${JSON.stringify(obj)}\n\n`
618
+ for (const res of clients) res.write(data)
619
+ }
620
+ function broadcast() { send(model()) }
621
+ // activity ticks carry only what moved — the 600KB tree stays home
622
+ function broadcastTick() {
623
+ send({ partial: true, runs: liveRuns(), activity, recentActivity, touched: { ...compTouched }, compRecent, handoffs, asks, hotFiles: hotFiles(), now: Date.now() }) // AOS patch: asks ride along so open questions update live
624
+ }
625
+
626
+ // ---------- watcher ----------
627
+ let planDebounce = null
628
+ try {
629
+ fs.watch(repo, { recursive: true }, (_ev, filename) => {
630
+ if (!filename) return
631
+ const f = filename.toString().split(path.sep).join('/')
632
+ if (IGNORE.test(f) || TMP_FILE.test(f)) return
633
+ if (path.resolve(repo, f) === planPath) {
634
+ clearTimeout(planDebounce)
635
+ planDebounce = setTimeout(() => {
636
+ planText = safeRead(planPath)
637
+ parsed = parsePlan(planText)
638
+ rebuildMatchers()
639
+ planMtime = statMtime(planPath)
640
+ broadcast()
641
+ }, 150)
642
+ return
643
+ }
644
+ // plain repo churn → liveness signal
645
+ treeDirty = true
646
+ stateDirty = true
647
+ activity = { file: f, at: Date.now() }
648
+ heatFile(f, activity.at)
649
+ touchComponents(f, activity.at)
650
+ if (!recentActivity.length || recentActivity[0].file !== f) recentActivity.unshift(activity)
651
+ else recentActivity[0] = activity
652
+ recentActivity = recentActivity.slice(0, 12)
653
+ throttleBroadcast()
654
+ })
655
+ } catch (e) {
656
+ // recursive watch is unsupported on some platforms (older linux) — degrade:
657
+ // PLAN.md and top-level changes still tracked, deep activity limited.
658
+ try {
659
+ fs.watch(repo, (_ev, filename) => {
660
+ if (!filename) return
661
+ const f = filename.toString()
662
+ if (path.resolve(repo, f) === planPath) {
663
+ clearTimeout(planDebounce)
664
+ planDebounce = setTimeout(() => {
665
+ planText = safeRead(planPath)
666
+ parsed = parsePlan(planText)
667
+ rebuildMatchers()
668
+ planMtime = statMtime(planPath)
669
+ broadcast()
670
+ }, 150)
671
+ }
672
+ })
673
+ console.error('note: recursive file watching unavailable here — plan updates still live, deep file activity limited')
674
+ } catch (e2) {
675
+ console.error('watcher failed:', e2.message)
676
+ }
677
+ }
678
+ let lastActivityPush = 0
679
+ function throttleBroadcast() {
680
+ const now = Date.now()
681
+ if (now - lastActivityPush <= 1000) return
682
+ lastActivityPush = now
683
+ if (treeDirty && now - lastTreeSent > 10000) { lastTreeSent = now; broadcast() } // tree refresh, at most every 10s
684
+ else broadcastTick()
685
+ }
686
+
687
+ // ---------- sibling boards (multi-repo: one daemon per repo, boards link to each other) ----------
688
+ let boards = [{ port, project: path.basename(repo), self: true, repoPath: repo }]
689
+ async function discoverBoards() {
690
+ const found = [{ port, project: session.project, self: true, repoPath: repo }]
691
+ await Promise.allSettled(Array.from({ length: 15 }, (_, i) => 5330 + i).filter(p => p !== port).map(async p => {
692
+ try {
693
+ const r = await fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(300) })
694
+ const j = await r.json()
695
+ if (j && j.project) found.push({ port: p, project: j.project, self: false, repoPath: j.repoPath })
696
+ } catch {}
697
+ }))
698
+ boards = found.sort((a, b) => a.port - b.port)
699
+ }
700
+ discoverBoards()
701
+ for (const t of [3000, 8000]) setTimeout(discoverBoards, t).unref() // cold-start: siblings may not be up yet
702
+ setInterval(discoverBoards, 30000).unref()
703
+
704
+ // ---------- fleet (the zoomed-out altitude) ----------
705
+ function summaryModel() {
706
+ return {
707
+ port, project: session.project, hasPlan: planText.length > 0,
708
+ components: parsed.nodes.filter(n => n.level === 'component').map(n => ({ id: n.id, title: n.title, status: n.status, touchedAt: compTouched[n.id] || null })),
709
+ runs: liveRuns().map(r => ({ agent: r.agent, ended: r.ended, componentId: r.componentId, startedAt: r.startedAt, tool: r.currentTool ? `${r.currentTool.name} · ${r.currentTool.detail}` : null, todos: (r.todos || []).length, todosDone: (r.todos || []).filter(t => t.status === 'completed').length })),
710
+ activity,
711
+ }
712
+ }
713
+ function liteModel() {
714
+ const m = model()
715
+ const { tree, treeTruncated, ...rest } = m
716
+ return { ...rest, port, project: session.project }
717
+ }
718
+ let worldCache = { at: 0, data: null }
719
+ async function worldModel() {
720
+ if (Date.now() - worldCache.at < 1500 && worldCache.data) return worldCache.data
721
+ const out = []
722
+ await Promise.allSettled(boards.map(async b => {
723
+ if (b.self) { out.push(liteModel()); return }
724
+ try {
725
+ const r = await fetch(`http://127.0.0.1:${b.port}/board-lite`, { signal: AbortSignal.timeout(500) })
726
+ out.push(await r.json())
727
+ } catch {}
728
+ }))
729
+ out.sort((a, b) => a.port - b.port)
730
+ worldCache = { at: Date.now(), data: out }
731
+ return out
732
+ }
733
+ let fleetCache = { at: 0, data: null }
734
+ async function fleetModel() {
735
+ if (Date.now() - fleetCache.at < 1500 && fleetCache.data) return fleetCache.data
736
+ const out = []
737
+ await Promise.allSettled(boards.map(async b => {
738
+ if (b.self) { out.push(summaryModel()); return }
739
+ try {
740
+ const r = await fetch(`http://127.0.0.1:${b.port}/summary`, { signal: AbortSignal.timeout(400) })
741
+ out.push(await r.json())
742
+ } catch {}
743
+ }))
744
+ out.sort((a, b) => a.port - b.port)
745
+ fleetCache = { at: Date.now(), data: out }
746
+ return out
747
+ }
748
+
749
+ // ---------- http ----------
750
+ const indexPath = path.join(__dirname, '..', 'public', 'index.html')
751
+ const server = http.createServer((req, res) => {
752
+ const u = new URL(req.url, 'http://x')
753
+ if (u.pathname === '/') {
754
+ res.writeHead(200, { 'content-type': 'text/html' }).end(fs.readFileSync(indexPath, 'utf8'))
755
+ } else if (u.pathname.startsWith('/production_artifacts/')) {
756
+ // AOS patch: serve pipeline artifacts so a card's relative `url:` (e.g. an archify diagram) opens
757
+ let root
758
+ let f
759
+ try {
760
+ root = fs.realpathSync(path.join(repo, 'production_artifacts'))
761
+ f = fs.realpathSync(path.resolve(repo, '.' + decodeURIComponent(u.pathname)))
762
+ } catch {
763
+ res.writeHead(404).end()
764
+ return
765
+ }
766
+ let isFile = false
767
+ try { isFile = fs.statSync(f).isFile() } catch {}
768
+ if (!isWithin(root, f) || !isFile) { res.writeHead(404).end(); return }
769
+ const type = f.endsWith('.html') ? 'text/html' : f.endsWith('.svg') ? 'image/svg+xml' : f.endsWith('.json') ? 'application/json' : 'text/plain'
770
+ res.writeHead(200, { 'content-type': type + '; charset=utf-8' }).end(fs.readFileSync(f))
771
+ } else if (u.pathname === '/whoami') {
772
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ project: session.project, port, repoPath: repo }))
773
+ } else if (u.pathname === '/board-lite') {
774
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(liteModel()))
775
+ } else if (u.pathname === '/world') {
776
+ worldModel().then(d => res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(d)))
777
+ } else if (u.pathname === '/tree') {
778
+ if (treeDirty) { tree = buildTree(repo); treeDirty = false }
779
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tree, treeTruncated }))
780
+ } else if (u.pathname === '/suggest') {
781
+ const seen = new Set(), out = []
782
+ const add = p => { if (p && p !== repo && !seen.has(p) && fs.existsSync(p)) { seen.add(p); out.push(p) } }
783
+ try {
784
+ const dir = path.join(os.homedir(), '.agenttrail')
785
+ for (const f of fs.readdirSync(dir)) {
786
+ if (!f.endsWith('.json')) continue
787
+ try { add(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')).repoPath) } catch {}
788
+ }
789
+ } catch {}
790
+ try {
791
+ for (const sib of fs.readdirSync(path.dirname(repo), { withFileTypes: true })) {
792
+ if (!sib.isDirectory()) continue
793
+ const p = path.join(path.dirname(repo), sib.name)
794
+ if (p !== repo && fs.existsSync(path.join(p, '.git'))) add(p)
795
+ }
796
+ } catch {}
797
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out.slice(0, 15)))
798
+ } else if (u.pathname === '/setup' && req.method === 'POST') {
799
+ init().then(() => {
800
+ planText = safeRead(planPath); parsed = parsePlan(planText); rebuildMatchers(); planMtime = statMtime(planPath)
801
+ broadcast()
802
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, prompt: BACKFILL_PROMPT }))
803
+ }).catch(e => res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: String(e) })))
804
+ } else if (u.pathname === '/setup-board' && req.method === 'POST') {
805
+ readCapped(req, res, async body => {
806
+ let out = { ok: false, error: 'no board there' }
807
+ try {
808
+ const p = Number(JSON.parse(body || '{}').port)
809
+ if (!Number.isInteger(p) || p < 1 || p > 65535) out = { ok: false, error: 'bad port' }
810
+ else out = await fetch(`http://127.0.0.1:${p}/setup`, { method: 'POST', signal: AbortSignal.timeout(5000) }).then(r => r.json())
811
+ } catch {}
812
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out))
813
+ }, MAX_CONTROL_BODY_BYTES)
814
+ } else if (u.pathname === '/spawn' && req.method === 'POST') {
815
+ readCapped(req, res, async body => {
816
+ let out = { ok: false, error: 'bad request' }
817
+ try {
818
+ const p = allowedSpawnDirectory(JSON.parse(body || '{}').path)
819
+ if (!p) out = { ok: false, error: 'spawn target is outside the allowed workspace' }
820
+ else {
821
+ let already = null
822
+ for (const b of boards) {
823
+ try {
824
+ const w = await fetch(`http://127.0.0.1:${b.port}/whoami`, { signal: AbortSignal.timeout(300) }).then(r => r.json())
825
+ if (w.repoPath && realDirectory(w.repoPath) === p) { already = b.port; break }
826
+ } catch {}
827
+ }
828
+ if (already) out = { ok: true, already }
829
+ else if (spawningPaths.has(p)) out = { ok: true }
830
+ else {
831
+ spawningPaths.add(p); setTimeout(() => spawningPaths.delete(p), 30000)
832
+ const cp = await import('node:child_process')
833
+ const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), p, '--no-open'], { detached: true, stdio: 'ignore', shell: false })
834
+ child.unref()
835
+ setTimeout(discoverBoards, 2500)
836
+ setTimeout(discoverBoards, 6000)
837
+ out = { ok: true }
838
+ }
839
+ }
840
+ } catch {}
841
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out))
842
+ }, MAX_CONTROL_BODY_BYTES)
843
+ } else if (u.pathname === '/nudge') {
844
+ const cwd = u.searchParams.get('cwd') || ''
845
+ const st = planStaleness()
846
+ const candidate = realDirectory(cwd)
847
+ const root = realDirectory(repo)
848
+ const mine = !!(candidate && root && isWithin(root, candidate))
849
+ res.writeHead(200, { 'content-type': 'text/plain' }).end(mine && st.stale
850
+ ? `Note from agenttrail: PLAN.md in this repo is ${st.minutes} minutes behind the code. Re-verify task statuses against what you and other sessions actually changed, mark your current task [~], and keep files: globs current.`
851
+ : '')
852
+ } else if (u.pathname === '/tree-of') {
853
+ const p = parseInt(u.searchParams.get('port'), 10)
854
+ fetch(`http://127.0.0.1:${p}/tree`, { signal: AbortSignal.timeout(1500) })
855
+ .then(r => r.text()).then(t => res.writeHead(200, { 'content-type': 'application/json' }).end(t))
856
+ .catch(() => res.writeHead(502).end('{}'))
857
+ } else if (u.pathname === '/summary') {
858
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(summaryModel()))
859
+ } else if (u.pathname === '/fleet') {
860
+ fleetModel().then(d => res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(d)))
861
+ } else if (u.pathname === '/model') {
862
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(model()))
863
+ } else if (u.pathname === '/events') {
864
+ res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
865
+ res.write(`data: ${JSON.stringify(model())}\n\n`)
866
+ clients.add(res)
867
+ req.on('close', () => clients.delete(res))
868
+ } else if (u.pathname === '/hook' && req.method === 'POST') {
869
+ readCapped(req, res, body => {
870
+ try { if (handleHookEvent(JSON.parse(body || '{}'))) hookTick() } catch {}
871
+ res.writeHead(200).end()
872
+ }, MAX_CONTROL_BODY_BYTES)
873
+ } else if (u.pathname === '/ask' && req.method === 'POST') {
874
+ // AOS patch: ask engine — register a question the human answers on the map.
875
+ // Pure relay: an answer here never runs anything (go-gate keeps sole authority).
876
+ readCapped(req, res, body => {
877
+ let q = null, askedBy = 'agent', timeoutMs = 30 * 60e3
878
+ try {
879
+ const j = JSON.parse(body || '{}')
880
+ if (typeof j.question === 'string' && j.question.trim()) q = j.question.trim().slice(0, 500)
881
+ if (Number.isFinite(j.timeoutMs) && j.timeoutMs > 0) timeoutMs = Math.min(j.timeoutMs, 24 * 3600e3)
882
+ if (typeof j.askedBy === 'string' && j.askedBy.trim()) askedBy = j.askedBy.trim().slice(0, 60)
883
+ } catch {}
884
+ if (!q) { res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'question required' })); return }
885
+ const now = Date.now()
886
+ const ask = { id: crypto.randomUUID(), question: q, askedBy, askedAt: now, expiryAt: now + timeoutMs, status: 'pending', decidedAt: null, by: null, expired: false }
887
+ asks.push(ask)
888
+ if (asks.length > 100) { const i = asks.findIndex(x => x.status !== 'pending'); asks.splice(i === -1 ? 0 : i, 1) } // cap: drop oldest resolved
889
+ stateDirty = true
890
+ broadcastTick()
891
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, id: ask.id, port }))
892
+ })
893
+ } else if (u.pathname.startsWith('/ask/') && req.method === 'GET') {
894
+ // AOS patch: ask engine — poll one question; a pending one past expiry flips to
895
+ // deny (by:'timeout', logged) on read; carries the last 10 decisions for the map
896
+ expireAsks()
897
+ const a = asks.find(x => x.id === u.pathname.slice(5))
898
+ if (!a) res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'unknown ask' }))
899
+ else res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, ask: a, decisions: decisions.slice(-10) }))
900
+ } else if (u.pathname === '/answer' && req.method === 'POST') {
901
+ // AOS patch: ask engine, CSRF-hard. Requires exact `content-type: application/json`
902
+ // (foreign form posts die here; cross-origin fetches die in the preflight this
903
+ // server never answers — no OPTIONS handler, no Access-Control-* headers anywhere)
904
+ // and, when an `origin` header is present, it must be the map's own
905
+ // http://localhost:<port> / http://127.0.0.1:<port>. Non-browser local callers
906
+ // (AO, scripts) legitimately send no Origin and stay allowed. Loopback-only bind.
907
+ const ct = req.headers['content-type']
908
+ const origin = req.headers['origin']
909
+ const ownOrigin = origin === `http://localhost:${port}` || origin === `http://127.0.0.1:${port}`
910
+ if (ct !== 'application/json' || (origin !== undefined && !ownOrigin)) {
911
+ res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'forbidden' }))
912
+ } else {
913
+ readCapped(req, res, body => {
914
+ let id = null, decision = null
915
+ try {
916
+ const j = JSON.parse(body || '{}')
917
+ if (typeof j.id === 'string') id = j.id
918
+ if (j.decision === 'go' || j.decision === 'deny') decision = j.decision
919
+ } catch {}
920
+ const a = id ? asks.find(x => x.id === id) : null
921
+ if (!a || !decision) { res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'bad request' })); return }
922
+ if (a.status !== 'pending') { res.writeHead(409, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'already decided' })); return }
923
+ a.status = decision; a.decidedAt = Date.now(); a.by = 'you'
924
+ decisions.push({ question: a.question, decision, by: 'you', at: new Date().toISOString() })
925
+ if (decisions.length > 100) decisions.splice(0, decisions.length - 100)
926
+ stateDirty = true
927
+ broadcastTick()
928
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
929
+ })
930
+ }
931
+ } else res.writeHead(404).end()
932
+ })
933
+ // AOS patch: body reader for the ask/answer routes, capped at 4 KB
934
+ function readCapped(req, res, cb, maxBytes = MAX_CONTROL_BODY_BYTES) {
935
+ const chunks = []
936
+ let size = 0
937
+ let done = false
938
+ req.on('data', chunk => {
939
+ if (done) return
940
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
941
+ size += buffer.length
942
+ if (size > maxBytes) {
943
+ done = true
944
+ chunks.length = 0
945
+ res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'body too large' }))
946
+ req.destroy()
947
+ return
948
+ }
949
+ chunks.push(buffer)
950
+ })
951
+ req.on('end', () => {
952
+ if (done) return
953
+ done = true
954
+ cb(Buffer.concat(chunks).toString('utf8'))
955
+ })
956
+ req.on('error', () => { done = true })
957
+ }
958
+ let lastHookTick = 0
959
+ function hookTick() {
960
+ const now = Date.now()
961
+ if (now - lastHookTick > 300) { lastHookTick = now; broadcastTick() }
962
+ }
963
+ function listenWithFallback(tries = 20) {
964
+ server.once('error', e => {
965
+ if (e.code === 'EADDRINUSE' && tries > 0) { port += 1; listenWithFallback(tries - 1) }
966
+ else { console.error('could not bind a port:', e.message); process.exit(1) }
967
+ })
968
+ server.listen(port, '127.0.0.1', onListen)
969
+ }
970
+ const BACKFILL_PROMPT = 'Read the "agenttrail plan convention" section in CLAUDE.md or AGENTS.md. Then study this repo in trust order — the code and directory layout first (what exists), git log for what is actually recent, decision logs and any in-progress build/handoff docs for what is in flight, and README/roadmap prose LAST and only for open intent (founding docs rot; cross-check their claims against git log) — and rewrite PLAN.md as the real map of this codebase: components with stable {#id}s, needs:/links: edges between them, files: globs for the paths each owns, and verb-led concrete titles with tech: sublines. Keep it to 5-9 components no matter how big the repo — a component is a part one agent could own for a session, with its own doneness and at least one edge; anything smaller is a task inside one. Verify every status against the CODE, not the docs — READMEs and roadmaps rot; mark [x] only after finding the implementing source or artifact, and cite that evidence on its tech: line (e.g. tech: TurnView.swift) so a human can audit every tick; if you cannot find evidence, leave it unchecked and say so in the decisions note. Statuses must be honest — [x] only for what verifiably exists, [~] only for what you are working on right now, everything else [ ]. Tag open tasks with an indented from: line naming provenance — from: agent when an in-progress build/handoff doc or your own declared intent claims it imminently, from: roadmap when sourced only from planning docs; omit when unsure. Record the backfill under ## decisions.'
971
+ async function firstRunFlow() {
972
+ if (planText.length || !process.stdin.isTTY) return
973
+ console.log('\nno PLAN.md here yet — the live layer works now; the map needs a plan.')
974
+ if (await askYesNo('set up the plan convention (PLAN.md skeleton + agent instructions + local hooks)? [Y/n] ')) {
975
+ await init()
976
+ planText = safeRead(planPath); parsed = parsePlan(planText); rebuildMatchers(); planMtime = statMtime(planPath)
977
+ const copied = await copyToClipboard(BACKFILL_PROMPT)
978
+ console.log(copied
979
+ ? 'backfill prompt is on your clipboard — paste it to your agent in this repo and the map draws itself.'
980
+ : 'paste this to your agent in this repo:\n\n ' + BACKFILL_PROMPT)
981
+ broadcast()
982
+ }
983
+ }
984
+ function onListen() {
985
+ console.log(`agenttrail · ${session.project} · http://localhost:${port}`)
986
+ // AOS patch: when --plan is given, show a different startup message
987
+ if (!planText) {
988
+ if (planArg) {
989
+ const planFile = path.relative(repo, planPath).split(path.sep).join('/')
990
+ console.log(`waiting for the plan: ${planFile}`)
991
+ } else {
992
+ console.log('no PLAN.md found — run `agenttrail init` in the repo to scaffold one')
993
+ }
994
+ }
995
+ if (openBrowser && !noOpen) {
996
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
997
+ import('node:child_process').then(cp => cp.spawn(opener, [`http://localhost:${port}`], { stdio: 'ignore', detached: true, shell: false }))
998
+ }
999
+ firstRunFlow()
1000
+ }
1001
+ async function bootDedup() {
1002
+ const probes = []
1003
+ for (let p = 5330; p < 5345; p++) probes.push(
1004
+ fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(400) }).then(r => r.json()).then(w => w.repoPath === repo ? p : null).catch(() => null))
1005
+ const hit = (await Promise.all(probes)).find(Boolean)
1006
+ if (hit) { console.log(`agenttrail is already running for this repo · http://localhost:${hit}`); process.exit(0) }
1007
+ listenWithFallback()
1008
+ }
1009
+ bootDedup()
1010
+
1011
+ // ---------- init ----------
1012
+ async function init() {
1013
+ fs.mkdirSync(atDir, { recursive: true })
1014
+ if (!fs.existsSync(planPath)) {
1015
+ fs.writeFileSync(planPath, `# ${path.basename(repo)}
1016
+
1017
+ ## Set up the project {#setup}
1018
+ tech: scaffolding
1019
+ - [ ] First task {#setup-first}
1020
+
1021
+ ## decisions
1022
+ `)
1023
+ console.log('wrote PLAN.md skeleton')
1024
+ }
1025
+ const marker = '<!-- agenttrail -->'
1026
+ const snippet = `\n${marker}\n## agenttrail plan convention\nMaintain PLAN.md as the living plan. It is read by the project OWNER, not by you — write it for them.\n- nodes are COMPONENTS of the system being built (\`## Plain-language name {#id}\`), not phases or sprints; keep the map at 5-9 components regardless of repo size — grow tasks, not cards, and split a component only when one agent could no longer own it for a session\n- naming rule: titles are verb-led, plain-language, and CONCRETE — the owner can tell when it is done ("Read alerts out loud", "Watch the repo"). Never engineer-speak ("fs watcher + activity signal") and never vague vibes ("Decide what matters"); put the engineer phrasing on a \`tech:\` line under the heading\n- tasks inside a component: \`- [ ] Plain outcome {#id}\`, optional indented \`tech:\` line beneath; mark a task \`[~]\` BEFORE you start it and save PLAN.md immediately — this drives the live in-progress view; flip it to \`[x]\` the moment it completes, \`[!]\` if stuck (clear once unblocked). Never batch plan updates for the end of the session\n- when you mark a task \`[~]\`, add an indented \`by: <your name>\` line under it (claude, codex, cursor, …) and leave it there when done — it is the record of who did what\n- edges under a component heading: \`needs: [id, id]\` = must come after those components; \`links: [id, id]\` = interconnected with / talks to\n- \`files: [src/audio/**, config.py]\` under a component declares which paths it owns — keep it current; it is how the live view knows which component you are really working in, including when you revisit finished work\n- \`{#id}\`s are stable — never rename, only add or remove nodes\n- if a component's job is storing or distilling KNOWLEDGE (a vault, research notes, learnings) rather than producing the product itself, add an indented \`kind: knowledge\` line under its heading — the map renders it as a knowledge organ. If a component is a HUMAN step (a person must act — review, approve, post by hand), mark it \`kind: human\` instead — it renders dashed in its own color so the owner can spot where they enter the loop. A component whose output is an operable artifact (a local console, dashboard, generated page) may add an indented \`url:\` line — the card becomes clickable and opens it; omit kind:/url: for everything else\n- open tasks carry an indented \`from:\` line naming their provenance — \`from: agent\` when YOU are declaring it as your own imminent build intent (the owner corrects these on sight if wrong), \`from: roadmap\` when it comes from planning documents (durable intent, backloggable); omit when neither\n- new work NEVER creates a component by default: it lands as your session todos plus tasks under the component whose files it touches. Add a NEW component only when the system grows a durable new part. Durable is not a prediction — test it NOW: something else already depends on it (load-bearing), deleting it would change what the product does, you can name a plausible second task for it, and it owns files no component claims. Record the addition under \`## decisions\`. Time corrects mistakes cheaply: merge a card back into its neighbor if no second change ever comes; promote a task to a card when work keeps clustering in files its siblings never touch. Remove a component only when that part is deleted from the product\n- before ending a session, graduate your plan-worthy completed todos into PLAN.md as \`[x]\` tasks (with \`by:\`) — housekeeping todos stay out of the plan\n- record any plan-affecting decision under \`## decisions\` BEFORE implementing it\n`
1027
+ // CLAUDE.md is read by Claude Code, AGENTS.md by Codex/Cursor and friends —
1028
+ // the convention block goes in both so any agent maintains the same plan.
1029
+ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
1030
+ const p = path.join(repo, name)
1031
+ if (!safeRead(p).includes(marker)) {
1032
+ fs.appendFileSync(p, snippet)
1033
+ console.log(`appended agenttrail convention block to ${name}`)
1034
+ }
1035
+ }
1036
+ if (await askYesNo('wire Claude Code hooks into .claude/settings.local.json (gitignored, local only)? [Y/n] ')) installHooks()
1037
+ const gi = path.join(repo, '.gitignore')
1038
+ const giText = safeRead(gi)
1039
+ if (!giText.includes('.agenttrail')) fs.appendFileSync(gi, (giText.endsWith('\n') || !giText ? '' : '\n') + '.agenttrail/\n')
1040
+ console.log('done — start the daemon with: agenttrail ' + repo)
1041
+ console.log('\nnext: have your agent draw the real map. Paste this to Claude Code or Codex in this repo:\n')
1042
+ console.log(' Read the "agenttrail plan convention" section in CLAUDE.md or AGENTS.md. Then study this repo in trust order — the code and directory layout first (what exists), git log for what is actually recent, decision logs and any in-progress build/handoff docs for what is in flight, and README/roadmap prose LAST and only for open intent (founding docs rot; cross-check their claims against git log) — and rewrite PLAN.md as the real map of this codebase: components with stable {#id}s, needs:/links: edges between them, files: globs for the paths each owns, and verb-led concrete titles with tech: sublines. Keep it to 5-9 components no matter how big the repo — a component is a part one agent could own for a session, with its own doneness and at least one edge; anything smaller is a task inside one. Verify every status against the CODE, not the docs — READMEs and roadmaps rot; mark [x] only after finding the implementing source or artifact, and cite that evidence on its tech: line (e.g. tech: TurnView.swift) so a human can audit every tick; if you cannot find evidence, leave it unchecked and say so in the decisions note. Statuses must be honest — [x] only for what verifiably exists, [~] only for what you are working on right now, everything else [ ]. Tag open tasks with an indented from: line naming provenance — from: agent when an in-progress build/handoff doc or your own declared intent claims it imminently, from: roadmap when sourced only from planning docs; omit when unsure. Record the backfill under ## decisions.')
1043
+ }
1044
+
1045
+ // merge our hook relay into the repo's .claude/settings.json so Claude Code
1046
+ // sessions stream tool calls + todos to the board. Additive and idempotent.
1047
+ function installHooks() {
1048
+ const dir = path.join(repo, '.claude')
1049
+ const sp = path.join(dir, 'settings.local.json')
1050
+ let cfg = {}
1051
+ try { cfg = JSON.parse(fs.readFileSync(sp, 'utf8')) } catch {}
1052
+ const cmd = `node ${JSON.stringify(fileURLToPath(import.meta.url))} hook`
1053
+ cfg.hooks = cfg.hooks || {}
1054
+ let added = 0
1055
+ for (const ev of ['PreToolUse', 'PostToolUse', 'Stop', 'SessionStart', 'SubagentStop']) {
1056
+ const list = cfg.hooks[ev] = cfg.hooks[ev] || []
1057
+ const present = JSON.stringify(list).includes('agenttrail')
1058
+ if (!present) { list.push({ matcher: '*', hooks: [{ type: 'command', command: cmd }] }); added++ }
1059
+ }
1060
+ if (added) {
1061
+ fs.mkdirSync(dir, { recursive: true })
1062
+ fs.writeFileSync(sp, JSON.stringify(cfg, null, 2) + '\n')
1063
+ console.log(`wired ${added} claude code hooks into .claude/settings.local.json (live tool + todo stream)`)
1064
+ }
1065
+ }
1066
+
1067
+ // ---------- lifecycle: resurrect known boards / start at login ----------
1068
+ async function upAll() {
1069
+ const dir = path.join(os.homedir(), '.agenttrail')
1070
+ let entries = []
1071
+ try { entries = fs.readdirSync(dir).filter(f => f.endsWith('.json')) } catch {}
1072
+ const known = []
1073
+ for (const f of entries) {
1074
+ try {
1075
+ const st = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))
1076
+ if (st.repoPath && fs.existsSync(st.repoPath)) known.push({ repo: st.repoPath, port: st.port || 5330 })
1077
+ } catch {}
1078
+ }
1079
+ if (!known.length) { console.log('no known repos yet — run agenttrail in a repo first'); return }
1080
+ const cp = await import('node:child_process')
1081
+ for (const k of known) {
1082
+ const live = await fetch(`http://127.0.0.1:${k.port}/whoami`, { signal: AbortSignal.timeout(300) }).then(r => r.json()).catch(() => null)
1083
+ if (live) { console.log(`already up: ${k.repo} (:${k.port})`); continue }
1084
+ const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), k.repo, '--port', String(k.port), '--no-open'], { detached: true, stdio: 'ignore', shell: false })
1085
+ child.unref()
1086
+ console.log(`started: ${k.repo} (:${k.port})`)
1087
+ }
1088
+ }
1089
+ function autostart() {
1090
+ const self = fileURLToPath(import.meta.url)
1091
+ if (process.platform === 'darwin') {
1092
+ const label = 'dev.agenttrail.' + crypto.createHash('sha1').update(repo).digest('hex').slice(0, 8)
1093
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n <key>Label</key><string>${label}</string>\n <key>ProgramArguments</key><array><string>${process.execPath}</string><string>${self}</string><string>${repo}</string><string>--port</string><string>${String(port)}</string><string>--no-open</string></array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><true/>\n</dict></plist>\n`
1094
+ const dest = path.join(os.homedir(), 'Library', 'LaunchAgents', label + '.plist')
1095
+ if (printFlag) { console.log(plist); return }
1096
+ if (removeFlag) {
1097
+ try { fs.unlinkSync(dest); console.log('removed', dest, '— run: launchctl unload', dest) } catch { console.log('nothing to remove') }
1098
+ return
1099
+ }
1100
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
1101
+ fs.writeFileSync(dest, plist)
1102
+ console.log(`wrote ${dest}\nactivate now: launchctl load ${dest}\nthe board for ${repo} will start at every login and restart if it dies.`)
1103
+ } else if (process.platform === 'linux') {
1104
+ const name = 'agenttrail-' + crypto.createHash('sha1').update(repo).digest('hex').slice(0, 8)
1105
+ const unit = `[Unit]\nDescription=agenttrail board for ${repo}\n\n[Service]\nExecStart=${process.execPath} ${self} ${repo} --port ${String(port)} --no-open\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n`
1106
+ const dest = path.join(os.homedir(), '.config', 'systemd', 'user', name + '.service')
1107
+ if (printFlag) { console.log(unit); return }
1108
+ if (removeFlag) { try { fs.unlinkSync(dest); console.log('removed', dest) } catch { console.log('nothing to remove') } return }
1109
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
1110
+ fs.writeFileSync(dest, unit)
1111
+ console.log(`wrote ${dest}\nactivate now: systemctl --user enable --now ${name}`)
1112
+ } else console.log('autostart: macOS and Linux only for now — on Windows, add "agenttrail up" to your startup apps')
1113
+ }