@principles/pd-cli 1.133.0 → 1.134.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 (211) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +40 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  107. package/dist/services/pain-flood-simulation-runner.js +9 -0
  108. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  109. package/dist/services/pd-config-loader.js +2 -2
  110. package/dist/services/pd-config-loader.js.map +1 -1
  111. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  112. package/dist/services/proven-channel-baseline-runner.js +10 -2
  113. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  114. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  115. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  116. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  117. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  118. package/dist/services/rulehost-pipeline-runner.js +111 -7
  119. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  120. package/dist/services/rulehost-readiness.js.map +1 -1
  121. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  122. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  123. package/dist/services/runtime-adapter-resolver.js +3 -0
  124. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  125. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  126. package/dist/services/synthetic-baseline-runner.js +11 -0
  127. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  128. package/dist/utils/production-workspace-guard.d.ts +4 -4
  129. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  130. package/dist/utils/production-workspace-guard.js +19 -16
  131. package/dist/utils/production-workspace-guard.js.map +1 -1
  132. package/dist/utils/production-workspace-guard.test.js +54 -19
  133. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  134. package/package.json +8 -5
  135. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  136. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  137. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  138. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  139. package/src/commands/admission-gate.ts +30 -0
  140. package/src/commands/candidate.ts +74 -6
  141. package/src/commands/console.ts +40 -0
  142. package/src/commands/demo-story-a.ts +4 -2
  143. package/src/commands/diagnose.ts +158 -10
  144. package/src/commands/errors-list.ts +440 -0
  145. package/src/commands/health-codex.ts +249 -0
  146. package/src/commands/health.ts +60 -0
  147. package/src/commands/intent.ts +98 -29
  148. package/src/commands/pain-evidence.ts +6 -0
  149. package/src/commands/pain-record.ts +21 -0
  150. package/src/commands/pain-retry.ts +353 -35
  151. package/src/commands/proven-channel-baseline.ts +4 -2
  152. package/src/commands/runtime-activation.ts +657 -12
  153. package/src/commands/runtime-canary.ts +31 -5
  154. package/src/commands/runtime-health-snapshot.ts +4 -2
  155. package/src/commands/runtime-init.ts +305 -0
  156. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  157. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  158. package/src/commands/runtime-internalization-integrity.ts +4 -2
  159. package/src/commands/runtime-internalization-run-once.ts +20 -5
  160. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  161. package/src/commands/runtime-uat.guard.test.ts +14 -16
  162. package/src/commands/runtime-uat.ts +4 -3
  163. package/src/config-reader.ts +1 -1
  164. package/src/index.ts +69 -78
  165. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  166. package/src/services/config-doctor.ts +9 -2
  167. package/src/services/pain-flood-simulation-runner.ts +9 -0
  168. package/src/services/pd-config-loader.ts +2 -2
  169. package/src/services/proven-channel-baseline-runner.ts +10 -3
  170. package/src/services/rulehost-pipeline-runner.ts +157 -6
  171. package/src/services/runtime-adapter-resolver.ts +4 -0
  172. package/src/services/synthetic-baseline-runner.ts +11 -0
  173. package/src/utils/production-workspace-guard.test.ts +62 -20
  174. package/src/utils/production-workspace-guard.ts +19 -16
  175. package/tests/bdd/cli-contract.steps.ts +343 -0
  176. package/tests/bdd/context-trace.steps.ts +229 -0
  177. package/tests/bdd/support/gherkin-loader.ts +147 -0
  178. package/tests/bdd/support/repo-root.ts +62 -0
  179. package/tests/bdd/support/vitest-bdd.ts +179 -0
  180. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  181. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  182. package/tests/commands/candidate-intake.test.ts +155 -10
  183. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  184. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  185. package/tests/commands/console-open.test.ts +19 -0
  186. package/tests/commands/diagnose.test.ts +619 -14
  187. package/tests/commands/errors-list.test.ts +678 -0
  188. package/tests/commands/health-codex.test.ts +243 -0
  189. package/tests/commands/health-host-wiring.test.ts +76 -0
  190. package/tests/commands/intent.test.ts +4 -4
  191. package/tests/commands/pain-record.test.ts +80 -0
  192. package/tests/commands/pain-retry.test.ts +94 -3
  193. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  194. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  195. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  196. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  197. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  198. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  199. package/tests/commands/runtime-activation.test.ts +1090 -5
  200. package/tests/commands/runtime-canary.test.ts +50 -0
  201. package/tests/commands/runtime-features.test.ts +2 -0
  202. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  203. package/tests/commands/runtime-init.test.ts +346 -0
  204. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  205. package/tests/commands/runtime-internalization.test.ts +2 -2
  206. package/tests/config-reader.test.ts +6 -5
  207. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  208. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  209. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  210. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  211. package/vitest.config.ts +1 -1
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
10
  import { Command } from 'commander';
11
- import { registerIntentCommand } from '../intent.js';
11
+ import { registerIntentCommand, parseLang } from '../intent.js';
12
12
 
13
13
  type ActionOptions = Record<string, unknown>;
14
14
  interface CapturedAction {
@@ -54,7 +54,7 @@ describe('pd intent — command registration', () => {
54
54
  });
55
55
 
56
56
  describe('pd intent init — option metadata', () => {
57
- it('has --workspace (-w), --force, --dry-run, --confirm, and --json options', () => {
57
+ it('has --workspace (-w), --force, --dry-run, --confirm, --lang, and --json options', () => {
58
58
  const program = freshProgram();
59
59
  registerIntentCommand(program);
60
60
  const intentCmd = requireCmd(program.commands.find((c) => c.name() === 'intent'), 'intent');
@@ -65,6 +65,7 @@ describe('pd intent init — option metadata', () => {
65
65
  expect(initCmd.options.find((o) => o.long === '--force')).toBeDefined();
66
66
  expect(initCmd.options.find((o) => o.long === '--dry-run')).toBeDefined();
67
67
  expect(initCmd.options.find((o) => o.long === '--confirm')).toBeDefined();
68
+ expect(initCmd.options.find((o) => o.long === '--lang')).toBeDefined();
68
69
  expect(initCmd.options.find((o) => o.long === '--json')).toBeDefined();
69
70
  });
70
71
 
@@ -82,7 +83,7 @@ describe('pd intent init — option metadata', () => {
82
83
  });
83
84
 
84
85
  describe('pd intent show — option metadata', () => {
85
- it('has --workspace (-w) and --json options, no --force', () => {
86
+ it('has --workspace (-w), --lang, and --json options, no --force', () => {
86
87
  const program = freshProgram();
87
88
  registerIntentCommand(program);
88
89
  const intentCmd = requireCmd(program.commands.find((c) => c.name() === 'intent'), 'intent');
@@ -90,6 +91,7 @@ describe('pd intent show — option metadata', () => {
90
91
 
91
92
  expect(showCmd.options.find((o) => o.long === '--workspace')).toBeDefined();
92
93
  expect(showCmd.options.find((o) => o.long === '--workspace')?.short).toBe('-w');
94
+ expect(showCmd.options.find((o) => o.long === '--lang')).toBeDefined();
93
95
  expect(showCmd.options.find((o) => o.long === '--json')).toBeDefined();
94
96
  expect(showCmd.options.find((o) => o.long === '--force')).toBeUndefined();
95
97
  });
@@ -195,3 +197,86 @@ describe('pd intent show — parser-level dispatch', () => {
195
197
  expect(captured.opts?.workspace).toBe('/tmp/ws');
196
198
  });
197
199
  });
200
+
201
+ describe('pd intent --lang — parser-level dispatch (cli-7)', () => {
202
+ it('init: parses --lang zh-CN', async () => {
203
+ const program = freshProgram();
204
+ const intentCmd = registerIntentCommand(program);
205
+ const initCmd = requireCmd(intentCmd.commands.find((c) => c.name() === 'init'), 'init');
206
+ const captured: CapturedAction = { opts: null };
207
+ attachCapture(initCmd, captured);
208
+
209
+ await program.parseAsync(['node', 'pd', 'intent', 'init', '--lang', 'zh-CN']);
210
+ expect(captured.opts).not.toBeNull();
211
+ expect(captured.opts?.lang).toBe('zh-CN');
212
+ });
213
+
214
+ it('init: parses --lang en', async () => {
215
+ const program = freshProgram();
216
+ const intentCmd = registerIntentCommand(program);
217
+ const initCmd = requireCmd(intentCmd.commands.find((c) => c.name() === 'init'), 'init');
218
+ const captured: CapturedAction = { opts: null };
219
+ attachCapture(initCmd, captured);
220
+
221
+ await program.parseAsync(['node', 'pd', 'intent', 'init', '--lang', 'en']);
222
+ expect(captured.opts).not.toBeNull();
223
+ expect(captured.opts?.lang).toBe('en');
224
+ });
225
+
226
+ it('init: lang defaults to undefined when not passed', async () => {
227
+ const program = freshProgram();
228
+ const intentCmd = registerIntentCommand(program);
229
+ const initCmd = requireCmd(intentCmd.commands.find((c) => c.name() === 'init'), 'init');
230
+ const captured: CapturedAction = { opts: null };
231
+ attachCapture(initCmd, captured);
232
+
233
+ await program.parseAsync(['node', 'pd', 'intent', 'init']);
234
+ expect(captured.opts).not.toBeNull();
235
+ expect(captured.opts?.lang).toBeUndefined();
236
+ });
237
+
238
+ it('show: parses --lang en', async () => {
239
+ const program = freshProgram();
240
+ const intentCmd = registerIntentCommand(program);
241
+ const showCmd = requireCmd(intentCmd.commands.find((c) => c.name() === 'show'), 'show');
242
+ const captured: CapturedAction = { opts: null };
243
+ attachCapture(showCmd, captured);
244
+
245
+ await program.parseAsync(['node', 'pd', 'intent', 'show', '--lang', 'en']);
246
+ expect(captured.opts).not.toBeNull();
247
+ expect(captured.opts?.lang).toBe('en');
248
+ });
249
+
250
+ it('show: lang defaults to undefined when not passed', async () => {
251
+ const program = freshProgram();
252
+ const intentCmd = registerIntentCommand(program);
253
+ const showCmd = requireCmd(intentCmd.commands.find((c) => c.name() === 'show'), 'show');
254
+ const captured: CapturedAction = { opts: null };
255
+ attachCapture(showCmd, captured);
256
+
257
+ await program.parseAsync(['node', 'pd', 'intent', 'show']);
258
+ expect(captured.opts).not.toBeNull();
259
+ expect(captured.opts?.lang).toBeUndefined();
260
+ });
261
+ });
262
+
263
+ describe('parseLang — invalid value rejection (rc-9)', () => {
264
+ it('returns zh-CN when value is undefined (default)', () => {
265
+ expect(parseLang(undefined)).toBe('zh-CN');
266
+ });
267
+
268
+ it('returns zh-CN when value is zh-CN', () => {
269
+ expect(parseLang('zh-CN')).toBe('zh-CN');
270
+ });
271
+
272
+ it('returns en when value is en', () => {
273
+ expect(parseLang('en')).toBe('en');
274
+ });
275
+
276
+ it('returns null for invalid value (no silent fallback)', () => {
277
+ expect(parseLang('fr')).toBeNull();
278
+ expect(parseLang('zh')).toBeNull();
279
+ expect(parseLang('english')).toBeNull();
280
+ expect(parseLang('')).toBeNull();
281
+ });
282
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * CLI flag-wiring tests for `pd pain retry --maxTokens` (PRI-509 follow-up).
3
+ *
4
+ * Covers:
5
+ * - `--maxTokens <n>` parses to a number and is passed to the handler
6
+ * - `--maxTokens` falls back to undefined when the flag is omitted
7
+ *
8
+ * Mirrors the flag-wiring test pattern from run-rulehost-flag-wiring.test.ts.
9
+ */
10
+ import { describe, it, expect, beforeEach } from 'vitest';
11
+ import { Command } from 'commander';
12
+ import { registerPainRetryCommand } from '../pain-retry.js';
13
+
14
+ function freshProgram(): Command {
15
+ return new Command();
16
+ }
17
+
18
+ function attachCapture(cmd: Command, captured: { opts: Record<string, unknown> | null }) {
19
+ const origAction = cmd.action.bind(cmd);
20
+ cmd.action = ((handler: (opts: Record<string, unknown>) => void) => {
21
+ return origAction((opts: Record<string, unknown>) => {
22
+ captured.opts = opts;
23
+ return handler(opts);
24
+ });
25
+ }) as typeof cmd.action;
26
+ }
27
+
28
+ function register(painCmd: Command, captured: { opts: Record<string, unknown> | null }) {
29
+ const cmd = registerPainRetryCommand(painCmd, async (opts) => {
30
+ captured.opts = opts as unknown as Record<string, unknown>;
31
+ });
32
+ attachCapture(cmd, captured);
33
+ }
34
+
35
+ describe('pd pain retry --maxTokens flag wiring', () => {
36
+ let captured: { opts: Record<string, unknown> | null };
37
+
38
+ beforeEach(() => {
39
+ captured = { opts: null };
40
+ });
41
+
42
+ it('parses --maxTokens to a number and passes it to the handler', async () => {
43
+ const program = freshProgram();
44
+ const painCmd = program.command('pain');
45
+ register(painCmd, captured);
46
+
47
+ await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'pain-1', '--runtime', 'pi-ai', '--maxTokens', '16000']);
48
+
49
+ expect(captured.opts).not.toBeNull();
50
+ expect(captured.opts?.maxTokens).toBe(16000);
51
+ expect(typeof captured.opts?.maxTokens).toBe('number');
52
+ });
53
+
54
+ it('parses --maxTokens with a numeric string (parseInt path)', async () => {
55
+ const program = freshProgram();
56
+ const painCmd = program.command('pain');
57
+ register(painCmd, captured);
58
+
59
+ await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'pain-1', '--runtime', 'pi-ai', '--maxTokens', '32000']);
60
+
61
+ expect(captured.opts?.maxTokens).toBe(32000);
62
+ });
63
+
64
+ it('keeps maxTokens undefined when flag is omitted (falls back to config)', async () => {
65
+ const program = freshProgram();
66
+ const painCmd = program.command('pain');
67
+ register(painCmd, captured);
68
+
69
+ await program.parseAsync(['node', 'pd', 'pain', 'retry', '--pain-id', 'pain-1', '--runtime', 'pi-ai']);
70
+
71
+ expect(captured.opts?.maxTokens).toBeUndefined();
72
+ });
73
+ });
@@ -52,6 +52,13 @@ function freshProgram(): Command {
52
52
  return program;
53
53
  }
54
54
 
55
+ /** Find a subcommand by name, throwing if absent (replaces non-null assertions). */
56
+ function requireSubcommand(parent: Command, name: string): Command {
57
+ const cmd = parent.commands.find((c) => c.name() === name);
58
+ if (!cmd) throw new Error(`Expected subcommand "${name}" to be registered`);
59
+ return cmd;
60
+ }
61
+
55
62
  describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
56
63
  // ── spec subcommand ───────────────────────────────────────────────────────
57
64
 
@@ -62,10 +69,10 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
62
69
  const specCmd = rulecodeCmd.commands.find((c) => c.name() === 'spec');
63
70
  expect(specCmd).toBeDefined();
64
71
 
65
- const jsonOpt = specCmd!.options.find((o) => o.long === '--json');
72
+ const jsonOpt = (specCmd as Command).options.find((o) => o.long === '--json');
66
73
  expect(jsonOpt).toBeDefined();
67
74
 
68
- const wsOpt = specCmd!.options.find((o) => o.long === '--workspace');
75
+ const wsOpt = (specCmd as Command).options.find((o) => o.long === '--workspace');
69
76
  expect(wsOpt).toBeDefined();
70
77
  expect(wsOpt?.short).toBe('-w');
71
78
  });
@@ -75,7 +82,7 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
75
82
  const rulecodeCmd = registerRulecodeCommand(program);
76
83
 
77
84
  const specCmd = rulecodeCmd.commands.find((c) => c.name() === 'spec');
78
- const codeOpt = specCmd!.options.find((o) => o.long === '--code');
85
+ const codeOpt = (specCmd as Command).options.find((o) => o.long === '--code');
79
86
  expect(codeOpt).toBeUndefined();
80
87
  });
81
88
 
@@ -88,16 +95,16 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
88
95
  const validateCmd = rulecodeCmd.commands.find((c) => c.name() === 'validate');
89
96
  expect(validateCmd).toBeDefined();
90
97
 
91
- const codeOpt = validateCmd!.options.find((o) => o.long === '--code');
98
+ const codeOpt = (validateCmd as Command).options.find((o) => o.long === '--code');
92
99
  expect(codeOpt).toBeDefined();
93
100
 
94
- const codeFileOpt = validateCmd!.options.find((o) => o.long === '--code-file');
101
+ const codeFileOpt = (validateCmd as Command).options.find((o) => o.long === '--code-file');
95
102
  expect(codeFileOpt).toBeDefined();
96
103
 
97
- const jsonOpt = validateCmd!.options.find((o) => o.long === '--json');
104
+ const jsonOpt = (validateCmd as Command).options.find((o) => o.long === '--json');
98
105
  expect(jsonOpt).toBeDefined();
99
106
 
100
- const wsOpt = validateCmd!.options.find((o) => o.long === '--workspace');
107
+ const wsOpt = (validateCmd as Command).options.find((o) => o.long === '--workspace');
101
108
  expect(wsOpt).toBeDefined();
102
109
  expect(wsOpt?.short).toBe('-w');
103
110
  });
@@ -105,7 +112,7 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
105
112
  it('validate --code is NOT required at parser level (can use --code-file instead)', async () => {
106
113
  const program = freshProgram();
107
114
  const rulecodeCmd = registerRulecodeCommand(program);
108
- const validateCmd = rulecodeCmd.commands.find((c) => c.name() === 'validate')!;
115
+ const validateCmd = requireSubcommand(rulecodeCmd, 'validate');
109
116
  const captured: CapturedAction = { opts: null };
110
117
  attachCapture(validateCmd, captured);
111
118
 
@@ -113,7 +120,7 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
113
120
  await program.parseAsync(['node', 'pd', 'rulecode', 'validate', '--json']);
114
121
 
115
122
  expect(captured.opts).not.toBeNull();
116
- expect(captured.opts!.code).toBeUndefined();
123
+ expect((captured.opts as ActionOptions).code).toBeUndefined();
117
124
  });
118
125
 
119
126
  // ── replay subcommand ─────────────────────────────────────────────────────
@@ -125,19 +132,19 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
125
132
  const replayCmd = rulecodeCmd.commands.find((c) => c.name() === 'replay');
126
133
  expect(replayCmd).toBeDefined();
127
134
 
128
- const codeOpt = replayCmd!.options.find((o) => o.long === '--code');
135
+ const codeOpt = (replayCmd as Command).options.find((o) => o.long === '--code');
129
136
  expect(codeOpt).toBeDefined();
130
137
 
131
- const codeFileOpt = replayCmd!.options.find((o) => o.long === '--code-file');
138
+ const codeFileOpt = (replayCmd as Command).options.find((o) => o.long === '--code-file');
132
139
  expect(codeFileOpt).toBeDefined();
133
140
 
134
- const gtOpt = replayCmd!.options.find((o) => o.long === '--golden-trace');
141
+ const gtOpt = (replayCmd as Command).options.find((o) => o.long === '--golden-trace');
135
142
  expect(gtOpt).toBeDefined();
136
143
 
137
- const jsonOpt = replayCmd!.options.find((o) => o.long === '--json');
144
+ const jsonOpt = (replayCmd as Command).options.find((o) => o.long === '--json');
138
145
  expect(jsonOpt).toBeDefined();
139
146
 
140
- const wsOpt = replayCmd!.options.find((o) => o.long === '--workspace');
147
+ const wsOpt = (replayCmd as Command).options.find((o) => o.long === '--workspace');
141
148
  expect(wsOpt).toBeDefined();
142
149
  expect(wsOpt?.short).toBe('-w');
143
150
  });
@@ -147,7 +154,7 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
147
154
  const rulecodeCmd = registerRulecodeCommand(program);
148
155
 
149
156
  const replayCmd = rulecodeCmd.commands.find((c) => c.name() === 'replay');
150
- const gtOpt = replayCmd!.options.find((o) => o.long === '--golden-trace');
157
+ const gtOpt = (replayCmd as Command).options.find((o) => o.long === '--golden-trace');
151
158
  expect(gtOpt?.required).toBe(true);
152
159
  });
153
160
 
@@ -168,20 +175,20 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
168
175
  it('parseAsync dispatches spec subcommand with json=true', async () => {
169
176
  const program = freshProgram();
170
177
  const rulecodeCmd = registerRulecodeCommand(program);
171
- const specCmd = rulecodeCmd.commands.find((c) => c.name() === 'spec')!;
178
+ const specCmd = requireSubcommand(rulecodeCmd, 'spec');
172
179
  const captured: CapturedAction = { opts: null };
173
180
  attachCapture(specCmd, captured);
174
181
 
175
182
  await program.parseAsync(['node', 'pd', 'rulecode', 'spec', '--json']);
176
183
 
177
184
  expect(captured.opts).not.toBeNull();
178
- expect(captured.opts!.json).toBe(true);
185
+ expect((captured.opts as ActionOptions).json).toBe(true);
179
186
  });
180
187
 
181
188
  it('parseAsync dispatches validate with --code', async () => {
182
189
  const program = freshProgram();
183
190
  const rulecodeCmd = registerRulecodeCommand(program);
184
- const validateCmd = rulecodeCmd.commands.find((c) => c.name() === 'validate')!;
191
+ const validateCmd = requireSubcommand(rulecodeCmd, 'validate');
185
192
  const captured: CapturedAction = { opts: null };
186
193
  attachCapture(validateCmd, captured);
187
194
 
@@ -192,14 +199,14 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
192
199
  ]);
193
200
 
194
201
  expect(captured.opts).not.toBeNull();
195
- expect(captured.opts!.code).toContain('function evaluate');
196
- expect(captured.opts!.json).toBe(true);
202
+ expect((captured.opts as ActionOptions).code).toContain('function evaluate');
203
+ expect((captured.opts as ActionOptions).json).toBe(true);
197
204
  });
198
205
 
199
206
  it('parseAsync dispatches replay with --code and --golden-trace', async () => {
200
207
  const program = freshProgram();
201
208
  const rulecodeCmd = registerRulecodeCommand(program);
202
- const replayCmd = rulecodeCmd.commands.find((c) => c.name() === 'replay')!;
209
+ const replayCmd = requireSubcommand(rulecodeCmd, 'replay');
203
210
  const captured: CapturedAction = { opts: null };
204
211
  attachCapture(replayCmd, captured);
205
212
 
@@ -211,9 +218,9 @@ describe('pd rulecode — flag wiring (CLI gate rule 7)', () => {
211
218
  ]);
212
219
 
213
220
  expect(captured.opts).not.toBeNull();
214
- expect(captured.opts!.code).toContain('function evaluate');
215
- expect(captured.opts!.goldenTrace).toBe('/tmp/trace.json');
216
- expect(captured.opts!.json).toBe(true);
221
+ expect((captured.opts as ActionOptions).code).toContain('function evaluate');
222
+ expect((captured.opts as ActionOptions).goldenTrace).toBe('/tmp/trace.json');
223
+ expect((captured.opts as ActionOptions).json).toBe(true);
217
224
  });
218
225
 
219
226
  it('parseAsync rejects replay without --golden-trace (requiredOption)', async () => {
@@ -88,6 +88,18 @@ describe('pd runtime internalization run-rulehost — flag wiring (CLI gate rule
88
88
  expect(opt?.long).toBe('--json');
89
89
  });
90
90
 
91
+ it('parses --behavior-examples as the Owner-labelled evidence file', async () => {
92
+ const program = freshProgram();
93
+ const intCmd = program.command('internalization');
94
+ const runCmd = registerRunRuleHostCommand(intCmd);
95
+ const captured: CapturedAction = { opts: null };
96
+ attachCapture(runCmd, captured);
97
+
98
+ await program.parseAsync(['node', 'pd', 'internalization', 'run-rulehost', '--pain-id', 'pain-1', '--behavior-examples', 'examples.json']);
99
+
100
+ expect(captured.opts?.behaviorExamples).toBe('examples.json');
101
+ });
102
+
91
103
  it('registers --pain-id as required option', () => {
92
104
  const program = freshProgram();
93
105
  const intCmd = program.command('internalization');
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared admission gate helper for CLI commands that call intakeService.intake().
3
+ *
4
+ * PRI-442 Stage 4 / PRI-503: previously only `candidate.ts` checked the gate;
5
+ * `pain-retry.ts` and `diagnose.ts` bypassed it (ERR-089 sibling-branch defect).
6
+ * This shared helper is the single entry point so future CLI commands that
7
+ * call intake() can reuse it instead of re-implementing (or skipping) the check.
8
+ *
9
+ * Runtime Contract: rc-9-no-silent-fallback — refusal includes reason + nextAction
10
+ * (callers MUST surface these in their structured CLI output).
11
+ */
12
+ import { evaluateCandidateAdmissionFromRecord, type AdmissionGateResult } from '@principles/core/runtime-v2';
13
+
14
+ /**
15
+ * Check admission gate for a candidate. Returns null if admitted (caller may
16
+ * proceed); returns AdmissionGateResult with reason + nextAction if refused
17
+ * (caller must refuse and surface reason/nextAction in CLI output).
18
+ *
19
+ * @param candidate - Must carry recommendationKind and confidence from the candidate record
20
+ */
21
+ export function checkAdmissionGate(
22
+ candidate: { recommendationKind: string; confidence: number | null },
23
+ ): AdmissionGateResult | null {
24
+ const admission = evaluateCandidateAdmissionFromRecord({
25
+ recommendationKind: candidate.recommendationKind as Parameters<typeof evaluateCandidateAdmissionFromRecord>[0]['recommendationKind'],
26
+ confidence: candidate.confidence,
27
+ });
28
+ if (admission.decision === 'admitted') return null;
29
+ return admission;
30
+ }
@@ -27,6 +27,7 @@ import { loadLedger, getLedgerFilePathPublic } from '@principles/core/principle-
27
27
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
28
28
  import { createRemediationResult, remediationAction } from './remediation-output.js';
29
29
  import type { RemediationResult } from './remediation-output.js';
30
+ import { checkAdmissionGate } from './admission-gate.js';
30
31
 
31
32
  // ── Interfaces ────────────────────────────────────────────────────────────────
32
33
 
@@ -353,6 +354,7 @@ export async function handleCandidateInternalize(opts: CandidateInternalizeOptio
353
354
  }
354
355
 
355
356
  const seed = buildDreamerSeedFromCandidate(candidate, { route: decision.route, ready: decision.ready, sourcePainId });
357
+ // eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (BridgeTaskSeed | BridgeDecision)
356
358
  if ('decision' in seed) {
357
359
  const decisionResult = seed as { decision: string; reason?: string; taskId?: string };
358
360
  const reason = decisionResult.decision === 'already_exists'
@@ -494,6 +496,13 @@ export async function handleCandidateShow(opts: CandidateShowOptions): Promise<v
494
496
  if (result.ledgerEntryId) {
495
497
  console.log(` Ledger Entry: ${result.ledgerEntryId}`);
496
498
  }
499
+ // F10-1: surface dangling sourceRunId warning in text mode (rc-9).
500
+ if (result.warning) {
501
+ console.log(` ⚠ WARNING: ${result.warning}`);
502
+ }
503
+ if (result.nextAction) {
504
+ console.log(` Next Action: ${result.nextAction}`);
505
+ }
497
506
  console.log('');
498
507
  } finally {
499
508
  await stateManager.close();
@@ -520,12 +529,37 @@ export async function handleCandidateIntake(opts: CandidateIntakeOptions): Promi
520
529
  const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
521
530
  const service = new CandidateIntakeService({ stateManager, ledgerAdapter });
522
531
 
523
- if (opts.dryRun) {
524
- const candidate = await stateManager.getCandidate(opts.candidateId);
525
- if (!candidate) {
526
- console.error(`Candidate not found: ${opts.candidateId}`);
527
- process.exit(1);
532
+ // Fetch candidate once for admission gate check and downstream paths.
533
+ const candidate = await stateManager.getCandidate(opts.candidateId);
534
+ if (!candidate) {
535
+ console.error(`Candidate not found: ${opts.candidateId}`);
536
+ process.exit(1);
537
+ return;
538
+ }
539
+
540
+ // PRI-442 Stage 4: admission gate check — refuse non-admitted candidates
541
+ // to prevent CLI from bypassing the production admission gate.
542
+ const admissionBlock = checkAdmissionGate(candidate);
543
+ if (admissionBlock) {
544
+ const result = {
545
+ candidateId: opts.candidateId,
546
+ status: 'refused',
547
+ admissionDecision: admissionBlock.decision,
548
+ reason: admissionBlock.reason,
549
+ nextAction: admissionBlock.nextAction,
550
+ };
551
+ if (opts.json) {
552
+ console.log(JSON.stringify(result, null, 2));
553
+ } else {
554
+ console.error(`Admission gate refused candidate ${opts.candidateId}: ${admissionBlock.decision}`);
555
+ console.error(` Reason: ${admissionBlock.reason}`);
556
+ console.error(` Next Action: ${admissionBlock.nextAction}`);
528
557
  }
558
+ process.exit(1);
559
+ return;
560
+ }
561
+
562
+ if (opts.dryRun) {
529
563
  const artifact = await stateManager.getArtifact(candidate.artifactId);
530
564
  if (!artifact) {
531
565
  console.error(`Artifact not found for candidate: ${opts.candidateId}`);
@@ -564,7 +598,6 @@ export async function handleCandidateIntake(opts: CandidateIntakeOptions): Promi
564
598
  const entry = await service.intake(opts.candidateId);
565
599
 
566
600
  // Check if already consumed before this call
567
- const candidate = await stateManager.getCandidate(opts.candidateId);
568
601
  if (candidate?.status === 'consumed') {
569
602
  const infoMessage = `Candidate ${opts.candidateId} was already consumed. Ledger entry: ${entry.id}`;
570
603
  if (opts.json) {
@@ -724,6 +757,30 @@ export async function handleCandidateRepair(opts: CandidateRepairOptions): Promi
724
757
  process.exit(1);
725
758
  }
726
759
 
760
+ // PRI-442 Stage 4: admission gate check — even for consumed candidates,
761
+ // re-verify admission before re-intaking to prevent bypassing the gate.
762
+ // A consumed candidate that would fail the gate indicates data corruption
763
+ // or a prior bypass; refuse and surface the reason.
764
+ const admissionBlock = checkAdmissionGate(candidate);
765
+ if (admissionBlock) {
766
+ const result = {
767
+ candidateId: opts.candidateId,
768
+ status: 'refused',
769
+ admissionDecision: admissionBlock.decision,
770
+ reason: admissionBlock.reason,
771
+ nextAction: admissionBlock.nextAction,
772
+ };
773
+ if (opts.json) {
774
+ console.log(JSON.stringify(result, null, 2));
775
+ } else {
776
+ console.error(`Admission gate refused candidate ${opts.candidateId}: ${admissionBlock.decision}`);
777
+ console.error(` Reason: ${admissionBlock.reason}`);
778
+ console.error(` Next Action: ${admissionBlock.nextAction}`);
779
+ }
780
+ process.exit(1);
781
+ return;
782
+ }
783
+
727
784
  const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: path.join(workspaceDir, '.state') });
728
785
  const service = new CandidateIntakeService({ stateManager, ledgerAdapter });
729
786
 
@@ -931,6 +988,7 @@ export async function handleCandidateInternalizationBackfill(opts: CandidateBack
931
988
  continue;
932
989
  }
933
990
  const seed = buildDreamerSeedFromCandidate(candidate, { route: decision.route, ready: decision.ready, sourcePainId });
991
+ // eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (BridgeTaskSeed | BridgeDecision)
934
992
  if ('decision' in seed) {
935
993
  output.deferred++;
936
994
  const decisionResult = seed as { decision: string; reason?: string; taskId?: string };
@@ -998,6 +1056,15 @@ export async function handleCandidateInternalizationBackfill(opts: CandidateBack
998
1056
  continue;
999
1057
  }
1000
1058
 
1059
+ // PRI-442 Stage 4: admission gate check — refuse non-admitted pending
1060
+ // candidates to prevent backfill from bypassing the admission gate.
1061
+ const admissionBlock = checkAdmissionGate(candidate);
1062
+ if (admissionBlock) {
1063
+ output.intakeFailed++;
1064
+ output.results.push({ candidateId, route: 'unknown', status: 'intake_failed', reason: `Admission gate refused: ${admissionBlock.reason}`, statusBefore: 'pending', statusAfter: 'pending', intakeDecision: 'skipped', seedDecision: 'skipped', nextAction: admissionBlock.nextAction });
1065
+ continue;
1066
+ }
1067
+
1001
1068
  const recommendation = resolveCandidateRecommendation(candidate, stateManager, candidateId);
1002
1069
  const decision = decideInternalizationRoute(recommendation as Parameters<typeof decideInternalizationRoute>[0]);
1003
1070
 
@@ -1010,6 +1077,7 @@ export async function handleCandidateInternalizationBackfill(opts: CandidateBack
1010
1077
  continue;
1011
1078
  }
1012
1079
  const seed = buildDreamerSeedFromCandidate(candidate, { route: decision.route, ready: decision.ready, sourcePainId });
1080
+ // eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (BridgeTaskSeed | BridgeDecision)
1013
1081
  if ('decision' in seed) {
1014
1082
  output.deferred++;
1015
1083
  const decisionResult = seed as { decision: string; reason?: string; taskId?: string };
@@ -72,6 +72,20 @@ export async function handleConsole(opts: ConsoleOptions = {}): Promise<void> {
72
72
  return;
73
73
  }
74
74
 
75
+ // EP-06 regression guard (PR #1169): verify web UI bundle exists before launch.
76
+ // Without dist/web/index.html the server returns 404 "Run npm run build:ui first".
77
+ const webIndex = path.join(consoleDir, 'dist', 'web', 'index.html');
78
+ if (!fs.existsSync(webIndex)) {
79
+ const msg = `Console web UI not found at ${webIndex}. The console bundle is corrupted. Re-run installer.`;
80
+ if (opts.json) {
81
+ console.log(JSON.stringify({ success: false, reason: msg, nextAction: 'npx create-principles-disciple' }));
82
+ } else {
83
+ console.error(msg);
84
+ }
85
+ process.exit(1);
86
+ return;
87
+ }
88
+
75
89
  const port = opts.port || '3100';
76
90
  const host = '127.0.0.1';
77
91
  const args = [serverEntry, '--workspace', workspaceDir, '--port', port, '--host', host];
@@ -305,6 +319,32 @@ export async function handleConsoleOpen(opts: ConsoleOpenOptions = {}): Promise<
305
319
  return;
306
320
  }
307
321
 
322
+ // EP-06 regression guard (PR #1169): verify web UI bundle exists before launch.
323
+ // Without dist/web/index.html the server returns 404 "Run npm run build:ui first"
324
+ // — a fatal first-impression bug for new users.
325
+ const webIndex = path.join(consoleDir, 'dist', 'web', 'index.html');
326
+ if (!fs.existsSync(webIndex)) {
327
+ const result: ConsoleLaunchResult = {
328
+ status: 'failed',
329
+ url: '',
330
+ port: 0,
331
+ host: '127.0.0.1',
332
+ workspaceDir,
333
+ reused: false,
334
+ browserOpened: false,
335
+ reason: 'console_web_ui_missing',
336
+ nextAction: `Re-run installer: npx create-principles-disciple (expected ${webIndex})`,
337
+ };
338
+ if (opts.json) {
339
+ console.log(JSON.stringify(result, null, 2));
340
+ } else {
341
+ console.error(`error: ${result.reason}`);
342
+ console.error(`next: ${result.nextAction}`);
343
+ }
344
+ process.exit(1);
345
+ return;
346
+ }
347
+
308
348
  // 4) Read auth token for health probes (PD_CONSOLE_TOKEN)
309
349
  const token = opts.token ?? process.env.PD_CONSOLE_TOKEN;
310
350
 
@@ -183,8 +183,10 @@ export async function handleDemoStoryA(opts: DemoStoryAOptions): Promise<void> {
183
183
  }
184
184
 
185
185
  if (result.status !== 'passed') {
186
- console.error('');
187
- console.error(`FAIL: status=${result.status}`);
186
+ if (!opts.json) {
187
+ console.error('');
188
+ console.error(`FAIL: status=${result.status}`);
189
+ }
188
190
  process.exitCode = 1;
189
191
  }
190
192
  } finally {