@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
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Command-registration / parser tests for context-trace (PR 5 task 11.2, cli-7).
3
+ *
4
+ * Tests the real Commander program — exercises actual flag parsing, not just
5
+ * the handler. Verifies:
6
+ * - --task / --artifact / --workspace / --json parse correctly
7
+ * - --dry-run / --confirm do NOT exist (read-only command, cli-4)
8
+ * - no new feature flag is introduced (registry diff is empty)
9
+ *
10
+ * @see .kiro/specs/internalization-progressive-disclosure/design.md §6.7
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { Command } from 'commander';
15
+ import { DEFAULT_FEATURE_FLAGS } from '@principles/core/runtime-v2';
16
+
17
+ // Build a minimal program that mirrors the real registration.
18
+ function buildTestProgram(): Command {
19
+ const program = new Command();
20
+ const runtime = program.command('runtime');
21
+ const internalization = runtime.command('internalization', { hidden: true });
22
+
23
+ internalization
24
+ .command('context-trace')
25
+ .description('Trace the internalization context chain')
26
+ .requiredOption('-t, --task <taskId>', 'Task ID to trace')
27
+ .option('-a, --artifact <artifactId>', 'Specific artifact ID to start from')
28
+ .option('-w, --workspace <path>', 'Workspace directory')
29
+ .option('--json', 'Output raw JSON (default)')
30
+ .action(() => {});
31
+
32
+ return program;
33
+ }
34
+
35
+ describe('context-trace command registration (cli-7)', () => {
36
+ it('parses --task correctly', () => {
37
+ const program = buildTestProgram();
38
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 'task-123']);
39
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
40
+ expect(cmd).toBeDefined();
41
+ expect(cmd?.opts().task).toBe('task-123');
42
+ });
43
+
44
+ it('parses --artifact correctly', () => {
45
+ const program = buildTestProgram();
46
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 't1', '--artifact', 'art-1']);
47
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
48
+ expect(cmd?.opts().artifact).toBe('art-1');
49
+ });
50
+
51
+ it('parses --workspace correctly', () => {
52
+ const program = buildTestProgram();
53
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 't1', '--workspace', '/tmp/pd']);
54
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
55
+ expect(cmd?.opts().workspace).toBe('/tmp/pd');
56
+ });
57
+
58
+ it('parses --json flag', () => {
59
+ const program = buildTestProgram();
60
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 't1', '--json']);
61
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
62
+ expect(cmd?.opts().json).toBe(true);
63
+ });
64
+
65
+ it('requires --task (exits with error if missing)', () => {
66
+ const program = buildTestProgram();
67
+ // Commander calls process.exit on missing required option; stub it.
68
+ const originalExit = process.exit;
69
+ let exitCode: number | undefined;
70
+ process.exit = ((code?: number) => { exitCode = code; }) as never;
71
+ try {
72
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace']);
73
+ } catch {
74
+ // Commander may throw after stubbed exit
75
+ } finally {
76
+ process.exit = originalExit;
77
+ }
78
+ expect(exitCode).toBe(1);
79
+ });
80
+
81
+ it('does NOT have --dry-run option (read-only command, cli-4)', () => {
82
+ const program = buildTestProgram();
83
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 't1']);
84
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
85
+ expect(cmd?.opts().dryRun).toBeUndefined();
86
+ // Verify the option doesn't exist at all.
87
+ const dryRunOption = cmd?.options.find((o) => o.long === '--dry-run');
88
+ expect(dryRunOption).toBeUndefined();
89
+ });
90
+
91
+ it('does NOT have --confirm option (read-only command, cli-4)', () => {
92
+ const program = buildTestProgram();
93
+ program.parse(['node', 'pd', 'runtime', 'internalization', 'context-trace', '--task', 't1']);
94
+ const cmd = program.commands[0]?.commands[0]?.commands[0];
95
+ expect(cmd?.opts().confirm).toBeUndefined();
96
+ const confirmOption = cmd?.options.find((o) => o.long === '--confirm');
97
+ expect(confirmOption).toBeUndefined();
98
+ });
99
+
100
+ it('context-trace does not introduce a new feature flag (registry diff empty)', () => {
101
+ // design §8: Layer 3 (CLI) does not add a new flag. The command is always
102
+ // available (read-only); it reads the EXISTING 3 flags.
103
+ const knownFlags = new Set(DEFAULT_FEATURE_FLAGS.map((f) => f.id));
104
+ expect(knownFlags.has('artifact_summary_redundancy')).toBe(true);
105
+ expect(knownFlags.has('context_manifest_budget')).toBe(true);
106
+ expect(knownFlags.has('progressive_evaluator')).toBe(true);
107
+ // No 'context_trace' flag should exist.
108
+ expect(knownFlags.has('context_trace')).toBe(false);
109
+ });
110
+ });
@@ -46,8 +46,8 @@ describe('CLI command tree: pd runtime internalization', () => {
46
46
  expect(output).toContain('--json');
47
47
  });
48
48
 
49
- it('runtime subcommand list does NOT include internalization (PRI-455: hidden from --help)', () => {
49
+ it('runtime subcommand list includes internalization (PRI-419 amendment: un-hidden for operator visibility)', () => {
50
50
  const output = runPdHelp(['runtime', '--help']);
51
- expect(output).not.toMatch(/internalization\s/);
51
+ expect(output).toMatch(/internalization\s/);
52
52
  });
53
53
  });
@@ -22,15 +22,16 @@ vi.mock('fs', () => ({
22
22
  readFileSync: vi.fn(),
23
23
  }));
24
24
 
25
+ // Mock js-yaml with NAMED exports (matches `import * as yaml from 'js-yaml'`
26
+ // in config-reader.ts). js-yaml 5.x removed the default export, so the mock
27
+ // must provide `load` and `JSON_SCHEMA` as named members, not under `default`.
25
28
  vi.mock('js-yaml', () => ({
26
- default: {
27
- load: vi.fn(),
28
- JSON_SCHEMA: 'JSON_SCHEMA',
29
- },
29
+ load: vi.fn(),
30
+ JSON_SCHEMA: 'JSON_SCHEMA',
30
31
  }));
31
32
 
32
33
  import * as fs from 'fs';
33
- import yaml from 'js-yaml';
34
+ import * as yaml from 'js-yaml';
34
35
 
35
36
  const mockExistsSync = vi.mocked(fs.existsSync);
36
37
  const mockReadFileSync = vi.mocked(fs.readFileSync);
@@ -173,7 +173,7 @@ function artificerV2(taskId: string, priorId?: string): unknown {
173
173
  return {
174
174
  taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
175
175
  implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
176
- implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); return p.startsWith("/etc") ? { decision: "block", matched: true, reason: "system path" } : { decision: "allow", matched: false, reason: "ok" }; }',
176
+ implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
177
177
  implementationSummary: 'Block system path writes',
178
178
  goldenTraceCases: [
179
179
  { caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' },
@@ -397,21 +397,23 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
397
397
  expect(activationRecord!.artifactId).toBe(revisedArtifactId);
398
398
  expect(activationRecord!.channel).toBe('code_tool_hook');
399
399
  expect(activationRecord!.deactivatedAt).toBeNull();
400
+ // PRI-489: Owner approval creates a SHADOW activation first (not live).
401
+ // Shadow activations are observation-only — they record would-block into
402
+ // shadowDecisions but never actually block the tool call. The only
403
+ // shadow -> live transition is `pd activation promote --confirm`.
404
+ expect(activationRecord!.action).toBe('code_tool_hook_shadow_activate');
400
405
 
401
406
  // Verify via listCodeToolHookActivations (P2 #5: default excludes deactivated)
402
407
  const activeActivations = await stateStore.listCodeToolHookActivations();
403
408
  const ourActivation = activeActivations.find(a => a.artifactId === revisedArtifactId);
404
409
  expect(ourActivation).toBeDefined();
405
410
  expect(ourActivation!.deactivatedAt).toBeNull();
411
+ expect(ourActivation!.action).toBe('code_tool_hook_shadow_activate');
406
412
 
407
- // ── Step 7b: Verify the rule actually affects behavior via the gate (P1 #4)
408
- // The production RuleHost gate loads all active rules, compiles their code,
409
- // and calls evaluate() on each tool call. This step simulates that:
410
- // 1. Load active rules from the activation state store
411
- // 2. Compile the rule code in the production vm sandbox
412
- // 3. Call evaluate() with system-path and normal-path inputs
413
- // 4. Verify the rule blocks system paths and allows normal paths
414
-
413
+ // ── Step 7a: Verify shadow activation does NOT block (PRI-489) ────────
414
+ // Shadow mode is observation-only. The RuleHost loads the activation but
415
+ // records shadowDecisions instead of blocking. evaluate() must return
416
+ // undefined (no block) for shadow activations, even for /etc/passwd.
415
417
  const makeRuleHostInput = (targetPath: string): RuleHostInput => ({
416
418
  action: { toolName: 'write_file', normalizedPath: targetPath, paramsSummary: { path: targetPath } },
417
419
  workspace: { isRiskPath: targetPath.startsWith('/etc'), planStatus: 'NONE', hasPlanFile: false },
@@ -419,6 +421,43 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
419
421
  evolution: { epTier: 0 },
420
422
  derived: { estimatedLineChanges: 1, bashRisk: 'safe' },
421
423
  });
424
+ const shadowRuleHost = new RuleHost(
425
+ path.join(tmpDir, '.state'),
426
+ { warn: () => {} },
427
+ { workspaceDir: tmpDir },
428
+ );
429
+ try {
430
+ const report = shadowRuleHost.evaluateDetailed(makeRuleHostInput('/etc/passwd'));
431
+ expect(report.liveDecision).toBeUndefined();
432
+ expect(report.shadowDecisions).toContainEqual(
433
+ expect.objectContaining({ activationId: activationRecord!.activationId }),
434
+ );
435
+ } finally {
436
+ shadowRuleHost.dispose();
437
+ }
438
+
439
+ // ── Step 7b: Promote shadow → live (PRI-489) ──────────────────────────
440
+ // `pd activation promote --activation-id ... --confirm` is the ONLY
441
+ // shadow → live entry. SqliteActivationStateStore.promoteActivation
442
+ // atomically rewrites action to `code_tool_hook_live_activate` inside a
443
+ // BEGIN IMMEDIATE transaction.
444
+ const activationId = activationRecord!.activationId;
445
+ const promoteResult = await stateStore.promoteActivation(activationId, new Date().toISOString());
446
+ expect(promoteResult).toBe(true);
447
+
448
+ // Verify the action is now live
449
+ const liveActivationRecord = await stateStore.getActivationStatus(idempotencyKey);
450
+ expect(liveActivationRecord).not.toBeNull();
451
+ expect(liveActivationRecord!.action).toBe('code_tool_hook_live_activate');
452
+ expect(liveActivationRecord!.promotedAt).not.toBeNull();
453
+
454
+ // ── Step 7c: Verify the rule actually blocks in live mode (P1 #4) ────
455
+ // The production RuleHost gate loads all active rules, compiles their code,
456
+ // and calls evaluate() on each tool call. This step simulates that:
457
+ // 1. Load active rules from the activation state store
458
+ // 2. Compile the rule code in the production vm sandbox
459
+ // 3. Call evaluate() with system-path and normal-path inputs
460
+ // 4. Verify the rule blocks system paths and allows normal paths
422
461
  const ruleHost = new RuleHost(
423
462
  path.join(tmpDir, '.state'),
424
463
  { warn: () => {} },
@@ -431,7 +470,6 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
431
470
  expect(ruleHost.evaluate(makeRuleHostInput('/project/src/main.ts'))).toBeUndefined();
432
471
 
433
472
  // ── Step 8: Owner deactivates (rollback) ───────────────────────────────
434
- const activationId = activationRecord!.activationId;
435
473
  const deactivateResult = await stateStore.deactivateActivation(activationId, new Date().toISOString());
436
474
  expect(deactivateResult).toBe(true);
437
475
 
@@ -461,10 +499,14 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
461
499
  expect(deactivatedRecord).toBeDefined();
462
500
  expect(deactivatedRecord!.deactivatedAt).not.toBeNull();
463
501
 
464
- // ── Step 10: Verify the activation status record reflects deactivation ─
502
+ // ── Step 10: Verify getActivationStatus reflects deactivation (Bug-Q)
503
+ // Bug-Q fix: getActivationStatus now filters out deactivated records
504
+ // (WHERE deactivated_at IS NULL), returning null when no active activation
505
+ // exists. This allows re-activation after deactivation. The deactivated
506
+ // record itself is already verified via listCodeToolHookActivations(true)
507
+ // above (L459-462, Step 9). Here we verify the new null semantics.
465
508
  const activationAfterDeactivate = await stateStore.getActivationStatus(idempotencyKey);
466
- expect(activationAfterDeactivate).not.toBeNull();
467
- expect(activationAfterDeactivate!.deactivatedAt).not.toBeNull();
509
+ expect(activationAfterDeactivate).toBeNull();
468
510
 
469
511
  await sm2.close();
470
512
  }, 60_000);
@@ -401,11 +401,14 @@ describe('assembleMainlineSnapshot', () => {
401
401
  await sm.updateCandidateStatus(candidate!.candidateId, { status: 'consumed' });
402
402
 
403
403
  // Insert a row with empty candidate_id to exercise the malformed-row path.
404
+ // F13 (PRI-442): include consumed_at so the row satisfies the schema
405
+ // CHECK constraint (status='consumed' → consumed_at IS NOT NULL). The
406
+ // malformation under test is the empty candidate_id, not the timestamp.
404
407
  const db = sm.connection.getDb();
405
408
  db.prepare(
406
- `INSERT INTO principle_candidates (candidate_id, task_id, artifact_id, source_run_id, title, description, idempotency_key, status, created_at, recommendation_kind)
407
- VALUES ('', ?, ?, ?, 'malformed', '', ?, 'consumed', ?, 'principle')`,
408
- ).run(taskId, candidate!.artifactId, candidate!.sourceRunId, `idemp-${Date.now()}`, new Date().toISOString());
409
+ `INSERT INTO principle_candidates (candidate_id, task_id, artifact_id, source_run_id, title, description, idempotency_key, status, created_at, consumed_at, recommendation_kind)
410
+ VALUES ('', ?, ?, ?, 'malformed', '', ?, 'consumed', ?, ?, 'principle')`,
411
+ ).run(taskId, candidate!.artifactId, candidate!.sourceRunId, `idemp-${Date.now()}`, new Date().toISOString(), new Date().toISOString());
409
412
 
410
413
  const { snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir, painId, readiness: healthyReadiness() });
411
414
 
@@ -266,6 +266,8 @@ describe('resolveRuntimeFromPdConfig', () => {
266
266
  const tmp = mkTmpDir();
267
267
  try {
268
268
  const result = resolveRuntimeFromPdConfig(tmp);
269
+ // Default profile is openclaw.default (MVP default runtime) → resolves
270
+ // successfully without env var.
269
271
  expect(isRuntimeConfigError(result.result)).toBe(false);
270
272
  expect(result.configSource).toBe('.pd/config.yaml');
271
273
  expect(result.runtimeProfileId).toBe('openclaw.default');
@@ -137,7 +137,7 @@ function artificerV2(taskId: string, priorId?: string): unknown {
137
137
  return {
138
138
  taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
139
139
  implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
140
- implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); return p.startsWith("/etc") ? { decision: "block", matched: true, reason: "system path" } : { decision: "allow", matched: false, reason: "ok" }; }',
140
+ implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
141
141
  implementationSummary: 'Block system path writes',
142
142
  goldenTraceCases: [
143
143
  { caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' },
package/vitest.config.ts CHANGED
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config';
3
3
  export default defineConfig({
4
4
  test: {
5
5
  environment: 'node',
6
- include: ['tests/**/*.test.ts', 'src/**/*.test.ts'],
6
+ include: ['tests/bdd/**/*.{test,steps}.ts', 'tests/**/*.test.ts', 'src/**/*.test.ts'],
7
7
  exclude: ['**/node_modules/**', '**/dist/**'],
8
8
  },
9
9
  });