@principles/host-runtime 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -144,12 +144,23 @@ export type AdmitGovernanceSignalsResult = {
144
144
  ok: true;
145
145
  outcomes: readonly GovernanceAdmissionOutcome[];
146
146
  } | Degradation;
147
+ /** Public generic intake stays Codex + explicit WorkBuddy only. */
148
+ export declare function admitGovernanceSignals(input: AdmitGovernanceSignalsInput): AdmitGovernanceSignalsResult;
149
+ /** Pure check: no DB, no filesystem, no classifier, no historical transcript. */
150
+ export declare function assessWorkbuddyLiveKeyword(text: string, sessionId: string): {
151
+ matched: boolean;
152
+ reason: 'matched' | 'not_a_signal' | 'prompt_too_large';
153
+ };
147
154
  /**
148
- * Admit governance signal candidates. Each candidate commits in its own
149
- * trajectory.db transaction (marker + pain row + quota together), so one
150
- * failing candidate never rolls back another's admission.
155
+ * Narrow native event gateway: validate true current user origin and flags.
156
+ * The ordinary generic admission API cannot bypass this WorkBuddy gate.
151
157
  */
152
- export declare function admitGovernanceSignals(input: AdmitGovernanceSignalsInput): AdmitGovernanceSignalsResult;
158
+ export declare function admitTrustedWorkbuddyLiveCorrection(input: {
159
+ workspaceDir: string;
160
+ rawEvent: unknown;
161
+ now?: Date;
162
+ databaseFactory?: ObservationDatabaseFactory;
163
+ }): AdmitGovernanceSignalsResult;
153
164
  export interface EnsureGovernanceTaskInput {
154
165
  readonly workspaceDir: string;
155
166
  readonly logicalObservationKey: string;
@@ -34,7 +34,7 @@
34
34
  import fs from 'node:fs';
35
35
  import path from 'node:path';
36
36
  import Database from 'better-sqlite3';
37
- import { collectSync, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
37
+ import { collectSync, computeFeatureFlagsFromConfig, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
38
38
  import { deriveProductionCorrectionPainIdentity, deriveProductionToolPainIdentity, hasProductionPainSchema, PRODUCTION_WRITE_TOOLS, } from './production-pain-evidence.js';
39
39
  import { promoteGovernanceEvidence, ensureGovernanceSchema } from './governance-observation-store.js';
40
40
  import { loadPdConfigForPlugin } from './pd-config.js';
@@ -432,7 +432,8 @@ function toolFailureTaskPayload(fields) {
432
432
  };
433
433
  return JSON.stringify(payload);
434
434
  }
435
- function taskSubmissionPayload(candidate, workspaceDir, reason) {
435
+ function taskSubmissionPayload(candidate, workspaceDir, details) {
436
+ const { reason, canonicalPainId } = details;
436
437
  if (candidate.kind === 'user_correction') {
437
438
  // P1-1 privacy (review round 2): sanitize the FULL text before any
438
439
  // truncation — a token crossing the 200-char boundary must be redacted, not
@@ -446,9 +447,14 @@ function taskSubmissionPayload(candidate, workspaceDir, reason) {
446
447
  hostKind: candidate.hostKind,
447
448
  // WorkBuddy explicit intake has no transcript observation to promote.
448
449
  // The classified Owner statement is its input basis, not a pseudo-trace.
449
- evidence: candidate.hostKind === 'workbuddy' && candidate.explicitOwnerStatement === true
450
- ? []
451
- : [{ sourceRef: `governance_observation:${candidate.logicalObservationKey}`, note: sanitizeString(candidate.text, workspaceDir) }],
450
+ evidence: candidate.hostKind === 'workbuddy'
451
+ ? candidate.explicitOwnerStatement === true ? [] : [{
452
+ // WorkBuddy has no governance_observation, anchor to the canonical
453
+ // Pain persisted in this same trajectory.db transaction.
454
+ sourceRef: 'pain_event:' + (canonicalPainId ?? ''),
455
+ note: sanitizeString(candidate.text, workspaceDir),
456
+ }]
457
+ : [{ sourceRef: 'governance_observation:' + candidate.logicalObservationKey, note: sanitizeString(candidate.text, workspaceDir) }],
452
458
  // SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
453
459
  // Owner statement rides as classified correction evidence — never
454
460
  // summarized by an AI, never inside evidence[] pseudo-trace entries.
@@ -544,7 +550,7 @@ function admitCorrection({ db, candidate, workspaceDir, now, nowIso, keywordStor
544
550
  // detection, so `detection` is optional — no ruleVersion exists for them.
545
551
  ruleVersion: detection?.ruleVersion ?? null,
546
552
  reason,
547
- payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
553
+ payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason, canonicalPainId: painId }),
548
554
  nowIso,
549
555
  });
550
556
  })();
@@ -637,7 +643,7 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
637
643
  canonicalPainId: painId,
638
644
  ruleVersion: null,
639
645
  reason: sanitizeString(reason, workspaceDir),
640
- payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
646
+ payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason }),
641
647
  nowIso,
642
648
  });
643
649
  }
@@ -650,8 +656,8 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
650
656
  }
651
657
  return { disposition: 'admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: null };
652
658
  }
653
- function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation }) {
654
- if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && candidate.explicitOwnerStatement === true)) {
659
+ function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation, trustedWorkbuddyLive }) {
660
+ if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && (candidate.explicitOwnerStatement === true || trustedWorkbuddyLive === true))) {
655
661
  return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission accepts Codex observations or explicit WorkBuddy Owner statements' };
656
662
  }
657
663
  const marker = db.prepare('SELECT * FROM governance_signal_admissions WHERE logical_observation_key = ?').get(candidate.logicalObservationKey);
@@ -676,7 +682,7 @@ function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDe
676
682
  * trajectory.db transaction (marker + pain row + quota together), so one
677
683
  * failing candidate never rolls back another's admission.
678
684
  */
679
- export function admitGovernanceSignals(input) {
685
+ function admitGovernanceSignalsInternal(input, trustedWorkbuddyLive = false) {
680
686
  const opened = openAdmissionStore(input.workspaceDir, input.databaseFactory);
681
687
  if (!('db' in opened))
682
688
  return opened;
@@ -691,7 +697,7 @@ export function admitGovernanceSignals(input) {
691
697
  const outcomes = [];
692
698
  try {
693
699
  for (const candidate of input.candidates) {
694
- outcomes.push(admitOne({ db, candidate, workspaceDir, now, nowIso, ...(input.keywordStore ? { keywordStore: input.keywordStore } : {}), ...(input.onDegradation ? { onDegradation: input.onDegradation } : {}) }));
700
+ outcomes.push(admitOne({ db, candidate, workspaceDir, now, nowIso, ...(input.keywordStore ? { keywordStore: input.keywordStore } : {}), ...(input.onDegradation ? { onDegradation: input.onDegradation } : {}), trustedWorkbuddyLive }));
695
701
  }
696
702
  return { ok: true, outcomes };
697
703
  }
@@ -703,6 +709,87 @@ export function admitGovernanceSignals(input) {
703
709
  close();
704
710
  }
705
711
  }
712
+ /** Public generic intake stays Codex + explicit WorkBuddy only. */
713
+ export function admitGovernanceSignals(input) {
714
+ return admitGovernanceSignalsInternal(input);
715
+ }
716
+ /** P0: a four-term projection of existing canonical high-precision seeds. */
717
+ const WORKBUDDY_AUTO_TERMS = new Set(['这是错的', '不要自作主张', '不应该这么做', '又搞错']);
718
+ const WORKBUDDY_MAX_PROMPT_BYTES = 16 * 1024;
719
+ function workbuddyP0KeywordStore() {
720
+ const seed = buildSharedSeedKeywordStore();
721
+ const terms = {};
722
+ for (const [term, def] of Object.entries(seed.terms)) {
723
+ if (WORKBUDDY_AUTO_TERMS.has(term) && def.category === 'correction' && def.precision === 'high') {
724
+ terms[term] = def;
725
+ }
726
+ }
727
+ return { version: seed.version, terms };
728
+ }
729
+ /** Pure check: no DB, no filesystem, no classifier, no historical transcript. */
730
+ export function assessWorkbuddyLiveKeyword(text, sessionId) {
731
+ if (Buffer.byteLength(text, 'utf8') > WORKBUDDY_MAX_PROMPT_BYTES) {
732
+ return { matched: false, reason: 'prompt_too_large' };
733
+ }
734
+ const store = workbuddyP0KeywordStore();
735
+ // Prefer precision to recall: reject quoted/embedded/reported phrases.
736
+ if (!Object.keys(store.terms).some(term => text.trimStart().toLowerCase().startsWith(term.toLowerCase()))) {
737
+ return { matched: false, reason: 'not_a_signal' };
738
+ }
739
+ const out = collectSync(text, sessionId, store, SYNC_DETECTION_CONFIG, new Date().toISOString());
740
+ const matched = out.isSignal && out.strength === 'STRONG' &&
741
+ out.type === 'correction' && out.matchedPrecision === 'high';
742
+ return { matched, reason: matched ? 'matched' : 'not_a_signal' };
743
+ }
744
+ /**
745
+ * Narrow native event gateway: validate true current user origin and flags.
746
+ * The ordinary generic admission API cannot bypass this WorkBuddy gate.
747
+ */
748
+ export function admitTrustedWorkbuddyLiveCorrection(input) {
749
+ const raw = input.rawEvent;
750
+ if (!isRecord(raw) || own(raw, 'hook_event_name') !== 'UserPromptSubmit' ||
751
+ own(raw, 'is_internal_continuation') !== false) {
752
+ return { ok: false, reason: 'native_current_user_unverified', nextAction: 'Require a real current WorkBuddy UserPromptSubmit event.' };
753
+ }
754
+ const sessionId = own(raw, 'session_id');
755
+ const generationId = own(raw, 'generation_id');
756
+ const prompt = own(raw, 'prompt');
757
+ if (typeof sessionId !== 'string' || !sessionId.trim() ||
758
+ typeof generationId !== 'string' || !generationId.trim() ||
759
+ typeof prompt !== 'string') {
760
+ return { ok: false, reason: 'native_identity_invalid', nextAction: 'Require nonblank native session_id/generation_id and prompt.' };
761
+ }
762
+ const config = loadPdConfigForPlugin(input.workspaceDir);
763
+ if (!config.ok)
764
+ return { ok: false, reason: 'workbuddy_config_invalid', nextAction: 'Repair WorkBuddy governance workspace config.' };
765
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
766
+ if (flags['host.workbuddy']?.enabled !== true || flags.workbuddy_live_keyword_pain?.enabled !== true) {
767
+ return { ok: false, reason: 'workbuddy_auto_disabled', nextAction: 'Explicitly enable both WorkBuddy host and live keyword Pain.' };
768
+ }
769
+ if (prompt.trimStart().startsWith('PD记录痛苦')) {
770
+ return { ok: false, reason: 'explicit_owner_statement_reserved', nextAction: 'Use original Owner intake; no keyword fallback.' };
771
+ }
772
+ if (!assessWorkbuddyLiveKeyword(prompt, sessionId).matched) {
773
+ return { ok: true, outcomes: [] };
774
+ }
775
+ const nativeIdentity = JSON.stringify(['workbuddy', sessionId, generationId]);
776
+ return admitGovernanceSignalsInternal({
777
+ workspaceDir: input.workspaceDir,
778
+ candidates: [{
779
+ kind: 'user_correction',
780
+ hostKind: 'workbuddy',
781
+ logicalObservationKey: nativeIdentity + '|user',
782
+ rolloutIdentity: JSON.stringify(['workbuddy', sessionId]),
783
+ rootSessionId: sessionId,
784
+ hostTurnId: nativeIdentity,
785
+ text: prompt,
786
+ observedAt: (input.now ?? new Date()).toISOString(),
787
+ }],
788
+ keywordStore: workbuddyP0KeywordStore(),
789
+ ...(input.now ? { now: input.now } : {}),
790
+ ...(input.databaseFactory ? { databaseFactory: input.databaseFactory } : {}),
791
+ }, true);
792
+ }
706
793
  // ─── Diagnostician continuation (one pain → exactly one pending task) ───────
707
794
  function isGovernanceTaskPayload(value) {
708
795
  if (!isRecord(value))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "lint": "eslint \"src/**/*.ts\""
32
32
  },
33
33
  "dependencies": {
34
- "@principles/core": "^1.293.5",
34
+ "@principles/core": "^1.294.1",
35
35
  "@principles/install-layout": "^0.4.0",
36
36
  "better-sqlite3": "^13.0.3",
37
37
  "js-yaml": "^5.4.3"