@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,710 @@
1
+ // graph.mjs — knowledge-graph loader + BFS primitives for `pa knowledge`.
2
+ //
3
+ // The operator's knowledge graph lives at
4
+ // `<operator-root>/operator/knowledge/.graph.json`. Its shape (as of 2026-07):
5
+ //
6
+ // {
7
+ // version: '1.0',
8
+ // generatedAt: '<ISO timestamp>',
9
+ // nodes: [ { id: '<slug>', title: '<display>', domain: '<name>' | null } ],
10
+ // edges: [ { from: '<slug>', to: '<slug>', type: '<name>' } ]
11
+ // }
12
+ //
13
+ // Edge types currently observed: `link`, `backlink`. Design assumption:
14
+ // more types may appear (e.g. `supersedes`, `contradicts`); output must
15
+ // carry the type so a caller can filter downstream. We do NOT filter by
16
+ // type at this layer — that is a future flag if the need arises.
17
+ //
18
+ // Slug matching. The graph's `id` field is a path-like slug — sometimes
19
+ // a bare filename (`principles.md`), sometimes a repo-relative path
20
+ // (`operator/knowledge/clients/clive/accounts.md`). `resolveSlug` accepts
21
+ // either an exact match, a suffix match on the id, or a match against
22
+ // the title (case-insensitive). Ambiguous matches (>1 hit) throw with
23
+ // the candidate list — the caller then either accepts (search) or
24
+ // bails (related, path).
25
+
26
+ import { readFile } from 'node:fs/promises';
27
+ import { existsSync } from 'node:fs';
28
+ import { join, resolve, dirname } from 'node:path';
29
+
30
+ const GRAPH_RELATIVE_PATH = 'operator/knowledge/.graph.json';
31
+
32
+ // --- loader ---------------------------------------------------------------
33
+
34
+ export async function loadGraph({ repoRoot, graphPath } = {}) {
35
+ const path = graphPath ?? join(repoRoot, GRAPH_RELATIVE_PATH);
36
+ if (!existsSync(path)) {
37
+ const err = new Error(`knowledge graph not found at ${path}`);
38
+ err.exitCode = 3;
39
+ throw err;
40
+ }
41
+ let raw;
42
+ try {
43
+ raw = await readFile(path, 'utf8');
44
+ } catch (e) {
45
+ const err = new Error(`could not read knowledge graph at ${path}: ${e.message}`);
46
+ err.exitCode = 3;
47
+ throw err;
48
+ }
49
+ let json;
50
+ try {
51
+ json = JSON.parse(raw);
52
+ } catch (e) {
53
+ const err = new Error(`could not parse knowledge graph at ${path}: ${e.message}`);
54
+ err.exitCode = 3;
55
+ throw err;
56
+ }
57
+ if (!Array.isArray(json.nodes) || !Array.isArray(json.edges)) {
58
+ const err = new Error(`knowledge graph at ${path} is missing nodes[] or edges[]`);
59
+ err.exitCode = 3;
60
+ throw err;
61
+ }
62
+ return json;
63
+ }
64
+
65
+ // --- adjacency ------------------------------------------------------------
66
+
67
+ // Build outbound + inbound adjacency maps keyed by node id. Each entry
68
+ // carries the destination id and the edge type — the caller decides
69
+ // whether to filter or annotate.
70
+ export function buildAdjacency(graph) {
71
+ const out = new Map();
72
+ const in_ = new Map();
73
+ for (const node of graph.nodes) {
74
+ out.set(node.id, []);
75
+ in_.set(node.id, []);
76
+ }
77
+ for (const edge of graph.edges) {
78
+ // Silently skip edges to/from unknown nodes rather than reject the
79
+ // whole graph — the generator may have observed a dangling link.
80
+ if (!out.has(edge.from) || !in_.has(edge.to)) continue;
81
+ out.get(edge.from).push({ id: edge.to, type: edge.type });
82
+ in_.get(edge.to).push({ id: edge.from, type: edge.type });
83
+ }
84
+ return { out, in: in_ };
85
+ }
86
+
87
+ // --- slug resolution ------------------------------------------------------
88
+
89
+ // Resolve a user-supplied slug against the graph. Match order:
90
+ // 1. Exact id match.
91
+ // 2. Path-suffix match on id (`accounts.md` matches
92
+ // `operator/knowledge/clients/clive/accounts.md`).
93
+ // 3. Title match (case-insensitive, substring).
94
+ //
95
+ // Returns { matches: [nodeId, ...] }. The caller decides what to do
96
+ // with 0 / 1 / N results.
97
+ export function resolveSlug(graph, slug) {
98
+ const q = slug.toLowerCase();
99
+ const byId = new Map();
100
+ const bySuffix = [];
101
+ const byTitle = [];
102
+ for (const node of graph.nodes) {
103
+ if (node.id === slug) return { matches: [node.id] };
104
+ byId.set(node.id.toLowerCase(), node);
105
+ if (node.id.toLowerCase().endsWith('/' + q) || node.id.toLowerCase() === q) {
106
+ bySuffix.push(node.id);
107
+ }
108
+ if (node.title && node.title.toLowerCase().includes(q)) {
109
+ byTitle.push(node.id);
110
+ }
111
+ }
112
+ // Case-insensitive exact match on id?
113
+ if (byId.has(q)) return { matches: [byId.get(q).id] };
114
+ if (bySuffix.length > 0) return { matches: dedupe(bySuffix) };
115
+ if (byTitle.length > 0) return { matches: dedupe(byTitle) };
116
+ return { matches: [] };
117
+ }
118
+
119
+ function dedupe(xs) {
120
+ return Array.from(new Set(xs));
121
+ }
122
+
123
+ // --- BFS ------------------------------------------------------------------
124
+
125
+ // Shortest path between two node ids. Returns an array of `{ id, type }`
126
+ // entries where `type` is the edge type used to reach that node (null for
127
+ // the starting node). Direction: undirected — the graph is treated as
128
+ // symmetric for path-finding, so a `path a b` and `path b a` produce
129
+ // mirror-image results. Returns null if no path.
130
+ export function shortestPath(graph, from, to) {
131
+ if (from === to) return [{ id: from, type: null }];
132
+ const adjacency = buildUndirected(graph);
133
+ const visited = new Set([from]);
134
+ const parents = new Map();
135
+ const queue = [from];
136
+ while (queue.length > 0) {
137
+ const current = queue.shift();
138
+ const neighbours = adjacency.get(current) || [];
139
+ for (const { id: next, type } of neighbours) {
140
+ if (visited.has(next)) continue;
141
+ visited.add(next);
142
+ parents.set(next, { from: current, type });
143
+ if (next === to) return reconstructPath(from, to, parents);
144
+ queue.push(next);
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function buildUndirected(graph) {
151
+ const adj = new Map();
152
+ for (const node of graph.nodes) adj.set(node.id, []);
153
+ for (const edge of graph.edges) {
154
+ if (!adj.has(edge.from) || !adj.has(edge.to)) continue;
155
+ adj.get(edge.from).push({ id: edge.to, type: edge.type });
156
+ adj.get(edge.to).push({ id: edge.from, type: edge.type });
157
+ }
158
+ return adj;
159
+ }
160
+
161
+ function reconstructPath(from, to, parents) {
162
+ const out = [{ id: to, type: parents.get(to).type }];
163
+ let cursor = to;
164
+ while (cursor !== from) {
165
+ const p = parents.get(cursor);
166
+ if (!p) return null; // shouldn't happen
167
+ if (p.from === from) {
168
+ out.unshift({ id: from, type: null });
169
+ return out;
170
+ }
171
+ out.unshift({ id: p.from, type: parents.get(p.from).type });
172
+ cursor = p.from;
173
+ }
174
+ return out;
175
+ }
176
+
177
+ // --- verb: related --------------------------------------------------------
178
+
179
+ // 1-hop neighbourhood. `direction` is 'both' (default) | 'out' | 'in'.
180
+ // Returns { anchor: nodeId, neighbours: [{id, title, domain, type, direction}] }.
181
+ export function related(graph, slug, { direction = 'both' } = {}) {
182
+ const resolved = resolveSlug(graph, slug);
183
+ if (resolved.matches.length === 0) {
184
+ const err = new Error(`unknown slug: ${slug}`);
185
+ err.exitCode = 2;
186
+ throw err;
187
+ }
188
+ if (resolved.matches.length > 1) {
189
+ const err = new Error(
190
+ `ambiguous slug: ${slug} — matches ${resolved.matches.length} nodes:\n ${resolved.matches.slice(0, 10).join('\n ')}` +
191
+ (resolved.matches.length > 10 ? `\n ...` : ''),
192
+ );
193
+ err.exitCode = 2;
194
+ throw err;
195
+ }
196
+ const anchor = resolved.matches[0];
197
+ const adj = buildAdjacency(graph);
198
+ const byId = nodeIndex(graph);
199
+ const results = [];
200
+ if (direction === 'both' || direction === 'out') {
201
+ for (const { id, type } of adj.out.get(anchor) || []) {
202
+ results.push(annotate(byId, id, type, 'out'));
203
+ }
204
+ }
205
+ if (direction === 'both' || direction === 'in') {
206
+ for (const { id, type } of adj.in.get(anchor) || []) {
207
+ results.push(annotate(byId, id, type, 'in'));
208
+ }
209
+ }
210
+ return { anchor, neighbours: results };
211
+ }
212
+
213
+ // --- verb: search ---------------------------------------------------------
214
+
215
+ // Title-only search by default. `full` flag toggles doc-content scan (which
216
+ // requires `repoRoot` so we can read the file).
217
+ //
218
+ // Matching: the query is tokenised on whitespace. A node matches when
219
+ // EVERY token matches (substring, case-insensitive) against its id or
220
+ // title — so `search "barry voice"` finds a doc titled "Barry voice guide"
221
+ // AND a doc whose fields carry the two tokens separately (e.g. id
222
+ // `.../writing/barry-voice.md` split by the hyphen). The historical
223
+ // whole-query substring behaviour is preserved: single-token queries are
224
+ // unchanged, and an exact-phrase substring hit scores the same as it
225
+ // always did.
226
+ //
227
+ // Ranking: exact title/id match (score 3) > whole-query substring or
228
+ // all-tokens hit on title/id (score 2) > body hit (score 1, `--full`
229
+ // only — whole-query substring in the body, or every token found across
230
+ // id/title/body). Tie-break within a score band: (1) higher title-token
231
+ // count — the number of query tokens matched in id/title (added post-#60
232
+ // to fix the "title-heavy doc buried under body-heavy doc" miss); then
233
+ // (2) higher edge count; then (3) id.
234
+ //
235
+ // Naming: this function is the STRICT AND-match. `searchCascade()` below
236
+ // wraps it in the fallback ladder used by the default `pa knowledge search`
237
+ // (Deliverable A, w-2026-07-06-020). `--strict` on the CLI dispatches
238
+ // straight here for scripted callers that depend on the current behaviour.
239
+ export async function search(graph, term, { full = false, repoRoot = null } = {}) {
240
+ const q = term.toLowerCase();
241
+ const tokens = q.split(/\s+/).filter(Boolean);
242
+ const multiToken = tokens.length > 1;
243
+ // Content tokens (non-stopword) — used ONLY for the titleTokens tie-break.
244
+ // The AND-match itself still requires every original token to hit, so
245
+ // this can't change which docs qualify; it only re-orders within a
246
+ // score band. See file-level cascade comment for why.
247
+ const contentTokens = tokens.filter((t) => !STOPWORDS.has(t));
248
+ const adj = buildAdjacency(graph);
249
+ const hits = [];
250
+ for (const node of graph.nodes) {
251
+ const title = (node.title || '').toLowerCase();
252
+ const id = node.id.toLowerCase();
253
+ const tokenInFields = (t) => title.includes(t) || id.includes(t);
254
+ // Count content-token overlap with id/title — load-bearing for the
255
+ // body-tier tie-break so a doc whose TITLE is what the caller asked
256
+ // about outranks one that only happens to mention every token
257
+ // somewhere in a rambling body. Structural stopwords ("to" is a
258
+ // substring of "operator") are excluded so they can't set the ranking.
259
+ const titleTokens = contentTokens.reduce((n, t) => n + (tokenInFields(t) ? 1 : 0), 0);
260
+ let score = 0;
261
+ if (title === q || id === q) score = 3;
262
+ else if (title.includes(q) || id.includes(q)) score = 2;
263
+ else if (multiToken && tokens.every(tokenInFields)) score = 2;
264
+ if (score > 0) {
265
+ hits.push({
266
+ node,
267
+ score,
268
+ titleTokens,
269
+ edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
270
+ });
271
+ continue;
272
+ }
273
+ if (full && repoRoot) {
274
+ const body = await readBody(repoRoot, node.id);
275
+ if (body != null) {
276
+ const hay = body.toLowerCase();
277
+ const bodyHit = hay.includes(q) ||
278
+ (multiToken && tokens.every((t) => tokenInFields(t) || hay.includes(t)));
279
+ if (bodyHit) {
280
+ hits.push({
281
+ node,
282
+ score: 1,
283
+ titleTokens,
284
+ edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
285
+ });
286
+ }
287
+ }
288
+ }
289
+ }
290
+ hits.sort((a, b) =>
291
+ (b.score - a.score) ||
292
+ (b.titleTokens - a.titleTokens) ||
293
+ (b.edgeCount - a.edgeCount) ||
294
+ a.node.id.localeCompare(b.node.id),
295
+ );
296
+ return hits.map((h) => ({
297
+ id: h.node.id,
298
+ title: h.node.title,
299
+ domain: h.node.domain,
300
+ score: h.score,
301
+ titleTokens: h.titleTokens,
302
+ edgeCount: h.edgeCount,
303
+ }));
304
+ }
305
+
306
+ // OR-match variant. Multi-token queries hit any node where AT LEAST ONE
307
+ // token matches (id/title, plus body when `full`). Ranked primarily by how
308
+ // many CONTENT tokens (non-stopword) land in id/title — a title-heavy
309
+ // 2-of-4 content-token doc outranks a body-heavy 4-of-4 doc, because the
310
+ // title says "this doc is about X" while the body only says "this doc
311
+ // mentions X". The stopword exclusion for the title-count is load-bearing:
312
+ // otherwise every doc's id contains "to" (via "operator") and the
313
+ // title-boost degenerates into "all docs matched 1 title token" — which
314
+ // puts research docs with query-echoing filenames unfairly at the top of
315
+ // the noisy "what to X versus Y" cases. Secondary sort: total tokens
316
+ // matched (all tokens, including body); then edge count; then id.
317
+ //
318
+ // Zero-token overlap is dropped. Returns rows carrying `tokensMatched`
319
+ // (title/id/body total, back-compat name), `tokensTotal`, plus
320
+ // `titleTokensMatched` (content-only) for the eval and cascade to inspect.
321
+ //
322
+ // Single-token queries collapse to a substring hit over id/title/body,
323
+ // giving the caller a graceful fallback when `search()` (AND) turned up
324
+ // empty but a related term appears in a body somewhere.
325
+ export async function searchOr(graph, term, { full = false, repoRoot = null } = {}) {
326
+ const q = term.toLowerCase();
327
+ const tokens = q.split(/\s+/).filter(Boolean);
328
+ const tokensTotal = tokens.length;
329
+ if (tokensTotal === 0) return [];
330
+ // Content tokens (non-stopword) drive the title-boost — see fn header.
331
+ // A caller can still hit on stopword tokens (via `matched`), but a
332
+ // stopword substring in a doc id shouldn't confer title-relevance.
333
+ const contentTokens = tokens.filter((t) => !STOPWORDS.has(t));
334
+ const adj = buildAdjacency(graph);
335
+ const hits = [];
336
+ for (const node of graph.nodes) {
337
+ const title = (node.title || '').toLowerCase();
338
+ const id = node.id.toLowerCase();
339
+ let body = null;
340
+ if (full && repoRoot) body = (await readBody(repoRoot, node.id))?.toLowerCase() ?? null;
341
+ let titleMatched = 0;
342
+ for (const t of contentTokens) {
343
+ if (title.includes(t) || id.includes(t)) titleMatched += 1;
344
+ }
345
+ let matched = 0;
346
+ for (const t of tokens) {
347
+ const inTitleOrId = title.includes(t) || id.includes(t);
348
+ if (inTitleOrId || (body != null && body.includes(t))) matched += 1;
349
+ }
350
+ if (matched > 0) {
351
+ hits.push({
352
+ node,
353
+ titleMatched,
354
+ matched,
355
+ edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
356
+ });
357
+ }
358
+ }
359
+ hits.sort((a, b) =>
360
+ (b.titleMatched - a.titleMatched) ||
361
+ (b.matched - a.matched) ||
362
+ (b.edgeCount - a.edgeCount) ||
363
+ a.node.id.localeCompare(b.node.id),
364
+ );
365
+ return hits.map((h) => ({
366
+ id: h.node.id,
367
+ title: h.node.title,
368
+ domain: h.node.domain,
369
+ score: 0,
370
+ tokensMatched: h.matched,
371
+ titleTokensMatched: h.titleMatched,
372
+ tokensTotal,
373
+ edgeCount: h.edgeCount,
374
+ }));
375
+ }
376
+
377
+ // Zero-result fallback cascade for `pa knowledge search` (Deliverable A,
378
+ // w-2026-07-06-020; stopword + title-boost pass w-2026-07-08-008).
379
+ //
380
+ // The original audit failure mode was silent misses: a caller types a
381
+ // natural phrasing, gets `(no matches)`, and reads that as an authoritative
382
+ // "not documented." First-live-eval (2026-07-08) surfaced the sibling
383
+ // failure mode: stopword dilution. Queries like "spec and review workflow"
384
+ // or "when does concurrent work need a project" AND-match nothing on title
385
+ // (grammatical filler drags the requirement), then the body-tier ranker
386
+ // (all body-hits score 1, tie-break edge count) pulls body-heavy random
387
+ // docs above the title-heavy target. Tiers:
388
+ //
389
+ // 1. mode:'title' — AND match on id/title with the caller's tokens.
390
+ // 1b (still 'title') — same, with English stopwords stripped. Fires only
391
+ // if stripping actually removed a token. Monotonic:
392
+ // fewer required tokens → strict superset of hits.
393
+ // 2. mode:'body' — AND match retried against the body (`--full`).
394
+ // Considered against tier 3 (below) before returning.
395
+ // 3. mode:'partial' — OR match ranked title-tokens-first (see searchOr).
396
+ // PROMOTED past tier 2 when its top result has more
397
+ // title-token overlap than tier 2's — the "title
398
+ // says what the doc is about" heuristic.
399
+ // 4. mode:'miss' — genuine miss. Envelope carries a `hint` with
400
+ // nearest domain names so the caller's next move is
401
+ // a browse rather than a guess.
402
+ //
403
+ // Envelope shape (JSON-stable; consumed by the eval runner and formatters):
404
+ // { mode, results, tokensTotal?, hint? { nearestDomains: string[] } }
405
+ //
406
+ // Single-term hits at tier 1 stay byte-identical to the pre-cascade world —
407
+ // this is a load-bearing invariant for scripted callers that grep the
408
+ // table output; see the corresponding CLI test.
409
+ export async function searchCascade(graph, term, { full = false, repoRoot = null } = {}) {
410
+ // Tier 1: title-AND on original tokens (unchanged behaviour).
411
+ const t1 = await search(graph, term, { full, repoRoot });
412
+ if (t1.length > 0) return { mode: 'title', results: t1 };
413
+
414
+ const originalTokens = term.toLowerCase().split(/\s+/).filter(Boolean);
415
+ const strippedTokens = filterStopwords(originalTokens);
416
+ const strippedDiffers = strippedTokens.length > 0 && strippedTokens.length < originalTokens.length;
417
+ const strippedTerm = strippedTokens.join(' ');
418
+
419
+ // Tier 1b: title-AND on stopword-stripped tokens. Only fires when
420
+ // stripping actually removed content — otherwise it's identical to tier 1
421
+ // and would double the work for nothing.
422
+ if (strippedDiffers) {
423
+ const t1b = await search(graph, strippedTerm, { full, repoRoot });
424
+ if (t1b.length > 0) return { mode: 'title', results: t1b };
425
+ }
426
+
427
+ // Tier 2: body-AND. Try original tokens first, then stripped as a widening
428
+ // fallback. We union rather than replace — the stripped run can only ADD
429
+ // hits, and the sort inside search() puts the best (title-heaviest)
430
+ // first, so unioning is safe.
431
+ let t2 = [];
432
+ if (!full) {
433
+ t2 = await search(graph, term, { full: true, repoRoot });
434
+ if (strippedDiffers) {
435
+ const t2b = await search(graph, strippedTerm, { full: true, repoRoot });
436
+ t2 = unionByIdKeepBest(t2, t2b);
437
+ }
438
+ }
439
+
440
+ // Tier 3: OR on ORIGINAL tokens. Load-bearing: using original (not
441
+ // stripped) preserves the discriminating power of the caller's full
442
+ // query — a 5-token query has 5 possible tokens to match; if we ran on
443
+ // the stripped 3-token set, docs matching the 3 stripped tokens would
444
+ // compress into a single tier and the target (which may only match 2)
445
+ // gets buried. The title-boost inside searchOr does the real ranking
446
+ // work here; stripping the OR set turned out to REGRESS the noisier
447
+ // "what to escalate versus execute" case in first-live eval, so we
448
+ // keep the original tokens. Multi-token only — a single-token OR
449
+ // degenerates to a substring hit over id/title/body, which the
450
+ // callers of the previous cascade version did not expect.
451
+ let t3 = [];
452
+ if (originalTokens.length > 1) {
453
+ t3 = await searchOr(graph, term, { full: true, repoRoot });
454
+ }
455
+
456
+ // Decide between tier 2 and tier 3. If tier 3's top result has strictly
457
+ // more title-token overlap than tier 2's top result, use tier 3 — that's
458
+ // the case where an AND-match trapped the ranker on a body-heavy doc
459
+ // (e.g. `writing/README.md` for "spec and review workflow") when a
460
+ // title-heavy doc (`spec-and-review.md`) is what the caller wants.
461
+ const t2TopTitleTokens = t2.length > 0 ? (t2[0].titleTokens ?? 0) : -1;
462
+ const t3TopTitleTokens = t3.length > 0 ? (t3[0].titleTokensMatched ?? 0) : -1;
463
+ if (t2.length > 0 && t3TopTitleTokens > t2TopTitleTokens) {
464
+ return { mode: 'partial', results: t3, tokensTotal: originalTokens.length };
465
+ }
466
+ if (t2.length > 0) {
467
+ return { mode: 'body', results: t2 };
468
+ }
469
+ if (t3.length > 0) {
470
+ return { mode: 'partial', results: t3, tokensTotal: originalTokens.length };
471
+ }
472
+
473
+ return { mode: 'miss', results: [], hint: guidedMissHint(graph, term) };
474
+ }
475
+
476
+ // Union two search() result arrays by id, keeping the row with the highest
477
+ // (score, titleTokens, edgeCount). Preserves search()'s stable sort order
478
+ // afterwards. Used to fold tier-2 (original tokens) and tier-2b (stripped
479
+ // tokens) into a single body-tier ranking without letting the stripped
480
+ // pass shift the winners the original pass already ordered.
481
+ function unionByIdKeepBest(a, b) {
482
+ const bestById = new Map();
483
+ const better = (x, y) => {
484
+ if (x.score !== y.score) return x.score > y.score;
485
+ if ((x.titleTokens ?? 0) !== (y.titleTokens ?? 0)) return (x.titleTokens ?? 0) > (y.titleTokens ?? 0);
486
+ if (x.edgeCount !== y.edgeCount) return x.edgeCount > y.edgeCount;
487
+ return false;
488
+ };
489
+ for (const row of [...a, ...b]) {
490
+ const prev = bestById.get(row.id);
491
+ if (!prev || better(row, prev)) bestById.set(row.id, row);
492
+ }
493
+ const merged = [...bestById.values()];
494
+ merged.sort((x, y) =>
495
+ (y.score - x.score) ||
496
+ ((y.titleTokens ?? 0) - (x.titleTokens ?? 0)) ||
497
+ (y.edgeCount - x.edgeCount) ||
498
+ x.id.localeCompare(y.id),
499
+ );
500
+ return merged;
501
+ }
502
+
503
+ // Common English stopwords — grammatical function words only. Anything
504
+ // that could reasonably appear in a knowledge-doc TITLE is deliberately
505
+ // NOT here (workflow, routine, process, guide, guidelines, patterns,
506
+ // discipline, standard, all, every, use — all title-plausible content).
507
+ // The list is articles, conjunctions, prepositions, auxiliary/copula
508
+ // verbs, question pronouns, demonstratives, personal pronouns, and a
509
+ // small tail of very high-frequency discourse fillers ("need", "want")
510
+ // that appear in "how do I ..." phrasings and never in doc titles here.
511
+ //
512
+ // If a query is nothing but stopwords ("the and or"), `filterStopwords`
513
+ // returns the original set rather than an empty one — a nonsense query
514
+ // still goes through, just not any differently from before.
515
+ export const STOPWORDS = new Set([
516
+ // articles
517
+ 'a', 'an', 'the',
518
+ // conjunctions
519
+ 'and', 'or', 'but', 'nor', 'so', 'yet',
520
+ // prepositions
521
+ 'of', 'in', 'on', 'at', 'to', 'for', 'with', 'by', 'from',
522
+ 'into', 'onto', 'over', 'under', 'about', 'against', 'between',
523
+ // auxiliary + copula verbs
524
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am',
525
+ 'do', 'does', 'did', 'doing',
526
+ 'have', 'has', 'had', 'having',
527
+ 'will', 'would', 'shall', 'should', 'can', 'could', 'may', 'might', 'must',
528
+ // question / interrogative pronouns
529
+ 'when', 'where', 'what', 'which', 'who', 'whom', 'why', 'how',
530
+ // demonstratives
531
+ 'this', 'that', 'these', 'those',
532
+ // personal pronouns
533
+ 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'them',
534
+ 'my', 'your', 'his', 'her', 'our', 'their', 'its',
535
+ // discourse verbs / fillers common in "how do I …" phrasings.
536
+ // Deliberate scope: no title in the current graph uses these; if a
537
+ // future doc titled "user needs" or "want-triggered flows" lands,
538
+ // trim this section rather than reasoning around it.
539
+ 'need', 'needs', 'needed', 'want', 'wants',
540
+ // small connectives
541
+ 'as', 'if', 'than', 'then',
542
+ ]);
543
+
544
+ export function filterStopwords(tokens) {
545
+ const filtered = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
546
+ return filtered.length > 0 ? filtered : tokens.slice();
547
+ }
548
+
549
+ // Rank domains by relevance to the miss so the caller can pivot to
550
+ // `pa knowledge domain <name>` rather than re-derive from scratch. Score:
551
+ // +2 per token that appears as a substring of the domain name
552
+ // +1 per doc in the domain whose id or title carries any token
553
+ // Falls back to the top-N-by-node-count domains when nothing scores — the
554
+ // operator still gets a browsable index of the graph rather than a bare
555
+ // `(no matches)`.
556
+ function guidedMissHint(graph, term) {
557
+ const tokens = term.toLowerCase().split(/\s+/).filter(Boolean);
558
+ const domains = uniqueDomains(graph);
559
+ const scores = new Map();
560
+ for (const d of domains) scores.set(d, 0);
561
+ for (const d of domains) {
562
+ const dl = d.toLowerCase();
563
+ for (const t of tokens) if (dl.includes(t)) scores.set(d, scores.get(d) + 2);
564
+ }
565
+ for (const node of graph.nodes) {
566
+ if (!node.domain) continue;
567
+ const title = (node.title || '').toLowerCase();
568
+ const id = node.id.toLowerCase();
569
+ let touched = false;
570
+ for (const t of tokens) if (title.includes(t) || id.includes(t)) { touched = true; break; }
571
+ if (touched) scores.set(node.domain, (scores.get(node.domain) ?? 0) + 1);
572
+ }
573
+ const counts = new Map();
574
+ for (const node of graph.nodes) if (node.domain) counts.set(node.domain, (counts.get(node.domain) ?? 0) + 1);
575
+ const ranked = [...scores.entries()].sort((a, b) => {
576
+ if (b[1] !== a[1]) return b[1] - a[1];
577
+ return (counts.get(b[0]) ?? 0) - (counts.get(a[0]) ?? 0);
578
+ });
579
+ const top = ranked.filter(([, s]) => s > 0).slice(0, 3).map(([d]) => d);
580
+ if (top.length > 0) return { nearestDomains: top };
581
+ return { nearestDomains: ranked.slice(0, 3).map(([d]) => d) };
582
+ }
583
+
584
+ async function readBody(repoRoot, id) {
585
+ // The id is either a path relative to the repo root, or a bare slug.
586
+ // We try repoRoot/<id> first, then repoRoot/operator/knowledge/<id>.
587
+ const candidates = [
588
+ resolve(repoRoot, id),
589
+ resolve(repoRoot, 'operator', 'knowledge', id),
590
+ ];
591
+ for (const path of candidates) {
592
+ if (existsSync(path)) {
593
+ try {
594
+ return await readFile(path, 'utf8');
595
+ } catch { return null; }
596
+ }
597
+ }
598
+ return null;
599
+ }
600
+
601
+ // --- verb: domain ---------------------------------------------------------
602
+
603
+ // Return every node in the named domain + the edges between them.
604
+ export function domain(graph, name) {
605
+ const nodes = graph.nodes.filter((n) => n.domain === name);
606
+ if (nodes.length === 0) {
607
+ const known = uniqueDomains(graph);
608
+ const err = new Error(
609
+ `unknown domain: ${name}\navailable domains: ${known.join(', ') || '(none)'}`,
610
+ );
611
+ err.exitCode = 2;
612
+ throw err;
613
+ }
614
+ const nodeIds = new Set(nodes.map((n) => n.id));
615
+ const edges = graph.edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
616
+ return { domain: name, nodes, edges };
617
+ }
618
+
619
+ // --- verb: orphans --------------------------------------------------------
620
+
621
+ // Nodes with zero inbound edges. Excludes null-domain nodes (auto-generated
622
+ // stubs) if `includeStubs` is false. Default is to include everything so
623
+ // operators see the full picture.
624
+ export function orphans(graph, { includeStubs = true } = {}) {
625
+ const adj = buildAdjacency(graph);
626
+ const results = [];
627
+ for (const node of graph.nodes) {
628
+ if (!includeStubs && node.domain == null) continue;
629
+ if ((adj.in.get(node.id)?.length ?? 0) === 0) {
630
+ results.push({ id: node.id, title: node.title, domain: node.domain, outDegree: adj.out.get(node.id)?.length ?? 0 });
631
+ }
632
+ }
633
+ results.sort((a, b) => (a.domain ?? '').localeCompare(b.domain ?? '') || a.id.localeCompare(b.id));
634
+ return results;
635
+ }
636
+
637
+ // --- verb: path -----------------------------------------------------------
638
+
639
+ // Shortest path between two slugs. Wraps `shortestPath` with slug
640
+ // resolution + node-index annotation.
641
+ export function path(graph, a, b) {
642
+ const from = resolveSingle(graph, a, 'from');
643
+ const to = resolveSingle(graph, b, 'to');
644
+ const raw = shortestPath(graph, from, to);
645
+ if (raw == null) return { from, to, path: null };
646
+ const byId = nodeIndex(graph);
647
+ return {
648
+ from,
649
+ to,
650
+ path: raw.map((step) => ({
651
+ id: step.id,
652
+ title: byId.get(step.id)?.title ?? null,
653
+ domain: byId.get(step.id)?.domain ?? null,
654
+ type: step.type,
655
+ })),
656
+ };
657
+ }
658
+
659
+ function resolveSingle(graph, slug, label) {
660
+ const r = resolveSlug(graph, slug);
661
+ if (r.matches.length === 0) {
662
+ const err = new Error(`unknown slug (${label}): ${slug}`);
663
+ err.exitCode = 2;
664
+ throw err;
665
+ }
666
+ if (r.matches.length > 1) {
667
+ const err = new Error(
668
+ `ambiguous slug (${label}): ${slug} — matches ${r.matches.length} nodes:\n ${r.matches.slice(0, 10).join('\n ')}` +
669
+ (r.matches.length > 10 ? `\n ...` : ''),
670
+ );
671
+ err.exitCode = 2;
672
+ throw err;
673
+ }
674
+ return r.matches[0];
675
+ }
676
+
677
+ // --- helpers --------------------------------------------------------------
678
+
679
+ export function nodeIndex(graph) {
680
+ const m = new Map();
681
+ for (const n of graph.nodes) m.set(n.id, n);
682
+ return m;
683
+ }
684
+
685
+ function annotate(byId, id, type, direction) {
686
+ const node = byId.get(id);
687
+ return {
688
+ id,
689
+ title: node?.title ?? null,
690
+ domain: node?.domain ?? null,
691
+ type,
692
+ direction,
693
+ };
694
+ }
695
+
696
+ export function uniqueDomains(graph) {
697
+ const set = new Set();
698
+ for (const n of graph.nodes) {
699
+ if (n.domain != null) set.add(n.domain);
700
+ }
701
+ return Array.from(set).sort();
702
+ }
703
+
704
+ // --- repo-root discovery --------------------------------------------------
705
+
706
+ // `pa knowledge` needs an operator-repo root to find the graph. The
707
+ // existing pa CLI has `pa/repo-root.mjs`; import there rather than
708
+ // duplicate. This module keeps its dependency footprint to node stdlib +
709
+ // pa/repo-root.mjs.
710
+ export { getRepoRoot } from '../pa/repo-root.mjs';