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
@@ -2,12 +2,19 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type {
5
+ CommandGateId,
5
6
  ConfigScope,
6
7
  SupipowersConfig,
7
8
  ReleaseChannel,
8
9
  QualityGatesConfig,
9
10
  } from "../types.js";
10
11
  import type { PlatformPaths } from "../platform/types.js";
12
+ import { resolvePackageManager } from "../workspace/package-manager.js";
13
+ import {
14
+ discoverWorkspaceTargets,
15
+ normalizeWorkspaceRelativePath,
16
+ } from "../workspace/targets.js";
17
+ import { getRootConfigPath } from "../workspace/state-paths.js";
11
18
  import { DEFAULT_CONFIG } from "./defaults.js";
12
19
  import {
13
20
  collectConfigValidationErrors,
@@ -32,16 +39,49 @@ export interface QualityGateRecoveryInspection {
32
39
  scopes: ScopedConfigInspection[];
33
40
  }
34
41
 
35
- function getProjectConfigPath(paths: PlatformPaths, cwd: string): string {
36
- return paths.project(cwd, "config.json");
42
+ export interface ConfigResolutionOptions {
43
+ repoRoot?: string;
44
+ }
45
+
46
+ export interface ConfigMutationOptions extends ConfigResolutionOptions {
47
+ scope?: ConfigScope;
48
+ }
49
+
50
+ interface ResolvedConfigContext {
51
+ repoRoot: string;
52
+ }
53
+
54
+ interface ResolvedConfigLayer {
55
+ scope: ConfigScope;
56
+ path: string;
57
+ }
58
+
59
+ function resolveConfigContext(cwd: string, options?: ConfigResolutionOptions): ResolvedConfigContext {
60
+ return { repoRoot: options?.repoRoot ?? cwd };
37
61
  }
38
62
 
39
63
  function getGlobalConfigPath(paths: PlatformPaths): string {
40
64
  return paths.global("config.json");
41
65
  }
42
66
 
43
- function getConfigPath(paths: PlatformPaths, cwd: string, scope: ConfigScope): string {
44
- return scope === "global" ? getGlobalConfigPath(paths) : getProjectConfigPath(paths, cwd);
67
+ function getConfigPath(
68
+ paths: PlatformPaths,
69
+ cwd: string,
70
+ scope: ConfigScope,
71
+ options?: ConfigResolutionOptions,
72
+ ): string {
73
+ const { repoRoot } = resolveConfigContext(cwd, options);
74
+
75
+ switch (scope) {
76
+ case "global":
77
+ return getGlobalConfigPath(paths);
78
+ case "root":
79
+ return getRootConfigPath(paths, repoRoot);
80
+ }
81
+ }
82
+
83
+ function getInspectionScopes(): ConfigScope[] {
84
+ return ["global", "root"];
45
85
  }
46
86
 
47
87
  function readJsonFile(
@@ -136,6 +176,49 @@ function asRecord(value: unknown): Record<string, unknown> | null {
136
176
  : null;
137
177
  }
138
178
 
179
+ function hasMeaningfulUltraPlanPolicy(config: Record<string, unknown> | null): boolean {
180
+ const ultraplan = asRecord(config?.ultraplan);
181
+ if (!ultraplan) {
182
+ return false;
183
+ }
184
+
185
+ const slots = asRecord(ultraplan.slots);
186
+ if (slots && Object.values(slots).some((override) => {
187
+ const slotOverride = asRecord(override);
188
+ return !!slotOverride && (
189
+ typeof slotOverride.agentName === "string"
190
+ || typeof slotOverride.model === "string"
191
+ || typeof slotOverride.thinkingLevel === "string"
192
+ );
193
+ })) {
194
+ return true;
195
+ }
196
+
197
+ const reviewGates = asRecord(ultraplan.reviewGates);
198
+ return !!reviewGates && Object.values(reviewGates).some((policy) => {
199
+ const reviewGate = asRecord(policy);
200
+ return !!reviewGate && typeof reviewGate.enabled === "boolean";
201
+ });
202
+ }
203
+
204
+ function omitMeaninglessUltraPlanFromGlobalRead(
205
+ config: Record<string, unknown> | null,
206
+ scope: ConfigScope,
207
+ ): Record<string, unknown> | null {
208
+ if (
209
+ scope !== "global"
210
+ || !config
211
+ || !Object.prototype.hasOwnProperty.call(config, "ultraplan")
212
+ || hasMeaningfulUltraPlanPolicy(config)
213
+ ) {
214
+ return config;
215
+ }
216
+
217
+ const { ultraplan: _ultraplan, ...scoped } = config;
218
+ return scoped;
219
+ }
220
+
221
+
139
222
  function normalizeReleaseChannels(
140
223
  existingChannels: unknown,
141
224
  pipeline: string | null,
@@ -153,6 +236,93 @@ function normalizeReleaseChannels(
153
236
  return [];
154
237
  }
155
238
 
239
+ const COMMAND_GATE_IDS: CommandGateId[] = ["lint", "typecheck", "format", "test-suite", "build"];
240
+
241
+ function createAllTargetsRun(command: string): { command: string; target: { scope: "all-targets" } } {
242
+ return {
243
+ command,
244
+ target: { scope: "all-targets" },
245
+ };
246
+ }
247
+
248
+ function normalizeCommandGateRunTarget(target: unknown): unknown {
249
+ const record = asRecord(target);
250
+ if (!record || typeof record.scope !== "string") {
251
+ return target;
252
+ }
253
+
254
+ switch (record.scope) {
255
+ case "all-targets":
256
+ case "root":
257
+ case "all-workspaces":
258
+ return { scope: record.scope };
259
+ case "workspace":
260
+ return {
261
+ scope: "workspace",
262
+ relativeDir:
263
+ typeof record.relativeDir === "string"
264
+ ? normalizeWorkspaceRelativePath(record.relativeDir.trim())
265
+ : record.relativeDir,
266
+ };
267
+ default:
268
+ return target;
269
+ }
270
+ }
271
+
272
+ function normalizeCommandGateRuns(runs: unknown): unknown {
273
+ if (!Array.isArray(runs)) {
274
+ return runs;
275
+ }
276
+
277
+ return runs.map((run) => {
278
+ const record = asRecord(run);
279
+ if (!record) {
280
+ return run;
281
+ }
282
+
283
+ return {
284
+ ...record,
285
+ command: typeof record.command === "string" ? record.command.trim() : record.command,
286
+ target: normalizeCommandGateRunTarget(record.target),
287
+ };
288
+ });
289
+ }
290
+
291
+ function migrateCommandGateConfig(config: unknown): unknown {
292
+ const record = asRecord(config);
293
+ if (!record) {
294
+ return config;
295
+ }
296
+
297
+ if (record.enabled === false) {
298
+ return { enabled: false };
299
+ }
300
+
301
+ if (record.enabled !== true) {
302
+ return config;
303
+ }
304
+
305
+ if (Array.isArray(record.runs)) {
306
+ return {
307
+ enabled: true,
308
+ runs: normalizeCommandGateRuns(record.runs),
309
+ };
310
+ }
311
+
312
+ const legacyCommand =
313
+ typeof record.command === "string" && record.command.trim().length > 0
314
+ ? record.command.trim()
315
+ : null;
316
+ if (legacyCommand) {
317
+ return {
318
+ enabled: true,
319
+ runs: [createAllTargetsRun(legacyCommand)],
320
+ };
321
+ }
322
+
323
+ return { enabled: true };
324
+ }
325
+
156
326
  function legacyGatesFromProfile(
157
327
  profileName: string | null,
158
328
  legacyTestCommand: string | null,
@@ -164,7 +334,10 @@ function legacyGatesFromProfile(
164
334
  }
165
335
 
166
336
  if (legacyTestCommand) {
167
- gates["test-suite"] = { enabled: true, command: legacyTestCommand };
337
+ gates["test-suite"] = {
338
+ enabled: true,
339
+ runs: [createAllTargetsRun(legacyTestCommand)],
340
+ };
168
341
  }
169
342
 
170
343
  return Object.keys(gates).length > 0 ? gates : null;
@@ -207,6 +380,12 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
207
380
  if (gates) {
208
381
  // Strip legacy ai-review gate — removed from the schema in the checks/review split.
209
382
  delete gates["ai-review"];
383
+
384
+ for (const gateId of COMMAND_GATE_IDS) {
385
+ if (gateId in gates) {
386
+ gates[gateId] = migrateCommandGateConfig(gates[gateId]);
387
+ }
388
+ }
210
389
  }
211
390
  if (!gates || Object.keys(gates).length === 0) {
212
391
  const legacyGates = legacyGatesFromProfile(legacyProfile, legacyTestCommand);
@@ -214,7 +393,10 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
214
393
  quality.gates = legacyGates as unknown as Record<string, unknown>;
215
394
  }
216
395
  } else if (legacyTestCommand && !("test-suite" in gates)) {
217
- gates["test-suite"] = { enabled: true, command: legacyTestCommand };
396
+ gates["test-suite"] = {
397
+ enabled: true,
398
+ runs: [createAllTargetsRun(legacyTestCommand)],
399
+ };
218
400
  quality.gates = gates;
219
401
  }
220
402
  migrated.quality = quality;
@@ -226,17 +408,14 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
226
408
 
227
409
  function mergeConfigLayers(
228
410
  defaults: SupipowersConfig,
229
- globalData: Record<string, unknown> | null,
230
- projectData: Record<string, unknown> | null,
231
- ): Record<string, unknown> {
411
+ ...layers: Array<Record<string, unknown> | null>
412
+ ): Record<string, unknown> {
232
413
  let merged = structuredClone(defaults) as unknown as Record<string, unknown>;
233
414
 
234
- if (globalData) {
235
- merged = applyConfigOverride(merged, globalData);
236
- }
237
-
238
- if (projectData) {
239
- merged = applyConfigOverride(merged, projectData);
415
+ for (const layer of layers) {
416
+ if (layer) {
417
+ merged = applyConfigOverride(merged, layer);
418
+ }
240
419
  }
241
420
 
242
421
  // The config schema changed without a version bump, so normalize known
@@ -246,14 +425,112 @@ function mergeConfigLayers(
246
425
  return merged;
247
426
  }
248
427
 
428
+ interface ResolvedConfigLayerRead extends ResolvedConfigLayer {
429
+ readResult: { data: Record<string, unknown> | null; error: ConfigParseError | null };
430
+ }
431
+
432
+ function readConfigLayers(
433
+ paths: PlatformPaths,
434
+ cwd: string,
435
+ options?: ConfigResolutionOptions,
436
+ ): ResolvedConfigLayerRead[] {
437
+ return getInspectionScopes().map((scope) => {
438
+ const filePath = getConfigPath(paths, cwd, scope, options);
439
+ const readResult = readJsonFile(scope, filePath);
440
+ return {
441
+ scope,
442
+ path: filePath,
443
+ readResult: {
444
+ ...readResult,
445
+ data: omitMeaninglessUltraPlanFromGlobalRead(readResult.data, scope),
446
+ },
447
+ };
448
+ });
449
+ }
450
+
451
+ function collectCommandGateSelectorValidationErrors(
452
+ config: Record<string, unknown>,
453
+ repoRoot: string,
454
+ ): ConfigValidationError[] {
455
+ const quality = asRecord(config.quality);
456
+ const gates = asRecord(quality?.gates);
457
+ if (!gates) {
458
+ return [];
459
+ }
460
+
461
+ const workspaceRelativeDirs = new Set(
462
+ discoverWorkspaceTargets(repoRoot, resolvePackageManager(repoRoot).id)
463
+ .filter((target) => target.kind === "workspace")
464
+ .map((target) => target.relativeDir),
465
+ );
466
+
467
+ const errors: ConfigValidationError[] = [];
468
+ for (const gateId of COMMAND_GATE_IDS) {
469
+ const gateConfig = asRecord(gates[gateId]);
470
+ if (!gateConfig || gateConfig.enabled !== true || !Array.isArray(gateConfig.runs)) {
471
+ continue;
472
+ }
473
+
474
+ gateConfig.runs.forEach((run, index) => {
475
+ const target = asRecord(asRecord(run)?.target);
476
+ if (target?.scope !== "workspace") {
477
+ return;
478
+ }
479
+
480
+ const relativeDir =
481
+ typeof target.relativeDir === "string"
482
+ ? normalizeWorkspaceRelativePath(target.relativeDir)
483
+ : null;
484
+ if (relativeDir && workspaceRelativeDirs.has(relativeDir)) {
485
+ return;
486
+ }
487
+
488
+ errors.push({
489
+ path: `quality.gates.${gateId}.runs.${index}.target.relativeDir`,
490
+ message: `Unknown workspace target "${String(target.relativeDir)}"`,
491
+ });
492
+ });
493
+ }
494
+
495
+ return errors;
496
+ }
497
+
498
+ function collectScopeValidationErrors(
499
+ config: Record<string, unknown> | null,
500
+ scope: ConfigScope,
501
+ ): ConfigValidationError[] {
502
+ if (scope !== "global" || !hasMeaningfulUltraPlanPolicy(config)) {
503
+ return [];
504
+ }
505
+
506
+ return [
507
+ {
508
+ path: "ultraplan",
509
+ message: "Only repository config may define ultraplan",
510
+ },
511
+ ];
512
+ }
513
+
514
+
515
+ function collectAllValidationErrors(
516
+ config: Record<string, unknown>,
517
+ repoRoot: string,
518
+ ): ConfigValidationError[] {
519
+ return [
520
+ ...collectConfigValidationErrors(config),
521
+ ...collectCommandGateSelectorValidationErrors(config, repoRoot),
522
+ ];
523
+ }
524
+
249
525
  function inspectScopeConfig(
250
526
  paths: PlatformPaths,
251
527
  cwd: string,
252
528
  scope: ConfigScope,
529
+ options?: ConfigResolutionOptions,
253
530
  ): ScopedConfigInspection {
254
- const filePath = getConfigPath(paths, cwd, scope);
531
+ const { repoRoot } = resolveConfigContext(cwd, options);
532
+ const filePath = getConfigPath(paths, cwd, scope, options);
255
533
  const readResult = readJsonFile(scope, filePath);
256
-
257
534
  if (readResult.error) {
258
535
  return {
259
536
  scope,
@@ -269,11 +546,12 @@ function inspectScopeConfig(
269
546
  }
270
547
 
271
548
  const hasOwnQualityGates = !!readResult.data && hasOwnNestedProperty(readResult.data, "quality", "gates");
272
- const mergedConfig =
273
- scope === "global"
274
- ? mergeConfigLayers(DEFAULT_CONFIG, readResult.data, null)
275
- : mergeConfigLayers(DEFAULT_CONFIG, null, readResult.data);
276
- const validationErrors = collectConfigValidationErrors(mergedConfig);
549
+ const scopeData = omitMeaninglessUltraPlanFromGlobalRead(readResult.data, scope);
550
+ const mergedConfig = mergeConfigLayers(DEFAULT_CONFIG, scopeData);
551
+ const validationErrors = [
552
+ ...collectAllValidationErrors(mergedConfig, repoRoot),
553
+ ...collectScopeValidationErrors(readResult.data, scope),
554
+ ];
277
555
  const qualityGateValidationErrors = hasOwnQualityGates
278
556
  ? validationErrors.filter((error) => error.path === "quality.gates" || error.path.startsWith("quality.gates."))
279
557
  : [];
@@ -295,6 +573,7 @@ function inspectScopeConfig(
295
573
  };
296
574
  }
297
575
 
576
+
298
577
  function removeQualityGatesFromRecord(config: Record<string, unknown>): Record<string, unknown> {
299
578
  const next = structuredClone(config) as Record<string, unknown>;
300
579
  const quality = asRecord(next.quality);
@@ -317,14 +596,21 @@ function writeRawConfigFile(filePath: string, config: Record<string, unknown>):
317
596
  fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
318
597
  }
319
598
 
599
+ export function inspectConfigScopes(
600
+ paths: PlatformPaths,
601
+ cwd: string,
602
+ options?: ConfigResolutionOptions,
603
+ ): ScopedConfigInspection[] {
604
+ return getInspectionScopes().map((scope) => inspectScopeConfig(paths, cwd, scope, options));
605
+ }
606
+
320
607
  export function inspectQualityGateRecovery(
321
608
  paths: PlatformPaths,
322
609
  cwd: string,
323
- ): QualityGateRecoveryInspection {
610
+ options?: ConfigResolutionOptions,
611
+ ): QualityGateRecoveryInspection {
324
612
  return {
325
- scopes: (["global", "project"] as ConfigScope[]).map((scope) =>
326
- inspectScopeConfig(paths, cwd, scope),
327
- ),
613
+ scopes: inspectConfigScopes(paths, cwd, options),
328
614
  };
329
615
  }
330
616
 
@@ -333,8 +619,9 @@ export function writeQualityGatesConfig(
333
619
  cwd: string,
334
620
  scope: ConfigScope,
335
621
  gates: QualityGatesConfig,
336
- ): void {
337
- const configPath = getConfigPath(paths, cwd, scope);
622
+ options?: ConfigResolutionOptions,
623
+ ): void {
624
+ const configPath = getConfigPath(paths, cwd, scope, options);
338
625
  const current = readJsonFile(scope, configPath);
339
626
  if (current.error) {
340
627
  throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
@@ -355,8 +642,9 @@ export function removeQualityGatesConfig(
355
642
  paths: PlatformPaths,
356
643
  cwd: string,
357
644
  scope: ConfigScope,
358
- ): boolean {
359
- const configPath = getConfigPath(paths, cwd, scope);
645
+ options?: ConfigResolutionOptions,
646
+ ): boolean {
647
+ const configPath = getConfigPath(paths, cwd, scope, options);
360
648
  const current = readJsonFile(scope, configPath);
361
649
  if (current.error) {
362
650
  throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
@@ -369,10 +657,14 @@ export function removeQualityGatesConfig(
369
657
  return true;
370
658
  }
371
659
 
660
+ function describeConfigSource(source: ConfigParseError["source"]): string {
661
+ return source === "root" ? "repository" : source;
662
+ }
663
+
372
664
  export function formatConfigErrors(result: InspectionLoadResult): string {
373
665
  const messages = [
374
666
  ...result.parseErrors.map(
375
- (error) => `${error.source} config ${error.path}: ${error.message}`,
667
+ (error) => `${describeConfigSource(error.source)} config ${error.path}: ${error.message}`,
376
668
  ),
377
669
  ...result.validationErrors.map(
378
670
  (error) => `${error.path}: ${error.message}`,
@@ -382,14 +674,21 @@ export function formatConfigErrors(result: InspectionLoadResult): string {
382
674
  return messages.join("\n") || "Unknown config error";
383
675
  }
384
676
 
385
- export function inspectConfig(paths: PlatformPaths, cwd: string): InspectionLoadResult {
386
- const globalRead = readJsonFile("global", getGlobalConfigPath(paths));
387
- const projectRead = readJsonFile("project", getProjectConfigPath(paths, cwd));
388
- const mergedConfig = mergeConfigLayers(DEFAULT_CONFIG, globalRead.data, projectRead.data);
389
- const parseErrors = [globalRead.error, projectRead.error].filter(
390
- (error): error is ConfigParseError => error !== null,
677
+ function buildInspectionLoadResult(
678
+ layers: ResolvedConfigLayerRead[],
679
+ repoRoot: string,
680
+ ): InspectionLoadResult {
681
+ const mergedConfig = mergeConfigLayers(
682
+ DEFAULT_CONFIG,
683
+ ...layers.map((layer) => layer.readResult.data),
391
684
  );
392
- const validationErrors = collectConfigValidationErrors(mergedConfig);
685
+ const parseErrors = layers
686
+ .map((layer) => layer.readResult.error)
687
+ .filter((error): error is ConfigParseError => error !== null);
688
+ const validationErrors = [
689
+ ...collectAllValidationErrors(mergedConfig, repoRoot),
690
+ ...layers.flatMap((layer) => collectScopeValidationErrors(layer.readResult.data, layer.scope)),
691
+ ];
393
692
 
394
693
  return {
395
694
  mergedConfig,
@@ -402,9 +701,38 @@ export function inspectConfig(paths: PlatformPaths, cwd: string): InspectionLoad
402
701
  };
403
702
  }
404
703
 
405
- /** Load config with global -> project layering over defaults. */
406
- export function loadConfig(paths: PlatformPaths, cwd: string): SupipowersConfig {
407
- const result = inspectConfig(paths, cwd);
704
+ export function inspectConfigAtScope(
705
+ paths: PlatformPaths,
706
+ cwd: string,
707
+ scope: ConfigScope,
708
+ options?: ConfigResolutionOptions,
709
+ ): InspectionLoadResult {
710
+ const { repoRoot } = resolveConfigContext(cwd, options);
711
+ const layers = readConfigLayers(paths, cwd, options).filter((layer) =>
712
+ scope === "global"
713
+ ? layer.scope === "global"
714
+ : layer.scope === "global" || layer.scope === "root",
715
+ );
716
+
717
+ return buildInspectionLoadResult(layers, repoRoot);
718
+ }
719
+
720
+ export function inspectConfig(
721
+ paths: PlatformPaths,
722
+ cwd: string,
723
+ options?: ConfigResolutionOptions,
724
+ ): InspectionLoadResult {
725
+ const { repoRoot } = resolveConfigContext(cwd, options);
726
+ return buildInspectionLoadResult(readConfigLayers(paths, cwd, options), repoRoot);
727
+ }
728
+
729
+ /** Load config with global -> repository layering over defaults. */
730
+ export function loadConfig(
731
+ paths: PlatformPaths,
732
+ cwd: string,
733
+ options?: ConfigResolutionOptions,
734
+ ): SupipowersConfig {
735
+ const result = inspectConfig(paths, cwd, options);
408
736
 
409
737
  if (!result.effectiveConfig) {
410
738
  throw new Error(formatConfigErrors(result));
@@ -413,43 +741,103 @@ export function loadConfig(paths: PlatformPaths, cwd: string): SupipowersConfig
413
741
  return result.effectiveConfig;
414
742
  }
415
743
 
416
- function assertValidConfig(data: unknown): void {
417
- const validationErrors = collectConfigValidationErrors(data);
744
+ // Global scope never persists UltraPlan policy; writes only keep the shared config surface it may own.
745
+ function omitUnsupportedUltraPlanFromGlobalWrite(
746
+ config: Record<string, unknown>,
747
+ scope: ConfigScope,
748
+ ): Record<string, unknown> {
749
+ if (scope !== "global") {
750
+ return config;
751
+ }
418
752
 
419
- if (validationErrors.length === 0) {
753
+ const { ultraplan: _ultraplan, ...scoped } = config;
754
+ return scoped;
755
+ }
756
+
757
+ function prepareScopedConfigWrite(
758
+ fullConfig: unknown,
759
+ scopeData: Record<string, unknown>,
760
+ repoRoot: string,
761
+ scope: ConfigScope,
762
+ ): Record<string, unknown> {
763
+ const persistedScopeData = omitUnsupportedUltraPlanFromGlobalWrite(scopeData, scope);
764
+ const validatedScopeData = scope === "global" && !hasMeaningfulUltraPlanPolicy(scopeData)
765
+ ? persistedScopeData
766
+ : scopeData;
767
+
768
+ assertValidConfig(fullConfig, repoRoot, scope, validatedScopeData);
769
+ return persistedScopeData;
770
+ }
771
+
772
+ function assertValidConfig(
773
+ data: unknown,
774
+ repoRoot: string,
775
+ scope?: ConfigScope,
776
+ scopeData?: Record<string, unknown> | null,
777
+ ): void {
778
+ const record = asRecord(data);
779
+ const validationErrors = record ? collectAllValidationErrors(record, repoRoot) : collectConfigValidationErrors(data);
780
+ const scopeValidationErrors = scope ? collectScopeValidationErrors(scopeData ?? null, scope) : [];
781
+
782
+ if (validationErrors.length === 0 && scopeValidationErrors.length === 0) {
420
783
  return;
421
784
  }
422
785
 
423
786
  throw new Error(
424
- validationErrors
787
+ [...validationErrors, ...scopeValidationErrors]
425
788
  .map((error) => `${error.path}: ${error.message}`)
426
789
  .join("\n"),
427
790
  );
428
791
  }
429
792
 
430
-
431
- /** Save project-level config. */
432
- export function saveConfig(paths: PlatformPaths, cwd: string, config: SupipowersConfig): void {
433
- assertValidConfig(config);
434
-
435
- const configPath = getProjectConfigPath(paths, cwd);
793
+ /** Save a full config document to the selected scope. */
794
+ export function saveConfig(
795
+ paths: PlatformPaths,
796
+ cwd: string,
797
+ config: SupipowersConfig,
798
+ options?: ConfigMutationOptions,
799
+ ): void {
800
+ const { repoRoot } = resolveConfigContext(cwd, options);
801
+ const scope = options?.scope ?? "root";
802
+ const rawConfig = prepareScopedConfigWrite(
803
+ config,
804
+ config as unknown as Record<string, unknown>,
805
+ repoRoot,
806
+ scope,
807
+ );
808
+ const configPath = getConfigPath(paths, cwd, scope, options);
436
809
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
437
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
810
+ fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
438
811
  }
439
812
 
440
- /** Update specific config fields (deep merge into current). */
813
+ /** Update specific config fields in the selected raw scope. */
441
814
  export function updateConfig(
442
815
  paths: PlatformPaths,
443
816
  cwd: string,
444
817
  updates: Record<string, unknown>,
445
- ): SupipowersConfig {
446
- const current = loadConfig(paths, cwd);
447
- const updated = applyConfigOverride(
448
- structuredClone(current) as unknown as Record<string, unknown>,
818
+ options?: ConfigMutationOptions,
819
+ ): SupipowersConfig {
820
+ const { repoRoot } = resolveConfigContext(cwd, options);
821
+ const scope = options?.scope ?? "root";
822
+ const configPath = getConfigPath(paths, cwd, scope, options);
823
+ const current = readJsonFile(scope, configPath);
824
+ if (current.error) {
825
+ throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
826
+ }
827
+
828
+ const nextScopeData = applyConfigOverride(
829
+ current.data ? structuredClone(current.data) as Record<string, unknown> : {},
449
830
  updates,
450
831
  );
451
- assertValidConfig(updated);
452
832
 
453
- saveConfig(paths, cwd, updated as unknown as SupipowersConfig);
454
- return updated as unknown as SupipowersConfig;
833
+ const scopedLayerData = omitUnsupportedUltraPlanFromGlobalWrite(nextScopeData, scope);
834
+ const layers = readConfigLayers(paths, cwd, options);
835
+ const mergedConfig = mergeConfigLayers(
836
+ DEFAULT_CONFIG,
837
+ ...layers.map((layer) => layer.scope === scope ? scopedLayerData : layer.readResult.data),
838
+ );
839
+ const rawConfig = prepareScopedConfigWrite(mergedConfig, nextScopeData, repoRoot, scope);
840
+
841
+ writeRawConfigFile(configPath, rawConfig);
842
+ return mergedConfig as unknown as SupipowersConfig;
455
843
  }