@principles/pd-cli 1.149.0 → 1.149.2
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/evolution-tasks-list.d.ts.map +1 -1
- package/dist/commands/evolution-tasks-list.js +15 -12
- package/dist/commands/evolution-tasks-list.js.map +1 -1
- package/dist/commands/evolution-tasks-show.d.ts.map +1 -1
- package/dist/commands/evolution-tasks-show.js +7 -4
- package/dist/commands/evolution-tasks-show.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +3 -1
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts +6 -0
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +78 -29
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +29 -2
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/samples-list.d.ts.map +1 -1
- package/dist/commands/samples-list.js +5 -9
- package/dist/commands/samples-list.js.map +1 -1
- package/dist/commands/samples-review.d.ts.map +1 -1
- package/dist/commands/samples-review.js +5 -1
- package/dist/commands/samples-review.js.map +1 -1
- package/dist/commands/trajectory-db-unavailable.d.ts +11 -0
- package/dist/commands/trajectory-db-unavailable.d.ts.map +1 -0
- package/dist/commands/trajectory-db-unavailable.js +15 -0
- package/dist/commands/trajectory-db-unavailable.js.map +1 -0
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +18 -3
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/evolution-tasks-list.ts +18 -14
- package/src/commands/evolution-tasks-show.ts +7 -5
- package/src/commands/pain-evidence.ts +3 -1
- package/src/commands/pain-record.ts +82 -29
- package/src/commands/runtime-internalization-run-once.ts +30 -3
- package/src/commands/samples-list.ts +5 -9
- package/src/commands/samples-review.ts +5 -1
- package/src/commands/trajectory-db-unavailable.ts +19 -0
- package/src/index.ts +12 -3
- package/src/services/rulehost-pipeline-runner.ts +18 -2
- package/tests/commands/evolution-tasks-list.test.ts +41 -0
- package/tests/commands/evolution-tasks-show.test.ts +46 -0
- package/tests/commands/pain-record-session-parser.test.ts +25 -1
- package/tests/commands/pain-record.test.ts +68 -9
- package/tests/commands/runtime-activation.test.ts +1 -1
- package/tests/commands/runtime-internalization-run-once.test.ts +8 -1
- package/tests/commands/samples-list.test.ts +43 -0
- package/tests/commands/samples-review.test.ts +41 -0
|
@@ -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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
153
|
-
// the bound correlation
|
|
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:
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
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
|
|
201
|
-
if (opts.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
?
|
|
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
|
|
|
@@ -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
|
-
{
|
|
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
|
);
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Usage: pd samples list [--status pending|approved|rejected]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { listCorrectionSamples } from '@principles/core/trajectory-store';
|
|
7
|
+
import { listCorrectionSamples, TrajectoryDbUnavailableError } from '@principles/core/trajectory-store';
|
|
8
8
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
9
|
+
import { exitWithTrajectoryDbUnavailable } from './trajectory-db-unavailable.js';
|
|
9
10
|
|
|
10
11
|
interface SamplesListOptions {
|
|
11
12
|
status?: 'pending' | 'approved' | 'rejected';
|
|
@@ -15,19 +16,14 @@ export async function handleSamplesList(opts: SamplesListOptions): Promise<void>
|
|
|
15
16
|
const workspaceDir = resolveWorkspaceDir();
|
|
16
17
|
const status = opts.status ?? 'pending';
|
|
17
18
|
|
|
18
|
-
|
|
19
19
|
let samples;
|
|
20
20
|
try {
|
|
21
21
|
samples = listCorrectionSamples(workspaceDir, status);
|
|
22
22
|
} catch (err) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
console.log('No correction samples found.');
|
|
26
|
-
} else {
|
|
27
|
-
console.error('Failed to list samples:', message);
|
|
28
|
-
throw err;
|
|
23
|
+
if (err instanceof TrajectoryDbUnavailableError) {
|
|
24
|
+
return exitWithTrajectoryDbUnavailable(err);
|
|
29
25
|
}
|
|
30
|
-
|
|
26
|
+
throw err;
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
if (samples.length === 0) {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Usage: pd samples review <sample-id> approve|reject [note]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { reviewCorrectionSample } from '@principles/core/trajectory-store';
|
|
7
|
+
import { reviewCorrectionSample, TrajectoryDbUnavailableError } from '@principles/core/trajectory-store';
|
|
8
8
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
9
|
+
import { exitWithTrajectoryDbUnavailable } from './trajectory-db-unavailable.js';
|
|
9
10
|
|
|
10
11
|
type ReviewDecision = 'approved' | 'rejected';
|
|
11
12
|
|
|
@@ -21,6 +22,9 @@ export async function handleSamplesReview(opts: SamplesReviewOptions): Promise<v
|
|
|
21
22
|
try {
|
|
22
23
|
reviewCorrectionSample(opts.sampleId, opts.decision, opts.note, workspaceDir);
|
|
23
24
|
} catch (err) {
|
|
25
|
+
if (err instanceof TrajectoryDbUnavailableError) {
|
|
26
|
+
return exitWithTrajectoryDbUnavailable(err);
|
|
27
|
+
}
|
|
24
28
|
if (err instanceof Error && err.message.includes('Sample not found')) {
|
|
25
29
|
console.error(`Error: Sample not found: ${opts.sampleId}`);
|
|
26
30
|
process.exit(1);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared operator-facing failure for an unavailable trajectory database (PRI-753).
|
|
3
|
+
*
|
|
4
|
+
* The four trajectory-reading commands (`pd samples list/review`,
|
|
5
|
+
* `pd evolution tasks list/show`) surface the same structured failure:
|
|
6
|
+
* the error message carries path + reason, the second line is the next
|
|
7
|
+
* action (cli-6), and the process exits before any further mutation (cli-5).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TrajectoryDbUnavailableError } from '@principles/core/trajectory-store';
|
|
11
|
+
|
|
12
|
+
const NEXT_ACTION =
|
|
13
|
+
"Trajectory data lives in the workspace database written by the PD plugin. Initialize this workspace with 'pd runtime init --confirm' or run PD here first.";
|
|
14
|
+
|
|
15
|
+
export function exitWithTrajectoryDbUnavailable(err: TrajectoryDbUnavailableError): never {
|
|
16
|
+
console.error(`Error: ${err.message}`);
|
|
17
|
+
console.error(NEXT_ACTION);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
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
|
|
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
|
|
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')
|
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd evolution tasks list command tests — PRI-753 (Trajectory Reader Reality Fix).
|
|
3
|
+
*
|
|
4
|
+
* Before PRI-753 this handler wrapped every error as "No evolution tasks
|
|
5
|
+
* found." — including a missing database and real database failures. A
|
|
6
|
+
* missing trajectory database must now fail loud with reason and next action
|
|
7
|
+
* (cli-6), never masquerade as an empty task list (rc-9).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
|
|
11
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
12
|
+
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/pri753-missing-workspace'),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { handleEvolutionTasksList } from '../../src/commands/evolution-tasks-list.js';
|
|
16
|
+
|
|
17
|
+
describe('handleEvolutionTasksList — unavailable database (PRI-753)', () => {
|
|
18
|
+
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
19
|
+
let exitSpy: ReturnType<typeof vi.spyOn>;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
23
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code: number) => {
|
|
24
|
+
throw new Error(`process.exit:${String(code)}`);
|
|
25
|
+
}) as never);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('fails loud with reason and next action when trajectory.db is missing', async () => {
|
|
33
|
+
await expect(handleEvolutionTasksList({ status: 'all' })).rejects.toThrow('process.exit:1');
|
|
34
|
+
|
|
35
|
+
const stderrText = errorSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
36
|
+
expect(stderrText).toContain('Trajectory database unavailable');
|
|
37
|
+
expect(stderrText).toContain('trajectory.db');
|
|
38
|
+
expect(stderrText).toContain('pd runtime init --confirm');
|
|
39
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd evolution tasks show command tests — PRI-753 (Trajectory Reader Reality Fix).
|
|
3
|
+
*
|
|
4
|
+
* "Task not found" (database present, row absent) and "database unavailable"
|
|
5
|
+
* (file missing) are different failures and must stay distinguishable (cli-6).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
|
|
9
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
10
|
+
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/pri753-missing-workspace'),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { handleEvolutionTasksShow } from '../../src/commands/evolution-tasks-show.js';
|
|
14
|
+
|
|
15
|
+
describe('handleEvolutionTasksShow — unavailable database (PRI-753)', () => {
|
|
16
|
+
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
17
|
+
let exitSpy: ReturnType<typeof vi.spyOn>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
21
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code: number) => {
|
|
22
|
+
throw new Error(`process.exit:${String(code)}`);
|
|
23
|
+
}) as never);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('fails loud with reason and next action when trajectory.db is missing', async () => {
|
|
31
|
+
await expect(handleEvolutionTasksShow({ id: 'task-001' })).rejects.toThrow('process.exit:1');
|
|
32
|
+
|
|
33
|
+
const stderrText = errorSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
34
|
+
expect(stderrText).toContain('Trajectory database unavailable');
|
|
35
|
+
expect(stderrText).toContain('trajectory.db');
|
|
36
|
+
expect(stderrText).toContain('pd runtime init --confirm');
|
|
37
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reports unavailable instead of "Task not found" when the database is missing', async () => {
|
|
41
|
+
await expect(handleEvolutionTasksShow({ id: 'task-001' })).rejects.toThrow('process.exit:1');
|
|
42
|
+
|
|
43
|
+
const stderrText = errorSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
44
|
+
expect(stderrText).not.toContain('Task not found');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -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
|
|
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(
|
|
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
|
|
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({
|
|
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: '
|
|
433
|
-
expect(jsonOutput.
|
|
434
|
-
|
|
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:
|
|
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(
|
|
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,
|