supipowers 1.5.2 → 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 +149 -45
  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 +19 -9
  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 +298 -127
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +115 -14
  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 +11 -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 +1401 -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
@@ -1,7 +1,7 @@
1
1
  // src/context-mode/hooks.ts
2
2
  import type { Platform } from "../platform/types.js";
3
3
  import type { SupipowersConfig } from "../types.js";
4
- import { compressToolResult } from "./compressor.js";
4
+ import { OMP_MINIMIZER_FOOTER_RE, runEmissionPipeline } from "./compressor.js";
5
5
  import { detectContextMode, type ContextModeStatus } from "./detector.js";
6
6
  import { EventStore } from "./event-store.js";
7
7
  import { extractEvents, extractPromptEvents } from "./event-extractor.js";
@@ -9,10 +9,27 @@ import { buildResumeSnapshot } from "./snapshot-builder.js";
9
9
  import { routeToolCall } from "./routing.js";
10
10
  import { KnowledgeStore } from "./knowledge/store.js";
11
11
  import { registerContextModeTools } from "./tools.js";
12
+ import { MetricsStore, __setMetricsStoreForTest, _resetMetricsStoreCache } from "./metrics-store.js";
13
+ import { _resetAutoIndexAttempts, _forgetAutoIndexSession } from "./tools.js";
14
+ import { CacheStore } from "./cache-store.js";
15
+ import { MemoryStore, _setMemoryStoreRef } from "./memory-store.js";
16
+ import { toMetricRow } from "./metrics-recorder.js";
17
+ import { combinedTextOf, createDedupState, maybeSubstitute, type DedupState } from "./dedup.js";
18
+ import { uniqueSourceHash } from "./source-hash.js";
19
+ import { basename } from "node:path";
20
+ import { getProjectStateDir } from "../workspace/state-paths.js";
12
21
  import { createHash } from "node:crypto";
13
- import { readFileSync, mkdirSync } from "node:fs";
14
- import { join, dirname } from "node:path";
15
- import { fileURLToPath } from "node:url";
22
+ import { mkdirSync, readFileSync, statSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { registerUltraPlanHookBridge } from "../ultraplan/runtime/hook-bridge.js";
25
+ import { getProjectStatePath } from "../workspace/state-paths.js";
26
+ import { compressToolResultWithLLM } from "./compressor.js";
27
+ import { COMPACTION_SUMMARIZER_ACTION_ID } from "./model.js";
28
+ import { loadModelConfig } from "../config/model-config.js";
29
+ import { modelRegistry } from "../config/model-registry-instance.js";
30
+ import { createModelBridge, resolveModelForAction } from "../config/model-resolver.js";
31
+ import { extractFinalAssistantText } from "../ai/final-message.js";
32
+ import { normalizeSystemPromptBlocks } from "../platform/system-prompt.js";
16
33
 
17
34
  type SessionContextLike = {
18
35
  cwd?: string;
@@ -22,14 +39,56 @@ type SessionContextLike = {
22
39
  // Cached detection result
23
40
  let cachedStatus: ContextModeStatus | null = null;
24
41
 
25
- function loadRoutingSkill(): string | null {
26
- try {
27
- const __dirname = dirname(fileURLToPath(import.meta.url));
28
- const skillPath = join(__dirname, "..", "..", "skills", "context-mode", "SKILL.md");
29
- return readFileSync(skillPath, "utf-8");
30
- } catch {
31
- return null;
42
+ function buildActiveRoutingGuidance(status: ContextModeStatus): string | null {
43
+ if (!status.available) return null;
44
+
45
+ const activeTools = activeContextToolNames(status);
46
+ const rescueTools = activeTools.filter((tool) =>
47
+ tool === "ctx_execute" || tool === "ctx_search" || tool === "ctx_batch_execute",
48
+ );
49
+ const lines = [
50
+ "# supi-context-mode",
51
+ "Use active `ctx_*` tools shown in the tool catalog for high-output work; inactive ctx tools are intentionally unavailable this turn.",
52
+ `Active context-mode rescue tools: ${rescueTools.length > 0 ? rescueTools.join(", ") : "none"}.`,
53
+ "Routing blocks native tools only when the named replacement is active. If a specialized ctx tool is absent, use an active rescue tool or proceed with the native tool.",
54
+ ];
55
+
56
+ const searchGatherTools = ["ctx_search", "ctx_batch_execute"].filter((tool) =>
57
+ tool === "ctx_search" ? status.tools.ctxSearch : status.tools.ctxBatchExecute,
58
+ );
59
+ if (searchGatherTools.length > 0) {
60
+ lines.push(`For search/gather work, prefer active ${searchGatherTools.map((tool) => `\`${tool}\``).join(" or ")} over Search/Find outputs.`);
61
+ }
62
+ if (status.tools.ctxExecute) {
63
+ lines.push("Use active `ctx_execute` for shell/data processing that may emit large output.");
64
+ }
65
+ if (status.tools.ctxFetchAndIndex) {
66
+ lines.push("Use active `ctx_fetch_and_index` for URLs, curl/wget, Fetch/WebFetch, or web docs.");
32
67
  }
68
+ if (status.tools.ctxExecuteFile) {
69
+ lines.push("Use active `ctx_execute_file` for analysis-only large-file processing without loading the file into context.");
70
+ }
71
+ if (status.tools.ctxOpenCached) {
72
+ lines.push("Use active `ctx_open_cached` to read `cache://<sha>` handles in bounded slices via offset/limit.");
73
+ }
74
+
75
+ return lines.join("\n");
76
+ }
77
+
78
+ function activeContextToolNames(status: ContextModeStatus): string[] {
79
+ const names: string[] = [];
80
+ if (status.tools.ctxExecute) names.push("ctx_execute");
81
+ if (status.tools.ctxSearch) names.push("ctx_search");
82
+ if (status.tools.ctxBatchExecute) names.push("ctx_batch_execute");
83
+ if (status.tools.ctxExecuteFile) names.push("ctx_execute_file");
84
+ if (status.tools.ctxFetchAndIndex) names.push("ctx_fetch_and_index");
85
+ if (status.tools.ctxOpenCached) names.push("ctx_open_cached");
86
+ if (status.tools.ctxIndex) names.push("ctx_index");
87
+ if (status.tools.ctxStats) names.push("ctx_stats");
88
+ if (status.tools.ctxPurge) names.push("ctx_purge");
89
+ if (status.tools.ctxRepomap) names.push("ctx_repomap");
90
+ if (status.tools.ctxSymbol) names.push("ctx_symbol");
91
+ return names;
33
92
  }
34
93
 
35
94
  function resolveSessionCwd(ctx?: SessionContextLike): string {
@@ -48,8 +107,278 @@ function deriveSessionId(ctx?: SessionContextLike): string {
48
107
  return `session-${Date.now()}`;
49
108
  }
50
109
 
110
+ function textContentBytes(content: Array<{ type: string; text?: string }> | undefined): number {
111
+ if (!content) return 0;
112
+ return new TextEncoder().encode(combinedTextOf(content)).byteLength;
113
+ }
114
+
115
+ function isTextOnlyContent(content: Array<{ type: string; text?: string }>): boolean {
116
+ return content.every((entry) => entry.type === "text");
117
+ }
118
+
119
+ function pickMemoryBody(category: string, data: Record<string, unknown>): string | null {
120
+ switch (category) {
121
+ case "decision": {
122
+ const prompt = typeof data.prompt === "string" ? data.prompt.trim() : "";
123
+ return prompt ? `decision: ${prompt.slice(0, 240)}` : null;
124
+ }
125
+ case "task": {
126
+ const input = data.input as Record<string, unknown> | undefined;
127
+ if (!input) return null;
128
+ const text = JSON.stringify(input).slice(0, 240);
129
+ return text ? `task: ${text}` : null;
130
+ }
131
+ case "intent": {
132
+ const intent = typeof data.intent === "string" ? data.intent : null;
133
+ const prompt = typeof data.prompt === "string" ? data.prompt.slice(0, 200) : "";
134
+ if (!intent) return null;
135
+ return `intent ${intent}: ${prompt}`.trim();
136
+ }
137
+ case "rule": {
138
+ const file = typeof data.path === "string" ? data.path : null;
139
+ return file ? `rule loaded: ${file}` : null;
140
+ }
141
+ default:
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function memoryTypeFor(category: string): "observation" | "decision" | "task" {
147
+ switch (category) {
148
+ case "decision":
149
+ return "decision";
150
+ case "task":
151
+ return "task";
152
+ default:
153
+ return "observation";
154
+ }
155
+ }
156
+
157
+ function buildMemoryInjectionBlock(
158
+ memoryStore: MemoryStore | null,
159
+ sessionId: string,
160
+ config: { byteBudget: number; maxRows: number },
161
+ ): string | null {
162
+ if (!memoryStore) return null;
163
+ let rows;
164
+ try {
165
+ rows = memoryStore.retrieve({
166
+ sessionId,
167
+ byteBudget: config.byteBudget,
168
+ limit: config.maxRows,
169
+ });
170
+ } catch {
171
+ return null;
172
+ }
173
+ if (rows.length === 0) return null;
174
+ const lines = ["# Cross-session memory"];
175
+ for (const row of rows) {
176
+ lines.push(`- [${row.type}] ${row.body}`);
177
+ }
178
+ return lines.join("\n");
179
+ }
180
+
181
+ function buildFocusChainBlock(
182
+ eventStore: EventStore | null,
183
+ sessionId: string,
184
+ opts: { cadence: number; turnCount: number },
185
+ ): string | null {
186
+ if (!eventStore) return null;
187
+ // Cadence gate: turn 1 always injects; subsequent turns inject only every Nth.
188
+ // cadence < 1 is a config error guarded upstream by the schema; treat as 1
189
+ // here for defensive behavior.
190
+ const cadence = Math.max(1, Math.floor(opts.cadence));
191
+ const turnCount = Math.max(1, Math.floor(opts.turnCount));
192
+ if (turnCount !== 1 && turnCount % cadence !== 0) return null;
193
+ let events;
194
+ try {
195
+ events = eventStore.getEvents(sessionId, { categories: ["task"], limit: 1 });
196
+ } catch {
197
+ return null;
198
+ }
199
+ if (events.length === 0) return null;
200
+ let data: any;
201
+ try {
202
+ data = JSON.parse(events[0].data);
203
+ } catch {
204
+ return null;
205
+ }
206
+ const ops = (data?.input?.ops as Array<Record<string, unknown>> | undefined) ?? [];
207
+ const summary = ops
208
+ .map((op) => {
209
+ const verb = typeof op.op === "string" ? op.op : "";
210
+ const target = (typeof op.task === "string" && op.task)
211
+ || (typeof op.phase === "string" && op.phase)
212
+ || "";
213
+ if (!verb) return null;
214
+ return target ? `${verb}: ${target}` : verb;
215
+ })
216
+ .filter((line): line is string => Boolean(line));
217
+ if (summary.length === 0) return null;
218
+ return ["# Focus chain", ...summary.slice(0, 5).map((line) => `- ${line}`)].join("\n");
219
+ }
220
+
221
+ function readCompactOverride(paths: Platform["paths"], cwd: string): string | null {
222
+ try {
223
+ const filePath = paths.project(cwd, "compact.md");
224
+ const stat = statSync(filePath);
225
+ if (!stat.isFile()) return null;
226
+ if (stat.size > 32 * 1024) return null;
227
+ const text = readFileSync(filePath, "utf8");
228
+ return text.trim() || null;
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+
51
234
  function getSessionDbPath(platform: Platform, cwd: string): string {
52
- return join(platform.paths.project(cwd, "sessions"), "events.db");
235
+ return join(getProjectStatePath(platform.paths, cwd, "sessions"), "events.db");
236
+ }
237
+
238
+ function byteLengthOf(text: string): number {
239
+ return new TextEncoder().encode(text).byteLength;
240
+ }
241
+
242
+ /**
243
+ * Default per-call timeout for the compaction-time LLM summarizer. Compaction
244
+ * already runs synchronously from OMP's perspective; bounding the LLM step
245
+ * keeps it from extending compaction indefinitely on slow models or stuck
246
+ * streams. The deterministic snapshot is persisted before this runs, so a
247
+ * timeout fail is safe.
248
+ */
249
+ const COMPACTION_LLM_TIMEOUT_MS = 10_000;
250
+
251
+ function buildSummarizeCallback(
252
+ platform: Platform,
253
+ cwd: string,
254
+ modelId: string | undefined,
255
+ thinkingLevel: string | null,
256
+ timeoutMs: number,
257
+ ): (text: string, toolName: string) => Promise<string> {
258
+ return async (text: string, _toolName: string): Promise<string> => {
259
+ if (typeof platform.createAgentSession !== "function") {
260
+ // Platform stub or unavailable session API: degrade silently.
261
+ return "";
262
+ }
263
+ let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
264
+ try {
265
+ session = await platform.createAgentSession({
266
+ cwd,
267
+ ...(modelId ? { model: modelId } : {}),
268
+ thinkingLevel: thinkingLevel ?? null,
269
+ });
270
+ } catch (e) {
271
+ (platform as any).logger?.debug?.(
272
+ "supi-context-mode: createAgentSession unavailable for summarizer",
273
+ e,
274
+ );
275
+ return "";
276
+ }
277
+
278
+ let timer: ReturnType<typeof setTimeout> | null = null;
279
+ try {
280
+ const promptPromise = session.prompt(text, { expandPromptTemplates: false });
281
+ const timeoutPromise = new Promise<never>((_, reject) => {
282
+ timer = setTimeout(() => reject(new Error("compaction-summarizer timeout")), timeoutMs);
283
+ });
284
+ await Promise.race([promptPromise, timeoutPromise]);
285
+ const finalText = extractFinalAssistantText(session.state.messages);
286
+ return finalText ?? "";
287
+ } catch (e) {
288
+ (platform as any).logger?.debug?.("supi-context-mode: summarizer prompt failed", e);
289
+ return "";
290
+ } finally {
291
+ if (timer) clearTimeout(timer);
292
+ try {
293
+ await session.dispose();
294
+ } catch {
295
+ // best effort
296
+ }
297
+ }
298
+ };
299
+ }
300
+
301
+ interface SummarizeSnapshotOpts {
302
+ platform: Platform;
303
+ cwd: string;
304
+ sessionId: string;
305
+ snapshot: string;
306
+ eventCount: number;
307
+ eventStore: EventStore;
308
+ compressionThreshold: number;
309
+ llmThreshold: number;
310
+ }
311
+
312
+ /**
313
+ * Wrap the deterministic resume snapshot in a synthetic ToolResultEventLike
314
+ * and ask `compressToolResultWithLLM` to summarize it. On a successful
315
+ * non-empty replacement, overwrite the resume row. On any failure, the
316
+ * deterministic snapshot already persisted at the call site remains in place.
317
+ */
318
+ async function summarizeSnapshotIfBudget(opts: SummarizeSnapshotOpts): Promise<void> {
319
+ const modelConfig = (() => {
320
+ try {
321
+ return loadModelConfig(opts.platform.paths, opts.cwd);
322
+ } catch {
323
+ return null;
324
+ }
325
+ })();
326
+ if (!modelConfig) return;
327
+
328
+ const resolved = resolveModelForAction(
329
+ COMPACTION_SUMMARIZER_ACTION_ID,
330
+ modelRegistry,
331
+ modelConfig,
332
+ createModelBridge(opts.platform),
333
+ );
334
+
335
+ // No model resolved at all (no action override, no default, no role, no
336
+ // main session model) — skip silently. The deterministic snapshot stays.
337
+ if (!resolved.model) {
338
+ (opts.platform as any).logger?.debug?.(
339
+ "supi-context-mode: no model resolved for compaction-summarizer; skipping",
340
+ );
341
+ return;
342
+ }
343
+
344
+ const summarize = buildSummarizeCallback(
345
+ opts.platform,
346
+ opts.cwd,
347
+ resolved.model,
348
+ resolved.thinkingLevel,
349
+ COMPACTION_LLM_TIMEOUT_MS,
350
+ );
351
+
352
+ const syntheticEvent = {
353
+ toolName: "context-mode-snapshot",
354
+ input: {},
355
+ content: [{ type: "text", text: opts.snapshot }],
356
+ isError: false,
357
+ details: null,
358
+ };
359
+
360
+ const result = await compressToolResultWithLLM(
361
+ syntheticEvent,
362
+ opts.compressionThreshold,
363
+ opts.llmThreshold,
364
+ summarize,
365
+ );
366
+
367
+ if (!result || !result.content || result.content.length === 0) return;
368
+ const summarized = result.content
369
+ .map((entry) => (entry.type === "text" && typeof entry.text === "string" ? entry.text : ""))
370
+ .join("\n")
371
+ .trim();
372
+ if (!summarized) return;
373
+
374
+ try {
375
+ opts.eventStore.upsertResume(opts.sessionId, summarized, opts.eventCount);
376
+ } catch (e) {
377
+ (opts.platform as any).logger?.warn?.(
378
+ "supi-context-mode: failed to overwrite resume with summary",
379
+ e,
380
+ );
381
+ }
53
382
  }
54
383
 
55
384
  /** Register supi-context-mode hooks on the platform */
@@ -58,8 +387,17 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
58
387
 
59
388
  let eventStore: EventStore | null = null;
60
389
  let eventStorePath: string | null = null;
390
+ let metricsStore: MetricsStore | null = null;
391
+ let metricsStorePath: string | null = null;
392
+ let cacheStore: CacheStore | null = null;
393
+ let cacheStorePath: string | null = null;
394
+ let memoryStore: MemoryStore | null = null;
395
+ let memoryStorePath: string | null = null;
396
+ let knowledgeStore: KnowledgeStore | null = null;
397
+ let knowledgeStorePath: string | null = null;
61
398
  let sessionCwd = process.cwd();
62
399
  let sessionId = deriveSessionId();
400
+ let dedupState: DedupState = createDedupState();
63
401
 
64
402
  const ensureEventStore = (cwd: string): EventStore | null => {
65
403
  if (!config.contextMode.eventTracking) return null;
@@ -76,7 +414,7 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
76
414
  }
77
415
 
78
416
  try {
79
- mkdirSync(platform.paths.project(cwd, "sessions"), { recursive: true });
417
+ mkdirSync(getProjectStatePath(platform.paths, cwd, "sessions"), { recursive: true });
80
418
  eventStore = new EventStore(dbPath);
81
419
  eventStore.init();
82
420
  eventStore.pruneOldSessions(7);
@@ -92,44 +430,193 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
92
430
  }
93
431
  };
94
432
 
433
+ /** Mirror of ensureEventStore for the metrics sidecar; cwd-keyed.
434
+ * Closes any prior store when the cwd changes so we never write rows to
435
+ * the wrong project's metrics.db. */
436
+ const ensureMetricsStore = (cwd: string): MetricsStore | null => {
437
+ const dbPath = join(getProjectStatePath(platform.paths, cwd, "sessions"), "metrics.db");
438
+ if (metricsStore && metricsStorePath === dbPath) return metricsStore;
439
+
440
+ if (metricsStore) {
441
+ try {
442
+ metricsStore.close();
443
+ } catch {
444
+ // Best effort: we are about to reopen against the active project's metrics.db.
445
+ }
446
+ }
447
+
448
+ try {
449
+ mkdirSync(getProjectStatePath(platform.paths, cwd, "sessions"), { recursive: true });
450
+ const slug = basename(getProjectStateDir(platform.paths, cwd));
451
+ metricsStore = new MetricsStore({ dbPath, projectSlug: slug });
452
+ metricsStore.init();
453
+ metricsStorePath = dbPath;
454
+ __setMetricsStoreForTest(metricsStore);
455
+ return metricsStore;
456
+ } catch (e) {
457
+ metricsStore = null;
458
+ metricsStorePath = null;
459
+ __setMetricsStoreForTest(null);
460
+ (platform as any).logger?.error?.("supi-context-mode: failed to initialize metrics store", e);
461
+ return null;
462
+ }
463
+ };
464
+
465
+ const ensureCacheStore = (cwd: string): CacheStore | null => {
466
+ const sessionsDir = getProjectStatePath(platform.paths, cwd, "sessions");
467
+ const dbPath = join(sessionsDir, "cache.db");
468
+ const payloadRoot = join(sessionsDir, "cache-payloads");
469
+ if (cacheStore && cacheStorePath === dbPath) {
470
+ cacheStore.setMetricsRecorder(metricsStore, sessionId);
471
+ return cacheStore;
472
+ }
473
+
474
+ if (cacheStore) {
475
+ try {
476
+ cacheStore.close();
477
+ } catch {
478
+ // Best effort: we are about to reopen against the active project's cache.db.
479
+ }
480
+ }
481
+
482
+ try {
483
+ mkdirSync(sessionsDir, { recursive: true });
484
+ const slug = basename(getProjectStateDir(platform.paths, cwd));
485
+ cacheStore = new CacheStore({
486
+ dbPath,
487
+ payloadRoot,
488
+ projectSlug: slug,
489
+ metricsStore,
490
+ metricsSessionId: sessionId,
491
+ });
492
+ cacheStore.init();
493
+ cacheStorePath = dbPath;
494
+ _cacheStoreRef = cacheStore;
495
+ return cacheStore;
496
+ } catch (e) {
497
+ cacheStore = null;
498
+ cacheStorePath = null;
499
+ _cacheStoreRef = null;
500
+ (platform as any).logger?.error?.("supi-context-mode: failed to initialize cache store", e);
501
+ return null;
502
+ }
503
+ };
504
+
505
+ const ensureMemoryStore = (cwd: string): MemoryStore | null => {
506
+ if (!config.contextMode.memory.enabled) return null;
507
+ const sessionsDir = getProjectStatePath(platform.paths, cwd, "sessions");
508
+ const dbPath = join(sessionsDir, "memory.db");
509
+ if (memoryStore && memoryStorePath === dbPath) return memoryStore;
510
+
511
+ if (memoryStore) {
512
+ try { memoryStore.close(); } catch { /* best effort */ }
513
+ }
514
+
515
+ try {
516
+ mkdirSync(sessionsDir, { recursive: true });
517
+ const slug = basename(getProjectStateDir(platform.paths, cwd));
518
+ memoryStore = new MemoryStore({ dbPath, projectSlug: slug });
519
+ memoryStore.init();
520
+ memoryStorePath = dbPath;
521
+ _setMemoryStoreRef(memoryStore);
522
+ return memoryStore;
523
+ } catch (e) {
524
+ memoryStore = null;
525
+ memoryStorePath = null;
526
+ _setMemoryStoreRef(null);
527
+ (platform as any).logger?.error?.("supi-context-mode: failed to initialize memory store", e);
528
+ return null;
529
+ }
530
+ };
531
+
532
+
533
+ const ensureKnowledgeStore = (cwd: string): KnowledgeStore | null => {
534
+ const sessionsDir = getProjectStatePath(platform.paths, cwd, "sessions");
535
+ const dbPath = join(sessionsDir, "knowledge.db");
536
+ if (knowledgeStore && knowledgeStorePath === dbPath) return knowledgeStore;
537
+
538
+ if (knowledgeStore) {
539
+ try {
540
+ knowledgeStore.close();
541
+ } catch {
542
+ // Best effort: we are about to reopen against the active project's knowledge.db.
543
+ }
544
+ }
545
+
546
+ try {
547
+ mkdirSync(sessionsDir, { recursive: true });
548
+ knowledgeStore = new KnowledgeStore(dbPath);
549
+ knowledgeStore.init();
550
+ knowledgeStorePath = dbPath;
551
+ _knowledgeStoreRef = knowledgeStore;
552
+ return knowledgeStore;
553
+ } catch (e) {
554
+ knowledgeStore = null;
555
+ knowledgeStorePath = null;
556
+ _knowledgeStoreRef = null;
557
+ (platform as any).logger?.error?.("supi-context-mode: failed to initialize knowledge store", e);
558
+ return null;
559
+ }
560
+ };
95
561
  ensureEventStore(sessionCwd);
562
+ ensureMetricsStore(sessionCwd);
563
+ ensureCacheStore(sessionCwd);
564
+ ensureMemoryStore(sessionCwd);
565
+ const initialKnowledgeStore = ensureKnowledgeStore(sessionCwd);
96
566
 
97
567
  _sessionIdRef = sessionId;
98
568
 
99
- // Initialize knowledge store for native ctx_* tools (independent of event tracking)
100
- let knowledgeStore: KnowledgeStore | null = null;
101
- try {
102
- const sessionsDir = platform.paths.project(sessionCwd, "sessions");
103
- mkdirSync(sessionsDir, { recursive: true });
104
- const kdbPath = join(sessionsDir, "knowledge.db");
105
- knowledgeStore = new KnowledgeStore(kdbPath);
106
- knowledgeStore.init();
107
- _knowledgeStoreRef = knowledgeStore;
108
- } catch (e) {
109
- (platform as any).logger?.error?.("supi-context-mode: failed to initialize knowledge store", e);
110
- }
111
-
112
- // Register native context-mode tools
113
- if (knowledgeStore) {
114
- registerContextModeTools(platform, knowledgeStore);
115
- }
569
+ // Register native context-mode tools. Store-dependent knowledge tools are
570
+ // omitted when the knowledge DB cannot initialize, so routing never steers
571
+ // the agent toward ctx_search/ctx_index calls that cannot satisfy requests.
572
+ registerContextModeTools(platform, () => knowledgeStore, {
573
+ knowledgeToolsEnabled: initialKnowledgeStore !== null,
574
+ repomap: {
575
+ enabled: config.contextMode.repomap.enabled,
576
+ tokenBudget: config.contextMode.repomap.tokenBudget,
577
+ maxFiles: config.contextMode.repomap.maxFiles,
578
+ },
579
+ });
580
+ // Slice-2: register the UltraPlan hook bridge. The bridge is the only UltraPlan runtime module
581
+ // this file imports; business decisions (normalization, reducer, migration, repair, tracker
582
+ // storage) all live inside the bridge. When no canonical UltraPlan session is active, the
583
+ // bridge's handlers are no-ops.
584
+ registerUltraPlanHookBridge(platform);
116
585
 
117
586
  platform.on("session_start", (_event, ctx) => {
118
587
  sessionCwd = resolveSessionCwd(ctx as SessionContextLike | undefined);
119
588
  sessionId = deriveSessionId(ctx as SessionContextLike | undefined);
120
589
  _sessionIdRef = sessionId;
590
+ dedupState = createDedupState();
591
+ // Reset focus-chain turn counter so the next before_agent_start re-arms turn 1.
592
+ _focusChainTurnCounters.delete(sessionId);
121
593
 
122
594
  const store = ensureEventStore(sessionCwd);
123
- if (!store) return;
595
+ if (store) {
596
+ try {
597
+ store.upsertMeta(sessionId, sessionCwd);
598
+ } catch (e) {
599
+ (platform as any).logger?.warn?.("supi-context-mode: failed to initialize session metadata", e);
600
+ }
601
+ }
124
602
 
125
- try {
126
- store.upsertMeta(sessionId, sessionCwd);
127
- } catch (e) {
128
- (platform as any).logger?.warn?.("supi-context-mode: failed to initialize session metadata", e);
603
+ const metrics = ensureMetricsStore(sessionCwd);
604
+ if (metrics) {
605
+ try {
606
+ metrics.upsertSession({ session_id: sessionId, cwd: sessionCwd });
607
+ } catch (e) {
608
+ (platform as any).logger?.warn?.("supi-context-mode: failed to initialize metrics session metadata", e);
609
+ }
129
610
  }
611
+
612
+ ensureCacheStore(sessionCwd);
613
+ ensureMemoryStore(sessionCwd);
614
+ ensureKnowledgeStore(sessionCwd);
130
615
  });
131
616
 
132
617
  platform.on("session_shutdown", () => {
618
+ dedupState = createDedupState();
619
+ _forgetAutoIndexSession(sessionId);
133
620
  // Close knowledge store
134
621
  if (knowledgeStore) {
135
622
  try {
@@ -139,9 +626,78 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
139
626
  } finally {
140
627
  knowledgeStore = null;
141
628
  _knowledgeStoreRef = null;
629
+ knowledgeStorePath = null;
630
+ }
631
+ }
632
+ // Promote high-priority observations into memory before closing event store.
633
+ if (memoryStore && eventStore && config.contextMode.memory.enabled) {
634
+ try {
635
+ const events = eventStore.getEvents(sessionId, {
636
+ categories: ["decision", "task", "intent", "rule"],
637
+ limit: 50,
638
+ });
639
+ for (const event of events) {
640
+ let data: any = null;
641
+ try { data = JSON.parse(event.data); } catch { /* skip malformed payloads */ }
642
+ if (!data) continue;
643
+ const body = pickMemoryBody(event.category, data);
644
+ if (!body) continue;
645
+ memoryStore.put({
646
+ ownerScope: "project",
647
+ type: memoryTypeFor(event.category),
648
+ body,
649
+ priority: event.priority,
650
+ });
651
+ }
652
+ } catch (e) {
653
+ (platform as any).logger?.warn?.("supi-context-mode: memory promotion failed", e);
654
+ }
655
+ }
656
+
657
+ // Prune + close cache store before metrics so L3 prune rows can be recorded.
658
+ if (cacheStore) {
659
+ try {
660
+ cacheStore.setMetricsRecorder(metricsStore, sessionId);
661
+ cacheStore.pruneOldSessions(7);
662
+ cacheStore.close();
663
+ } catch (e) {
664
+ (platform as any).logger?.warn?.("supi-context-mode: failed to close cache store", e);
665
+ } finally {
666
+ cacheStore = null;
667
+ cacheStorePath = null;
668
+ _cacheStoreRef = null;
669
+ }
670
+ }
671
+
672
+
673
+ // Prune + close metrics store independently of event store status.
674
+ if (metricsStore) {
675
+ try {
676
+ metricsStore.pruneOldSessions(7);
677
+ metricsStore.close();
678
+ } catch (e) {
679
+ (platform as any).logger?.warn?.("supi-context-mode: failed to close metrics store", e);
680
+ } finally {
681
+ metricsStore = null;
682
+ metricsStorePath = null;
683
+ __setMetricsStoreForTest(null);
684
+ }
685
+ }
686
+
687
+ if (memoryStore) {
688
+ try {
689
+ memoryStore.pruneOld(config.contextMode.memory.retentionDays);
690
+ memoryStore.close();
691
+ } catch (e) {
692
+ (platform as any).logger?.warn?.("supi-context-mode: failed to close memory store", e);
693
+ } finally {
694
+ memoryStore = null;
695
+ memoryStorePath = null;
696
+ _setMemoryStoreRef(null);
142
697
  }
143
698
  }
144
699
 
700
+
145
701
  if (!eventStore) {
146
702
  _eventStoreRef = null;
147
703
  _sessionIdRef = "";
@@ -163,19 +719,121 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
163
719
 
164
720
  // Phase 1: Result compression + Phase 2: Event extraction
165
721
  platform.on("tool_result", (event) => {
166
- // Phase 1: compression
167
- const compressed = compressToolResult(event, config.contextMode.compressionThreshold);
722
+ const projectSlug = basename(getProjectStateDir(platform.paths, sessionCwd));
723
+
724
+ // Phase 1: compression + forward-only same-source dedup
725
+ const pipeline = runEmissionPipeline(event, config.contextMode.compressionThreshold, {
726
+ processors: config.contextMode.processors,
727
+ });
728
+ let compressed = pipeline.result;
729
+ let processorKey = pipeline.processorKey;
730
+ const sourceHash = uniqueSourceHash({
731
+ tool: event.toolName,
732
+ input: event.input,
733
+ cwd: sessionCwd,
734
+ projectSlug,
735
+ });
736
+
737
+ try {
738
+ const processedBytes = compressed?.content
739
+ ? new TextEncoder().encode(combinedTextOf(compressed.content)).byteLength
740
+ : 0;
741
+ const deduped = maybeSubstitute({
742
+ result: compressed,
743
+ processorKey,
744
+ sourceHash,
745
+ dedupState,
746
+ processedBytes,
747
+ });
748
+ compressed = deduped.result;
749
+ processorKey = deduped.processorKey;
750
+ } catch (e) {
751
+ (platform as any).logger?.warn?.("supi-context-mode: dedup substitution failed", e);
752
+ }
753
+
754
+ // Phase 3: optional L3 cache-handle spill for oversized current emissions.
755
+ if (config.contextMode.cacheHandles.enabled && cacheStore && !event.isError && isTextOnlyContent(event.content)) {
756
+ const finalContent = compressed?.content ?? event.content;
757
+ const finalText = combinedTextOf(finalContent);
758
+ const finalBytes = textContentBytes(finalContent);
759
+ const originalText = combinedTextOf(event.content);
760
+ if (
761
+ finalBytes > config.contextMode.cacheHandles.spillThresholdBytes
762
+ && originalText.length > 0
763
+ && !OMP_MINIMIZER_FOOTER_RE.test(finalText)
764
+ && !OMP_MINIMIZER_FOOTER_RE.test(originalText)
765
+ ) {
766
+ try {
767
+ const cached = cacheStore.putText({
768
+ sessionId,
769
+ text: originalText,
770
+ sourceTool: event.toolName,
771
+ sourceHash,
772
+ previewBytes: config.contextMode.cacheHandles.previewBytes,
773
+ recordMetric: false,
774
+ });
775
+ const replacementText = [
776
+ `Cached oversized ${event.toolName} result as ${cached.handle}.`,
777
+ `Original size: ${cached.sizeBytes} bytes. Preview below is bounded to ${config.contextMode.cacheHandles.previewBytes} chars.`,
778
+ `Open the full payload with ctx_open_cached(handle: "${cached.handle}", offset: 0, limit: <chars>).`,
779
+ "",
780
+ "--- preview ---",
781
+ cached.preview,
782
+ ].join("\n");
783
+ compressed = { content: [{ type: "text", text: replacementText }] };
784
+ processorKey = "cache-spill";
785
+ } catch (e) {
786
+ (platform as any).logger?.warn?.("supi-context-mode: cache spill failed", e);
787
+ }
788
+ }
789
+ }
168
790
 
169
791
  // Phase 2: event extraction (fire-and-forget)
170
792
  if (eventStore && config.contextMode.eventTracking) {
171
793
  try {
172
- const events = extractEvents(event, sessionId);
794
+ const events = extractEvents(event, sessionId, sourceHash);
173
795
  if (events.length > 0) eventStore.writeEvents(events);
174
796
  } catch (e) {
175
797
  (platform as any).logger?.warn?.("supi-context-mode: event extraction failed", e);
176
798
  }
177
799
  }
178
800
 
801
+ // Metrics recording (fire-and-forget; never throws back to the agent).
802
+ if (metricsStore) {
803
+ try {
804
+ const usage = (() => {
805
+ try {
806
+ const raw = (event as any).contextUsage
807
+ ?? (event as any).context
808
+ ?? null;
809
+ if (!raw || typeof raw !== "object") return null;
810
+ return {
811
+ tokens: typeof raw.tokens === "number" ? raw.tokens : null,
812
+ contextWindow: typeof raw.contextWindow === "number" ? raw.contextWindow : null,
813
+ percent: typeof raw.percent === "number" ? raw.percent : null,
814
+ };
815
+ } catch {
816
+ return null;
817
+ }
818
+ })();
819
+ const row = toMetricRow({
820
+ event,
821
+ compressed,
822
+ sessionId,
823
+ cwd: sessionCwd,
824
+ projectSlug,
825
+ contextUsage: usage,
826
+ ts: Date.now(),
827
+ processorKey,
828
+ sourceHash,
829
+ layer: processorKey === "cache-spill" ? "L3" : "L2",
830
+ });
831
+ metricsStore.record(row);
832
+ } catch (e) {
833
+ (platform as any).logger?.warn?.("supi-context-mode: metrics record failed", e);
834
+ }
835
+ }
836
+
179
837
  return compressed;
180
838
  });
181
839
 
@@ -192,7 +850,7 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
192
850
  });
193
851
 
194
852
  // Phase 1: Routing instructions + Phase 2: Prompt event extraction
195
- platform.on("before_agent_start", (event) => {
853
+ platform.on("before_agent_start", (event, ctx) => {
196
854
  // Phase 2: prompt event extraction (fire-and-forget)
197
855
  if (eventStore && config.contextMode.eventTracking) {
198
856
  try {
@@ -206,16 +864,39 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
206
864
  }
207
865
  }
208
866
 
209
- // Phase 1: routing instructions always inject when enforceRouting is on,
210
- // regardless of MCP tool detection (tools may load after this hook fires)
211
- if (!config.contextMode.routingInstructions && !config.contextMode.enforceRouting) return;
212
-
213
- const skill = loadRoutingSkill();
214
- if (!skill) return;
867
+ // Phase 1 routing instructions are gated by config; memory + focus chain are always considered.
868
+ const routingEnabled = config.contextMode.routingInstructions || config.contextMode.enforceRouting;
869
+ const status = detectContextMode(platform.getActiveTools());
870
+ const guidance = routingEnabled ? buildActiveRoutingGuidance(status) : null;
871
+ const memoryBlock = config.mempalace?.enabled
872
+ ? null
873
+ : buildMemoryInjectionBlock(memoryStore, sessionId, config.contextMode.memory);
874
+ // Increment per-session turn counter for focus-chain cadence gating.
875
+ // First call after session_start is turn 1 (always injects).
876
+ const prevTurn = _focusChainTurnCounters.get(sessionId) ?? 0;
877
+ const turnCount = prevTurn + 1;
878
+ _focusChainTurnCounters.set(sessionId, turnCount);
879
+ const focusBlock = buildFocusChainBlock(eventStore, sessionId, {
880
+ cadence: config.contextMode.memory.focusChainCadence,
881
+ turnCount,
882
+ });
883
+ const sections = [guidance, memoryBlock, focusBlock].filter((s): s is string => Boolean(s));
884
+ if (sections.length === 0) return;
885
+ const injection = sections.join("\n\n");
215
886
 
216
- const systemPrompt = (event as any).systemPrompt as string | undefined;
217
- if (!systemPrompt) return { systemPrompt: skill };
218
- return { systemPrompt: systemPrompt + "\n\n" + skill };
887
+ let systemPromptBlocks = normalizeSystemPromptBlocks((event as any).systemPrompt);
888
+ const getSystemPrompt = (ctx as any)?.getSystemPrompt;
889
+ if (typeof getSystemPrompt === "function") {
890
+ try {
891
+ const currentPrompt = getSystemPrompt();
892
+ if (currentPrompt != null) {
893
+ systemPromptBlocks = normalizeSystemPromptBlocks(currentPrompt);
894
+ }
895
+ } catch (e) {
896
+ (platform as any).logger?.warn?.("supi-context-mode: failed to read current system prompt", e);
897
+ }
898
+ }
899
+ return { systemPrompt: [...systemPromptBlocks, injection] };
219
900
  });
220
901
 
221
902
  // Phase 3: Compaction integration
@@ -229,7 +910,7 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
229
910
  // Non-fatal: metadata is supplementary
230
911
  }
231
912
 
232
- platform.on("session_before_compact", () => {
913
+ platform.on("session_before_compact", async () => {
233
914
  // Re-detect MCP tools: they may have loaded since init
234
915
  const status = cachedStatus ?? detectContextMode(platform.getActiveTools());
235
916
  const searchAvailable = status.tools.ctxSearch;
@@ -250,25 +931,56 @@ export function registerContextModeHooks(platform: Platform, config: SupipowersC
250
931
  // Non-fatal
251
932
  }
252
933
 
934
+ let snapshot: string | null = null;
935
+ let eventCount = 0;
253
936
  try {
254
- const snapshot = buildResumeSnapshot(eventStore!, sessionId, {
937
+ const compactOverride = readCompactOverride(platform.paths, sessionCwd);
938
+ let built = buildResumeSnapshot(eventStore!, sessionId, {
255
939
  compactCount,
256
940
  searchTool,
257
941
  searchAvailable,
258
942
  });
943
+ if (compactOverride) {
944
+ built = `${compactOverride}\n\n${built ?? ""}`.trim();
945
+ }
259
946
 
260
- // Persist to DB so it survives crashes
261
- if (snapshot) {
262
- const eventCount = Object.values(eventStore!.getEventCounts(sessionId))
947
+ // Persist deterministic snapshot to DB so it survives crashes — this
948
+ // is the contract. The LLM step below is a best-effort improvement.
949
+ if (built) {
950
+ eventCount = Object.values(eventStore!.getEventCounts(sessionId))
263
951
  .reduce((a, b) => a + b, 0);
264
- eventStore!.upsertResume(sessionId, snapshot, eventCount);
952
+ eventStore!.upsertResume(sessionId, built, eventCount);
953
+ snapshot = built;
265
954
  }
266
-
267
- return undefined; // don't cancel or replace compaction
268
955
  } catch (e) {
269
956
  (platform as any).logger?.warn?.("context-mode: snapshot build failed", e);
270
957
  return undefined;
271
958
  }
959
+
960
+ // Best-effort LLM summarization, gated by config + size. Failures keep
961
+ // the deterministic snapshot already persisted above.
962
+ if (
963
+ snapshot
964
+ && config.contextMode.llmSummarization
965
+ && byteLengthOf(snapshot) > config.contextMode.llmThreshold
966
+ ) {
967
+ try {
968
+ await summarizeSnapshotIfBudget({
969
+ platform,
970
+ cwd: sessionCwd,
971
+ sessionId,
972
+ snapshot,
973
+ eventCount,
974
+ eventStore: eventStore!,
975
+ compressionThreshold: config.contextMode.compressionThreshold,
976
+ llmThreshold: config.contextMode.llmThreshold,
977
+ });
978
+ } catch (e) {
979
+ (platform as any).logger?.warn?.("context-mode: LLM summarization failed", e);
980
+ }
981
+ }
982
+
983
+ return undefined; // don't cancel or replace compaction
272
984
  });
273
985
 
274
986
  platform.on("session_compact", () => {
@@ -309,6 +1021,24 @@ export function getEventStore(): EventStore | null {
309
1021
  return _eventStoreRef;
310
1022
  }
311
1023
 
1024
+ /** Get the metrics store instance (for use by /supi:context, ctx_stats, /supi:clear). */
1025
+ export { getMetricsStore } from "./metrics-store.js";
1026
+
1027
+ /** Get the active knowledge store (for use by /supi:clear and scoped context reset). */
1028
+ export function getKnowledgeStore(): KnowledgeStore | null {
1029
+ return _knowledgeStoreRef;
1030
+ }
1031
+
1032
+ /** Get the cache store instance (for use by ctx_open_cached and /supi:clear). */
1033
+ export function getCacheStore(): CacheStore | null {
1034
+ return _cacheStoreRef;
1035
+ }
1036
+
1037
+ /** Test-only cache store setter for tool/command tests. */
1038
+ export function __setCacheStoreForTest(store: CacheStore | null): void {
1039
+ _cacheStoreRef = store;
1040
+ }
1041
+
312
1042
  /** Get the session ID (for use by compaction hooks) */
313
1043
  export function getSessionId(): string {
314
1044
  return _sessionIdRef;
@@ -317,12 +1047,24 @@ export function getSessionId(): string {
317
1047
  // Module-level refs updated by registerContextModeHooks
318
1048
  let _eventStoreRef: EventStore | null = null;
319
1049
  let _knowledgeStoreRef: KnowledgeStore | null = null;
1050
+ let _cacheStoreRef: CacheStore | null = null;
320
1051
  let _sessionIdRef = "";
321
1052
 
1053
+ /**
1054
+ * Per-session turn counter for focus-chain cadence gating. Held in-memory
1055
+ * only — see L5 design spec D5: loss across crashes is acceptable, worst case
1056
+ * is one extra reinjection on resume. Keyed by sessionId.
1057
+ */
1058
+ const _focusChainTurnCounters = new Map<string, number>();
1059
+
322
1060
  /** Reset cached state (for testing) */
323
1061
  export function _resetCache(): void {
324
1062
  cachedStatus = null;
325
1063
  _eventStoreRef = null;
326
1064
  _knowledgeStoreRef = null;
1065
+ _cacheStoreRef = null;
327
1066
  _sessionIdRef = "";
1067
+ _focusChainTurnCounters.clear();
1068
+ _resetMetricsStoreCache();
1069
+ _resetAutoIndexAttempts();
328
1070
  }