create-principles-disciple 1.147.1 → 1.147.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/ingestion/admission.d.ts +5 -10
  3. package/codex-adapter/dist/ingestion/admission.js +19 -77
  4. package/codex-adapter/dist/ingestion/explicit-owner-statement.js +1 -1
  5. package/codex-adapter/dist/ingestion/ingestion.d.ts +0 -2
  6. package/codex-adapter/dist/ingestion/ingestion.js +6 -4
  7. package/codex-adapter/dist/pd-hook.js +5 -7
  8. package/codex-adapter/package.json +1 -1
  9. package/core/dist/runtime-v2/evidence-triage/index.d.ts +0 -1
  10. package/core/dist/runtime-v2/evidence-triage/index.d.ts.map +1 -1
  11. package/core/dist/runtime-v2/evidence-triage/index.js +0 -2
  12. package/core/dist/runtime-v2/evidence-triage/index.js.map +1 -1
  13. package/core/dist/runtime-v2/evidence-triage/source-descriptors.d.ts +3 -8
  14. package/core/dist/runtime-v2/evidence-triage/source-descriptors.d.ts.map +1 -1
  15. package/core/dist/runtime-v2/evidence-triage/source-descriptors.js +5 -17
  16. package/core/dist/runtime-v2/evidence-triage/source-descriptors.js.map +1 -1
  17. package/core/dist/runtime-v2/evidence-triage/triage-policy.d.ts +11 -9
  18. package/core/dist/runtime-v2/evidence-triage/triage-policy.d.ts.map +1 -1
  19. package/core/dist/runtime-v2/evidence-triage/triage-policy.js +10 -103
  20. package/core/dist/runtime-v2/evidence-triage/triage-policy.js.map +1 -1
  21. package/core/dist/runtime-v2/evidence-triage/trigger-controller.js +1 -1
  22. package/core/dist/runtime-v2/evidence-triage/trigger-controller.js.map +1 -1
  23. package/core/dist/runtime-v2/evidence-triage/types.d.ts +4 -16
  24. package/core/dist/runtime-v2/evidence-triage/types.d.ts.map +1 -1
  25. package/core/dist/runtime-v2/evidence-triage/types.js.map +1 -1
  26. package/core/dist/runtime-v2/index.d.ts +1 -1
  27. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  28. package/core/dist/runtime-v2/index.js +1 -1
  29. package/core/dist/runtime-v2/index.js.map +1 -1
  30. package/core/package.json +1 -1
  31. package/host-runtime/dist/governance-signal-admission.d.ts +1 -77
  32. package/host-runtime/dist/governance-signal-admission.js +34 -294
  33. package/host-runtime/dist/production-pain-evidence.d.ts +8 -69
  34. package/host-runtime/dist/production-pain-evidence.js +19 -157
  35. package/host-runtime/package.json +1 -1
  36. package/package.json +3 -3
  37. package/pd-cli/package.json +1 -1
  38. package/pd-mcp-server/dist/version.d.ts +1 -1
  39. package/pd-mcp-server/dist/version.js +1 -1
  40. package/pd-mcp-server/package.json +1 -1
  41. package/plugin/dist/bundle.js +411 -412
  42. package/plugin/dist/hooks/after-tool-call-helpers.d.ts +2 -35
  43. package/plugin/dist/hooks/after-tool-call-types.d.ts +1 -30
  44. package/plugin/dist/hooks/pain.d.ts +6 -19
  45. package/plugin/dist/hooks/trajectory-evidence.d.ts +3 -10
  46. package/plugin/dist/hooks/triage-adapter.d.ts +7 -20
  47. package/plugin/dist/host-runtime/openclaw-host-runtime.d.ts +0 -1
  48. package/plugin/dist/openclaw.plugin.json +1 -1
  49. package/plugin/openclaw.plugin.json +1 -1
  50. package/plugin/package.json +1 -1
  51. package/release-manager/package.json +1 -1
  52. package/workbuddy-adapter/dist/pd-hook.js +0 -26
  53. package/workbuddy-adapter/dist/worker/workspace-worker.d.ts +1 -7
  54. package/workbuddy-adapter/dist/worker/workspace-worker.js +11 -21
  55. package/workbuddy-adapter/package.json +1 -1
@@ -35,8 +35,8 @@ import fs from 'node:fs';
35
35
  import path from 'node:path';
36
36
  import { randomUUID } from 'node:crypto';
37
37
  import Database from 'better-sqlite3';
38
- import { collectSync, computeFeatureFlagsFromConfig, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, evaluateStage2ConfidenceGate, evaluatePainIngress, STAGE2_MAX_ATTEMPTS, PENDING_CANDIDATE_TTL_MS, } from '@principles/core/runtime-v2';
39
- import { deriveProductionCorrectionPainIdentity, deriveProductionToolPainIdentity, hasProductionPainSchema, PRODUCTION_WRITE_TOOLS, } from './production-pain-evidence.js';
38
+ import { collectSync, computeFeatureFlagsFromConfig, CORRECTION_SEED_KEYWORDS, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, evaluateStage2ConfidenceGate, evaluatePainIngress, STAGE2_MAX_ATTEMPTS, PENDING_CANDIDATE_TTL_MS, } from '@principles/core/runtime-v2';
39
+ import { deriveProductionCorrectionPainIdentity, hasProductionPainSchema, } from './production-pain-evidence.js';
40
40
  import { promoteGovernanceEvidence, ensureGovernanceSchema } from './governance-observation-store.js';
41
41
  import { loadPdConfigForPlugin } from './pd-config.js';
42
42
  // ─── Shared detector constants (one truth for both hosts) ───────────────────
@@ -305,8 +305,8 @@ function ensureGovernanceAdmissionSchema(db) {
305
305
  db.transaction(() => {
306
306
  for (const statement of ADMISSION_CREATE_STATEMENTS)
307
307
  db.exec(statement);
308
- // SPEC-CODEX-PAIN-CLOSURE-001 CR-01: this store also writes tool_calls
309
- // (admitToolFailure); make sure the host tool-use-id join column exists on
308
+ // SPEC-CODEX-PAIN-CLOSURE-001 CR-01: trajectory evidence writers persist
309
+ // tool_calls; make sure the host tool-use-id join column exists on
310
310
  // workspaces whose base schema predates it (idempotent additive ALTER —
311
311
  // only when the table itself exists: governance-only databases without a
312
312
  // tool_calls table are a valid pre-existing state, not an error).
@@ -439,29 +439,6 @@ function insertCorrectionPain({ db, candidate, workspaceDir, painId, reason, now
439
439
  .run(candidate.rootSessionId, 'user_correction', GOVERNANCE_STRONG_PAIN_SCORE, reason.slice(0, MAX_REASON_BOUND), 'severe', 'system_infer', null, excerpt, painId, null, candidate.hostKind, nowIso);
440
440
  return reason;
441
441
  }
442
- /**
443
- * ONE builder for the tool_failure task payload shape — the Codex inline
444
- * admission path and the WorkBuddy bare-pain sweep must produce the SAME
445
- * shape, differing only in the attributed host and the pain facts.
446
- */
447
- function buildToolFailureTaskPayload(fields) {
448
- // P1-1 privacy (review round 2): sanitize the FULL text before any
449
- // truncation — a token crossing the 200-char boundary must be redacted, not
450
- // split into a fragment that survives verbatim. sanitizeString bounds itself.
451
- const payload = {
452
- painType: 'tool_failure',
453
- source: fields.toolName,
454
- reason: fields.reason || `tool=${fields.toolName}`,
455
- score: fields.score,
456
- sessionId: fields.sessionId,
457
- hostKind: fields.hostKind,
458
- evidence: [{ sourceRef: `governance_observation:${fields.logicalObservationKey}`, note: sanitizeString(fields.reason || fields.toolName, fields.workspaceDir) }],
459
- };
460
- return payload;
461
- }
462
- function toolFailureTaskPayload(fields) {
463
- return JSON.stringify(buildToolFailureTaskPayload(fields));
464
- }
465
442
  /**
466
443
  * P1 (review round 3): a `behavior_trace` may only name a row that EXISTS in
467
444
  * trajectory.db with the SAME lineage tuple. WorkBuddy currently has no
@@ -486,47 +463,36 @@ function persistedBehaviorTraceEvidence(args) {
486
463
  }
487
464
  function buildTaskSubmissionPayload(candidate, workspaceDir, details) {
488
465
  const { reason, db } = details;
489
- if (candidate.kind === 'user_correction') {
490
- // P1-1 privacy (review round 2): sanitize the FULL text before any
491
- // truncation — a token crossing the 200-char boundary must be redacted, not
492
- // split into a fragment that survives verbatim. sanitizeString bounds itself.
493
- const payload = {
494
- painType: 'user_frustration',
495
- source: 'user_correction',
496
- reason: reason || 'User correction detected',
497
- score: GOVERNANCE_STRONG_PAIN_SCORE,
498
- sessionId: candidate.rootSessionId,
499
- hostKind: candidate.hostKind,
500
- evidence: persistedBehaviorTraceEvidence({
501
- db,
502
- hostKind: candidate.hostKind,
503
- logicalObservationKey: candidate.logicalObservationKey,
504
- rolloutIdentity: candidate.rolloutIdentity,
505
- rootSessionId: candidate.rootSessionId,
506
- hostTurnId: candidate.hostTurnId,
507
- text: candidate.text,
508
- workspaceDir,
509
- }),
510
- // SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
511
- // Owner statement rides as classified correction evidence — never
512
- // summarized by an AI, never inside evidence[] pseudo-trace entries.
513
- ...(candidate.explicitOwnerStatement === true
514
- ? { correctionEvidence: candidate.hostKind === 'workbuddy'
515
- ? { text: boundCorrectionEvidenceText({ text: candidate.text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }, workspaceDir).text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }
516
- : { text: sanitizeString(candidate.text, workspaceDir), classification: 'owner_statement', channel: candidate.explicitChannel === 'codex_explicit_agent' ? 'codex_explicit_agent' : 'codex_explicit_prefix' } }
517
- : {}),
518
- };
519
- return payload;
520
- }
521
- return buildToolFailureTaskPayload({
522
- hostKind: candidate.hostKind,
523
- logicalObservationKey: candidate.logicalObservationKey,
466
+ // P1-1 privacy (review round 2): sanitize the FULL text before any
467
+ // truncation — a token crossing the 200-char boundary must be redacted, not
468
+ // split into a fragment that survives verbatim. sanitizeString bounds itself.
469
+ const payload = {
470
+ painType: 'user_frustration',
471
+ source: 'user_correction',
472
+ reason: reason || 'User correction detected',
473
+ score: GOVERNANCE_STRONG_PAIN_SCORE,
524
474
  sessionId: candidate.rootSessionId,
525
- toolName: candidate.toolName,
526
- reason,
527
- score: 70,
528
- workspaceDir,
529
- });
475
+ hostKind: candidate.hostKind,
476
+ evidence: persistedBehaviorTraceEvidence({
477
+ db,
478
+ hostKind: candidate.hostKind,
479
+ logicalObservationKey: candidate.logicalObservationKey,
480
+ rolloutIdentity: candidate.rolloutIdentity,
481
+ rootSessionId: candidate.rootSessionId,
482
+ hostTurnId: candidate.hostTurnId,
483
+ text: candidate.text,
484
+ workspaceDir,
485
+ }),
486
+ // SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
487
+ // Owner statement rides as classified correction evidence — never
488
+ // summarized by an AI, never inside evidence[] pseudo-trace entries.
489
+ ...(candidate.explicitOwnerStatement === true
490
+ ? { correctionEvidence: candidate.hostKind === 'workbuddy'
491
+ ? { text: boundCorrectionEvidenceText({ text: candidate.text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }, workspaceDir).text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }
492
+ : { text: sanitizeString(candidate.text, workspaceDir), classification: 'owner_statement', channel: candidate.explicitChannel === 'codex_explicit_agent' ? 'codex_explicit_agent' : 'codex_explicit_prefix' } }
493
+ : {}),
494
+ };
495
+ return payload;
530
496
  }
531
497
  function taskSubmissionPayload(candidate, workspaceDir, details) {
532
498
  return JSON.stringify(buildTaskSubmissionPayload(candidate, workspaceDir, details));
@@ -1040,106 +1006,6 @@ function admitCorrection({ db, candidate, workspaceDir, now, nowIso, keywordStor
1040
1006
  }
1041
1007
  return { disposition: 'admitted', kind: 'user_correction', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: detection?.ruleVersion ?? null };
1042
1008
  }
1043
- function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
1044
- const identity = deriveProductionToolPainIdentity({
1045
- workspaceDir,
1046
- sessionId: candidate.rootSessionId,
1047
- turnId: candidate.hostTurnId,
1048
- toolName: candidate.toolName,
1049
- source: candidate.source,
1050
- // I1 identity parity: same call id the live handler derived from (the
1051
- // candidate builder forwards the host `tool_use_id`).
1052
- ...(candidate.toolCallId !== undefined ? { toolCallId: candidate.toolCallId } : {}),
1053
- ...(candidate.toolInput !== undefined ? { toolInput: candidate.toolInput } : {}),
1054
- ...(candidate.toolOutput !== undefined ? { toolOutput: candidate.toolOutput } : {}),
1055
- });
1056
- const { outcome, sanitizedParams, paramsJson, resultPreview, painId } = identity;
1057
- // Admission gate — identical semantics to the live production handler on a
1058
- // fresh process (ADR-0020 accepts cold in-memory cooldowns for Codex;
1059
- // canonical idempotency is the guard).
1060
- const sourceObservation = buildToolFailureObservation({ toolName: candidate.toolName, error: outcome.error, exitCode: outcome.exitCode ?? undefined });
1061
- const sourceKind = resolveSourceKind({
1062
- observedAt: nowIso,
1063
- workspaceId: workspaceDir,
1064
- sessionId: candidate.rootSessionId,
1065
- toolName: candidate.toolName,
1066
- failureSource: sourceObservation.failureSource,
1067
- toolNotFound: sourceObservation.toolNotFound,
1068
- nonZeroExit: outcome.exitCode !== null && outcome.exitCode !== 0,
1069
- });
1070
- // Risk evaluation mirrors the production handler: it reads the RAW params
1071
- // (sanitized params replace paths with placeholders and would lose the
1072
- // absolute-path risk signal). Identity still uses sanitized params.
1073
- const rawFile = isRecord(outcome.params) ? (own(outcome.params, 'file_path') ?? own(outcome.params, 'path')) : undefined;
1074
- const relativePath = String(rawFile ?? 'unknown').slice(0, 500);
1075
- const isRisky = path.isAbsolute(relativePath) && !path.resolve(relativePath).startsWith(`${workspaceDir}${path.sep}`);
1076
- const painScore = Math.min(100, (outcome.failure ? 70 : 0) + (isRisky ? 20 : 0));
1077
- const triage = evaluateTriage({ sourceKind, score: painScore, isRisky });
1078
- const trigger = evaluateTriggerController({ triageResult: triage, isOwnerManual: false, isCooldownActive: false, isValid: true, score: painScore, sessionId: candidate.rootSessionId });
1079
- const gateAdmitted = outcome.failure && PRODUCTION_WRITE_TOOLS.has(candidate.toolName) && trigger.shouldCreateDiagnosticTask;
1080
- void sanitizedParams;
1081
- if (!gateAdmitted) {
1082
- return { disposition: 'not_admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, reason: trigger.reason };
1083
- }
1084
- let duplicate = false;
1085
- let duplicateWithoutPain = false;
1086
- let reason = '';
1087
- db.transaction(() => {
1088
- // Same duplicate probes as the production handler: the host tool-call id
1089
- // (CR-01) is the stable join key; the content-shape probe stays the
1090
- // fallback for hosts/candidates without ids.
1091
- // Review R3/P2: admission dedup is host-scoped — a colliding row from
1092
- // another host is not this candidate's duplicate.
1093
- if (candidate.toolUseId !== undefined
1094
- && (candidate.hostKind !== undefined
1095
- ? db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_use_id = ? AND host_kind = ?').get(candidate.rootSessionId, candidate.toolUseId, candidate.hostKind)
1096
- : db.prepare('SELECT 1 FROM tool_calls WHERE session_id = ? AND tool_use_id = ?').get(candidate.rootSessionId, candidate.toolUseId)) !== undefined) {
1097
- duplicate = true;
1098
- duplicateWithoutPain = db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?').get(painId) === undefined;
1099
- }
1100
- else if ((candidate.hostKind !== undefined
1101
- ? 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 = ?')
1102
- .get(candidate.rootSessionId, candidate.toolName, paramsJson, outcome.outcome, outcome.exitCode, outcome.error ?? null, resultPreview, candidate.hostKind)
1103
- : 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 ?')
1104
- .get(candidate.rootSessionId, candidate.toolName, paramsJson, outcome.outcome, outcome.exitCode, outcome.error ?? null, resultPreview)) !== undefined) {
1105
- duplicate = true;
1106
- duplicateWithoutPain = db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?').get(painId) === undefined;
1107
- }
1108
- else {
1109
- db.prepare(`INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at`)
1110
- .run(candidate.rootSessionId, nowIso, nowIso);
1111
- const projectDir = typeof candidate.projectDir === 'string' && path.isAbsolute(candidate.projectDir)
1112
- ? path.resolve(candidate.projectDir)
1113
- : null;
1114
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1115
- .run(candidate.rootSessionId, candidate.toolName, outcome.outcome, outcome.durationMs ?? null, outcome.exitCode, outcome.error ? outcome.error.split(/[\s:]/, 1)[0] : null, outcome.error ?? null, null, null, paramsJson, resultPreview, nowIso, candidate.toolUseId ?? null, projectDir, candidate.hostKind);
1116
- reason = `tool=${candidate.toolName}; error=${outcome.error ?? `exit=${outcome.exitCode}`}; path=${relativePath}`;
1117
- db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, host_kind, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1118
- .run(candidate.rootSessionId, sourceObservation.failureSource ?? 'tool_failure', painScore, reason, painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild', 'system_infer', null, null, painId, null, candidate.hostKind, nowIso);
1119
- }
1120
- // PRI-640 §17: enrich unknown → codex on the already-admitted duplicate
1121
- // pain row; the IS NULL guard keeps any durable attribution.
1122
- db.prepare('UPDATE pain_events SET host_kind = ? WHERE canonical_pain_id = ? AND host_kind IS NULL').run(candidate.hostKind, painId);
1123
- if (!duplicateWithoutPain) {
1124
- insertAdmissionMarker({
1125
- db,
1126
- identity: { hostKind: candidate.hostKind, logicalObservationKey: candidate.logicalObservationKey, rolloutIdentity: candidate.rolloutIdentity, rootSessionId: candidate.rootSessionId, signalKind: candidate.kind },
1127
- canonicalPainId: painId,
1128
- ruleVersion: null,
1129
- reason: sanitizeString(reason, workspaceDir),
1130
- payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason, db }),
1131
- nowIso,
1132
- });
1133
- }
1134
- // A duplicate call row without an admitted pain mirrors the production
1135
- // handler (no pain insert on duplicates) and records no marker, so a later
1136
- // delivery whose gate reaches threshold can still admit.
1137
- })();
1138
- if (duplicateWithoutPain) {
1139
- return { disposition: 'not_admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, reason: 'duplicate_tool_call_without_admitted_pain' };
1140
- }
1141
- return { disposition: 'admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: null };
1142
- }
1143
1009
  function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation, trustedWorkbuddyLive }) {
1144
1010
  if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && (candidate.explicitOwnerStatement === true || trustedWorkbuddyLive === true))) {
1145
1011
  return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission accepts Codex observations or explicit WorkBuddy Owner statements' };
@@ -1171,10 +1037,7 @@ function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDe
1171
1037
  };
1172
1038
  }
1173
1039
  try {
1174
- if (candidate.kind === 'user_correction') {
1175
- return admitCorrection({ db, candidate, workspaceDir, now, nowIso, ...(keywordStore ? { keywordStore } : {}), ...(onDegradation ? { onDegradation } : {}) });
1176
- }
1177
- return admitToolFailure({ db, candidate, workspaceDir, nowIso });
1040
+ return admitCorrection({ db, candidate, workspaceDir, now, nowIso, ...(keywordStore ? { keywordStore } : {}), ...(onDegradation ? { onDegradation } : {}) });
1178
1041
  }
1179
1042
  catch (error) {
1180
1043
  // RateLimitedSignal never reaches here — admitCorrection converts it to a
@@ -2117,129 +1980,6 @@ export async function ensureGovernanceContinuation(input) {
2117
1980
  close();
2118
1981
  }
2119
1982
  }
2120
- /**
2121
- * Admit bare host pains — failure rows the shared production pain handler
2122
- * already persisted (host_kind set, canonical_pain_id set, runtime_task_id
2123
- * NULL) but that no inline admission exists for. The WorkBuddy hook is the
2124
- * only evidence producer on that host (SPEC workbuddy-host-adapter §1: no
2125
- * transcript reads), so these rows would otherwise stay stranded as
2126
- * evidence-only forever: reconcile only continues ADMITTED markers, and no
2127
- * WorkBuddy admission step existed (review R2/P1).
2128
- *
2129
- * Policy: only failures scoring at/above the Codex tool_failure admission
2130
- * floor (GOVERNANCE_STRONG_PAIN_SCORE = 70) are queued for diagnosis — that
2131
- * floor is the existing signal-quality boundary for write-tool failures, and
2132
- * lower-scored failures stay evidence-only in pain_events/tool_calls rather
2133
- * than diluting the diagnostician queue. Idempotency reuses the ONE existing
2134
- * authorities: the unique admission marker (canonical_pain_id probe +
2135
- * UNIQUE logical key) and the deterministic task id, so repeated sweeps and
2136
- * crash restarts never mint a second task. Task creation reuses
2137
- * `ensureGovernanceDiagnosticianTask` unchanged (including its Case-B link
2138
- * repair). Evidence promotion is deliberately NOT attempted here: the
2139
- * promotion substrate (governance_rollouts/observations) is fed by transcript
2140
- * ingestion, which the WorkBuddy MVP excludes.
2141
- */
2142
- export async function admitBareHostPains(input) {
2143
- const opened = openAdmissionStore(input.workspaceDir, input.databaseFactory);
2144
- if (!('db' in opened)) {
2145
- return { ok: false, reason: opened.reason, nextAction: opened.nextAction, admitted: 0, ensured: 0, skippedLowScore: 0, skippedInvalid: 0, degradations: [opened.reason] };
2146
- }
2147
- const { db, close } = opened;
2148
- if (!hasProductionPainSchema(db)) {
2149
- close();
2150
- return { ok: false, reason: 'trajectory_schema_invalid', nextAction: 'run the supported PD workspace migration before enabling the worker', admitted: 0, ensured: 0, skippedLowScore: 0, skippedInvalid: 0, degradations: ['trajectory_schema_invalid'] };
2151
- }
2152
- const nowIso = (input.now ?? new Date()).toISOString();
2153
- const workspaceDir = path.resolve(input.workspaceDir);
2154
- const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
2155
- // Bare pains of this host that no admission marker covers yet. A marker is
2156
- // the durable "already handled" fact; runtime_task_id additionally excludes
2157
- // pains that reached a task through another path.
2158
- const barePainPredicate = `
2159
- FROM pain_events p
2160
- WHERE p.host_kind = ?
2161
- AND p.runtime_task_id IS NULL
2162
- AND p.canonical_pain_id IS NOT NULL
2163
- AND NOT EXISTS (SELECT 1 FROM governance_signal_admissions a WHERE a.canonical_pain_id = p.canonical_pain_id)`;
2164
- try {
2165
- const lowScoreRow = db.prepare(`SELECT COUNT(*) AS n ${barePainPredicate} AND p.score < ?`).get(input.hostKind, GOVERNANCE_STRONG_PAIN_SCORE);
2166
- const skippedLowScore = typeof rowField(lowScoreRow, 'n') === 'number' ? rowField(lowScoreRow, 'n') : 0;
2167
- const rows = db.prepare(`SELECT p.canonical_pain_id, p.session_id, p.source, p.score, p.reason ${barePainPredicate} AND p.score >= ? ORDER BY p.id LIMIT ?`)
2168
- .all(input.hostKind, GOVERNANCE_STRONG_PAIN_SCORE, limit);
2169
- let admitted = 0;
2170
- let ensured = 0;
2171
- let skippedInvalid = 0;
2172
- const degradations = [];
2173
- const freshMarkers = [];
2174
- for (const row of rows) {
2175
- const painId = rowField(row, 'canonical_pain_id');
2176
- const sessionId = rowField(row, 'session_id');
2177
- const score = rowField(row, 'score');
2178
- if (typeof painId !== 'string' || painId.length === 0 || typeof sessionId !== 'string' || sessionId.length === 0
2179
- || typeof score !== 'number' || !Number.isFinite(score)) {
2180
- skippedInvalid += 1;
2181
- continue;
2182
- }
2183
- const logicalObservationKey = `${input.hostKind}|${sessionId}|${painId}`;
2184
- const reason = typeof rowField(row, 'reason') === 'string' ? rowField(row, 'reason') : '';
2185
- const source = typeof rowField(row, 'source') === 'string' ? rowField(row, 'source') : 'tool_failure';
2186
- // The pain row carries the tool name only inside its reason prefix
2187
- // (`tool=<name>; ...` — the production writer's format); the row's
2188
- // source column is the failure class, used as the fallback.
2189
- const toolName = /^tool=([^;]{1,120});/.exec(reason)?.[1] ?? source;
2190
- const inserted = db.transaction(() => {
2191
- // Re-probe inside the transaction: a concurrent sweep or live admission
2192
- // may have marked this pain between the scan and this write.
2193
- if (db.prepare('SELECT 1 FROM governance_signal_admissions WHERE canonical_pain_id = ?').get(painId) !== undefined)
2194
- return false;
2195
- insertAdmissionMarker({
2196
- db,
2197
- identity: { hostKind: input.hostKind, logicalObservationKey, rolloutIdentity: sessionId, rootSessionId: sessionId, signalKind: 'tool_failure' },
2198
- canonicalPainId: painId,
2199
- ruleVersion: null,
2200
- reason,
2201
- payloadJson: toolFailureTaskPayload({ hostKind: input.hostKind, logicalObservationKey, sessionId, toolName, reason, score, workspaceDir }),
2202
- nowIso,
2203
- });
2204
- return true;
2205
- })();
2206
- if (inserted) {
2207
- admitted += 1;
2208
- freshMarkers.push({ logicalObservationKey, canonicalPainId: painId });
2209
- }
2210
- }
2211
- // Reuse the crash-safe ensure semantics unchanged: deterministic task id,
2212
- // task-store PK idempotency, Case-B link repair.
2213
- for (const marker of freshMarkers) {
2214
- const continuation = await ensureGovernanceDiagnosticianTask({
2215
- workspaceDir,
2216
- logicalObservationKey: marker.logicalObservationKey,
2217
- canonicalPainId: marker.canonicalPainId,
2218
- ...(input.databaseFactory ? { databaseFactory: input.databaseFactory } : {}),
2219
- });
2220
- if (continuation.ok) {
2221
- if (continuation.created)
2222
- ensured += 1;
2223
- }
2224
- else {
2225
- degradations.push(`${marker.logicalObservationKey}:${continuation.reason}`);
2226
- }
2227
- }
2228
- return {
2229
- ok: degradations.length === 0,
2230
- ...(degradations.length > 0 ? { reason: 'bare_pain_admission_degradations', nextAction: 'inspect the per-item degradations; the admitted pains and markers remain durable and reconcile retries task creation' } : {}),
2231
- admitted, ensured, skippedLowScore, skippedInvalid,
2232
- degradations: degradations.slice(0, 10),
2233
- };
2234
- }
2235
- catch (error) {
2236
- const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
2237
- return { ok: false, reason: `bare_pain_admission_failed:${detail}`, nextAction: 'inspect the workspace trajectory database and retry; committed markers remain durable', admitted: 0, ensured: 0, skippedLowScore: 0, skippedInvalid: 0, degradations: [`bare_pain_admission_failed:${detail}`] };
2238
- }
2239
- finally {
2240
- close();
2241
- }
2242
- }
2243
1983
  /**
2244
1984
  * Narrow idempotent reconciliation between the trajectory admission markers
2245
1985
  * and the Runtime V2 task store (separate SQLite databases — no cross-store
@@ -1,80 +1,14 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import type { GovernanceHostKind } from '@principles/core/runtime-v2';
3
3
  import type { HostEvent, HostEventEmitter, HostEventResult } from '@principles/core/host';
4
- /** Shared with the governance admission path so both gate tool failures identically. */
5
- export declare const PRODUCTION_WRITE_TOOLS: ReadonlySet<string>;
6
- export interface PainEvidenceEntry {
7
- sourceRef: string;
8
- note: string;
9
- }
10
4
  export interface ProductionPainEnrichment {
5
+ /** Stable host-supplied occurrence identity for the tool call. */
11
6
  eventId?: string;
12
- painScore?: number;
13
- isRisky?: boolean;
14
- consecutiveErrors?: number;
15
- relativePath?: string;
16
- agentId?: string;
17
- errorHash?: string;
18
- evidence?: readonly PainEvidenceEntry[];
19
7
  }
20
8
  export type PainEnrichmentProvider = (event: HostEvent) => unknown | Promise<unknown>;
21
9
  export type PainDatabaseFactory = (databasePath: string) => Database.Database;
22
10
  /** SPEC-CODEX-PAIN-CLOSURE-001 CR-01: a tool result is success, failure or unknown. */
23
11
  export type ToolOutcomeClassification = 'success' | 'failure' | 'unknown';
24
- interface NormalizedOutcome {
25
- outcome: ToolOutcomeClassification;
26
- failure: boolean;
27
- exitCode: number | null;
28
- /** Bounded structured reason present when the outcome is unknown (rc-9). */
29
- unknownReason?: string;
30
- error?: string;
31
- durationMs?: number;
32
- params: unknown;
33
- result: unknown;
34
- }
35
- /** Structural input accepted by the canonical derivations (HostEvent satisfies it without a cast). */
36
- export interface ProductionToolEventFields {
37
- workspaceDir: string;
38
- sessionId: string;
39
- turnId?: string;
40
- /**
41
- * I1: the host's real per-call identity (Codex `tool_use_id`, WorkBuddy
42
- * `call_id`). Two calls with the same session/generation/tool/params/result
43
- * but DIFFERENT call ids are two real occurrences and must derive two
44
- * identities; a redelivery of the SAME call id is one occurrence. Optional:
45
- * hosts that supply no call id keep the historical content-derived identity
46
- * (identical-content calls then cannot be distinguished — a documented
47
- * limitation, never fabricated precision).
48
- */
49
- toolCallId?: string;
50
- toolName?: string;
51
- toolInput?: unknown;
52
- toolOutput?: unknown;
53
- source: string;
54
- }
55
- export interface DerivedToolPainIdentity {
56
- eventId: string;
57
- painId: string;
58
- outcome: NormalizedOutcome;
59
- sanitizedParams: Record<string, unknown>;
60
- paramsJson: string;
61
- resultPreview: string | null;
62
- }
63
- /**
64
- * The canonical tool-pain identity derivation, exposed for the governance
65
- * admission path (Codex Governance Closure SPEC §10): the same normalized
66
- * fields fed to the live production handler derive the same deterministic
67
- * `pain_host_<sha256>` id, so live and observation-delivered admissions of one
68
- * tool call converge on one canonical pain.
69
- *
70
- * I1 parity obligation: when the host supplies a call id, BOTH paths must pass
71
- * the same `toolCallId` (Codex admission candidates carry it from the same
72
- * `tool_use_id` the live decoder required) — otherwise one real call would
73
- * derive two canonical pains.
74
- */
75
- export declare function deriveProductionToolPainIdentity(fields: ProductionToolEventFields & {
76
- canonicalEventId?: string;
77
- }): DerivedToolPainIdentity;
78
12
  /**
79
13
  * The canonical correction-pain identity derivation (SPEC §10/§12): deterministic,
80
14
  * content-derived, retry-safe — replacing the legacy random `correction_<traceId>`
@@ -137,6 +71,13 @@ export declare function recordVerifiedToolOutcomes(args: {
137
71
  hostKind?: string;
138
72
  }): VerifiedToolOutcomeStatus[];
139
73
  /**
74
+ * Shared production tool-call evidence handler.
75
+ *
76
+ * Records the real host tool call in `trajectory.db` (tool_calls) and emits the
77
+ * receipt event. Phase A retired the legacy automatic Pain admission that used
78
+ * to ride on this path: a technical tool failure is trajectory + health
79
+ * evidence, never an Owner-pain by itself.
80
+ *
140
81
  * PRI-640: host attribution is supplied by the host adapter that constructs
141
82
  * this shared handler (OpenClaw / Codex adapters) — never guessed inside the
142
83
  * writer. Omitted -> NULL (unknown).
@@ -148,6 +89,4 @@ export declare function createProductionPainEvidenceHandler(options?: {
148
89
  projectDir?: string;
149
90
  events?: HostEventEmitter;
150
91
  }): (event: HostEvent) => Promise<HostEventResult>;
151
- export declare function resetProductionPainCooldownForTest(): void;
152
- export declare function productionPainCooldownEntryCountForTest(): number;
153
92
  export { hasCanonicalSchema as hasProductionPainSchema };