supipowers 1.5.3 → 2.0.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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -47,10 +47,22 @@ export function isBashSearchCommand(command: unknown): boolean {
47
47
  return BASH_SEARCH_PATTERNS.some((p) => p.test(command));
48
48
  }
49
49
 
50
- /** Check if a Read call is a full-file read (no limit/offset/sel = likely analysis, not edit prep) */
50
+ /** Check if a Read call is a full-file read (no limit/offset/path selector = likely analysis, not edit prep) */
51
51
  export function isFullFileRead(input: Record<string, unknown> | undefined): boolean {
52
52
  if (!input) return true;
53
- return input.limit == null && input.offset == null && input.sel == null;
53
+ return input.limit == null && input.offset == null && !hasEmbeddedReadSelector(input);
54
+ }
55
+
56
+ const READ_PATH_SELECTOR_RE = /:(?:raw|\d+(?:[-+]\d+)?|L\d+(?:-L?\d+|\+L?\d+)?)$/i;
57
+
58
+ function getReadPath(input: Record<string, unknown>): string | null {
59
+ const path = input.path ?? input.file_path;
60
+ return typeof path === "string" && path.length > 0 ? path : null;
61
+ }
62
+
63
+ function hasEmbeddedReadSelector(input: Record<string, unknown>): boolean {
64
+ const path = getReadPath(input);
65
+ return path != null && READ_PATH_SELECTOR_RE.test(path);
54
66
  }
55
67
 
56
68
  /** Block result returned by routing functions */
@@ -63,33 +75,35 @@ export interface BlockResult {
63
75
  export function routeToolCall(
64
76
  toolName: string,
65
77
  input: Record<string, unknown> | undefined,
66
- _status: ContextModeStatus,
78
+ status: ContextModeStatus,
67
79
  options: { enforceRouting: boolean; blockHttpCommands: boolean },
68
80
  ): BlockResult | undefined {
69
- // Native tools are always available — no availability check needed.
81
+ const searchReplacement = getSearchReplacement(status);
82
+ const shellSearchReplacement = getShellSearchReplacement(status);
83
+ const fetchReplacement = status.tools.ctxFetchAndIndex ? "ctx_fetch_and_index" : null;
84
+ const bashHttpReplacement = getBashHttpReplacement(status);
70
85
 
71
- // Grep → block, redirect to ctx_search
72
- if (options.enforceRouting && toolName === "grep") {
86
+ // Search → block only when an active search/shell replacement exists.
87
+ if (options.enforceRouting && toolName === "search" && searchReplacement) {
73
88
  return {
74
89
  block: true,
75
- reason:
76
- 'Use ctx_search(queries: ["<pattern>"]) or ctx_batch_execute instead of Grep. ' +
77
- "Results are indexed and compressed to save context window.",
90
+ reason: formatSearchReplacementReason(searchReplacement),
78
91
  };
79
92
  }
80
93
 
81
- // Find/Glob → block, redirect to ctx_execute or ctx_batch_execute
82
- if (options.enforceRouting && toolName === "find") {
94
+ // Find/Glob → block only when an active shell/search replacement exists.
95
+ if (options.enforceRouting && toolName === "find" && shellSearchReplacement) {
83
96
  return {
84
97
  block: true,
85
98
  reason:
86
- 'Use ctx_execute(language: "shell", code: "find ...") or ctx_batch_execute instead of Find/Glob. ' +
87
- "Results are indexed and compressed to save context window.",
99
+ shellSearchReplacement === "ctx_execute"
100
+ ? 'Use ctx_execute(language: "shell", code: "find ...") or ctx_batch_execute instead of Find/Glob. Results are indexed and compressed to save context window.'
101
+ : "Use ctx_batch_execute instead of Find/Glob. Results are indexed and compressed to save context window.",
88
102
  };
89
103
  }
90
104
 
91
- // Fetch/WebFetch → block, redirect to ctx_fetch_and_index
92
- if (toolName === "fetch" || toolName === "web_fetch") {
105
+ // Fetch/WebFetch → block only when ctx_fetch_and_index is active.
106
+ if ((toolName === "fetch" || toolName === "web_fetch") && fetchReplacement) {
93
107
  return {
94
108
  block: true,
95
109
  reason:
@@ -102,26 +116,85 @@ export function routeToolCall(
102
116
  if (toolName === "bash") {
103
117
  const command = input?.command;
104
118
 
105
- // Bash search commands → block, redirect to ctx_execute
106
- if (options.enforceRouting && isBashSearchCommand(command)) {
119
+ // Bash search commands → block only when an active shell/search replacement exists.
120
+ if (options.enforceRouting && isBashSearchCommand(command) && shellSearchReplacement) {
107
121
  return {
108
122
  block: true,
109
123
  reason:
110
- 'Use ctx_execute(language: "shell", code: "<command>") instead of Bash for search commands. ' +
111
- "For multiple commands, use ctx_batch_execute. Results stay in sandbox and are auto-indexed.",
124
+ shellSearchReplacement === "ctx_execute"
125
+ ? 'Use ctx_execute(language: "shell", code: "<command>") instead of Bash for search commands. For multiple commands, use ctx_batch_execute. Results stay in sandbox and are auto-indexed.'
126
+ : "Use ctx_batch_execute instead of Bash for search commands. Results stay in sandbox and are auto-indexed.",
112
127
  };
113
128
  }
114
129
 
115
- // Bash HTTP commands → block, redirect to ctx_fetch_and_index
116
- if (options.blockHttpCommands && isHttpCommand(command)) {
130
+ // Bash HTTP commands → block only when an active HTTP replacement exists.
131
+ if (options.blockHttpCommands && isHttpCommand(command) && bashHttpReplacement) {
117
132
  return {
118
133
  block: true,
119
- reason:
120
- "Use ctx_fetch_and_index instead of curl/wget. " +
121
- "It fetches the URL, indexes the content, and returns a compressed summary.",
134
+ reason: formatBashHttpReplacementReason(bashHttpReplacement),
122
135
  };
123
136
  }
124
137
  }
125
138
 
126
139
  return undefined;
140
+ }
141
+
142
+ function getSearchReplacement(status: ContextModeStatus): "ctx_search" | "ctx_batch_execute" | "ctx_execute" | null {
143
+ if (status.tools.ctxSearch) return "ctx_search";
144
+ if (status.tools.ctxBatchExecute) return "ctx_batch_execute";
145
+ if (status.tools.ctxExecute) return "ctx_execute";
146
+ return null;
147
+ }
148
+
149
+ function getShellSearchReplacement(status: ContextModeStatus): "ctx_execute" | "ctx_batch_execute" | null {
150
+ if (status.tools.ctxExecute) return "ctx_execute";
151
+ if (status.tools.ctxBatchExecute) return "ctx_batch_execute";
152
+ return null;
153
+ }
154
+
155
+ function getBashHttpReplacement(status: ContextModeStatus): "ctx_fetch_and_index" | "ctx_execute" | null {
156
+ if (status.tools.ctxFetchAndIndex) return "ctx_fetch_and_index";
157
+ if (status.tools.ctxExecute) return "ctx_execute";
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Native host tools that are fully shadowed by an active ctx_* replacement.
163
+ *
164
+ * When `enforceRouting` is on, these tools should be hidden from the model's
165
+ * active tool catalog (via `setActiveTools`) so the LLM never tries to call
166
+ * them only to receive a routing-block error. The `routeToolCall` runtime
167
+ * block remains as a safety net for hosts that cannot filter the tool list.
168
+ *
169
+ * Bash is intentionally NOT included: it is needed for non-search shell work,
170
+ * and `routeToolCall` already blocks only the search/HTTP subset.
171
+ */
172
+ export function getShadowedNativeTools(status: ContextModeStatus): string[] {
173
+ const shadowed: string[] = [];
174
+ if (getSearchReplacement(status)) shadowed.push("search");
175
+ if (getShellSearchReplacement(status)) shadowed.push("find");
176
+ if (status.tools.ctxFetchAndIndex) {
177
+ shadowed.push("fetch", "web_fetch");
178
+ }
179
+ return shadowed;
180
+ }
181
+
182
+ function formatSearchReplacementReason(replacement: "ctx_search" | "ctx_batch_execute" | "ctx_execute"): string {
183
+ if (replacement === "ctx_search") {
184
+ return 'Use active ctx_search(queries: ["<pattern>"]) instead of Search. Results are indexed and compressed to save context window.';
185
+ }
186
+ if (replacement === "ctx_batch_execute") {
187
+ return "Use active ctx_batch_execute instead of Search. Results are indexed and compressed to save context window.";
188
+ }
189
+ return 'Use active ctx_execute(language: "shell", code: "grep ...") instead of Search. Results stay in sandbox to save context window.';
190
+ }
191
+
192
+ function formatBashHttpReplacementReason(replacement: "ctx_fetch_and_index" | "ctx_execute"): string {
193
+ if (replacement === "ctx_fetch_and_index") {
194
+ return (
195
+ "Use ctx_fetch_and_index instead of curl/wget. " +
196
+ "It fetches the URL, indexes the content, and returns a compressed summary."
197
+ );
198
+ }
199
+ return 'Use ctx_execute(language: "shell", code: "<http request>") instead of Bash HTTP commands. Only printed summaries enter context.';
127
200
  }
@@ -5,10 +5,14 @@ export interface LanguageRunner {
5
5
  compileCmd?: (srcPath: string, outPath: string) => string[];
6
6
  }
7
7
 
8
+ function getPythonBinary(): string[] {
9
+ return process.platform === "win32" ? ["python"] : ["python3"];
10
+ }
11
+
8
12
  const RUNNERS: Record<string, LanguageRunner> = {
9
13
  javascript: { binary: ["bun", "run"], fileExt: ".js" },
10
14
  typescript: { binary: ["bun", "run"], fileExt: ".ts" },
11
- python: { binary: ["python3"], fileExt: ".py" },
15
+ python: { binary: getPythonBinary(), fileExt: ".py" },
12
16
  shell: { binary: ["bash"], fileExt: ".sh" },
13
17
  ruby: { binary: ["ruby"], fileExt: ".rb" },
14
18
  go: { binary: ["go", "run"], fileExt: ".go" },
@@ -19,6 +19,14 @@ function escapeXML(str: string): string {
19
19
  .replace(/'/g, "&apos;");
20
20
  }
21
21
 
22
+ function latestFirst(events: TrackedEvent[]): TrackedEvent[] {
23
+ return [...events].sort((a, b) => {
24
+ const byTimestamp = b.timestamp - a.timestamp;
25
+ if (byTimestamp !== 0) return byTimestamp;
26
+ return (b.id ?? 0) - (a.id ?? 0);
27
+ });
28
+ }
29
+
22
30
  interface SnapshotOpts {
23
31
  compactCount?: number;
24
32
  searchTool?: string;
@@ -81,22 +89,47 @@ function buildReferenceSnapshot(eventStore: EventStore, sessionId: string, opts:
81
89
  }
82
90
 
83
91
  // --- files ---
84
- const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
92
+ const fileEvents = latestFirst(eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 }));
85
93
  if (fileEvents.length > 0) {
86
94
  const edited = new Set<string>();
87
95
  const read = new Set<string>();
96
+ const modSeen = new Set<string>();
97
+ const readSeen = new Set<string>();
98
+ let maskedStale = 0;
88
99
  for (const f of fileEvents) {
89
100
  const data = safeParse(f.data);
90
101
  const p = typeof data?.path === "string" ? data.path : null;
91
102
  if (!p) continue;
92
- if (data?.op === "edit" || data?.op === "write") edited.add(p);
93
- else if (data?.op === "read") read.add(p);
103
+ const op = data?.op;
104
+ const sourceKey = typeof data?.sourceHash === "string" ? data.sourceHash : p.replace(/\\/g, "/");
105
+ if (op === "edit" || op === "write") {
106
+ if (modSeen.has(sourceKey)) {
107
+ maskedStale += 1;
108
+ continue;
109
+ }
110
+ modSeen.add(sourceKey);
111
+ edited.add(p);
112
+ } else if (op === "read") {
113
+ if (modSeen.has(sourceKey)) {
114
+ maskedStale += 1;
115
+ continue;
116
+ }
117
+ if (readSeen.has(sourceKey)) {
118
+ maskedStale += 1;
119
+ continue;
120
+ }
121
+ readSeen.add(sourceKey);
122
+ read.add(p);
123
+ }
94
124
  }
125
+ // Modifications dominate: do not double-list a path that was both edited and read.
126
+ for (const p of edited) read.delete(p);
95
127
  if (edited.size > 0 || read.size > 0) {
96
128
  sections.push("");
97
- sections.push(` <files count="${edited.size + read.size}">`);
129
+ sections.push(` <files count="${edited.size + read.size}" stale_masked="${maskedStale}">`);
98
130
  if (edited.size > 0) sections.push(` Edited: ${[...edited].map(escapeXML).join(", ")}`);
99
131
  if (read.size > 0) sections.push(` Read: ${[...read].map(escapeXML).join(", ")}`);
132
+ if (maskedStale > 0) sections.push(` Masked stale observations: ${maskedStale}`);
100
133
  const queryPaths = [...edited, ...read].slice(0, 5);
101
134
  sections.push(` For full details:`);
102
135
  sections.push(` ctx_search(queries: [${queryPaths.map((p) => `"${escapeXML(p)}"`).join(", ")}], source: "session-events")`);
@@ -288,18 +321,28 @@ function buildFallbackSnapshot(eventStore: EventStore, sessionId: string): strin
288
321
  }
289
322
 
290
323
  // Files modified (write/edit only, deduplicated)
291
- const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
324
+ const fileEvents = latestFirst(eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 }));
292
325
  const modifiedPaths = new Set<string>();
326
+ const seenSources = new Set<string>();
327
+ let maskedStaleFiles = 0;
293
328
  for (const f of fileEvents) {
294
329
  const data = safeParse(f.data);
295
- if (data?.op === "edit" || data?.op === "write") {
296
- if (typeof data.path === "string") modifiedPaths.add(data.path);
330
+ const p = typeof data?.path === "string" ? data.path : null;
331
+ if (!p) continue;
332
+ if (data?.op !== "edit" && data?.op !== "write") continue;
333
+ const sourceKey = typeof data?.sourceHash === "string" ? data.sourceHash : p.replace(/\\/g, "/");
334
+ if (seenSources.has(sourceKey)) {
335
+ maskedStaleFiles += 1;
336
+ continue;
297
337
  }
338
+ seenSources.add(sourceKey);
339
+ modifiedPaths.add(p);
298
340
  }
299
341
  if (modifiedPaths.size > 0) {
300
342
  sections.push(" <files_modified>");
301
343
  const paths = [...modifiedPaths].slice(0, CAPS.files);
302
344
  for (const p of paths) sections.push(` - ${escapeXML(p)}`);
345
+ if (maskedStaleFiles > 0) sections.push(` - stale observations masked: ${maskedStaleFiles}`);
303
346
  sections.push(" </files_modified>");
304
347
  }
305
348
 
@@ -346,11 +389,63 @@ function safeParse(json: string): Record<string, unknown> | null {
346
389
  function extractTaskContent(data: Record<string, unknown> | null): string | null {
347
390
  if (!data?.input) return null;
348
391
  const input = data.input as Record<string, unknown>;
349
- if (Array.isArray(input.ops)) {
350
- const ops = input.ops as Array<{ content?: string; op?: string }>;
351
- return ops.map((o) => `${o.op ?? "task"}: ${o.content ?? ""}`).join("; ");
392
+ if (!Array.isArray(input.ops)) return JSON.stringify(input).slice(0, 100);
393
+
394
+ const parts: string[] = [];
395
+ for (const rawOp of input.ops as Array<Record<string, unknown>>) {
396
+ if (!rawOp || typeof rawOp !== "object") continue;
397
+ const verb = typeof rawOp.op === "string" ? rawOp.op : "task";
398
+
399
+ if (verb === "init" && Array.isArray(rawOp.list)) {
400
+ for (const phase of rawOp.list as Array<Record<string, unknown>>) {
401
+ const items = Array.isArray(phase?.items) ? phase.items : [];
402
+ for (const item of items) {
403
+ if (typeof item === "string" && item) parts.push(`init: ${item}`);
404
+ else if (item && typeof item === "object" && typeof (item as { label?: unknown }).label === "string") {
405
+ parts.push(`init: ${(item as { label: string }).label}`);
406
+ }
407
+ }
408
+ }
409
+ } else if (verb === "replace" && Array.isArray(rawOp.phases)) {
410
+ // Legacy `todo_write` shape (pre-14.5.11): `op:"replace", phases:[{ name, tasks:[{ content }] }]`.
411
+ // Persisted event rows still carry this shape until 7-day retention expires; we keep
412
+ // read-side compatibility here so resume snapshots remain truthful for those rows.
413
+ for (const phase of rawOp.phases as Array<Record<string, unknown>>) {
414
+ const tasks = Array.isArray(phase?.tasks) ? phase.tasks : [];
415
+ for (const task of tasks) {
416
+ const content = task && typeof task === "object" && typeof (task as { content?: unknown }).content === "string"
417
+ ? (task as { content: string }).content
418
+ : "";
419
+ if (content) parts.push(`replace: ${content}`);
420
+ }
421
+ }
422
+ } else if (verb === "append" && Array.isArray(rawOp.items)) {
423
+ for (const item of rawOp.items) {
424
+ if (typeof item === "string" && item) parts.push(`append: ${item}`);
425
+ else if (item && typeof item === "object" && typeof (item as { label?: unknown }).label === "string") {
426
+ // Legacy append items shaped as objects with `label`.
427
+ parts.push(`append: ${(item as { label: string }).label}`);
428
+ }
429
+ }
430
+ } else if (verb === "note") {
431
+ const text = typeof rawOp.text === "string" ? rawOp.text : "";
432
+ if (text) parts.push(`note: ${text}`);
433
+ } else {
434
+ const legacyContent = typeof rawOp.content === "string" ? rawOp.content : "";
435
+ if (legacyContent) {
436
+ parts.push(`${verb}: ${legacyContent}`);
437
+ continue;
438
+ }
439
+
440
+ const target = (typeof rawOp.task === "string" && rawOp.task)
441
+ || (typeof rawOp.phase === "string" && rawOp.phase)
442
+ || "all";
443
+ parts.push(`${verb}: ${target}`);
444
+ }
352
445
  }
353
- return JSON.stringify(input).slice(0, 100);
446
+
447
+ // Preserve the existing 100-char cap so prompts stay bounded.
448
+ return parts.length > 0 ? parts.join("; ").slice(0, 100) : null;
354
449
  }
355
450
 
356
451
  function formatErrorSummary(data: Record<string, unknown> | null): string | null {
@@ -0,0 +1,173 @@
1
+ // src/context-mode/source-hash.ts
2
+ //
3
+ // Compute the per-row `unique_source_hash` for L1 metrics. The hash exists
4
+ // to power "unique-source share" without storing raw paths or commands. Two
5
+ // invariants:
6
+ //
7
+ // 1. The same logical source produces the same hash regardless of platform
8
+ // separator or relative-vs-absolute spelling, given the same `cwd`.
9
+ // 2. The same path under two different `projectSlug` values produces
10
+ // different hashes, so a hash is never a cross-project identifier.
11
+ //
12
+ // Tool inputs are never copied verbatim; only canonicalized prefixes flow
13
+ // into the hash input.
14
+
15
+ import { createHash } from "node:crypto";
16
+
17
+ import { canonicalToolName } from "./tool-name.js";
18
+
19
+ export interface UniqueSourceHashOpts {
20
+ tool: string;
21
+ input: Record<string, unknown> | undefined;
22
+ cwd: string;
23
+ projectSlug: string;
24
+ }
25
+
26
+ /**
27
+ * Detect whether a path string is absolute on either POSIX or Windows.
28
+ *
29
+ * Recognized absolute forms:
30
+ * - leading `/` (POSIX)
31
+ * - drive-letter prefix `[A-Za-z]:` followed by `\` or `/` (Windows)
32
+ * - UNC prefix `\\` or `//` (network share / WSL `\\?\` namespace)
33
+ */
34
+ function isAbsolutePath(p: string): boolean {
35
+ if (p.length === 0) return false;
36
+ if (p[0] === "/" || p[0] === "\\") {
37
+ // Includes both POSIX `/x` and Windows `\\server\share`
38
+ return true;
39
+ }
40
+ // Drive-letter Windows path: `C:\…` or `c:/…`
41
+ if (/^[A-Za-z]:[\\/]/.test(p)) return true;
42
+ return false;
43
+ }
44
+
45
+ /** Normalize separators and collapse `.` / `..` segments using POSIX rules. */
46
+ function toPosix(p: string): string {
47
+ return p.replace(/\\/g, "/");
48
+ }
49
+
50
+ function stripCurrentDirPrefixBeforeWindowsAbsolute(p: string): string {
51
+ return p.replace(/^\.[\\/]+(?=[A-Za-z]:[\\/])/, "");
52
+ }
53
+
54
+
55
+ /** Resolve `pathInput` to a canonical absolute POSIX form. */
56
+ export function canonicalizeSourcePath(pathInput: string, cwd: string): string {
57
+ const normalizedInput = stripCurrentDirPrefixBeforeWindowsAbsolute(pathInput);
58
+ const posix = toPosix(normalizedInput);
59
+ if (isAbsolutePath(normalizedInput)) {
60
+ return collapsePosix(posix);
61
+ }
62
+ const cwdPosix = toPosix(cwd);
63
+ return collapsePosix(joinPosix(cwdPosix, posix));
64
+ }
65
+
66
+ /** Lightweight POSIX `path.normalize` — segment-by-segment without depending on
67
+ * Node's platform-specific `path.posix.normalize` differences. */
68
+ function collapsePosix(p: string): string {
69
+ const isAbs = p.startsWith("/");
70
+ const parts = p.split("/").filter(Boolean);
71
+ const stack: string[] = [];
72
+ for (const part of parts) {
73
+ if (part === ".") continue;
74
+ if (part === "..") {
75
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
76
+ stack.pop();
77
+ } else if (!isAbs) {
78
+ stack.push("..");
79
+ }
80
+ continue;
81
+ }
82
+ stack.push(part);
83
+ }
84
+ const joined = stack.join("/");
85
+ return isAbs ? "/" + joined : joined || ".";
86
+ }
87
+
88
+ function joinPosix(left: string, right: string): string {
89
+ if (left === "") return right;
90
+ if (left.endsWith("/")) return left + right;
91
+ return left + "/" + right;
92
+ }
93
+
94
+ function sha256Hex(input: string): string {
95
+ return createHash("sha256").update(input).digest("hex");
96
+ }
97
+
98
+ /**
99
+ * Truncate a bash command to a stable, leak-free identifier:
100
+ * - split on whitespace, take the first 4 tokens, lowercase them, rejoin.
101
+ *
102
+ * Anything past the 4th token (env exports, secrets, file paths, command
103
+ * arguments) is dropped. The first 4 tokens give us enough resolution to
104
+ * distinguish `bun run typecheck` from `bun run test` while never copying
105
+ * the rest of the command into the hash input.
106
+ */
107
+ function truncateBashCommand(command: string): string {
108
+ return command
109
+ .trim()
110
+ .split(/\s+/)
111
+ .slice(0, 4)
112
+ .map((token) => token.toLowerCase())
113
+ .join(" ");
114
+ }
115
+
116
+ /**
117
+ * Produce the unique-source hash, or `null` for tools whose inputs do not
118
+ * map to a stable source identifier.
119
+ */
120
+ export function uniqueSourceHash(opts: UniqueSourceHashOpts): string | null {
121
+ const { tool, input, cwd, projectSlug } = opts;
122
+ const canonical = canonicalToolName(tool);
123
+
124
+ switch (canonical) {
125
+ case "read":
126
+ case "open": {
127
+ const p = typeof input?.path === "string" ? input.path : null;
128
+ if (!p) return null;
129
+ const absolute = canonicalizeSourcePath(p, cwd);
130
+ return sha256Hex(`file:${absolute}:${projectSlug}`);
131
+ }
132
+ case "search": {
133
+ const paths = Array.isArray(input?.paths)
134
+ ? (input.paths as unknown[]).filter((p): p is string => typeof p === "string")
135
+ : [];
136
+ const pattern = typeof input?.pattern === "string" ? input.pattern : "";
137
+ if (paths.length === 0) {
138
+ // Pattern-only search (no scope): keep a deterministic salt so distinct
139
+ // pattern-only calls dedup correctly per-project.
140
+ return sha256Hex(`search:${pattern}:${projectSlug}`);
141
+ }
142
+ // Order matters: OMP runs each path under root-level resolution, so [a,b] != [b,a].
143
+ // Use SOH (\u0001) as the joiner — it cannot appear in a path on any platform.
144
+ const joined = paths.map((p) => canonicalizeSourcePath(p, cwd)).join("\u0001");
145
+ return sha256Hex(`search:${joined}:${pattern}:${projectSlug}`);
146
+ }
147
+ case "find": {
148
+ const paths = Array.isArray(input?.paths)
149
+ ? (input.paths as unknown[]).filter((p): p is string => typeof p === "string")
150
+ : [];
151
+ if (paths.length === 0) {
152
+ // Defensive: 14.7.x requires `paths`, so this should not happen.
153
+ return sha256Hex(`find::${projectSlug}`);
154
+ }
155
+ const joined = paths.map((p) => canonicalizeSourcePath(p, cwd)).join("\u0001");
156
+ return sha256Hex(`find:${joined}:${projectSlug}`);
157
+ }
158
+ case "edit":
159
+ case "write": {
160
+ const p = typeof input?.path === "string" ? input.path : null;
161
+ if (!p) return null;
162
+ const absolute = canonicalizeSourcePath(p, cwd);
163
+ return sha256Hex(`file:${absolute}:${projectSlug}`);
164
+ }
165
+ case "bash": {
166
+ const command = typeof input?.command === "string" ? input.command : "";
167
+ const truncated = truncateBashCommand(command);
168
+ return sha256Hex(`bash:${truncated}:${projectSlug}`);
169
+ }
170
+ default:
171
+ return null;
172
+ }
173
+ }
@@ -0,0 +1,11 @@
1
+ // src/context-mode/tool-name.ts
2
+ //
3
+ // OMP 14.3.0 renamed `read` to `open` at the wire level while keeping `read`
4
+ // as a legacy alias. Internally we keep using the historical `read` key so
5
+ // every dispatch site only has to learn one name. Apply this normalizer to
6
+ // the raw `tool_call`/`tool_result` `toolName` before any switch.
7
+
8
+ /** Map an OMP tool name to its canonical internal key. */
9
+ export function canonicalToolName(name: string): string {
10
+ return name === "open" ? "read" : name;
11
+ }