@webpresso/agent-kit 3.1.30 → 3.2.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.

Potentially problematic release.


This version of @webpresso/agent-kit might be problematic. Click here for more details.

Files changed (257) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +179 -19
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  17. package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
  18. package/dist/esm/audit/ci-test-perf.js +30 -0
  19. package/dist/esm/audit/command-surface.js +4 -2
  20. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  21. package/dist/esm/audit/host-substitution-risk.js +256 -0
  22. package/dist/esm/audit/registry.d.ts +4 -4
  23. package/dist/esm/audit/registry.js +8 -1
  24. package/dist/esm/audit/toolchain-isolation.js +1 -2
  25. package/dist/esm/audit/typescript-version.d.ts +2 -0
  26. package/dist/esm/audit/typescript-version.js +123 -0
  27. package/dist/esm/blueprint/core/parser.js +43 -1
  28. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  29. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  30. package/dist/esm/blueprint/core/validation/state.js +25 -14
  31. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  32. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  33. package/dist/esm/blueprint/lifecycle/audit.js +65 -10
  34. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  35. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  36. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  37. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  38. package/dist/esm/blueprint/lifecycle/review-provenance.js +36 -11
  39. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  40. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  41. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  42. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  43. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  44. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  45. package/dist/esm/blueprint/trust/gates.js +20 -0
  46. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  47. package/dist/esm/blueprint/trust/promotion.js +62 -112
  48. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  49. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  50. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  51. package/dist/esm/blueprint/trust/validator.js +7 -1
  52. package/dist/esm/blueprint/utils/archive.js +27 -5
  53. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  54. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  55. package/dist/esm/build/cli-mcp-parity.js +10 -0
  56. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  57. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  58. package/dist/esm/cli/cli.d.ts +1 -1
  59. package/dist/esm/cli/cli.js +34 -0
  60. package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
  61. package/dist/esm/cli/commands/agent-launch.js +82 -4
  62. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  63. package/dist/esm/cli/commands/audit-core.js +8 -1
  64. package/dist/esm/cli/commands/audit.js +17 -5
  65. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  66. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  67. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  68. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  69. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  70. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  71. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  72. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  73. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  74. package/dist/esm/cli/commands/ci-preflight.js +6 -3
  75. package/dist/esm/cli/commands/compile.js +5 -31
  76. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  77. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  78. package/dist/esm/cli/commands/doctor.js +5 -0
  79. package/dist/esm/cli/commands/hook.d.ts +5 -0
  80. package/dist/esm/cli/commands/hook.js +15 -9
  81. package/dist/esm/cli/commands/init/config.js +2 -2
  82. package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
  83. package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
  84. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  85. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  86. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  87. package/dist/esm/cli/commands/init/convergence.js +57 -0
  88. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  89. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  90. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  91. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  92. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  93. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  94. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  95. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  96. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  97. package/dist/esm/cli/commands/init/index.d.ts +1 -0
  98. package/dist/esm/cli/commands/init/index.js +326 -173
  99. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  100. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  101. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  102. package/dist/esm/cli/commands/init/merge.js +30 -1
  103. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  104. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  105. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  106. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  107. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
  108. package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
  109. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  110. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  111. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  112. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
  113. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  114. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  115. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
  116. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
  117. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  118. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  119. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  120. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  121. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  122. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  123. package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
  124. package/dist/esm/cli/commands/opencode-rotate.js +135 -0
  125. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  126. package/dist/esm/cli/commands/public-readiness.js +42 -0
  127. package/dist/esm/cli/commands/review.d.ts +35 -3
  128. package/dist/esm/cli/commands/review.js +729 -209
  129. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  130. package/dist/esm/cli/commands/run-wait.js +122 -0
  131. package/dist/esm/cli/commands/setup-help.js +1 -1
  132. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  133. package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
  134. package/dist/esm/cli/direct-provider-launch.js +9 -5
  135. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  136. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  137. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  138. package/dist/esm/daemon/domains/git-actions.js +136 -0
  139. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  140. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  141. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  142. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  143. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  144. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  145. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  146. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  147. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  148. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  149. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  150. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  151. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  152. package/dist/esm/daemon/handlers/product.js +10 -119
  153. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  154. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  155. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  156. package/dist/esm/daemon/handlers/shared.js +16 -0
  157. package/dist/esm/daemon/methods.d.ts +8 -1
  158. package/dist/esm/daemon/methods.js +7 -0
  159. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  160. package/dist/esm/daemon/protocol/contract.js +26 -0
  161. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  162. package/dist/esm/daemon/protocol/index.js +1 -1
  163. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  164. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  165. package/dist/esm/hooks/conformance/batch.js +5 -0
  166. package/dist/esm/hooks/doctor.d.ts +3 -3
  167. package/dist/esm/hooks/doctor.js +30 -15
  168. package/dist/esm/hooks/permission-request/index.js +9 -3
  169. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  170. package/dist/esm/hooks/permission-request/policy.js +13 -0
  171. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  172. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  173. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  174. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  175. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  176. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  177. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  178. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  179. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  180. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  181. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  182. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  183. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  184. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  185. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  186. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  187. package/dist/esm/hooks/shared/types.d.ts +8 -5
  188. package/dist/esm/hooks/shared/types.js +23 -8
  189. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  190. package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
  191. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  192. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
  193. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  194. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  195. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  196. package/dist/esm/mcp/blueprint/registration.js +3 -2
  197. package/dist/esm/mcp/server.d.ts +1 -0
  198. package/dist/esm/mcp/server.js +27 -0
  199. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  200. package/dist/esm/mcp/tools/_names.js +5 -0
  201. package/dist/esm/mcp/tools/_registry.js +10 -0
  202. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  203. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  204. package/dist/esm/mcp/tools/audit.d.ts +4 -1
  205. package/dist/esm/mcp/tools/audit.js +30 -0
  206. package/dist/esm/mcp/tools/audits.d.ts +3 -0
  207. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  208. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  209. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  210. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  211. package/dist/esm/mcp/tools/review-run.js +216 -0
  212. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  213. package/dist/esm/mcp/tools/run-wait.js +593 -0
  214. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  215. package/dist/esm/mcp/tools/session-id.js +45 -0
  216. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  217. package/dist/esm/mcp/tools/session-info.js +49 -0
  218. package/dist/esm/output-transforms/audit.d.ts +23 -0
  219. package/dist/esm/output-transforms/audit.js +47 -0
  220. package/dist/esm/output-transforms/index.js +2 -0
  221. package/dist/esm/review/authority.js +13 -5
  222. package/dist/esm/review/availability.d.ts +22 -0
  223. package/dist/esm/review/availability.js +107 -20
  224. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  225. package/dist/esm/review/delivery-verifier.js +115 -0
  226. package/dist/esm/review/events.d.ts +3 -2
  227. package/dist/esm/review/events.js +23 -3
  228. package/dist/esm/review/execution/adapters.js +26 -9
  229. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  230. package/dist/esm/review/execution/output-policy.js +12 -0
  231. package/dist/esm/review/execution/review-checkout.js +87 -6
  232. package/dist/esm/review/execution/supervisor.js +5 -0
  233. package/dist/esm/review/execution/types.d.ts +8 -0
  234. package/dist/esm/review/opencode-account.d.ts +24 -0
  235. package/dist/esm/review/opencode-account.js +68 -0
  236. package/dist/esm/review/opencode-auth.d.ts +33 -0
  237. package/dist/esm/review/opencode-auth.js +79 -0
  238. package/dist/esm/review/subject.d.ts +12 -0
  239. package/dist/esm/review/subject.js +74 -22
  240. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  241. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  242. package/dist/esm/status/snapshot.js +30 -19
  243. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  244. package/dist/esm/test/duration-sequencer.js +54 -0
  245. package/dist/esm/test/shard-durations.json +922 -0
  246. package/dist/esm/test/worker-budget.d.ts +6 -0
  247. package/dist/esm/test/worker-budget.js +59 -1
  248. package/dist/esm/test-helpers/global-setup.d.ts +1 -1
  249. package/dist/esm/test-helpers/global-setup.js +68 -24
  250. package/dist/esm/ultragoal/service.d.ts +7 -0
  251. package/dist/esm/ultragoal/service.js +42 -2
  252. package/dist/esm/utils/write-json-file.js +3 -1
  253. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  254. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  255. package/dist/esm/worktrees/registry.d.ts +8 -0
  256. package/dist/esm/worktrees/registry.js +18 -3
  257. package/package.json +16 -13
@@ -2,6 +2,7 @@ import type { CAC } from "cac";
2
2
  import { type DashPermissionMode, type DashProvider } from "#cli/commands/dash/provider-capabilities.js";
3
3
  import { type RuntimeCommandOptions } from "#runtime/executor.js";
4
4
  import { type SecretsConfig } from "#runtime/secrets-config.js";
5
+ import { resolveOpencodeAccountsGracefully } from "#runtime/opencode-account-materializer.js";
5
6
  export declare const AGENT_LAUNCH_COMMANDS: readonly ["claude", "codex", "grok", "opencode"];
6
7
  export type AgentLaunchCommand = DashProvider;
7
8
  export declare const DEFAULT_AGENT_SECRET_PROFILE = "preview";
@@ -18,6 +19,10 @@ export interface AgentLaunchDeps {
18
19
  error?: Error;
19
20
  };
20
21
  readonly ensureCodexSkillSurface?: (cwd: string, env?: NodeJS.ProcessEnv) => string | null;
22
+ readonly resolveOpencodeAccounts?: typeof resolveOpencodeAccountsGracefully;
23
+ readonly materializeGlobalOpencodeAuth?: (key: string) => void;
24
+ readonly readActiveKey?: () => string | undefined;
25
+ readonly recordCooldown?: (canonicalId: string) => Promise<unknown>;
21
26
  }
22
27
  export declare function buildDefaultAgentArgs(command: AgentLaunchCommand, args?: readonly string[], permissionMode?: DashPermissionMode): readonly string[];
23
28
  export declare function isAgentLaunchCommand(command: string | undefined): command is AgentLaunchCommand;
@@ -29,5 +34,5 @@ export declare function buildAgentRuntimeCommandOptions(input: {
29
34
  readonly env?: NodeJS.ProcessEnv;
30
35
  readonly secretsConfig: SecretsConfig | null;
31
36
  }): RuntimeCommandOptions;
32
- export declare function runAgentLaunchCommand(command: AgentLaunchCommand, args?: readonly string[], options?: AgentLaunchOptions, deps?: AgentLaunchDeps): number;
37
+ export declare function runAgentLaunchCommand(command: AgentLaunchCommand, args?: readonly string[], options?: AgentLaunchOptions, deps?: AgentLaunchDeps): Promise<number>;
33
38
  export declare function registerAgentLaunchCommand(cli: CAC, command: AgentLaunchCommand): void;
@@ -7,6 +7,11 @@ import { WP_AGENT_LAUNCHED_VIA_WP_ENV, WP_AGENT_LAUNCHED_VIA_WP_VALUE, } from "#
7
7
  import { readSecretsConfig } from "#runtime/secrets-config.js";
8
8
  import { resolveFreshAgentKitPackageRoot } from "#cli/commands/init/package-root";
9
9
  import { ensureCodexUserPlugin } from "#cli/commands/init/scaffolders/codex-plugin/index.js";
10
+ import { resolveOpencodeAccountsGracefully } from "#runtime/opencode-account-materializer.js";
11
+ import { loadOpencodeAccountAvailability, pruneExpiredOpencodeAccountSuppressions, } from "#review/availability.js";
12
+ import { selectActiveOpencodeAccount } from "#review/opencode-account.js";
13
+ import { materializeOpencodeGoAuth } from "#review/opencode-auth.js";
14
+ import { describeOpencodeRotate, OPENCODE_ROTATE_FLAG, rotateActiveOpencodeAccount, } from "#cli/commands/opencode-rotate.js";
10
15
  export const AGENT_LAUNCH_COMMANDS = DASH_PROVIDERS;
11
16
  export const DEFAULT_AGENT_SECRET_PROFILE = "preview";
12
17
  export const AGENT_SECRET_PROFILE_ENV = "WP_AGENT_SECRET_PROFILE";
@@ -83,7 +88,67 @@ function ensureCodexSkillSurface(cwd, env = process.env) {
83
88
  return `Codex plugin refresh failed (${result.kind}). Run \`wp install codex\` and retry.`;
84
89
  }
85
90
  }
86
- export function runAgentLaunchCommand(command, args = [], options = {}, deps = {}) {
91
+ /**
92
+ * Choose the active opencode-go account and materialize its key into the
93
+ * global opencode `auth.json` before the TUI launches.
94
+ *
95
+ * Honest limitation: interactive opencode is a long-lived TUI — the account is
96
+ * chosen once at launch and a mid-session dollar-cap is NOT auto-swapped. A
97
+ * plain relaunch does NOT rotate either: nothing on this path records a
98
+ * cooldown, so an exhausted account stays "first non-suppressed" and gets
99
+ * re-picked every time. Run `wp opencode --wp-rotate` to cool the current
100
+ * account and launch on the next one. Only the review gate does automatic
101
+ * in-flight failover, because only it can observe the usage-limit response.
102
+ *
103
+ * Never throws: a Doppler outage or materialize failure must not block the
104
+ * TUI from launching, so every failure is logged and swallowed.
105
+ */
106
+ function selectAndMaterializeOpencodeAccount(cwd, deps, secretProfile) {
107
+ try {
108
+ const resolution = (deps.resolveOpencodeAccounts ?? resolveOpencodeAccountsGracefully)({
109
+ cwd,
110
+ secretProfile,
111
+ });
112
+ if (!resolution.ok)
113
+ return;
114
+ const pruned = pruneExpiredOpencodeAccountSuppressions(loadOpencodeAccountAvailability());
115
+ const selection = selectActiveOpencodeAccount(resolution.accounts, pruned.state);
116
+ if (!selection)
117
+ return;
118
+ (deps.materializeGlobalOpencodeAuth ?? ((key) => materializeOpencodeGoAuth(key)))(selection.account.key);
119
+ if (selection.cooled) {
120
+ console.error(`wp opencode: all OpenCode Go accounts are within their usage cooldown; launching on the soonest-expiring account (${selection.account.id}).`);
121
+ }
122
+ }
123
+ catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ console.error(`wp opencode: account selection failed, launching without it (${message}).`);
126
+ }
127
+ }
128
+ /**
129
+ * Run the opencode-only pre-launch credential step and return the args to
130
+ * forward to the binary. `--wp-rotate` is consumed here and must never reach
131
+ * opencode, which would treat it as a prompt.
132
+ */
133
+ async function prepareOpencodeLaunch(cwd, args, secretsConfig, deps, secretProfile) {
134
+ if (!args.includes(OPENCODE_ROTATE_FLAG)) {
135
+ if (secretsConfig !== null)
136
+ selectAndMaterializeOpencodeAccount(cwd, deps, secretProfile);
137
+ return args;
138
+ }
139
+ const outcome = await rotateActiveOpencodeAccount(cwd, secretProfile, deps);
140
+ for (const line of describeOpencodeRotate(outcome))
141
+ console.error(line);
142
+ // A successful rotation already materialized the next account, so the normal
143
+ // launch-time selection is skipped to avoid a second, redundant write. When
144
+ // rotation could not run, fall through to that selection so the launch still
145
+ // gets the best account it can rather than whatever the store happens to hold.
146
+ if (outcome.kind === "unavailable" && secretsConfig !== null) {
147
+ selectAndMaterializeOpencodeAccount(cwd, deps, secretProfile);
148
+ }
149
+ return args.filter((arg) => arg !== OPENCODE_ROTATE_FLAG);
150
+ }
151
+ export async function runAgentLaunchCommand(command, args = [], options = {}, deps = {}) {
87
152
  const cwd = options.cwd ?? process.cwd();
88
153
  if (command === "codex") {
89
154
  const skillSurfaceError = (deps.ensureCodexSkillSurface ?? ensureCodexSkillSurface)(cwd, options.env ?? process.env);
@@ -93,9 +158,19 @@ export function runAgentLaunchCommand(command, args = [], options = {}, deps = {
93
158
  }
94
159
  }
95
160
  const secretsConfig = (deps.readSecretsConfig ?? readSecretsConfig)(cwd);
161
+ // Account discovery must resolve secrets through the same profile the launch
162
+ // itself uses (below, via buildAgentRuntimeCommandOptions). Without it no
163
+ // environment selector reaches the secret manager, which then falls back to
164
+ // its own ambient, path-scoped config — that resolves only in a checkout that
165
+ // happens to sit under the scoped directory and fails in every managed
166
+ // worktree, CI clone, and fresh checkout.
167
+ const secretProfile = resolveAgentSecretProfile(options.env ?? process.env);
168
+ const launchArgs = command === "opencode"
169
+ ? await prepareOpencodeLaunch(cwd, args, secretsConfig, deps, secretProfile)
170
+ : args;
96
171
  const runtimeOptions = buildAgentRuntimeCommandOptions({
97
172
  command,
98
- args,
173
+ args: launchArgs,
99
174
  cwd,
100
175
  env: options.env ?? process.env,
101
176
  secretsConfig,
@@ -108,8 +183,11 @@ export function runAgentLaunchCommand(command, args = [], options = {}, deps = {
108
183
  return result.status ?? 1;
109
184
  }
110
185
  export function registerAgentLaunchCommand(cli, command) {
186
+ const rotateHint = command === "opencode"
187
+ ? ` (pass \`${OPENCODE_ROTATE_FLAG}\` to cool the current OpenCode Go account and launch on the next one)`
188
+ : "";
111
189
  cli
112
- .command(`${command} [...args]`, `Launch ${command} with provider-backed agent secrets and safe-by-default permissions (run \`wp yolo on\` for persistent full-auto)`)
190
+ .command(`${command} [...args]`, `Launch ${command} with provider-backed agent secrets and safe-by-default permissions (run \`wp yolo on\` for persistent full-auto)${rotateHint}`)
113
191
  .allowUnknownOptions()
114
- .action((args = []) => runAgentLaunchCommand(command, args, { cwd: process.cwd() }));
192
+ .action(async (args = []) => await runAgentLaunchCommand(command, args, { cwd: process.cwd() }));
115
193
  }
@@ -1,4 +1,4 @@
1
- import type { AuditKindId } from "#audit/registry";
1
+ import { type AuditKindId } from "#audit/registry";
2
2
  import type { RepoAuditResult } from "#audit/repo-guardrails";
3
3
  export type AuditKind = AuditKindId;
4
4
  export type AuditOutcome = {
@@ -1,3 +1,4 @@
1
+ import { getAuditKindDescriptor } from "#audit/registry";
1
2
  function optionsForCompositeRepoAudit(name, options) {
2
3
  if (name !== "reference-parity-matrix" || options.strict !== true)
3
4
  return options;
@@ -94,7 +95,13 @@ export async function runAuditDispatch(auditKind, targets, options, deps) {
94
95
  let allOk = true;
95
96
  for (const name of deps.knownRepoKinds) {
96
97
  const result = await deps.runRepoAudit(name, root, optionsForCompositeRepoAudit(name, options));
97
- if (!result.ok)
98
+ // Signal-class kinds (registry.ts gateClass) still run and still
99
+ // report their findings via `results` below, but a failing signal
100
+ // kind must not flip the umbrella — only blocking-class failures do.
101
+ // Unclassified kinds default to blocking, matching the registry's
102
+ // own descriptor() default.
103
+ const gateClass = getAuditKindDescriptor(name)?.gateClass ?? "blocking";
104
+ if (!result.ok && gateClass !== "signal")
98
105
  allOk = false;
99
106
  results.push({ name, result });
100
107
  }
@@ -15,6 +15,7 @@ import { createCliLogSink } from "#cli/commands/quality-log-store.js";
15
15
  import { emitCliCommandOutput, runLoggedChildCommand } from "#cli/commands/quality-runner.js";
16
16
  export const REPO_AUDIT_REGISTRY = {
17
17
  "catalog-drift": async (root) => (await import("#audit/repo-guardrails")).auditCatalogDrift(root),
18
+ "typescript-version": async (root) => (await import("#audit/typescript-version")).auditTypescriptVersion(root),
18
19
  "package-surface": async (root) => (await import("#audit/package-surface")).auditPackageSurface(root),
19
20
  "command-surface": async (root) => (await import("#audit/command-surface")).auditCommandSurface(root),
20
21
  "reference-parity-matrix": async (root, options) => (await import("#audit/reference-parity-matrix")).auditReferenceParityMatrix(root, undefined, {
@@ -123,6 +124,7 @@ export const REPO_AUDIT_REGISTRY = {
123
124
  "secrets-config": async (root) => (await import("#audit/secrets-config")).auditSecretsConfig(root),
124
125
  "consumer-agent-kit-dependency": async (root) => (await import("#audit/consumer-agent-kit-dependency")).auditConsumerAgentKitDependency(root),
125
126
  "ci-test-perf": async (root) => (await import("#audit/ci-test-perf")).auditCiTestPerf(root),
127
+ "ci-guardrails-wiring": async (root) => (await import("#audit/ci-guardrails-wiring")).auditCiGuardrailsWiring(root),
126
128
  "test-scan-perf": async (root, options) => (await import("#audit/test-scan-perf")).auditTestScanPerf(root, {
127
129
  affectedFiles: options.affectedFiles,
128
130
  }),
@@ -140,6 +142,7 @@ export const REPO_AUDIT_REGISTRY = {
140
142
  draftTechDebt: options.draftTechDebt,
141
143
  }),
142
144
  "harness-overlay-evidence": async (root) => (await import("#audit/harness-overlay-evidence")).auditHarnessOverlayEvidence(root),
145
+ "host-substitution-risk": async (root) => (await import("#audit/host-substitution-risk")).auditHostSubstitutionRisk(root),
143
146
  "legacy-workflow-identity": async (root) => (await import("#audit/legacy-workflow-identity")).auditLegacyWorkflowIdentity(root),
144
147
  rules: async (root) => runContentAudit(root, "rule"),
145
148
  skills: async (root) => runContentAudit(root, "skill"),
@@ -467,22 +470,31 @@ export function registerAuditCommand(cli) {
467
470
  }
468
471
  case "aggregate-result": {
469
472
  const { formatRepoAuditReport } = await import("#audit/repo-guardrails");
473
+ const isSignal = (name) => getAuditKindDescriptor(name)?.gateClass === "signal";
470
474
  for (const { name, result } of outcome.results) {
471
475
  if (result.ok)
472
476
  continue;
473
- sink.write(`\n[${name}]\n${formatRepoAuditReport(result)}\n`);
477
+ const label = isSignal(name) ? `${name} (signal, non-blocking)` : name;
478
+ sink.write(`\n[${label}]\n${formatRepoAuditReport(result)}\n`);
474
479
  }
475
480
  const failed = outcome.results.filter(({ result }) => !result.ok);
481
+ const blockingFailed = failed.filter(({ name }) => !isSignal(name));
482
+ const signalFailed = failed.filter(({ name }) => isSignal(name));
476
483
  if (failed.length > 0) {
477
- sink.write(`\nguardrails: ${failed.length}/${outcome.results.length} audits failed: ${failed
478
- .map(({ name }) => name)
479
- .join(", ")}\n`);
484
+ const parts = [];
485
+ if (blockingFailed.length > 0) {
486
+ parts.push(`${blockingFailed.length} blocking: ${blockingFailed.map(({ name }) => name).join(", ")}`);
487
+ }
488
+ if (signalFailed.length > 0) {
489
+ parts.push(`${signalFailed.length} signal/non-blocking: ${signalFailed.map(({ name }) => name).join(", ")}`);
490
+ }
491
+ sink.write(`\nguardrails: ${failed.length}/${outcome.results.length} audits reported findings (${parts.join("; ")})\n`);
480
492
  }
481
493
  exitCode = outcome.code;
482
494
  summary =
483
495
  outcome.code === 0
484
496
  ? "audit guardrails passed"
485
- : `audit guardrails failed (${failed.length}/${outcome.results.length})`;
497
+ : `audit guardrails failed (${blockingFailed.length}/${outcome.results.length})`;
486
498
  break;
487
499
  }
488
500
  case "quality-exit": {
@@ -9,6 +9,7 @@ import { setTimeout as delay } from "node:timers/promises";
9
9
  import { applyRuntimeProgressSnapshot, buildBlueprintProgressBridgeState, normalizeRuntimeTaskSnapshot, projectBlueprintLifecycleFromRuntime, readBlueprintExecutionArtifacts, runtimeStateSnapshotSchema, writeBlueprintExecutionArtifacts, writeBlueprintExecutionMetadata, } from "#index";
10
10
  import { applyBlueprintLifecycleToFile, parseBlueprint } from "#local";
11
11
  import { resolveBlueprintRoot } from "#utils/blueprint-root";
12
+ import { resolveBlueprintDocumentIdentity } from "#utils/document-paths.js";
12
13
  import { clearBlueprintExecutionState, persistBlueprintExecutionArtifacts, persistBlueprintExecutionMetadata, persistBlueprintProgressBridgeState, readBlueprintExecutionArtifactsState, readBlueprintExecutionState, readBlueprintProgressBridgeState, readBlueprintRuntimeSnapshot, writeBlueprintRuntimeSnapshot, } from "./execution-io.js";
13
14
  import { assertCompletionEvidence, mergeExecutionArtifacts, } from "./execution-state.js";
14
15
  import { buildBlueprintExecutionControlCommand, buildBlueprintExecutionLaunchCommand, buildBlueprintExecutionRuntimePaths, buildBlueprintLaunchSpec, buildListTasksVerificationCommand, isMissingFileError, nowIsoTimestamp, parseRuntimeApiResponse, parseTeamExecutionId, resolveControlStatus, toProjectRelativePath, uniqueStrings, } from "./execution-spec.js";
@@ -114,22 +115,33 @@ export async function reconcileBlueprintRuntimeSnapshot(projectRoot, blueprintPa
114
115
  const currentStatus = currentDir.split(blueprintsRoot + path.sep)[1]?.split(path.sep)[0];
115
116
  const relativeSlug = slug.replace(/^[^/]+\//u, "");
116
117
  const targetDir = path.join(blueprintsRoot, nextStatus, relativeSlug);
117
- const targetPath = path.join(targetDir, "_overview.md");
118
+ // A flat document owns only its own file; renaming its parent directory
119
+ // would move the entire lifecycle state directory. Lifecycle moves are
120
+ // shape-preserving: flat stays flat, folder moves as a folder.
121
+ const isFlatDocument = resolveBlueprintDocumentIdentity(blueprintsRoot, blueprintPath)?.shape === "flat";
122
+ const targetPath = isFlatDocument ? `${targetDir}.md` : path.join(targetDir, "_overview.md");
123
+ const documentMoved = blueprintPath !== targetPath;
118
124
  const nextMarkdown = completionEvidence
119
125
  ? writeBlueprintExecutionArtifacts(result.markdown, completionEvidence)
120
126
  : result.markdown;
121
- if (currentDir !== targetDir && currentStatus && currentStatus !== nextStatus) {
127
+ if (documentMoved && currentStatus && currentStatus !== nextStatus) {
122
128
  const { mkdir, rename } = await import("node:fs/promises");
123
- await mkdir(path.dirname(targetDir), { recursive: true });
124
- await rename(currentDir, targetDir);
129
+ if (isFlatDocument) {
130
+ await mkdir(path.dirname(targetPath), { recursive: true });
131
+ await rename(blueprintPath, targetPath);
132
+ }
133
+ else {
134
+ await mkdir(path.dirname(targetDir), { recursive: true });
135
+ await rename(currentDir, targetDir);
136
+ }
125
137
  await writeFile(targetPath, nextMarkdown, "utf-8");
126
138
  }
127
139
  else {
128
140
  await writeFile(blueprintPath, nextMarkdown, "utf-8");
129
141
  }
130
142
  return {
131
- moved: currentDir !== targetDir,
132
- path: currentDir !== targetDir ? targetPath : blueprintPath,
143
+ moved: documentMoved,
144
+ path: documentMoved ? targetPath : blueprintPath,
133
145
  status: result.execution.status,
134
146
  };
135
147
  }
@@ -24,9 +24,9 @@ import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
24
24
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
25
25
  import { reIngestProjection } from "#projection-ready.js";
26
26
  import { assertAllTasksHaveCanonicalPassingEvidence } from "#verification.js";
27
- import { applyPromotionTrustGate } from "#trust/promotion.js";
27
+ import { assertPromotionTrustProofCurrent, preparePromotionTrustGate, } from "#trust/promotion.js";
28
28
  import matter from "gray-matter";
29
- import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
29
+ import { assertCompletedApprovalGate, evaluateApprovalGate, formatApprovalGateRequirement, } from "#lifecycle/audit";
30
30
  /**
31
31
  * Module-level factory. `null` = use the production default (loadSyncCredentials
32
32
  * from auth.ts + BlueprintSyncClient + ReplicaManager — lazy-imported so that
@@ -321,24 +321,38 @@ async function advanceTaskLocked(cwd, blueprintSlug, taskId, toStatus) {
321
321
  * - Re-ingests into DB
322
322
  */
323
323
  export async function promoteBlueprint(cwd, slug, toState) {
324
+ const adapter = await resolveSyncAdapterForCli(cwd);
325
+ // Pull platform state under the mutation lock, then release it before any
326
+ // Trust Dossier command spawns a child `wp`. Child startup/ingestion can
327
+ // legitimately need the same repo-scoped lock.
328
+ await withMarkdownWriteLock(cwd, () => runCliPlatformMutationSync(adapter, {
329
+ label: "wp_blueprint_promote",
330
+ ensureFreshSlug: slug,
331
+ }));
332
+ const found = findBlueprintDocument(resolveBlueprintRoot(cwd), slug);
333
+ if (!found) {
334
+ throw new Error(`Blueprint "${slug}" not found in any state directory under ${resolveBlueprintRoot(cwd)}`);
335
+ }
336
+ const crossesDraftApprovalBoundary = found.state === "draft" && (toState === "planned" || toState === "in-progress");
337
+ const trustProof = crossesDraftApprovalBoundary
338
+ ? preparePromotionTrustGate({
339
+ repoRoot: cwd,
340
+ file: found.documentPath,
341
+ markdown: readFileSync(found.documentPath, "utf8"),
342
+ })
343
+ : null;
324
344
  // F9/R7: cross-worktree markdown writes serialize via the repo-scoped lock.
325
- return withMarkdownWriteLock(cwd, () => promoteBlueprintLocked(cwd, slug, toState));
345
+ // The proof is revalidated inside this lock before any lifecycle write.
346
+ return withMarkdownWriteLock(cwd, () => promoteBlueprintLocked(cwd, slug, toState, adapter, trustProof));
326
347
  }
327
- async function promoteBlueprintLocked(cwd, slug, toState) {
348
+ async function promoteBlueprintLocked(cwd, slug, toState, adapter, trustProof) {
328
349
  const blueprintRoot = resolveBlueprintRoot(cwd);
329
350
  const found = findBlueprintDocument(blueprintRoot, slug);
330
351
  if (!found) {
331
352
  throw new Error(`Blueprint "${slug}" not found in any state directory under ${blueprintRoot}`);
332
353
  }
333
354
  const { dir: currentDir, documentPath: currentDocumentPath, shape, state: currentState } = found;
334
- // Guard: refuse to complete if any tasks are not done/dropped.
335
- // Markdown `_overview.md` is SSOT for task status + verification evidence.
336
- // The projection DB is derived and can lag when agents edit task Status
337
- // blocks without going through `task complete` (hash-skipping reingest then
338
- // leaves stale todo rows). Do not fail completion on projection lag —
339
- // reingest after the markdown gate so consumers see fresh rows.
340
- if (toState === "completed") {
341
- const markdown = readFileSync(currentDocumentPath, "utf8");
355
+ const assertCompletionReady = (markdown) => {
342
356
  const blueprint = parseBlueprint(markdown, slug);
343
357
  if (blueprint.tasks.length === 0) {
344
358
  throw new Error(`Cannot promote "${slug}" to completed: zero-task blueprints cannot complete through the public lifecycle surface`);
@@ -349,30 +363,30 @@ async function promoteBlueprintLocked(cwd, slug, toState) {
349
363
  throw new Error(`Cannot promote "${slug}" to completed: the following tasks are not done/dropped: ${list}`);
350
364
  }
351
365
  assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status === "done").map((task) => task.id));
352
- // Best-effort projection refresh. Clear content hash first so a lagging
353
- // tasks table is rewritten even when a prior corrupt UPDATE left the
354
- // stored hash equal to current markdown.
355
- await forceReIngestBlueprint(cwd, slug, currentDocumentPath);
356
- }
357
- // Platform freshness must be resolved before trust validation so the proof
358
- // applies to the current replica, not stale local markdown.
359
- // Iron rule: resolveSyncAdapterForCli() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
360
- const adapter = await resolveSyncAdapterForCli(cwd);
361
- await runCliPlatformMutationSync(adapter, {
362
- label: "wp_blueprint_promote",
363
- ensureFreshSlug: slug,
364
- });
366
+ // Completion parity with CI: the CLI completion owner uses the same
367
+ // audit-owned gate as the MCP funnel and execution engine, so all normal
368
+ // paths into `completed` enforce the same plan-purpose approval.
369
+ try {
370
+ assertCompletedApprovalGate(currentDocumentPath, markdown);
371
+ }
372
+ catch (error) {
373
+ throw new Error(`Cannot complete "${slug}": ${error instanceof Error ? error.message : String(error)}`);
374
+ }
375
+ };
365
376
  // Trust must be proven before any platform status_changed event is published.
366
377
  let content = readFileSync(currentDocumentPath, "utf8");
367
378
  let trustedSource = content;
379
+ if (toState === "completed") {
380
+ assertCompletionReady(content);
381
+ }
368
382
  const crossesDraftApprovalBoundary = currentState === "draft" && (toState === "planned" || toState === "in-progress");
369
383
  if (crossesDraftApprovalBoundary) {
370
- content = applyPromotionTrustGate({
371
- repoRoot: cwd,
372
- file: currentDocumentPath,
373
- markdown: content,
374
- });
375
- trustedSource = readFileSync(currentDocumentPath, "utf8");
384
+ if (trustProof === null) {
385
+ throw new Error("Blueprint trust gate invalidated: no external gate proof is available");
386
+ }
387
+ assertPromotionTrustProofCurrent({ repoRoot: cwd, file: currentDocumentPath, markdown: content }, trustProof);
388
+ content = trustProof.trustedMarkdown;
389
+ trustedSource = trustProof.sourceMarkdown;
376
390
  // Governance Piece 1 — HARD gate at the promotion boundary. One
377
391
  // provenance-backed Claude/Codex/Grok or OpenCode-Go model approval can
378
392
  // promote. Check AFTER the trust gate so trust failures surface first.
@@ -402,18 +416,23 @@ async function promoteBlueprintLocked(cwd, slug, toState) {
402
416
  if (crossesDraftApprovalBoundary) {
403
417
  const refreshedSource = readFileSync(currentDocumentPath, "utf8");
404
418
  if (refreshedSource !== trustedSource) {
405
- content = applyPromotionTrustGate({
406
- repoRoot: cwd,
407
- file: currentDocumentPath,
408
- markdown: refreshedSource,
409
- });
410
- const approvalEvaluation = evaluateApprovalGate(currentDocumentPath, matter(content).data.approvals);
411
- if (!approvalEvaluation.satisfied) {
412
- throw new Error(`Cannot promote "${slug}" to ${toState}: frontmatter \`approvals:\` does not satisfy the promotion reviewer mix (${formatApprovalGateRequirement(approvalEvaluation)}). Record approvals, commit the matching review ledger (e.g. reviews.md), and include tracked review transcript/artifact files referenced by each structured review entry — see catalog/agent/rules/pre-implementation.md.`);
413
- }
419
+ throw new Error(`Cannot promote "${slug}" to ${toState}: blueprint changed after external trust gates ran; retry promotion`);
414
420
  }
421
+ assertPromotionTrustProofCurrent({ repoRoot: cwd, file: currentDocumentPath, markdown: refreshedSource }, trustProof);
422
+ }
423
+ if (toState === "completed") {
424
+ // The platform pull can replace the document after the first completion
425
+ // check. Re-read and re-assert against the exact content written below.
426
+ content = readFileSync(currentDocumentPath, "utf8");
427
+ assertCompletionReady(content);
415
428
  }
416
429
  }
430
+ if (toState === "completed") {
431
+ // Best-effort projection refresh. Clear content hash first so a lagging
432
+ // tasks table is rewritten even when a prior corrupt UPDATE left the
433
+ // stored hash equal to current markdown.
434
+ await forceReIngestBlueprint(cwd, slug, currentDocumentPath);
435
+ }
417
436
  // Always update local markdown + SQLite.
418
437
  // Platform-first: these become derived artifacts; disabled: these are canonical.
419
438
  // Update frontmatter in the current location first, then move
@@ -1,6 +1,6 @@
1
1
  import type { BlueprintAuditResult, BlueprintSummary } from "#local";
2
2
  import type { BlueprintTemplateEntry } from "#sync/types.js";
3
- import type { AdvanceTaskResult, BlueprintCommandOptions, BlueprintLifecycleMutationResult, CreateBlueprintResult, ExportBlueprintResult, ExecuteBlueprintResult, MoveBlueprintResult, PromoteBlueprintResult, ShowBlueprintResult } from "./router.js";
3
+ import type { AdvanceTaskResult, BlueprintCommandOptions, BlueprintCanonicalizeResult, BlueprintLifecycleMutationResult, CreateBlueprintResult, ExportBlueprintResult, ExecuteBlueprintResult, MoveBlueprintResult, PromoteBlueprintResult, ShowBlueprintResult } from "./router.js";
4
4
  /**
5
5
  * Override the platform template fetcher — for tests only.
6
6
  * Pass `null` to restore the production default.
@@ -21,6 +21,7 @@ export declare class BlueprintAuditFailedError extends Error {
21
21
  interface BlueprintCommandDependencies {
22
22
  advanceBlueprintTask: (slug: string, taskId: string, toStatus: string, options: BlueprintCommandOptions) => Promise<AdvanceTaskResult>;
23
23
  auditBlueprints: (options: BlueprintCommandOptions) => Promise<BlueprintAuditResult>;
24
+ canonicalizeBlueprintBySlug: (slug: string, options: BlueprintCommandOptions) => Promise<BlueprintCanonicalizeResult>;
24
25
  controlBlueprintExec: (action: "status" | "resume" | "stop", slug: string, options: BlueprintCommandOptions) => Promise<ExecuteBlueprintResult>;
25
26
  readBlueprintExecutionLogs: (slug: string, options: BlueprintCommandOptions) => Promise<ExecuteBlueprintResult>;
26
27
  createBlueprint: (goal: string, options: BlueprintCommandOptions) => Promise<CreateBlueprintResult>;
@@ -3,7 +3,9 @@ import { tmpdir } from "node:os";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { executeBlueprintDbSubcommand } from "./db-commands.js";
6
+ import { getBlueprintSubcommandNames } from "./router-output.js";
6
7
  import { listTemplates, resolveTemplate } from "./template-resolver.js";
8
+ import { TRUST_DOSSIER_SCAFFOLD } from "#trust/scaffold.js";
7
9
  // ---------------------------------------------------------------------------
8
10
  // Platform template fetcher (injectable for tests)
9
11
  // ---------------------------------------------------------------------------
@@ -232,6 +234,15 @@ export async function executeBlueprintSubcommand(subcommand, args, options, deps
232
234
  deps.printBlueprintOutput(options.json ? result : result.message, options.json);
233
235
  return;
234
236
  }
237
+ case "canonicalize": {
238
+ const slug = args[0];
239
+ if (!slug) {
240
+ throw new Error("Usage: wp blueprint canonicalize <slug>");
241
+ }
242
+ const result = await deps.canonicalizeBlueprintBySlug(slug, options);
243
+ deps.printBlueprintOutput(options.json ? { from: result.from, to: result.to, moved: result.moved } : result.message, options.json);
244
+ return;
245
+ }
235
246
  case "start": {
236
247
  const slug = args[0];
237
248
  if (!slug) {
@@ -350,8 +361,16 @@ export async function executeBlueprintSubcommand(subcommand, args, options, deps
350
361
  deps.printBlueprintOutput(options.json ? result : result.message, options.json);
351
362
  return;
352
363
  }
364
+ case "trust-dossier": {
365
+ // Read-only: prints an already-valid "## Trust Dossier" starter block
366
+ // (src/blueprint/trust/scaffold.ts). No file writes, no worktree/lock
367
+ // interaction — an author pastes the output into their overview and
368
+ // edits the example values.
369
+ deps.printBlueprintOutput(options.json ? { markdown: TRUST_DOSSIER_SCAFFOLD } : TRUST_DOSSIER_SCAFFOLD, options.json);
370
+ return;
371
+ }
353
372
  default: {
354
- throw new Error(`Unknown blueprint subcommand: ${subcommand}\n\nUse one of: list, new, show, exec, start, park, task, finalize, promote, audit, move, control, logs, db, export`);
373
+ throw new Error(`Unknown blueprint subcommand: ${subcommand}\n\nUse one of: ${getBlueprintSubcommandNames().join(", ")}`);
355
374
  }
356
375
  }
357
376
  }
@@ -1,7 +1,28 @@
1
1
  import type { Blueprint, BlueprintAuditResult, BlueprintSummary } from "#local";
2
2
  import type { CreateBlueprintResult, ExecuteBlueprintResult, ShowBlueprintResult } from "./router.js";
3
+ /**
4
+ * Canonical registry of every subcommand `router-dispatch.ts` actually
5
+ * dispatches. This is the single source of truth for both the `--help`
6
+ * `Commands:` block (see `BLUEPRINT_HELP` below) and the unknown-subcommand
7
+ * error list — so a subcommand can't ship dispatched-but-undocumented
8
+ * again. `router-dispatch.test.ts` asserts this list stays equal to the
9
+ * live `case "…":` labels in the dispatch switch.
10
+ */
11
+ export interface BlueprintSubcommandSpec {
12
+ /** Matches a `case "<name>":` label in router-dispatch.ts exactly. */
13
+ readonly name: string;
14
+ /** One or more `Commands:` lines (without the leading indent). */
15
+ readonly usage: readonly string[];
16
+ }
17
+ export declare const BLUEPRINT_SUBCOMMANDS: readonly BlueprintSubcommandSpec[];
3
18
  export declare function formatTaskLine(task: Blueprint["tasks"][number]): string;
4
19
  export declare function getBlueprintHelpText(): string;
20
+ /**
21
+ * Subcommand names in registry order, for the unknown-subcommand error
22
+ * message. Kept as a function (rather than exporting a derived constant)
23
+ * so the source of truth stays singular: `BLUEPRINT_SUBCOMMANDS`.
24
+ */
25
+ export declare function getBlueprintSubcommandNames(): readonly string[];
5
26
  export interface BlueprintInventorySummary {
6
27
  total: number;
7
28
  byStatus: Record<string, number>;
@@ -1,25 +1,48 @@
1
1
  import { buildRoadmapModel, planStatusSchema } from "#local";
2
+ export const BLUEPRINT_SUBCOMMANDS = [
3
+ { name: "list", usage: ["list [status]"] },
4
+ {
5
+ name: "new",
6
+ usage: ["new <goal> --complexity <XS|S|M|L|XL> [--type <blueprint|parent-roadmap>]"],
7
+ },
8
+ { name: "show", usage: ["show <slug>"] },
9
+ {
10
+ name: "exec",
11
+ usage: [
12
+ "exec <slug>",
13
+ "exec status <slug>",
14
+ "exec resume <slug>",
15
+ "exec stop <slug>",
16
+ "exec logs <slug>",
17
+ ],
18
+ },
19
+ { name: "start", usage: ["start <slug>"] },
20
+ { name: "park", usage: ["park <slug>"] },
21
+ {
22
+ name: "task",
23
+ usage: [
24
+ "task start <slug> <taskId>",
25
+ "task block <slug> <taskId> --reason <text>",
26
+ "task unblock <slug> <taskId>",
27
+ "task complete <slug> <taskId>",
28
+ ],
29
+ },
30
+ { name: "finalize", usage: ["finalize <slug>"] },
31
+ { name: "promote", usage: ["promote <slug> <planned|in-progress|completed|parked>"] },
32
+ { name: "audit", usage: ["audit [--staged|--all] [--strict]"] },
33
+ { name: "move", usage: ["move <slug> <status> --force-recovery"] },
34
+ { name: "canonicalize", usage: ["canonicalize <slug>"] },
35
+ { name: "control", usage: ["control <status|resume|stop> <slug>"] },
36
+ { name: "logs", usage: ["logs <slug>"] },
37
+ { name: "db", usage: ["db <verb> [...args]"] },
38
+ { name: "export", usage: ["export --format spec-kit <slug>"] },
39
+ { name: "trust-dossier", usage: ["trust-dossier"] },
40
+ ];
2
41
  const BLUEPRINT_HELP = [
3
42
  "Blueprint management",
4
43
  "",
5
44
  "Commands:",
6
- " list [status]",
7
- " new <goal> --complexity <XS|S|M|L|XL> [--type <blueprint|parent-roadmap>]",
8
- " show <slug>",
9
- " exec <slug>",
10
- " exec status <slug>",
11
- " exec resume <slug>",
12
- " exec stop <slug>",
13
- " exec logs <slug>",
14
- " start <slug>",
15
- " park <slug>",
16
- " task start <slug> <taskId>",
17
- " task block <slug> <taskId> --reason <text>",
18
- " task unblock <slug> <taskId>",
19
- " task complete <slug> <taskId>",
20
- " finalize <slug>",
21
- " audit [--staged|--all] [--strict]",
22
- " move <slug> <status> --force-recovery",
45
+ ...BLUEPRINT_SUBCOMMANDS.flatMap((spec) => spec.usage.map((line) => ` ${line}`)),
23
46
  ].join("\n");
24
47
  export function formatTaskLine(task) {
25
48
  const checkbox = task.status === "done" || task.status === "dropped" ? "x" : " ";
@@ -28,6 +51,14 @@ export function formatTaskLine(task) {
28
51
  export function getBlueprintHelpText() {
29
52
  return BLUEPRINT_HELP;
30
53
  }
54
+ /**
55
+ * Subcommand names in registry order, for the unknown-subcommand error
56
+ * message. Kept as a function (rather than exporting a derived constant)
57
+ * so the source of truth stays singular: `BLUEPRINT_SUBCOMMANDS`.
58
+ */
59
+ export function getBlueprintSubcommandNames() {
60
+ return BLUEPRINT_SUBCOMMANDS.map((spec) => spec.name);
61
+ }
31
62
  export function summarizeBlueprintInventory(summaries) {
32
63
  const byStatus = Object.fromEntries(planStatusSchema.options.toSorted().map((status) => [status, 0]));
33
64
  const byType = {
@@ -73,6 +73,12 @@ export interface BlueprintLifecycleMutationResult {
73
73
  status: string;
74
74
  taskId?: string;
75
75
  }
76
+ export interface BlueprintCanonicalizeResult {
77
+ from: string;
78
+ to: string;
79
+ moved: boolean;
80
+ message: string;
81
+ }
76
82
  export interface CreateBlueprintResult extends CreatedBlueprint {
77
83
  message: string;
78
84
  /** True when draft was auto-routed off a primary-like checkout (H-009 DX). */
@@ -123,6 +129,7 @@ export declare function exportBlueprint(slug: string, format: string, options?:
123
129
  export declare function advanceBlueprintTask(slug: string, taskId: string, toStatus: string, options?: BlueprintMoveOptions): Promise<AdvanceTaskResult>;
124
130
  export declare function promoteBlueprintToState(slug: string, toState: string, options?: BlueprintMoveOptions): Promise<PromoteBlueprintResult>;
125
131
  export declare function finalizeBlueprintBySlug(slug: string, options?: BlueprintMoveOptions): Promise<PromoteBlueprintResult>;
132
+ export declare function canonicalizeBlueprintBySlug(slug: string, options?: BlueprintMoveOptions): Promise<BlueprintCanonicalizeResult>;
126
133
  export declare function registerBlueprintRouter(cli: CAC): void;
127
134
  export declare function _setBlueprintExecCommandExistsForTests(probe: ((command: string) => boolean) | null): void;
128
135
  export declare function _setBlueprintExecStarterForTests(starter: ((slug: string, options: BlueprintMoveOptions) => Promise<BlueprintLifecycleMutationResult>) | null): void;