@principles/pd-cli 1.148.0 → 1.149.1

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 (33) hide show
  1. package/dist/commands/errors-list.d.ts.map +1 -1
  2. package/dist/commands/errors-list.js +9 -10
  3. package/dist/commands/errors-list.js.map +1 -1
  4. package/dist/commands/pain-record.d.ts +6 -0
  5. package/dist/commands/pain-record.d.ts.map +1 -1
  6. package/dist/commands/pain-record.js +78 -29
  7. package/dist/commands/pain-record.js.map +1 -1
  8. package/dist/commands/runtime-init.d.ts +3 -1
  9. package/dist/commands/runtime-init.d.ts.map +1 -1
  10. package/dist/commands/runtime-init.js +7 -47
  11. package/dist/commands/runtime-init.js.map +1 -1
  12. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  13. package/dist/commands/runtime-internalization-run-once.js +29 -2
  14. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  15. package/dist/index.js +13 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  18. package/dist/services/rulehost-pipeline-runner.js +18 -3
  19. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/commands/errors-list.ts +14 -13
  22. package/src/commands/pain-record.ts +82 -29
  23. package/src/commands/runtime-init.ts +7 -45
  24. package/src/commands/runtime-internalization-run-once.ts +30 -3
  25. package/src/index.ts +13 -4
  26. package/src/services/rulehost-pipeline-runner.ts +18 -2
  27. package/tests/commands/errors-list.test.ts +5 -4
  28. package/tests/commands/pain-record-session-parser.test.ts +25 -1
  29. package/tests/commands/pain-record.test.ts +68 -9
  30. package/tests/commands/runtime-activation.test.ts +1 -1
  31. package/tests/commands/runtime-init-empty-workspace.test.ts +5 -13
  32. package/tests/commands/runtime-init.test.ts +6 -32
  33. package/tests/commands/runtime-internalization-run-once.test.ts +8 -1
@@ -11,16 +11,20 @@
11
11
  * workspace's trajectory.db, and collect raw evidence entries.
12
12
  *
13
13
  * Usage:
14
- * pd pain record --reason <text> [--score N] [--source manual] [--workspace <path>] [--session <id>] [--host openclaw|codex] [--json]
14
+ * pd pain record --reason <text> [--score N] [--source manual] [--workspace <path>] [--session <id>] [--host openclaw|codex] [--rollout-id <id> --host-turn-id <id>] [--json]
15
15
  *
16
16
  * PRI-743: `--host` makes host attribution explicit instead of assumed.
17
17
  * `--host openclaw` records the default attribution without the disclosure
18
- * warning; `--host codex` refuses loudly — the CLI can verify OpenClaw
19
- * trajectory sessions but can never verify a Codex lineage tuple
20
- * (rolloutIdentity / logicalObservationKey, rc-6), so codex pains must come
21
- * from the Codex ingestion path, never from the CLI. The correlation schema
22
- * itself is NOT changed — PainIngressCorrelationV1 bound branches only admit
23
- * openclaw/codex.
18
+ * warning. `--host codex` requires the caller to supply the REAL Codex
19
+ * lineage — `--rollout-id` / `--host-turn-id`, with `--session` as the Codex
20
+ * root session — because the shared ingress evaluator only writes
21
+ * `host_kind=codex` for a bound correlation carrying the complete lineage
22
+ * tuple (rc-6). The CLI never synthesises that tuple: an unverifiable Codex
23
+ * attribution would be a PRI-642 recurrence in another shape (Evidence Over
24
+ * Assumption). `--logical-key` overrides the derived observation key; a key
25
+ * that does not embed the given rollout identity is refused by the evaluator
26
+ * (lineage_mismatch). The correlation schema itself is NOT changed —
27
+ * PainIngressCorrelationV1 bound branches only admit openclaw/codex.
24
28
  */
25
29
  import {
26
30
  PainToPrincipleService,
@@ -31,7 +35,7 @@ import {
31
35
  isBuiltinPiAiProvider,
32
36
  evaluatePainIngress,
33
37
  } from '@principles/core/runtime-v2';
34
- import type { PainIngressDecision, IngressEvidenceEntry, PainEvidenceEntry } from '@principles/core/runtime-v2';
38
+ import type { PainIngressDecision, IngressEvidenceEntry, PainEvidenceEntry, PainCorrelation } from '@principles/core/runtime-v2';
35
39
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
36
40
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
37
41
  import { acquireTrajectoryEvidenceFromDb } from './build-trajectory-evidence.js';
@@ -46,6 +50,12 @@ interface RecordOptions {
46
50
  wait?: boolean;
47
51
  /** PRI-743: explicit host attribution for a bound --session pain. */
48
52
  host?: string;
53
+ /** PRI-743 follow-up: Codex rollout identity — required by --host codex. */
54
+ rolloutId?: string;
55
+ /** PRI-743 follow-up: Codex turn id — required by --host codex. */
56
+ hostTurnId?: string;
57
+ /** PRI-743 follow-up: Codex logical observation key override for --host codex. */
58
+ logicalKey?: string;
49
59
  }
50
60
 
51
61
  function emitSessionBindingFailure(
@@ -107,6 +117,36 @@ function resolveIngressDecision(
107
117
  score,
108
118
  };
109
119
 
120
+ // PRI-743 follow-up: an explicit Codex attribution. The lineage ids are the
121
+ // caller's (presence validated in handlePainRecord); the logical observation
122
+ // key is derived with the SAME rule the Codex ingestion path uses
123
+ // (codex-adapter admission.ts buildLiveCorrectionCandidate →
124
+ // `codex|<rolloutIdentity>|<turnId>|user`) unless --logical-key overrides it.
125
+ // The CLI cannot read the Codex trajectory (that is the ingestion path's
126
+ // job), so the evidence class is honestly 'unavailable': the shared
127
+ // evaluator takes its bound+unavailable 'degrade' branch and submits the
128
+ // pain with real lineage and empty evidence rather than fabricating either.
129
+ if (opts.host === 'codex') {
130
+ const rolloutIdentity = opts.rolloutId as string;
131
+ const hostTurnId = opts.hostTurnId as string;
132
+ const rootSessionId = opts.session as string;
133
+ const correlation: PainCorrelation = {
134
+ status: 'bound',
135
+ hostKind: 'codex',
136
+ rootSessionId,
137
+ rolloutIdentity,
138
+ logicalObservationKey: opts.logicalKey ?? `codex|${rolloutIdentity}|${hostTurnId}|user`,
139
+ hostTurnId,
140
+ };
141
+ const decision = evaluatePainIngress({
142
+ ...base,
143
+ origin: { kind: 'owner_manual', channel: 'cli_explicit_session' },
144
+ correlation,
145
+ evidence: { status: 'unavailable', reason: 'trajectory_unavailable' },
146
+ });
147
+ return { decision, acquisitionDetail: null, acquisitionReason: null };
148
+ }
149
+
110
150
  if (!opts.session) {
111
151
  // No --session: external unbound Owner report (matrix row 6). Allowed
112
152
  // by SPEC §7.4 with a disclosure warning; never claims host binding.
@@ -149,8 +189,8 @@ function resolveIngressDecision(
149
189
  // PRI-743: host attribution is explicit when --host openclaw is given;
150
190
  // absent --host keeps the pre-PRI-743 'openclaw' default (validated and
151
191
  // disclosed by handlePainRecord — rc-9). --host codex never reaches this
152
- // point: it is refused before any mutation (no verifiable lineage), so
153
- // the bound correlation below is always the openclaw shape.
192
+ // point — it returns from its own branch above and must not read the
193
+ // OpenClaw trajectory — so the bound correlation here is the openclaw shape.
154
194
  const hostKind = 'openclaw' as const;
155
195
  const decision = evaluatePainIngress({
156
196
  ...base,
@@ -192,30 +232,40 @@ export async function handlePainRecord(opts: RecordOptions): Promise<void> {
192
232
  return; // guard: test stubs of process.exit continue execution (cli-2-exit-stops)
193
233
  }
194
234
 
195
- // PRI-743: a CLI-claimed codex attribution would be a PRI-642 recurrence in
196
- // another shape — the shared ingress evaluator requires the rollout lineage
197
- // tuple (rc-6) that only the Codex ingestion path can produce, and the CLI
198
- // owns no Codex identity. Refuse before any mutation instead of faking it.
235
+ // PRI-743 follow-up: --host codex must carry the REAL Codex lineage. The
236
+ // shared ingress evaluator only attributes `host_kind=codex` to a bound
237
+ // correlation holding the complete tuple (rootSessionId / rolloutIdentity /
238
+ // logicalObservationKey / hostTurnId — rc-6); the CLI never synthesises that
239
+ // tuple, because an unverifiable Codex attribution would itself be the
240
+ // misattribution PRI-743 exists to remove. Missing lineage fails loud before
241
+ // any mutation (cli-5/cli-6) — the caller owns those ids.
199
242
  if (opts.host === 'codex') {
200
- const message = 'the CLI cannot verify Codex lineage (rolloutIdentity / logicalObservationKey) — codex pains are produced by the Codex ingestion path, not by pd pain record';
201
- if (opts.json) {
202
- console.log(JSON.stringify({
203
- status: 'failed',
204
- reason: 'codex_lineage_unverifiable_by_cli',
205
- message,
206
- nextAction: 'Record Codex pains through the Codex ingestion path (pd codex setup enables it), or use --host openclaw for OpenClaw-attributed sessions.',
207
- }, null, 2));
208
- } else {
209
- console.error(`Error: ${message}`);
210
- console.error('Next action: record Codex pains through the Codex ingestion path (pd codex setup enables it), or use --host openclaw for OpenClaw-attributed sessions.');
243
+ const missing: string[] = [];
244
+ if (!opts.session) missing.push('--session <codex root session id>');
245
+ if (!opts.rolloutId) missing.push('--rollout-id <codex rollout identity>');
246
+ if (!opts.hostTurnId) missing.push('--host-turn-id <codex turn id>');
247
+ if (missing.length > 0) {
248
+ const message = `--host codex requires the real Codex lineage — missing ${missing.join(', ')}`;
249
+ const nextAction = 'Pass --session <root session id> --rollout-id <rollout identity> --host-turn-id <turn id> so the pain carries verifiable Codex lineage, or use --host openclaw for OpenClaw-attributed sessions.';
250
+ if (opts.json) {
251
+ console.log(JSON.stringify({
252
+ status: 'failed',
253
+ reason: 'codex_lineage_required',
254
+ message,
255
+ nextAction,
256
+ }, null, 2));
257
+ } else {
258
+ console.error(`Error: ${message}`);
259
+ console.error(`Next action: ${nextAction}`);
260
+ }
261
+ process.exit(1);
262
+ return; // guard: test stubs of process.exit continue execution (cli-2-exit-stops)
211
263
  }
212
- process.exit(1);
213
- return; // guard: test stubs of process.exit continue execution (cli-2-exit-stops)
214
264
  }
215
265
 
216
266
  if (!opts.reason) {
217
267
  console.error('Error: --reason <text> is required');
218
- console.error('Usage: pd pain record --reason <text> [--score N] [--source manual] [--workspace <path>] [--session <id>] [--host openclaw|codex] [--json]');
268
+ console.error('Usage: pd pain record --reason <text> [--score N] [--source manual] [--workspace <path>] [--session <id>] [--host openclaw|codex] [--rollout-id <id> --host-turn-id <id>] [--json]');
219
269
  process.exit(1);
220
270
  return;
221
271
  }
@@ -422,8 +472,11 @@ export async function handlePainRecord(opts: RecordOptions): Promise<void> {
422
472
  // PRI-743: the pre-existing 'openclaw' default is an assumption, not
423
473
  // evidence — disclose it whenever it actually applies (rc-9), so host
424
474
  // attribution can never silently distort PRI-743-style host comparisons.
475
+ // Unbound reports (no --session) keep host_kind NULL: they carry no host
476
+ // trace at all, so labelling them 'openclaw' would be a fresh misattribution
477
+ // rather than a compatibility default.
425
478
  const hostDefaultWarning = binding.sessionId !== undefined && opts.host === undefined
426
- ? `host attribution defaulted to 'openclaw' (--host not given); pass --host codex to attribute this pain to the Codex host.`
479
+ ? 'host kind not specified, defaulting to openclaw; pass --host codex with --rollout-id/--host-turn-id to attribute this pain to the Codex host.'
427
480
  : null;
428
481
  if (hostDefaultWarning !== null) cliWarnings.push(hostDefaultWarning);
429
482
 
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * pd runtime init — Initialize all PD SQLite databases for a workspace.
3
3
  *
4
- * Creates state.db, trajectory.db, and subagent_workflows.db with full schema
4
+ * Creates state.db and trajectory.db with full schema
5
5
  * (tables + indexes + views + migrations). Idempotent: safe to run on existing
6
6
  * workspaces (all CREATE statements use IF NOT EXISTS).
7
+ * (subagent_workflows.db initialization removed in PRI-737 with the legacy
8
+ * evolution worker chain — the workflow store had no remaining producers.)
7
9
  *
8
10
  * Output contract (cli-1 strict-json, cli-4 dry-run/confirm mutex, cli-6 nextAction):
9
11
  * - Default mode is --dry-run (no writes); use --confirm to actually initialize.
@@ -22,7 +24,7 @@ import * as fs from 'fs';
22
24
  import * as yaml from 'js-yaml';
23
25
  import { SqliteConnection, SchemaConformanceReadModel } from '@principles/core/runtime-v2';
24
26
  import { getDefaultPdConfig, validatePdConfig } from '@principles/core/runtime-v2';
25
- import { initTrajectorySchema, initWorkflowSchema } from 'principles-disciple';
27
+ import { initTrajectorySchema } from 'principles-disciple';
26
28
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
27
29
  import { emitResult, emitFlagConflict, emitError } from '../services/cli-output.js';
28
30
 
@@ -82,7 +84,6 @@ export interface RuntimeInitOutput {
82
84
  const DB_NAMES = {
83
85
  state: 'state.db',
84
86
  trajectory: 'trajectory.db',
85
- workflow: 'subagent_workflows.db',
86
87
  } as const;
87
88
 
88
89
  // ── Config file generation ─────────────────────────────────────────────────────
@@ -290,48 +291,9 @@ export function buildRuntimeInitOutput(workspaceDir: string, confirm: boolean):
290
291
  }
291
292
  }
292
293
 
293
- // 3. subagent_workflows.db (via initWorkflowSchema)
294
- if (!confirm) {
295
- databases.push({
296
- name: DB_NAMES.workflow,
297
- path: path.join(resolvedWorkspace, '.state', 'subagent_workflows.db'),
298
- tables: ['schema_version', 'subagent_workflows', 'subagent_workflow_events'],
299
- status: 'skipped',
300
- warnings: [],
301
- });
302
- } else {
303
- try {
304
- const result = initWorkflowSchema(resolvedWorkspace);
305
- if (result.warnings.length > 0) {
306
- warnings.push(...result.warnings.map((w: string) => `${DB_NAMES.workflow}: ${w}`));
307
- }
308
- databases.push({
309
- name: DB_NAMES.workflow,
310
- path: path.join(resolvedWorkspace, '.state', 'subagent_workflows.db'),
311
- tables: result.tables,
312
- status: 'initialized',
313
- warnings: result.warnings,
314
- });
315
- } catch (err) {
316
- const reason = err instanceof Error ? err.message : String(err);
317
- databases.push({
318
- name: DB_NAMES.workflow,
319
- path: path.join(resolvedWorkspace, '.state', 'subagent_workflows.db'),
320
- tables: [],
321
- status: 'failed',
322
- warnings: [reason],
323
- });
324
- return {
325
- ok: false,
326
- mode: 'confirm',
327
- workspace: resolvedWorkspace,
328
- databases,
329
- warnings,
330
- reason: `subagent_workflows.db initialization failed: ${reason}`,
331
- nextAction: 'Check workspace directory permissions and disk space, then retry.',
332
- };
333
- }
334
- }
294
+ // 3. subagent_workflows.db — no longer initialized (PRI-737): the legacy
295
+ // workflow store had zero producers and the worker chain is retired.
296
+ // Existing legacy databases in old workspaces are left untouched.
335
297
 
336
298
  // Verify state.db schema conformance after initialization (confirm mode only)
337
299
  if (confirm) {
@@ -17,8 +17,8 @@ import {
17
17
  DefaultRolloutReviewerValidator,
18
18
  TestDoubleRuntimeAdapter,
19
19
  } from '@principles/core/runtime-v2';
20
- import type { WakeOnceResult, DreamerRunnerResult, PhilosopherRunnerResult, ScribeRunnerResult, ArtificerRunnerResult, EvaluatorRunnerResult, RolloutReviewerRunnerResult, PDRuntimeAdapter, PeerRunnerKind, OutputLanguage } from '@principles/core/runtime-v2';
21
- import { resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError } from '@principles/core/runtime-v2';
20
+ import type { WakeOnceResult, DreamerRunnerResult, PhilosopherRunnerResult, ScribeRunnerResult, ArtificerRunnerResult, EvaluatorRunnerResult, RolloutReviewerRunnerResult, PDRuntimeAdapter, PeerRunnerKind, OutputLanguage, ArtificerHostSemanticContext } from '@principles/core/runtime-v2';
21
+ import { resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, buildArtificerHostSemanticContext } from '@principles/core/runtime-v2';
22
22
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
23
23
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
24
24
  import { loadPdConfig } from '../services/pd-config-loader.js';
@@ -463,6 +463,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
463
463
  // BEFORE leasing any task (cli-5: no mutation on refusal) with the same
464
464
  // structured semantics as runtime-activation's code_tool_hook refusal.
465
465
  let evaluatorGateDeps: RefinerRuleHostGateDeps | undefined;
466
+ let evaluatorHostSemanticContext: ArtificerHostSemanticContext | undefined;
466
467
  if (runnerKind === 'evaluator') {
467
468
  const evaluatorContext = createEvaluatorRuntimeContext({ workspaceDir });
468
469
  if (!evaluatorContext.ok) {
@@ -482,6 +483,11 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
482
483
  return;
483
484
  }
484
485
  evaluatorGateDeps = evaluatorContext.gateDeps;
486
+ // PRI-741 (review round): the projection derives from the SAME registry
487
+ // snapshot the gateDeps were built from — never a second independent read
488
+ // of the durable declaration (snapshot divergence would let the sandbox
489
+ // and the parity case disagree on canonical kinds).
490
+ evaluatorHostSemanticContext = buildArtificerHostSemanticContext(evaluatorContext.registry, evaluatorContext.hostKinds ?? []);
485
491
  }
486
492
 
487
493
  const stateManager = new RuntimeStateManager({ workspaceDir });
@@ -593,9 +599,27 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
593
599
  runnerResult = await runner.run(wakeResult.taskId);
594
600
  } else if (runnerKind === 'artificer') {
595
601
  const validator = new DefaultArtificerValidator();
602
+ // PRI-741: artificer generation anchors on the workspace host
603
+ // declaration (same durable provenance the evaluator gate uses).
604
+ // Unresolvable provenance degrades OBSERVABLY (rc-9/EP-03) to a
605
+ // prompt without the host block — never a silent wrong-host guess.
606
+ const hostSemantics = resolveWorkspaceHostToolSemantics(workspaceDir);
607
+ if (!hostSemantics.ok) {
608
+ console.error(`[PD:run-once] artificer prompt without host tool semantics: ${hostSemantics.reason} — ${hostSemantics.nextAction}`);
609
+ }
596
610
  const runner = new ArtificerRunner(
597
611
  { stateManager, runtimeAdapter, eventEmitter, validator, artifactStore, contentHashFn },
598
- { owner: OWNER, runtimeKind: runtimeAdapter.kind(), pollIntervalMs: 100, timeoutMs: effectiveTimeoutMs, effectiveConfig, outputLanguage },
612
+ {
613
+ owner: OWNER,
614
+ runtimeKind: runtimeAdapter.kind(),
615
+ pollIntervalMs: 100,
616
+ timeoutMs: effectiveTimeoutMs,
617
+ effectiveConfig,
618
+ outputLanguage,
619
+ ...(hostSemantics.ok
620
+ ? { hostSemanticContext: buildArtificerHostSemanticContext(hostSemantics.registry, hostSemantics.hostKinds) }
621
+ : {}),
622
+ },
599
623
  );
600
624
  runnerResult = await runner.run(wakeResult.taskId);
601
625
  } else if (runnerKind === 'evaluator') {
@@ -629,6 +653,9 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
629
653
  timeoutMs: effectiveTimeoutMs,
630
654
  effectiveConfig,
631
655
  gateDeps: evaluatorGateDeps,
656
+ // PRI-741: host-name parity replay case (same provenance as
657
+ // gateDeps; undefined = observable skip event in the runner).
658
+ ...(evaluatorHostSemanticContext !== undefined ? { hostSemanticContext: evaluatorHostSemanticContext } : {}),
632
659
  outputLanguage,
633
660
  },
634
661
  );
package/src/index.ts CHANGED
@@ -105,7 +105,7 @@ function handleVersionFlag(args: readonly string[]): boolean {
105
105
 
106
106
  program
107
107
  .name('pd')
108
- .description('PD CLI — Pain recording, sample management, and evolution tasks')
108
+ .description('PD CLI — Pain recording, sample management, and evolution task history')
109
109
  .option('-V, --version', 'output the canonical PD product version')
110
110
  .enablePositionalOptions();
111
111
 
@@ -123,7 +123,10 @@ painCmd
123
123
  .option('-S, --source <text>', 'Source of the pain signal', 'manual')
124
124
  .option('-w, --workspace <path>', 'Workspace directory')
125
125
  .option('--session <id>', 'Session ID to bind (validated against this workspace\'s trajectory.db; without it the record is unbound: no trajectory evidence, candidates likely gated by the admission threshold)')
126
- .option('--host <kind>', 'PRI-743: explicit host attribution (openclaw | codex). openclaw = default attribution without the disclosure warning; codex refuses — the CLI cannot verify Codex lineage.', undefined)
126
+ .option('--host <kind>', 'PRI-743: explicit host attribution (openclaw | codex). openclaw = default attribution without the disclosure warning; codex requires the real Codex lineage (--rollout-id, --host-turn-id, and --session as the root session).', undefined)
127
+ .option('--rollout-id <id>', 'PRI-743: Codex rollout identity — required by --host codex', undefined)
128
+ .option('--host-turn-id <id>', 'PRI-743: Codex turn id — required by --host codex', undefined)
129
+ .option('--logical-key <key>', 'PRI-743: Codex logical observation key override for --host codex (default: codex|<rollout-id>|<host-turn-id>|user)', undefined)
127
130
  .option('--wait', 'Wait for diagnosis to complete (sync mode, overrides async flag)')
128
131
  .option('--json', 'Output raw JSON')
129
132
  .action(async (opts) => {
@@ -179,9 +182,15 @@ samplesCmd
179
182
  await handleSamplesReview({ sampleId, decision: decision === 'approve' ? 'approved' : 'rejected', note });
180
183
  });
181
184
 
185
+ // ── Evolution task reader (hidden) ────────────────────────────────────────────
186
+ // PRI-752 review: the worker is gone (flag retired to `gone`), but this
187
+ // read-only surface stays for historical `evolution_tasks` rows in already-
188
+ // provisioned workspaces — data-retention disposition is a separate Owner
189
+ // decision (PRI-737 reality audit §7.3). Table schema + quality-scorecard
190
+ // reader are retained accordingly.
182
191
  const evolutionCmd = program
183
192
  .command('evolution', { hidden: true })
184
- .description('Evolution task management');
193
+ .description('Evolution task management (historical data reader)');
185
194
 
186
195
  const tasksCmd = evolutionCmd
187
196
  .command('tasks')
@@ -375,7 +384,7 @@ runtimeCmd
375
384
  // when `pd runtime init` is actually invoked.
376
385
  runtimeCmd
377
386
  .command('init')
378
- .description('Initialize all PD SQLite databases (state.db, trajectory.db, subagent_workflows.db)')
387
+ .description('Initialize all PD SQLite databases (state.db, trajectory.db)')
379
388
  .option('-w, --workspace <path>', 'Workspace directory')
380
389
  .option('--dry-run', 'Show what would be initialized without writing (default)')
381
390
  .option('--confirm', 'Actually initialize the databases (required to write)')
@@ -54,6 +54,7 @@ import {
54
54
  // PRI-714: resolve outputLanguage from the same effective config the
55
55
  // runners already receive (EP-07: canonical resolved value, not raw input).
56
56
  resolveOutputLanguage,
57
+ buildArtificerHostSemanticContext,
57
58
  } from '@principles/core/runtime-v2';
58
59
  import type {
59
60
  AdversarialLoopResult,
@@ -411,12 +412,21 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
411
412
  onProgress('adversarial_loop', 'failed', refusalReason);
412
413
  return rejectedResult(opts.painId, stages, `evaluator_runtime_context_unresolvable: ${evaluatorContext.reason} (nextAction: ${evaluatorContext.nextAction})`);
413
414
  }
415
+ // PRI-741: artificer generation + evaluator parity BOTH anchor on the ONE
416
+ // registry snapshot evaluatorContext resolved above (review round: a
417
+ // second independent resolveWorkspaceHostToolSemantics read could observe
418
+ // a different declaration snapshot than the replay gate). Unresolvable
419
+ // provenance already failed loud at the evaluatorContext refusal above.
420
+ const hostSemanticContext = buildArtificerHostSemanticContext(evaluatorContext.registry, evaluatorContext.hostKinds ?? []);
414
421
  const artificerRunner = new ArtificerRunner(
415
422
  {
416
423
  stateManager, runtimeAdapter: capability.artificerAdapter, eventEmitter, validator: new DefaultArtificerValidator(), artifactStore,
417
424
  contextMode: opts.contextMode ?? 'v1', behaviorExamplePack: opts.behaviorExamplePack, contentHashFn,
418
425
  },
419
- runnerOptsFor(capability.artificerAdapter),
426
+ {
427
+ ...runnerOptsFor(capability.artificerAdapter),
428
+ hostSemanticContext,
429
+ },
420
430
  );
421
431
  // PRI-510 (DEFECT-004): construct EvaluatorRunnerDeps via the centralized
422
432
  // helper so the repair-loop wiring (isRepairLoopEnabled + seeder) is
@@ -435,7 +445,13 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
435
445
  // from durable provenance) — replaces the previous sandbox-only gate
436
446
  // deps so generation-time replay verdicts match the activation gate on
437
447
  // the same workspace. Options argument (2nd), never deps (1st).
438
- { ...runnerOptsFor(agentAdapters.evaluator), gateDeps: evaluatorContext.gateDeps },
448
+ {
449
+ ...runnerOptsFor(agentAdapters.evaluator),
450
+ gateDeps: evaluatorContext.gateDeps,
451
+ // PRI-741: host-name parity replay case from the SAME registry
452
+ // snapshot as the gateDeps above (single resolution).
453
+ hostSemanticContext,
454
+ },
439
455
  );
440
456
 
441
457
  const loopResult = await runAdversarialLoop({
@@ -282,8 +282,9 @@ describe('handleErrorsList — handler behavior', () => {
282
282
  expect(parsed.workerErrors).toEqual([]);
283
283
  expect(parsed.total).toBe(0);
284
284
  expect(parsed.nextAction).toBe('No errors found. PD pipeline is healthy.');
285
- // worker-status.json missing → workerStatusMissing=true
286
- expect(parsed.workerStatusMissing).toBe(true);
285
+ // worker-status.json missing → expected steady state since PRI-737
286
+ // (the writer retired); no workerStatusMissing flag is emitted.
287
+ expect(parsed.workerStatusMissing).toBeUndefined();
287
288
  expect(parsed.workerStatusPath).toContain('worker-status.json');
288
289
  // cli-2-exit-stops: success path does NOT set exitCode to failure (1).
289
290
  // We assert "not 1" rather than "toBe(0)" because vitest may reset
@@ -657,14 +658,14 @@ describe('handleErrorsList — handler behavior', () => {
657
658
  expect(text).toContain('total: 2');
658
659
  });
659
660
 
660
- it('text output: worker-status.json missing is reported in workerStatus line', async () => {
661
+ it('text output: worker-status.json missing is not reported (expected steady state since PRI-737)', async () => {
661
662
  setStateDbExists();
662
663
  mockListFailedTasks.mockResolvedValue([]);
663
664
 
664
665
  await handleErrorsList({ workspace: WS, json: false });
665
666
 
666
667
  const text = consoleLogSpy.mock.calls.map((c) => c.join(' ')).join('\n');
667
- expect(text).toContain('workerStatus: (missing');
668
+ expect(text).not.toContain('workerStatus:');
668
669
  });
669
670
 
670
671
  it('connection.close() called on success path (resource cleanup)', async () => {
@@ -93,7 +93,31 @@ describe('pd pain record --session (real Commander + real trajectory.db)', () =>
93
93
  // Commander wraps help text; normalize whitespace before asserting content.
94
94
  const normalized = result.stdout.replace(/\s+/g, ' ');
95
95
  expect(normalized).toContain('openclaw | codex');
96
- expect(normalized).toContain('codex refuses');
96
+ expect(normalized).toContain('codex requires the real Codex lineage');
97
+ }, 15_000);
98
+
99
+ it('PRI-743: --help registers the Codex lineage flags used by --host codex', async () => {
100
+ const result = await runBuiltCli(['pain', 'record', '--help']);
101
+ expect(result.status).toBe(0);
102
+ expect(result.stdout).toContain('--rollout-id <id>');
103
+ expect(result.stdout).toContain('--host-turn-id <id>');
104
+ expect(result.stdout).toContain('--logical-key <key>');
105
+ }, 15_000);
106
+
107
+ it('PRI-743: --host codex without lineage exits non-zero with codex_lineage_required', async () => {
108
+ const result = await runBuiltCli([
109
+ 'pain', 'record',
110
+ '--reason', 'parser test pain',
111
+ '--host', 'codex',
112
+ '--workspace', tmpDir,
113
+ '--json',
114
+ ]);
115
+
116
+ expect(result.status).not.toBe(0);
117
+ const parsed = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
118
+ expect(parsed.status).toBe('failed');
119
+ expect(parsed.reason).toBe('codex_lineage_required');
120
+ expect(typeof parsed.nextAction).toBe('string');
97
121
  }, 15_000);
98
122
 
99
123
  it('PRI-743: an invalid --host value exits non-zero with a single structured JSON object', async () => {
@@ -414,24 +414,63 @@ describe('pd pain record', () => {
414
414
 
415
415
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { hostAttribution?: string; warnings?: string[] };
416
416
  expect(jsonOutput.hostAttribution).toBe('openclaw');
417
- expect((jsonOutput.warnings ?? []).some((w) => w.includes("defaulted to 'openclaw'"))).toBe(false);
417
+ expect((jsonOutput.warnings ?? []).some((w) => w.includes('host kind not specified, defaulting to openclaw'))).toBe(false);
418
418
 
419
419
  logSpy.mockRestore();
420
420
  exitSpy.mockRestore();
421
421
  });
422
422
 
423
- it('PRI-743: --host codex refuses loudly — the CLI cannot verify Codex lineage (rc-6)', async () => {
423
+ it('PRI-743: --host codex records host_kind=codex with the caller-supplied Codex lineage', async () => {
424
424
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
425
425
  const exitSpy = mockProcessExit();
426
426
 
427
- await handlePainRecord({ reason: 'codex mistake', session: 'sess-codex', host: 'codex', json: true });
427
+ await handlePainRecord({
428
+ reason: 'codex mistake',
429
+ session: 'codex-root-sess',
430
+ host: 'codex',
431
+ rolloutId: 'rollout-abc',
432
+ hostTurnId: 'turn-7',
433
+ json: true,
434
+ });
435
+
436
+ expect(lastRecordPainInput).toBeTruthy();
437
+ expect(lastRecordPainInput!.hostKind).toBe('codex');
438
+ expect(lastRecordPainInput!.sessionId).toBe('codex-root-sess');
439
+ expect(lastRecordPainInput!.provenance).toBe('host_context_bound');
440
+ expect(lastRecordPainInput!.recordObservability).toBe(true);
441
+ // The CLI must NOT read the OpenClaw trajectory for a Codex attribution.
442
+ expect(acquireTrajectoryEvidenceFromDb).not.toHaveBeenCalled();
443
+ // Codex lineage is retained unflattened in the versioned payload (SPEC §8.1);
444
+ // the derived key mirrors the ingestion rule (codex|<rollout>|<turn>|user).
445
+ expect(lastRecordPainInput!.painIngress.correlation).toEqual({
446
+ status: 'bound',
447
+ hostKind: 'codex',
448
+ rootSessionId: 'codex-root-sess',
449
+ rolloutIdentity: 'rollout-abc',
450
+ logicalObservationKey: 'codex|rollout-abc|turn-7|user',
451
+ hostTurnId: 'turn-7',
452
+ });
453
+ // The degraded-but-honest path still submits (no fabricated evidence).
454
+ expect(lastRecordPainInput!.evidence).toEqual([]);
455
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
456
+
457
+ logSpy.mockRestore();
458
+ exitSpy.mockRestore();
459
+ });
460
+
461
+ it('PRI-743: --host codex without the Codex lineage fails loudly before any mutation', async () => {
462
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
463
+ const exitSpy = mockProcessExit();
464
+
465
+ await handlePainRecord({ reason: 'codex mistake', host: 'codex', json: true });
428
466
 
429
467
  // cli-1: exactly one JSON object on stdout
430
468
  expect(logSpy).toHaveBeenCalledTimes(1);
431
- const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; nextAction: string };
432
- expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_unverifiable_by_cli' });
433
- expect(jsonOutput.nextAction).toContain('Codex ingestion');
434
- // cli-2/cli-5: refused before any evidence acquisition or service mutation
469
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; nextAction: string; message: string };
470
+ expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_required' });
471
+ expect(jsonOutput.message).toContain('--rollout-id');
472
+ expect(jsonOutput.nextAction).toContain('--host-turn-id');
473
+ // cli-2/cli-5: stopped before any evidence acquisition or service mutation
435
474
  expect(lastRecordPainInput).toBeNull();
436
475
  expect(acquireTrajectoryEvidenceFromDb).not.toHaveBeenCalled();
437
476
  expect(exitSpy).toHaveBeenCalledWith(1);
@@ -440,7 +479,27 @@ describe('pd pain record', () => {
440
479
  exitSpy.mockRestore();
441
480
  });
442
481
 
443
- it('PRI-743: omitting --host keeps the openclaw default but discloses the assumption (rc-9)', async () => {
482
+ it('PRI-743: --host codex with PARTIAL lineage still fails loudly (only the missing flag is reported)', async () => {
483
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
484
+ const exitSpy = mockProcessExit();
485
+
486
+ await handlePainRecord({ reason: 'codex mistake', session: 'codex-root', host: 'codex', rolloutId: 'rollout-abc', json: true });
487
+
488
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; message: string };
489
+ expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_required' });
490
+ expect(jsonOutput.message).toContain('--host-turn-id');
491
+ expect(jsonOutput.message).not.toContain('--rollout-id');
492
+ expect(lastRecordPainInput).toBeNull();
493
+ expect(exitSpy).toHaveBeenCalledWith(1);
494
+
495
+ logSpy.mockRestore();
496
+ exitSpy.mockRestore();
497
+ });
498
+
499
+ // Task test #3/#4: legacy call compatibility — the pre-PRI-743 invocation
500
+ // (`--reason … --session …`, no --host) keeps host_kind=openclaw and only
501
+ // gains the disclosure wording.
502
+ it('PRI-743: legacy call omitting --host keeps the openclaw default but discloses it (rc-9)', async () => {
444
503
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
445
504
  const exitSpy = mockProcessExit();
446
505
 
@@ -450,7 +509,7 @@ describe('pd pain record', () => {
450
509
  expect(lastRecordPainInput!.hostKind).toBe('openclaw');
451
510
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { hostAttribution?: string; warnings?: string[] };
452
511
  expect(jsonOutput.hostAttribution).toBe('openclaw');
453
- expect((jsonOutput.warnings ?? []).some((w) => w.includes("defaulted to 'openclaw'") && w.includes('--host codex'))).toBe(true);
512
+ expect((jsonOutput.warnings ?? []).some((w) => w.includes('host kind not specified, defaulting to openclaw') && w.includes('--host codex'))).toBe(true);
454
513
 
455
514
  logSpy.mockRestore();
456
515
  exitSpy.mockRestore();
@@ -2035,7 +2035,7 @@ describe('handleRuntimeActivationDispatch — PRI-634-F R2 host provenance refus
2035
2035
  });
2036
2036
 
2037
2037
  it('code_tool_hook dispatch threads the resolved registry into RuleHostWriter and gateDeps', async () => {
2038
- const registry = { version: 1, hasHostLayer: true, resolve: () => 'write', lookup: () => null, hasHostTool: () => true };
2038
+ const registry = { version: 1, hasHostLayer: true, resolve: () => 'write', lookup: () => null, hasHostTool: () => true, hostMappings: () => [{ rawToolName: 'write', canonicalKind: 'write' as const }] };
2039
2039
  vi.mocked(resolveWorkspaceToolSemantics).mockReturnValueOnce({ ok: true, registry, hostKind: 'openclaw' });
2040
2040
  await handleRuntimeActivationDispatch({
2041
2041
  workspace: WS,