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.
Files changed (80) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/ingestion/ingestion.js +3 -1
  3. package/codex-adapter/package.json +1 -1
  4. package/console/dist/server/models/ActivationsConsoleModel.d.ts +19 -2
  5. package/console/dist/server/models/ActivationsConsoleModel.js +124 -10
  6. package/console/dist/server/models/OwnerDecisionConsoleModel.d.ts +3 -1
  7. package/console/dist/server/models/OwnerDecisionConsoleModel.js +6 -2
  8. package/console/dist/server/routes/activations.js +7 -2
  9. package/console/dist/server/routes/owner-decisions.js +1 -1
  10. package/core/dist/runtime-v2/activation/index.d.ts +6 -0
  11. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  12. package/core/dist/runtime-v2/activation/index.js +3 -0
  13. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  14. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts +75 -0
  15. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts.map +1 -1
  16. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js +131 -37
  17. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js.map +1 -1
  18. package/core/dist/runtime-v2/activation/production-gate-deps.d.ts.map +1 -1
  19. package/core/dist/runtime-v2/activation/production-gate-deps.js +7 -3
  20. package/core/dist/runtime-v2/activation/production-gate-deps.js.map +1 -1
  21. package/core/dist/runtime-v2/activation/recovery-composition.d.ts +54 -0
  22. package/core/dist/runtime-v2/activation/recovery-composition.d.ts.map +1 -0
  23. package/core/dist/runtime-v2/activation/recovery-composition.js +129 -0
  24. package/core/dist/runtime-v2/activation/recovery-composition.js.map +1 -0
  25. package/core/dist/runtime-v2/activation/recovery-control-surfaces.d.ts +62 -0
  26. package/core/dist/runtime-v2/activation/recovery-control-surfaces.d.ts.map +1 -0
  27. package/core/dist/runtime-v2/activation/recovery-control-surfaces.js +84 -0
  28. package/core/dist/runtime-v2/activation/recovery-control-surfaces.js.map +1 -0
  29. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts +13 -1
  30. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts.map +1 -1
  31. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js +24 -1
  32. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js.map +1 -1
  33. package/core/dist/runtime-v2/index.d.ts +2 -2
  34. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  35. package/core/dist/runtime-v2/index.js +1 -1
  36. package/core/dist/runtime-v2/index.js.map +1 -1
  37. package/core/dist/runtime-v2/trajectory-schema.d.ts.map +1 -1
  38. package/core/dist/runtime-v2/trajectory-schema.js +12 -1
  39. package/core/dist/runtime-v2/trajectory-schema.js.map +1 -1
  40. package/core/dist/runtime-v2/types/event-types.d.ts +10 -0
  41. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  42. package/core/dist/runtime-v2/types/event-types.js +1 -0
  43. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  44. package/core/package.json +1 -1
  45. package/host-runtime/dist/cli-recovery-probe.d.ts +30 -0
  46. package/host-runtime/dist/cli-recovery-probe.js +169 -0
  47. package/host-runtime/dist/governance-signal-admission.js +22 -5
  48. package/host-runtime/dist/host-liveness-contract.d.ts +51 -28
  49. package/host-runtime/dist/host-liveness-contract.js +143 -46
  50. package/host-runtime/dist/index.d.ts +2 -0
  51. package/host-runtime/dist/index.js +2 -0
  52. package/host-runtime/dist/production-pain-evidence.d.ts +2 -0
  53. package/host-runtime/dist/production-pain-evidence.js +46 -8
  54. package/host-runtime/dist/production-rulehost-gate.js +24 -6
  55. package/host-runtime/dist/workbuddy-rule-context-provider.d.ts +33 -0
  56. package/host-runtime/dist/workbuddy-rule-context-provider.js +126 -0
  57. package/host-runtime/dist/workspace-behavior-evidence.d.ts +22 -5
  58. package/host-runtime/dist/workspace-behavior-evidence.js +41 -11
  59. package/host-runtime/package.json +1 -1
  60. package/package.json +2 -2
  61. package/pd-cli/dist/commands/recovery-reachability-probe.d.ts +25 -0
  62. package/pd-cli/dist/commands/recovery-reachability-probe.d.ts.map +1 -0
  63. package/pd-cli/dist/commands/recovery-reachability-probe.js +50 -0
  64. package/pd-cli/dist/commands/recovery-reachability-probe.js.map +1 -0
  65. package/pd-cli/dist/commands/runtime-activation.d.ts +10 -1
  66. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  67. package/pd-cli/dist/commands/runtime-activation.js +81 -3
  68. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  69. package/pd-cli/package.json +1 -1
  70. package/plugin/dist/bundle.js +385 -385
  71. package/plugin/dist/governance-audit.js +1 -1
  72. package/plugin/dist/openclaw.plugin.json +1 -1
  73. package/plugin/dist/rulehost-evidence.js +1 -1
  74. package/plugin/openclaw.plugin.json +1 -1
  75. package/plugin/package.json +1 -1
  76. package/release-manager/package.json +2 -2
  77. package/workbuddy-adapter/dist/codec/input-decoder.d.ts +14 -11
  78. package/workbuddy-adapter/dist/codec/input-decoder.js +28 -14
  79. package/workbuddy-adapter/dist/pd-hook.js +31 -12
  80. 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
- { capabilityId: 'pd_status', hostToolAliases: ['bash', 'exec_command'] },
12
- { capabilityId: 'rulecode_deactivate', hostToolAliases: ['bash', 'exec_command'] },
13
- { capabilityId: 'rulecode_global_pause', hostToolAliases: ['bash', 'exec_command'] },
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
- { probeId: 'probe-pd-status', capabilityId: 'pd_status', toolName: 'bash', params: { command: 'pd status' }, expectedDecision: 'allow' },
18
- { probeId: 'probe-rule-deactivate', capabilityId: 'rulecode_deactivate', toolName: 'bash', params: { command: 'pd activation deactivate --activation-id test' }, expectedDecision: 'allow' },
19
- { probeId: 'probe-global-pause', capabilityId: 'rulecode_global_pause', toolName: 'bash', params: { command: 'pd activation emergency-pause' }, expectedDecision: 'allow' },
20
- { probeId: 'probe-owner-review', capabilityId: 'owner_review_access', toolName: 'owner_review_access', params: {}, expectedDecision: 'allow' },
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
- * PRI-813: resolve the promotion host-liveness contract from the workspace's
111
- * REAL host state instead of unconditionally inheriting the OpenClaw
112
- * contract (which made Codex workspaces report runtime_compatibility /
113
- * runtime_shadow_evidence as passed — a false capability claim; the promotion
114
- * then only ever failed on sample count, hiding the unsupported truth).
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
- * Resolution rules (fail closed outside the one host with an approved
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 intentionally creates no Codex contract, no capability
140
- * registry, and no new store — it only routes the existing single authority
141
- * truthfully over state that already exists.
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, now via the
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
- // The promotion host can only be the OpenClaw contract when nothing on the
156
- // workspace — neither a declaration nor behavioral evidence — says Codex.
157
- if (!effectiveKinds.includes('codex')) {
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
- if (!declaredKinds.includes('codex')) {
167
- // Codex evidence exists (declaration gone, or only openclaw.json
168
- // remains) — the exact state where a silent OpenClaw fallback would
169
- // resurrect the false capability PASS (Owner review round-4).
170
- return promotionHostFailure('host_declaration_missing_for_configured_host', effectiveKinds, 'Codex evidence exists in this workspace but .pd/host-tool-semantics/codex.json is missing; re-run the Codex worker so it re-persists its declaration (or deactivate the Codex activation) before promoting');
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
- if (declaredKinds.length > 1) {
173
- return promotionHostFailure('multi_host_promotion_ambiguous', effectiveKinds, `the workspace declares multiple hosts (${declaredKinds.join(', ')}); promotion requires an unambiguous governing host — decide the promotion target host first`);
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
- return promotionHostFailure('promotion_host_unsupported', effectiveKinds, `host '${declaredKinds[0]}' has no approved promotion capability authority; promotion on this host is explicitly unsupported until an evidence-backed capability decision exists`);
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
- const findByToolUseId = db.prepare('SELECT id, outcome FROM tool_calls WHERE session_id = ? AND tool_use_id = ? ORDER BY id LIMIT 1');
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 = findByToolUseId.get(args.sessionId, input.toolUseId);
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
- && db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_use_id = ?').get(event.context.sessionId, event.context.toolCallId) !== undefined) {
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
- if (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)) {
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
- if (options.hostKind === 'codex') {
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 Codex V2 execution eligibility.
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
- `).get(artifactId);
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('codex') || row.owner_approved !== 1
383
+ || !hosts.includes(options.hostKind) || row.owner_approved !== 1
379
384
  || !contentBindingVerified) {
380
- addWarning(warnings, `codex_rule_context_v2_unqualified: activation=${activationId}`, 'regenerate and replay this artifact for Codex, approve it for shadow observation, and use the existing Owner promotion flow for live execution');
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
- addWarning(warnings, `rule_context_v2_unavailable: ${fault} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
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
+ }