create-principles-disciple 1.145.3 → 1.145.4
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/_release/product-identity.json +2 -2
- package/codex-adapter/dist/worker/workspace-worker.js +2 -1
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/config/pd-config-store.js +3 -0
- package/core/dist/runtime-v2/index.d.ts +3 -3
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +8 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +32 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.js +51 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/core/dist/runtime-v2/recovery-actions-log.d.ts +15 -0
- package/core/dist/runtime-v2/recovery-actions-log.d.ts.map +1 -1
- package/core/dist/runtime-v2/recovery-actions-log.js +7 -1
- package/core/dist/runtime-v2/recovery-actions-log.js.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts +116 -1
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.js +252 -13
- package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
- package/core/dist/runtime-v2/runtime-state-handle.d.ts +8 -0
- package/core/dist/runtime-v2/runtime-state-handle.d.ts.map +1 -1
- package/core/dist/runtime-v2/runtime-state-handle.js +1 -0
- package/core/dist/runtime-v2/runtime-state-handle.js.map +1 -1
- package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +10 -0
- package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/runtime-state-manager.js +1 -0
- package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
- package/core/dist/runtime-v2/store/task/memory-task-store.d.ts +4 -0
- package/core/dist/runtime-v2/store/task/memory-task-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/task/memory-task-store.js +10 -0
- package/core/dist/runtime-v2/store/task/memory-task-store.js.map +1 -1
- package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts +4 -0
- package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/task/sqlite-task-store.js +21 -0
- package/core/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
- package/core/dist/runtime-v2/store/task/task-store.d.ts +14 -6
- package/core/dist/runtime-v2/store/task/task-store.d.ts.map +1 -1
- package/core/package.json +1 -1
- package/dist/installer.d.ts +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +5 -1
- package/dist/installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +5 -1
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/dist/manifest-host-union.d.ts +23 -0
- package/dist/manifest-host-union.d.ts.map +1 -0
- package/dist/manifest-host-union.js +22 -0
- package/dist/manifest-host-union.js.map +1 -0
- package/dist/uninstaller.d.ts +2 -1
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +7 -1
- package/dist/uninstaller.js.map +1 -1
- package/host-runtime/dist/behavior-example-pack-assembler.d.ts +41 -0
- package/host-runtime/dist/behavior-example-pack-assembler.js +243 -0
- package/host-runtime/dist/behavior-history.d.ts +6 -0
- package/host-runtime/dist/behavior-history.js +76 -0
- package/host-runtime/dist/index.d.ts +3 -0
- package/host-runtime/dist/index.js +3 -0
- package/host-runtime/dist/workspace-behavior-evidence.d.ts +3 -0
- package/host-runtime/dist/workspace-behavior-evidence.js +74 -0
- package/host-runtime/package.json +10 -2
- package/install-layout/dist/index.d.ts +40 -1
- package/install-layout/dist/index.js +88 -6
- package/install-layout/package.json +1 -1
- package/package.json +3 -3
- package/pd-cli/dist/commands/runtime-internalization-evidence.d.ts +2 -3
- package/pd-cli/dist/commands/runtime-internalization-evidence.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-evidence.js +7 -26
- package/pd-cli/dist/commands/runtime-internalization-evidence.js.map +1 -1
- package/pd-cli/dist/commands/runtime-recovery-failed-task.d.ts +22 -0
- package/pd-cli/dist/commands/runtime-recovery-failed-task.d.ts.map +1 -0
- package/pd-cli/dist/commands/runtime-recovery-failed-task.js +470 -0
- package/pd-cli/dist/commands/runtime-recovery-failed-task.js.map +1 -0
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.d.ts +24 -0
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js +4 -1
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js.map +1 -1
- package/pd-cli/dist/index.js +5 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +501 -497
- package/plugin/dist/core/behavior-example-pack-assembler.d.ts +4 -32
- package/plugin/dist/core/rule-context-assembler.d.ts +0 -9
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +19 -19
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/installer.d.ts +1 -1
- package/release-manager/dist/installer.d.ts.map +1 -1
- package/release-manager/dist/installer.js +5 -1
- package/release-manager/dist/installer.js.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.js +5 -1
- package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/release-manager/dist/manifest-host-union.d.ts +23 -0
- package/release-manager/dist/manifest-host-union.d.ts.map +1 -0
- package/release-manager/dist/manifest-host-union.js +22 -0
- package/release-manager/dist/manifest-host-union.js.map +1 -0
- package/release-manager/dist/uninstaller.d.ts +2 -1
- package/release-manager/dist/uninstaller.d.ts.map +1 -1
- package/release-manager/dist/uninstaller.js +7 -1
- package/release-manager/dist/uninstaller.js.map +1 -1
- package/release-manager/package.json +2 -2
- package/workbuddy-adapter/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Owner-labelled BehaviorExamplePack assembly (PRI-484 / PRI-992).
|
|
3
|
+
*
|
|
4
|
+
* Reads pain lineage + trajectory rows from TrajectoryDatabase and assembles
|
|
5
|
+
* a validated BehaviorExamplePack for the Artificer. Fail loud on any gap
|
|
6
|
+
* (missing pain, empty trajectory, invalid pack) — no silent fallback.
|
|
7
|
+
*
|
|
8
|
+
* Err-prevention:
|
|
9
|
+
* - ERR-001: every DB row field validated as `unknown` (no `as` bypass on
|
|
10
|
+
* parsed params_json or row casts).
|
|
11
|
+
* - ERR-069: Artificer shared schema — invalid pack must fail loud.
|
|
12
|
+
* - ERR-026: reuses production TrajectoryDatabase (no hand-written DDL).
|
|
13
|
+
* - rc-2: structural type guards, no `as T` on parsed input.
|
|
14
|
+
* - rc-5: Object.hasOwn over `in`.
|
|
15
|
+
* - rc-9: every failure path throws a structured Error with a clear reason.
|
|
16
|
+
* - PRI-980 review P2-1: trajectory STORE I/O failures throw the typed
|
|
17
|
+
* `evidence_source_error` (readTrajectoryStore) so the readiness contract
|
|
18
|
+
* preserves them as storage faults, not Owner evidence faults (§6.2).
|
|
19
|
+
*
|
|
20
|
+
* Spec: docs/superpowers/specs/2026-06-27-rulecode-context-vision-design.md §7.2
|
|
21
|
+
*
|
|
22
|
+
* Boundary: this module is I/O. Pure logic (type + validator) lives in
|
|
23
|
+
* `@principles/core/runtime-v2` → `behavior-example-pack.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { buildRuleHostAction } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
|
|
26
|
+
import { computeBehaviorFacts } from '@principles/core/runtime-v2/internalization/rule-context-v2';
|
|
27
|
+
import { validateBehaviorExamplePack } from '@principles/core/runtime-v2/internalization/behavior-example-pack';
|
|
28
|
+
// Factory lives in evidence-readiness.ts (the classification authority); it is
|
|
29
|
+
// consumed through its registered LEAF subpath. A VALUE import of the
|
|
30
|
+
// runtime-v2 barrel on this satellite path re-pulls the whole LLM graph and
|
|
31
|
+
// fails the satellite-purity guard (OPT-002, scripts/build/check-satellite-purity.mjs) —
|
|
32
|
+
// the leaf is on core's exports allowlist for exactly this consumer.
|
|
33
|
+
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
34
|
+
import { assembleBehaviorHistory } from './behavior-history.js';
|
|
35
|
+
// ── internal constants ─────────────────────────────────────────────────────
|
|
36
|
+
const MAX_POSITIVES = 3;
|
|
37
|
+
const MAX_EVIDENCE_REFS = 5;
|
|
38
|
+
const HISTORY_LIMIT = 100;
|
|
39
|
+
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
40
|
+
/**
|
|
41
|
+
* PRI-980 review P2-1 (SPEC-PD-IAER-001 §6.2 case 3): the trajectory STORE
|
|
42
|
+
* throwing is an I/O/storage failure of the evidence source itself — it must
|
|
43
|
+
* surface as the typed `evidence_source_error` kind so the readiness contract
|
|
44
|
+
* preserves it as a storage fault instead of misreporting "Owner labelled bad
|
|
45
|
+
* evidence". Structural, never message sniffing: the store RETURNING no row
|
|
46
|
+
* is a data gap (the typed scope kinds below); the store THROWING is the I/O
|
|
47
|
+
* failure signal. Assembler-owned data-shape validation errors stay plain
|
|
48
|
+
* Errors (→ `invalid`), unchanged.
|
|
49
|
+
*/
|
|
50
|
+
function readTrajectoryStore(what, op) {
|
|
51
|
+
try {
|
|
52
|
+
return op();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', `[BehaviorExamplePackAssembler] trajectory store ${what} failed (evidence source error, SPEC-PD-IAER-001 §6.2): ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── type guards (rc-2: no `as` bypass) ─────────────────────────────────────
|
|
60
|
+
function isPlainObject(value) {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function hasNoProtoKeys(value) {
|
|
64
|
+
for (const key of Object.keys(value)) {
|
|
65
|
+
if (PROTO_KEYS.has(key))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// ── BehaviorExamplePackAssembler ───────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Host-neutral assembler: pain lineage + trajectory → validated BehaviorExamplePack.
|
|
73
|
+
*
|
|
74
|
+
* The caller owns source lifecycle and supplies the real host vocabulary.
|
|
75
|
+
*/
|
|
76
|
+
function validateOwnerSelection(input) {
|
|
77
|
+
if (!Number.isSafeInteger(input.sourceNegativeToolCallId) || input.sourceNegativeToolCallId <= 0) {
|
|
78
|
+
throw new Error('[BehaviorExamplePackAssembler] sourceNegativeToolCallId must be a positive integer');
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(input.positiveToolCallIds) || input.positiveToolCallIds.length === 0 || input.positiveToolCallIds.length > MAX_POSITIVES) {
|
|
81
|
+
throw new Error('[BehaviorExamplePackAssembler] positiveToolCallIds must contain 1 to at most 3 IDs');
|
|
82
|
+
}
|
|
83
|
+
const allIds = [input.sourceNegativeToolCallId, ...input.positiveToolCallIds];
|
|
84
|
+
if (allIds.some((id) => !Number.isSafeInteger(id) || id <= 0) || new Set(allIds).size !== allIds.length) {
|
|
85
|
+
throw new Error('[BehaviorExamplePackAssembler] selected tool call IDs must be unique positive integers');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function requireSelectedRow(row, id) {
|
|
89
|
+
if (!row) {
|
|
90
|
+
// PRI-983: typed scope classification → stable `owner_behavior_examples_stale`
|
|
91
|
+
// at the readiness contract (the referenced trajectory row does not exist).
|
|
92
|
+
throw createOwnerEvidenceScopeError('tool_call_not_found', `[BehaviorExamplePackAssembler] selected tool call id=${id} not found`);
|
|
93
|
+
}
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
// ── internal: case builder ─────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Build a GoldenTraceCaseInput from a trajectory tool_call row.
|
|
99
|
+
*
|
|
100
|
+
* ERR-001: parses params_json as unknown and structurally validates (must be
|
|
101
|
+
* a plain object without proto-pollution keys). Throws on malformed input
|
|
102
|
+
* (rc-9: no silent fallback).
|
|
103
|
+
*/
|
|
104
|
+
function buildCaseFromRow(row, kind, expectedDecision) {
|
|
105
|
+
if (typeof row.toolName !== 'string' || row.toolName.length === 0) {
|
|
106
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: toolName must be a non-empty string (rc-9 fail loud)`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof row.paramsJson !== 'string') {
|
|
109
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must be a string (rc-9 fail loud)`);
|
|
110
|
+
}
|
|
111
|
+
let parsedParams;
|
|
112
|
+
try {
|
|
113
|
+
parsedParams = JSON.parse(row.paramsJson);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson is not valid JSON (rc-9 fail loud)`);
|
|
117
|
+
}
|
|
118
|
+
if (!isPlainObject(parsedParams)) {
|
|
119
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must parse to a non-array object (rc-9 fail loud)`);
|
|
120
|
+
}
|
|
121
|
+
if (!hasNoProtoKeys(parsedParams)) {
|
|
122
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must not carry prototype-pollution keys (ERR-076, rc-9 fail loud)`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
caseId: `case-${kind}-${row.id}`,
|
|
126
|
+
kind,
|
|
127
|
+
toolName: row.toolName,
|
|
128
|
+
params: parsedParams,
|
|
129
|
+
expectedDecision,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Redact sensitive fields from tool call params before they enter the pack.
|
|
134
|
+
*
|
|
135
|
+
* First-version policy:
|
|
136
|
+
* - Absolute paths outside the project tree → replaced with `<redacted:absolute-path>`
|
|
137
|
+
* - String values that look like secrets (long base64 / AKIA-style keys / etc.)
|
|
138
|
+
* → replaced with `<redacted:secret>`
|
|
139
|
+
*
|
|
140
|
+
* Records each redaction in `notes` so the Artificer and the owner can see
|
|
141
|
+
* what was stripped (spec §7.2: redactionNotes is part of the pack contract).
|
|
142
|
+
*/
|
|
143
|
+
function looksLikeAbsolutePath(value) {
|
|
144
|
+
// POSIX absolute path or Windows drive path
|
|
145
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
146
|
+
}
|
|
147
|
+
const SECRET_PATTERNS = [
|
|
148
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key ID
|
|
149
|
+
/[A-Za-z0-9+/]{40,}/, // long base64-ish token (≥40 chars, no spaces)
|
|
150
|
+
];
|
|
151
|
+
function looksLikeSecret(value) {
|
|
152
|
+
if (value.length < 20)
|
|
153
|
+
return false; // too short to be a real secret
|
|
154
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
155
|
+
if (pattern.test(value))
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function redactParams(params, projectDir) {
|
|
161
|
+
const notes = [];
|
|
162
|
+
const redacted = {};
|
|
163
|
+
for (const key of Object.keys(params)) {
|
|
164
|
+
if (PROTO_KEYS.has(key))
|
|
165
|
+
continue; // defense in depth (ERR-076)
|
|
166
|
+
const value = params[key];
|
|
167
|
+
if (typeof value !== 'string') {
|
|
168
|
+
redacted[key] = value;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Path-like values
|
|
172
|
+
if (looksLikeAbsolutePath(value) && !value.startsWith(projectDir)) {
|
|
173
|
+
redacted[key] = '<redacted:absolute-path>';
|
|
174
|
+
notes.push(`redacted absolute path in field "${key}" (outside project tree)`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Secret-like values (AWS keys, long base64 tokens, etc.)
|
|
178
|
+
if (looksLikeSecret(value)) {
|
|
179
|
+
redacted[key] = '<redacted:secret>';
|
|
180
|
+
notes.push(`redacted possible secret in field "${key}"`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
redacted[key] = value;
|
|
184
|
+
}
|
|
185
|
+
return { redacted, notes };
|
|
186
|
+
}
|
|
187
|
+
export class BehaviorExamplePackAssembler {
|
|
188
|
+
db;
|
|
189
|
+
toolSemantics;
|
|
190
|
+
constructor(opts) {
|
|
191
|
+
this.db = opts.source;
|
|
192
|
+
this.toolSemantics = opts.toolSemantics;
|
|
193
|
+
}
|
|
194
|
+
/** Assemble only from explicit Owner labels; execution outcome is not a label. */
|
|
195
|
+
assemble(input) {
|
|
196
|
+
const pain = readTrajectoryStore(`pain lineage read for canonical_pain_id="${input.sourcePainId}"`, () => this.db.getPainEventByCanonicalId(input.sourcePainId));
|
|
197
|
+
if (!pain) {
|
|
198
|
+
// PRI-983: typed scope classification (structural marker — the readiness
|
|
199
|
+
// contract maps this to the stable `owner_behavior_examples_stale`
|
|
200
|
+
// code; FR-03: an unresolvable pain reference must never be silently
|
|
201
|
+
// reused). Message text unchanged (historical audit signature).
|
|
202
|
+
throw createOwnerEvidenceScopeError('pain_not_found', `[BehaviorExamplePackAssembler] pain event not found for canonical_pain_id="${input.sourcePainId}" (ERR-069 fail loud)`);
|
|
203
|
+
}
|
|
204
|
+
validateOwnerSelection(input);
|
|
205
|
+
const negativeRow = requireSelectedRow(readTrajectoryStore(`evidence row read for id=${input.sourceNegativeToolCallId}`, () => this.db.getRuleHostEvidenceRow(input.sourceNegativeToolCallId)), input.sourceNegativeToolCallId);
|
|
206
|
+
const positiveRows = input.positiveToolCallIds.map((id) => requireSelectedRow(readTrajectoryStore(`evidence row read for id=${id}`, () => this.db.getRuleHostEvidenceRow(id)), id));
|
|
207
|
+
for (const row of [negativeRow, ...positiveRows]) {
|
|
208
|
+
if (row.sessionId !== pain.sessionId) {
|
|
209
|
+
// PRI-983: cross pain/session selection → typed scope error → stable
|
|
210
|
+
// `owner_behavior_examples_scope_mismatch` at the readiness contract.
|
|
211
|
+
throw createOwnerEvidenceScopeError('session_mismatch', `[BehaviorExamplePackAssembler] selected tool call ${row.id} session mismatch; all examples must belong to pain session ${pain.sessionId}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const redactionNotes = [];
|
|
215
|
+
const buildCtx = { projectDir: input.projectDir, redactionNotes };
|
|
216
|
+
const sourceNegativeCase = this.buildSelectedCase(negativeRow, 'negative', buildCtx);
|
|
217
|
+
const positiveCounterexamples = positiveRows.map((row) => this.buildSelectedCase(row, 'positive', buildCtx));
|
|
218
|
+
const pack = {
|
|
219
|
+
sourceNegativeCase,
|
|
220
|
+
ownerDesiredOutcome: input.ownerDesiredOutcome,
|
|
221
|
+
positiveCounterexamples,
|
|
222
|
+
evidenceRefs: [`pain:${pain.id}`, `tool_call:${negativeRow.id}`, ...positiveRows.map((row) => `tool_call:${row.id}`)].slice(0, MAX_EVIDENCE_REFS),
|
|
223
|
+
redactionNotes,
|
|
224
|
+
};
|
|
225
|
+
// ── 9. Validate — fail loud (ERR-069) ──
|
|
226
|
+
const validation = validateBehaviorExamplePack(pack);
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
throw new Error(`[BehaviorExamplePackAssembler] assembled pack failed validateBehaviorExamplePack: ${validation.errors.join('; ')} (ERR-069 fail loud)`);
|
|
229
|
+
}
|
|
230
|
+
return pack;
|
|
231
|
+
}
|
|
232
|
+
buildSelectedCase(row, kind, ctx) {
|
|
233
|
+
const { projectDir, redactionNotes } = ctx;
|
|
234
|
+
const base = buildCaseFromRow(row, kind, kind === 'negative' ? 'block' : 'allow');
|
|
235
|
+
const redaction = redactParams(base.params, projectDir);
|
|
236
|
+
redactionNotes.push(...redaction.notes);
|
|
237
|
+
const historyResult = readTrajectoryStore(`context history read for session=${row.sessionId} before id=${row.id}`, () => this.db.getRuleHostContextRowsBefore(row.sessionId, row.id, HISTORY_LIMIT));
|
|
238
|
+
const history = assembleBehaviorHistory(historyResult.rows, historyResult.truncated, { projectDir, toolSemantics: this.toolSemantics });
|
|
239
|
+
const action = buildRuleHostAction(row.toolName, redaction.redacted, projectDir, { canonicalKind: this.toolSemantics.resolve(row.toolName) });
|
|
240
|
+
const ruleContext = { version: 2, history, facts: computeBehaviorFacts(history, action.normalizedPath || null, null) };
|
|
241
|
+
return { ...base, params: redaction.redacted, ruleContext };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RuleHistoryWindow, ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
|
+
import type { RuleHostContextRow } from '@principles/core/trajectory-types';
|
|
3
|
+
export declare function assembleBehaviorHistory(rows: readonly RuleHostContextRow[], truncated: boolean, options: {
|
|
4
|
+
projectDir: string;
|
|
5
|
+
toolSemantics: ToolSemanticRegistry;
|
|
6
|
+
}): RuleHistoryWindow;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Shared validated history assembly; host vocabulary is supplied by its owner. */
|
|
2
|
+
import { extractFilePathFromParams, normalizePathPure } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
|
|
3
|
+
const VALID_OUTCOMES = new Set(['success', 'failure', 'blocked']);
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isRuleToolOutcome(value) {
|
|
8
|
+
return typeof value === 'string' && VALID_OUTCOMES.has(value);
|
|
9
|
+
}
|
|
10
|
+
function unavailable(reason) {
|
|
11
|
+
return {
|
|
12
|
+
status: 'unavailable',
|
|
13
|
+
unavailableReason: reason,
|
|
14
|
+
truncated: false,
|
|
15
|
+
calls: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function assembleBehaviorHistory(rows, truncated, options) {
|
|
19
|
+
const { projectDir, toolSemantics } = options;
|
|
20
|
+
const records = [];
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
// ── validate tool_name (ERR-001: unknown) ──
|
|
23
|
+
const { toolName } = row;
|
|
24
|
+
if (typeof toolName !== 'string' || toolName.length === 0) {
|
|
25
|
+
return unavailable(`row id=${row.id}: tool_name must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
// ── validate outcome (ERR-001: unknown, enum check) ──
|
|
28
|
+
const { outcome } = row;
|
|
29
|
+
if (!isRuleToolOutcome(outcome)) {
|
|
30
|
+
return unavailable(`row id=${row.id}: outcome "${row.outcome}" is not a valid RuleToolOutcome`);
|
|
31
|
+
}
|
|
32
|
+
// ── validate & parse params_json (ERR-001: unknown) ──
|
|
33
|
+
const paramsJsonStr = row.paramsJson;
|
|
34
|
+
if (typeof paramsJsonStr !== 'string') {
|
|
35
|
+
return unavailable(`row id=${row.id}: params_json must be a string`);
|
|
36
|
+
}
|
|
37
|
+
let parsedParams;
|
|
38
|
+
try {
|
|
39
|
+
parsedParams = JSON.parse(paramsJsonStr);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return unavailable(`row id=${row.id}: params_json is not valid JSON`);
|
|
43
|
+
}
|
|
44
|
+
// Must be a non-array object (spec §5.2)
|
|
45
|
+
if (!isPlainObject(parsedParams)) {
|
|
46
|
+
return unavailable(`row id=${row.id}: params_json must parse to a non-array object`);
|
|
47
|
+
}
|
|
48
|
+
// ── build RuleToolCallRecord ──
|
|
49
|
+
// PRI-634-F: resolve through the supplied registry (baseline + host
|
|
50
|
+
// layer) instead of baseline-only canonicalizeToolKind — production tools
|
|
51
|
+
// like 'shell'/'cmd'/'delete_file' previously degraded to 'other' here
|
|
52
|
+
// while the gate classified them as bash/write (vocabulary drift).
|
|
53
|
+
const canonicalKind = toolSemantics.resolve(toolName);
|
|
54
|
+
// Extract normalized path using the same pure logic as buildRuleHostAction
|
|
55
|
+
// (avoids production/replay drift, spec §4.4)
|
|
56
|
+
const rawPath = extractFilePathFromParams(parsedParams, {
|
|
57
|
+
isBashTool: canonicalKind === 'execute',
|
|
58
|
+
isWriteTool: canonicalKind === 'write',
|
|
59
|
+
toolName,
|
|
60
|
+
});
|
|
61
|
+
const normalizedPath = normalizePathPure(rawPath, projectDir);
|
|
62
|
+
records.push({
|
|
63
|
+
sequenceId: row.id,
|
|
64
|
+
toolName,
|
|
65
|
+
canonicalKind,
|
|
66
|
+
normalizedPath: normalizedPath === '' ? null : normalizedPath,
|
|
67
|
+
paramsSummary: parsedParams,
|
|
68
|
+
outcome,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
status: 'available',
|
|
73
|
+
truncated,
|
|
74
|
+
calls: records,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -85,3 +85,6 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
|
|
|
85
85
|
*/
|
|
86
86
|
events?: HostEventEmitter;
|
|
87
87
|
}): HostRuntime;
|
|
88
|
+
export * from './behavior-example-pack-assembler.js';
|
|
89
|
+
export * from './behavior-history.js';
|
|
90
|
+
export * from './workspace-behavior-evidence.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BehaviorEvidencePort, ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
|
+
/** Lazy storage access: missing labels need no DB, and a failed open remains observable. */
|
|
3
|
+
export declare function createWorkspaceBehaviorEvidencePort(workspaceDir: string, toolSemantics?: ToolSemanticRegistry): BehaviorEvidencePort;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Read-only adapter over the canonical trajectory store; never creates/migrates it. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
5
|
+
import { BehaviorExamplePackAssembler } from './behavior-example-pack-assembler.js';
|
|
6
|
+
import { resolveWorkspaceHostToolSemantics } from './host-tool-semantic-resolver.js';
|
|
7
|
+
function record(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function contextRow(value) {
|
|
11
|
+
if (!record(value) || typeof value.id !== 'number' || !Number.isSafeInteger(value.id)
|
|
12
|
+
|| typeof value.tool_name !== 'string' || typeof value.outcome !== 'string' || typeof value.params_json !== 'string') {
|
|
13
|
+
throw new Error('Malformed trajectory context row');
|
|
14
|
+
}
|
|
15
|
+
const params = JSON.parse(value.params_json);
|
|
16
|
+
if (!record(params))
|
|
17
|
+
throw new Error('Malformed trajectory params object');
|
|
18
|
+
if (Object.keys(params).some((key) => key === '__proto__' || key === 'constructor' || key === 'prototype')) {
|
|
19
|
+
throw new Error('Unsafe keys in persisted trajectory params');
|
|
20
|
+
}
|
|
21
|
+
return { id: value.id, toolName: value.tool_name, outcome: value.outcome, paramsJson: value.params_json };
|
|
22
|
+
}
|
|
23
|
+
function source(db) {
|
|
24
|
+
return {
|
|
25
|
+
getPainEventByCanonicalId(id) {
|
|
26
|
+
const row = db.prepare('SELECT id, session_id FROM pain_events WHERE canonical_pain_id = ? LIMIT 1').get(id);
|
|
27
|
+
if (row === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
if (!record(row) || typeof row.id !== 'number' || typeof row.session_id !== 'string')
|
|
30
|
+
throw new Error('Malformed pain lineage row');
|
|
31
|
+
return { id: row.id, sessionId: row.session_id };
|
|
32
|
+
},
|
|
33
|
+
getRuleHostEvidenceRow(id) {
|
|
34
|
+
const row = db.prepare('SELECT id, session_id, tool_name, outcome, params_json FROM tool_calls WHERE id = ? LIMIT 1').get(id);
|
|
35
|
+
if (row === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
if (!record(row) || typeof row.session_id !== 'string')
|
|
38
|
+
throw new Error('Malformed trajectory evidence row');
|
|
39
|
+
return { ...contextRow(row), sessionId: row.session_id };
|
|
40
|
+
},
|
|
41
|
+
getRuleHostContextRowsBefore(sessionId, beforeId, limit) {
|
|
42
|
+
const rows = db.prepare('SELECT id, tool_name, outcome, params_json FROM tool_calls WHERE session_id = ? AND id < ? ORDER BY id DESC LIMIT ?').all(sessionId, beforeId, limit + 1);
|
|
43
|
+
return { rows: rows.slice(0, limit).reverse().map(contextRow), truncated: rows.length > limit };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Lazy storage access: missing labels need no DB, and a failed open remains observable. */
|
|
48
|
+
export function createWorkspaceBehaviorEvidencePort(workspaceDir, toolSemantics) {
|
|
49
|
+
return {
|
|
50
|
+
assemble(selection) {
|
|
51
|
+
const resolved = toolSemantics === undefined ? resolveWorkspaceHostToolSemantics(workspaceDir) : null;
|
|
52
|
+
if (resolved !== null && !resolved.ok)
|
|
53
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', resolved.reason);
|
|
54
|
+
const semantics = toolSemantics ?? (resolved !== null && resolved.ok ? resolved.registry : undefined);
|
|
55
|
+
if (semantics === undefined)
|
|
56
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', 'Host tool semantics unavailable');
|
|
57
|
+
let db;
|
|
58
|
+
try {
|
|
59
|
+
db = new Database(path.join(workspaceDir, '.state', 'trajectory.db'), { readonly: true, fileMustExist: true });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', `Trajectory evidence unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// One read snapshot prevents a pack combining different trajectory revisions.
|
|
66
|
+
return db.transaction(() => new BehaviorExamplePackAssembler({ source: source(db), toolSemantics: semantics })
|
|
67
|
+
.assemble({ ...selection, projectDir: workspaceDir }))();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./package.json": "./package.json"
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./behavior-example-pack-assembler": {
|
|
15
|
+
"types": "./dist/behavior-example-pack-assembler.d.ts",
|
|
16
|
+
"default": "./dist/behavior-example-pack-assembler.js"
|
|
17
|
+
},
|
|
18
|
+
"./behavior-history": {
|
|
19
|
+
"types": "./dist/behavior-history.d.ts",
|
|
20
|
+
"default": "./dist/behavior-history.js"
|
|
21
|
+
}
|
|
14
22
|
},
|
|
15
23
|
"files": [
|
|
16
24
|
"dist",
|
|
@@ -6,7 +6,23 @@ export type InstallHost = (typeof INSTALL_HOSTS)[number];
|
|
|
6
6
|
export interface InstallManifest {
|
|
7
7
|
layoutVersion: typeof INSTALL_LAYOUT_VERSION;
|
|
8
8
|
mode: InstallLayoutMode;
|
|
9
|
+
/**
|
|
10
|
+
* Hosts THIS reader version recognizes. Host strings the reader does not
|
|
11
|
+
* know are NOT here — they land in `unknownHosts` so an install registered
|
|
12
|
+
* with a newer host keeps resolving instead of being rejected as malformed.
|
|
13
|
+
*/
|
|
9
14
|
hosts: InstallHost[];
|
|
15
|
+
/**
|
|
16
|
+
* Host strings present in the persisted `hosts` array that this reader
|
|
17
|
+
* version does not know (forward compatibility, 2026-10-16). A NEWER
|
|
18
|
+
* installer may register a host an OLDER reader never heard of; that is a
|
|
19
|
+
* compatibility warning, never "manifest malformed" — treating it as one
|
|
20
|
+
* made every pre-WorkBuddy Companion report `console_runtime_not_installed`
|
|
21
|
+
* against a healthy 3-host install and mass-stop its workers. Fail-closed
|
|
22
|
+
* stays reserved for structural damage: wrong layoutVersion, non-array or
|
|
23
|
+
* non-string hosts, bad mode, illegal workspace paths.
|
|
24
|
+
*/
|
|
25
|
+
unknownHosts?: readonly string[];
|
|
10
26
|
/**
|
|
11
27
|
* Canonical absolute paths of Workspaces the install serves (PRI-624 Slice C).
|
|
12
28
|
* Optional so pre-PRI-624 manifests stay valid; always normalized to
|
|
@@ -16,6 +32,11 @@ export interface InstallManifest {
|
|
|
16
32
|
*/
|
|
17
33
|
workspaces?: readonly string[];
|
|
18
34
|
}
|
|
35
|
+
/** Structured forward-compatibility observation from manifest parsing (rc-9). */
|
|
36
|
+
export interface InstallManifestWarning {
|
|
37
|
+
code: 'install_manifest_unknown_host';
|
|
38
|
+
host: string;
|
|
39
|
+
}
|
|
19
40
|
export interface InstallLayoutPaths {
|
|
20
41
|
homeDir: string;
|
|
21
42
|
pdDir: string;
|
|
@@ -58,6 +79,8 @@ export interface LayoutResolution {
|
|
|
58
79
|
manifest?: InstallManifest;
|
|
59
80
|
reason?: string;
|
|
60
81
|
nextAction?: string;
|
|
82
|
+
/** Forward-compatibility warnings surfaced while parsing the manifest. */
|
|
83
|
+
warnings?: readonly InstallManifestWarning[];
|
|
61
84
|
}
|
|
62
85
|
export interface ResolveInstallLayoutOptions {
|
|
63
86
|
homeDir: string;
|
|
@@ -65,10 +88,26 @@ export interface ResolveInstallLayoutOptions {
|
|
|
65
88
|
canonicalRuntimeExists: boolean;
|
|
66
89
|
legacyExtensionExists: boolean;
|
|
67
90
|
}
|
|
68
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
|
|
69
93
|
export declare function mergeInstallManifestWorkspaces(current: InstallManifest | undefined, workspaceDir: string): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The `hosts` array a manifest WRITER must persist: every registration the
|
|
96
|
+
* reader recognized plus every unknown one, deduped, known hosts first.
|
|
97
|
+
* Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
|
|
98
|
+
* newer host's registration — the write-side twin of the read-side
|
|
99
|
+
* unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
|
|
100
|
+
* fresh-install path keeps working unchanged.
|
|
101
|
+
*/
|
|
102
|
+
export declare function installManifestHostsForPersist(manifest: InstallManifest | undefined): string[];
|
|
103
|
+
/**
|
|
104
|
+
* Parse the install manifest. Structural damage fails closed with `error`;
|
|
105
|
+
* a host string this reader version does not know is a `warnings` entry and
|
|
106
|
+
* the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
|
|
107
|
+
*/
|
|
70
108
|
export declare function parseInstallManifest(value: unknown): {
|
|
71
109
|
manifest?: InstallManifest;
|
|
110
|
+
warnings?: readonly InstallManifestWarning[];
|
|
72
111
|
error?: string;
|
|
73
112
|
};
|
|
74
113
|
export declare function getInstallLayoutPaths(homeDir: string): InstallLayoutPaths;
|
|
@@ -8,6 +8,9 @@ function isRecord(value) {
|
|
|
8
8
|
function isHost(value) {
|
|
9
9
|
return typeof value === 'string' && INSTALL_HOSTS.includes(value);
|
|
10
10
|
}
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === 'string' && value.length > 0;
|
|
13
|
+
}
|
|
11
14
|
/** Workspace entries must be non-empty absolute paths — the Companion spawns per-Workspace workers keyed by canonical path. */
|
|
12
15
|
function parseWorkspaceList(value) {
|
|
13
16
|
if (value === undefined)
|
|
@@ -25,7 +28,8 @@ function parseWorkspaceList(value) {
|
|
|
25
28
|
}
|
|
26
29
|
return { workspaces: resolved };
|
|
27
30
|
}
|
|
28
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
|
|
29
33
|
export function mergeInstallManifestWorkspaces(current, workspaceDir) {
|
|
30
34
|
const canonical = path.resolve(workspaceDir);
|
|
31
35
|
const resolved = [];
|
|
@@ -38,6 +42,22 @@ export function mergeInstallManifestWorkspaces(current, workspaceDir) {
|
|
|
38
42
|
resolved.push(canonical);
|
|
39
43
|
return resolved;
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The `hosts` array a manifest WRITER must persist: every registration the
|
|
47
|
+
* reader recognized plus every unknown one, deduped, known hosts first.
|
|
48
|
+
* Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
|
|
49
|
+
* newer host's registration — the write-side twin of the read-side
|
|
50
|
+
* unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
|
|
51
|
+
* fresh-install path keeps working unchanged.
|
|
52
|
+
*/
|
|
53
|
+
export function installManifestHostsForPersist(manifest) {
|
|
54
|
+
return [...new Set([...(manifest?.hosts ?? []), ...(manifest?.unknownHosts ?? [])])];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse the install manifest. Structural damage fails closed with `error`;
|
|
58
|
+
* a host string this reader version does not know is a `warnings` entry and
|
|
59
|
+
* the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
|
|
60
|
+
*/
|
|
41
61
|
export function parseInstallManifest(value) {
|
|
42
62
|
if (!isRecord(value))
|
|
43
63
|
return { error: 'install_manifest_malformed: expected an object' };
|
|
@@ -47,14 +67,55 @@ export function parseInstallManifest(value) {
|
|
|
47
67
|
if (value.mode !== 'canonical' && value.mode !== 'legacy') {
|
|
48
68
|
return { error: 'install_manifest_malformed: mode must be canonical or legacy' };
|
|
49
69
|
}
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
// Host entries must be non-empty strings; WHICH strings are valid hosts is
|
|
71
|
+
// version-relative knowledge, so unknown strings warn instead of failing.
|
|
72
|
+
if (!Array.isArray(value.hosts) || !value.hosts.every(isNonEmptyString)) {
|
|
73
|
+
return { error: 'install_manifest_malformed: hosts must be an array of host name strings' };
|
|
74
|
+
}
|
|
75
|
+
// Idempotency: the parser must re-read its own output. An all-unknown-hosts
|
|
76
|
+
// manifest parses to hosts: [] + unknownHosts, so the optional persisted
|
|
77
|
+
// `unknownHosts` field is accepted on input too (installManifestHostsForPersist
|
|
78
|
+
// reunifies them on write; writers persist the union into `hosts`).
|
|
79
|
+
let carriedUnknownHosts = [];
|
|
80
|
+
if (value.unknownHosts !== undefined) {
|
|
81
|
+
if (!Array.isArray(value.unknownHosts) || !value.unknownHosts.every(isNonEmptyString)) {
|
|
82
|
+
return { error: 'install_manifest_malformed: unknownHosts must be an array of host name strings' };
|
|
83
|
+
}
|
|
84
|
+
carriedUnknownHosts = value.unknownHosts.filter((host) => !isHost(host));
|
|
85
|
+
}
|
|
86
|
+
if (value.hosts.length === 0 && carriedUnknownHosts.length === 0) {
|
|
87
|
+
return { error: 'install_manifest_malformed: hosts must contain at least one host' };
|
|
88
|
+
}
|
|
89
|
+
const hosts = [];
|
|
90
|
+
const unknownHosts = [];
|
|
91
|
+
const warnings = [];
|
|
92
|
+
for (const host of [...new Set(value.hosts)]) {
|
|
93
|
+
if (isHost(host))
|
|
94
|
+
hosts.push(host);
|
|
95
|
+
else {
|
|
96
|
+
unknownHosts.push(host);
|
|
97
|
+
warnings.push({ code: 'install_manifest_unknown_host', host });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const host of carriedUnknownHosts) {
|
|
101
|
+
if (!unknownHosts.includes(host)) {
|
|
102
|
+
unknownHosts.push(host);
|
|
103
|
+
warnings.push({ code: 'install_manifest_unknown_host', host });
|
|
104
|
+
}
|
|
52
105
|
}
|
|
53
|
-
const hosts = [...new Set(value.hosts)];
|
|
54
106
|
const workspaces = parseWorkspaceList(value.workspaces);
|
|
55
107
|
if (workspaces.error)
|
|
56
108
|
return { error: workspaces.error };
|
|
57
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
manifest: {
|
|
111
|
+
layoutVersion: INSTALL_LAYOUT_VERSION,
|
|
112
|
+
mode: value.mode,
|
|
113
|
+
hosts,
|
|
114
|
+
...(unknownHosts.length > 0 ? { unknownHosts } : {}),
|
|
115
|
+
workspaces: workspaces.workspaces,
|
|
116
|
+
},
|
|
117
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
118
|
+
};
|
|
58
119
|
}
|
|
59
120
|
export function getInstallLayoutPaths(homeDir) {
|
|
60
121
|
const home = path.resolve(homeDir);
|
|
@@ -96,8 +157,28 @@ export function getWorkbuddyWorkspaceDir(homeDir) {
|
|
|
96
157
|
export function resolveInstallLayout(options) {
|
|
97
158
|
const paths = getInstallLayoutPaths(options.homeDir);
|
|
98
159
|
const parsed = parseInstallManifest(options.manifest);
|
|
160
|
+
const { warnings } = parsed;
|
|
99
161
|
if (parsed.manifest && parsed.manifest.mode === 'canonical' && options.canonicalRuntimeExists) {
|
|
100
|
-
return { mode: 'canonical', paths, manifest: parsed.manifest };
|
|
162
|
+
return { mode: 'canonical', paths, manifest: parsed.manifest, ...(warnings !== undefined ? { warnings } : {}) };
|
|
163
|
+
}
|
|
164
|
+
// RF-01 (review 2026-10-16): when a canonical runtime EXISTS, the manifest
|
|
165
|
+
// is the install-state authority. A manifest that was READ but cannot be
|
|
166
|
+
// honored (newer layoutVersion, structural damage) must refuse the whole
|
|
167
|
+
// resolution — falling through to the legacy extension dir would launch a
|
|
168
|
+
// runtime that does not match the installed protocol. Legacy fallback stays
|
|
169
|
+
// legitimate only when the reader genuinely saw NO manifest (absent file →
|
|
170
|
+
// `options.manifest === undefined`), i.e. a real legacy-only install, or
|
|
171
|
+
// when there is no canonical runtime to bypass.
|
|
172
|
+
if (options.canonicalRuntimeExists && options.manifest !== undefined && parsed.manifest === undefined) {
|
|
173
|
+
return {
|
|
174
|
+
mode: 'missing',
|
|
175
|
+
paths,
|
|
176
|
+
reason: parsed.error ?? 'install_manifest_malformed',
|
|
177
|
+
nextAction: parsed.error?.startsWith('install_manifest_unsupported_version')
|
|
178
|
+
? 'Update PD Companion from https://github.com/csuzngjh/principles/releases (companion-v* Windows installer); do not reinstall the healthy PD runtime.'
|
|
179
|
+
: 'Run the PD installer/updater to repair the structurally damaged installation manifest.',
|
|
180
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
181
|
+
};
|
|
101
182
|
}
|
|
102
183
|
if (options.legacyExtensionExists) {
|
|
103
184
|
return {
|
|
@@ -106,6 +187,7 @@ export function resolveInstallLayout(options) {
|
|
|
106
187
|
...(parsed.manifest ? { manifest: parsed.manifest } : {}),
|
|
107
188
|
reason: parsed.error ?? 'install_layout_legacy_fallback',
|
|
108
189
|
nextAction: 'Run the installer update to migrate the shared runtime to ~/.pd/runtime.',
|
|
190
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
109
191
|
};
|
|
110
192
|
}
|
|
111
193
|
return {
|