create-principles-disciple 1.101.0 → 1.102.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 (228) hide show
  1. package/console/dist/server/index.js +9 -0
  2. package/console/dist/server/models/ActivationsConsoleModel.d.ts +37 -1
  3. package/console/dist/server/models/ActivationsConsoleModel.js +114 -13
  4. package/console/dist/server/routes/onboarding.d.ts +34 -0
  5. package/console/dist/server/routes/onboarding.js +324 -0
  6. package/console/dist/ui/App.js +74 -15
  7. package/console/dist/ui/api.d.ts +17 -1
  8. package/console/dist/ui/api.js +6 -1
  9. package/console/dist/ui/components/onboarding/CircuitDiagram.d.ts +8 -0
  10. package/console/dist/ui/components/onboarding/CircuitDiagram.js +18 -0
  11. package/console/dist/ui/components/onboarding/DemoResultView.d.ts +22 -0
  12. package/console/dist/ui/components/onboarding/DemoResultView.js +15 -0
  13. package/console/dist/ui/i18n/en.json +127 -0
  14. package/console/dist/ui/i18n/zh-CN.json +127 -0
  15. package/console/dist/ui/pages/activation/ActivationValidators.js +1 -1
  16. package/console/dist/ui/pages/control-center/AgentCard.js +10 -3
  17. package/console/dist/ui/pages/control-center/ControlCenterPage.js +1 -1
  18. package/console/dist/ui/pages/settings/SettingsPage.js +64 -5
  19. package/console/dist/ui/pages/signal-keywords/KeywordEditDialog.d.ts +12 -0
  20. package/console/dist/ui/pages/signal-keywords/KeywordEditDialog.js +41 -0
  21. package/console/dist/ui/pages/signal-keywords/KeywordListSection.d.ts +10 -0
  22. package/console/dist/ui/pages/signal-keywords/KeywordListSection.js +39 -0
  23. package/console/dist/ui/pages/signal-keywords/PendingTermsSection.d.ts +14 -0
  24. package/console/dist/ui/pages/signal-keywords/PendingTermsSection.js +35 -0
  25. package/console/dist/ui/pages/signal-keywords/SignalKeywordsPage.d.ts +1 -0
  26. package/console/dist/ui/pages/signal-keywords/SignalKeywordsPage.js +49 -0
  27. package/console/dist/ui/pages/welcome/WelcomePage.d.ts +5 -0
  28. package/console/dist/ui/pages/welcome/WelcomePage.js +185 -0
  29. package/console/dist/ui/utils/__tests__/onboarding-state.test.d.ts +1 -0
  30. package/console/dist/ui/utils/__tests__/onboarding-state.test.js +72 -0
  31. package/console/dist/ui/utils/agent-metadata.d.ts +13 -0
  32. package/console/dist/ui/utils/agent-metadata.js +26 -0
  33. package/console/dist/ui/utils/onboarding-state.d.ts +25 -0
  34. package/console/dist/ui/utils/onboarding-state.js +77 -0
  35. package/console/dist/ui/utils/signal-keywords-api.d.ts +68 -0
  36. package/console/dist/ui/utils/signal-keywords-api.js +121 -0
  37. package/console/dist/ui/utils/signal-keywords-types.d.ts +57 -0
  38. package/console/dist/ui/utils/signal-keywords-types.js +10 -0
  39. package/console/dist/ui/utils/signal-keywords-validators.d.ts +20 -0
  40. package/console/dist/ui/utils/signal-keywords-validators.js +114 -0
  41. package/console/dist/web/assets/app.css +401 -0
  42. package/console/dist/web/assets/app.js +6904 -1835
  43. package/core/dist/index.d.ts +2 -0
  44. package/core/dist/index.d.ts.map +1 -1
  45. package/core/dist/index.js +1 -0
  46. package/core/dist/index.js.map +1 -1
  47. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +14 -2
  48. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  49. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js +28 -0
  50. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js.map +1 -1
  51. package/core/dist/runtime-v2/__tests__/pain-to-principle-service.test.js +8 -0
  52. package/core/dist/runtime-v2/__tests__/pain-to-principle-service.test.js.map +1 -1
  53. package/core/dist/runtime-v2/activation/index.d.ts +1 -1
  54. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  55. package/core/dist/runtime-v2/activation/index.js +1 -1
  56. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  57. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js +240 -1
  58. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js.map +1 -1
  59. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts +12 -0
  60. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts.map +1 -1
  61. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js +81 -1
  62. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js.map +1 -1
  63. package/core/dist/runtime-v2/config/__tests__/internal-agent-names.test.d.ts +2 -0
  64. package/core/dist/runtime-v2/config/__tests__/internal-agent-names.test.d.ts.map +1 -0
  65. package/core/dist/runtime-v2/config/__tests__/internal-agent-names.test.js +15 -0
  66. package/core/dist/runtime-v2/config/__tests__/internal-agent-names.test.js.map +1 -0
  67. package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js +7 -0
  68. package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js.map +1 -1
  69. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +1 -0
  70. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
  71. package/core/dist/runtime-v2/config/__tests__/pd-config-redaction-deep.test.js +1 -0
  72. package/core/dist/runtime-v2/config/__tests__/pd-config-redaction-deep.test.js.map +1 -1
  73. package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js +1 -0
  74. package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js.map +1 -1
  75. package/core/dist/runtime-v2/config/index.d.ts +5 -3
  76. package/core/dist/runtime-v2/config/index.d.ts.map +1 -1
  77. package/core/dist/runtime-v2/config/index.js +8 -2
  78. package/core/dist/runtime-v2/config/index.js.map +1 -1
  79. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +2 -1
  80. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
  81. package/core/dist/runtime-v2/config/pd-config-defaults.js +12 -0
  82. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  83. package/core/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -1
  84. package/core/dist/runtime-v2/config/pd-config-effective.js +25 -1
  85. package/core/dist/runtime-v2/config/pd-config-effective.js.map +1 -1
  86. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
  87. package/core/dist/runtime-v2/config/pd-config-redaction.js +9 -0
  88. package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
  89. package/core/dist/runtime-v2/config/pd-config-types.d.ts +110 -1
  90. package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
  91. package/core/dist/runtime-v2/config/pd-config-types.js +7 -0
  92. package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -1
  93. package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
  94. package/core/dist/runtime-v2/config/pd-config-validate.js +114 -1
  95. package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
  96. package/core/dist/runtime-v2/config/pd-profile-constants.d.ts +27 -0
  97. package/core/dist/runtime-v2/config/pd-profile-constants.d.ts.map +1 -0
  98. package/core/dist/runtime-v2/config/pd-profile-constants.js +145 -0
  99. package/core/dist/runtime-v2/config/pd-profile-constants.js.map +1 -0
  100. package/core/dist/runtime-v2/config/pd-validate-profile.d.ts +28 -0
  101. package/core/dist/runtime-v2/config/pd-validate-profile.d.ts.map +1 -0
  102. package/core/dist/runtime-v2/config/pd-validate-profile.js +482 -0
  103. package/core/dist/runtime-v2/config/pd-validate-profile.js.map +1 -0
  104. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +26 -0
  105. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  106. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  107. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +8 -0
  108. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  109. package/core/dist/runtime-v2/index.d.ts +5 -3
  110. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  111. package/core/dist/runtime-v2/index.js +4 -2
  112. package/core/dist/runtime-v2/index.js.map +1 -1
  113. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js +25 -0
  114. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js.map +1 -1
  115. package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js +139 -0
  116. package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js.map +1 -1
  117. package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js +20 -1
  118. package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js.map +1 -1
  119. package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-intent-tension.test.js +7 -0
  120. package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-intent-tension.test.js.map +1 -1
  121. package/core/dist/runtime-v2/internalization/__tests__/internalization-task-guards.test.js +1 -53
  122. package/core/dist/runtime-v2/internalization/__tests__/internalization-task-guards.test.js.map +1 -1
  123. package/core/dist/runtime-v2/internalization/__tests__/rule-host-input-builder.test.js +23 -0
  124. package/core/dist/runtime-v2/internalization/__tests__/rule-host-input-builder.test.js.map +1 -1
  125. package/core/dist/runtime-v2/internalization/artificer-output.d.ts +7 -0
  126. package/core/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
  127. package/core/dist/runtime-v2/internalization/artificer-output.js +22 -1
  128. package/core/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
  129. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  130. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +2 -0
  131. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  132. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  133. package/core/dist/runtime-v2/internalization/artificer-runner.js +13 -0
  134. package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  135. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  136. package/core/dist/runtime-v2/internalization/evaluator-runner.js +7 -1
  137. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  138. package/core/dist/runtime-v2/internalization/index.d.ts +1 -1
  139. package/core/dist/runtime-v2/internalization/index.d.ts.map +1 -1
  140. package/core/dist/runtime-v2/internalization/index.js +1 -1
  141. package/core/dist/runtime-v2/internalization/index.js.map +1 -1
  142. package/core/dist/runtime-v2/internalization/internalization-task-guards.d.ts +0 -19
  143. package/core/dist/runtime-v2/internalization/internalization-task-guards.d.ts.map +1 -1
  144. package/core/dist/runtime-v2/internalization/internalization-task-guards.js +1 -29
  145. package/core/dist/runtime-v2/internalization/internalization-task-guards.js.map +1 -1
  146. package/core/dist/runtime-v2/signal-collector/__tests__/keyword-stage.test.d.ts +2 -0
  147. package/core/dist/runtime-v2/signal-collector/__tests__/keyword-stage.test.d.ts.map +1 -0
  148. package/core/dist/runtime-v2/signal-collector/__tests__/keyword-stage.test.js +39 -0
  149. package/core/dist/runtime-v2/signal-collector/__tests__/keyword-stage.test.js.map +1 -0
  150. package/core/dist/runtime-v2/signal-collector/__tests__/llm-stage.test.d.ts +2 -0
  151. package/core/dist/runtime-v2/signal-collector/__tests__/llm-stage.test.d.ts.map +1 -0
  152. package/core/dist/runtime-v2/signal-collector/__tests__/llm-stage.test.js +34 -0
  153. package/core/dist/runtime-v2/signal-collector/__tests__/llm-stage.test.js.map +1 -0
  154. package/core/dist/runtime-v2/signal-collector/__tests__/signal-collector.test.d.ts +2 -0
  155. package/core/dist/runtime-v2/signal-collector/__tests__/signal-collector.test.d.ts.map +1 -0
  156. package/core/dist/runtime-v2/signal-collector/__tests__/signal-collector.test.js +222 -0
  157. package/core/dist/runtime-v2/signal-collector/__tests__/signal-collector.test.js.map +1 -0
  158. package/core/dist/runtime-v2/signal-collector/__tests__/types.test.d.ts +2 -0
  159. package/core/dist/runtime-v2/signal-collector/__tests__/types.test.d.ts.map +1 -0
  160. package/core/dist/runtime-v2/signal-collector/__tests__/types.test.js +35 -0
  161. package/core/dist/runtime-v2/signal-collector/__tests__/types.test.js.map +1 -0
  162. package/core/dist/runtime-v2/signal-collector/index.d.ts +5 -0
  163. package/core/dist/runtime-v2/signal-collector/index.d.ts.map +1 -0
  164. package/core/dist/runtime-v2/signal-collector/index.js +5 -0
  165. package/core/dist/runtime-v2/signal-collector/index.js.map +1 -0
  166. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts +20 -0
  167. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -0
  168. package/core/dist/runtime-v2/signal-collector/keyword-stage.js +61 -0
  169. package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -0
  170. package/core/dist/runtime-v2/signal-collector/llm-stage.d.ts +16 -0
  171. package/core/dist/runtime-v2/signal-collector/llm-stage.d.ts.map +1 -0
  172. package/core/dist/runtime-v2/signal-collector/llm-stage.js +34 -0
  173. package/core/dist/runtime-v2/signal-collector/llm-stage.js.map +1 -0
  174. package/core/dist/runtime-v2/signal-collector/signal-collector.d.ts +16 -0
  175. package/core/dist/runtime-v2/signal-collector/signal-collector.d.ts.map +1 -0
  176. package/core/dist/runtime-v2/signal-collector/signal-collector.js +71 -0
  177. package/core/dist/runtime-v2/signal-collector/signal-collector.js.map +1 -0
  178. package/core/dist/runtime-v2/signal-collector/types.d.ts +61 -0
  179. package/core/dist/runtime-v2/signal-collector/types.d.ts.map +1 -0
  180. package/core/dist/runtime-v2/signal-collector/types.js +45 -0
  181. package/core/dist/runtime-v2/signal-collector/types.js.map +1 -0
  182. package/core/dist/runtime-v2/store/schema-conformance.test.js +43 -0
  183. package/core/dist/runtime-v2/store/schema-conformance.test.js.map +1 -1
  184. package/core/dist/runtime-v2/types/event-types.d.ts +40 -3
  185. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  186. package/core/dist/runtime-v2/types/event-types.js +9 -0
  187. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  188. package/core/dist/runtime-v2/types/index.d.ts +2 -2
  189. package/core/dist/runtime-v2/types/index.d.ts.map +1 -1
  190. package/core/dist/runtime-v2/types/index.js +1 -1
  191. package/core/dist/runtime-v2/types/index.js.map +1 -1
  192. package/core/dist/workflow-funnel-loader.js +1 -1
  193. package/core/dist/workflow-funnel-loader.js.map +1 -1
  194. package/dist/i18n.d.ts.map +1 -1
  195. package/dist/i18n.js +6 -0
  196. package/dist/i18n.js.map +1 -1
  197. package/dist/index.js +12 -3
  198. package/dist/index.js.map +1 -1
  199. package/dist/installer.d.ts +3 -0
  200. package/dist/installer.d.ts.map +1 -1
  201. package/dist/installer.js +146 -1
  202. package/dist/installer.js.map +1 -1
  203. package/dist/mvp-config.d.ts +8 -0
  204. package/dist/mvp-config.d.ts.map +1 -1
  205. package/dist/mvp-config.js +12 -2
  206. package/dist/mvp-config.js.map +1 -1
  207. package/package.json +1 -1
  208. package/pd-cli/dist/commands/runtime-activation.d.ts +4 -0
  209. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  210. package/pd-cli/dist/commands/runtime-activation.js +181 -10
  211. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  212. package/pd-cli/dist/index.js +5 -63
  213. package/pd-cli/dist/index.js.map +1 -1
  214. package/plugin/dist/commands/context.js +17 -11
  215. package/plugin/dist/commands/samples.js +25 -0
  216. package/plugin/dist/core/event-log.d.ts +11 -1
  217. package/plugin/dist/core/event-log.js +13 -0
  218. package/plugin/dist/core/init.js +3 -4
  219. package/plugin/dist/core/rule-host.d.ts +47 -0
  220. package/plugin/dist/core/rule-host.js +102 -18
  221. package/plugin/dist/core/signal-collector-host.d.ts +105 -0
  222. package/plugin/dist/core/signal-collector-host.js +337 -0
  223. package/plugin/dist/hooks/prompt.d.ts +3 -2
  224. package/plugin/dist/hooks/prompt.js +48 -586
  225. package/plugin/dist/types/event-types.d.ts +1 -1
  226. package/plugin/dist/types.d.ts +5 -25
  227. package/plugin/dist/types.js +4 -9
  228. package/plugin/dist/utils/hashing.js +1 -1
@@ -17,6 +17,7 @@ import { handleGovernanceRoute, disposeGovernanceModels } from './routes/governa
17
17
  import { handleEvidenceChainRoute, disposeEvidenceChainModels } from './routes/evidence-chain.js';
18
18
  import { handleIntentRoute, disposeIntentModels } from './routes/intent.js';
19
19
  import { handleIntentDecisionsRoute, disposeIntentDecisionModels } from './routes/intent-decisions.js';
20
+ import { handleOnboardingRoute, disposeOnboardingModels } from './routes/onboarding.js';
20
21
  import { createWorkspacesRoutes } from './routes/workspaces.js';
21
22
  import { handleUpdateRoute } from './routes/update.js';
22
23
  import { handleUpdateHistoryRoute } from './routes/update-history.js';
@@ -229,6 +230,7 @@ async function closeServices() {
229
230
  disposeEvidenceChainModels();
230
231
  disposeIntentModels();
231
232
  disposeIntentDecisionModels();
233
+ disposeOnboardingModels();
232
234
  }
233
235
  // ── Route handler ───────────────────────────────────────────────────────────
234
236
  function handleRequest(services) {
@@ -340,6 +342,13 @@ function handleRequest(services) {
340
342
  asyncHandler(() => handleIntentDecisionsRoute(req, res, services.workspaceDir, subPath))(req, res);
341
343
  return;
342
344
  }
345
+ // Onboarding wizard: POST /api/v1/onboarding/run-demo — spawns `pd demo story-a`
346
+ // (spec 2026-06-30-new-user-onboarding-design.md §6.3 改动 5)
347
+ if (urlPath === '/api/v1/onboarding' || urlPath.startsWith('/api/v1/onboarding/')) {
348
+ const subPath = urlPath === '/api/v1/onboarding' ? '' : urlPath.slice('/api/v1/onboarding'.length);
349
+ asyncHandler(() => handleOnboardingRoute(req, res, { workspaceDir: services.workspaceDir, subPath }))(req, res);
350
+ return;
351
+ }
343
352
  // PRI-331: GET /api/v1/evidence-chain
344
353
  if (urlPath === '/api/v1/evidence-chain') {
345
354
  asyncHandler(() => handleEvidenceChainRoute(req, res, services.workspaceDir))(req, res);
@@ -1,3 +1,12 @@
1
+ /**
2
+ * PRI-491 — owner-observable activation record.
3
+ *
4
+ * Mirrors the CLI's `AnnotatedActivation` so the Console surfaces the same
5
+ * mode / status / contextVersion / evidenceRefs / nextAction fields the owner
6
+ * sees in `pd activation list`. Without this enrichment the Console would
7
+ * hide suspended-by-flag state, silently showing v2 activations as "active"
8
+ * even when the rulecode_context_v2 flag is off.
9
+ */
1
10
  export interface ActivationRecord {
2
11
  id: string;
3
12
  artifactId: string;
@@ -6,7 +15,34 @@ export interface ActivationRecord {
6
15
  action: string;
7
16
  targetRef: string;
8
17
  activatedAt: string | null;
9
- status: 'active' | 'inactive';
18
+ /** When this activation was promoted from shadow to live (null for never-promoted). */
19
+ promotedAt: string | null;
20
+ /** When this activation was deactivated (null for active). */
21
+ deactivatedAt: string | null;
22
+ /**
23
+ * Lifecycle mode derived from action.
24
+ * - 'shadow' for code_tool_hook_shadow_activate
25
+ * - 'live' for code_tool_hook_live_activate
26
+ * - undefined for unrecognized actions (cannot be safely mode-tagged).
27
+ */
28
+ mode?: 'shadow' | 'live';
29
+ /**
30
+ * Owner-visible status. Precedence: deactivated > suspended_by_flag > active.
31
+ * - 'deactivated' — deactivatedAt is non-null.
32
+ * - 'suspended_by_flag' — v2 artifact but rulecode_context_v2 flag is off.
33
+ * - 'active' — loaded and (for v2) flag is on.
34
+ */
35
+ status: 'active' | 'deactivated' | 'suspended_by_flag';
36
+ /** Artifact context version derived from requiresContextVersion field. */
37
+ contextVersion?: 'v1' | 'v2';
38
+ /** Owner-labelled evidence refs preserved from the artifact (PRI-490). */
39
+ evidenceRefs?: string[];
40
+ /** Human-readable summary of evidenceRefs for display. */
41
+ evidenceSummary?: string;
42
+ /** Next CLI command the owner should run to act on this activation. */
43
+ nextAction?: string;
44
+ /** Present when the activation references a non-existent artifact. */
45
+ warning?: string;
10
46
  }
11
47
  export interface ActivationsResponse {
12
48
  activations: ActivationRecord[];
@@ -1,6 +1,14 @@
1
- import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, } from '@principles/core/runtime-v2';
1
+ import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, } from '@principles/core/runtime-v2';
2
+ import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
2
3
  import * as fs from 'node:fs';
3
4
  import * as path from 'node:path';
5
+ /**
6
+ * Type guard for parsed JSON objects (rc-2-no-as-bypass).
7
+ * Replaces `as Record<string, unknown>` casts on untrusted contentJson.
8
+ */
9
+ function isRecord(value) {
10
+ return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
11
+ }
4
12
  function isMissingTableError(err) {
5
13
  if (!(err instanceof Error))
6
14
  return false;
@@ -56,13 +64,45 @@ export class ActivationsConsoleModel {
56
64
  // Without this, dreamer artifacts whose sourcePrincipleId was stripped
57
65
  // (non-core-principle case) would show 'unlinked' even when contentJson
58
66
  // carries a resolvable principleId.
67
+ //
68
+ // PRI-491: also collect contextVersion + evidenceRefs from contentJson so
69
+ // the Console can show whether a rule will block and why. rc-1/rc-2:
70
+ // contentJson is parsed as unknown and type-narrowed with typeof; never
71
+ // `as`-cast without a prior typeof check.
59
72
  const artifactPrincipleMap = new Map();
73
+ const artifactMetadata = new Map();
74
+ const danglingArtifactIds = new Set();
60
75
  for (const activation of allActivations) {
61
76
  if (!artifactPrincipleMap.has(activation.artifactId)) {
62
77
  try {
63
78
  const artifact = await artifactStore.getArtifactById(activation.artifactId);
64
- const principleId = artifact ? extractPrincipleId(toSnapshot(artifact)) : null;
65
- artifactPrincipleMap.set(activation.artifactId, principleId);
79
+ if (!artifact) {
80
+ danglingArtifactIds.add(activation.artifactId);
81
+ artifactPrincipleMap.set(activation.artifactId, null);
82
+ }
83
+ else {
84
+ const principleId = extractPrincipleId(toSnapshot(artifact));
85
+ artifactPrincipleMap.set(activation.artifactId, principleId);
86
+ // PRI-491: extract contextVersion + evidenceRefs from contentJson.
87
+ // rc-1: treat parsed JSON as unknown; rc-2: narrow with typeof.
88
+ let parsedContent = null;
89
+ try {
90
+ const parsed = JSON.parse(artifact.contentJson);
91
+ if (isRecord(parsed)) {
92
+ parsedContent = parsed;
93
+ }
94
+ }
95
+ catch {
96
+ // Malformed contentJson — treat as no metadata. Not dangling,
97
+ // just unreadable; the principleId may still resolve via column.
98
+ }
99
+ const requiresCtxV2 = parsedContent !== null
100
+ && Object.hasOwn(parsedContent, 'requiresContextVersion')
101
+ && parsedContent.requiresContextVersion === 2;
102
+ const contextVersion = requiresCtxV2 ? 'v2' : 'v1';
103
+ const evidenceRefs = parsedContent !== null ? extractEvidenceRefs(parsedContent) : null;
104
+ artifactMetadata.set(activation.artifactId, { contextVersion, evidenceRefs });
105
+ }
66
106
  }
67
107
  catch (err) {
68
108
  if (isMissingTableError(err)) {
@@ -74,16 +114,77 @@ export class ActivationsConsoleModel {
74
114
  }
75
115
  }
76
116
  }
77
- const facts = allActivations.map((record) => ({
78
- id: record.activationId,
79
- artifactId: record.artifactId,
80
- principleId: artifactPrincipleMap.get(record.artifactId) ?? 'unlinked',
81
- channel: record.channel,
82
- action: record.action,
83
- targetRef: record.targetRef,
84
- activatedAt: record.activatedAt,
85
- status: record.deactivatedAt === null ? 'active' : 'inactive',
86
- }));
117
+ // PRI-491: Probe rulecode_context_v2 flag to determine suspended_by_flag
118
+ // status for v2 activations. When the flag is off, v2 activations are
119
+ // suspended (not executing) even though they remain active in the DB.
120
+ const featureFlags = computeFlagsFromLoadResult(loadPdConfig(this.workspaceDir));
121
+ const v2FlagEnabled = featureFlags.flags.rulecode_context_v2?.enabled === true;
122
+ const facts = allActivations.map((record) => {
123
+ const meta = artifactMetadata.get(record.artifactId);
124
+ const contextVersion = meta?.contextVersion;
125
+ const evidenceRefs = meta?.evidenceRefs ?? undefined;
126
+ const evidenceSummary = evidenceRefs && evidenceRefs.length > 0
127
+ ? `${evidenceRefs.length} evidence ref(s): ${evidenceRefs.slice(0, 3).join(', ')}${evidenceRefs.length > 3 ? '...' : ''}`
128
+ : undefined;
129
+ // Derive mode from action (shadow_activate -> shadow, live_activate -> live).
130
+ const mode = record.action === 'code_tool_hook_shadow_activate'
131
+ ? 'shadow'
132
+ : record.action === 'code_tool_hook_live_activate'
133
+ ? 'live'
134
+ : undefined;
135
+ // Derive status: deactivated > suspended_by_flag > active (matches CLI).
136
+ let status;
137
+ let nextAction;
138
+ if (record.deactivatedAt) {
139
+ status = 'deactivated';
140
+ nextAction = undefined;
141
+ }
142
+ else if (contextVersion === 'v2' && !v2FlagEnabled) {
143
+ status = 'suspended_by_flag';
144
+ nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${record.activationId} --confirm`;
145
+ }
146
+ else {
147
+ status = 'active';
148
+ if (mode === 'shadow') {
149
+ nextAction = `pd activation promote --activation-id ${record.activationId} --confirm`;
150
+ }
151
+ else if (mode === 'live') {
152
+ nextAction = `pd activation deactivate --activation-id ${record.activationId} --confirm`;
153
+ }
154
+ else {
155
+ nextAction = undefined;
156
+ }
157
+ }
158
+ const enriched = {
159
+ id: record.activationId,
160
+ artifactId: record.artifactId,
161
+ principleId: artifactPrincipleMap.get(record.artifactId) ?? 'unlinked',
162
+ channel: record.channel,
163
+ action: record.action,
164
+ targetRef: record.targetRef,
165
+ activatedAt: record.activatedAt,
166
+ promotedAt: record.promotedAt ?? null,
167
+ deactivatedAt: record.deactivatedAt,
168
+ mode,
169
+ status,
170
+ contextVersion,
171
+ evidenceRefs,
172
+ evidenceSummary,
173
+ nextAction,
174
+ };
175
+ if (danglingArtifactIds.has(record.artifactId)) {
176
+ enriched.warning = `artifact_id "${record.artifactId}" does not exist in pi_artifacts - activation is orphaned`;
177
+ }
178
+ return enriched;
179
+ });
180
+ // rc-9: surface dangling references instead of silently returning a degraded list.
181
+ if (danglingArtifactIds.size > 0) {
182
+ return {
183
+ activations: facts,
184
+ generatedAt: new Date().toISOString(),
185
+ note: `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`,
186
+ };
187
+ }
87
188
  return {
88
189
  activations: facts,
89
190
  generatedAt: new Date().toISOString(),
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Onboarding route - POST /api/v1/onboarding/run-demo
3
+ *
4
+ * Spec: docs/superpowers/specs/2026-06-30-new-user-onboarding-design.md 6.3 change 5
5
+ *
6
+ * Spawns `pd demo story-a --workspace <path> --json` as a subprocess, waits for
7
+ * it to complete, validates the JSON stdout, and returns 200 with the demo
8
+ * result. The console backend NEVER writes SQLite directly - all DB I/O happens
9
+ * inside the pd-cli subprocess (EP-06 Source of Truth).
10
+ *
11
+ * Feature flag gate: `new_user_onboarding` (default true, registered in
12
+ * packages/principles-core/src/runtime-v2/feature-flags/feature-flag-contract.ts).
13
+ *
14
+ * ERR entries considered:
15
+ * - EP-02 (Production Path Wiring): registered in server/index.ts handleRequest
16
+ * - EP-03 (Fail Loud): spawn failure -> 500 with reason + nextAction;
17
+ * flag disabled -> 403 with reason + nextAction (rc-9-no-silent-fallback)
18
+ * - EP-06 (Source of Truth): reuses pd-cli demo command, no direct DB writes
19
+ * - EP-08 (Security Boundary): no shell:true — argv passed directly to OS,
20
+ * eliminating command-injection risk; demo runs in a temp workspace so no
21
+ * user-controlled path enters the command at all
22
+ * - rc-1-treat-as-unknown / rc-2-no-as-bypass / rc-4-validate-array-elements:
23
+ * parsed stdout is validated by parseDemoStdout before use
24
+ * - rc-9-no-silent-fallback: timeout, error, and invalid-stdout paths each
25
+ * return a structured error with reason + nextAction
26
+ */
27
+ import type { IncomingMessage, ServerResponse } from 'node:http';
28
+ export interface OnboardingRouteContext {
29
+ workspaceDir: string;
30
+ subPath: string;
31
+ }
32
+ export declare function handleOnboardingRoute(req: IncomingMessage, res: ServerResponse, ctx: OnboardingRouteContext): Promise<void>;
33
+ /** No persistent resources to dispose - included for parity with other routes. */
34
+ export declare function disposeOnboardingModels(): void;
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Onboarding route - POST /api/v1/onboarding/run-demo
3
+ *
4
+ * Spec: docs/superpowers/specs/2026-06-30-new-user-onboarding-design.md 6.3 change 5
5
+ *
6
+ * Spawns `pd demo story-a --workspace <path> --json` as a subprocess, waits for
7
+ * it to complete, validates the JSON stdout, and returns 200 with the demo
8
+ * result. The console backend NEVER writes SQLite directly - all DB I/O happens
9
+ * inside the pd-cli subprocess (EP-06 Source of Truth).
10
+ *
11
+ * Feature flag gate: `new_user_onboarding` (default true, registered in
12
+ * packages/principles-core/src/runtime-v2/feature-flags/feature-flag-contract.ts).
13
+ *
14
+ * ERR entries considered:
15
+ * - EP-02 (Production Path Wiring): registered in server/index.ts handleRequest
16
+ * - EP-03 (Fail Loud): spawn failure -> 500 with reason + nextAction;
17
+ * flag disabled -> 403 with reason + nextAction (rc-9-no-silent-fallback)
18
+ * - EP-06 (Source of Truth): reuses pd-cli demo command, no direct DB writes
19
+ * - EP-08 (Security Boundary): no shell:true — argv passed directly to OS,
20
+ * eliminating command-injection risk; demo runs in a temp workspace so no
21
+ * user-controlled path enters the command at all
22
+ * - rc-1-treat-as-unknown / rc-2-no-as-bypass / rc-4-validate-array-elements:
23
+ * parsed stdout is validated by parseDemoStdout before use
24
+ * - rc-9-no-silent-fallback: timeout, error, and invalid-stdout paths each
25
+ * return a structured error with reason + nextAction
26
+ */
27
+ import { spawn } from 'node:child_process';
28
+ import { fileURLToPath } from 'node:url';
29
+ import * as fs from 'fs';
30
+ import * as os from 'os';
31
+ import * as path from 'path';
32
+ import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
33
+ import { sendError, sendJson } from '../utils/response.js';
34
+ /** Demo subprocess timeout in milliseconds (60s). */
35
+ const DEMO_TIMEOUT_MS = 60_000;
36
+ // P1-1: Best-effort removal of the temp demo workspace.
37
+ function cleanupTempWorkspace(dir) {
38
+ try {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ catch {
42
+ // Best-effort cleanup — temp dir will be cleaned by OS eventually.
43
+ }
44
+ }
45
+ /** Send a structured error with reason + nextAction (Runtime Contract rc-9). */
46
+ function sendStructuredError(res, payload) {
47
+ sendError(res, payload.statusCode, payload.error, payload.reason, {
48
+ reason: payload.reason,
49
+ nextAction: payload.nextAction,
50
+ });
51
+ }
52
+ /** Load the new_user_onboarding flag state from .pd/config.yaml. */
53
+ function loadOnboardingFlagEnabled(workspaceDir) {
54
+ const configResult = loadPdConfig(workspaceDir);
55
+ const flagsResult = computeFlagsFromLoadResult(configResult);
56
+ return flagsResult.flags.new_user_onboarding?.enabled === true;
57
+ }
58
+ /**
59
+ * Resolve the pd CLI invocation: spawn `process.execPath` (Node itself) with
60
+ * the absolute path to the pd-cli `dist/index.js` entry, instead of spawning
61
+ * the bare `pd` binary.
62
+ *
63
+ * P1-A (Windows .cmd resolution): `spawn('pd', args, { shell: false })` fails
64
+ * on Windows with ENOENT because Node's CreateProcess cannot resolve npm's
65
+ * `.cmd` shims (pd.cmd) without a shell. Spawning `process.execPath` with the
66
+ * JS entry path sidesteps this entirely — there is no `.cmd` involved. This is
67
+ * the same pattern used in installer.ts:autoLaunchConsole and
68
+ * pd-cli's runtime-uat.ts `findPdCliPath`.
69
+ *
70
+ * P1-2 (command injection): this approach also keeps `shell: false` — argv is
71
+ * passed directly to the OS, so no user-controlled value is ever interpreted
72
+ * by a shell (EP-08 Security Boundary).
73
+ *
74
+ * Returns `{ cmd, extraArgs }`:
75
+ * - On success: `{ cmd: process.execPath, extraArgs: [pdCliEntryPath] }`
76
+ * - On failure (pd-cli not resolvable / entry missing): falls back to
77
+ * `{ cmd: 'pd', extraArgs: [] }`. This works on Linux/macOS via PATH; on
78
+ * Windows the spawn will emit 'error', which the caller handles with a
79
+ * clear nextAction (rc-9-no-silent-fallback).
80
+ */
81
+ function findPdCli() {
82
+ // Both the monorepo and installed extension place pd-cli beside console.
83
+ // source: pd-console/src/server/routes -> packages/pd-cli
84
+ // install: console/dist/server/routes -> principles-disciple/pd-cli
85
+ const entryPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../pd-cli/dist/index.js');
86
+ // Do not preflight with existsSync: the Console test job intentionally runs
87
+ // before pd-cli is built, while production installation guarantees this
88
+ // sibling layout. Node will exit non-zero with a structured error if the
89
+ // delivered entry is unexpectedly missing.
90
+ return { cmd: process.execPath, extraArgs: [entryPath] };
91
+ }
92
+ /**
93
+ * Validate the parsed JSON stdout from `pd demo story-a --json`.
94
+ *
95
+ * rc-1-treat-as-unknown: parsed JSON is treated as unknown until validated.
96
+ * rc-2-no-as-bypass: no `as` cast is used to bypass validation - all fields
97
+ * are checked with typeof / Array.isArray before the value is accepted.
98
+ * rc-4-validate-array-elements: the `stages` array is type-checked as an array
99
+ * (element-level shape validation happens downstream in the UI validator).
100
+ *
101
+ * Required fields: status (string), generatedAt (string), narrative (string),
102
+ * stages (array). Returns the validated object or null if invalid.
103
+ */
104
+ function parseDemoStdout(stdout) {
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(stdout);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ if (typeof parsed !== 'object' || parsed === null)
113
+ return null;
114
+ const status = Object.hasOwn(parsed, 'status') ? Reflect.get(parsed, 'status') : undefined;
115
+ const generatedAt = Object.hasOwn(parsed, 'generatedAt') ? Reflect.get(parsed, 'generatedAt') : undefined;
116
+ const narrative = Object.hasOwn(parsed, 'narrative') ? Reflect.get(parsed, 'narrative') : undefined;
117
+ const stages = Object.hasOwn(parsed, 'stages') ? Reflect.get(parsed, 'stages') : undefined;
118
+ if (typeof status !== 'string' || typeof generatedAt !== 'string' || typeof narrative !== 'string')
119
+ return null;
120
+ if (!Array.isArray(stages) || !stages.every((stage) => {
121
+ if (typeof stage !== 'object' || stage === null)
122
+ return false;
123
+ const name = Object.hasOwn(stage, 'name') ? Reflect.get(stage, 'name') : undefined;
124
+ const stageStatus = Object.hasOwn(stage, 'status') ? Reflect.get(stage, 'status') : undefined;
125
+ return typeof name === 'string' &&
126
+ (stageStatus === 'passed' || stageStatus === 'failed' || stageStatus === 'degraded' || stageStatus === 'skipped');
127
+ }))
128
+ return null;
129
+ return parsed;
130
+ }
131
+ /**
132
+ * Spawn `pd demo story-a --json`, wait for it to complete, validate stdout,
133
+ * and return 200 with { success: true, data: { simulated: true, demo: ... } }.
134
+ *
135
+ * The demo typically completes in < 30s. We block on the subprocess so the
136
+ * response carries the validated demo result in a single round-trip - the
137
+ * frontend does not need to poll for the demo itself (it still polls
138
+ * /api/v1/evidence-chain in step 3 for live evidence detection).
139
+ *
140
+ * Failure modes (rc-9-no-silent-fallback - every path returns a structured
141
+ * error with reason + nextAction):
142
+ * - spawn throws synchronously (e.g. ENOENT) -> 500 demo_spawn_failed
143
+ * - child emits 'error' after spawn -> 500 demo_subprocess_error
144
+ * - subprocess exceeds DEMO_TIMEOUT_MS -> 504 demo_timeout (subprocess killed)
145
+ * - subprocess exits with non-zero code -> 500 demo_exit_nonzero
146
+ * - stdout cannot be parsed/validated -> 500 demo_invalid_stdout
147
+ */
148
+ async function handleRunDemo(res, _workspaceDir) {
149
+ // P1-1: Use a temporary workspace so the demo's simulated DB writes
150
+ // (tasks, artifacts, approvals, activations) do NOT pollute the user's
151
+ // real workspace state.db.
152
+ const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-onboarding-demo-'));
153
+ let child;
154
+ try {
155
+ const { cmd, extraArgs } = findPdCli();
156
+ // P1-A: spawn process.execPath (Node) with the pd-cli entry JS path to
157
+ // avoid the Windows .cmd resolution problem. P1-2: no shell:true — argv is
158
+ // passed directly to the OS, eliminating command-injection risk from
159
+ // user-controlled paths (the temp workspace path is the only variable
160
+ // here, and it is a mkdtemp result, not user input).
161
+ child = spawn(cmd, [...extraArgs, 'demo', 'story-a', '--workspace', tempWorkspace, '--json'], {
162
+ stdio: ['pipe', 'pipe', 'pipe'],
163
+ // No shell:true — argv is passed directly to the OS.
164
+ });
165
+ }
166
+ catch (err) {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ cleanupTempWorkspace(tempWorkspace);
169
+ sendStructuredError(res, {
170
+ statusCode: 500,
171
+ error: 'demo_spawn_failed',
172
+ reason: `spawn failed: ${message}`,
173
+ nextAction: 'Check that the pd CLI is installed (npm i -g @principles/pd-cli) and that its dist/index.js entry exists, or that pd is on PATH.',
174
+ });
175
+ return;
176
+ }
177
+ // P2-D: declared outside the try so the finally block can clear it. Captures
178
+ // the 60s timeout handle to prevent the timer from leaking after demo ends.
179
+ let timeoutHandle;
180
+ try {
181
+ // Capture stdout into a buffer. stderr is logged for observability (rc-9).
182
+ const stdoutChunks = [];
183
+ let stderrText = '';
184
+ child.stdout?.on('data', (data) => {
185
+ stdoutChunks.push(data);
186
+ });
187
+ child.stderr?.on('data', (data) => {
188
+ const chunk = data.toString();
189
+ stderrText += chunk;
190
+ console.error('[pd-console] onboarding demo stderr:', chunk.trim());
191
+ });
192
+ // Wrap the subprocess lifecycle in a Promise that resolves with the exit code
193
+ // or rejects on 'error'. A timeout is enforced via a separate Promise that
194
+ // resolves with a sentinel value - Promise.race is used below.
195
+ const settled = new Promise((resolve) => {
196
+ let done = false;
197
+ const finish = (code, error) => {
198
+ if (done)
199
+ return;
200
+ done = true;
201
+ resolve({ code, error });
202
+ };
203
+ child.on('error', (err) => {
204
+ finish(null, err);
205
+ });
206
+ child.on('close', (code) => {
207
+ finish(code, null);
208
+ });
209
+ });
210
+ const timeout = new Promise((resolve) => {
211
+ timeoutHandle = setTimeout(() => resolve('timeout'), DEMO_TIMEOUT_MS);
212
+ });
213
+ const outcome = await Promise.race([settled, timeout]);
214
+ // -- Timeout: kill the subprocess and return 504 --
215
+ if (outcome === 'timeout') {
216
+ try {
217
+ child.kill();
218
+ }
219
+ catch {
220
+ // Best-effort kill; ignore failures (rc-9: the 504 response still carries
221
+ // the reason + nextAction so the operator is not left in the dark).
222
+ }
223
+ sendStructuredError(res, {
224
+ statusCode: 504,
225
+ error: 'demo_timeout',
226
+ reason: `demo subprocess exceeded ${DEMO_TIMEOUT_MS}ms timeout`,
227
+ nextAction: 'Retry the demo; if it persists, check pd CLI health (pd doctor).',
228
+ });
229
+ return;
230
+ }
231
+ // -- 'error' event after spawn (e.g. EACCES, broken pipe) -> 500 --
232
+ if (outcome.error !== null) {
233
+ sendStructuredError(res, {
234
+ statusCode: 500,
235
+ error: 'demo_subprocess_error',
236
+ reason: `subprocess error: ${outcome.error.message}`,
237
+ nextAction: 'Check pd CLI installation and permissions; see server logs for stderr.',
238
+ });
239
+ return;
240
+ }
241
+ // -- Non-zero exit code -> 500 --
242
+ if (outcome.code !== 0) {
243
+ sendStructuredError(res, {
244
+ statusCode: 500,
245
+ error: 'demo_exit_nonzero',
246
+ reason: `demo exited with code ${outcome.code}`,
247
+ nextAction: stderrText.trim()
248
+ ? `pd stderr: ${stderrText.trim()}`
249
+ : 'Run `pd demo story-a --json` manually for diagnostics.',
250
+ });
251
+ return;
252
+ }
253
+ // -- Validate stdout (rc-1/rc-2/rc-4) --
254
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
255
+ const validated = parseDemoStdout(stdout);
256
+ if (validated === null) {
257
+ sendStructuredError(res, {
258
+ statusCode: 500,
259
+ error: 'demo_invalid_stdout',
260
+ reason: 'demo stdout did not match the expected JSON schema',
261
+ nextAction: 'Run `pd demo story-a --json` manually and inspect the output.',
262
+ });
263
+ return;
264
+ }
265
+ // -- 200 OK with validated demo result --
266
+ sendJson(res, 200, {
267
+ success: true,
268
+ data: {
269
+ simulated: true,
270
+ demo: validated,
271
+ },
272
+ });
273
+ }
274
+ finally {
275
+ // P2-D: clear the 60s timeout timer so it doesn't leak after the demo
276
+ // completes. The guard handles the case where an error was thrown before
277
+ // the timeout Promise executor assigned the handle.
278
+ if (timeoutHandle)
279
+ clearTimeout(timeoutHandle);
280
+ // P1-1: always clean up the temp workspace, whether the demo succeeded,
281
+ // failed, timed out, or threw. Best-effort — OS will reap temp dirs eventually.
282
+ cleanupTempWorkspace(tempWorkspace);
283
+ }
284
+ }
285
+ export async function handleOnboardingRoute(req, res, ctx) {
286
+ const { workspaceDir, subPath } = ctx;
287
+ // -- Feature flag gate (spec 6.3 change 5) --
288
+ // Checked BEFORE method/sub-path dispatch so a disabled flag cannot be probed
289
+ // via 405/404 side-channels (rc-9-no-silent-fallback: explicit 403 + reason).
290
+ if (!loadOnboardingFlagEnabled(workspaceDir)) {
291
+ sendStructuredError(res, {
292
+ statusCode: 403,
293
+ error: 'flag_disabled',
294
+ reason: 'flag_disabled',
295
+ nextAction: 'Enable the new_user_onboarding feature flag in .pd/config.yaml or the Settings page.',
296
+ });
297
+ return;
298
+ }
299
+ // -- POST /api/v1/onboarding/run-demo --
300
+ if (subPath === '/run-demo') {
301
+ if (req.method !== 'POST') {
302
+ sendStructuredError(res, {
303
+ statusCode: 405,
304
+ error: 'method_not_allowed',
305
+ reason: 'method_not_allowed',
306
+ nextAction: 'Use POST to trigger the demo.',
307
+ });
308
+ return;
309
+ }
310
+ await handleRunDemo(res, workspaceDir);
311
+ return;
312
+ }
313
+ // -- Unknown sub-path --
314
+ sendStructuredError(res, {
315
+ statusCode: 404,
316
+ error: 'not_found',
317
+ reason: 'not_found',
318
+ nextAction: `Route /api/v1/onboarding${subPath} not found. Use POST /api/v1/onboarding/run-demo.`,
319
+ });
320
+ }
321
+ /** No persistent resources to dispose - included for parity with other routes. */
322
+ export function disposeOnboardingModels() {
323
+ // No-op: handleRunDemo holds no cached models or open handles.
324
+ }