@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.
- package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +40 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +40 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +19 -0
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import type { Command } from 'commander';
|
|
2
3
|
import {
|
|
3
4
|
RuntimeStateManager,
|
|
4
5
|
ActivationDispatcher,
|
|
@@ -9,11 +10,30 @@ import {
|
|
|
9
10
|
SqliteActivationStateStore,
|
|
10
11
|
SqliteApprovalQueueStore,
|
|
11
12
|
SqlitePIArtifactStore,
|
|
13
|
+
ApprovalQueue,
|
|
14
|
+
ApprovalCompletionService,
|
|
12
15
|
isArtifactRevisionOf,
|
|
16
|
+
extractEvidenceRefs,
|
|
17
|
+
extractPrincipleId,
|
|
13
18
|
} from '@principles/core/runtime-v2';
|
|
14
|
-
import type {
|
|
15
|
-
|
|
19
|
+
import type {
|
|
20
|
+
ActivationDecision,
|
|
21
|
+
PIArtifactSnapshot,
|
|
22
|
+
RolloutActivationDecision,
|
|
23
|
+
ApprovalDecisionResult,
|
|
24
|
+
ApprovalCompletionResult,
|
|
25
|
+
} from '@principles/core/runtime-v2';
|
|
26
|
+
import type { PIArtifactRecord, ActivationStatusRecord } from '@principles/core/runtime-v2';
|
|
16
27
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
28
|
+
import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard for parsed JSON objects (rc-2-no-as-bypass).
|
|
32
|
+
* Replaces `as Record<string, unknown>` casts on untrusted contentJson.
|
|
33
|
+
*/
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
interface ActivationDispatchOptions {
|
|
19
39
|
workspace?: string;
|
|
@@ -150,6 +170,7 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
|
|
|
150
170
|
|
|
151
171
|
const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
|
|
152
172
|
const approvalQueueStore = new SqliteApprovalQueueStore(stateManager.connection);
|
|
173
|
+
const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
|
|
153
174
|
// Wire all three MVP-Core writers, including RuleHostWriter for code_tool_hook.
|
|
154
175
|
// PRI-408: fixes P0 breakpoint where code_tool_hook channel could not activate.
|
|
155
176
|
const dispatcher = new ActivationDispatcher(
|
|
@@ -158,7 +179,10 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
|
|
|
158
179
|
{
|
|
159
180
|
writers: [
|
|
160
181
|
new PromptWriter(),
|
|
161
|
-
new RuleHostWriter({
|
|
182
|
+
new RuleHostWriter({
|
|
183
|
+
gateDeps: createProductionGateDeps(),
|
|
184
|
+
featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
|
|
185
|
+
}),
|
|
162
186
|
new DeferArchiveWriter(),
|
|
163
187
|
],
|
|
164
188
|
approvalQueueStore,
|
|
@@ -210,7 +234,7 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
|
|
|
210
234
|
ok: false,
|
|
211
235
|
activationId: '',
|
|
212
236
|
reason: 'activation_id_required',
|
|
213
|
-
nextAction: 'Provide --activation-id <id> from `pd
|
|
237
|
+
nextAction: 'Provide --activation-id <id> from `pd activation list`',
|
|
214
238
|
};
|
|
215
239
|
if (opts.json) {
|
|
216
240
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -240,7 +264,7 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
|
|
|
240
264
|
ok: false,
|
|
241
265
|
activationId: opts.activationId,
|
|
242
266
|
reason: 'not_found_or_already_deactivated',
|
|
243
|
-
nextAction: 'Check activation ID with `pd
|
|
267
|
+
nextAction: 'Check activation ID with `pd activation list`, or it may already be deactivated',
|
|
244
268
|
};
|
|
245
269
|
|
|
246
270
|
if (opts.json) {
|
|
@@ -281,6 +305,114 @@ export async function handleRuntimeActivationDeactivate(opts: ActivationDeactiva
|
|
|
281
305
|
}
|
|
282
306
|
}
|
|
283
307
|
|
|
308
|
+
export interface ActivationPromoteOptions {
|
|
309
|
+
workspace?: string;
|
|
310
|
+
activationId?: string;
|
|
311
|
+
dryRun?: boolean;
|
|
312
|
+
confirm?: boolean;
|
|
313
|
+
json?: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface ActivationPromoteResult {
|
|
317
|
+
ok: boolean;
|
|
318
|
+
decision: 'would_promote' | 'promoted' | 'refused';
|
|
319
|
+
activationId: string;
|
|
320
|
+
promotedAt?: string;
|
|
321
|
+
reason?: string;
|
|
322
|
+
nextAction?: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function handleRuntimeActivationPromote(opts: ActivationPromoteOptions): Promise<void> {
|
|
326
|
+
const activationId = opts.activationId?.trim() ?? '';
|
|
327
|
+
const refuse = (reason: string, nextAction: string): void => {
|
|
328
|
+
const result: ActivationPromoteResult = { ok: false, decision: 'refused', activationId, reason, nextAction };
|
|
329
|
+
if (opts.json) console.log(JSON.stringify(result));
|
|
330
|
+
else {
|
|
331
|
+
console.error(`Promotion refused: ${reason}`);
|
|
332
|
+
console.error(`Next action: ${nextAction}`);
|
|
333
|
+
}
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (!activationId) {
|
|
338
|
+
refuse('activation_id_required', 'Provide --activation-id from `pd activation list --channel code_tool_hook`.');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (opts.dryRun === true && opts.confirm === true) {
|
|
342
|
+
refuse('dry_run_confirm_mutually_exclusive', 'Choose either --dry-run or --confirm.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let stateManager: RuntimeStateManager | undefined;
|
|
347
|
+
try {
|
|
348
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
349
|
+
stateManager = new RuntimeStateManager({ workspaceDir });
|
|
350
|
+
await stateManager.initialize();
|
|
351
|
+
const store = new SqliteActivationStateStore(stateManager.connection);
|
|
352
|
+
const activeHooks = await store.listCodeToolHookActivations(false);
|
|
353
|
+
// CodeRabbit PR2 Comment 3: dry-run must apply the same eligibility checks
|
|
354
|
+
// as the real promote path. `promoteActivation` runs a COUNT guard inside a
|
|
355
|
+
// BEGIN IMMEDIATE transaction and refuses when the count of matching shadow
|
|
356
|
+
// rows is not exactly 1. The previous dry-run branch used `find()` (returns
|
|
357
|
+
// the first match) and reported `would_promote` even when duplicates would
|
|
358
|
+
// make the confirm path throw. Mirror the store's uniqueness check here so
|
|
359
|
+
// dry-run and confirm agree (cli-5: failure paths must not mutate state;
|
|
360
|
+
// cli-6: degraded/refused results carry a structured reason + nextAction).
|
|
361
|
+
const matchingShadows = activeHooks.filter(
|
|
362
|
+
(record) => record.activationId === activationId && record.action === 'code_tool_hook_shadow_activate',
|
|
363
|
+
);
|
|
364
|
+
if (matchingShadows.length === 0) {
|
|
365
|
+
refuse(
|
|
366
|
+
'not_found_inactive_or_not_shadow',
|
|
367
|
+
'Refresh `pd activation list --channel code_tool_hook`; only active shadow activations can be promoted.',
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (matchingShadows.length > 1) {
|
|
372
|
+
refuse(
|
|
373
|
+
'duplicate_shadow_activations',
|
|
374
|
+
`${matchingShadows.length} shadow activations share activation_id=${activationId}; resolve duplicates before promoting.`,
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (opts.confirm !== true) {
|
|
380
|
+
const result: ActivationPromoteResult = {
|
|
381
|
+
ok: true,
|
|
382
|
+
decision: 'would_promote',
|
|
383
|
+
activationId,
|
|
384
|
+
nextAction: `Run pd activation promote --activation-id ${activationId} --confirm to enable live blocking.`,
|
|
385
|
+
};
|
|
386
|
+
if (opts.json) console.log(JSON.stringify(result));
|
|
387
|
+
else {
|
|
388
|
+
console.log(`Would promote: ${activationId}`);
|
|
389
|
+
console.log(` nextAction: ${result.nextAction}`);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const promotedAt = new Date().toISOString();
|
|
395
|
+
const promoted = await store.promoteActivation(activationId, promotedAt);
|
|
396
|
+
if (!promoted) {
|
|
397
|
+
refuse('promotion_precondition_changed', 'Refresh the activation list; the activation changed before promotion.');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const result: ActivationPromoteResult = { ok: true, decision: 'promoted', activationId, promotedAt };
|
|
401
|
+
if (opts.json) console.log(JSON.stringify(result));
|
|
402
|
+
else {
|
|
403
|
+
console.log(`Promoted live: ${activationId}`);
|
|
404
|
+
console.log(` promotedAt: ${promotedAt}`);
|
|
405
|
+
}
|
|
406
|
+
} catch (err: unknown) {
|
|
407
|
+
refuse(
|
|
408
|
+
`promotion_failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
409
|
+
'Check workspace configuration and database integrity; no promotion was applied.',
|
|
410
|
+
);
|
|
411
|
+
} finally {
|
|
412
|
+
await stateManager?.close();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
284
416
|
// ── List Activations Command (PRI-408 Contract D — observability) ────────────
|
|
285
417
|
|
|
286
418
|
interface ActivationListOptions {
|
|
@@ -335,23 +467,180 @@ export async function handleRuntimeActivationList(opts: ActivationListOptions):
|
|
|
335
467
|
? records
|
|
336
468
|
: records.filter(r => r.deactivatedAt === null);
|
|
337
469
|
|
|
470
|
+
// F9-1: Validate artifact_id → pi_artifacts.artifact_id reference for each
|
|
471
|
+
// activation. A dangling reference means the activation cannot function
|
|
472
|
+
// (RuleHost cannot load implementationCode, dispatcher cannot verify
|
|
473
|
+
// lineage). Emit degraded status with reason + nextAction instead of
|
|
474
|
+
// silently returning corrupted records (rc-9-no-silent-fallback,
|
|
475
|
+
// rc-6-lineage-consistency; related ERR: ERR-002).
|
|
476
|
+
const {piArtifactStore} = stateManager;
|
|
477
|
+
const uniqueArtifactIds = new Set(filtered.map(r => r.artifactId));
|
|
478
|
+
const danglingArtifactIds = new Set<string>();
|
|
479
|
+
// PRI-491: Map artifactId to { contextVersion, evidenceRefs } extracted
|
|
480
|
+
// from contentJson. rc-1/rc-2: contentJson is parsed as unknown and
|
|
481
|
+
// type-narrowed; never `as`-cast without prior typeof check.
|
|
482
|
+
const artifactMetadata = new Map<string, { contextVersion: 'v1' | 'v2'; evidenceRefs: string[] | null; principleId: string | null }>();
|
|
483
|
+
for (const artifactId of uniqueArtifactIds) {
|
|
484
|
+
try {
|
|
485
|
+
const artifact = await piArtifactStore.getArtifactById(artifactId);
|
|
486
|
+
if (!artifact) {
|
|
487
|
+
danglingArtifactIds.add(artifactId);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
// PRI-491: Parse contentJson to extract contextVersion + evidenceRefs.
|
|
491
|
+
// rc-1: treat parsed JSON as unknown; rc-2: narrow with typeof before use.
|
|
492
|
+
let parsedContent: Record<string, unknown> | null = null;
|
|
493
|
+
try {
|
|
494
|
+
const parsed: unknown = JSON.parse(artifact.contentJson);
|
|
495
|
+
if (isRecord(parsed)) {
|
|
496
|
+
parsedContent = parsed;
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
// Malformed contentJson - treat as no metadata (not dangling, just unreadable)
|
|
500
|
+
}
|
|
501
|
+
const requiresCtxV2 = parsedContent !== null
|
|
502
|
+
&& Object.hasOwn(parsedContent, 'requiresContextVersion')
|
|
503
|
+
&& parsedContent.requiresContextVersion === 2;
|
|
504
|
+
const contextVersion: 'v1' | 'v2' = requiresCtxV2 ? 'v2' : 'v1';
|
|
505
|
+
const evidenceRefs = parsedContent !== null ? extractEvidenceRefs(parsedContent) : null;
|
|
506
|
+
// PRI-500: extract principleId to match Console's ActivationRecord field.
|
|
507
|
+
// Uses extractPrincipleId (4-step fallback) so dreamer artifacts whose
|
|
508
|
+
// sourcePrincipleId was stripped still resolve via contentJson.
|
|
509
|
+
const principleId = extractPrincipleId(toSnapshot(artifact));
|
|
510
|
+
artifactMetadata.set(artifactId, { contextVersion, evidenceRefs, principleId });
|
|
511
|
+
} catch {
|
|
512
|
+
// Treat lookup failure as dangling - fail loud rather than silent (rc-9).
|
|
513
|
+
danglingArtifactIds.add(artifactId);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const hasDangling = danglingArtifactIds.size > 0;
|
|
517
|
+
|
|
518
|
+
// PRI-491: Probe rulecode_context_v2 flag to determine suspended_by_flag
|
|
519
|
+
// status for v2 activations. When the flag is off, v2 activations are
|
|
520
|
+
// suspended (not executing) even though they remain active in the DB.
|
|
521
|
+
const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
|
|
522
|
+
const v2FlagEnabled = featureFlags.flags.rulecode_context_v2?.enabled === true;
|
|
523
|
+
|
|
524
|
+
// PRI-491: Enriched activation record with owner-observable fields.
|
|
525
|
+
// mode/status/contextVersion/evidenceRefs/nextAction let the owner
|
|
526
|
+
// understand rule state without reading SQLite or logs.
|
|
527
|
+
interface AnnotatedActivation extends ActivationStatusRecord {
|
|
528
|
+
principleId?: string;
|
|
529
|
+
mode?: 'shadow' | 'live';
|
|
530
|
+
status: 'active' | 'deactivated' | 'suspended_by_flag';
|
|
531
|
+
contextVersion?: 'v1' | 'v2';
|
|
532
|
+
evidenceRefs?: string[];
|
|
533
|
+
evidenceSummary?: string;
|
|
534
|
+
nextAction?: string;
|
|
535
|
+
warning?: string;
|
|
536
|
+
}
|
|
537
|
+
const annotated: AnnotatedActivation[] = filtered.map(r => {
|
|
538
|
+
// Derive mode from action (shadow_activate -> shadow, live_activate -> live)
|
|
539
|
+
const mode: 'shadow' | 'live' | undefined = r.action === 'code_tool_hook_shadow_activate'
|
|
540
|
+
? 'shadow'
|
|
541
|
+
: r.action === 'code_tool_hook_live_activate'
|
|
542
|
+
? 'live'
|
|
543
|
+
: undefined;
|
|
544
|
+
|
|
545
|
+
// Look up artifact metadata (contextVersion, evidenceRefs)
|
|
546
|
+
const meta = artifactMetadata.get(r.artifactId);
|
|
547
|
+
const contextVersion = meta?.contextVersion;
|
|
548
|
+
const evidenceRefs = meta?.evidenceRefs ?? undefined;
|
|
549
|
+
const evidenceSummary = evidenceRefs && evidenceRefs.length > 0
|
|
550
|
+
? `${evidenceRefs.length} evidence ref(s): ${evidenceRefs.slice(0, 3).join(', ')}${evidenceRefs.length > 3 ? '...' : ''}`
|
|
551
|
+
: undefined;
|
|
552
|
+
|
|
553
|
+
// Derive status: deactivated > suspended_by_flag > active
|
|
554
|
+
let status: 'active' | 'deactivated' | 'suspended_by_flag';
|
|
555
|
+
let nextAction: string | undefined;
|
|
556
|
+
if (r.deactivatedAt) {
|
|
557
|
+
status = 'deactivated';
|
|
558
|
+
nextAction = undefined;
|
|
559
|
+
} else if (contextVersion === 'v2' && !v2FlagEnabled) {
|
|
560
|
+
status = 'suspended_by_flag';
|
|
561
|
+
nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${r.activationId} --confirm`;
|
|
562
|
+
} else {
|
|
563
|
+
status = 'active';
|
|
564
|
+
if (mode === 'shadow') {
|
|
565
|
+
nextAction = `pd activation promote --activation-id ${r.activationId} --confirm`;
|
|
566
|
+
} else if (mode === 'live') {
|
|
567
|
+
nextAction = `pd activation deactivate --activation-id ${r.activationId} --confirm`;
|
|
568
|
+
} else {
|
|
569
|
+
nextAction = undefined;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const record: AnnotatedActivation = {
|
|
574
|
+
...r,
|
|
575
|
+
principleId: meta?.principleId ?? 'unlinked',
|
|
576
|
+
mode,
|
|
577
|
+
status,
|
|
578
|
+
contextVersion,
|
|
579
|
+
evidenceRefs,
|
|
580
|
+
evidenceSummary,
|
|
581
|
+
nextAction,
|
|
582
|
+
};
|
|
583
|
+
if (danglingArtifactIds.has(r.artifactId)) {
|
|
584
|
+
record.warning = `artifact_id "${r.artifactId}" does not exist in pi_artifacts - activation is orphaned`;
|
|
585
|
+
}
|
|
586
|
+
return record;
|
|
587
|
+
});
|
|
588
|
+
|
|
338
589
|
if (opts.json) {
|
|
339
|
-
// Strict JSON mode: exactly one parseable JSON object on stdout
|
|
340
|
-
|
|
590
|
+
// Strict JSON mode: exactly one parseable JSON object on stdout.
|
|
591
|
+
// When dangling references are detected, emit degraded status with
|
|
592
|
+
// reason + nextAction so operators can act on the corruption (rc-9,
|
|
593
|
+
// cli-6-output-next-action).
|
|
594
|
+
const payload: Record<string, unknown> = { activations: annotated };
|
|
595
|
+
if (hasDangling) {
|
|
596
|
+
payload.status = 'degraded';
|
|
597
|
+
payload.reason = `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`;
|
|
598
|
+
payload.nextAction = 'Run `pd runtime internalization integrity` for full chain diagnostics. Consider deactivating orphaned activations via `pd runtime activation deactivate` or restoring the missing artifacts.';
|
|
599
|
+
} else {
|
|
600
|
+
payload.status = 'ok';
|
|
601
|
+
}
|
|
602
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
341
603
|
} else {
|
|
342
|
-
if (
|
|
604
|
+
if (annotated.length === 0) {
|
|
343
605
|
console.log('No active activations found.');
|
|
344
606
|
} else {
|
|
345
|
-
for (const r of
|
|
346
|
-
|
|
347
|
-
|
|
607
|
+
for (const r of annotated) {
|
|
608
|
+
// PRI-491: Text output shows mode/status/contextVersion so the owner
|
|
609
|
+
// can tell at a glance whether a rule will block now.
|
|
610
|
+
const statusLabel = r.deactivatedAt
|
|
611
|
+
? `[DEACTIVATED ${r.deactivatedAt}]`
|
|
612
|
+
: r.status === 'suspended_by_flag'
|
|
613
|
+
? `[SUSPENDED by flag]`
|
|
614
|
+
: `[ACTIVE]`;
|
|
615
|
+
const modeLabel = r.mode ? ` (${r.mode})` : '';
|
|
616
|
+
console.log(`${statusLabel}${modeLabel} ${r.activationId}`);
|
|
617
|
+
console.log(` principleId: ${r.principleId ?? 'unlinked'}`);
|
|
348
618
|
console.log(` artifactId: ${r.artifactId}`);
|
|
349
619
|
console.log(` channel: ${r.channel}`);
|
|
350
620
|
console.log(` action: ${r.action}`);
|
|
351
621
|
console.log(` targetRef: ${r.targetRef}`);
|
|
352
622
|
console.log(` activatedAt: ${r.activatedAt}`);
|
|
623
|
+
if (r.promotedAt) {
|
|
624
|
+
console.log(` promotedAt: ${r.promotedAt}`);
|
|
625
|
+
}
|
|
626
|
+
if (r.contextVersion) {
|
|
627
|
+
console.log(` contextVersion: ${r.contextVersion}`);
|
|
628
|
+
}
|
|
629
|
+
if (r.evidenceSummary) {
|
|
630
|
+
console.log(` evidence: ${r.evidenceSummary}`);
|
|
631
|
+
}
|
|
632
|
+
if (r.nextAction) {
|
|
633
|
+
console.log(` nextAction: ${r.nextAction}`);
|
|
634
|
+
}
|
|
635
|
+
if (r.warning) {
|
|
636
|
+
console.log(` WARNING: ${r.warning}`);
|
|
637
|
+
}
|
|
353
638
|
console.log('');
|
|
354
639
|
}
|
|
640
|
+
if (hasDangling) {
|
|
641
|
+
console.error(`Warning: ${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s).`);
|
|
642
|
+
console.error(`Next action: Run \`pd runtime internalization integrity\` for full chain diagnostics. Consider deactivating orphaned activations or restoring missing artifacts.`);
|
|
643
|
+
}
|
|
355
644
|
}
|
|
356
645
|
}
|
|
357
646
|
} catch (err: unknown) {
|
|
@@ -610,7 +899,7 @@ export async function handleRuntimeActivationEdit(opts: ActivationEditOptions):
|
|
|
610
899
|
console.log(` previousArtifactId: ${result.previousArtifactId}`);
|
|
611
900
|
}
|
|
612
901
|
console.log(` editedAt: ${result.editedAt}`);
|
|
613
|
-
console.log('Next action: review the new artifact, then approve
|
|
902
|
+
console.log('Next action: review the new artifact, then run `pd activation approve --approval-id <id>` to approve and dispatch');
|
|
614
903
|
}
|
|
615
904
|
} catch (err: unknown) {
|
|
616
905
|
// P2 #5: initialize/DB exceptions must not break --json contract.
|
|
@@ -632,3 +921,359 @@ export async function handleRuntimeActivationEdit(opts: ActivationEditOptions):
|
|
|
632
921
|
await stateManager.close();
|
|
633
922
|
}
|
|
634
923
|
}
|
|
924
|
+
|
|
925
|
+
// ── Approve Command (Bug-M fix: CLI closed loop) ──────────────────────────────
|
|
926
|
+
//
|
|
927
|
+
// Bug-M root cause: `pd activation` had dispatch/list/edit/deactivate but no
|
|
928
|
+
// `approve`. Owners who completed `dispatch → edit` via CLI had to switch to
|
|
929
|
+
// Console to approve. This handler closes the CLI loop by reusing the same
|
|
930
|
+
// ApprovalQueue + ApprovalCompletionService that the Console model uses,
|
|
931
|
+
// without depending on the pd-console package (which is a private Web UI).
|
|
932
|
+
|
|
933
|
+
interface ActivationApproveOptions {
|
|
934
|
+
workspace?: string;
|
|
935
|
+
approvalId?: string;
|
|
936
|
+
decidedBy?: string;
|
|
937
|
+
note?: string;
|
|
938
|
+
json?: boolean;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
interface ApproveResult {
|
|
942
|
+
ok: boolean;
|
|
943
|
+
approvalId: string;
|
|
944
|
+
activationId?: string;
|
|
945
|
+
decision?: string;
|
|
946
|
+
reason?: string;
|
|
947
|
+
nextAction?: string;
|
|
948
|
+
approvalRolledBack?: boolean;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export async function handleActivationApprove(opts: ActivationApproveOptions): Promise<void> {
|
|
952
|
+
if (!opts.approvalId) {
|
|
953
|
+
const result: ApproveResult = {
|
|
954
|
+
ok: false,
|
|
955
|
+
approvalId: '',
|
|
956
|
+
reason: 'approval_id_required',
|
|
957
|
+
nextAction: 'Provide --approval-id <id>. Run `pd approval list` to see pending approvals.',
|
|
958
|
+
};
|
|
959
|
+
if (opts.json) {
|
|
960
|
+
console.log(JSON.stringify(result, null, 2));
|
|
961
|
+
} else {
|
|
962
|
+
console.error('Error: --approval-id is required');
|
|
963
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
964
|
+
}
|
|
965
|
+
process.exitCode = 1;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const decidedBy = opts.decidedBy ?? 'cli-operator';
|
|
970
|
+
// CodeRabbit review fix (cli-1-strict-json): declare stateManager outside
|
|
971
|
+
// try so the finally block can close it, but resolve workspace INSIDE try
|
|
972
|
+
// so resolveWorkspaceDir() failures are caught and routed through --json.
|
|
973
|
+
let stateManager: RuntimeStateManager | null = null;
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
977
|
+
stateManager = new RuntimeStateManager({ workspaceDir });
|
|
978
|
+
await stateManager.initialize();
|
|
979
|
+
const sqliteConn = stateManager.connection;
|
|
980
|
+
const approvalQueueStore = new SqliteApprovalQueueStore(sqliteConn);
|
|
981
|
+
const queue = new ApprovalQueue(approvalQueueStore);
|
|
982
|
+
|
|
983
|
+
// Step 1: approve the pending approval record.
|
|
984
|
+
let approvalResult: ApprovalDecisionResult;
|
|
985
|
+
try {
|
|
986
|
+
approvalResult = await queue.approve(opts.approvalId, decidedBy, opts.note);
|
|
987
|
+
} catch (err) {
|
|
988
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
989
|
+
const result: ApproveResult = {
|
|
990
|
+
ok: false,
|
|
991
|
+
approvalId: opts.approvalId,
|
|
992
|
+
reason: `approve_write_failed: ${errMsg}`,
|
|
993
|
+
nextAction: 'Check workspace DB integrity. Try `pd runtime diagnostics`.',
|
|
994
|
+
};
|
|
995
|
+
if (opts.json) {
|
|
996
|
+
console.log(JSON.stringify(result, null, 2));
|
|
997
|
+
} else {
|
|
998
|
+
console.error(`Error: ${result.reason}`);
|
|
999
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
1000
|
+
}
|
|
1001
|
+
process.exitCode = 1;
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (!approvalResult.ok) {
|
|
1006
|
+
// Map ApprovalDecisionResult errors to ApproveResult (rc-3 fail-loud).
|
|
1007
|
+
// ApprovalDecisionResult.error is the discriminated union 'not_found' | 'already_decided';
|
|
1008
|
+
// both branches are exhaustive, so no third case is reachable at runtime.
|
|
1009
|
+
const reason = approvalResult.error === 'not_found'
|
|
1010
|
+
? 'approval_not_found'
|
|
1011
|
+
: `already_decided: status is ${approvalResult.status ?? 'unknown'}`;
|
|
1012
|
+
const nextAction = approvalResult.error === 'not_found'
|
|
1013
|
+
? 'Check the approval ID. Run `pd approval list` to see pending approvals.'
|
|
1014
|
+
: 'Only pending approvals can be approved. The approval is already decided.';
|
|
1015
|
+
const result: ApproveResult = {
|
|
1016
|
+
ok: false,
|
|
1017
|
+
approvalId: opts.approvalId,
|
|
1018
|
+
reason,
|
|
1019
|
+
nextAction,
|
|
1020
|
+
};
|
|
1021
|
+
if (opts.json) {
|
|
1022
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1023
|
+
} else {
|
|
1024
|
+
console.error(`Error: ${result.reason}`);
|
|
1025
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
1026
|
+
}
|
|
1027
|
+
process.exitCode = 1;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Step 2: dispatch activation via ApprovalCompletionService (handles
|
|
1032
|
+
// idempotency, feature flag, and rollback on failure).
|
|
1033
|
+
const activationStateStore = new SqliteActivationStateStore(sqliteConn);
|
|
1034
|
+
const piArtifactStore = new SqlitePIArtifactStore(sqliteConn);
|
|
1035
|
+
const artifactReadModel = {
|
|
1036
|
+
getArtifactById: async (id: string): Promise<PIArtifactSnapshot | null> => {
|
|
1037
|
+
const rec = await piArtifactStore.getArtifactById(id);
|
|
1038
|
+
return rec ? toSnapshot(rec) : null;
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
// PRI-489: inject the real workspace `rulecode_context_v2` feature flag
|
|
1042
|
+
// probe into the approve path's RuleHostWriter — same wiring as the
|
|
1043
|
+
// dispatch path (handleRuntimeActivationDispatch) and the Console model
|
|
1044
|
+
// (ApprovalsConsoleModel.dispatchActivationAfterApproval). Previously
|
|
1045
|
+
// this path constructed RuleHostWriter without the probe, so a v2
|
|
1046
|
+
// artifact in a flag-off workspace would pass canActivate here while
|
|
1047
|
+
// being rejected by dispatch/Console — an inconsistent contract that
|
|
1048
|
+
// violated ERR-024 (validator wired in one enforcement path but not
|
|
1049
|
+
// another) and ERR-089 (sibling approval path diverged from dispatch).
|
|
1050
|
+
const featureFlags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
|
|
1051
|
+
const dispatcher = new ActivationDispatcher(
|
|
1052
|
+
artifactReadModel,
|
|
1053
|
+
activationStateStore,
|
|
1054
|
+
{
|
|
1055
|
+
writers: [
|
|
1056
|
+
new PromptWriter(),
|
|
1057
|
+
new RuleHostWriter({
|
|
1058
|
+
gateDeps: createProductionGateDeps(),
|
|
1059
|
+
featureFlagProbe: (flagId) => featureFlags.flags[flagId]?.enabled === true,
|
|
1060
|
+
}),
|
|
1061
|
+
new DeferArchiveWriter(),
|
|
1062
|
+
],
|
|
1063
|
+
approvalQueueStore,
|
|
1064
|
+
},
|
|
1065
|
+
);
|
|
1066
|
+
const completionService = new ApprovalCompletionService(
|
|
1067
|
+
approvalQueueStore,
|
|
1068
|
+
dispatcher,
|
|
1069
|
+
activationStateStore,
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
let completionResult: ApprovalCompletionResult;
|
|
1073
|
+
try {
|
|
1074
|
+
completionResult = await completionService.completeApproval({
|
|
1075
|
+
approvalId: opts.approvalId,
|
|
1076
|
+
actor: { kind: 'human', userId: decidedBy },
|
|
1077
|
+
now: new Date().toISOString(),
|
|
1078
|
+
});
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
// CodeRabbit review fix (cli-5-failure-no-mutation): approval was
|
|
1081
|
+
// written but completion threw unexpectedly. Roll back the approval to
|
|
1082
|
+
// 'pending' so the operator can retry without a stale 'approved' record
|
|
1083
|
+
// that has no activation. Mirrors ApprovalsConsoleModel.approve() L168-182.
|
|
1084
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1085
|
+
let approvalRolledBack = false;
|
|
1086
|
+
try {
|
|
1087
|
+
const rollbackResult = await queue.resetToPending(opts.approvalId);
|
|
1088
|
+
approvalRolledBack = rollbackResult.ok;
|
|
1089
|
+
} catch { /* best-effort rollback */ }
|
|
1090
|
+
const result: ApproveResult = {
|
|
1091
|
+
ok: false,
|
|
1092
|
+
approvalId: opts.approvalId,
|
|
1093
|
+
reason: `activation_completion_failed: ${errMsg}`,
|
|
1094
|
+
nextAction: approvalRolledBack
|
|
1095
|
+
? 'Approval rolled back to pending. Fix the issue and re-run `pd activation approve --approval-id <id>`.'
|
|
1096
|
+
: `Approval remains 'approved'. Run \`pd activation dispatch --artifact-id ${approvalResult.record.artifactId} --confirm\` to retry activation.`,
|
|
1097
|
+
approvalRolledBack,
|
|
1098
|
+
};
|
|
1099
|
+
if (opts.json) {
|
|
1100
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1101
|
+
} else {
|
|
1102
|
+
console.error(`Error: ${result.reason}`);
|
|
1103
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
1104
|
+
}
|
|
1105
|
+
process.exitCode = 1;
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (!completionResult.ok) {
|
|
1110
|
+
// CodeRabbit review fix (cli-5-failure-no-mutation): activation
|
|
1111
|
+
// returned !ok. Roll back the approval to 'pending' so the operator
|
|
1112
|
+
// can retry without a stale 'approved' record. Mirrors
|
|
1113
|
+
// ApprovalsConsoleModel.approve() L168-182.
|
|
1114
|
+
let approvalRolledBack = false;
|
|
1115
|
+
try {
|
|
1116
|
+
const rollbackResult = await queue.resetToPending(opts.approvalId);
|
|
1117
|
+
approvalRolledBack = rollbackResult.ok;
|
|
1118
|
+
} catch { /* best-effort rollback */ }
|
|
1119
|
+
const result: ApproveResult = {
|
|
1120
|
+
ok: false,
|
|
1121
|
+
approvalId: opts.approvalId,
|
|
1122
|
+
reason: `activation_failed: ${completionResult.reason}`,
|
|
1123
|
+
nextAction: approvalRolledBack
|
|
1124
|
+
? `Approval rolled back to pending. ${completionResult.nextAction ?? 'Fix the issue and re-run `pd activation approve --approval-id <id>`.'}`
|
|
1125
|
+
: `Approval remains 'approved'. ${completionResult.nextAction ?? 'Check the artifact validation status and retry.'}`,
|
|
1126
|
+
approvalRolledBack,
|
|
1127
|
+
};
|
|
1128
|
+
if (opts.json) {
|
|
1129
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1130
|
+
} else {
|
|
1131
|
+
console.error(`Error: ${result.reason}`);
|
|
1132
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
1133
|
+
}
|
|
1134
|
+
process.exitCode = 1;
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Step 3: success — surface activation result.
|
|
1139
|
+
const result: ApproveResult = {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
approvalId: opts.approvalId,
|
|
1142
|
+
activationId: completionResult.activationId,
|
|
1143
|
+
decision: completionResult.decision.decision,
|
|
1144
|
+
nextAction: 'pd activation list',
|
|
1145
|
+
};
|
|
1146
|
+
if (opts.json) {
|
|
1147
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1148
|
+
} else {
|
|
1149
|
+
console.log(`Approval approved: ${result.approvalId}`);
|
|
1150
|
+
console.log(` activationId: ${result.activationId ?? 'N/A'}`);
|
|
1151
|
+
console.log(` decision: ${result.decision}`);
|
|
1152
|
+
console.log(`Next action: ${result.nextAction}`);
|
|
1153
|
+
}
|
|
1154
|
+
} catch (err: unknown) {
|
|
1155
|
+
// P2 #5: initialize/DB exceptions must not break --json contract.
|
|
1156
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1157
|
+
const result: ApproveResult = {
|
|
1158
|
+
ok: false,
|
|
1159
|
+
approvalId: opts.approvalId ?? '',
|
|
1160
|
+
reason: `initialize_failed: ${errMsg}`,
|
|
1161
|
+
nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
|
|
1162
|
+
};
|
|
1163
|
+
if (opts.json) {
|
|
1164
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1165
|
+
} else {
|
|
1166
|
+
console.error(`Error: ${result.reason}`);
|
|
1167
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
1168
|
+
}
|
|
1169
|
+
process.exitCode = 1;
|
|
1170
|
+
} finally {
|
|
1171
|
+
// stateManager may be null if resolveWorkspaceDir() threw before assignment.
|
|
1172
|
+
await stateManager?.close();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ── Commander Wiring (CLI gate rule 7: test the real command wiring) ─────────
|
|
1177
|
+
//
|
|
1178
|
+
// Extracted so parser-level tests can exercise the actual flag registration
|
|
1179
|
+
// without importing all of `index.ts`. Mirrors `registerRunRuleHostCommand`.
|
|
1180
|
+
|
|
1181
|
+
export function registerRuntimeActivationPromoteCommand(parent: Command): Command {
|
|
1182
|
+
return parent
|
|
1183
|
+
.command('promote')
|
|
1184
|
+
.description('Promote a code_tool_hook activation from shadow observation to live blocking')
|
|
1185
|
+
.requiredOption('--activation-id <id>', 'Shadow activation ID to promote')
|
|
1186
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1187
|
+
.option('--dry-run', 'Validate eligibility without changing activation state')
|
|
1188
|
+
.option('--confirm', 'Confirm promotion to live blocking')
|
|
1189
|
+
.option('--json', 'Output raw JSON')
|
|
1190
|
+
.action(async (opts) => {
|
|
1191
|
+
await handleRuntimeActivationPromote({
|
|
1192
|
+
activationId: opts.activationId,
|
|
1193
|
+
workspace: opts.workspace,
|
|
1194
|
+
dryRun: opts.dryRun,
|
|
1195
|
+
confirm: opts.confirm,
|
|
1196
|
+
json: opts.json,
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// PRI-493: Extracted helpers for parser-level flag-wiring tests (cli-7).
|
|
1202
|
+
// Mirrors registerRuntimeActivationPromoteCommand — single source of truth
|
|
1203
|
+
// shared with index.ts. Flag typos in production surface at parseAsync time.
|
|
1204
|
+
|
|
1205
|
+
export function registerRuntimeActivationDeactivateCommand(parent: Command): Command {
|
|
1206
|
+
return parent
|
|
1207
|
+
.command('deactivate')
|
|
1208
|
+
.description('Deactivate an activation by activation ID')
|
|
1209
|
+
.requiredOption('--activation-id <id>', 'Activation ID to deactivate')
|
|
1210
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1211
|
+
.option('--json', 'Output raw JSON')
|
|
1212
|
+
.action(async (opts) => {
|
|
1213
|
+
await handleRuntimeActivationDeactivate({
|
|
1214
|
+
activationId: opts.activationId,
|
|
1215
|
+
workspace: opts.workspace,
|
|
1216
|
+
json: opts.json,
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export function registerRuntimeActivationListCommand(parent: Command): Command {
|
|
1222
|
+
return parent
|
|
1223
|
+
.command('list')
|
|
1224
|
+
.description('List all activations for a workspace')
|
|
1225
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1226
|
+
.option('-c, --channel <channel>', 'Filter by channel (prompt|code_tool_hook)')
|
|
1227
|
+
.option('--include-deactivated', 'Include deactivated records in output')
|
|
1228
|
+
.option('--json', 'Output raw JSON')
|
|
1229
|
+
.action(async (opts) => {
|
|
1230
|
+
await handleRuntimeActivationList({
|
|
1231
|
+
workspace: opts.workspace,
|
|
1232
|
+
channel: opts.channel,
|
|
1233
|
+
includeDeactivated: opts.includeDeactivated,
|
|
1234
|
+
json: opts.json,
|
|
1235
|
+
});
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
export function registerRuntimeActivationDispatchCommand(parent: Command): Command {
|
|
1240
|
+
return parent
|
|
1241
|
+
.command('dispatch')
|
|
1242
|
+
.description('Dispatch an activation for a rollout-reviewed artifact')
|
|
1243
|
+
.option('-a, --artifact-id <id>', 'PIArtifact ID to activate')
|
|
1244
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1245
|
+
.option('-c, --channel <channel>', 'Activation channel (prompt|defer_archive)', 'prompt')
|
|
1246
|
+
.option('--dry-run', 'Dry-run mode (default, no writes)')
|
|
1247
|
+
.option('--confirm', 'Confirm and write activation record')
|
|
1248
|
+
.option('--json', 'Output raw JSON')
|
|
1249
|
+
.action(async (opts) => {
|
|
1250
|
+
await handleRuntimeActivationDispatch({
|
|
1251
|
+
workspace: opts.workspace,
|
|
1252
|
+
artifactId: opts.artifactId,
|
|
1253
|
+
channel: opts.channel,
|
|
1254
|
+
dryRun: opts.dryRun,
|
|
1255
|
+
confirm: opts.confirm,
|
|
1256
|
+
json: opts.json,
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export function registerRuntimeActivationApproveCommand(parent: Command): Command {
|
|
1262
|
+
return parent
|
|
1263
|
+
.command('approve')
|
|
1264
|
+
.description('Approve a pending approval and dispatch its activation')
|
|
1265
|
+
.requiredOption('-a, --approval-id <id>', 'Approval ID to approve')
|
|
1266
|
+
.option('--decided-by <user>', 'Reviewer name (default: cli-operator)')
|
|
1267
|
+
.option('--note <text>', 'Optional approval note')
|
|
1268
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1269
|
+
.option('--json', 'Output raw JSON')
|
|
1270
|
+
.action(async (opts) => {
|
|
1271
|
+
await handleActivationApprove({
|
|
1272
|
+
approvalId: opts.approvalId,
|
|
1273
|
+
decidedBy: opts.decidedBy,
|
|
1274
|
+
note: opts.note,
|
|
1275
|
+
workspace: opts.workspace,
|
|
1276
|
+
json: opts.json,
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
}
|