@principles/pd-cli 1.113.0 → 1.115.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/runtime-activation.d.ts +37 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +416 -2
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/index.js +54 -1
- package/dist/index.js.map +1 -1
- package/dist/services/demo-rule-compiler.d.ts.map +1 -1
- package/dist/services/demo-rule-compiler.js +30 -6
- package/dist/services/demo-rule-compiler.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +8 -0
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +43 -1
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/scripts/llm-dogfood.ts +419 -0
- package/src/commands/runtime-activation.ts +459 -1
- package/src/index.ts +57 -1
- package/src/services/demo-rule-compiler.ts +35 -15
- package/src/services/rulehost-pipeline-runner.ts +53 -0
- package/tests/commands/cli-command-tree.test.ts +14 -0
- package/tests/commands/run-rulehost-handler.test.ts +253 -0
- package/tests/commands/runtime-activation.test.ts +553 -1
- package/tests/e2e/cross-package-acceptance.test.ts +549 -0
- package/tests/services/demo-rule-compiler.test.ts +242 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +6 -0
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
ActivationDispatcher,
|
|
5
5
|
PromptWriter,
|
|
6
6
|
DeferArchiveWriter,
|
|
7
|
+
RuleHostWriter,
|
|
8
|
+
createProductionGateDeps,
|
|
7
9
|
SqliteActivationStateStore,
|
|
8
10
|
SqliteApprovalQueueStore,
|
|
11
|
+
SqlitePIArtifactStore,
|
|
12
|
+
isArtifactRevisionOf,
|
|
9
13
|
} from '@principles/core/runtime-v2';
|
|
10
14
|
import type { ActivationDecision, PIArtifactSnapshot, RolloutActivationDecision } from '@principles/core/runtime-v2';
|
|
11
15
|
import type { PIArtifactRecord } from '@principles/core/runtime-v2';
|
|
@@ -146,10 +150,19 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
|
|
|
146
150
|
|
|
147
151
|
const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
|
|
148
152
|
const approvalQueueStore = new SqliteApprovalQueueStore(stateManager.connection);
|
|
153
|
+
// Wire all three MVP-Core writers, including RuleHostWriter for code_tool_hook.
|
|
154
|
+
// PRI-408: fixes P0 breakpoint where code_tool_hook channel could not activate.
|
|
149
155
|
const dispatcher = new ActivationDispatcher(
|
|
150
156
|
artifactReadModel,
|
|
151
157
|
activationStateStore,
|
|
152
|
-
{
|
|
158
|
+
{
|
|
159
|
+
writers: [
|
|
160
|
+
new PromptWriter(),
|
|
161
|
+
new RuleHostWriter({ gateDeps: createProductionGateDeps() }),
|
|
162
|
+
new DeferArchiveWriter(),
|
|
163
|
+
],
|
|
164
|
+
approvalQueueStore,
|
|
165
|
+
},
|
|
153
166
|
);
|
|
154
167
|
|
|
155
168
|
const result = await dispatcher.dispatch({
|
|
@@ -174,3 +187,448 @@ export async function handleRuntimeActivationDispatch(opts: ActivationDispatchOp
|
|
|
174
187
|
await stateManager.close();
|
|
175
188
|
}
|
|
176
189
|
}
|
|
190
|
+
|
|
191
|
+
// ── Deactivate Command (PRI-408 Contract E) ─────────────────────────────────
|
|
192
|
+
|
|
193
|
+
interface ActivationDeactivateOptions {
|
|
194
|
+
workspace?: string;
|
|
195
|
+
activationId?: string;
|
|
196
|
+
json?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface DeactivateResult {
|
|
200
|
+
ok: boolean;
|
|
201
|
+
activationId: string;
|
|
202
|
+
deactivatedAt?: string;
|
|
203
|
+
reason?: string;
|
|
204
|
+
nextAction?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function handleRuntimeActivationDeactivate(opts: ActivationDeactivateOptions): Promise<void> {
|
|
208
|
+
if (!opts.activationId) {
|
|
209
|
+
const result: DeactivateResult = {
|
|
210
|
+
ok: false,
|
|
211
|
+
activationId: '',
|
|
212
|
+
reason: 'activation_id_required',
|
|
213
|
+
nextAction: 'Provide --activation-id <id> from `pd runtime activation list`',
|
|
214
|
+
};
|
|
215
|
+
if (opts.json) {
|
|
216
|
+
console.log(JSON.stringify(result, null, 2));
|
|
217
|
+
} else {
|
|
218
|
+
console.error(`Error: --activation-id is required`);
|
|
219
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
220
|
+
}
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
226
|
+
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await stateManager.initialize();
|
|
230
|
+
const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
|
|
231
|
+
const deactivatedAt = new Date().toISOString();
|
|
232
|
+
|
|
233
|
+
// Idempotent deactivate: returns false if already deactivated or not found.
|
|
234
|
+
// Both cases are safe to call repeatedly (Contract E: rollback must be idempotent).
|
|
235
|
+
const success = await activationStateStore.deactivateActivation(opts.activationId, deactivatedAt);
|
|
236
|
+
|
|
237
|
+
const result: DeactivateResult = success
|
|
238
|
+
? { ok: true, activationId: opts.activationId, deactivatedAt }
|
|
239
|
+
: {
|
|
240
|
+
ok: false,
|
|
241
|
+
activationId: opts.activationId,
|
|
242
|
+
reason: 'not_found_or_already_deactivated',
|
|
243
|
+
nextAction: 'Check activation ID with `pd runtime activation list`, or it may already be deactivated',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (opts.json) {
|
|
247
|
+
// Strict JSON mode: exactly one parseable JSON object on stdout
|
|
248
|
+
console.log(JSON.stringify(result, null, 2));
|
|
249
|
+
} else {
|
|
250
|
+
if (success) {
|
|
251
|
+
console.log(`Deactivated: ${opts.activationId}`);
|
|
252
|
+
console.log(` deactivatedAt: ${deactivatedAt}`);
|
|
253
|
+
} else {
|
|
254
|
+
console.log(`Not deactivated: ${opts.activationId}`);
|
|
255
|
+
console.log(` reason: ${result.reason}`);
|
|
256
|
+
console.log(` nextAction: ${result.nextAction}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!success) {
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
}
|
|
263
|
+
} catch (err: unknown) {
|
|
264
|
+
// P2 #5: initialize/DB exceptions must not break --json contract.
|
|
265
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
const result: DeactivateResult = {
|
|
267
|
+
ok: false,
|
|
268
|
+
activationId: opts.activationId,
|
|
269
|
+
reason: `initialize_failed: ${errMsg}`,
|
|
270
|
+
nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
|
|
271
|
+
};
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify(result, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.error(`Error: ${result.reason}`);
|
|
276
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
277
|
+
}
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
} finally {
|
|
280
|
+
await stateManager.close();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── List Activations Command (PRI-408 Contract D — observability) ────────────
|
|
285
|
+
|
|
286
|
+
interface ActivationListOptions {
|
|
287
|
+
workspace?: string;
|
|
288
|
+
channel?: string;
|
|
289
|
+
includeDeactivated?: boolean;
|
|
290
|
+
json?: boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function handleRuntimeActivationList(opts: ActivationListOptions): Promise<void> {
|
|
294
|
+
// P2 #5 fix: fail loud on invalid channel instead of silently listing all.
|
|
295
|
+
const VALID_CHANNELS = new Set(['prompt', 'code_tool_hook', undefined]);
|
|
296
|
+
if (opts.channel !== undefined && !VALID_CHANNELS.has(opts.channel)) {
|
|
297
|
+
const result = {
|
|
298
|
+
ok: false,
|
|
299
|
+
reason: `invalid_channel: ${opts.channel}`,
|
|
300
|
+
nextAction: 'Use one of: prompt, code_tool_hook, or omit --channel to list all',
|
|
301
|
+
};
|
|
302
|
+
if (opts.json) {
|
|
303
|
+
console.log(JSON.stringify(result, null, 2));
|
|
304
|
+
} else {
|
|
305
|
+
console.error(`Error: invalid channel "${opts.channel}"`);
|
|
306
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
307
|
+
}
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
313
|
+
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
await stateManager.initialize();
|
|
317
|
+
const activationStateStore = new SqliteActivationStateStore(stateManager.connection);
|
|
318
|
+
|
|
319
|
+
// P2 #5 fix: pass includeDeactivated to the store so channel-specific queries
|
|
320
|
+
// also return deactivated records when requested. Previously the SQL hardcoded
|
|
321
|
+
// `WHERE deactivated_at IS NULL`, making --include-deactivated a no-op for
|
|
322
|
+
// channel-filtered queries.
|
|
323
|
+
let records;
|
|
324
|
+
if (opts.channel === 'prompt') {
|
|
325
|
+
records = await activationStateStore.listPromptActivations(opts.includeDeactivated ?? false);
|
|
326
|
+
} else if (opts.channel === 'code_tool_hook') {
|
|
327
|
+
records = await activationStateStore.listCodeToolHookActivations(opts.includeDeactivated ?? false);
|
|
328
|
+
} else {
|
|
329
|
+
records = await activationStateStore.listAllActivations();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// For listAllActivations, still apply the includeDeactivated filter at the
|
|
333
|
+
// caller level since listAllActivations() does not take the parameter.
|
|
334
|
+
const filtered = opts.includeDeactivated
|
|
335
|
+
? records
|
|
336
|
+
: records.filter(r => r.deactivatedAt === null);
|
|
337
|
+
|
|
338
|
+
if (opts.json) {
|
|
339
|
+
// Strict JSON mode: exactly one parseable JSON object on stdout
|
|
340
|
+
console.log(JSON.stringify({ activations: filtered }, null, 2));
|
|
341
|
+
} else {
|
|
342
|
+
if (filtered.length === 0) {
|
|
343
|
+
console.log('No active activations found.');
|
|
344
|
+
} else {
|
|
345
|
+
for (const r of filtered) {
|
|
346
|
+
const status = r.deactivatedAt ? `[DEACTIVATED ${r.deactivatedAt}]` : '[ACTIVE]';
|
|
347
|
+
console.log(`${status} ${r.activationId}`);
|
|
348
|
+
console.log(` artifactId: ${r.artifactId}`);
|
|
349
|
+
console.log(` channel: ${r.channel}`);
|
|
350
|
+
console.log(` action: ${r.action}`);
|
|
351
|
+
console.log(` targetRef: ${r.targetRef}`);
|
|
352
|
+
console.log(` activatedAt: ${r.activatedAt}`);
|
|
353
|
+
console.log('');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (err: unknown) {
|
|
358
|
+
// P2 #5: initialize/DB exceptions must not break --json contract.
|
|
359
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
360
|
+
const result = {
|
|
361
|
+
ok: false,
|
|
362
|
+
reason: `initialize_failed: ${errMsg}`,
|
|
363
|
+
nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
|
|
364
|
+
};
|
|
365
|
+
if (opts.json) {
|
|
366
|
+
console.log(JSON.stringify(result, null, 2));
|
|
367
|
+
} else {
|
|
368
|
+
console.error(`Error: ${result.reason}`);
|
|
369
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
370
|
+
}
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
} finally {
|
|
373
|
+
await stateManager.close();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Edit Pending Approval Command (P1 #2 fix — Owner edit entry point) ──────
|
|
378
|
+
|
|
379
|
+
interface ActivationEditOptions {
|
|
380
|
+
workspace?: string;
|
|
381
|
+
approvalId?: string;
|
|
382
|
+
newArtifactId?: string;
|
|
383
|
+
editReason?: string;
|
|
384
|
+
json?: boolean;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface EditApprovalResult {
|
|
388
|
+
ok: boolean;
|
|
389
|
+
approvalId?: string;
|
|
390
|
+
newArtifactId?: string;
|
|
391
|
+
previousArtifactId?: string;
|
|
392
|
+
editedAt?: string;
|
|
393
|
+
reason?: string;
|
|
394
|
+
nextAction?: string;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function handleRuntimeActivationEdit(opts: ActivationEditOptions): Promise<void> {
|
|
398
|
+
if (!opts.approvalId) {
|
|
399
|
+
const result: EditApprovalResult = {
|
|
400
|
+
ok: false,
|
|
401
|
+
reason: 'approval_id_required',
|
|
402
|
+
nextAction: 'Provide --approval-id <id> from Console approvals page',
|
|
403
|
+
};
|
|
404
|
+
if (opts.json) {
|
|
405
|
+
console.log(JSON.stringify(result, null, 2));
|
|
406
|
+
} else {
|
|
407
|
+
console.error('Error: --approval-id is required');
|
|
408
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
409
|
+
}
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!opts.newArtifactId) {
|
|
415
|
+
const result: EditApprovalResult = {
|
|
416
|
+
ok: false,
|
|
417
|
+
reason: 'new_artifact_id_required',
|
|
418
|
+
nextAction: 'Create a new artifact first (e.g. via pd candidate intake), then pass its ID with --new-artifact-id',
|
|
419
|
+
};
|
|
420
|
+
if (opts.json) {
|
|
421
|
+
console.log(JSON.stringify(result, null, 2));
|
|
422
|
+
} else {
|
|
423
|
+
console.error('Error: --new-artifact-id is required');
|
|
424
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
425
|
+
}
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!opts.editReason) {
|
|
431
|
+
const result: EditApprovalResult = {
|
|
432
|
+
ok: false,
|
|
433
|
+
reason: 'edit_reason_required',
|
|
434
|
+
nextAction: 'Provide --edit-reason explaining why the artifact is being revised',
|
|
435
|
+
};
|
|
436
|
+
if (opts.json) {
|
|
437
|
+
console.log(JSON.stringify(result, null, 2));
|
|
438
|
+
} else {
|
|
439
|
+
console.error('Error: --edit-reason is required');
|
|
440
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
441
|
+
}
|
|
442
|
+
process.exitCode = 1;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
447
|
+
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
await stateManager.initialize();
|
|
451
|
+
const approvalStore = new SqliteApprovalQueueStore(stateManager.connection);
|
|
452
|
+
const artifactStore = new SqlitePIArtifactStore(stateManager.connection);
|
|
453
|
+
const now = new Date().toISOString();
|
|
454
|
+
|
|
455
|
+
// P1 #2: validate the new artifact before swapping the approval pointer.
|
|
456
|
+
// The new artifact must exist, be validated, and have lineage consistent
|
|
457
|
+
// with the original approval's artifact (same sourceTaskId).
|
|
458
|
+
const existingApproval = await approvalStore.getById(opts.approvalId);
|
|
459
|
+
if (!existingApproval) {
|
|
460
|
+
const result: EditApprovalResult = {
|
|
461
|
+
ok: false,
|
|
462
|
+
approvalId: opts.approvalId,
|
|
463
|
+
reason: 'not_found',
|
|
464
|
+
nextAction: 'Check the approval ID on Console approvals page',
|
|
465
|
+
};
|
|
466
|
+
if (opts.json) {
|
|
467
|
+
console.log(JSON.stringify(result, null, 2));
|
|
468
|
+
} else {
|
|
469
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
470
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
471
|
+
}
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
await stateManager.close();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (existingApproval.status !== 'pending') {
|
|
477
|
+
const result: EditApprovalResult = {
|
|
478
|
+
ok: false,
|
|
479
|
+
approvalId: opts.approvalId,
|
|
480
|
+
reason: 'already_decided',
|
|
481
|
+
nextAction: `Approval is already decided (status: ${existingApproval.status}). Only pending approvals can be edited.`,
|
|
482
|
+
};
|
|
483
|
+
if (opts.json) {
|
|
484
|
+
console.log(JSON.stringify(result, null, 2));
|
|
485
|
+
} else {
|
|
486
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
487
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
488
|
+
}
|
|
489
|
+
process.exitCode = 1;
|
|
490
|
+
await stateManager.close();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const newArtifact = await artifactStore.getArtifactById(opts.newArtifactId);
|
|
495
|
+
if (!newArtifact) {
|
|
496
|
+
const result: EditApprovalResult = {
|
|
497
|
+
ok: false,
|
|
498
|
+
approvalId: opts.approvalId,
|
|
499
|
+
reason: 'artifact_not_found',
|
|
500
|
+
nextAction: `Artifact ${opts.newArtifactId} does not exist. Create it first via pd candidate intake.`,
|
|
501
|
+
};
|
|
502
|
+
if (opts.json) {
|
|
503
|
+
console.log(JSON.stringify(result, null, 2));
|
|
504
|
+
} else {
|
|
505
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
506
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
507
|
+
}
|
|
508
|
+
process.exitCode = 1;
|
|
509
|
+
await stateManager.close();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (newArtifact.validationStatus !== 'validated') {
|
|
513
|
+
const result: EditApprovalResult = {
|
|
514
|
+
ok: false,
|
|
515
|
+
approvalId: opts.approvalId,
|
|
516
|
+
reason: `artifact_not_validated: ${newArtifact.validationStatus}`,
|
|
517
|
+
nextAction: `Artifact ${opts.newArtifactId} has validationStatus '${newArtifact.validationStatus}'. Run the production gate to validate it first.`,
|
|
518
|
+
};
|
|
519
|
+
if (opts.json) {
|
|
520
|
+
console.log(JSON.stringify(result, null, 2));
|
|
521
|
+
} else {
|
|
522
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
523
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
524
|
+
}
|
|
525
|
+
process.exitCode = 1;
|
|
526
|
+
await stateManager.close();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const originalArtifact = await artifactStore.getArtifactById(existingApproval.artifactId);
|
|
530
|
+
if (!originalArtifact || !isArtifactRevisionOf(newArtifact, originalArtifact)) {
|
|
531
|
+
const result: EditApprovalResult = {
|
|
532
|
+
ok: false,
|
|
533
|
+
approvalId: opts.approvalId,
|
|
534
|
+
reason: 'artifact_lineage_mismatch',
|
|
535
|
+
nextAction: originalArtifact
|
|
536
|
+
? `Artifact ${opts.newArtifactId} must reference ${originalArtifact.artifactId} or its source principle.`
|
|
537
|
+
: `Original artifact ${existingApproval.artifactId} is missing; restore it before editing this approval.`,
|
|
538
|
+
};
|
|
539
|
+
if (opts.json) {
|
|
540
|
+
console.log(JSON.stringify(result, null, 2));
|
|
541
|
+
} else {
|
|
542
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
543
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
544
|
+
}
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
await stateManager.close();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let editResult;
|
|
551
|
+
try {
|
|
552
|
+
editResult = await approvalStore.edit({
|
|
553
|
+
approvalId: opts.approvalId,
|
|
554
|
+
editedBy: 'operator',
|
|
555
|
+
newArtifactId: opts.newArtifactId,
|
|
556
|
+
editReason: opts.editReason,
|
|
557
|
+
now,
|
|
558
|
+
});
|
|
559
|
+
} catch (err: unknown) {
|
|
560
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
561
|
+
const result: EditApprovalResult = {
|
|
562
|
+
ok: false,
|
|
563
|
+
approvalId: opts.approvalId,
|
|
564
|
+
reason: `edit_failed: ${errMsg}`,
|
|
565
|
+
nextAction: 'Check workspace DB integrity and that the approval + new artifact exist',
|
|
566
|
+
};
|
|
567
|
+
if (opts.json) {
|
|
568
|
+
console.log(JSON.stringify(result, null, 2));
|
|
569
|
+
} else {
|
|
570
|
+
console.error(`Edit failed: ${result.reason}`);
|
|
571
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
572
|
+
}
|
|
573
|
+
process.exitCode = 1;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!editResult.ok) {
|
|
578
|
+
const result: EditApprovalResult = {
|
|
579
|
+
ok: false,
|
|
580
|
+
approvalId: opts.approvalId,
|
|
581
|
+
reason: editResult.error,
|
|
582
|
+
nextAction: editResult.error === 'not_found'
|
|
583
|
+
? 'Check the approval ID on Console approvals page'
|
|
584
|
+
: `Approval is already decided (status: ${editResult.status ?? 'unknown'}). Only pending approvals can be edited.`,
|
|
585
|
+
};
|
|
586
|
+
if (opts.json) {
|
|
587
|
+
console.log(JSON.stringify(result, null, 2));
|
|
588
|
+
} else {
|
|
589
|
+
console.error(`Edit refused: ${result.reason}`);
|
|
590
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
591
|
+
}
|
|
592
|
+
process.exitCode = 1;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const result: EditApprovalResult = {
|
|
597
|
+
ok: true,
|
|
598
|
+
approvalId: editResult.record.approvalId,
|
|
599
|
+
newArtifactId: editResult.record.artifactId,
|
|
600
|
+
previousArtifactId: editResult.record.previousArtifactId ?? undefined,
|
|
601
|
+
editedAt: editResult.record.editedAt ?? now,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
if (opts.json) {
|
|
605
|
+
console.log(JSON.stringify(result, null, 2));
|
|
606
|
+
} else {
|
|
607
|
+
console.log(`Approval edited: ${result.approvalId}`);
|
|
608
|
+
console.log(` newArtifactId: ${result.newArtifactId}`);
|
|
609
|
+
if (result.previousArtifactId) {
|
|
610
|
+
console.log(` previousArtifactId: ${result.previousArtifactId}`);
|
|
611
|
+
}
|
|
612
|
+
console.log(` editedAt: ${result.editedAt}`);
|
|
613
|
+
console.log('Next action: review the new artifact, then approve via Console or `pd runtime activation dispatch`');
|
|
614
|
+
}
|
|
615
|
+
} catch (err: unknown) {
|
|
616
|
+
// P2 #5: initialize/DB exceptions must not break --json contract.
|
|
617
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
618
|
+
const result: EditApprovalResult = {
|
|
619
|
+
ok: false,
|
|
620
|
+
approvalId: opts.approvalId,
|
|
621
|
+
reason: `initialize_failed: ${errMsg}`,
|
|
622
|
+
nextAction: 'Check workspace directory and DB integrity. Try `pd runtime diagnostics`.',
|
|
623
|
+
};
|
|
624
|
+
if (opts.json) {
|
|
625
|
+
console.log(JSON.stringify(result, null, 2));
|
|
626
|
+
} else {
|
|
627
|
+
console.error(`Error: ${result.reason}`);
|
|
628
|
+
console.error(`Next action: ${result.nextAction}`);
|
|
629
|
+
}
|
|
630
|
+
process.exitCode = 1;
|
|
631
|
+
} finally {
|
|
632
|
+
await stateManager.close();
|
|
633
|
+
}
|
|
634
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { handleRuntimeDiagnosticsExport } from './commands/runtime-diagnostics-e
|
|
|
47
47
|
import { handleRuntimeRecoverySweep } from './commands/runtime-recovery.js';
|
|
48
48
|
import { handleRuntimeRecoveryFailedTasks } from './commands/runtime-recovery-failed-tasks.js';
|
|
49
49
|
import { handleRuntimeActivationDispatch } from './commands/runtime-activation.js';
|
|
50
|
+
import { handleRuntimeActivationDeactivate, handleRuntimeActivationList, handleRuntimeActivationEdit } from './commands/runtime-activation.js';
|
|
50
51
|
import { handleProvenChannelBaseline } from './commands/proven-channel-baseline.js';
|
|
51
52
|
import { handleDemoStoryA } from './commands/demo-story-a.js';
|
|
52
53
|
import { handleRuntimeFeaturesStatus } from './commands/runtime-features.js';
|
|
@@ -577,7 +578,7 @@ const activationCmd = runtimeCmd
|
|
|
577
578
|
activationCmd
|
|
578
579
|
.command('dispatch')
|
|
579
580
|
.description('Dispatch an activation for a rollout-reviewed artifact')
|
|
580
|
-
.
|
|
581
|
+
.option('-a, --artifact-id <id>', 'PIArtifact ID to activate')
|
|
581
582
|
.option('-w, --workspace <path>', 'Workspace directory')
|
|
582
583
|
.option('-c, --channel <channel>', 'Activation channel (prompt|defer_archive)', 'prompt')
|
|
583
584
|
.option('--dry-run', 'Dry-run mode (default, no writes)')
|
|
@@ -594,6 +595,61 @@ activationCmd
|
|
|
594
595
|
});
|
|
595
596
|
});
|
|
596
597
|
|
|
598
|
+
// PRI-408 Contract E: Owner-initiated rollback/deactivate of an activation.
|
|
599
|
+
// Idempotent — calling twice on the same ID is safe and returns ok=false with reason.
|
|
600
|
+
activationCmd
|
|
601
|
+
.command('deactivate')
|
|
602
|
+
.description('Deactivate (rollback) an active activation — idempotent (PRI-408 Contract E)')
|
|
603
|
+
.option('-a, --activation-id <id>', 'Activation ID to deactivate')
|
|
604
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
605
|
+
.option('--json', 'Output raw JSON')
|
|
606
|
+
.action(async (opts) => {
|
|
607
|
+
await handleRuntimeActivationDeactivate({
|
|
608
|
+
workspace: opts.workspace,
|
|
609
|
+
activationId: opts.activationId,
|
|
610
|
+
json: opts.json,
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// PRI-408 Contract D: Owner observability — list current activations.
|
|
615
|
+
activationCmd
|
|
616
|
+
.command('list')
|
|
617
|
+
.description('List activations (default: active only) — PRI-408 Contract D observability')
|
|
618
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
619
|
+
.option('-c, --channel <channel>', 'Filter by channel (prompt|code_tool_hook)')
|
|
620
|
+
.option('--include-deactivated', 'Include deactivated records in output')
|
|
621
|
+
.option('--json', 'Output raw JSON')
|
|
622
|
+
.action(async (opts) => {
|
|
623
|
+
await handleRuntimeActivationList({
|
|
624
|
+
workspace: opts.workspace,
|
|
625
|
+
channel: opts.channel,
|
|
626
|
+
includeDeactivated: opts.includeDeactivated,
|
|
627
|
+
json: opts.json,
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// P1 #2 fix: Owner edit entry point — swap a pending approval's artifact.
|
|
632
|
+
// Required because ApprovalQueue.edit() was dead code with no CLI/OpenClaw entry.
|
|
633
|
+
// P2 #5: use .option() instead of .requiredOption() so missing-flag errors
|
|
634
|
+
// produce structured JSON output via the handler, not Commander's pre-handler exit.
|
|
635
|
+
activationCmd
|
|
636
|
+
.command('edit')
|
|
637
|
+
.description('Edit a pending approval to swap its artifact — P1 #2 owner edit entry point')
|
|
638
|
+
.option('-a, --approval-id <id>', 'Approval ID to edit (must be pending)')
|
|
639
|
+
.option('-n, --new-artifact-id <id>', 'New PIArtifact ID to swap to')
|
|
640
|
+
.option('-r, --edit-reason <text>', 'Reason for the edit')
|
|
641
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
642
|
+
.option('--json', 'Output raw JSON')
|
|
643
|
+
.action(async (opts) => {
|
|
644
|
+
await handleRuntimeActivationEdit({
|
|
645
|
+
workspace: opts.workspace,
|
|
646
|
+
approvalId: opts.approvalId,
|
|
647
|
+
newArtifactId: opts.newArtifactId,
|
|
648
|
+
editReason: opts.editReason,
|
|
649
|
+
json: opts.json,
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
597
653
|
const diagnosticsCmd = runtimeCmd
|
|
598
654
|
.command('diagnostics')
|
|
599
655
|
.description('Control plane diagnostic bundle operations');
|
|
@@ -18,7 +18,7 @@ import * as vm from 'node:vm';
|
|
|
18
18
|
import type { RuleHostInput, RuleHostResult } from '@principles/core/runtime-v2';
|
|
19
19
|
import type { RuleHostHelpers } from '@principles/core/runtime-v2';
|
|
20
20
|
import type { ReplayEvaluateFn } from '@principles/core/runtime-v2';
|
|
21
|
-
import { safeStringifyPreview } from '@principles/core/runtime-v2';
|
|
21
|
+
import { safeStringifyPreview, validateCorrectionProposal } from '@principles/core/runtime-v2';
|
|
22
22
|
|
|
23
23
|
function normalizeSource(sourceCode: string): string {
|
|
24
24
|
const withoutExports = sourceCode
|
|
@@ -32,6 +32,29 @@ globalThis.__pdRuleModule = {
|
|
|
32
32
|
};`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRuleEvaluator(value: unknown): value is (input: RuleHostInput, helpers: RuleHostHelpers) => unknown {
|
|
40
|
+
return typeof value === 'function';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRuleHostResult(value: unknown): value is RuleHostResult {
|
|
44
|
+
if (!isRecord(value)) return false;
|
|
45
|
+
const decision = Object.hasOwn(value, 'decision') ? value.decision : undefined;
|
|
46
|
+
const matched = Object.hasOwn(value, 'matched') ? value.matched : undefined;
|
|
47
|
+
const reason = Object.hasOwn(value, 'reason') ? value.reason : undefined;
|
|
48
|
+
if (decision !== 'allow' && decision !== 'block' && decision !== 'requireApproval' && decision !== 'auto_correct') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (typeof matched !== 'boolean' || typeof reason !== 'string') return false;
|
|
52
|
+
if (decision === 'auto_correct') {
|
|
53
|
+
const proposal = Object.hasOwn(value, 'correctionProposal') ? value.correctionProposal : undefined;
|
|
54
|
+
return validateCorrectionProposal(proposal).valid;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
35
58
|
/**
|
|
36
59
|
* Compile rule implementation code and return a typed evaluate function.
|
|
37
60
|
* Mirrors `createReplayEvaluateFromCode` in openclaw-plugin.
|
|
@@ -40,30 +63,27 @@ globalThis.__pdRuleModule = {
|
|
|
40
63
|
*/
|
|
41
64
|
export function compileDemoRule(code: string, sourceLabel: string): ReplayEvaluateFn {
|
|
42
65
|
const context = vm.createContext(Object.create(null));
|
|
43
|
-
const script = new vm.Script(normalizeSource(code), {
|
|
66
|
+
const script = new vm.Script(normalizeSource(code), {
|
|
67
|
+
filename: sourceLabel,
|
|
68
|
+
});
|
|
44
69
|
|
|
45
70
|
script.runInContext(context, { timeout: 1000, displayErrors: true });
|
|
46
71
|
|
|
47
|
-
const moduleExports = (context as { __pdRuleModule?: { meta?: unknown; evaluate?: unknown } })
|
|
48
|
-
.__pdRuleModule;
|
|
72
|
+
const moduleExports = (context as { __pdRuleModule?: { meta?: unknown; evaluate?: unknown } }).__pdRuleModule;
|
|
49
73
|
delete (context as { __pdRuleModule?: unknown }).__pdRuleModule;
|
|
50
74
|
|
|
51
|
-
if (!moduleExports ||
|
|
52
|
-
throw new Error(
|
|
53
|
-
`[compileDemoRule] ${sourceLabel}: compiled module has no evaluate function`,
|
|
54
|
-
);
|
|
75
|
+
if (!moduleExports || !isRuleEvaluator(moduleExports.evaluate)) {
|
|
76
|
+
throw new Error(`[compileDemoRule] ${sourceLabel}: compiled module has no evaluate function`);
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
const evaluateFn = moduleExports.evaluate
|
|
58
|
-
input: RuleHostInput,
|
|
59
|
-
helpers: RuleHostHelpers,
|
|
60
|
-
) => RuleHostResult;
|
|
61
|
-
|
|
79
|
+
const evaluateFn = moduleExports.evaluate;
|
|
62
80
|
return (input: RuleHostInput, helpers: RuleHostHelpers): RuleHostResult => {
|
|
63
81
|
const result = evaluateFn(input, helpers);
|
|
64
|
-
if (
|
|
82
|
+
if (!isRuleHostResult(result)) {
|
|
65
83
|
throw new Error(
|
|
66
|
-
`[${sourceLabel}]: evaluate returned invalid RuleHostResult (got ${
|
|
84
|
+
`[${sourceLabel}]: evaluate returned invalid RuleHostResult (got ${
|
|
85
|
+
typeof result === 'object' && result !== null ? safeStringifyPreview(result) : String(result)
|
|
86
|
+
})`,
|
|
67
87
|
);
|
|
68
88
|
}
|
|
69
89
|
return result;
|