sentinelayer-cli 0.6.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +1009 -996
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +64 -63
  6. package/src/agents/ai-governance/index.js +12 -0
  7. package/src/agents/ai-governance/tools/base.js +171 -0
  8. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  9. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  10. package/src/agents/ai-governance/tools/index.js +52 -0
  11. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  12. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  13. package/src/agents/backend/index.js +12 -0
  14. package/src/agents/backend/tools/base.js +189 -0
  15. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  16. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  17. package/src/agents/backend/tools/index.js +87 -0
  18. package/src/agents/backend/tools/retry-audit.js +132 -0
  19. package/src/agents/backend/tools/timeout-audit.js +144 -0
  20. package/src/agents/code-quality/index.js +12 -0
  21. package/src/agents/code-quality/tools/base.js +159 -0
  22. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  23. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  24. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  25. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  26. package/src/agents/code-quality/tools/index.js +89 -0
  27. package/src/agents/data-layer/index.js +12 -0
  28. package/src/agents/data-layer/tools/base.js +181 -0
  29. package/src/agents/data-layer/tools/index-audit.js +165 -0
  30. package/src/agents/data-layer/tools/index.js +83 -0
  31. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  32. package/src/agents/data-layer/tools/query-explain.js +120 -0
  33. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  34. package/src/agents/documentation/index.js +12 -0
  35. package/src/agents/documentation/tools/api-diff.js +91 -0
  36. package/src/agents/documentation/tools/base.js +151 -0
  37. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  38. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  39. package/src/agents/documentation/tools/index.js +52 -0
  40. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  41. package/src/agents/envelope/fix-cycle.js +45 -0
  42. package/src/agents/envelope/index.js +31 -0
  43. package/src/agents/envelope/loop.js +150 -0
  44. package/src/agents/envelope/pulse.js +18 -0
  45. package/src/agents/envelope/stream.js +40 -0
  46. package/src/agents/infrastructure/index.js +12 -0
  47. package/src/agents/infrastructure/tools/base.js +171 -0
  48. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  49. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  50. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  51. package/src/agents/infrastructure/tools/index.js +52 -0
  52. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  53. package/src/agents/jules/config/definition.js +160 -160
  54. package/src/agents/jules/config/system-prompt.js +182 -182
  55. package/src/agents/jules/error-intake.js +51 -51
  56. package/src/agents/jules/fix-cycle.js +17 -17
  57. package/src/agents/jules/loop.js +460 -450
  58. package/src/agents/jules/pulse.js +10 -10
  59. package/src/agents/jules/stream.js +187 -186
  60. package/src/agents/jules/swarm/file-scanner.js +74 -74
  61. package/src/agents/jules/swarm/index.js +11 -11
  62. package/src/agents/jules/swarm/orchestrator.js +362 -362
  63. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  64. package/src/agents/jules/swarm/sub-agent.js +315 -309
  65. package/src/agents/jules/tools/aidenid-email.js +189 -189
  66. package/src/agents/jules/tools/auth-audit.js +1708 -1691
  67. package/src/agents/jules/tools/dispatch.js +340 -335
  68. package/src/agents/jules/tools/file-edit.js +2 -2
  69. package/src/agents/jules/tools/file-read.js +2 -2
  70. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  71. package/src/agents/jules/tools/glob.js +2 -2
  72. package/src/agents/jules/tools/grep.js +2 -2
  73. package/src/agents/jules/tools/index.js +29 -29
  74. package/src/agents/jules/tools/path-guards.js +2 -2
  75. package/src/agents/jules/tools/runtime-audit.js +507 -507
  76. package/src/agents/jules/tools/shell.js +2 -2
  77. package/src/agents/jules/tools/url-policy.js +100 -100
  78. package/src/agents/mode.js +113 -0
  79. package/src/agents/observability/index.js +12 -0
  80. package/src/agents/observability/tools/alert-audit.js +39 -0
  81. package/src/agents/observability/tools/base.js +181 -0
  82. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  83. package/src/agents/observability/tools/index.js +54 -0
  84. package/src/agents/observability/tools/log-schema-check.js +74 -0
  85. package/src/agents/observability/tools/span-coverage.js +74 -0
  86. package/src/agents/persona-visuals.js +102 -61
  87. package/src/agents/release/index.js +12 -0
  88. package/src/agents/release/tools/base.js +181 -0
  89. package/src/agents/release/tools/changelog-diff.js +86 -0
  90. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  91. package/src/agents/release/tools/index.js +61 -0
  92. package/src/agents/release/tools/rollback-verify.js +129 -0
  93. package/src/agents/release/tools/semver-check.js +109 -0
  94. package/src/agents/reliability/index.js +12 -0
  95. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  96. package/src/agents/reliability/tools/base.js +181 -0
  97. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  98. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  99. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  100. package/src/agents/reliability/tools/index.js +87 -0
  101. package/src/agents/run-persona.js +109 -0
  102. package/src/agents/security/index.js +12 -0
  103. package/src/agents/security/tools/authz-audit.js +134 -0
  104. package/src/agents/security/tools/base.js +190 -0
  105. package/src/agents/security/tools/crypto-review.js +175 -0
  106. package/src/agents/security/tools/index.js +97 -0
  107. package/src/agents/security/tools/sast-scan.js +175 -0
  108. package/src/agents/security/tools/secrets-scan.js +216 -0
  109. package/src/agents/shared-tools/dispatch-core.js +320 -315
  110. package/src/agents/shared-tools/file-edit.js +180 -180
  111. package/src/agents/shared-tools/file-read.js +100 -100
  112. package/src/agents/shared-tools/glob.js +168 -168
  113. package/src/agents/shared-tools/grep.js +228 -228
  114. package/src/agents/shared-tools/index.js +46 -46
  115. package/src/agents/shared-tools/path-guards.js +161 -161
  116. package/src/agents/shared-tools/shell.js +383 -383
  117. package/src/agents/supply-chain/index.js +12 -0
  118. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  119. package/src/agents/supply-chain/tools/base.js +151 -0
  120. package/src/agents/supply-chain/tools/index.js +52 -0
  121. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  122. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  123. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  124. package/src/agents/testing/index.js +12 -0
  125. package/src/agents/testing/tools/base.js +202 -0
  126. package/src/agents/testing/tools/coverage-gap.js +144 -0
  127. package/src/agents/testing/tools/flake-detect.js +125 -0
  128. package/src/agents/testing/tools/index.js +85 -0
  129. package/src/agents/testing/tools/mutation-test.js +143 -0
  130. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  131. package/src/ai/aidenid.js +1021 -1009
  132. package/src/ai/client.js +553 -553
  133. package/src/ai/domain-target-store.js +268 -268
  134. package/src/ai/identity-store.js +270 -270
  135. package/src/ai/proxy.js +137 -137
  136. package/src/ai/site-store.js +145 -145
  137. package/src/audit/agents/architecture.js +180 -180
  138. package/src/audit/agents/compliance.js +179 -179
  139. package/src/audit/agents/documentation.js +165 -165
  140. package/src/audit/agents/performance.js +145 -145
  141. package/src/audit/agents/security.js +215 -215
  142. package/src/audit/agents/testing.js +172 -172
  143. package/src/audit/orchestrator.js +557 -557
  144. package/src/audit/package.js +204 -204
  145. package/src/audit/registry.js +284 -284
  146. package/src/audit/replay.js +103 -103
  147. package/src/auth/gate.js +428 -371
  148. package/src/auth/http.js +681 -611
  149. package/src/auth/service.js +1106 -1106
  150. package/src/auth/session-store.js +813 -813
  151. package/src/cli.js +257 -252
  152. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  153. package/src/commands/ai/provision-governance.js +1272 -1272
  154. package/src/commands/ai/shared.js +147 -147
  155. package/src/commands/ai.js +11 -11
  156. package/src/commands/apply.js +12 -12
  157. package/src/commands/audit.js +1171 -1166
  158. package/src/commands/auth.js +419 -419
  159. package/src/commands/chat.js +184 -191
  160. package/src/commands/config.js +184 -184
  161. package/src/commands/cost.js +311 -311
  162. package/src/commands/daemon/core.js +850 -850
  163. package/src/commands/daemon/extended.js +1048 -1048
  164. package/src/commands/daemon/shared.js +213 -213
  165. package/src/commands/daemon.js +11 -11
  166. package/src/commands/guide.js +174 -174
  167. package/src/commands/ingest.js +58 -58
  168. package/src/commands/init.js +55 -55
  169. package/src/commands/legacy-args.js +20 -10
  170. package/src/commands/mcp.js +461 -461
  171. package/src/commands/omargate.js +63 -29
  172. package/src/commands/persona.js +65 -20
  173. package/src/commands/plugin.js +260 -260
  174. package/src/commands/policy.js +132 -132
  175. package/src/commands/prompt.js +238 -238
  176. package/src/commands/review.js +704 -704
  177. package/src/commands/scan.js +865 -872
  178. package/src/commands/session.js +1238 -0
  179. package/src/commands/spec.js +771 -716
  180. package/src/commands/swarm.js +651 -651
  181. package/src/commands/telemetry.js +202 -202
  182. package/src/commands/watch.js +511 -511
  183. package/src/config/agent-dictionary.js +182 -182
  184. package/src/config/io.js +56 -56
  185. package/src/config/paths.js +18 -18
  186. package/src/config/schema.js +55 -55
  187. package/src/config/service.js +184 -184
  188. package/src/coord/events-log.js +141 -0
  189. package/src/coord/handshake.js +719 -0
  190. package/src/coord/index.js +35 -0
  191. package/src/coord/paths.js +84 -0
  192. package/src/coord/priority.js +62 -0
  193. package/src/coord/tarjan.js +157 -0
  194. package/src/cost/budget.js +235 -235
  195. package/src/cost/history.js +188 -188
  196. package/src/cost/tokenizer.js +160 -0
  197. package/src/cost/tracker.js +232 -171
  198. package/src/daemon/artifact-lineage.js +896 -534
  199. package/src/daemon/assignment-ledger.js +1083 -770
  200. package/src/daemon/ast-drift.js +496 -0
  201. package/src/daemon/ast-parser-layer.js +258 -258
  202. package/src/daemon/budget-governor.js +633 -633
  203. package/src/daemon/callgraph-overlay.js +646 -646
  204. package/src/daemon/error-worker.js +1209 -626
  205. package/src/daemon/fix-cycle.js +384 -377
  206. package/src/daemon/hybrid-mapper.js +929 -929
  207. package/src/daemon/ingest-refresh.js +79 -11
  208. package/src/daemon/jira-lifecycle.js +767 -632
  209. package/src/daemon/operator-control.js +657 -657
  210. package/src/daemon/pulse.js +327 -327
  211. package/src/daemon/reliability-lane.js +471 -471
  212. package/src/daemon/scope-engine.js +1068 -0
  213. package/src/daemon/watchdog.js +971 -971
  214. package/src/events/schema.js +190 -0
  215. package/src/guide/generator.js +316 -316
  216. package/src/ingest/engine.js +933 -918
  217. package/src/ingest/ownership.js +380 -0
  218. package/src/interactive/index.js +97 -97
  219. package/src/legacy-cli.js +3228 -2994
  220. package/src/mcp/registry.js +695 -695
  221. package/src/memory/blackboard.js +301 -301
  222. package/src/memory/retrieval.js +581 -581
  223. package/src/orchestrator/kai-chen.js +126 -0
  224. package/src/plugin/manifest.js +553 -553
  225. package/src/policy/packs.js +144 -144
  226. package/src/prompt/generator.js +136 -118
  227. package/src/review/ai-review.js +672 -679
  228. package/src/review/compliance-pack.js +389 -0
  229. package/src/review/investor-dd-config.js +54 -0
  230. package/src/review/investor-dd-file-loop.js +303 -0
  231. package/src/review/investor-dd-file-router.js +406 -0
  232. package/src/review/investor-dd-html-report.js +233 -0
  233. package/src/review/investor-dd-notification.js +120 -0
  234. package/src/review/investor-dd-orchestrator.js +405 -0
  235. package/src/review/investor-dd-persona-runner.js +275 -0
  236. package/src/review/live-validator.js +253 -0
  237. package/src/review/local-review.js +1351 -1305
  238. package/src/review/omargate-interactive.js +68 -68
  239. package/src/review/omargate-orchestrator.js +492 -300
  240. package/src/review/persona-prompts.js +484 -296
  241. package/src/review/reconciliation-rules.js +329 -0
  242. package/src/review/replay.js +235 -235
  243. package/src/review/report.js +664 -664
  244. package/src/review/reproducibility-chain.js +136 -0
  245. package/src/review/scan-modes.js +147 -42
  246. package/src/review/spec-binding.js +487 -487
  247. package/src/scaffold/generator.js +67 -67
  248. package/src/scaffold/templates.js +150 -150
  249. package/src/scan/generator.js +418 -418
  250. package/src/scan/gh-secrets.js +107 -107
  251. package/src/session/agent-registry.js +359 -0
  252. package/src/session/analytics.js +479 -0
  253. package/src/session/daemon.js +1396 -0
  254. package/src/session/file-locks.js +666 -0
  255. package/src/session/paths.js +37 -0
  256. package/src/session/recap.js +567 -0
  257. package/src/session/redact.js +82 -0
  258. package/src/session/runtime-bridge.js +762 -0
  259. package/src/session/scoring.js +406 -0
  260. package/src/session/setup-guides.js +304 -0
  261. package/src/session/store.js +704 -0
  262. package/src/session/stream.js +333 -0
  263. package/src/session/sync.js +753 -0
  264. package/src/session/tasks.js +1054 -0
  265. package/src/session/templates.js +188 -0
  266. package/src/spec/generator.js +619 -519
  267. package/src/spec/regenerate.js +237 -237
  268. package/src/spec/templates.js +91 -91
  269. package/src/swarm/dashboard.js +247 -247
  270. package/src/swarm/factory.js +363 -363
  271. package/src/swarm/pentest.js +934 -934
  272. package/src/swarm/registry.js +419 -419
  273. package/src/swarm/report.js +158 -158
  274. package/src/swarm/runtime.js +569 -576
  275. package/src/swarm/scenario-dsl.js +272 -272
  276. package/src/telemetry/ledger.js +302 -302
  277. package/src/telemetry/session-tracker.js +234 -234
  278. package/src/telemetry/sync.js +203 -203
  279. package/src/ui/command-hints.js +13 -13
  280. package/src/ui/markdown.js +220 -220
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Per-file agentic review loop for investor-DD.
3
+ *
4
+ * Given a persona config, a list of files in scope, and an LLM-backed client,
5
+ * iterate file-by-file, running a bounded multi-turn tool-using loop per file.
6
+ * Emits structured events for session streaming and accumulates structured
7
+ * findings plus coverage proof (every file visited, how many turns, which
8
+ * tools were invoked).
9
+ *
10
+ * No fix-cycle path — review personas never mutate source. Tools are
11
+ * constrained to the caller-supplied list; the library does not import any
12
+ * edit/write tools.
13
+ *
14
+ * Budget: caller supplies a shared budget accumulator that is decremented
15
+ * on every tool call and LLM call. When the budget trips, the loop stops
16
+ * cleanly at the current file boundary so a partial-report generator can
17
+ * still emit what was finished.
18
+ */
19
+
20
+ import { runEnvelopeLoop } from "../agents/envelope/index.js";
21
+
22
+ export const INVESTOR_DD_DEFAULT_MAX_TURNS_PER_FILE = 6;
23
+ export const INVESTOR_DD_DEFAULT_STUCK_THRESHOLD = 2;
24
+
25
+ /**
26
+ * @typedef {object} InvestorDdBudgetState
27
+ * @property {number} spentUsd - Running USD spend.
28
+ * @property {number} maxUsd - Hard cap.
29
+ * @property {number} startedAtMs - Epoch ms when the run began.
30
+ * @property {number} maxRuntimeMs - Hard cap on runtime.
31
+ * @property {number} toolCalls - Running count of tool invocations.
32
+ * @property {number} llmCalls - Running count of LLM invocations.
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} InvestorDdFileLoopEvent
37
+ * @property {string} type
38
+ * @property {string} personaId
39
+ * @property {string} file
40
+ * @property {number} [turn]
41
+ * @property {string} [tool]
42
+ * @property {object} [finding]
43
+ * @property {string} [stopReason]
44
+ * @property {number} [turnsUsed]
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} InvestorDdFileLoopResult
49
+ * @property {string} personaId
50
+ * @property {Array<{file: string, findings: Array<object>, turnsUsed: number, stopReason: string|null, toolInvocations: Array<object>}>} perFile
51
+ * @property {Array<object>} findings - Flat list of all findings.
52
+ * @property {Array<string>} visited - Files the loop actually visited.
53
+ * @property {Array<string>} skipped - Files skipped because budget was exhausted.
54
+ * @property {"ok"|"budget-cost-exhausted"|"budget-runtime-exhausted"|"client-error"} terminationReason
55
+ */
56
+
57
+ /**
58
+ * Check whether the shared budget still permits further work. When false,
59
+ * the loop stops at the current file boundary and reports the remaining
60
+ * files as `skipped` so the caller can emit a partial report.
61
+ *
62
+ * @param {InvestorDdBudgetState} budget
63
+ * @returns {{ ok: true } | { ok: false, reason: "budget-cost-exhausted" | "budget-runtime-exhausted" }}
64
+ */
65
+ export function checkBudget(budget) {
66
+ if (!budget) return { ok: true };
67
+ if (Number.isFinite(budget.maxUsd) && budget.spentUsd >= budget.maxUsd) {
68
+ return { ok: false, reason: "budget-cost-exhausted" };
69
+ }
70
+ if (Number.isFinite(budget.maxRuntimeMs) && Number.isFinite(budget.startedAtMs)) {
71
+ const elapsed = Date.now() - budget.startedAtMs;
72
+ if (elapsed >= budget.maxRuntimeMs) {
73
+ return { ok: false, reason: "budget-runtime-exhausted" };
74
+ }
75
+ }
76
+ return { ok: true };
77
+ }
78
+
79
+ /**
80
+ * Instantiate a fresh budget state from caller-supplied caps.
81
+ *
82
+ * @param {object} [opts]
83
+ * @param {number} [opts.maxUsd]
84
+ * @param {number} [opts.maxRuntimeMs]
85
+ * @returns {InvestorDdBudgetState}
86
+ */
87
+ export function createBudgetState({ maxUsd = Infinity, maxRuntimeMs = Infinity } = {}) {
88
+ return {
89
+ spentUsd: 0,
90
+ maxUsd,
91
+ startedAtMs: Date.now(),
92
+ maxRuntimeMs,
93
+ toolCalls: 0,
94
+ llmCalls: 0,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Wrap the caller's tool array so every invocation increments the shared
100
+ * budget counters. The wrapping does not alter tool contract; it only
101
+ * observes + accounts.
102
+ *
103
+ * @param {Array<{name: string, invoke: Function, costUsd?: number}>} tools
104
+ * @param {InvestorDdBudgetState} budget
105
+ * @param {Function} onToolCall - (name, input) => void
106
+ */
107
+ function meterTools(tools, budget, onToolCall) {
108
+ return tools.map((tool) => ({
109
+ ...tool,
110
+ invoke: async (input) => {
111
+ budget.toolCalls += 1;
112
+ if (Number.isFinite(tool.costUsd)) {
113
+ budget.spentUsd += tool.costUsd;
114
+ }
115
+ try {
116
+ onToolCall(tool.name, input);
117
+ } catch {
118
+ // observer errors never break review
119
+ }
120
+ return tool.invoke(input);
121
+ },
122
+ }));
123
+ }
124
+
125
+ /**
126
+ * Wrap the caller's LLM client so every generatePlan call increments the
127
+ * llmCalls counter. Cost accounting for LLM calls is the client's
128
+ * responsibility (it knows the model and tokens), so the client adds to
129
+ * `budget.spentUsd` directly.
130
+ *
131
+ * @param {object} client
132
+ * @param {InvestorDdBudgetState} budget
133
+ */
134
+ function meterClient(client, budget) {
135
+ return {
136
+ ...client,
137
+ generatePlan: async (messages, options) => {
138
+ budget.llmCalls += 1;
139
+ return client.generatePlan(messages, options);
140
+ },
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Run the per-file agentic review loop for a single persona.
146
+ *
147
+ * @param {object} params
148
+ * @param {string} params.personaId
149
+ * @param {Array<string>} params.files - Files in scope for this persona.
150
+ * @param {object} params.client - Must implement generatePlan().
151
+ * @param {(file: string) => Array<{name: string, invoke: Function, costUsd?: number}>} params.buildTools
152
+ * Factory that returns the tool list scoped to a single file. Called once per file.
153
+ * @param {(file: string) => Array<object>} params.buildInitialMessages
154
+ * Factory that returns the LLM messages to seed the loop for this file.
155
+ * @param {InvestorDdBudgetState} params.budget - Shared budget state.
156
+ * @param {(event: InvestorDdFileLoopEvent) => void} [params.onEvent] - Event sink.
157
+ * @param {object} [params.options]
158
+ * @param {number} [params.options.maxTurnsPerFile]
159
+ * @param {number} [params.options.stuckThreshold]
160
+ * @returns {Promise<InvestorDdFileLoopResult>}
161
+ */
162
+ export async function runPerFileReviewLoop({
163
+ personaId,
164
+ files,
165
+ client,
166
+ buildTools,
167
+ buildInitialMessages,
168
+ budget,
169
+ onEvent = () => {},
170
+ options = {},
171
+ } = {}) {
172
+ if (!personaId || typeof personaId !== "string") {
173
+ throw new TypeError("runPerFileReviewLoop requires a personaId string");
174
+ }
175
+ if (!Array.isArray(files)) {
176
+ throw new TypeError("runPerFileReviewLoop requires a files array");
177
+ }
178
+ if (typeof buildTools !== "function") {
179
+ throw new TypeError("runPerFileReviewLoop requires buildTools(file) factory");
180
+ }
181
+ if (typeof buildInitialMessages !== "function") {
182
+ throw new TypeError("runPerFileReviewLoop requires buildInitialMessages(file) factory");
183
+ }
184
+ if (!client || typeof client.generatePlan !== "function") {
185
+ throw new TypeError("runPerFileReviewLoop requires a client with generatePlan()");
186
+ }
187
+
188
+ const maxTurns = Number.isInteger(options.maxTurnsPerFile)
189
+ ? options.maxTurnsPerFile
190
+ : INVESTOR_DD_DEFAULT_MAX_TURNS_PER_FILE;
191
+ const stuckThreshold = Number.isInteger(options.stuckThreshold)
192
+ ? options.stuckThreshold
193
+ : INVESTOR_DD_DEFAULT_STUCK_THRESHOLD;
194
+
195
+ const safeBudget = budget || createBudgetState();
196
+ const meteredClient = meterClient(client, safeBudget);
197
+
198
+ const perFile = [];
199
+ const allFindings = [];
200
+ const visited = [];
201
+ const skipped = [];
202
+ let terminationReason = "ok";
203
+
204
+ const emit = (event) => {
205
+ try {
206
+ onEvent(event);
207
+ } catch {
208
+ // sinks never break review
209
+ }
210
+ };
211
+
212
+ for (const file of files) {
213
+ const budgetCheck = checkBudget(safeBudget);
214
+ if (!budgetCheck.ok) {
215
+ terminationReason = budgetCheck.reason;
216
+ skipped.push(file);
217
+ emit({ type: "persona_file_skipped", personaId, file, stopReason: budgetCheck.reason });
218
+ continue;
219
+ }
220
+
221
+ emit({ type: "persona_file_start", personaId, file });
222
+
223
+ const fileTools = buildTools(file);
224
+ const meteredTools = meterTools(fileTools, safeBudget, (tool, input) => {
225
+ emit({ type: "persona_file_tool_call", personaId, file, tool, input });
226
+ });
227
+ const initialMessages = buildInitialMessages(file);
228
+
229
+ let loopResult;
230
+ try {
231
+ loopResult = await runEnvelopeLoop({
232
+ client: meteredClient,
233
+ initialMessages,
234
+ tools: meteredTools,
235
+ options: {
236
+ maxTurns,
237
+ stuckThreshold,
238
+ shouldAllowCall: () => {
239
+ const check = checkBudget(safeBudget);
240
+ return { allow: check.ok };
241
+ },
242
+ onTurn: ({ turn, plan }) => {
243
+ emit({
244
+ type: "persona_file_turn",
245
+ personaId,
246
+ file,
247
+ turn,
248
+ stopReason: plan?.stopReason ?? null,
249
+ });
250
+ const findings = Array.isArray(plan?.findings) ? plan.findings : [];
251
+ for (const f of findings) {
252
+ const decorated = { ...f, personaId, file };
253
+ allFindings.push(decorated);
254
+ emit({ type: "persona_finding", personaId, file, finding: decorated });
255
+ }
256
+ },
257
+ },
258
+ });
259
+ } catch (err) {
260
+ terminationReason = "client-error";
261
+ emit({
262
+ type: "persona_file_error",
263
+ personaId,
264
+ file,
265
+ stopReason: err instanceof Error ? err.message : String(err),
266
+ });
267
+ perFile.push({
268
+ file,
269
+ findings: [],
270
+ turnsUsed: 0,
271
+ stopReason: "client-error",
272
+ toolInvocations: [],
273
+ });
274
+ continue;
275
+ }
276
+
277
+ visited.push(file);
278
+ perFile.push({
279
+ file,
280
+ findings: Array.isArray(loopResult.findings) ? loopResult.findings : [],
281
+ turnsUsed: loopResult.turnsUsed ?? 0,
282
+ stopReason: loopResult.stuckReason ?? null,
283
+ toolInvocations: Array.isArray(loopResult.toolInvocations) ? loopResult.toolInvocations : [],
284
+ });
285
+
286
+ emit({
287
+ type: "persona_file_complete",
288
+ personaId,
289
+ file,
290
+ turnsUsed: loopResult.turnsUsed ?? 0,
291
+ stopReason: loopResult.stuckReason ?? null,
292
+ });
293
+ }
294
+
295
+ return {
296
+ personaId,
297
+ perFile,
298
+ findings: allFindings,
299
+ visited,
300
+ skipped,
301
+ terminationReason,
302
+ };
303
+ }
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Deterministic file routing engine for investor-DD.
3
+ *
4
+ * Given the full list of files in a target repo and the persona roster,
5
+ * produce a routing table `{ personaId: filesInScope[] }` based on
6
+ * domain-specific include/exclude patterns. The router is deterministic
7
+ * (same inputs → same routing) so a run is replayable, and overlap is
8
+ * allowed — a single file can land in multiple persona queues if the
9
+ * patterns match.
10
+ *
11
+ * When a persona has NO matches after pattern filtering, the router
12
+ * falls back to a capped subset of "risk-surface" files (entry points,
13
+ * config, routes) so the persona still has something to look at rather
14
+ * than silently reporting empty coverage.
15
+ */
16
+
17
+ const POSIX_SEP = "/";
18
+
19
+ /**
20
+ * Per-persona include/exclude rules. Rules are substring checks against
21
+ * POSIX-normalized relative paths (so they survive Windows/linux). Glob
22
+ * matchers are intentionally avoided here — every rule is a simple
23
+ * includes() check so the routing is easy to reason about and unit-test.
24
+ */
25
+ export const INVESTOR_DD_PERSONA_RULES = Object.freeze({
26
+ security: {
27
+ include: [
28
+ "/auth",
29
+ "/security",
30
+ "/crypto",
31
+ "/token",
32
+ "/password",
33
+ "/session",
34
+ "/login",
35
+ "/oauth",
36
+ "/permission",
37
+ "/role",
38
+ "/sanitiz",
39
+ "/escape",
40
+ "/middleware",
41
+ ],
42
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"],
43
+ exclude: ["/__fixtures__/", "/test-data/"],
44
+ },
45
+ backend: {
46
+ include: [
47
+ "/server",
48
+ "/api",
49
+ "/handler",
50
+ "/route",
51
+ "/controller",
52
+ "/service",
53
+ "/worker",
54
+ "/queue",
55
+ "/job",
56
+ "/middleware",
57
+ ],
58
+ extensions: [".js", ".ts", ".py", ".go", ".rs", ".java"],
59
+ exclude: ["/__fixtures__/", "/test-data/", "/web/", "/frontend/"],
60
+ },
61
+ "code-quality": {
62
+ include: [
63
+ "/src/",
64
+ "/lib/",
65
+ "/app/",
66
+ "/packages/",
67
+ ],
68
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"],
69
+ exclude: ["/node_modules/", "/dist/", "/build/", "/__snapshots__/", "/vendor/"],
70
+ },
71
+ testing: {
72
+ include: [
73
+ "/test/",
74
+ "/tests/",
75
+ "/__tests__/",
76
+ ".test.",
77
+ ".spec.",
78
+ "_test.",
79
+ "/conftest.py",
80
+ ],
81
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".mjs", ".py", ".go", ".rs", ".java"],
82
+ exclude: ["/node_modules/", "/dist/"],
83
+ },
84
+ "data-layer": {
85
+ include: [
86
+ "/db/",
87
+ "/database/",
88
+ "/models/",
89
+ "/schema",
90
+ "/migration",
91
+ "/query",
92
+ "/repository",
93
+ "/repositories/",
94
+ "/dao",
95
+ ".sql",
96
+ "/prisma/",
97
+ "/sequelize/",
98
+ "/orm/",
99
+ ],
100
+ extensions: [".js", ".ts", ".py", ".sql", ".prisma"],
101
+ exclude: ["/node_modules/"],
102
+ },
103
+ reliability: {
104
+ include: [
105
+ "/health",
106
+ "/readiness",
107
+ "/liveness",
108
+ "/retry",
109
+ "/circuit",
110
+ "/fallback",
111
+ "/backpressure",
112
+ "/rate-limit",
113
+ "/degradation",
114
+ ],
115
+ extensions: [".js", ".ts", ".py", ".go"],
116
+ exclude: ["/node_modules/"],
117
+ },
118
+ release: {
119
+ include: [
120
+ ".github/workflows/",
121
+ "/ci/",
122
+ "CHANGELOG",
123
+ "/release",
124
+ "/deploy",
125
+ "/rollout",
126
+ "/version",
127
+ "/feature-flag",
128
+ "/feature_flag",
129
+ "/flags",
130
+ ],
131
+ extensions: [".yml", ".yaml", ".js", ".ts", ".py", ".md"],
132
+ exclude: [],
133
+ },
134
+ observability: {
135
+ include: [
136
+ "/logger",
137
+ "/logging",
138
+ "/metric",
139
+ "/trace",
140
+ "/telemetry",
141
+ "/span",
142
+ "/dashboard",
143
+ "/grafana",
144
+ "/alert",
145
+ "/monitor",
146
+ ],
147
+ extensions: [".js", ".ts", ".py", ".go", ".yml", ".yaml", ".json"],
148
+ exclude: [],
149
+ },
150
+ infrastructure: {
151
+ include: [
152
+ "/terraform/",
153
+ ".tf",
154
+ ".tfvars",
155
+ "/kubernetes/",
156
+ "/k8s/",
157
+ "/manifests/",
158
+ "/helm/",
159
+ "/docker",
160
+ "Dockerfile",
161
+ ".github/workflows/",
162
+ "/cdk/",
163
+ "/pulumi/",
164
+ "/serverless",
165
+ ],
166
+ extensions: [".tf", ".yaml", ".yml", ".json", ".hcl"],
167
+ exclude: [],
168
+ },
169
+ "supply-chain": {
170
+ include: [
171
+ "package.json",
172
+ "package-lock.json",
173
+ "yarn.lock",
174
+ "pnpm-lock.yaml",
175
+ "requirements.txt",
176
+ "pyproject.toml",
177
+ "Pipfile.lock",
178
+ "go.mod",
179
+ "go.sum",
180
+ "Cargo.toml",
181
+ "Cargo.lock",
182
+ "Gemfile",
183
+ "Gemfile.lock",
184
+ ".github/workflows/",
185
+ "SBOM",
186
+ "sbom",
187
+ ],
188
+ extensions: [],
189
+ exclude: ["/node_modules/"],
190
+ },
191
+ frontend: {
192
+ include: [
193
+ "/frontend/",
194
+ "/web/",
195
+ "/client/",
196
+ "/pages/",
197
+ "/components/",
198
+ "/app/",
199
+ "/ui/",
200
+ "/views/",
201
+ "/templates/",
202
+ ".vue",
203
+ ".svelte",
204
+ ".astro",
205
+ ],
206
+ extensions: [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html", ".css", ".scss"],
207
+ exclude: ["/server/", "/api/", "/node_modules/"],
208
+ },
209
+ documentation: {
210
+ include: [
211
+ "README",
212
+ "CHANGELOG",
213
+ "CONTRIBUTING",
214
+ "SECURITY",
215
+ "CODE_OF_CONDUCT",
216
+ "/docs/",
217
+ ".md",
218
+ ".mdx",
219
+ ".rst",
220
+ ],
221
+ extensions: [".md", ".mdx", ".rst", ".txt"],
222
+ exclude: ["/node_modules/"],
223
+ },
224
+ "ai-governance": {
225
+ include: [
226
+ "/prompt",
227
+ "/prompts/",
228
+ "/llm/",
229
+ "/ai/",
230
+ "/agent",
231
+ "/eval",
232
+ "/evals/",
233
+ "/guardrail",
234
+ "/safety",
235
+ "/completion",
236
+ "/inference",
237
+ "/embeddings",
238
+ "/rag",
239
+ ],
240
+ extensions: [".py", ".js", ".ts", ".yaml", ".yml", ".json", ".md"],
241
+ exclude: [],
242
+ },
243
+ });
244
+
245
+ const DEFAULT_FALLBACK_CAP = 20;
246
+
247
+ const RISK_SURFACE_HINTS = Object.freeze([
248
+ "/server",
249
+ "/api",
250
+ "/main",
251
+ "/index",
252
+ "/app",
253
+ "/route",
254
+ "/handler",
255
+ "/auth",
256
+ "Dockerfile",
257
+ "/.github/workflows/",
258
+ ]);
259
+
260
+ /**
261
+ * Normalize a path to POSIX separators so the substring rules are
262
+ * portable across OSes.
263
+ *
264
+ * @param {string} p
265
+ * @returns {string}
266
+ */
267
+ export function toPosix(p) {
268
+ return String(p || "").split(/[\\/]/).join(POSIX_SEP);
269
+ }
270
+
271
+ /**
272
+ * Return true if `file` matches the persona rule: includes any of the
273
+ * include substrings AND (extensions empty OR matches an extension) AND
274
+ * excludes none of the exclude substrings.
275
+ *
276
+ * @param {string} file - POSIX-normalized path.
277
+ * @param {object} rule
278
+ * @param {string[]} rule.include
279
+ * @param {string[]} rule.exclude
280
+ * @param {string[]} rule.extensions
281
+ * @returns {boolean}
282
+ */
283
+ export function matchesRule(file, rule) {
284
+ if (!rule) return false;
285
+ const include = rule.include || [];
286
+ const exclude = rule.exclude || [];
287
+ const extensions = rule.extensions || [];
288
+
289
+ for (const pattern of exclude) {
290
+ if (file.includes(pattern)) return false;
291
+ }
292
+
293
+ let includeMatch = false;
294
+ for (const pattern of include) {
295
+ if (file.includes(pattern)) {
296
+ includeMatch = true;
297
+ break;
298
+ }
299
+ }
300
+ if (!includeMatch) return false;
301
+
302
+ if (extensions.length === 0) return true;
303
+
304
+ for (const ext of extensions) {
305
+ if (file.endsWith(ext)) return true;
306
+ }
307
+
308
+ // Files with no extension (e.g. Dockerfile, Makefile, Procfile) should
309
+ // still match when the rule's include pattern explicitly names them —
310
+ // the include hit already proved domain relevance.
311
+ const basename = file.split(POSIX_SEP).pop() || "";
312
+ if (!basename.includes(".")) {
313
+ for (const pattern of include) {
314
+ if (basename === pattern || basename.includes(pattern)) return true;
315
+ }
316
+ }
317
+ return false;
318
+ }
319
+
320
+ /**
321
+ * Simple heuristic scoring for fallback ranking. Higher = more risk-
322
+ * surface-like. Path segments known to harbor crosscutting concerns
323
+ * (auth/server/api/middleware/index/main) score higher.
324
+ *
325
+ * @param {string} file
326
+ * @returns {number}
327
+ */
328
+ export function scoreRiskSurface(file) {
329
+ let score = 0;
330
+ for (const hint of RISK_SURFACE_HINTS) {
331
+ if (file.includes(hint)) score += 10;
332
+ }
333
+ // Prefer shallower files (closer to repo root → more likely entry points)
334
+ const depth = file.split(POSIX_SEP).length;
335
+ score += Math.max(0, 10 - depth);
336
+ return score;
337
+ }
338
+
339
+ /**
340
+ * Route the full file list to each persona's queue.
341
+ *
342
+ * @param {object} params
343
+ * @param {string[]} params.files - All candidate files (relative POSIX paths or raw).
344
+ * @param {string[]} params.personas - Persona IDs to route to.
345
+ * @param {object} [params.rules] - Custom rule map (defaults to INVESTOR_DD_PERSONA_RULES).
346
+ * @param {number} [params.fallbackCap] - Max fallback files when rule yields 0 (default 20).
347
+ * @returns {Record<string, string[]>} - { personaId: filesInScope[] }
348
+ */
349
+ export function routeFilesToPersonas({
350
+ files = [],
351
+ personas = [],
352
+ rules = INVESTOR_DD_PERSONA_RULES,
353
+ fallbackCap = DEFAULT_FALLBACK_CAP,
354
+ } = {}) {
355
+ const normalized = files.map(toPosix);
356
+ const routing = {};
357
+
358
+ // Pre-compute fallback list once: risk-surface-sorted, capped.
359
+ const fallbackPool = [...normalized]
360
+ .sort((a, b) => scoreRiskSurface(b) - scoreRiskSurface(a))
361
+ .slice(0, fallbackCap);
362
+
363
+ for (const personaId of personas) {
364
+ const rule = rules[personaId];
365
+ if (!rule) {
366
+ routing[personaId] = [];
367
+ continue;
368
+ }
369
+
370
+ const matched = normalized.filter((f) => matchesRule(f, rule));
371
+ routing[personaId] = matched.length > 0 ? matched : [...fallbackPool];
372
+ }
373
+
374
+ return routing;
375
+ }
376
+
377
+ /**
378
+ * Produce a dense coverage summary suitable for persisting as part of a
379
+ * run's `plan.json` artifact.
380
+ *
381
+ * @param {Record<string, string[]>} routing
382
+ * @returns {{ totalFilesByPersona: Record<string, number>, uniqueFiles: number, dedupIndex: Record<string, string[]> }}
383
+ */
384
+ export function summarizeRouting(routing) {
385
+ const totalFilesByPersona = {};
386
+ const fileToPersonas = new Map();
387
+
388
+ for (const [personaId, files] of Object.entries(routing || {})) {
389
+ totalFilesByPersona[personaId] = files.length;
390
+ for (const file of files) {
391
+ if (!fileToPersonas.has(file)) fileToPersonas.set(file, []);
392
+ fileToPersonas.get(file).push(personaId);
393
+ }
394
+ }
395
+
396
+ const dedupIndex = {};
397
+ for (const [file, personas] of fileToPersonas.entries()) {
398
+ dedupIndex[file] = [...personas];
399
+ }
400
+
401
+ return {
402
+ totalFilesByPersona,
403
+ uniqueFiles: fileToPersonas.size,
404
+ dedupIndex,
405
+ };
406
+ }