create-principles-disciple 1.145.32 → 1.145.33
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 +1 -1
- package/codex-adapter/dist/ingestion/ingestion.js +3 -1
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/models/ActivationsConsoleModel.d.ts +19 -2
- package/console/dist/server/models/ActivationsConsoleModel.js +124 -10
- package/console/dist/server/models/OwnerDecisionConsoleModel.d.ts +3 -1
- package/console/dist/server/models/OwnerDecisionConsoleModel.js +6 -2
- package/console/dist/server/routes/activations.js +7 -2
- package/console/dist/server/routes/owner-decisions.js +1 -1
- package/core/dist/runtime-v2/activation/index.d.ts +6 -0
- package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/index.js +3 -0
- package/core/dist/runtime-v2/activation/index.js.map +1 -1
- package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts +75 -0
- package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js +131 -37
- package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js.map +1 -1
- package/core/dist/runtime-v2/activation/production-gate-deps.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/production-gate-deps.js +7 -3
- package/core/dist/runtime-v2/activation/production-gate-deps.js.map +1 -1
- package/core/dist/runtime-v2/activation/recovery-composition.d.ts +54 -0
- package/core/dist/runtime-v2/activation/recovery-composition.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/recovery-composition.js +129 -0
- package/core/dist/runtime-v2/activation/recovery-composition.js.map +1 -0
- package/core/dist/runtime-v2/activation/recovery-control-surfaces.d.ts +62 -0
- package/core/dist/runtime-v2/activation/recovery-control-surfaces.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/recovery-control-surfaces.js +84 -0
- package/core/dist/runtime-v2/activation/recovery-control-surfaces.js.map +1 -0
- package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts +13 -1
- package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js +24 -1
- package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -2
- 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/trajectory-schema.d.ts.map +1 -1
- package/core/dist/runtime-v2/trajectory-schema.js +12 -1
- package/core/dist/runtime-v2/trajectory-schema.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +10 -0
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +1 -0
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/package.json +1 -1
- package/host-runtime/dist/cli-recovery-probe.d.ts +30 -0
- package/host-runtime/dist/cli-recovery-probe.js +169 -0
- package/host-runtime/dist/governance-signal-admission.js +22 -5
- package/host-runtime/dist/host-liveness-contract.d.ts +51 -28
- package/host-runtime/dist/host-liveness-contract.js +143 -46
- package/host-runtime/dist/index.d.ts +2 -0
- package/host-runtime/dist/index.js +2 -0
- package/host-runtime/dist/production-pain-evidence.d.ts +2 -0
- package/host-runtime/dist/production-pain-evidence.js +46 -8
- package/host-runtime/dist/production-rulehost-gate.js +24 -6
- package/host-runtime/dist/workbuddy-rule-context-provider.d.ts +33 -0
- package/host-runtime/dist/workbuddy-rule-context-provider.js +126 -0
- package/host-runtime/dist/workspace-behavior-evidence.d.ts +22 -5
- package/host-runtime/dist/workspace-behavior-evidence.js +41 -11
- package/host-runtime/package.json +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/recovery-reachability-probe.d.ts +25 -0
- package/pd-cli/dist/commands/recovery-reachability-probe.d.ts.map +1 -0
- package/pd-cli/dist/commands/recovery-reachability-probe.js +50 -0
- package/pd-cli/dist/commands/recovery-reachability-probe.js.map +1 -0
- package/pd-cli/dist/commands/runtime-activation.d.ts +10 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.js +81 -3
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +385 -385
- package/plugin/dist/governance-audit.js +1 -1
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +2 -2
- package/workbuddy-adapter/dist/codec/input-decoder.d.ts +14 -11
- package/workbuddy-adapter/dist/codec/input-decoder.js +28 -14
- package/workbuddy-adapter/dist/pd-hook.js +31 -12
- package/workbuddy-adapter/package.json +1 -1
|
@@ -8,18 +8,98 @@ export const OPENCLAW_HOST_LIVENESS_CONTRACT = {
|
|
|
8
8
|
supportsShadowEvidence: true,
|
|
9
9
|
outOfBandControls: ['activation_deactivate', 'global_rulecode_pause', 'owner_review_console'],
|
|
10
10
|
protectedCapabilities: [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Review R1: `exec` is the ONLY shell tool name that reaches the OpenClaw
|
|
12
|
+
// hook face (constants/tools.ts PRI-741: `bash`/`exec_command` are
|
|
13
|
+
// config-level aliases that normalize away). Aliases and probes must use
|
|
14
|
+
// the real dispatched name or the neutral-probe replay rejects them.
|
|
15
|
+
{ capabilityId: 'pd_status', hostToolAliases: ['exec'] },
|
|
16
|
+
{ capabilityId: 'rulecode_deactivate', hostToolAliases: ['exec'] },
|
|
17
|
+
// Review R1: the global pause has NO pd CLI command — its real surface is
|
|
18
|
+
// the authenticated Owner Console route. Declaring a shell alias here
|
|
19
|
+
// would claim a recovery entry that does not exist.
|
|
20
|
+
{ capabilityId: 'rulecode_global_pause', hostToolAliases: ['rulecode_global_pause'] },
|
|
14
21
|
{ capabilityId: 'owner_review_access', hostToolAliases: ['owner_review_access'] },
|
|
15
22
|
],
|
|
16
23
|
neutralProbes: [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ probeId: 'probe-
|
|
24
|
+
// Review R1: probe commands are the REAL registered pd commands
|
|
25
|
+
// (verified against the recovery-control registry; `pd status` and
|
|
26
|
+
// `pd activation emergency-pause` do not exist and were removed).
|
|
27
|
+
{ probeId: 'probe-pd-status', capabilityId: 'pd_status', toolName: 'exec', params: { command: 'pd activation list --json' }, expectedDecision: 'allow' },
|
|
28
|
+
{ probeId: 'probe-rule-deactivate', capabilityId: 'rulecode_deactivate', toolName: 'exec', params: { command: 'pd activation deactivate --activation-id recovery-probe' }, expectedDecision: 'allow' },
|
|
29
|
+
// Review R3/P1: console-channel capabilities (global pause, owner review)
|
|
30
|
+
// carry NO probe — their recovery path never passes through the RuleHost
|
|
31
|
+
// gate, so a rule replay would misrepresent their verification. Their
|
|
32
|
+
// entry surfaces live in the recovery-control registry and their
|
|
33
|
+
// target-environment reachability is supplied by the readiness caller.
|
|
21
34
|
],
|
|
22
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 B1: the Codex capability contract. Every
|
|
38
|
+
* tool alias below is the REAL Codex tool surface with in-repo evidence
|
|
39
|
+
* (`Bash` + `apply_patch`, locked by the shipped hooks matcher — see
|
|
40
|
+
* codex-adapter tool-semantics.ts). The out-of-band controls are the
|
|
41
|
+
* host-neutral `pd` CLI and the Web Console: they act on the workspace
|
|
42
|
+
* state directly, never through the Codex hook process, so a broken rule
|
|
43
|
+
* cannot lock them. This contract declares the mapping ONLY; shadow-event
|
|
44
|
+
* attribution, neutral-probe composition and Owner decision authority are
|
|
45
|
+
* enforced by the shared readiness pipeline, not by this constant
|
|
46
|
+
* (SPEC §11.2 — a constant alone must never turn a check green).
|
|
47
|
+
*/
|
|
48
|
+
export const CODEX_HOST_LIVENESS_CONTRACT = {
|
|
49
|
+
version: 'codex-v2@1',
|
|
50
|
+
supportsShadowEvidence: true,
|
|
51
|
+
outOfBandControls: ['activation_deactivate', 'global_rulecode_pause', 'owner_review_console'],
|
|
52
|
+
protectedCapabilities: [
|
|
53
|
+
{ capabilityId: 'pd_status', hostToolAliases: ['Bash'] },
|
|
54
|
+
{ capabilityId: 'rulecode_deactivate', hostToolAliases: ['Bash'] },
|
|
55
|
+
{ capabilityId: 'rulecode_global_pause', hostToolAliases: ['rulecode_global_pause'] },
|
|
56
|
+
{ capabilityId: 'owner_review_access', hostToolAliases: ['owner_review_access'] },
|
|
57
|
+
],
|
|
58
|
+
neutralProbes: [
|
|
59
|
+
{ probeId: 'probe-pd-status', capabilityId: 'pd_status', toolName: 'Bash', params: { command: 'pd activation list --json' }, expectedDecision: 'allow' },
|
|
60
|
+
{ probeId: 'probe-rule-deactivate', capabilityId: 'rulecode_deactivate', toolName: 'Bash', params: { command: 'pd activation deactivate --activation-id recovery-probe' }, expectedDecision: 'allow' },
|
|
61
|
+
// Review R3/P1: console-channel capabilities (global pause, owner review)
|
|
62
|
+
// carry NO probe — their recovery path never passes through the RuleHost
|
|
63
|
+
// gate, so a rule replay would misrepresent their verification. Their
|
|
64
|
+
// entry surfaces live in the recovery-control registry and their
|
|
65
|
+
// target-environment reachability is supplied by the readiness caller.
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 C: the WorkBuddy capability contract.
|
|
70
|
+
* Tool aliases use WorkBuddy's REAL capitalized vocabulary (Read/Write/Edit/
|
|
71
|
+
* Bash — frozen Phase 0 fixtures + official hooks reference; see
|
|
72
|
+
* workbuddy-adapter tool-semantics.ts). Out-of-band controls are the same
|
|
73
|
+
* host-neutral `pd` CLI / Web Console pair, invoked outside the WorkBuddy
|
|
74
|
+
* hook channel (the hook failing closed with exit 2 on PreToolUse cannot
|
|
75
|
+
* prevent an Owner terminal or browser from reaching the workspace).
|
|
76
|
+
*/
|
|
77
|
+
export const WORKBUDDY_HOST_LIVENESS_CONTRACT = {
|
|
78
|
+
version: 'workbuddy-v1@1',
|
|
79
|
+
supportsShadowEvidence: true,
|
|
80
|
+
outOfBandControls: ['activation_deactivate', 'global_rulecode_pause', 'owner_review_console'],
|
|
81
|
+
protectedCapabilities: [
|
|
82
|
+
{ capabilityId: 'pd_status', hostToolAliases: ['Bash'] },
|
|
83
|
+
{ capabilityId: 'rulecode_deactivate', hostToolAliases: ['Bash'] },
|
|
84
|
+
{ capabilityId: 'rulecode_global_pause', hostToolAliases: ['rulecode_global_pause'] },
|
|
85
|
+
{ capabilityId: 'owner_review_access', hostToolAliases: ['owner_review_access'] },
|
|
86
|
+
],
|
|
87
|
+
neutralProbes: [
|
|
88
|
+
{ probeId: 'probe-pd-status', capabilityId: 'pd_status', toolName: 'Bash', params: { command: 'pd activation list --json' }, expectedDecision: 'allow' },
|
|
89
|
+
{ probeId: 'probe-rule-deactivate', capabilityId: 'rulecode_deactivate', toolName: 'Bash', params: { command: 'pd activation deactivate --activation-id recovery-probe' }, expectedDecision: 'allow' },
|
|
90
|
+
// Review R3/P1: console-channel capabilities (global pause, owner review)
|
|
91
|
+
// carry NO probe — their recovery path never passes through the RuleHost
|
|
92
|
+
// gate, so a rule replay would misrepresent their verification. Their
|
|
93
|
+
// entry surfaces live in the recovery-control registry and their
|
|
94
|
+
// target-environment reachability is supplied by the readiness caller.
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
/** The per-host contracts the resolver may hand out — one authority per host kind. */
|
|
98
|
+
const HOST_LIVENESS_CONTRACTS = {
|
|
99
|
+
openclaw: OPENCLAW_HOST_LIVENESS_CONTRACT,
|
|
100
|
+
codex: CODEX_HOST_LIVENESS_CONTRACT,
|
|
101
|
+
workbuddy: WORKBUDDY_HOST_LIVENESS_CONTRACT,
|
|
102
|
+
};
|
|
23
103
|
/**
|
|
24
104
|
* Existing durable provenance (Owner review round-4): `trajectory.db`
|
|
25
105
|
* `pain_events.host_kind` records which host actually produced governance
|
|
@@ -107,41 +187,41 @@ export function readWorkspaceHostKindFacts(workspaceDir) {
|
|
|
107
187
|
return { ok: true, declaredKinds, evidenceKinds, effectiveKinds };
|
|
108
188
|
}
|
|
109
189
|
/**
|
|
110
|
-
*
|
|
111
|
-
* REAL host state
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
190
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 §4 (A): resolve the promotion host-liveness
|
|
191
|
+
* contract from the workspace's REAL host state. The historical "anything
|
|
192
|
+
* that is not Codex inherits OpenClaw" fork is REPLACED by an explicit
|
|
193
|
+
* five-way routing over openclaw / codex / workbuddy / unknown / multi-host:
|
|
194
|
+
*
|
|
195
|
+
* - nothing declared, no evidence, or OpenClaw-only traces → OpenClaw
|
|
196
|
+
* contract (the documented historical default; SPEC §4.2 keeps this
|
|
197
|
+
* compatibility semantics ONLY for workspaces with no non-OpenClaw
|
|
198
|
+
* declaration or evidence — it must not extend to workspaces where
|
|
199
|
+
* WorkBuddy has appeared);
|
|
200
|
+
* - durable evidence for a known non-OpenClaw host whose declaration file is
|
|
201
|
+
* missing/corrupted → host_declaration_missing_for_configured_host
|
|
202
|
+
* (generalized from Codex-only to every known host — SPEC §4.3: never
|
|
203
|
+
* silently fall back to OpenClaw when the governing host's declaration
|
|
204
|
+
* cannot be read);
|
|
205
|
+
* - more than one effective host kind → multi_host_promotion_ambiguous
|
|
206
|
+
* (never guess a target host; SPEC §4.4);
|
|
207
|
+
* - exactly one known non-OpenClaw host (declared) → that host's contract;
|
|
208
|
+
* - an unknown declared host kind → promotion_host_unsupported (fail closed;
|
|
209
|
+
* the declaration proves tool mapping only, never promotion capability —
|
|
210
|
+
* SPEC §4.5).
|
|
115
211
|
*
|
|
116
212
|
* Two durable provenance sources, cross-checked (Owner review round-4):
|
|
117
213
|
* 1. host declarations — `.pd/host-tool-semantics/<hostKind>.json`;
|
|
118
214
|
* 2. trajectory.db `pain_events.host_kind` — which hosts actually produced
|
|
119
|
-
* governance evidence here (survives declaration deletion).
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* capability authority, and fail closed whenever Codex evidence exists but
|
|
123
|
-
* its declaration is missing — never silently fall back to OpenClaw):
|
|
124
|
-
* - no declaration AND no Codex evidence → OpenClaw contract (the historical
|
|
125
|
-
* governance default; a workspace where only OpenClaw ever ran — or no
|
|
126
|
-
* host ran — is exactly the legacy state, and existing workspaces rely
|
|
127
|
-
* on it);
|
|
128
|
-
* - only openclaw declared and no Codex evidence → OPENCLAW_HOST_LIVENESS_CONTRACT;
|
|
129
|
-
* - codex declared (alone) → promotion_host_unsupported (Codex promotion
|
|
130
|
-
* stays explicitly unsupported until an evidence-backed capability
|
|
131
|
-
* authority decision exists — no second host contract here);
|
|
132
|
-
* - multiple hosts declared → multi_host_promotion_ambiguous (never guess a
|
|
133
|
-
* target host; the Owner decides host-selection policy separately);
|
|
134
|
-
* - Codex evidence present but codex.json missing (deleted/corrupted —
|
|
135
|
-
* alone or alongside openclaw.json) → host_declaration_missing_for_configured_host
|
|
136
|
-
* (re-persist the declaration before promoting);
|
|
137
|
-
* - declarations or provenance unreadable → fail closed with the reason.
|
|
215
|
+
* governance evidence here (survives declaration deletion). Unknown-host
|
|
216
|
+
* evidence rows are filtered by the facts reader (known-host filter) and
|
|
217
|
+
* can never reach this resolver (SPEC §11.4).
|
|
138
218
|
*
|
|
139
|
-
* This function
|
|
140
|
-
*
|
|
141
|
-
*
|
|
219
|
+
* This function creates no capability registry and no new store — it routes
|
|
220
|
+
* the existing per-host contract authorities truthfully over state that
|
|
221
|
+
* already exists.
|
|
142
222
|
*/
|
|
143
223
|
export function resolvePromotionHostLiveness(workspaceDir) {
|
|
144
|
-
// Same two provenance sources and failure semantics as before,
|
|
224
|
+
// Same two provenance sources and failure semantics as before, via the
|
|
145
225
|
// shared facts reader (readWorkspaceHostKindFacts) so prompt-injection
|
|
146
226
|
// route resolution and promotion liveness cannot drift apart.
|
|
147
227
|
const facts = readWorkspaceHostKindFacts(workspaceDir);
|
|
@@ -152,9 +232,11 @@ export function resolvePromotionHostLiveness(workspaceDir) {
|
|
|
152
232
|
}
|
|
153
233
|
const { declaredKinds } = facts;
|
|
154
234
|
const { effectiveKinds } = facts;
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
235
|
+
const nonOpenClawKinds = effectiveKinds.filter((kind) => kind !== 'openclaw');
|
|
236
|
+
// Historical compatibility default (SPEC §4.2): a workspace where nothing
|
|
237
|
+
// non-OpenClaw ever declared itself or produced evidence is exactly the
|
|
238
|
+
// legacy state, and existing workspaces rely on the OpenClaw contract.
|
|
239
|
+
if (nonOpenClawKinds.length === 0) {
|
|
158
240
|
return {
|
|
159
241
|
ok: true,
|
|
160
242
|
hostKind: 'openclaw',
|
|
@@ -163,14 +245,29 @@ export function resolvePromotionHostLiveness(workspaceDir) {
|
|
|
163
245
|
hostRuntimeVersion: OPENCLAW_HOST_LIVENESS_CONTRACT.version,
|
|
164
246
|
};
|
|
165
247
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
248
|
+
// SPEC §4.3: durable evidence for a known non-OpenClaw host whose own
|
|
249
|
+
// declaration is gone (deleted/corrupted — alone or alongside openclaw.json)
|
|
250
|
+
// must fail closed with the precise reason; a silent OpenClaw fallback here
|
|
251
|
+
// would resurrect a false capability PASS (Owner review round-4, now
|
|
252
|
+
// generalized beyond Codex — a WorkBuddy-only workspace is the exact same
|
|
253
|
+
// misattribution class, SPEC §1.1 item 6).
|
|
254
|
+
const missingDeclarationKinds = facts.evidenceKinds.filter((kind) => kind !== 'openclaw' && !declaredKinds.includes(kind));
|
|
255
|
+
if (missingDeclarationKinds.length > 0) {
|
|
256
|
+
return promotionHostFailure('host_declaration_missing_for_configured_host', effectiveKinds, `durable evidence for host(s) ${missingDeclarationKinds.join(', ')} exists in this workspace but the matching .pd/host-tool-semantics/<hostKind>.json declaration is missing; re-run that host so it re-persists its declaration (or deactivate the activation) before promoting`);
|
|
257
|
+
}
|
|
258
|
+
if (effectiveKinds.length > 1) {
|
|
259
|
+
return promotionHostFailure('multi_host_promotion_ambiguous', effectiveKinds, `the workspace declares multiple hosts (${effectiveKinds.join(', ')}); promotion requires an unambiguous governing host — decide the promotion target host first`);
|
|
171
260
|
}
|
|
172
|
-
|
|
173
|
-
|
|
261
|
+
const [governingKind] = nonOpenClawKinds;
|
|
262
|
+
if (governingKind !== 'codex' && governingKind !== 'workbuddy') {
|
|
263
|
+
return promotionHostFailure('promotion_host_unsupported', effectiveKinds, `host '${governingKind}' has no approved promotion capability authority; promotion on this host is explicitly unsupported until an evidence-backed capability decision exists`);
|
|
174
264
|
}
|
|
175
|
-
|
|
265
|
+
const contract = HOST_LIVENESS_CONTRACTS[governingKind];
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
hostKind: governingKind,
|
|
269
|
+
hostKinds: declaredKinds,
|
|
270
|
+
hostContract: contract,
|
|
271
|
+
hostRuntimeVersion: contract.version,
|
|
272
|
+
};
|
|
176
273
|
}
|
|
@@ -87,6 +87,8 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
|
|
|
87
87
|
export * from './behavior-example-pack-assembler.js';
|
|
88
88
|
export * from './behavior-history.js';
|
|
89
89
|
export * from './codex-rule-context-provider.js';
|
|
90
|
+
export * from './cli-recovery-probe.js';
|
|
91
|
+
export * from './workbuddy-rule-context-provider.js';
|
|
90
92
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
91
93
|
export * from './explicit-owner-statement-prefix.js';
|
|
92
94
|
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
@@ -317,6 +317,8 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
317
317
|
export * from './behavior-example-pack-assembler.js';
|
|
318
318
|
export * from './behavior-history.js';
|
|
319
319
|
export * from './codex-rule-context-provider.js';
|
|
320
|
+
export * from './cli-recovery-probe.js';
|
|
321
|
+
export * from './workbuddy-rule-context-provider.js';
|
|
320
322
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
321
323
|
export * from './explicit-owner-statement-prefix.js';
|
|
322
324
|
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
@@ -133,6 +133,8 @@ export declare function recordVerifiedToolOutcomes(args: {
|
|
|
133
133
|
sessionId: string;
|
|
134
134
|
outcomes: readonly VerifiedToolOutcomeInput[];
|
|
135
135
|
painDatabaseFactory?: PainDatabaseFactory;
|
|
136
|
+
/** SPEC R2: producer host attribution stamped onto inserted rows (upgraded rows keep their original stamp). */
|
|
137
|
+
hostKind?: string;
|
|
136
138
|
}): VerifiedToolOutcomeStatus[];
|
|
137
139
|
/**
|
|
138
140
|
* PRI-640: host attribution is supplied by the host adapter that constructs
|
|
@@ -314,6 +314,22 @@ function ensureToolCallsProjectDirColumn(db) {
|
|
|
314
314
|
// the prepared-statement readiness check below fails loud if absent.
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 R2: additive migration for producer host
|
|
319
|
+
* attribution. Legacy rows stay NULL — the WorkBuddy rule-context reader
|
|
320
|
+
* treats a NULL stamp as unprovable (never guessed), so no backfill exists.
|
|
321
|
+
*/
|
|
322
|
+
function ensureToolCallsHostKindColumn(db) {
|
|
323
|
+
const columns = db.prepare('PRAGMA table_info(tool_calls)').all();
|
|
324
|
+
if (columns.some((row) => pragmaField(row, 'name') === 'host_kind'))
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
db.exec('ALTER TABLE tool_calls ADD COLUMN host_kind TEXT');
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Same concurrent-writer contract as the sibling migrations above.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
317
333
|
/**
|
|
318
334
|
* SPEC-CODEX-PAIN-CLOSURE-001 CR-01: upgrade the live-hook observation of a
|
|
319
335
|
* tool call with the host-verified final result (exit code) read through the
|
|
@@ -342,9 +358,18 @@ export function recordVerifiedToolOutcomes(args) {
|
|
|
342
358
|
if (!hasToolCalls)
|
|
343
359
|
applyTrajectorySchemaBase(db);
|
|
344
360
|
ensureToolCallsToolUseIdColumn(db);
|
|
345
|
-
|
|
361
|
+
ensureToolCallsHostKindColumn(db);
|
|
362
|
+
// Review R3/P2: a KNOWN host's call identity includes its attribution.
|
|
363
|
+
// The lookup matches ONLY rows of the same host — a colliding row from
|
|
364
|
+
// another host (or an unattributed legacy row) is a DIFFERENT record: the
|
|
365
|
+
// caller inserts its own row instead of swallowing or claiming it.
|
|
366
|
+
// Callers without a hostKind keep the historical match-any semantics
|
|
367
|
+
// (explicit compat contract, asserted by tests).
|
|
368
|
+
const findByToolUseId = args.hostKind !== undefined
|
|
369
|
+
? db.prepare('SELECT id, outcome FROM tool_calls WHERE session_id = ? AND tool_use_id = ? AND host_kind = ? ORDER BY id LIMIT 1')
|
|
370
|
+
: db.prepare('SELECT id, outcome FROM tool_calls WHERE session_id = ? AND tool_use_id = ? ORDER BY id LIMIT 1');
|
|
346
371
|
const upgradeUnknown = db.prepare("UPDATE tool_calls SET outcome = ?, exit_code = ?, result_preview = COALESCE(?, result_preview) WHERE id = ? AND outcome = 'unknown'");
|
|
347
|
-
const insertVerified = db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at, tool_use_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
372
|
+
const insertVerified = db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at, tool_use_id, host_kind) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
348
373
|
const upsertSession = db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at');
|
|
349
374
|
const createdAt = new Date().toISOString();
|
|
350
375
|
db.transaction(() => {
|
|
@@ -352,7 +377,9 @@ export function recordVerifiedToolOutcomes(args) {
|
|
|
352
377
|
if (typeof input.exitCode !== 'number' || !Number.isInteger(input.exitCode))
|
|
353
378
|
continue;
|
|
354
379
|
const finalOutcome = input.exitCode === 0 ? 'success' : 'failure';
|
|
355
|
-
const existing =
|
|
380
|
+
const existing = (args.hostKind !== undefined
|
|
381
|
+
? findByToolUseId.get(args.sessionId, input.toolUseId, args.hostKind)
|
|
382
|
+
: findByToolUseId.get(args.sessionId, input.toolUseId));
|
|
356
383
|
const boundedPreview = typeof input.outputPreview === 'string' && input.outputPreview.length > 0
|
|
357
384
|
? input.outputPreview.slice(0, MAX_PREVIEW)
|
|
358
385
|
: null;
|
|
@@ -368,7 +395,7 @@ export function recordVerifiedToolOutcomes(args) {
|
|
|
368
395
|
}
|
|
369
396
|
upsertSession.run(args.sessionId, createdAt, createdAt);
|
|
370
397
|
const sanitizedParams = sanitizeToolParams(input.params ?? {}, args.workspaceDir);
|
|
371
|
-
const info = insertVerified.run(args.sessionId, input.toolName, finalOutcome, null, input.exitCode, null, null, null, null, stable(sanitizedParams), boundedPreview, createdAt, input.toolUseId);
|
|
398
|
+
const info = insertVerified.run(args.sessionId, input.toolName, finalOutcome, null, input.exitCode, null, null, null, null, stable(sanitizedParams), boundedPreview, createdAt, input.toolUseId, args.hostKind ?? null);
|
|
372
399
|
results.push({ status: 'inserted', id: Number(info.lastInsertRowid) });
|
|
373
400
|
}
|
|
374
401
|
})();
|
|
@@ -426,6 +453,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
|
|
|
426
453
|
db.pragma('busy_timeout = 5000');
|
|
427
454
|
ensureToolCallsToolUseIdColumn(db);
|
|
428
455
|
ensureToolCallsProjectDirColumn(db);
|
|
456
|
+
ensureToolCallsHostKindColumn(db);
|
|
429
457
|
if (!hasCanonicalSchema(db)) {
|
|
430
458
|
db.close();
|
|
431
459
|
db = undefined;
|
|
@@ -520,20 +548,30 @@ export function createProductionPainEvidenceHandler(options = {}) {
|
|
|
520
548
|
// CR-01: the host tool-call id is the stable join key for the
|
|
521
549
|
// consent-gated transcript upgrade (recordVerifiedToolOutcomes);
|
|
522
550
|
// content-shape probing stays the fallback for hosts without ids.
|
|
551
|
+
// Review R3/P2: for a known host the identity is host-scoped — a
|
|
552
|
+
// colliding row from ANOTHER host (or an unattributed legacy row) is
|
|
553
|
+
// NOT this delivery's duplicate, so both hosts keep independent
|
|
554
|
+
// records instead of the second being swallowed.
|
|
523
555
|
if (event.context.toolCallId !== undefined
|
|
524
|
-
&&
|
|
556
|
+
&& (options.hostKind !== undefined
|
|
557
|
+
? db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_use_id = ? AND host_kind = ?').get(event.context.sessionId, event.context.toolCallId, options.hostKind)
|
|
558
|
+
: db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_use_id = ?').get(event.context.sessionId, event.context.toolCallId)) !== undefined) {
|
|
525
559
|
duplicate = true;
|
|
526
560
|
duplicateAdmitted = db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?').get(painId) !== undefined;
|
|
527
561
|
return;
|
|
528
562
|
}
|
|
529
|
-
|
|
563
|
+
// Review R3/P2: the content-shape fallback dedup is host-scoped the
|
|
564
|
+
// same way — identical content from two hosts is two records.
|
|
565
|
+
if ((options.hostKind !== undefined
|
|
566
|
+
? db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_name = ? AND params_json = ? AND outcome = ? AND exit_code IS ? AND error_message IS ? AND result_preview IS ? AND host_kind = ?').get(event.context.sessionId, toolName, paramsJson, outcome.outcome, outcome.exitCode, outcome.error ?? null, resultPreview, options.hostKind)
|
|
567
|
+
: db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_name = ? AND params_json = ? AND outcome = ? AND exit_code IS ? AND error_message IS ? AND result_preview IS ?').get(event.context.sessionId, toolName, paramsJson, outcome.outcome, outcome.exitCode, outcome.error ?? null, resultPreview)) !== undefined) {
|
|
530
568
|
duplicate = true;
|
|
531
569
|
duplicateAdmitted = db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?').get(painId) !== undefined;
|
|
532
570
|
return;
|
|
533
571
|
}
|
|
534
572
|
db.prepare(`INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at`).run(event.context.sessionId, createdAt, createdAt);
|
|
535
|
-
db.prepare(`INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at, tool_use_id, project_dir) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
536
|
-
.run(event.context.sessionId, toolName, outcome.outcome, outcome.durationMs ?? null, outcome.exitCode, outcome.error ? outcome.error.split(/[\s:]/, 1)[0] : null, outcome.error ?? null, null, null, paramsJson, resultPreview, createdAt, event.context.toolCallId ?? null, path.resolve(options.projectDir ?? event.context.workspaceDir));
|
|
573
|
+
db.prepare(`INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at, tool_use_id, project_dir, host_kind) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
574
|
+
.run(event.context.sessionId, toolName, outcome.outcome, outcome.durationMs ?? null, outcome.exitCode, outcome.error ? outcome.error.split(/[\s:]/, 1)[0] : null, outcome.error ?? null, null, null, paramsJson, resultPreview, createdAt, event.context.toolCallId ?? null, path.resolve(options.projectDir ?? event.context.workspaceDir), options.hostKind ?? null);
|
|
537
575
|
if (admitted) {
|
|
538
576
|
// Raw observation only (Pain Diagnosis Persistence SPEC §8): the
|
|
539
577
|
// pain row must record WHAT happened, not WHY. Attribution
|
|
@@ -350,21 +350,26 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
350
350
|
// evaluator replay; merely wiring history must not revive old
|
|
351
351
|
// activations. Live execution additionally needs the existing
|
|
352
352
|
// promotion decision's binding to this exact artifact snapshot.
|
|
353
|
-
|
|
353
|
+
// SPEC-RULECODE-HOST-PROMOTION-001 §11.3: the SAME qualification
|
|
354
|
+
// gates every host with a V2 context path — WorkBuddy must not
|
|
355
|
+
// auto-execute historical V2 artifacts stamped for another host
|
|
356
|
+
// (the hostKinds binding below rejects them by the exact same
|
|
357
|
+
// includes() check Codex uses).
|
|
358
|
+
if (options.hostKind === 'codex' || options.hostKind === 'workbuddy') {
|
|
354
359
|
const contract = content.ruleHostInputContract;
|
|
355
360
|
const hosts = isRecord(contract) ? contract.hostKinds : undefined;
|
|
356
361
|
let contentBindingVerified = digestCheck.outcome === 'verified';
|
|
357
362
|
if (activationMode === 'shadow' && row.owner_approved === 1) {
|
|
358
363
|
// Old databases remain readable without writing a migration
|
|
359
364
|
// from this enforcement reader. A legacy approval without a
|
|
360
|
-
// content digest cannot grant
|
|
365
|
+
// content digest cannot grant V2 execution eligibility.
|
|
361
366
|
const approvalColumns = connection.getDb().prepare('PRAGMA table_info(approvals)').all();
|
|
362
367
|
if (approvalColumns.some((column) => isRecord(column) && column.name === 'artifact_digest')) {
|
|
363
368
|
const approval = connection.getDb().prepare(`
|
|
364
369
|
SELECT artifact_digest FROM approvals
|
|
365
370
|
WHERE artifact_id = ? AND channel = 'code_tool_hook' AND status = 'approved'
|
|
366
371
|
ORDER BY decided_at DESC LIMIT 1
|
|
367
|
-
|
|
372
|
+
`).get(artifactId);
|
|
368
373
|
const approvalDigest = isRecord(approval) && typeof approval.artifact_digest === 'string'
|
|
369
374
|
? approval.artifact_digest : null;
|
|
370
375
|
contentBindingVerified = verifyPiArtifactRowDigest(contentRow, contentJson, approvalDigest).outcome === 'verified';
|
|
@@ -375,9 +380,9 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
375
380
|
}
|
|
376
381
|
if (!isRecord(contract) || contract.version !== 1 || contract.contextVersion !== 2
|
|
377
382
|
|| !Array.isArray(hosts) || !hosts.every((host) => typeof host === 'string')
|
|
378
|
-
|| !hosts.includes(
|
|
383
|
+
|| !hosts.includes(options.hostKind) || row.owner_approved !== 1
|
|
379
384
|
|| !contentBindingVerified) {
|
|
380
|
-
addWarning(warnings,
|
|
385
|
+
addWarning(warnings, `${options.hostKind}_rule_context_v2_unqualified: activation=${activationId}`, `regenerate and replay this artifact for ${options.hostKind}, approve it for shadow observation, and use the existing Owner promotion flow for live execution`);
|
|
381
386
|
continue;
|
|
382
387
|
}
|
|
383
388
|
}
|
|
@@ -419,7 +424,18 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
419
424
|
: context === UNAVAILABLE_RULE_CONTEXT
|
|
420
425
|
? (contextFault ?? 'provider_invalid')
|
|
421
426
|
: 'history_unavailable';
|
|
422
|
-
|
|
427
|
+
// SPEC-RULECODE-HOST-PROMOTION-001 R2: a schema-valid
|
|
428
|
+
// unavailable context carries the provider's precise reason
|
|
429
|
+
// (e.g. workbuddy_history_attribution_conflict) — surface it
|
|
430
|
+
// in the warning instead of collapsing every legit
|
|
431
|
+
// unavailability into one opaque class (rc-9).
|
|
432
|
+
const preciseReason = context !== undefined
|
|
433
|
+
&& context !== UNAVAILABLE_RULE_CONTEXT
|
|
434
|
+
&& typeof context.history.unavailableReason === 'string'
|
|
435
|
+
&& context.history.unavailableReason.length > 0
|
|
436
|
+
? `:${context.history.unavailableReason}`
|
|
437
|
+
: '';
|
|
438
|
+
addWarning(warnings, `rule_context_v2_unavailable: ${fault}${preciseReason} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
|
|
423
439
|
continue;
|
|
424
440
|
}
|
|
425
441
|
const inputContract = content.ruleHostInputContract;
|
|
@@ -547,6 +563,7 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
547
563
|
ruleId: candidate.ruleId,
|
|
548
564
|
activationId: candidate.implId,
|
|
549
565
|
activationMode: 'shadow',
|
|
566
|
+
...(options.hostKind !== undefined ? { hostKind: options.hostKind } : {}),
|
|
550
567
|
});
|
|
551
568
|
}
|
|
552
569
|
}
|
|
@@ -604,6 +621,7 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
604
621
|
...(result?.ruleId !== undefined ? { ruleId: result.ruleId } : {}),
|
|
605
622
|
...(liveActivationId !== undefined ? { activationId: liveActivationId } : {}),
|
|
606
623
|
activationMode: 'live',
|
|
624
|
+
...(options.hostKind !== undefined ? { hostKind: options.hostKind } : {}),
|
|
607
625
|
},
|
|
608
626
|
...shadowEvaluations,
|
|
609
627
|
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
|
+
import type { RuleContextProvider } from './production-rulehost-gate.js';
|
|
3
|
+
export interface WorkbuddyRuleContextProviderOptions {
|
|
4
|
+
/** The WorkBuddy governance workspace whose trajectory database is authoritative. */
|
|
5
|
+
workspaceDir: string;
|
|
6
|
+
/** The host project cwd the hook observed (payload cwd); history rows must have this exact stored scope. Absent on channels that carry none. */
|
|
7
|
+
projectDir?: string;
|
|
8
|
+
/** The existing `rulecode_context_v2` gate. */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** Timestamp captured immediately after decoding this hook event. */
|
|
11
|
+
cutoffAt: Date;
|
|
12
|
+
/** Host-authoritative current WorkBuddy call_id, excluded even on timestamp ties. */
|
|
13
|
+
currentToolCallId: string;
|
|
14
|
+
toolSemantics: ToolSemanticRegistry;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 C: the WorkBuddy RuleContext provider.
|
|
18
|
+
*
|
|
19
|
+
* It reads the SAME canonical trajectory store the shared afterToolCall
|
|
20
|
+
* production path writes for WorkBuddy (`tool_calls` rows carrying the
|
|
21
|
+
* host's natural session/call ids and the observed project cwd) — genuinely
|
|
22
|
+
* observable WorkBuddy data, never borrowed Codex history. WorkBuddy has no
|
|
23
|
+
* design-gate declaration entry point (the Codex-native attestation source
|
|
24
|
+
* is Codex-only), so this provider deliberately supplies NO designGate
|
|
25
|
+
* fact: a rule whose required facts include design_approval stays suspended
|
|
26
|
+
* on this host with the precise `rule_required_fact_unavailable` warning
|
|
27
|
+
* instead of inheriting another host's attestation.
|
|
28
|
+
*
|
|
29
|
+
* Disabled, unscoped or unverifiable sources return explicit unavailable
|
|
30
|
+
* contexts — v2 rules then suspend through the existing shared-gate
|
|
31
|
+
* contract, which is the honest state until the missing input exists.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createWorkbuddyRuleContextProvider(options: WorkbuddyRuleContextProviderOptions): RuleContextProvider;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { computeBehaviorFacts, } from '@principles/core/runtime-v2';
|
|
3
|
+
import { assembleBehaviorHistory } from './behavior-history.js';
|
|
4
|
+
import { readWorkspaceRuleContextRowsBefore } from './workspace-behavior-evidence.js';
|
|
5
|
+
const HISTORY_LIMIT = 100;
|
|
6
|
+
const MAX_REASON_LENGTH = 240;
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function unavailable(reason) {
|
|
11
|
+
const history = {
|
|
12
|
+
status: 'unavailable',
|
|
13
|
+
unavailableReason: reason.replace(/\s+/g, ' ').slice(0, MAX_REASON_LENGTH),
|
|
14
|
+
truncated: false,
|
|
15
|
+
calls: [],
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
version: 2,
|
|
19
|
+
history,
|
|
20
|
+
facts: computeBehaviorFacts(history, null, null),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* SPEC-RULECODE-HOST-PROMOTION-001 C: the WorkBuddy RuleContext provider.
|
|
25
|
+
*
|
|
26
|
+
* It reads the SAME canonical trajectory store the shared afterToolCall
|
|
27
|
+
* production path writes for WorkBuddy (`tool_calls` rows carrying the
|
|
28
|
+
* host's natural session/call ids and the observed project cwd) — genuinely
|
|
29
|
+
* observable WorkBuddy data, never borrowed Codex history. WorkBuddy has no
|
|
30
|
+
* design-gate declaration entry point (the Codex-native attestation source
|
|
31
|
+
* is Codex-only), so this provider deliberately supplies NO designGate
|
|
32
|
+
* fact: a rule whose required facts include design_approval stays suspended
|
|
33
|
+
* on this host with the precise `rule_required_fact_unavailable` warning
|
|
34
|
+
* instead of inheriting another host's attestation.
|
|
35
|
+
*
|
|
36
|
+
* Disabled, unscoped or unverifiable sources return explicit unavailable
|
|
37
|
+
* contexts — v2 rules then suspend through the existing shared-gate
|
|
38
|
+
* contract, which is the honest state until the missing input exists.
|
|
39
|
+
*/
|
|
40
|
+
export function createWorkbuddyRuleContextProvider(options) {
|
|
41
|
+
const workspaceDir = path.resolve(options.workspaceDir);
|
|
42
|
+
const projectDir = options.projectDir !== undefined ? path.resolve(options.projectDir) : undefined;
|
|
43
|
+
return (request) => {
|
|
44
|
+
if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0) {
|
|
45
|
+
return unavailable('session_id_invalid');
|
|
46
|
+
}
|
|
47
|
+
if (!options.enabled)
|
|
48
|
+
return unavailable('workbuddy_rule_context_disabled');
|
|
49
|
+
if (path.resolve(request.workspaceDir) !== workspaceDir)
|
|
50
|
+
return unavailable('workspace_scope_mismatch');
|
|
51
|
+
if (projectDir === undefined || !path.isAbsolute(projectDir)) {
|
|
52
|
+
return unavailable('workbuddy_project_scope_unavailable');
|
|
53
|
+
}
|
|
54
|
+
const raw = request.rawPayload;
|
|
55
|
+
const toolInput = isRecord(raw) && isRecord(raw.toolInput) ? raw.toolInput : null;
|
|
56
|
+
if (toolInput === null
|
|
57
|
+
|| toolInput.toolName !== request.toolName
|
|
58
|
+
|| !isRecord(toolInput.params)
|
|
59
|
+
|| options.currentToolCallId.trim().length === 0) {
|
|
60
|
+
return unavailable('workbuddy_pre_tool_scope_mismatch');
|
|
61
|
+
}
|
|
62
|
+
const cutoff = options.cutoffAt;
|
|
63
|
+
if (!(cutoff instanceof Date) || !Number.isFinite(cutoff.getTime()))
|
|
64
|
+
return unavailable('history_cutoff_invalid');
|
|
65
|
+
try {
|
|
66
|
+
const historyRows = readWorkspaceRuleContextRowsBefore(workspaceDir, {
|
|
67
|
+
sessionId: request.sessionId,
|
|
68
|
+
projectDir,
|
|
69
|
+
beforeCreatedAt: cutoff.toISOString(),
|
|
70
|
+
excludeToolUseId: options.currentToolCallId,
|
|
71
|
+
limit: HISTORY_LIMIT,
|
|
72
|
+
// Review R2: consume ONLY rows PROVEN to be WorkBuddy's (stamped by
|
|
73
|
+
// this host's own writer). "Same workspace + same session id +
|
|
74
|
+
// same project" is NOT proof — a Codex session id colliding here
|
|
75
|
+
// must never lend its reads to a WorkBuddy read-before-write rule.
|
|
76
|
+
hostKind: 'workbuddy',
|
|
77
|
+
});
|
|
78
|
+
if (historyRows.unscopedPriorRows)
|
|
79
|
+
return unavailable('trajectory_project_scope_incomplete');
|
|
80
|
+
if ((historyRows.otherHostRows ?? 0) > 0) {
|
|
81
|
+
return unavailable('workbuddy_history_attribution_conflict');
|
|
82
|
+
}
|
|
83
|
+
if (historyRows.rows.length === 0) {
|
|
84
|
+
// Fail closed with the precise reason: legacy rows exist but cannot
|
|
85
|
+
// be PROVEN WorkBuddy's (never back-filled, never assumed).
|
|
86
|
+
if ((historyRows.unattributedRows ?? 0) > 0)
|
|
87
|
+
return unavailable('workbuddy_history_attribution_unprovable');
|
|
88
|
+
return unavailable('no_prior_session_history');
|
|
89
|
+
}
|
|
90
|
+
if ((historyRows.unattributedRows ?? 0) > 0) {
|
|
91
|
+
// Mixed window: consuming only the proven subset could UNDERCOUNT
|
|
92
|
+
// prior reads (a hidden foreign read would flip priorReadOfTarget),
|
|
93
|
+
// so the window is unprovable as a whole — suspend precisely.
|
|
94
|
+
return unavailable('workbuddy_history_attribution_unprovable');
|
|
95
|
+
}
|
|
96
|
+
const history = assembleBehaviorHistory(historyRows.rows, historyRows.truncated, {
|
|
97
|
+
projectDir,
|
|
98
|
+
toolSemantics: options.toolSemantics,
|
|
99
|
+
});
|
|
100
|
+
// WorkBuddy exposes its shell as the Bash tool, and MCP-style tools may
|
|
101
|
+
// resolve to 'other' absent from this host's semantic registry. Either
|
|
102
|
+
// may read or mutate files, so target-read absence and exact action
|
|
103
|
+
// counts cannot be asserted from this partial semantic view (same
|
|
104
|
+
// conservative rule as the Codex provider).
|
|
105
|
+
const factsIncomplete = history.truncated || history.calls.some((call) => call.canonicalKind === 'execute' || call.canonicalKind === 'other');
|
|
106
|
+
const computedFacts = computeBehaviorFacts(history, factsIncomplete ? null : request.targetPath, null);
|
|
107
|
+
const facts = factsIncomplete
|
|
108
|
+
? {
|
|
109
|
+
priorReadOfTarget: 'unknown',
|
|
110
|
+
readCount: null,
|
|
111
|
+
writeCount: null,
|
|
112
|
+
uniqueWritePathCount: null,
|
|
113
|
+
sameActionBlockCount: null,
|
|
114
|
+
}
|
|
115
|
+
: computedFacts;
|
|
116
|
+
return {
|
|
117
|
+
version: 2,
|
|
118
|
+
history,
|
|
119
|
+
facts,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|