@principles/pd-cli 1.128.2 → 1.130.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 (41) hide show
  1. package/dist/commands/runtime-canary.d.ts.map +1 -1
  2. package/dist/commands/runtime-canary.js +10 -11
  3. package/dist/commands/runtime-canary.js.map +1 -1
  4. package/dist/commands/runtime-internalization-queue.d.ts.map +1 -1
  5. package/dist/commands/runtime-internalization-queue.js +13 -16
  6. package/dist/commands/runtime-internalization-queue.js.map +1 -1
  7. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  8. package/dist/commands/runtime-internalization-run-rulehost.js +61 -7
  9. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  10. package/dist/services/__tests__/rulehost-readiness.test.d.ts +2 -0
  11. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +1 -0
  12. package/dist/services/__tests__/rulehost-readiness.test.js +314 -0
  13. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -0
  14. package/dist/services/__tests__/runtime-adapter-resolver.test.js +20 -12
  15. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  16. package/dist/services/rulehost-readiness.d.ts +62 -0
  17. package/dist/services/rulehost-readiness.d.ts.map +1 -0
  18. package/dist/services/rulehost-readiness.js +214 -0
  19. package/dist/services/rulehost-readiness.js.map +1 -0
  20. package/dist/services/runtime-adapter-resolver.d.ts +1 -1
  21. package/dist/services/runtime-adapter-resolver.js +3 -3
  22. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/commands/runtime-canary.ts +11 -13
  25. package/src/commands/runtime-internalization-queue.ts +14 -19
  26. package/src/commands/runtime-internalization-run-rulehost.ts +68 -6
  27. package/src/services/__tests__/rulehost-readiness.test.ts +366 -0
  28. package/src/services/__tests__/runtime-adapter-resolver.test.ts +20 -12
  29. package/src/services/rulehost-readiness.ts +326 -0
  30. package/src/services/runtime-adapter-resolver.ts +3 -3
  31. package/tests/commands/run-rulehost-handler.test.ts +277 -0
  32. package/tests/commands/runtime-canary.test.ts +46 -36
  33. package/tests/commands/runtime-internalization-queue.test.ts +11 -24
  34. package/tests/commands/runtime-internalization-run-once.test.ts +14 -6
  35. package/tests/services/rulehost-pipeline-e2e.test.ts +71 -61
  36. package/dist/services/feature-flag-loader.d.ts +0 -6
  37. package/dist/services/feature-flag-loader.d.ts.map +0 -1
  38. package/dist/services/feature-flag-loader.js +0 -53
  39. package/dist/services/feature-flag-loader.js.map +0 -1
  40. package/src/services/feature-flag-loader.ts +0 -73
  41. package/tests/services/feature-flag-loader.test.ts +0 -207
@@ -46,22 +46,17 @@ vi.mock('@principles/core/runtime-v2', () => ({
46
46
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
47
47
  }));
48
48
 
49
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
50
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
51
- source: 'defaults',
52
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
53
- flags: {
54
- prompt: { id: 'prompt', category: 'core', enabled: true, since: '2026-05-24' },
55
- code_tool_hook: { id: 'code_tool_hook', category: 'core', enabled: true, since: '2026-05-24' },
56
- defer_archive: { id: 'defer_archive', category: 'core', enabled: true, since: '2026-05-24' },
57
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
58
- },
59
- warnings: [],
60
- }),
49
+ const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
50
+ mockLoadPdConfig: vi.fn(),
51
+ mockComputeFlagsFromLoadResult: vi.fn(),
52
+ }));
53
+
54
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
55
+ loadPdConfig: mockLoadPdConfig,
56
+ computeFlagsFromLoadResult: mockComputeFlagsFromLoadResult,
61
57
  }));
62
58
 
63
59
  import { runCanaryChecks } from '../../src/commands/runtime-canary.js';
64
- import { loadEffectiveFeatureFlags } from '../../src/services/feature-flag-loader.js';
65
60
 
66
61
  const WS = '/fake/workspace';
67
62
 
@@ -126,6 +121,26 @@ describe('runCanaryChecks', () => {
126
121
  mockAuditConsistency.mockResolvedValue({ status: 'ok', consumedCount: 0, orphanCandidateCount: 0, missingLedgerCount: 0 });
127
122
  mockBuildGfiSnapshot.mockReturnValue(healthyGfiSnapshot());
128
123
  mockClassifyGfiHealth.mockReturnValue({ status: 'healthy', reason: '0 active, 0 stale sessions', staleGfiDegradedThreshold: 40 });
124
+ // Default: GFI enabled, no warnings (canonical config loader)
125
+ mockLoadPdConfig.mockReturnValue({
126
+ ok: true,
127
+ effective: {},
128
+ source: 'defaults',
129
+ configPath: `${WS}/.pd/config.yaml`,
130
+ warnings: [],
131
+ legacyFilesDetected: [],
132
+ legacyFileNextActions: [],
133
+ });
134
+ mockComputeFlagsFromLoadResult.mockReturnValue({
135
+ flags: {
136
+ prompt: { id: 'prompt', category: 'core', enabled: true },
137
+ code_tool_hook: { id: 'code_tool_hook', category: 'core', enabled: true },
138
+ defer_archive: { id: 'defer_archive', category: 'core', enabled: true },
139
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
140
+ },
141
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
142
+ warnings: [],
143
+ });
129
144
  });
130
145
 
131
146
  it('returns healthy when all checks are healthy', async () => {
@@ -275,12 +290,11 @@ describe('runCanaryChecks', () => {
275
290
 
276
291
  describe('GFI config warning degraded status', () => {
277
292
  it('returns healthy when GFI disabled with no warnings', async () => {
278
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
279
- source: 'defaults',
280
- configPath: `${WS}/.pd/feature-flags.yaml`,
293
+ mockComputeFlagsFromLoadResult.mockReturnValue({
281
294
  flags: {
282
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
295
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
283
296
  },
297
+ enabledChannels: [],
284
298
  warnings: [],
285
299
  });
286
300
 
@@ -292,13 +306,12 @@ describe('runCanaryChecks', () => {
292
306
  });
293
307
 
294
308
  it('returns degraded when GFI disabled but config has warnings (malformed YAML)', async () => {
295
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
296
- source: 'defaults',
297
- configPath: `${WS}/.pd/feature-flags.yaml`,
309
+ mockComputeFlagsFromLoadResult.mockReturnValue({
298
310
  flags: {
299
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
311
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
300
312
  },
301
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
313
+ enabledChannels: [],
314
+ warnings: ['.pd/config.yaml: YAML parse error, using defaults'],
302
315
  });
303
316
 
304
317
  const result = await runCanaryChecks(WS);
@@ -307,16 +320,15 @@ describe('runCanaryChecks', () => {
307
320
  expect(gfiCheck?.status).toBe('degraded');
308
321
  expect(gfiCheck?.summary).toContain('warnings');
309
322
  expect(gfiCheck?.details).toBeDefined();
310
- expect(result.recommendedNextActions.some(a => a.includes('feature-flags.yaml'))).toBe(true);
323
+ expect(result.recommendedNextActions.some(a => a.includes('config.yaml'))).toBe(true);
311
324
  });
312
325
 
313
326
  it('returns degraded when GFI disabled but config has malformed override warning', async () => {
314
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
315
- source: 'workspace_file',
316
- configPath: `${WS}/.pd/feature-flags.yaml`,
327
+ mockComputeFlagsFromLoadResult.mockReturnValue({
317
328
  flags: {
318
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
329
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
319
330
  },
331
+ enabledChannels: [],
320
332
  warnings: ["flag 'gfi': malformed override kept default (enabled must be boolean)"],
321
333
  });
322
334
 
@@ -324,16 +336,15 @@ describe('runCanaryChecks', () => {
324
336
  const gfiCheck = result.checks.find(c => c.name === 'gfi_snapshot');
325
337
 
326
338
  expect(gfiCheck?.status).toBe('degraded');
327
- expect(result.recommendedNextActions.some(a => a.includes('feature-flags.yaml') || a.includes('pd runtime features'))).toBe(true);
339
+ expect(result.recommendedNextActions.some(a => a.includes('config.yaml') || a.includes('pd runtime features'))).toBe(true);
328
340
  });
329
341
 
330
342
  it('runs GFI snapshot when flag enabled with no warnings', async () => {
331
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
332
- source: 'workspace_file',
333
- configPath: `${WS}/.pd/feature-flags.yaml`,
343
+ mockComputeFlagsFromLoadResult.mockReturnValue({
334
344
  flags: {
335
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
345
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
336
346
  },
347
+ enabledChannels: [],
337
348
  warnings: [],
338
349
  });
339
350
 
@@ -346,12 +357,11 @@ describe('runCanaryChecks', () => {
346
357
  });
347
358
 
348
359
  it('recommends session lifecycle review for GFI session issues (not config)', async () => {
349
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
350
- source: 'defaults',
351
- configPath: `${WS}/.pd/feature-flags.yaml`,
360
+ mockComputeFlagsFromLoadResult.mockReturnValue({
352
361
  flags: {
353
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
362
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
354
363
  },
364
+ enabledChannels: [],
355
365
  warnings: [],
356
366
  });
357
367
  mockClassifyGfiHealth.mockReturnValue({
@@ -24,27 +24,14 @@ vi.mock('@principles/core/runtime-v2', () => ({
24
24
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
25
25
  }));
26
26
 
27
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
28
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
29
- flags: {
30
- prompt: { id: 'prompt', enabled: true, category: 'core' },
31
- code_tool_hook: { id: 'code_tool_hook', enabled: true, category: 'core' },
32
- defer_archive: { id: 'defer_archive', enabled: true, category: 'core' },
33
- },
34
- source: 'defaults',
35
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
36
- warnings: [],
37
- }),
38
- }));
39
-
40
27
  const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
41
- mockLoadPdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults' }),
28
+ mockLoadPdConfig: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults' }),
42
29
  mockComputeFlagsFromLoadResult: vi.fn().mockReturnValue({
43
30
  flags: {
44
31
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
45
32
  },
46
- source: 'defaults',
47
- errors: [],
33
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
34
+ warnings: [],
48
35
  }),
49
36
  }));
50
37
 
@@ -89,8 +76,8 @@ describe('handleRuntimeInternalizationQueue', () => {
89
76
  flags: {
90
77
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
91
78
  },
92
- source: 'defaults',
93
- errors: [],
79
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
80
+ warnings: [],
94
81
  });
95
82
  });
96
83
 
@@ -308,8 +295,8 @@ describe('handleRuntimeInternalizationQueue', () => {
308
295
  flags: {
309
296
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
310
297
  },
311
- source: 'config',
312
- errors: [],
298
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
299
+ warnings: [],
313
300
  });
314
301
 
315
302
  await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
@@ -332,8 +319,8 @@ describe('handleRuntimeInternalizationQueue', () => {
332
319
  flags: {
333
320
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
334
321
  },
335
- source: 'config',
336
- errors: [],
322
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
323
+ warnings: [],
337
324
  });
338
325
 
339
326
  await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
@@ -379,8 +366,8 @@ describe('handleRuntimeInternalizationQueue', () => {
379
366
  flags: {
380
367
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
381
368
  },
382
- source: 'config',
383
- errors: [],
369
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
370
+ warnings: [],
384
371
  });
385
372
 
386
373
  await handleRuntimeInternalizationQueue({ workspace: WS, json: false });
@@ -41,14 +41,22 @@ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
41
41
  resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
42
42
  }));
43
43
 
44
- // PRI-431: Mock feature-flag-loader so the shared resolver's L2 dreamer sub-branch
45
- // doesn't call the real loadEffectiveFeatureFlags (which needs computeEffectiveFlags
46
- // from @principles/core/runtime-v2 not included in the mock above).
47
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
48
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
44
+ // PRI-460: Mock pd-config-loader so the shared resolver's L2 dreamer sub-branch
45
+ // doesn't call the real loadPdConfig (which needs fs access).
46
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
47
+ loadPdConfig: vi.fn().mockReturnValue({
48
+ ok: true,
49
+ effective: {},
50
+ source: 'defaults',
51
+ configPath: '/fake/workspace/.pd/config.yaml',
52
+ warnings: [],
53
+ legacyFilesDetected: [],
54
+ legacyFileNextActions: [],
55
+ }),
56
+ computeFlagsFromLoadResult: vi.fn().mockReturnValue({
49
57
  flags: {},
58
+ enabledChannels: [],
50
59
  warnings: [],
51
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
52
60
  }),
53
61
  }));
54
62
 
@@ -52,6 +52,18 @@ function writeConfig(workspaceDir: string, content: object): void {
52
52
  );
53
53
  }
54
54
 
55
+ /** Type guard: narrows `unknown` to `Record<string, unknown>` without `as`. */
56
+ function isRecord(value: unknown): value is Record<string, unknown> {
57
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
58
+ }
59
+
60
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
61
+ if (!isRecord(value)) {
62
+ throw new Error(`${label} is not a record`);
63
+ }
64
+ return value;
65
+ }
66
+
55
67
  /** Config with both artificer+evaluator enabled, pi-ai profile, API key env set. */
56
68
  function makeCapabilityOnConfig(workspaceDir: string): object {
57
69
  return {
@@ -90,17 +102,19 @@ function makeCapabilityOnConfig(workspaceDir: string): object {
90
102
 
91
103
  /** Config with artificer disabled (capability must be OFF). */
92
104
  function makeArtificerDisabledConfig(workspaceDir: string): object {
93
- const cfg = makeCapabilityOnConfig(workspaceDir) as Record<string, unknown>;
94
- const internalAgents = cfg.internalAgents as { agents: Record<string, { enabled: boolean; runtimeProfile?: string }> };
95
- internalAgents.agents.artificer = { enabled: false };
105
+ const cfg = makeCapabilityOnConfig(workspaceDir);
106
+ const internalAgents = requireRecord(Reflect.get(cfg, 'internalAgents'), 'internalAgents');
107
+ const agents = requireRecord(Reflect.get(internalAgents, 'agents'), 'internalAgents.agents');
108
+ Reflect.set(agents, 'artificer', { enabled: false });
96
109
  return cfg;
97
110
  }
98
111
 
99
112
  /** Config with evaluator disabled (capability must be OFF). */
100
113
  function makeEvaluatorDisabledConfig(workspaceDir: string): object {
101
- const cfg = makeCapabilityOnConfig(workspaceDir) as Record<string, unknown>;
102
- const internalAgents = cfg.internalAgents as { agents: Record<string, { enabled: boolean; runtimeProfile?: string }> };
103
- internalAgents.agents.evaluator = { enabled: false };
114
+ const cfg = makeCapabilityOnConfig(workspaceDir);
115
+ const internalAgents = requireRecord(Reflect.get(cfg, 'internalAgents'), 'internalAgents');
116
+ const agents = requireRecord(Reflect.get(internalAgents, 'agents'), 'internalAgents.agents');
117
+ Reflect.set(agents, 'evaluator', { enabled: false });
104
118
  return cfg;
105
119
  }
106
120
 
@@ -158,10 +172,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
158
172
  }
159
173
 
160
174
  /** Extract the single JSON object written to stdout. Fails if not exactly one write. */
161
- function parseJsonOutput(): unknown {
175
+ function parseJsonOutput(): Record<string, unknown> {
162
176
  expect(stdoutSpy).toHaveBeenCalledTimes(1);
163
177
  const raw = stdoutSpy.mock.calls[0][0] as string;
164
- return JSON.parse(raw);
178
+ const parsed: unknown = JSON.parse(raw);
179
+ return requireRecord(parsed, 'stdout JSON');
165
180
  }
166
181
 
167
182
  // ── Capability ON: both agents enabled, API key set ──────────────────────
@@ -178,14 +193,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
178
193
  });
179
194
 
180
195
  const output = parseJsonOutput();
181
- expect(typeof output).toBe('object');
182
- expect(output).not.toBeNull();
183
- const obj = output as { status: string; codeRuleCapability?: { enabled: boolean; disabledReason?: string }; capabilityStatus?: string };
184
- expect(obj.status).toBe('dry_run');
185
- expect(obj.codeRuleCapability).toBeDefined();
186
- expect(obj.codeRuleCapability?.enabled).toBe(true);
187
- expect(obj.codeRuleCapability?.disabledReason).toBeUndefined();
188
- expect(obj.capabilityStatus).toContain('ON');
196
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
197
+ expect(output.status).toBe('dry_run');
198
+ expect(codeRuleCapability.enabled).toBe(true);
199
+ expect(codeRuleCapability.disabledReason).toBeUndefined();
200
+ expect(String(output.capabilityStatus)).toContain('ON');
189
201
  // exitCode must NOT be set for dry_run success
190
202
  expect(process.exitCode).toBeUndefined();
191
203
  });
@@ -203,16 +215,13 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
203
215
  await handleRunRuleHost({ workspace, painId: 'pain-flag-default', dryRun: true, json: true });
204
216
 
205
217
  const output = parseJsonOutput();
206
- // PRI-435 (CodeRabbit P3): narrow unknown before property access
207
- expect(typeof output).toBe('object');
208
- expect(output).not.toBeNull();
209
- const obj = output as { status: string; codeRuleCapability?: { enabled: boolean; disabledReason?: string }; capabilityStatus?: string };
210
- expect(obj.status).toBe('dry_run');
218
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
219
+ expect(output.status).toBe('dry_run');
211
220
  // PRI-435: code_rule_capability is now a core flag — defaults ON even when omitted from config.
212
221
  // It cannot be disabled by config. The capability is ON when artificer+evaluator are configured.
213
- expect(obj.codeRuleCapability).toEqual(expect.objectContaining({ enabled: true }));
214
- expect(obj.codeRuleCapability?.disabledReason).toBeUndefined();
215
- expect(String(obj.capabilityStatus)).toContain('ON');
222
+ expect(codeRuleCapability).toEqual(expect.objectContaining({ enabled: true }));
223
+ expect(codeRuleCapability.disabledReason).toBeUndefined();
224
+ expect(String(output.capabilityStatus)).toContain('ON');
216
225
  });
217
226
 
218
227
  it('PRI-435: explicit emergency disable via code_rule_capability.enabled=false is observable', async () => {
@@ -229,16 +238,13 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
229
238
  await handleRunRuleHost({ workspace, painId: 'pain-emergency-disable', dryRun: true, json: true });
230
239
 
231
240
  const output = parseJsonOutput();
232
- // PRI-435 (CodeRabbit P3): narrow unknown before property access
233
- expect(typeof output).toBe('object');
234
- expect(output).not.toBeNull();
235
- const obj = output as { status: string; codeRuleCapability?: { enabled: boolean; disabledReason?: string }; capabilityStatus?: string };
236
- expect(obj.status).toBe('dry_run');
241
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
242
+ expect(output.status).toBe('dry_run');
237
243
  // PRI-435: Emergency disable via code_rule_capability.enabled=false is preserved.
238
244
  // The capability is OFF with a structured reason.
239
- expect(obj.codeRuleCapability).toEqual(expect.objectContaining({ enabled: false }));
240
- expect(String(obj.codeRuleCapability?.disabledReason)).toContain('feature flag');
241
- expect(String(obj.capabilityStatus)).toContain('OFF');
245
+ expect(codeRuleCapability).toEqual(expect.objectContaining({ enabled: false }));
246
+ expect(String(codeRuleCapability.disabledReason)).toContain('feature flag');
247
+ expect(String(output.capabilityStatus)).toContain('OFF');
242
248
  });
243
249
 
244
250
  it('reports the resolved runtime profile for every executed agent', async () => {
@@ -271,10 +277,17 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
271
277
 
272
278
  await handleRunRuleHost({ workspace, painId: 'pain-disabled-philosopher', confirm: true, json: true });
273
279
 
280
+ // PRI-461: readiness gate catches disabled required agents BEFORE adapter
281
+ // construction, returning status='refused' with a structured reason instead
282
+ // of the old opaque 'agent_runtime_resolution_failed' error.
274
283
  const output = parseJsonOutput();
275
- expect(output.status).toBe('failed');
276
- expect(output.reason).toBe('agent_runtime_resolution_failed');
277
- expect(String(output.message)).toContain('philosopher');
284
+ expect(isRecord(output)).toBe(true);
285
+ if (!isRecord(output)) {
286
+ throw new Error('output is not a record');
287
+ }
288
+ expect(output.status).toBe('refused');
289
+ expect(String(output.reason)).toContain('philosopher');
290
+ expect(typeof output.nextAction).toBe('string');
278
291
  expect(process.exitCode).toBe(1);
279
292
  expect(fs.existsSync(path.join(workspace, '.state', 'runtime-v2.sqlite'))).toBe(false);
280
293
  });
@@ -308,10 +321,10 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
308
321
  });
309
322
 
310
323
  const output = parseJsonOutput();
311
- const obj = output as { status: string; codeRuleCapability: { enabled: boolean; disabledReason?: string } };
312
- expect(obj.status).toBe('dry_run');
313
- expect(obj.codeRuleCapability.enabled).toBe(false);
314
- expect(obj.codeRuleCapability.disabledReason).toContain('artificer');
324
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
325
+ expect(output.status).toBe('dry_run');
326
+ expect(codeRuleCapability.enabled).toBe(false);
327
+ expect(String(codeRuleCapability.disabledReason)).toContain('artificer');
315
328
  });
316
329
 
317
330
  // ── Capability OFF: evaluator disabled ───────────────────────────────────
@@ -328,10 +341,10 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
328
341
  });
329
342
 
330
343
  const output = parseJsonOutput();
331
- const obj = output as { status: string; codeRuleCapability: { enabled: boolean; disabledReason?: string } };
332
- expect(obj.status).toBe('dry_run');
333
- expect(obj.codeRuleCapability.enabled).toBe(false);
334
- expect(obj.codeRuleCapability.disabledReason).toContain('evaluator');
344
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
345
+ expect(output.status).toBe('dry_run');
346
+ expect(codeRuleCapability.enabled).toBe(false);
347
+ expect(String(codeRuleCapability.disabledReason)).toContain('evaluator');
335
348
  });
336
349
 
337
350
  // ── Capability OFF: API key not set ──────────────────────────────────────
@@ -393,10 +406,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
393
406
  });
394
407
 
395
408
  const output = parseJsonOutput();
396
- const obj = output as { status: string; codeRuleCapability: { enabled: boolean; disabledReason?: string } };
397
- expect(obj.status).toBe('dry_run');
398
- expect(obj.codeRuleCapability.enabled).toBe(false);
399
- expect(obj.codeRuleCapability.disabledReason).toContain('apiKeyEnv');
409
+ const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
410
+ expect(output.status).toBe('dry_run');
411
+ expect(codeRuleCapability.enabled).toBe(false);
412
+ expect(String(codeRuleCapability.disabledReason)).toContain('TEST_RULEHOST_ARTIFICER_KEY');
413
+ expect(String(codeRuleCapability.disabledReason)).toContain('not set');
400
414
  });
401
415
 
402
416
  // ── CLI gate: mutual exclusivity ─────────────────────────────────────────
@@ -414,10 +428,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
414
428
  });
415
429
 
416
430
  const output = parseJsonOutput();
417
- const obj = output as { status: string; reason: string; nextAction: string };
418
- expect(obj.status).toBe('failed');
419
- expect(obj.reason).toContain('mutually exclusive');
420
- expect(obj.nextAction).toBeTruthy();
431
+ expect(output.status).toBe('failed');
432
+ expect(String(output.reason)).toContain('mutually exclusive');
433
+ expect(output.nextAction).toBeTruthy();
421
434
  expect(process.exitCode).toBe(1);
422
435
  });
423
436
 
@@ -435,10 +448,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
435
448
  });
436
449
 
437
450
  const output = parseJsonOutput();
438
- const obj = output as { status: string; reason: string; nextAction: string };
439
- expect(obj.status).toBe('failed');
440
- expect(obj.reason).toContain('painId');
441
- expect(obj.nextAction).toBeTruthy();
451
+ expect(output.status).toBe('failed');
452
+ expect(String(output.reason)).toContain('painId');
453
+ expect(output.nextAction).toBeTruthy();
442
454
  expect(process.exitCode).toBe(1);
443
455
  });
444
456
 
@@ -480,8 +492,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
480
492
  });
481
493
 
482
494
  const output = parseJsonOutput();
483
- const obj = output as { status: string };
484
- expect(obj.status).toBe('dry_run');
495
+ expect(output.status).toBe('dry_run');
485
496
  // Must NOT set exitCode for dry_run
486
497
  expect(process.exitCode).toBeUndefined();
487
498
  });
@@ -501,10 +512,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
501
512
  });
502
513
 
503
514
  const output = parseJsonOutput();
504
- const obj = output as { status: string; reason: string; nextAction: string };
505
- expect(obj.status).toBe('failed');
506
- expect(obj.reason).toContain('unsupported channel');
507
- expect(obj.nextAction).toBeTruthy();
515
+ expect(output.status).toBe('failed');
516
+ expect(String(output.reason)).toContain('unsupported channel');
517
+ expect(output.nextAction).toBeTruthy();
508
518
  expect(process.exitCode).toBe(1);
509
519
  });
510
520
  });
@@ -1,6 +0,0 @@
1
- import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
2
- export declare const FEATURE_FLAGS_CONFIG_FILENAME = "feature-flags.yaml";
3
- export declare const FEATURE_FLAGS_CONFIG_DIR = ".pd";
4
- export declare function getFeatureFlagsConfigPath(workspaceDir: string): string;
5
- export declare function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags;
6
- //# sourceMappingURL=feature-flag-loader.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"feature-flag-loader.d.ts","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEzE,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAClE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAQ9C,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,CAkDrF"}
@@ -1,53 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, } from '@principles/core/runtime-v2';
5
- export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
6
- export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
7
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
8
- function isRecord(value) {
9
- return value !== null && typeof value === 'object' && !Array.isArray(value);
10
- }
11
- export function getFeatureFlagsConfigPath(workspaceDir) {
12
- return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
13
- }
14
- export function loadEffectiveFeatureFlags(workspaceDir) {
15
- const configPath = getFeatureFlagsConfigPath(workspaceDir);
16
- if (!fs.existsSync(configPath)) {
17
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
18
- }
19
- const raw = fs.readFileSync(configPath, 'utf8');
20
- let parsed;
21
- try {
22
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
23
- }
24
- catch {
25
- return {
26
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
27
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
28
- };
29
- }
30
- if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
31
- return {
32
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
33
- warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
34
- };
35
- }
36
- const parsedRecord = Object.create(null);
37
- const warnings = [];
38
- for (const key of Object.keys(parsed)) {
39
- if (DANGEROUS_KEYS.has(key)) {
40
- warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
41
- continue;
42
- }
43
- if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
44
- parsedRecord[key] = parsed[key];
45
- }
46
- }
47
- const result = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
48
- if (warnings.length > 0) {
49
- result.warnings = [...warnings, ...result.warnings];
50
- }
51
- return result;
52
- }
53
- //# sourceMappingURL=feature-flag-loader.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"feature-flag-loader.js","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"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"}
@@ -1,73 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import {
5
- computeEffectiveFlags,
6
- DEFAULT_FEATURE_FLAGS,
7
- } from '@principles/core/runtime-v2';
8
- import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
9
-
10
- export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
11
- export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
12
-
13
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
14
-
15
- function isRecord(value: unknown): value is Record<string, unknown> {
16
- return value !== null && typeof value === 'object' && !Array.isArray(value);
17
- }
18
-
19
- export function getFeatureFlagsConfigPath(workspaceDir: string): string {
20
- return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
21
- }
22
-
23
- export function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags {
24
- const configPath = getFeatureFlagsConfigPath(workspaceDir);
25
-
26
- if (!fs.existsSync(configPath)) {
27
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
28
- }
29
-
30
- const raw = fs.readFileSync(configPath, 'utf8');
31
-
32
-
33
- let parsed: unknown;
34
- try {
35
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
36
- } catch {
37
- return {
38
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
39
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
40
- };
41
- }
42
-
43
- if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
44
- return {
45
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
46
- warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
47
- };
48
- }
49
-
50
- const parsedRecord: Record<string, unknown> = Object.create(null);
51
- const warnings: string[] = [];
52
- for (const key of Object.keys(parsed)) {
53
- if (DANGEROUS_KEYS.has(key)) {
54
- warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
55
- continue;
56
- }
57
- if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
58
- parsedRecord[key] = parsed[key];
59
- }
60
- }
61
-
62
- const result = computeEffectiveFlags(
63
- parsedRecord,
64
- DEFAULT_FEATURE_FLAGS,
65
- configPath,
66
- );
67
-
68
- if (warnings.length > 0) {
69
- result.warnings = [...warnings, ...result.warnings];
70
- }
71
-
72
- return result;
73
- }