@hybridlabor-api/aos 4.6.6 → 4.7.0

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