@stravica-ai/digital-operator-pa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/bin/desk +9 -0
  4. package/bin/distill +2 -0
  5. package/bin/estate-sync +2 -0
  6. package/bin/launch +2 -0
  7. package/bin/memory-sync +2 -0
  8. package/bin/migrate-inspect +2 -0
  9. package/bin/pa +2 -0
  10. package/bin/pa-coordinator-hook +2 -0
  11. package/bin/pa-init +2 -0
  12. package/bin/research +2 -0
  13. package/bin/scheduler +2 -0
  14. package/bin/secrets +2 -0
  15. package/bin/slice +2 -0
  16. package/bin/smoke +9 -0
  17. package/bin/sync +2 -0
  18. package/dist/engineering-pa-cli/README.md +205 -0
  19. package/dist/engineering-pa-cli/bin/estate-sync +2 -0
  20. package/dist/engineering-pa-cli/bin/launch +2 -0
  21. package/dist/engineering-pa-cli/bin/pa +2 -0
  22. package/dist/engineering-pa-cli/bin/pa-coordinator-hook +12 -0
  23. package/dist/engineering-pa-cli/bin/research +2 -0
  24. package/dist/engineering-pa-cli/bin/scheduler +2 -0
  25. package/dist/engineering-pa-cli/bin/secrets +2 -0
  26. package/dist/engineering-pa-cli/bin/smoke +9 -0
  27. package/dist/engineering-pa-cli/bin/sync +2 -0
  28. package/dist/engineering-pa-cli/config/coordinator-allowlist.json +118 -0
  29. package/dist/engineering-pa-cli/docs/coordinator-hook.md +211 -0
  30. package/dist/engineering-pa-cli/docs/estate-sync.md +412 -0
  31. package/dist/engineering-pa-cli/docs/gsc.md +175 -0
  32. package/dist/engineering-pa-cli/docs/lint-linkedin.md +139 -0
  33. package/dist/engineering-pa-cli/docs/secrets.md +221 -0
  34. package/dist/engineering-pa-cli/docs/smoke.md +113 -0
  35. package/dist/engineering-pa-cli/docs/staging-alias.md +114 -0
  36. package/dist/engineering-pa-cli/index.mjs +17 -0
  37. package/dist/engineering-pa-cli/package.json +37 -0
  38. package/dist/engineering-pa-cli/src/autonomy/cli.mjs +329 -0
  39. package/dist/engineering-pa-cli/src/autonomy/envelope.mjs +301 -0
  40. package/dist/engineering-pa-cli/src/coordinator-hook/cli.mjs +140 -0
  41. package/dist/engineering-pa-cli/src/coordinator-hook/config.mjs +134 -0
  42. package/dist/engineering-pa-cli/src/coordinator-hook/message.mjs +47 -0
  43. package/dist/engineering-pa-cli/src/coordinator-hook/policy.mjs +211 -0
  44. package/dist/engineering-pa-cli/src/coordinator-hook/repo-root.mjs +44 -0
  45. package/dist/engineering-pa-cli/src/coordinator-hook/self.mjs +30 -0
  46. package/dist/engineering-pa-cli/src/environment/cli.mjs +287 -0
  47. package/dist/engineering-pa-cli/src/environment/probes.mjs +302 -0
  48. package/dist/engineering-pa-cli/src/gsc/auth.mjs +148 -0
  49. package/dist/engineering-pa-cli/src/gsc/cli.mjs +93 -0
  50. package/dist/engineering-pa-cli/src/gsc/cloudflare.mjs +318 -0
  51. package/dist/engineering-pa-cli/src/gsc/commands/add-property.mjs +70 -0
  52. package/dist/engineering-pa-cli/src/gsc/commands/list-properties.mjs +33 -0
  53. package/dist/engineering-pa-cli/src/gsc/commands/pull-analytics.mjs +130 -0
  54. package/dist/engineering-pa-cli/src/gsc/commands/submit-sitemap.mjs +41 -0
  55. package/dist/engineering-pa-cli/src/gsc/commands/verify-via-cf-dns.mjs +189 -0
  56. package/dist/engineering-pa-cli/src/knowledge/call-log.mjs +59 -0
  57. package/dist/engineering-pa-cli/src/knowledge/cli.mjs +242 -0
  58. package/dist/engineering-pa-cli/src/knowledge/eval.mjs +446 -0
  59. package/dist/engineering-pa-cli/src/knowledge/formatters.mjs +331 -0
  60. package/dist/engineering-pa-cli/src/knowledge/graph.mjs +710 -0
  61. package/dist/engineering-pa-cli/src/launch/README.md +214 -0
  62. package/dist/engineering-pa-cli/src/launch/cli.mjs +51 -0
  63. package/dist/engineering-pa-cli/src/launch/commands/launch.mjs +106 -0
  64. package/dist/engineering-pa-cli/src/launch/identity.mjs +43 -0
  65. package/dist/engineering-pa-cli/src/launch/planner.mjs +44 -0
  66. package/dist/engineering-pa-cli/src/launch/repo-root.mjs +53 -0
  67. package/dist/engineering-pa-cli/src/launch/runner.mjs +147 -0
  68. package/dist/engineering-pa-cli/src/launch/task-loader.mjs +85 -0
  69. package/dist/engineering-pa-cli/src/lint/cli.mjs +61 -0
  70. package/dist/engineering-pa-cli/src/lint/commands/linkedin.mjs +133 -0
  71. package/dist/engineering-pa-cli/src/lint/detectors.mjs +90 -0
  72. package/dist/engineering-pa-cli/src/lint/linter.mjs +69 -0
  73. package/dist/engineering-pa-cli/src/lint/reporter.mjs +32 -0
  74. package/dist/engineering-pa-cli/src/lint/rules.mjs +132 -0
  75. package/dist/engineering-pa-cli/src/pa/cli.mjs +369 -0
  76. package/dist/engineering-pa-cli/src/pa/doc-groups.mjs +164 -0
  77. package/dist/engineering-pa-cli/src/pa/io.mjs +80 -0
  78. package/dist/engineering-pa-cli/src/pa/project-cli.mjs +385 -0
  79. package/dist/engineering-pa-cli/src/pa/projects.mjs +352 -0
  80. package/dist/engineering-pa-cli/src/pa/repo-root.mjs +64 -0
  81. package/dist/engineering-pa-cli/src/pa/session.mjs +254 -0
  82. package/dist/engineering-pa-cli/src/pa/work-cli.mjs +164 -0
  83. package/dist/engineering-pa-cli/src/pa/work.mjs +196 -0
  84. package/dist/engineering-pa-cli/src/pa/workstreams.mjs +201 -0
  85. package/dist/engineering-pa-cli/src/research/README.md +360 -0
  86. package/dist/engineering-pa-cli/src/research/briefs/extractor.md +103 -0
  87. package/dist/engineering-pa-cli/src/research/briefs/searcher.md +113 -0
  88. package/dist/engineering-pa-cli/src/research/briefs/synthesizer.md +124 -0
  89. package/dist/engineering-pa-cli/src/research/briefs/verifier.md +86 -0
  90. package/dist/engineering-pa-cli/src/research/cli.mjs +141 -0
  91. package/dist/engineering-pa-cli/src/research/commands/gates.mjs +176 -0
  92. package/dist/engineering-pa-cli/src/research/commands/list.mjs +56 -0
  93. package/dist/engineering-pa-cli/src/research/commands/mark.mjs +97 -0
  94. package/dist/engineering-pa-cli/src/research/commands/paths.mjs +69 -0
  95. package/dist/engineering-pa-cli/src/research/commands/prune.mjs +86 -0
  96. package/dist/engineering-pa-cli/src/research/commands/run.mjs +94 -0
  97. package/dist/engineering-pa-cli/src/research/commands/show.mjs +43 -0
  98. package/dist/engineering-pa-cli/src/research/docs/operator-present-flow.md +120 -0
  99. package/dist/engineering-pa-cli/src/research/format.mjs +59 -0
  100. package/dist/engineering-pa-cli/src/research/promote.mjs +260 -0
  101. package/dist/engineering-pa-cli/src/research/schema/finding.schema.json +55 -0
  102. package/dist/engineering-pa-cli/src/research/schema/gates.schema.json +34 -0
  103. package/dist/engineering-pa-cli/src/research/schema/manifest.schema.json +59 -0
  104. package/dist/engineering-pa-cli/src/research/schema/plan.schema.json +41 -0
  105. package/dist/engineering-pa-cli/src/research/schema/sources.schema.json +31 -0
  106. package/dist/engineering-pa-cli/src/research/store/findings.mjs +61 -0
  107. package/dist/engineering-pa-cli/src/research/store/gates.mjs +73 -0
  108. package/dist/engineering-pa-cli/src/research/store/index.mjs +312 -0
  109. package/dist/engineering-pa-cli/src/research/store/manifest.mjs +56 -0
  110. package/dist/engineering-pa-cli/src/research/store/plan.mjs +59 -0
  111. package/dist/engineering-pa-cli/src/research/store/sources.mjs +44 -0
  112. package/dist/engineering-pa-cli/src/research/store/validate.mjs +92 -0
  113. package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +573 -0
  114. package/dist/engineering-pa-cli/src/scheduler/cli.mjs +197 -0
  115. package/dist/engineering-pa-cli/src/secrets/cli.mjs +254 -0
  116. package/dist/engineering-pa-cli/src/secrets/commands/backup-template.mjs +67 -0
  117. package/dist/engineering-pa-cli/src/secrets/commands/backup.mjs +85 -0
  118. package/dist/engineering-pa-cli/src/secrets/commands/delete.mjs +58 -0
  119. package/dist/engineering-pa-cli/src/secrets/commands/find.mjs +36 -0
  120. package/dist/engineering-pa-cli/src/secrets/commands/get.mjs +49 -0
  121. package/dist/engineering-pa-cli/src/secrets/commands/init.mjs +69 -0
  122. package/dist/engineering-pa-cli/src/secrets/commands/list.mjs +37 -0
  123. package/dist/engineering-pa-cli/src/secrets/commands/rotate.mjs +46 -0
  124. package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +123 -0
  125. package/dist/engineering-pa-cli/src/secrets/commands/status.mjs +146 -0
  126. package/dist/engineering-pa-cli/src/secrets/commands/vaults.mjs +57 -0
  127. package/dist/engineering-pa-cli/src/secrets/commands/verify.mjs +143 -0
  128. package/dist/engineering-pa-cli/src/secrets/vault.mjs +766 -0
  129. package/dist/engineering-pa-cli/src/smoke/checks.mjs +199 -0
  130. package/dist/engineering-pa-cli/src/smoke/cli.mjs +108 -0
  131. package/dist/engineering-pa-cli/src/smoke/commands/run.mjs +237 -0
  132. package/dist/engineering-pa-cli/src/smoke/runner.mjs +191 -0
  133. package/dist/engineering-pa-cli/src/staging-alias/cli.mjs +91 -0
  134. package/dist/engineering-pa-cli/src/staging-alias/cloudflare.mjs +380 -0
  135. package/dist/engineering-pa-cli/src/staging-alias/commands/list.mjs +36 -0
  136. package/dist/engineering-pa-cli/src/staging-alias/commands/set.mjs +188 -0
  137. package/dist/engineering-pa-cli/src/staging-alias/commands/status.mjs +113 -0
  138. package/dist/engineering-pa-cli/src/staging-alias/commands/unset.mjs +95 -0
  139. package/dist/engineering-pa-cli/src/staging-alias/sites.mjs +105 -0
  140. package/dist/engineering-pa-cli/src/sync/cli.mjs +151 -0
  141. package/dist/engineering-pa-cli/src/sync/git.mjs +149 -0
  142. package/dist/engineering-pa-cli/src/sync/lock.mjs +30 -0
  143. package/dist/engineering-pa-cli/src/sync/regenerated-artifacts.mjs +114 -0
  144. package/dist/engineering-pa-cli/src/sync/registry-scope.mjs +260 -0
  145. package/dist/engineering-pa-cli/src/sync/run.mjs +531 -0
  146. package/dist/engineering-pa-cli/src/sync/runner.mjs +62 -0
  147. package/dist/engineering-pa-cli/src/sync/runs-digest.mjs +138 -0
  148. package/dist/engineering-pa-core/README.md +60 -0
  149. package/dist/engineering-pa-core/bin/desk +9 -0
  150. package/dist/engineering-pa-core/bin/distill +2 -0
  151. package/dist/engineering-pa-core/bin/memory-sync +2 -0
  152. package/dist/engineering-pa-core/bin/slice +2 -0
  153. package/dist/engineering-pa-core/docs/autonomy-envelope-tracker.md +111 -0
  154. package/dist/engineering-pa-core/docs/blueprints/README.md +45 -0
  155. package/dist/engineering-pa-core/docs/blueprints/docker-services/blueprint.md +168 -0
  156. package/dist/engineering-pa-core/docs/blueprints/git-identity/blueprint.md +116 -0
  157. package/dist/engineering-pa-core/docs/blueprints/ideas/custom-workspace-views.md +9 -0
  158. package/dist/engineering-pa-core/docs/blueprints/ideas/knowledge-domain-patterns.md +15 -0
  159. package/dist/engineering-pa-core/docs/blueprints/ideas/office-integration.md +11 -0
  160. package/dist/engineering-pa-core/docs/blueprints/ideas/team-scaling.md +9 -0
  161. package/dist/engineering-pa-core/docs/blueprints/ideas/work-personal-separation.md +9 -0
  162. package/dist/engineering-pa-core/docs/blueprints/scheduled-distillation/blueprint.md +129 -0
  163. package/dist/engineering-pa-core/docs/blueprints/tool-integration/blueprint.md +110 -0
  164. package/dist/engineering-pa-core/docs/commands/init.md +159 -0
  165. package/dist/engineering-pa-core/docs/commands/schedule.md +98 -0
  166. package/dist/engineering-pa-core/docs/commands/upgrade.md +67 -0
  167. package/dist/engineering-pa-core/docs/entity-location-map.md +93 -0
  168. package/dist/engineering-pa-core/docs/knowledge-scaffold.md +121 -0
  169. package/dist/engineering-pa-core/docs/lock.md +155 -0
  170. package/dist/engineering-pa-core/docs/memory-sync.md +28 -0
  171. package/dist/engineering-pa-core/docs/pa-operating-model.md +457 -0
  172. package/dist/engineering-pa-core/docs/scheduling.md +35 -0
  173. package/dist/engineering-pa-core/docs/standards/README.md +24 -0
  174. package/dist/engineering-pa-core/docs/standards/briefing-contract.md +161 -0
  175. package/dist/engineering-pa-core/docs/standards/tooling.md +240 -0
  176. package/dist/engineering-pa-core/docs/tools/distill.md +230 -0
  177. package/dist/engineering-pa-core/docs/tools/memory-sync.md +98 -0
  178. package/dist/engineering-pa-core/docs/tools/scheduler.md +243 -0
  179. package/dist/engineering-pa-core/docs/tools/slice.md +150 -0
  180. package/dist/engineering-pa-core/docs/upstream-issue-filing.md +88 -0
  181. package/dist/engineering-pa-core/docs/working-directory.md +29 -0
  182. package/dist/engineering-pa-core/docs/workspace-operations.md +46 -0
  183. package/dist/engineering-pa-core/index.mjs +21 -0
  184. package/dist/engineering-pa-core/knowledge/README.md +53 -0
  185. package/dist/engineering-pa-core/knowledge/canonical/.gitkeep +0 -0
  186. package/dist/engineering-pa-core/knowledge/content-voice/README.md +64 -0
  187. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-baseline.md +133 -0
  188. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-shape.md +58 -0
  189. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +33 -0
  190. package/dist/engineering-pa-core/knowledge/content-voice/content-pipeline-routing.md +58 -0
  191. package/dist/engineering-pa-core/knowledge/content-voice/new-tell-capture-procedure.md +40 -0
  192. package/dist/engineering-pa-core/knowledge/content-voice/voice-guide-shape.md +43 -0
  193. package/dist/engineering-pa-core/knowledge/delegation-dispatch/README.md +34 -0
  194. package/dist/engineering-pa-core/knowledge/delegation-dispatch/briefing-contract.md +77 -0
  195. package/dist/engineering-pa-core/knowledge/delegation-dispatch/delegation-modes.md +84 -0
  196. package/dist/engineering-pa-core/knowledge/delegation-dispatch/dispatch-envelope.md +238 -0
  197. package/dist/engineering-pa-core/knowledge/delegation-dispatch/task-groups-and-orchestrators.md +62 -0
  198. package/dist/engineering-pa-core/knowledge/delegation-dispatch/team-improvement-loop.md +48 -0
  199. package/dist/engineering-pa-core/knowledge/delegation-dispatch/tool-development-policy.md +48 -0
  200. package/dist/engineering-pa-core/knowledge/engineering-discipline/README.md +33 -0
  201. package/dist/engineering-pa-core/knowledge/engineering-discipline/blast-radius-and-safety.md +50 -0
  202. package/dist/engineering-pa-core/knowledge/engineering-discipline/investigate-root-causes.md +48 -0
  203. package/dist/engineering-pa-core/knowledge/engineering-discipline/role-ladder.md +57 -0
  204. package/dist/engineering-pa-core/knowledge/engineering-discipline/verify-runtime-behaviour.md +53 -0
  205. package/dist/engineering-pa-core/knowledge/engineering-discipline/worktree-isolation.md +50 -0
  206. package/dist/engineering-pa-core/knowledge/knowledge-memory/README.md +33 -0
  207. package/dist/engineering-pa-core/knowledge/knowledge-memory/claude-md-hygiene.md +47 -0
  208. package/dist/engineering-pa-core/knowledge/knowledge-memory/journal-and-self-improvement.md +53 -0
  209. package/dist/engineering-pa-core/knowledge/knowledge-memory/knowledge-retrieval-discipline.md +62 -0
  210. package/dist/engineering-pa-core/knowledge/knowledge-memory/librarian-synthesis-mandate.md +56 -0
  211. package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +58 -0
  212. package/dist/engineering-pa-core/knowledge/operator-interface/README.md +32 -0
  213. package/dist/engineering-pa-core/knowledge/operator-interface/communication-style.md +33 -0
  214. package/dist/engineering-pa-core/knowledge/operator-interface/escalation-and-correction.md +45 -0
  215. package/dist/engineering-pa-core/knowledge/operator-interface/push-back-discipline.md +42 -0
  216. package/dist/engineering-pa-core/knowledge/operator-interface/session-boot-and-end.md +56 -0
  217. package/dist/engineering-pa-core/knowledge/platform-tooling/README.md +37 -0
  218. package/dist/engineering-pa-core/knowledge/platform-tooling/desk-cli-reference.md +52 -0
  219. package/dist/engineering-pa-core/knowledge/platform-tooling/environment-awareness.md +91 -0
  220. package/dist/engineering-pa-core/knowledge/platform-tooling/mcp-servers-catalogue.md +52 -0
  221. package/dist/engineering-pa-core/knowledge/platform-tooling/pa-cli-reference.md +55 -0
  222. package/dist/engineering-pa-core/knowledge/platform-tooling/scheduling-and-launchd.md +56 -0
  223. package/dist/engineering-pa-core/knowledge/platform-tooling/tool-discovery-discipline.md +49 -0
  224. package/dist/engineering-pa-core/knowledge/platform-tooling/working-directory-vs-project-root.md +43 -0
  225. package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-server.md +51 -0
  226. package/dist/engineering-pa-core/knowledge/raw/.gitkeep +0 -0
  227. package/dist/engineering-pa-core/knowledge/scaffold/domains.json +5 -0
  228. package/dist/engineering-pa-core/knowledge/schema/.gitkeep +0 -0
  229. package/dist/engineering-pa-core/knowledge/work-management/README.md +33 -0
  230. package/dist/engineering-pa-core/knowledge/work-management/desk-cli.md +84 -0
  231. package/dist/engineering-pa-core/knowledge/work-management/progress-md.md +48 -0
  232. package/dist/engineering-pa-core/knowledge/work-management/projects.md +64 -0
  233. package/dist/engineering-pa-core/knowledge/work-management/spawn-log.md +48 -0
  234. package/dist/engineering-pa-core/knowledge/work-management/work-items.md +67 -0
  235. package/dist/engineering-pa-core/package.json +26 -0
  236. package/dist/engineering-pa-core/roles/agent-ops/definition.md +79 -0
  237. package/dist/engineering-pa-core/roles/dispatch-briefer/definition.md +195 -0
  238. package/dist/engineering-pa-core/roles/librarian/definition.md +176 -0
  239. package/dist/engineering-pa-core/roles/pa/definition.md +93 -0
  240. package/dist/engineering-pa-core/roles/system-admin/definition.md +106 -0
  241. package/dist/engineering-pa-core/roles/tool-developer/definition.md +116 -0
  242. package/dist/engineering-pa-core/roles/writer/definition.md +130 -0
  243. package/dist/engineering-pa-core/schema/autonomy-envelope.schema.json +61 -0
  244. package/dist/engineering-pa-core/schema/project.json +114 -0
  245. package/dist/engineering-pa-core/schema/schedule.json +63 -0
  246. package/dist/engineering-pa-core/schema/work-item.json +92 -0
  247. package/dist/engineering-pa-core/src/config/pa-package-name.mjs +30 -0
  248. package/dist/engineering-pa-core/src/desk/cli.mjs +95 -0
  249. package/dist/engineering-pa-core/src/desk/commands.mjs +398 -0
  250. package/dist/engineering-pa-core/src/desk/format.mjs +84 -0
  251. package/dist/engineering-pa-core/src/desk/repo.mjs +172 -0
  252. package/dist/engineering-pa-core/src/desk/team-scope.mjs +142 -0
  253. package/dist/engineering-pa-core/src/distill/cli.mjs +112 -0
  254. package/dist/engineering-pa-core/src/distill/prereqs.mjs +171 -0
  255. package/dist/engineering-pa-core/src/distill/prompt.mjs +74 -0
  256. package/dist/engineering-pa-core/src/distill/run.mjs +230 -0
  257. package/dist/engineering-pa-core/src/distill/runner.mjs +86 -0
  258. package/dist/engineering-pa-core/src/distill/staging.mjs +59 -0
  259. package/dist/engineering-pa-core/src/knowledge/retrieval/backlink-indexer.mjs +154 -0
  260. package/dist/engineering-pa-core/src/knowledge/retrieval/graph-builder.mjs +186 -0
  261. package/dist/engineering-pa-core/src/knowledge/retrieval/index-builder.mjs +225 -0
  262. package/dist/engineering-pa-core/src/knowledge/retrieval/query.mjs +263 -0
  263. package/dist/engineering-pa-core/src/lock/lock.mjs +202 -0
  264. package/dist/engineering-pa-core/src/memory-sync/cli-support.mjs +12 -0
  265. package/dist/engineering-pa-core/src/memory-sync/cli.mjs +56 -0
  266. package/dist/engineering-pa-core/src/memory-sync/commands/pull.mjs +44 -0
  267. package/dist/engineering-pa-core/src/memory-sync/commands/push.mjs +46 -0
  268. package/dist/engineering-pa-core/src/memory-sync/mirror.mjs +114 -0
  269. package/dist/engineering-pa-core/src/memory-sync/repo.mjs +65 -0
  270. package/dist/engineering-pa-core/src/scheduler/detect.mjs +111 -0
  271. package/dist/engineering-pa-core/src/scheduler/host.mjs +38 -0
  272. package/dist/engineering-pa-core/src/scheduler/index.mjs +13 -0
  273. package/dist/engineering-pa-core/src/scheduler/install.mjs +854 -0
  274. package/dist/engineering-pa-core/src/scheduler/list.mjs +168 -0
  275. package/dist/engineering-pa-core/src/scheduler/remove.mjs +176 -0
  276. package/dist/engineering-pa-core/src/scheduler/verify.mjs +435 -0
  277. package/dist/engineering-pa-core/src/session-boot/autonomy-injection.mjs +84 -0
  278. package/dist/engineering-pa-core/src/slice/cli.mjs +139 -0
  279. package/dist/engineering-pa-core/src/slice/inputs.mjs +174 -0
  280. package/dist/engineering-pa-core/src/slice/prompt.mjs +94 -0
  281. package/dist/engineering-pa-core/src/slice/prompts/dave-boot.md +91 -0
  282. package/dist/engineering-pa-core/src/slice/run.mjs +334 -0
  283. package/dist/engineering-pa-core-extensions-atlassian/README.md +31 -0
  284. package/dist/engineering-pa-core-extensions-atlassian/index.mjs +9 -0
  285. package/dist/engineering-pa-core-extensions-atlassian/package.json +19 -0
  286. package/dist/engineering-pa-core-extensions-n8n/README.md +29 -0
  287. package/dist/engineering-pa-core-extensions-n8n/index.mjs +8 -0
  288. package/dist/engineering-pa-core-extensions-n8n/package.json +19 -0
  289. package/dist/engineering-pa-librarian/README.md +38 -0
  290. package/dist/engineering-pa-librarian/index.mjs +12 -0
  291. package/dist/engineering-pa-librarian/package.json +19 -0
  292. package/dist/engineering-pa-migrate-inspect/README.md +131 -0
  293. package/dist/engineering-pa-migrate-inspect/bin/migrate-inspect +2 -0
  294. package/dist/engineering-pa-migrate-inspect/package.json +21 -0
  295. package/dist/engineering-pa-migrate-inspect/src/age-key-migrate.mjs +83 -0
  296. package/dist/engineering-pa-migrate-inspect/src/cli.mjs +97 -0
  297. package/dist/engineering-pa-migrate-inspect/src/inspector.mjs +237 -0
  298. package/dist/engineering-pa-migrate-inspect/src/platforms/linux.mjs +87 -0
  299. package/dist/engineering-pa-migrate-inspect/src/platforms/macos.mjs +105 -0
  300. package/dist/engineering-pa-migrate-inspect/src/runner.mjs +87 -0
  301. package/dist/engineering-pa-runbook/README.md +103 -0
  302. package/dist/engineering-pa-runbook/package.json +18 -0
  303. package/dist/engineering-pa-runbook/reference/dex-template.runbook.json +66 -0
  304. package/dist/engineering-pa-runbook/reference/hq-dave.runbook.json +69 -0
  305. package/dist/engineering-pa-runbook/reference/smarthome01.runbook.json +92 -0
  306. package/dist/engineering-pa-runbook/schema/inspection.schema.json +113 -0
  307. package/dist/engineering-pa-runbook/schema/runbook.schema.json +67 -0
  308. package/dist/engineering-pa-runbook/src/validator.mjs +167 -0
  309. package/dist/engineering-pa-scaffold/README.md +164 -0
  310. package/dist/engineering-pa-scaffold/bin/pa-init +2 -0
  311. package/dist/engineering-pa-scaffold/index.mjs +42 -0
  312. package/dist/engineering-pa-scaffold/package.json +22 -0
  313. package/dist/engineering-pa-scaffold/src/cli.mjs +115 -0
  314. package/dist/engineering-pa-scaffold/src/commands/init.mjs +169 -0
  315. package/dist/engineering-pa-scaffold/src/generator.mjs +354 -0
  316. package/dist/engineering-pa-scaffold/src/prompts.mjs +125 -0
  317. package/dist/engineering-pa-scaffold/src/runner.mjs +142 -0
  318. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +196 -0
  319. package/dist/engineering-pa-scaffold/templates/operator-README.md.template +44 -0
  320. package/dist/engineering-pa-types-engineering/README.md +35 -0
  321. package/dist/engineering-pa-types-engineering/docs/commands/product-build.md +120 -0
  322. package/dist/engineering-pa-types-engineering/index.mjs +9 -0
  323. package/dist/engineering-pa-types-engineering/package.json +19 -0
  324. package/dist/engineering-pa-types-engineering/roles/product-engineer/definition.md +650 -0
  325. package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/build-spec.md +82 -0
  326. package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/preflight-checklist.md +158 -0
  327. package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/worker-brief.md +130 -0
  328. package/dist/engineering-pa-workspace/README.md +197 -0
  329. package/dist/engineering-pa-workspace/app/assets/md.js +187 -0
  330. package/dist/engineering-pa-workspace/app/assets/modal.js +120 -0
  331. package/dist/engineering-pa-workspace/app/assets/theme.css +132 -0
  332. package/dist/engineering-pa-workspace/app/assets/theme.js +159 -0
  333. package/dist/engineering-pa-workspace/app/banner.js +320 -0
  334. package/dist/engineering-pa-workspace/app/delegations.html +345 -0
  335. package/dist/engineering-pa-workspace/app/home.html +1060 -0
  336. package/dist/engineering-pa-workspace/app/index.html +361 -0
  337. package/dist/engineering-pa-workspace/app/knowledge-map.html +163 -0
  338. package/dist/engineering-pa-workspace/app/knowledge.html +304 -0
  339. package/dist/engineering-pa-workspace/app/nav.json +75 -0
  340. package/dist/engineering-pa-workspace/app/notes.html +199 -0
  341. package/dist/engineering-pa-workspace/app/onboarding.html +592 -0
  342. package/dist/engineering-pa-workspace/app/profile.html +243 -0
  343. package/dist/engineering-pa-workspace/app/project-detail.html +432 -0
  344. package/dist/engineering-pa-workspace/app/projects.html +297 -0
  345. package/dist/engineering-pa-workspace/app/repos.html +598 -0
  346. package/dist/engineering-pa-workspace/app/schedules.html +486 -0
  347. package/dist/engineering-pa-workspace/app/secrets.html +164 -0
  348. package/dist/engineering-pa-workspace/app/staging.html +319 -0
  349. package/dist/engineering-pa-workspace/app/tasks.html +465 -0
  350. package/dist/engineering-pa-workspace/app/vendor/chart.umd.min.js +21 -0
  351. package/dist/engineering-pa-workspace/app/viewer.html +116 -0
  352. package/dist/engineering-pa-workspace/docs/DAEMON.md +104 -0
  353. package/dist/engineering-pa-workspace/docs/page-blueprint.md +286 -0
  354. package/dist/engineering-pa-workspace/docs/status-mon.md +168 -0
  355. package/dist/engineering-pa-workspace/index.mjs +12 -0
  356. package/dist/engineering-pa-workspace/nav.json.example +8 -0
  357. package/dist/engineering-pa-workspace/package.json +27 -0
  358. package/dist/engineering-pa-workspace/src/api.mjs +2086 -0
  359. package/dist/engineering-pa-workspace/src/install-daemon.mjs +573 -0
  360. package/dist/engineering-pa-workspace/src/nav.mjs +190 -0
  361. package/dist/engineering-pa-workspace/src/secrets-api.mjs +175 -0
  362. package/dist/engineering-pa-workspace/src/server.mjs +516 -0
  363. package/dist/engineering-pa-workspace/src/status-mon/poll.mjs +220 -0
  364. package/dist/engineering-pa-workspace/src/status-mon/poller.mjs +113 -0
  365. package/dist/engineering-pa-workspace/src/status-mon/run.mjs +133 -0
  366. package/dist/engineering-pa-workspace/src/supervisor.mjs +264 -0
  367. package/dist/engineering-pa-workspace/src/ws.mjs +633 -0
  368. package/index.mjs +60 -0
  369. package/package.json +58 -0
  370. package/templates/banned-ai-tells.md +102 -0
  371. package/templates/purpose.md +105 -0
  372. package/templates/voice-guide.md +98 -0
@@ -0,0 +1,854 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.mjs -- Install a Dave schedule definition using the specified backend
4
+ *
5
+ * Usage: node node_modules/@stravica-ai/engineering-pa-core/src/scheduler/install.mjs <schedule.json> <backend>
6
+ *
7
+ * Reads a schedule JSON file and installs it using the specified backend
8
+ * (cron, launchd, systemd, schtasks). The schedule field uses cron syntax
9
+ * (minute hour day month weekday). This script translates to the native
10
+ * format for each backend.
11
+ */
12
+
13
+ import {
14
+ readFileSync, writeFileSync, mkdirSync, chmodSync, realpathSync,
15
+ existsSync, statSync, readdirSync,
16
+ } from 'node:fs';
17
+ import { join, dirname, resolve } from 'node:path';
18
+ import { homedir, hostname } from 'node:os';
19
+ import { execSync } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ import { hostMatches } from './host.mjs';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+
27
+ /** Default agent id used when a schedule does not declare one. Backward-compat anchor. */
28
+ export const DEFAULT_AGENT = 'dave';
29
+
30
+ /**
31
+ * Compute the launchd label prefix for an agent. Centralised so install,
32
+ * remove, and list all agree on the naming scheme. Defaults to the
33
+ * pre-existing `com.dave.` behaviour when no agent is given.
34
+ * @param {string|undefined} agent
35
+ * @returns {string}
36
+ */
37
+ export function launchdLabelPrefix(agent) {
38
+ return `com.${agent || DEFAULT_AGENT}.`;
39
+ }
40
+
41
+ /**
42
+ * Resolve the full launchd label for a schedule. Agent-derived: reads
43
+ * `sched.agent` (optional field on the schedule JSON), falling back to
44
+ * DEFAULT_AGENT so existing schedule files with no `agent` field keep
45
+ * producing exactly `com.dave.<id>`, unchanged from pre-feature behaviour.
46
+ * @param {object} sched
47
+ * @returns {string}
48
+ */
49
+ export function resolveLaunchdLabel(sched) {
50
+ return `${launchdLabelPrefix(sched.agent)}${sched.id}`;
51
+ }
52
+
53
+ /**
54
+ * Parse a cron expression into its five fields.
55
+ * @param {string} expr - Cron expression (minute hour dom month dow)
56
+ * @returns {{ minute: string, hour: string, dom: string, month: string, dow: string }}
57
+ */
58
+ function parseCron(expr) {
59
+ const parts = expr.trim().split(/\s+/);
60
+ if (parts.length < 5) {
61
+ throw new Error(`Invalid cron expression (need 5 fields): ${expr}`);
62
+ }
63
+ return {
64
+ minute: parts[0],
65
+ hour: parts[1],
66
+ dom: parts[2],
67
+ month: parts[3],
68
+ dow: parts[4],
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Read and parse a schedule JSON file.
74
+ * @param {string} filePath
75
+ * @returns {object}
76
+ */
77
+ function readSchedule(filePath) {
78
+ const raw = readFileSync(filePath, 'utf8');
79
+ return JSON.parse(raw);
80
+ }
81
+
82
+ /**
83
+ * Compute the resolved DAVE_ROOT for the current install.
84
+ * @returns {string}
85
+ */
86
+ function computeDaveRoot() {
87
+ return process.env.DAVE_ROOT || resolve(__dirname, '..', '..', '..');
88
+ }
89
+
90
+ /**
91
+ * Resolve the DAVE_ROOT variable and working directory.
92
+ * @param {string|undefined} workDir
93
+ * @returns {string}
94
+ */
95
+ function resolveWorkDir(workDir) {
96
+ const daveRoot = computeDaveRoot();
97
+ if (!workDir) return daveRoot;
98
+ return workDir.replace(/\$\{DAVE_ROOT\}/g, daveRoot);
99
+ }
100
+
101
+ /**
102
+ * Expand ${DAVE_ROOT}, ${HOME}, ${USER} inside a string value.
103
+ * Unknown ${...} tokens are left as-is (so users can reference
104
+ * environment variables that exist at execution time on the target backend).
105
+ * @param {string} value
106
+ * @returns {string}
107
+ */
108
+ function substituteVars(value) {
109
+ if (typeof value !== 'string') return value;
110
+ const daveRoot = computeDaveRoot();
111
+ const home = homedir();
112
+ const user = process.env.USER || process.env.USERNAME || '';
113
+ return value
114
+ .replace(/\$\{DAVE_ROOT\}/g, daveRoot)
115
+ .replace(/\$\{HOME\}/g, home)
116
+ .replace(/\$\{USER\}/g, user);
117
+ }
118
+
119
+ /**
120
+ * Resolve the environment map on a schedule, performing variable substitution
121
+ * on all values. Returns an empty object if no environment is set.
122
+ * @param {object} sched
123
+ * @returns {Record<string,string>}
124
+ */
125
+ function resolveEnvironment(sched) {
126
+ const env = sched.environment || {};
127
+ const out = {};
128
+ for (const [k, v] of Object.entries(env)) {
129
+ out[k] = substituteVars(String(v));
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /**
135
+ * Validate a RESOLVED (post-substitution) working directory before it is
136
+ * baked into an installed schedule. Three refusals, all exit-2 (never a
137
+ * silent empty/wrong path):
138
+ *
139
+ * (a) any `${...}` token still present -> an unresolvable variable
140
+ * (unknown template var, or a var that substituted to an empty
141
+ * string leaving a dangling `${}`-shaped remainder is caught by the
142
+ * same regex — "silently empty" never reaches the backend).
143
+ * (b) the resolved path has a `node_modules` path segment -> the
144
+ * versioned-dist-path trap: DAVE_ROOT unset falls back to
145
+ * `resolve(__dirname, '..','..','..')`, which under an INSTALLED
146
+ * package resolves inside `node_modules/.pnpm/@stravica-ai+digital-operator-pa@X.Y.Z/...`
147
+ * — a version-pinned store path that silently breaks on the next
148
+ * upgrade. Proven live (w-2026-07-15-003).
149
+ * (c) the resolved path does not exist, or exists but is not a
150
+ * directory.
151
+ *
152
+ * @param {string} resolved
153
+ * @throws {Error & { exitCode: 2 }}
154
+ */
155
+ export function validateWorkDir(resolved) {
156
+ if (/\$\{[^}]*\}/.test(resolved)) {
157
+ const e = new Error(
158
+ `workingDirectory resolves to "${resolved}", which still contains an unresolved `
159
+ + 'variable — refusing to install with a partially-substituted path. '
160
+ + 'Known vars: ${DAVE_ROOT}, ${HOME}, ${USER}.',
161
+ );
162
+ e.exitCode = 2;
163
+ throw e;
164
+ }
165
+ const segments = resolved.split(/[\\/]/);
166
+ if (segments.includes('node_modules')) {
167
+ const e = new Error(
168
+ `workingDirectory resolves to "${resolved}", which is under a node_modules directory — `
169
+ + 'this is almost always the versioned-dist-path trap (DAVE_ROOT unset, falling back to '
170
+ + 'the installed package\'s own directory, which is version-pinned and breaks on the next '
171
+ + 'upgrade). Set DAVE_ROOT explicitly, or set workingDirectory to the real operator repo root.',
172
+ );
173
+ e.exitCode = 2;
174
+ throw e;
175
+ }
176
+ let stat;
177
+ try {
178
+ stat = statSync(resolved);
179
+ } catch {
180
+ const e = new Error(`workingDirectory "${resolved}" does not exist.`);
181
+ e.exitCode = 2;
182
+ throw e;
183
+ }
184
+ if (!stat.isDirectory()) {
185
+ const e = new Error(`workingDirectory "${resolved}" exists but is not a directory.`);
186
+ e.exitCode = 2;
187
+ throw e;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Host-enforcement decision for `scheduler install`. A schedule with a
193
+ * `host` field declaring a machine other than the current one refuses to
194
+ * install here unless `forceHost` overrides it (still with a warning — the
195
+ * override is for deliberate cross-provisioning, not silent mismatch).
196
+ * A schedule with NO `host` field at all is a warning-only pass — host
197
+ * data backfill onto existing schedule files is estate-side, out of this
198
+ * package's scope.
199
+ *
200
+ * @param {object} sched
201
+ * @param {object} [opts]
202
+ * @param {string} [opts.currentHost] - defaults to the real os.hostname()
203
+ * @param {boolean} [opts.forceHost]
204
+ * @returns {{ ok: boolean, warning?: string, reason?: string }}
205
+ */
206
+ export function evaluateHostGate(sched, { currentHost = hostname(), forceHost = false } = {}) {
207
+ if (!sched.host) {
208
+ return {
209
+ ok: true,
210
+ warning: `schedule "${sched.id}" has no "host" field — installing without host verification `
211
+ + '(host data backfill onto existing schedule files is estate-side).',
212
+ };
213
+ }
214
+ if (hostMatches(sched.host, currentHost)) {
215
+ return { ok: true };
216
+ }
217
+ if (forceHost) {
218
+ return {
219
+ ok: true,
220
+ warning: `schedule "${sched.id}" declares host "${sched.host}" but this machine is `
221
+ + `"${currentHost}" — proceeding anyway due to --force-host.`,
222
+ };
223
+ }
224
+ return {
225
+ ok: false,
226
+ reason: `schedule "${sched.id}" declares host "${sched.host}" but this machine is `
227
+ + `"${currentHost}" — refusing to install here. Pass --force-host to override.`,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Refuse when another schedule file in the same schedules directory
233
+ * resolves to the same task id (and therefore the same backend tag —
234
+ * `dave-<id>` for cron, `com.<agent>.<id>` for launchd, etc.). Two
235
+ * different files sharing an id is exactly the namespace-clobber class
236
+ * (`namespace-clobber-flat-shared-dirs`): whichever installs last silently
237
+ * owns the backend entry, and the other file's association is lost with
238
+ * no error anywhere.
239
+ *
240
+ * Malformed sibling files are skipped (not this tool's job to validate
241
+ * every other file in the directory) rather than aborting the whole
242
+ * install over an unrelated broken file.
243
+ *
244
+ * @param {string} scheduleFile
245
+ * @param {object} sched
246
+ * @param {object} [opts]
247
+ * @param {string} [opts.schedulesDir] - defaults to dirname(scheduleFile)
248
+ * @throws {Error & { exitCode: 2 }}
249
+ */
250
+ export function checkNameCollision(scheduleFile, sched, opts = {}) {
251
+ const schedulesDir = opts.schedulesDir ?? dirname(resolve(scheduleFile));
252
+ const selfAbs = resolve(scheduleFile);
253
+ let entries;
254
+ try {
255
+ entries = readdirSync(schedulesDir);
256
+ } catch {
257
+ return;
258
+ }
259
+ for (const f of entries) {
260
+ if (!f.endsWith('.json')) continue;
261
+ const candidate = join(schedulesDir, f);
262
+ if (resolve(candidate) === selfAbs) continue;
263
+ let other;
264
+ try {
265
+ other = JSON.parse(readFileSync(candidate, 'utf8'));
266
+ } catch {
267
+ continue;
268
+ }
269
+ if (other && other.id === sched.id) {
270
+ const e = new Error(
271
+ `schedule id "${sched.id}" collides with ${candidate} — two different schedule files `
272
+ + `resolve to the same task id/tag ("dave-${sched.id}"). Rename one of the ids before installing.`,
273
+ );
274
+ e.exitCode = 2;
275
+ throw e;
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * XML-escape a string for safe embedding in plist values.
282
+ * @param {string} s
283
+ */
284
+ function xmlEscape(s) {
285
+ return String(s)
286
+ .replace(/&/g, '&amp;')
287
+ .replace(/</g, '&lt;')
288
+ .replace(/>/g, '&gt;')
289
+ .replace(/"/g, '&quot;');
290
+ }
291
+
292
+ // ── Backend: cron ────────────────────────────────────────────────────
293
+
294
+ /**
295
+ * Detect whether an nvm installation exists at the target's
296
+ * `$HOME/.nvm/nvm.sh` — tested AT INSTALL TIME against the real
297
+ * filesystem (not assumed). When present, the generated cron line sources
298
+ * it before running the command, so `pnpm`/`node` resolve exactly the way
299
+ * they do in an interactive nvm-managed shell. Proven necessary
300
+ * (w-2026-07-15-003): cron's own minimal environment has no nvm wrapper,
301
+ * so `pnpm` is otherwise invisible.
302
+ * @param {string} [homeDir]
303
+ * @returns {boolean}
304
+ */
305
+ export function detectNvm(homeDir = homedir()) {
306
+ return existsSync(join(homeDir, '.nvm', 'nvm.sh'));
307
+ }
308
+
309
+ /**
310
+ * Default per-run log path for a schedule's cron entry, matching the
311
+ * real house pattern (`/tmp/dave-knowledge-eval-daily.log` for
312
+ * agent=dave, id=knowledge-eval-daily).
313
+ * @param {object} sched
314
+ * @returns {string}
315
+ */
316
+ export function defaultCronLogPath(sched) {
317
+ return `/tmp/${sched.agent || DEFAULT_AGENT}-${sched.id}.log`;
318
+ }
319
+
320
+ /**
321
+ * Build the INNER shell script a cron entry (or a `scheduler verify` run
322
+ * simulating one) actually executes, plus the `bash -c "<inner>"`
323
+ * wrapper string. Kept separate from the inner content so `verify` can
324
+ * run the exact same inner script via `spawn('bash', ['-c', inner], ...)`
325
+ * — an argv array, never a re-parsed/re-split string — while `install`
326
+ * embeds the wrapper form into the crontab line.
327
+ *
328
+ * Shape (matches the real house pattern, e.g.
329
+ * `bash -c ". $HOME/.nvm/nvm.sh && cd /home/thefoot/dave-v2 && pnpm exec
330
+ * pa knowledge eval --format line >> /tmp/dave-knowledge-eval-daily.log
331
+ * 2>&1" # dave-knowledge-eval-daily`):
332
+ *
333
+ * [. $HOME/.nvm/nvm.sh && ]<export VAR='val'; >*cd <workDir> && <command> >> <logPath> 2>&1
334
+ *
335
+ * Env vars are emitted as `export VAR='val';` statements (not bare
336
+ * `VAR='val' cd ...` prefix-assignment, which in POSIX shell only scopes
337
+ * to the single following simple command — `cd` — and therefore never
338
+ * actually reaches `command` after the `&&`). `export` persists them for
339
+ * the rest of the script, which is what a schedule author setting
340
+ * `environment` actually wants.
341
+ *
342
+ * @param {object} sched
343
+ * @param {string} workDir
344
+ * @param {Record<string,string>} env
345
+ * @param {object} [opts]
346
+ * @param {boolean} [opts.hasNvm]
347
+ * @param {string} [opts.logPath]
348
+ * @returns {{ inner: string, wrapped: string }}
349
+ */
350
+ export function buildCronCommand(sched, workDir, env, opts = {}) {
351
+ const hasNvm = !!opts.hasNvm;
352
+ const logPath = opts.logPath || defaultCronLogPath(sched);
353
+ const preamble = hasNvm ? '. $HOME/.nvm/nvm.sh && ' : '';
354
+ const exportStmts = Object.entries(env)
355
+ .map(([k, v]) => `export ${k}='${String(v).replace(/'/g, `'\\''`)}'; `)
356
+ .join('');
357
+ const inner = `${preamble}${exportStmts}cd ${workDir} && ${sched.command} >> ${logPath} 2>&1`;
358
+ const wrapped = `bash -c "${inner.replace(/"/g, '\\"')}"`;
359
+ return { inner, wrapped };
360
+ }
361
+
362
+ /**
363
+ * Build the full cron line for a schedule (schedule expression + wrapped
364
+ * command + trailing tag comment).
365
+ * @param {object} sched
366
+ * @param {string} workDir
367
+ * @param {Record<string,string>} env
368
+ * @param {object} [opts] - see buildCronCommand
369
+ * @returns {string}
370
+ */
371
+ export function buildCronLine(sched, workDir, env, opts = {}) {
372
+ const tag = `dave-${sched.id}`;
373
+ const { wrapped } = buildCronCommand(sched, workDir, env, opts);
374
+ return `${sched.schedule} ${wrapped} # ${tag}`;
375
+ }
376
+
377
+ function installCron(sched, workDir) {
378
+ const env = resolveEnvironment(sched);
379
+ const tag = `# dave-${sched.id}`;
380
+ const hasNvm = detectNvm();
381
+ const entry = buildCronLine(sched, workDir, env, { hasNvm });
382
+
383
+ let existing = '';
384
+ try {
385
+ existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
386
+ } catch {
387
+ // no crontab yet
388
+ }
389
+
390
+ // Remove any existing entry for this schedule
391
+ const filtered = existing
392
+ .split('\n')
393
+ .filter(line => !line.includes(tag))
394
+ .join('\n')
395
+ .replace(/^\n+|\n+$/g, '');
396
+
397
+ const newCrontab = filtered ? `${filtered}\n${entry}\n` : `${entry}\n`;
398
+ execSync('crontab -', { input: newCrontab, encoding: 'utf8' });
399
+
400
+ console.log(`Installed cron schedule: dave-${sched.id}`);
401
+ console.log(` ${sched.schedule} -- ${sched.description || ''}`);
402
+
403
+ return { tag: `dave-${sched.id}` };
404
+ }
405
+
406
+ // ── Backend: launchd ─────────────────────────────────────────────────
407
+
408
+ /**
409
+ * Merge explicit environment with launchd defaults. Explicit values win.
410
+ * Defaults: PATH (current process PATH), HOME (os.homedir()),
411
+ * DAVE_ROOT (resolved). These guard against launchd's minimal agent
412
+ * environment (/usr/bin:/bin:/usr/sbin:/sbin, no HOME) which silently
413
+ * breaks anything installed via Homebrew.
414
+ * @param {Record<string,string>} explicitEnv
415
+ * @returns {Record<string,string>}
416
+ */
417
+ export function withLaunchdDefaults(explicitEnv) {
418
+ const defaults = {
419
+ PATH: process.env.PATH || '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
420
+ HOME: homedir(),
421
+ DAVE_ROOT: computeDaveRoot(),
422
+ };
423
+ return { ...defaults, ...explicitEnv };
424
+ }
425
+
426
+ /**
427
+ * Build the EnvironmentVariables <dict> block (indented 2 spaces, matching
428
+ * the surrounding plist indentation). Returns an empty string when env is
429
+ * empty so the block is omitted entirely.
430
+ * @param {Record<string,string>} env
431
+ * @returns {string}
432
+ */
433
+ function buildLaunchdEnvBlock(env) {
434
+ const entries = Object.entries(env);
435
+ if (entries.length === 0) return '';
436
+ const inner = entries
437
+ .map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
438
+ .join('\n');
439
+ return ` <key>EnvironmentVariables</key>\n <dict>\n${inner}\n </dict>\n`;
440
+ }
441
+
442
+ /**
443
+ * Expand a single cron field into either "wildcard" (no constraint — the
444
+ * launchd key is omitted, same as historical behaviour) or a sorted,
445
+ * deduped list of concrete integer values. Handles plain values, comma
446
+ * lists, ranges (`a-b`), stepped ranges (`a-b` over `n`), and step-wildcards
447
+ * (star over `n`) — the step-wildcard form is what BUG 4 could not express
448
+ * at all (launchd has no native step syntax; it needs the concrete value set).
449
+ * @param {string} field - Raw cron field text (e.g. a bare star, a stepped star, '3,6,9', '1-5')
450
+ * @param {number} min
451
+ * @param {number} max
452
+ * @returns {{ kind: 'wildcard' } | { kind: 'values', values: number[] }}
453
+ */
454
+ export function expandCronField(field, min, max) {
455
+ if (field === '*') return { kind: 'wildcard' };
456
+
457
+ const values = new Set();
458
+ for (const token of field.split(',')) {
459
+ const stepWildcard = token.match(/^\*\/(\d+)$/);
460
+ const stepRange = token.match(/^(\d+)-(\d+)\/(\d+)$/);
461
+ const plainRange = token.match(/^(\d+)-(\d+)$/);
462
+ const single = token.match(/^(\d+)$/);
463
+
464
+ if (stepWildcard) {
465
+ const step = Number(stepWildcard[1]);
466
+ if (step <= 0) throw new Error(`Invalid cron step (must be > 0): "${token}"`);
467
+ for (let v = min; v <= max; v += step) values.add(v);
468
+ } else if (stepRange) {
469
+ const a = Number(stepRange[1]);
470
+ const b = Number(stepRange[2]);
471
+ const step = Number(stepRange[3]);
472
+ if (step <= 0) throw new Error(`Invalid cron step (must be > 0): "${token}"`);
473
+ for (let v = a; v <= b; v += step) values.add(v);
474
+ } else if (plainRange) {
475
+ const a = Number(plainRange[1]);
476
+ const b = Number(plainRange[2]);
477
+ for (let v = a; v <= b; v++) values.add(v);
478
+ } else if (single) {
479
+ values.add(Number(single[1]));
480
+ } else {
481
+ throw new Error(`Unsupported cron field token for launchd translation: "${token}"`);
482
+ }
483
+ }
484
+
485
+ return { kind: 'values', values: [...values].sort((a, b) => a - b) };
486
+ }
487
+
488
+ const CRON_FIELD_SPECS = [
489
+ { key: 'Minute', field: 'minute', min: 0, max: 59 },
490
+ { key: 'Hour', field: 'hour', min: 0, max: 23 },
491
+ { key: 'Day', field: 'dom', min: 1, max: 31 },
492
+ { key: 'Month', field: 'month', min: 1, max: 12 },
493
+ { key: 'Weekday', field: 'dow', min: 0, max: 7 },
494
+ ];
495
+
496
+ // Sanity cap on the StartCalendarInterval array launchd will accept. A
497
+ // pathological step (e.g. every minute across an unconstrained day/month)
498
+ // would otherwise silently generate thousands of dict entries.
499
+ const MAX_CALENDAR_COMBINATIONS = 366;
500
+
501
+ /**
502
+ * Build the StartCalendarInterval block (BUG 4 fix). launchd has no cron
503
+ * step syntax — a stepped field (or a list/range) must be expressed as
504
+ * multiple concrete StartCalendarInterval dicts, one per combination of
505
+ * the non-wildcard fields' expanded values (cartesian product), inside an
506
+ * <array>. Schedules where every field is a plain single value or a
507
+ * wildcard collapse to exactly the old single <dict> shape, so existing
508
+ * plain-cron installs are byte-for-byte unaffected.
509
+ * @param {object} cron
510
+ * @returns {string}
511
+ */
512
+ export function buildStartCalendarIntervalBlock(cron) {
513
+ const expanded = CRON_FIELD_SPECS
514
+ .map((spec) => ({ key: spec.key, expansion: expandCronField(cron[spec.field], spec.min, spec.max) }))
515
+ .filter((e) => e.expansion.kind !== 'wildcard');
516
+
517
+ let combos = [[]];
518
+ for (const { key, expansion } of expanded) {
519
+ const next = [];
520
+ for (const combo of combos) {
521
+ for (const value of expansion.values) {
522
+ next.push([...combo, { key, value }]);
523
+ }
524
+ }
525
+ combos = next;
526
+ if (combos.length > MAX_CALENDAR_COMBINATIONS) {
527
+ throw new Error(
528
+ `Cron expression expands to more than ${MAX_CALENDAR_COMBINATIONS} launchd StartCalendarInterval entries — too fine-grained to express as a step schedule on launchd. Narrow the schedule or use the cron/systemd backend instead.`
529
+ );
530
+ }
531
+ }
532
+
533
+ const dictFor = (combo) => {
534
+ const inner = combo.map(({ key, value }) => ` <key>${key}</key>\n <integer>${value}</integer>`).join('\n');
535
+ return ` <dict>\n${inner}\n </dict>`;
536
+ };
537
+
538
+ if (combos.length <= 1) {
539
+ const combo = combos[0] || [];
540
+ const inner = combo.map(({ key, value }) => ` <key>${key}</key>\n <integer>${value}</integer>`).join('\n');
541
+ return ` <key>StartCalendarInterval</key>\n <dict>\n${inner}\n </dict>\n`;
542
+ }
543
+
544
+ const dicts = combos.map(dictFor).join('\n');
545
+ return ` <key>StartCalendarInterval</key>\n <array>\n${dicts}\n </array>\n`;
546
+ }
547
+
548
+ /**
549
+ * Generate the full plist content for a schedule. Pure function — safe to
550
+ * call in tests. Always emits an EnvironmentVariables block on launchd
551
+ * (auto-injected defaults when the schedule has no explicit environment).
552
+ * The launchd Label is agent-derived (`com.<sched.agent||'dave'>.<id>`) —
553
+ * see resolveLaunchdLabel.
554
+ * @param {object} sched
555
+ * @param {string} workDir
556
+ * @param {object} cron
557
+ * @returns {string}
558
+ */
559
+ export function buildLaunchdPlist(sched, workDir, cron) {
560
+ const label = resolveLaunchdLabel(sched);
561
+ const calendarBlock = buildStartCalendarIntervalBlock(cron);
562
+
563
+ const cmdParts = sched.command.split(/\s+/);
564
+ const programArgs = cmdParts.map(p => ` <string>${xmlEscape(p)}</string>`).join('\n');
565
+
566
+ const mergedEnv = withLaunchdDefaults(resolveEnvironment(sched));
567
+ const envBlock = buildLaunchdEnvBlock(mergedEnv);
568
+
569
+ return `<?xml version="1.0" encoding="UTF-8"?>
570
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
571
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
572
+ <plist version="1.0">
573
+ <dict>
574
+ <key>Label</key>
575
+ <string>${label}</string>
576
+ <key>ProgramArguments</key>
577
+ <array>
578
+ ${programArgs}
579
+ </array>
580
+ <key>WorkingDirectory</key>
581
+ <string>${xmlEscape(workDir)}</string>
582
+ ${envBlock}${calendarBlock} <key>StandardOutPath</key>
583
+ <string>/tmp/dave-${sched.id}-stdout.log</string>
584
+ <key>StandardErrorPath</key>
585
+ <string>/tmp/dave-${sched.id}-stderr.log</string>
586
+ <key>RunAtLoad</key>
587
+ <false/>
588
+ </dict>
589
+ </plist>
590
+ `;
591
+ }
592
+
593
+ function installLaunchd(sched, workDir, cron) {
594
+ const label = resolveLaunchdLabel(sched);
595
+ const plistDir = join(homedir(), 'Library', 'LaunchAgents');
596
+ const plistPath = join(plistDir, `${label}.plist`);
597
+
598
+ mkdirSync(plistDir, { recursive: true });
599
+ const plist = buildLaunchdPlist(sched, workDir, cron);
600
+ writeFileSync(plistPath, plist, 'utf8');
601
+
602
+ // Unload if already loaded, then load
603
+ const uid = execSync('id -u', { encoding: 'utf8' }).trim();
604
+ try { execSync(`launchctl bootout gui/${uid}/${label}`, { stdio: 'pipe' }); } catch { /* not loaded */ }
605
+ execSync(`launchctl bootstrap gui/${uid} ${plistPath}`);
606
+
607
+ console.log(`Installed launchd schedule: ${label}`);
608
+ console.log(` Plist: ${plistPath}`);
609
+ console.log(` ${sched.schedule} -- ${sched.description || ''}`);
610
+
611
+ return { label, plistPath };
612
+ }
613
+
614
+ // ── Backend: systemd ─────────────────────────────────────────────────
615
+
616
+ /**
617
+ * Build the .service unit contents.
618
+ * @param {object} sched
619
+ * @param {string} workDir
620
+ * @param {Record<string,string>} env
621
+ * @returns {string}
622
+ */
623
+ export function buildSystemdService(sched, workDir, env) {
624
+ const description = sched.description || `Dave scheduled task: ${sched.id}`;
625
+ const envLines = Object.entries(env)
626
+ .map(([k, v]) => `Environment=${k}=${v}`)
627
+ .join('\n');
628
+ const envBlock = envLines ? `${envLines}\n` : '';
629
+ return `[Unit]
630
+ Description=${description}
631
+
632
+ [Service]
633
+ Type=oneshot
634
+ WorkingDirectory=${workDir}
635
+ ${envBlock}ExecStart=/usr/bin/env bash -c '${sched.command.replace(/'/g, `'\\''`)}'
636
+
637
+ [Install]
638
+ WantedBy=default.target
639
+ `;
640
+ }
641
+
642
+ /**
643
+ * Build the .timer unit contents.
644
+ * @param {object} sched
645
+ * @param {object} cron
646
+ * @returns {string}
647
+ */
648
+ export function buildSystemdTimer(sched, cron) {
649
+ const description = sched.description || `Dave scheduled task: ${sched.id}`;
650
+ const dowNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
651
+ const dowPart = cron.dow !== '*' ? (dowNames[Number(cron.dow)] || '*') : '';
652
+ let onCalendar = `*-${cron.month}-${cron.dom} ${cron.hour}:${cron.minute}:00`;
653
+ if (dowPart) onCalendar = `${dowPart} ${onCalendar}`;
654
+ return `[Unit]
655
+ Description=Timer for ${description}
656
+
657
+ [Timer]
658
+ OnCalendar=${onCalendar}
659
+ Persistent=true
660
+
661
+ [Install]
662
+ WantedBy=timers.target
663
+ `;
664
+ }
665
+
666
+ function installSystemd(sched, workDir, cron) {
667
+ const unitName = `dave-${sched.id}`;
668
+ const unitDir = join(homedir(), '.config', 'systemd', 'user');
669
+ mkdirSync(unitDir, { recursive: true });
670
+
671
+ const env = resolveEnvironment(sched);
672
+ const service = buildSystemdService(sched, workDir, env);
673
+ const timer = buildSystemdTimer(sched, cron);
674
+ writeFileSync(join(unitDir, `${unitName}.service`), service, 'utf8');
675
+ writeFileSync(join(unitDir, `${unitName}.timer`), timer, 'utf8');
676
+
677
+ execSync('systemctl --user daemon-reload');
678
+ execSync(`systemctl --user enable --now ${unitName}.timer`);
679
+
680
+ const description = sched.description || `Dave scheduled task: ${sched.id}`;
681
+ console.log(`Installed systemd schedule: ${unitName}`);
682
+ console.log(` Service: ${join(unitDir, unitName + '.service')}`);
683
+ console.log(` Timer: ${join(unitDir, unitName + '.timer')}`);
684
+ console.log(` ${sched.schedule} -- ${description}`);
685
+
686
+ return { unitName };
687
+ }
688
+
689
+ // ── Backend: schtasks (Windows) ──────────────────────────────────────
690
+
691
+ /**
692
+ * Build the cmd.exe wrapper string used as the /TR argument to schtasks.
693
+ *
694
+ * schtasks has no native env-var support, so env vars are emitted as
695
+ * `set VAR=val &&` pairs before `cd /d workDir && command`. Callers
696
+ * that need stable cross-process env should instead write a .cmd wrapper
697
+ * to disk and point schtasks at it — we keep the inline form for
698
+ * parity with the other backends on simple schedules.
699
+ * @param {object} sched
700
+ * @param {string} workDir
701
+ * @param {Record<string,string>} env
702
+ * @returns {string}
703
+ */
704
+ export function buildSchtasksCommand(sched, workDir, env) {
705
+ const envPrefix = Object.entries(env)
706
+ .map(([k, v]) => `set "${k}=${String(v).replace(/"/g, '""')}" &&`)
707
+ .join(' ');
708
+ const inner = envPrefix
709
+ ? `${envPrefix} cd /d ${workDir} && ${sched.command}`
710
+ : `cd /d ${workDir} && ${sched.command}`;
711
+ return `cmd /c "${inner}"`;
712
+ }
713
+
714
+ function installSchtasks(sched, workDir, cron) {
715
+ const taskName = `dave-${sched.id}`;
716
+ const env = resolveEnvironment(sched);
717
+
718
+ const fmtHour = cron.hour === '*' ? '00' : cron.hour.padStart(2, '0');
719
+ const fmtMin = cron.minute === '*' ? '00' : cron.minute.padStart(2, '0');
720
+ const startTime = `${fmtHour}:${fmtMin}`;
721
+
722
+ let scType = 'DAILY';
723
+ let scModifier = '';
724
+ const dowNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
725
+
726
+ if (cron.dom === '*' && cron.month === '*' && cron.dow === '*') {
727
+ scType = 'DAILY';
728
+ } else if (cron.dow !== '*') {
729
+ scType = 'WEEKLY';
730
+ scModifier = `/D ${dowNames[Number(cron.dow)] || 'MON'}`;
731
+ } else if (cron.dom !== '*') {
732
+ scType = 'MONTHLY';
733
+ scModifier = `/D ${cron.dom}`;
734
+ }
735
+
736
+ try {
737
+ execSync(`schtasks.exe /Delete /TN "${taskName}" /F`, { stdio: 'pipe' });
738
+ } catch { /* not found */ }
739
+
740
+ const tr = buildSchtasksCommand(sched, workDir, env);
741
+ let cmd = `schtasks.exe /Create /TN "${taskName}" /TR "${tr}" /SC ${scType} /ST ${startTime}`;
742
+ if (scModifier) cmd += ` ${scModifier}`;
743
+
744
+ execSync(cmd);
745
+
746
+ console.log(`Installed schtasks schedule: ${taskName}`);
747
+ console.log(` ${sched.schedule} -- ${sched.description || ''}`);
748
+
749
+ return { taskName };
750
+ }
751
+
752
+ // ── Main ─────────────────────────────────────────────────────────────
753
+
754
+ /**
755
+ * Install a schedule definition using the specified backend.
756
+ *
757
+ * BUG 2 fix: this used to fall off the end of the function with no return
758
+ * statement, so every caller checking a result's `.ok` field (the CLI
759
+ * wrapper included) was reading `undefined.ok`. Every path now returns a
760
+ * result object; callers can rely on `.ok` being present.
761
+ * @param {string} scheduleFile - Path to the schedule JSON file
762
+ * @param {string} backend - Backend name (cron, launchd, systemd, schtasks)
763
+ * @param {object} [opts]
764
+ * @param {boolean} [opts.forceHost] - override a foreign-host refusal (with a warning)
765
+ * @param {string} [opts.schedulesDir] - collision-check dir; defaults to dirname(scheduleFile)
766
+ * @param {string} [opts.currentHost] - test-only override for the host-gate check
767
+ * @returns {{ ok: boolean, id: string, backend: string, skipped?: boolean, reason?: string, warnings?: string[] }}
768
+ */
769
+ export function install(scheduleFile, backend, opts = {}) {
770
+ const sched = readSchedule(scheduleFile);
771
+
772
+ if (!sched.id || !sched.schedule || !sched.command) {
773
+ throw new Error('Schedule file must have id, schedule, and command fields');
774
+ }
775
+
776
+ const warnings = [];
777
+
778
+ // Name-collision check — refuse before touching any backend if another
779
+ // schedule file in the same directory resolves to the same task id.
780
+ checkNameCollision(scheduleFile, sched, { schedulesDir: opts.schedulesDir });
781
+
782
+ // Host-enforcement gate — refuse (exit 2) a schedule declaring a foreign
783
+ // host unless --force-host overrides it. Missing `host` field is a
784
+ // warning, not a refusal (data backfill is estate-side).
785
+ const hostGate = evaluateHostGate(sched, { currentHost: opts.currentHost, forceHost: opts.forceHost });
786
+ if (!hostGate.ok) {
787
+ const e = new Error(hostGate.reason);
788
+ e.exitCode = 2;
789
+ throw e;
790
+ }
791
+ if (hostGate.warning) {
792
+ console.warn(`Warning: ${hostGate.warning}`);
793
+ warnings.push(hostGate.warning);
794
+ }
795
+
796
+ if (sched.enabled === false) {
797
+ console.log(`Schedule '${sched.id}' is disabled. Set enabled=true to install.`);
798
+ return { ok: true, id: sched.id, backend, skipped: true, reason: 'disabled', ...(warnings.length ? { warnings } : {}) };
799
+ }
800
+
801
+ const workDir = resolveWorkDir(sched.workingDirectory);
802
+ validateWorkDir(workDir);
803
+ const cron = parseCron(sched.schedule);
804
+
805
+ let detail;
806
+ switch (backend) {
807
+ case 'cron': detail = installCron(sched, workDir); break;
808
+ case 'launchd': detail = installLaunchd(sched, workDir, cron); break;
809
+ case 'systemd': detail = installSystemd(sched, workDir, cron); break;
810
+ case 'schtasks': detail = installSchtasks(sched, workDir, cron); break;
811
+ default:
812
+ throw new Error(`Unknown backend: ${backend}. Supported: cron, launchd, systemd, schtasks`);
813
+ }
814
+
815
+ return { ok: true, id: sched.id, backend, ...detail, ...(warnings.length ? { warnings } : {}) };
816
+ }
817
+
818
+ // Exported for tests — pure helpers.
819
+ export { parseCron, resolveWorkDir, resolveEnvironment, substituteVars };
820
+
821
+ // ── CLI entry point ──────────────────────────────────────────────────
822
+ //
823
+ // BUG 3 fix: the previous check compared the realpath-resolved
824
+ // import.meta.url against the raw (possibly symlinked) process.argv[1] via
825
+ // String#endsWith. Under pnpm, a direct invocation's argv[1] is frequently
826
+ // a symlink (workspace link or .pnpm store link) while import.meta.url is
827
+ // always the fully-resolved real path, so the comparison silently failed
828
+ // and this entrypoint became a no-op that still exited 0. Fix: realpath
829
+ // BOTH sides before comparing (matches the pattern already proven correct
830
+ // in packages/workspace/src/server.mjs and install-daemon.mjs).
831
+
832
+ function isDirectlyInvoked() {
833
+ if (!process.argv[1]) return false;
834
+ try {
835
+ return fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]));
836
+ } catch {
837
+ return false;
838
+ }
839
+ }
840
+
841
+ if (isDirectlyInvoked()) {
842
+ const args = process.argv.slice(2);
843
+ if (args.length < 2) {
844
+ console.error('Usage: node install.mjs <schedule.json> <backend>');
845
+ console.error('Backends: cron, launchd, systemd, schtasks');
846
+ process.exit(1);
847
+ }
848
+ try {
849
+ install(args[0], args[1]);
850
+ } catch (err) {
851
+ console.error(`Error: ${err.message}`);
852
+ process.exit(1);
853
+ }
854
+ }