@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.
- package/.agents/agents.md +1 -0
- package/.agents/nodes.json +5 -1
- package/.agents/state.schema.json +2 -1
- package/.agents/vendor-manifest.json +50 -0
- package/.claude/agents/architect.md +4 -0
- package/.claude/agents/security-reviewer.md +1 -1
- package/.claude/hooks/memb-inject.mjs +257 -135
- package/.claude/hooks/trail-relay.mjs +18 -0
- package/.claude/settings.json +70 -0
- package/.claude/workflows/startcycle-dispatch.mjs +29 -1
- package/.opencode/agents/architect.md +4 -0
- package/.opencode/plugins/bdb-aos.js +75 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/bin/aos-uninstall.mjs +55 -1
- package/docs/handover/agenttrail-for-ao.md +58 -0
- package/docs/handover/deja-for-ao.md +51 -0
- package/installer.js +141 -37
- package/mcp_config.json +4 -0
- package/mcps/mcsc/README.md +24 -0
- package/mcps/mcsc/package-lock.json +1234 -0
- package/mcps/mcsc/package.json +14 -0
- package/mcps/mcsc/packages/core/bin/mcsc.js +162 -0
- package/mcps/mcsc/packages/core/rulebook.default.yaml +49 -0
- package/mcps/mcsc/packages/core/schema/rulebook.schema.json +56 -0
- package/mcps/mcsc/packages/core/src/adapters/agy.js +107 -0
- package/mcps/mcsc/packages/core/src/adapters/codex.js +56 -0
- package/mcps/mcsc/packages/core/src/adapters/native.js +14 -0
- package/mcps/mcsc/packages/core/src/adapters/opencode.js +183 -0
- package/mcps/mcsc/packages/core/src/adapters/types.js +21 -0
- package/mcps/mcsc/packages/core/src/capabilities/buildOfferTable.js +34 -0
- package/mcps/mcsc/packages/core/src/capabilities/registry.js +99 -0
- package/mcps/mcsc/packages/core/src/capabilities/types.js +24 -0
- package/mcps/mcsc/packages/core/src/detect/cache.js +47 -0
- package/mcps/mcsc/packages/core/src/detect/inventory.js +73 -0
- package/mcps/mcsc/packages/core/src/detect/probes.js +158 -0
- package/mcps/mcsc/packages/core/src/detect/types.js +26 -0
- package/mcps/mcsc/packages/core/src/rulebook/load.js +53 -0
- package/mcps/mcsc/packages/core/src/rulebook/resolve.js +227 -0
- package/mcps/mcsc/packages/core/src/rulebook/rulebook.types.js +89 -0
- package/mcps/mcsc/packages/core/src/trail.js +17 -0
- package/mcps/mcsc/packages/mcp/server.js +302 -0
- package/mcps/mcsc/server.js +2 -0
- package/package.json +3 -1
- package/skills/basic/bdbmediastorm/SKILL.md +1 -1
- package/skills/basic/godmode-ui-ux/SKILL.md +2 -2
- package/skills/basic/godmode-ui-ux/references/anti-slop-rules.md +44 -40
- package/skills/basic/godmode-ui-ux/references/taste-archetypes.md +49 -0
- package/skills/basic/startcycle/SKILL.md +7 -0
- package/skills/basic/startcycle-graph/SKILL.md +12 -0
- package/skills/basic/startcycle-graph-user/SKILL.md +3 -0
- package/skills/basic/teamwork-preview/SKILL.md +2 -0
- package/skills/bdbrainstorm/SKILL.md +1 -0
- package/skills/global_config/agenttrail/LICENSE +21 -0
- package/skills/global_config/agenttrail/SKILL.md +48 -0
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +1002 -0
- package/skills/global_config/agenttrail/public/index.html +661 -0
- package/skills/global_config/aos-project-init/SKILL.md +25 -5
- package/skills/global_config/aos-setup/SKILL.md +34 -1
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +115 -1
- package/skills/global_config/archify/LICENSE +22 -0
- package/skills/global_config/archify/SKILL.md +140 -0
- package/skills/global_config/archify/THIRD_PARTY_NOTICES.md +69 -0
- package/skills/global_config/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/global_config/archify/assets/template.html +14937 -0
- package/skills/global_config/archify/bin/archify.mjs +2139 -0
- package/skills/global_config/archify/bin/open-artifact.mjs +86 -0
- package/skills/global_config/archify/bin/preview.mjs +653 -0
- package/skills/global_config/archify/bin/visual-check.mjs +829 -0
- package/skills/global_config/archify/brand-marks/README.md +31 -0
- package/skills/global_config/archify/brand-marks/catalog.json +131 -0
- package/skills/global_config/archify/delta/architecture-delta.mjs +1232 -0
- package/skills/global_config/archify/examples/agent-run.lifecycle.json +60 -0
- package/skills/global_config/archify/examples/agent-tool-call.workflow.json +94 -0
- package/skills/global_config/archify/examples/async-job-roundtrip.sequence.json +61 -0
- package/skills/global_config/archify/examples/brand-aware-delivery.architecture.json +47 -0
- package/skills/global_config/archify/examples/cache-miss-request.sequence.json +82 -0
- package/skills/global_config/archify/examples/checkout-platform.base.architecture.json +31 -0
- package/skills/global_config/archify/examples/checkout-platform.head.architecture.json +31 -0
- package/skills/global_config/archify/examples/deployment-release.lifecycle.json +49 -0
- package/skills/global_config/archify/examples/event-stream.dataflow.json +57 -0
- package/skills/global_config/archify/examples/incident-response.workflow.json +64 -0
- package/skills/global_config/archify/examples/product-analytics.dataflow.json +76 -0
- package/skills/global_config/archify/examples/production-deployment.architecture.json +71 -0
- package/skills/global_config/archify/examples/release-delivery.workflow.json +62 -0
- package/skills/global_config/archify/examples/web-app.architecture.json +46 -0
- package/skills/global_config/archify/migrations/workflow-v2.mjs +279 -0
- package/skills/global_config/archify/package.json +39 -0
- package/skills/global_config/archify/recipes/scenarios.mjs +391 -0
- package/skills/global_config/archify/references/authoring-contract.md +247 -0
- package/skills/global_config/archify/references/brand-marks.md +65 -0
- package/skills/global_config/archify/references/delivery-contract.md +138 -0
- package/skills/global_config/archify/references/viewer-runtime.md +45 -0
- package/skills/global_config/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/global_config/archify/renderers/architecture/render-architecture.mjs +1078 -0
- package/skills/global_config/archify/renderers/dataflow/README.md +104 -0
- package/skills/global_config/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- package/skills/global_config/archify/renderers/lifecycle/README.md +115 -0
- package/skills/global_config/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- package/skills/global_config/archify/renderers/sequence/README.md +114 -0
- package/skills/global_config/archify/renderers/sequence/render-sequence.mjs +464 -0
- package/skills/global_config/archify/renderers/shared/brand-marks.mjs +563 -0
- package/skills/global_config/archify/renderers/shared/cli.mjs +218 -0
- package/skills/global_config/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/skills/global_config/archify/renderers/shared/diagnostics.mjs +160 -0
- package/skills/global_config/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/global_config/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/skills/global_config/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/global_config/archify/renderers/shared/geometry.mjs +1423 -0
- package/skills/global_config/archify/renderers/shared/i18n.mjs +595 -0
- package/skills/global_config/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/global_config/archify/renderers/shared/legend.mjs +217 -0
- package/skills/global_config/archify/renderers/shared/output-path.mjs +340 -0
- package/skills/global_config/archify/renderers/shared/repository-evidence.mjs +239 -0
- package/skills/global_config/archify/renderers/shared/repository-location.mjs +58 -0
- package/skills/global_config/archify/renderers/shared/text-fit.mjs +49 -0
- package/skills/global_config/archify/renderers/shared/utils.mjs +232 -0
- package/skills/global_config/archify/renderers/shared/validator.mjs +86 -0
- package/skills/global_config/archify/renderers/workflow/README.md +223 -0
- package/skills/global_config/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/skills/global_config/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/skills/global_config/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/global_config/archify/schemas/README.md +211 -0
- package/skills/global_config/archify/schemas/architecture.schema.json +178 -0
- package/skills/global_config/archify/schemas/common.schema.json +115 -0
- package/skills/global_config/archify/schemas/dataflow.schema.json +243 -0
- package/skills/global_config/archify/schemas/lifecycle.schema.json +266 -0
- package/skills/global_config/archify/schemas/sequence.schema.json +223 -0
- package/skills/global_config/archify/schemas/workflow.schema.json +428 -0
- package/skills/global_config/archify/scripts/check-render-output.mjs +916 -0
- package/skills/global_config/archify/scripts/generate-brand-marks.mjs +141 -0
- package/skills/global_config/archify/scripts/generate-validators.mjs +66 -0
- package/skills/global_config/archify/scripts/render-examples.mjs +26 -0
- package/skills/global_config/archify/skill-release.json +10 -0
- package/skills/global_config/bdb-security-audit/SKILL.md +109 -0
- package/skills/global_config/bdb-security-audit/references/audit-checklists.md +172 -0
- package/skills/global_config/bdb-security-audit/references/skylos-guide.md +139 -0
- package/skills/global_config/bdb-security-audit/references/xalgorix-bridge.md +110 -0
- package/skills/global_config/bdbdesignpro/SKILL.md +4 -4
- package/skills/global_config/bdbdesignpro/references/bdb-motion-tokens.md +52 -31
- package/skills/global_config/bdbresilience/SKILL.md +10 -0
- package/skills/global_config/brag/SKILL.md +179 -0
- package/skills/global_config/brag/assets/music/README.md +20 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.json +3846 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-1-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.json +1826 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-10-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.json +2266 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-11-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.json +2806 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-12-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.json +2796 -0
- package/skills/global_config/brag/assets/music/cues/happy-beats-business-moves-vol-9-by-ende-dot-app.music-cues.md +35 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-1-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-10-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-11-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-12-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/music/happy-beats-business-moves-vol-9-by-ende-dot-app.mp3 +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-fan-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-fan-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-place-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shove-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-shuffle.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-7.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/card-slide-8.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/cards-pack-open-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chip-lay-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-collide-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-handle-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/chips-stack-6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-grab-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-grab-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-shake-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/dice-throw-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/casino/die-throw-4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_carpet_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_concrete_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_grass_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_snow_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/footstep_wood_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactBell_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGeneric_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactGlass_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMetal_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactMining_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlank_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPlate_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactPunch_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactSoft_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactTin_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_heavy_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_light_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_000.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/impact/impactWood_medium_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/bong_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/click_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/drop_003.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/error_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/error_006.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/glitch_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/glitch_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/select_008.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_001.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_002.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_004.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_005.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_006.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/interface/switch_007.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-001.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-002.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-003.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-004.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-005.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-006.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-007.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-008.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-009.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-010.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-011.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-012.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-013.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-014.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-015.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-016.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-017.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-018.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-019.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-020.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-021.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-022.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-023.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-024.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-025.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-026.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-027.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-028.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-029.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-030.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-031.wav +0 -0
- package/skills/global_config/brag/assets/sfx/keyboard/keypress-032.wav +0 -0
- package/skills/global_config/brag/assets/sfx/sfx-analysis.json +15584 -0
- package/skills/global_config/brag/assets/sfx/sfx-analysis.md +75 -0
- package/skills/global_config/brag/assets/sfx/ui/click1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/click5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/mouseclick1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/rollover5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch1.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch10.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch11.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch12.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch13.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch14.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch15.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch16.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch17.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch18.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch19.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch2.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch20.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch21.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch22.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch23.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch24.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch25.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch26.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch27.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch28.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch29.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch3.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch30.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch31.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch32.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch33.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch34.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch35.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch36.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch37.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch38.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch4.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch5.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch6.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch7.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch8.ogg +0 -0
- package/skills/global_config/brag/assets/sfx/ui/switch9.ogg +0 -0
- package/skills/global_config/brag/references/audio.md +307 -0
- package/skills/global_config/brag/references/step-1-inspect.md +133 -0
- package/skills/global_config/brag/references/step-2-plan.md +234 -0
- package/skills/global_config/brag/references/step-3-compose.md +210 -0
- package/skills/global_config/brag/references/step-4-deliver.md +168 -0
- package/skills/global_config/brag/references/tones.md +231 -0
- package/skills/global_config/brag/scripts/analyze_music_cues.py +309 -0
- package/skills/global_config/brag/scripts/pyproject.toml +14 -0
- package/skills/global_config/brag/scripts/uv.lock +819 -0
- package/skills/global_config/deja-memory/SKILL.md +42 -0
- package/skills/global_config/editable-design/LICENSE +202 -0
- package/skills/global_config/editable-design/README.md +34 -0
- package/skills/global_config/editable-design/SKILL.md +630 -0
- package/skills/global_config/editable-design/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/global_config/editable-design/agents/openai.yaml +4 -0
- package/skills/global_config/editable-design/assets/editor/layer-editor.css +262 -0
- package/skills/global_config/editable-design/assets/editor/layer-editor.js +1929 -0
- package/skills/global_config/editable-design/assets/poster-starter/README.md +92 -0
- package/skills/global_config/editable-design/assets/poster-starter/assets/.gitkeep +0 -0
- package/skills/global_config/editable-design/assets/poster-starter/index.html +91 -0
- package/skills/global_config/editable-design/assets/poster-starter/poster.json +6 -0
- package/skills/global_config/editable-design/assets/poster-starter/reference/.gitkeep +1 -0
- package/skills/global_config/editable-design/assets/replay/icons/assets.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/html.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/input.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/layers.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/plan.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/reference.png +0 -0
- package/skills/global_config/editable-design/assets/replay/icons/review.png +0 -0
- package/skills/global_config/editable-design/assets/replay/index.html +59 -0
- package/skills/global_config/editable-design/assets/replay/replay-motion.js +410 -0
- package/skills/global_config/editable-design/assets/replay/replay.css +380 -0
- package/skills/global_config/editable-design/assets/replay/replay.js +374 -0
- package/skills/global_config/editable-design/font-kit/package-lock.json +115 -0
- package/skills/global_config/editable-design/font-kit/package.json +19 -0
- package/skills/global_config/editable-design/pack.sh +20 -0
- package/skills/global_config/editable-design/references/asset-architecture.md +72 -0
- package/skills/global_config/editable-design/references/editor-pitfalls.md +70 -0
- package/skills/global_config/editable-design/references/editor-runtime.md +131 -0
- package/skills/global_config/editable-design/references/font-system.md +89 -0
- package/skills/global_config/editable-design/references/imagery.md +145 -0
- package/skills/global_config/editable-design/references/layout-typography.md +116 -0
- package/skills/global_config/editable-design/references/replay-contract.md +169 -0
- package/skills/global_config/editable-design/scripts/_browser.mjs +247 -0
- package/skills/global_config/editable-design/scripts/bake.mjs +87 -0
- package/skills/global_config/editable-design/scripts/build-replay.mjs +316 -0
- package/skills/global_config/editable-design/scripts/check-contract.mjs +293 -0
- package/skills/global_config/editable-design/scripts/check-fonts.sh +252 -0
- package/skills/global_config/editable-design/scripts/check-poster.sh +70 -0
- package/skills/global_config/editable-design/scripts/doctor.sh +78 -0
- package/skills/global_config/editable-design/scripts/explode.mjs +115 -0
- package/skills/global_config/editable-design/scripts/font-kit.mjs +187 -0
- package/skills/global_config/editable-design/scripts/import-asset.sh +23 -0
- package/skills/global_config/editable-design/scripts/init-poster.sh +64 -0
- package/skills/global_config/editable-design/scripts/install-font-kit.sh +13 -0
- package/skills/global_config/editable-design/scripts/package-lock.json +334 -0
- package/skills/global_config/editable-design/scripts/package.json +14 -0
- package/skills/global_config/editable-design/scripts/remove-chroma-key.py +201 -0
- package/skills/global_config/editable-design/scripts/render-poster.sh +202 -0
- package/skills/global_config/editable-design/scripts/trace.sh +159 -0
- package/skills/global_config/editable-design/scripts/verify-replay.mjs +97 -0
- package/skills/global_config/editable-design/scripts/verify.mjs +782 -0
- package/skills/global_config/editable-design/scripts/wire-editor.mjs +104 -0
- package/skills/global_config/editable-design/tests/fixtures/small-explode/index.html +86 -0
- 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
|
+
}
|