@principles/host-runtime 0.10.2 → 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.
@@ -1,5 +1,5 @@
1
1
  /** Shared validated history assembly; host vocabulary is supplied by its owner. */
2
- import { extractFilePathFromParams, normalizePathPure } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
2
+ import { buildRuleHostAction } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
3
3
  const VALID_OUTCOMES = new Set(['success', 'failure', 'blocked']);
4
4
  function isPlainObject(value) {
5
5
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -51,14 +51,9 @@ export function assembleBehaviorHistory(rows, truncated, options) {
51
51
  // like 'shell'/'cmd'/'delete_file' previously degraded to 'other' here
52
52
  // while the gate classified them as bash/write (vocabulary drift).
53
53
  const canonicalKind = toolSemantics.resolve(toolName);
54
- // Extract normalized path using the same pure logic as buildRuleHostAction
55
- // (avoids production/replay drift, spec §4.4)
56
- const rawPath = extractFilePathFromParams(parsedParams, {
57
- isBashTool: canonicalKind === 'execute',
58
- isWriteTool: canonicalKind === 'write',
59
- toolName,
60
- });
61
- const normalizedPath = normalizePathPure(rawPath, projectDir);
54
+ // Use the shared action builder so history and live/replay preserve the
55
+ // same distinction between extracted paths and opaque shell commands.
56
+ const { normalizedPath } = buildRuleHostAction(toolName, parsedParams, projectDir, { canonicalKind });
62
57
  records.push({
63
58
  sequenceId: row.id,
64
59
  toolName,
@@ -0,0 +1,23 @@
1
+ import { type ToolSemanticRegistry } from '@principles/core/runtime-v2';
2
+ import type { RuleContextProvider } from './production-rulehost-gate.js';
3
+ export interface CodexRuleContextProviderOptions {
4
+ /** The resolved PD workspace whose trajectory database is authoritative. */
5
+ workspaceDir: string;
6
+ /** Native Codex project cwd; history rows must have this exact stored scope. */
7
+ projectDir: string;
8
+ /** Existing `rulecode_context_v2` and `codex_conversation_ingestion` gates. */
9
+ enabled: boolean;
10
+ /** Timestamp captured immediately after decoding this PreToolUse event. */
11
+ cutoffAt: Date;
12
+ /** Host-authoritative current Codex tool_use_id, excluded even on timestamp ties. */
13
+ currentToolUseId: string;
14
+ toolSemantics: ToolSemanticRegistry;
15
+ }
16
+ /**
17
+ * Read a bounded same-session trajectory window for Codex PreToolUse events.
18
+ * It never reads the Codex transcript itself: only already-ingested tool rows
19
+ * from the resolved workspace are eligible, and rows at/after the captured
20
+ * hook epoch or belonging to the current tool_use_id are excluded. Disabled
21
+ * or unverifiable sources return explicit unknowns.
22
+ */
23
+ export declare function createCodexRuleContextProvider(options: CodexRuleContextProviderOptions): RuleContextProvider;
@@ -0,0 +1,89 @@
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 { version: 2, history, facts: computeBehaviorFacts(history, null, null) };
18
+ }
19
+ /**
20
+ * Read a bounded same-session trajectory window for Codex PreToolUse events.
21
+ * It never reads the Codex transcript itself: only already-ingested tool rows
22
+ * from the resolved workspace are eligible, and rows at/after the captured
23
+ * hook epoch or belonging to the current tool_use_id are excluded. Disabled
24
+ * or unverifiable sources return explicit unknowns.
25
+ */
26
+ export function createCodexRuleContextProvider(options) {
27
+ const workspaceDir = path.resolve(options.workspaceDir);
28
+ const projectDir = path.resolve(options.projectDir);
29
+ return (request) => {
30
+ if (!options.enabled)
31
+ return unavailable('codex_rule_context_disabled');
32
+ if (path.resolve(request.workspaceDir) !== workspaceDir)
33
+ return unavailable('workspace_scope_mismatch');
34
+ if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0)
35
+ return unavailable('session_id_invalid');
36
+ const raw = request.rawPayload;
37
+ const toolInput = isRecord(raw) && isRecord(raw.toolInput) ? raw.toolInput : null;
38
+ if (toolInput === null
39
+ || toolInput.toolName !== request.toolName
40
+ || !isRecord(toolInput.params)
41
+ || !path.isAbsolute(projectDir)
42
+ || options.currentToolUseId.trim().length === 0) {
43
+ return unavailable('codex_pre_tool_scope_mismatch');
44
+ }
45
+ const cutoff = options.cutoffAt;
46
+ if (!(cutoff instanceof Date) || !Number.isFinite(cutoff.getTime()))
47
+ return unavailable('history_cutoff_invalid');
48
+ try {
49
+ const historyRows = readWorkspaceRuleContextRowsBefore(workspaceDir, {
50
+ sessionId: request.sessionId,
51
+ projectDir,
52
+ beforeCreatedAt: cutoff.toISOString(),
53
+ excludeToolUseId: options.currentToolUseId,
54
+ limit: HISTORY_LIMIT,
55
+ });
56
+ if (historyRows.unscopedPriorRows)
57
+ return unavailable('trajectory_project_scope_incomplete');
58
+ if (historyRows.rows.length === 0)
59
+ return unavailable('no_prior_session_history');
60
+ const history = assembleBehaviorHistory(historyRows.rows, historyRows.truncated, {
61
+ projectDir,
62
+ toolSemantics: options.toolSemantics,
63
+ });
64
+ // Codex exposes shell as execute, and other host tools may be MCP calls
65
+ // absent from this host's semantic registry. Either may read or mutate
66
+ // files, so neither target-read absence nor exact action counts can be
67
+ // asserted from this partial semantic view.
68
+ const factsIncomplete = history.truncated || history.calls.some((call) => call.canonicalKind === 'execute' || call.canonicalKind === 'other');
69
+ const computedFacts = computeBehaviorFacts(history, factsIncomplete ? null : request.targetPath, null);
70
+ const facts = factsIncomplete
71
+ ? {
72
+ priorReadOfTarget: 'unknown',
73
+ readCount: null,
74
+ writeCount: null,
75
+ uniqueWritePathCount: null,
76
+ sameActionBlockCount: null,
77
+ }
78
+ : computedFacts;
79
+ return {
80
+ version: 2,
81
+ history,
82
+ facts,
83
+ };
84
+ }
85
+ catch (error) {
86
+ return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ };
89
+ }
@@ -98,6 +98,8 @@ export interface GovernanceToolFailureCandidate {
98
98
  readonly toolCallId?: string;
99
99
  /** Host tool-call id (SPEC-CODEX-PAIN-CLOSURE-001 CR-01) — same id the live PostToolUse path records into tool_calls.tool_use_id for the dedup join. */
100
100
  readonly toolUseId?: string;
101
+ /** Native host cwd, when supplied by a live hook; absent for transcript catch-up. */
102
+ readonly projectDir?: string;
101
103
  readonly toolInput?: unknown;
102
104
  readonly toolOutput?: unknown;
103
105
  readonly observedAt: string;
@@ -142,12 +144,23 @@ export type AdmitGovernanceSignalsResult = {
142
144
  ok: true;
143
145
  outcomes: readonly GovernanceAdmissionOutcome[];
144
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
+ };
145
154
  /**
146
- * Admit governance signal candidates. Each candidate commits in its own
147
- * trajectory.db transaction (marker + pain row + quota together), so one
148
- * 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.
149
157
  */
150
- 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;
151
164
  export interface EnsureGovernanceTaskInput {
152
165
  readonly workspaceDir: string;
153
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';
@@ -303,13 +303,23 @@ function ensureGovernanceAdmissionSchema(db) {
303
303
  if (hasToolCalls) {
304
304
  try {
305
305
  db.exec('ALTER TABLE tool_calls ADD COLUMN tool_use_id TEXT');
306
- db.exec('CREATE INDEX IF NOT EXISTS idx_tool_calls_session_tool_use_id ON tool_calls(session_id, tool_use_id)');
307
306
  }
308
307
  catch (err) {
309
308
  const message = err instanceof Error ? err.message : String(err);
310
309
  if (!message.includes('duplicate column name'))
311
310
  throw err;
312
311
  }
312
+ try {
313
+ // Additive project scope for live Codex admission rows. Existing rows
314
+ // deliberately remain NULL because their originating cwd is unknown.
315
+ db.exec('ALTER TABLE tool_calls ADD COLUMN project_dir TEXT');
316
+ }
317
+ catch (err) {
318
+ const message = err instanceof Error ? err.message : String(err);
319
+ if (!message.includes('duplicate column name'))
320
+ throw err;
321
+ }
322
+ db.exec('CREATE INDEX IF NOT EXISTS idx_tool_calls_session_tool_use_id ON tool_calls(session_id, tool_use_id)');
313
323
  }
314
324
  })();
315
325
  }
@@ -422,7 +432,8 @@ function toolFailureTaskPayload(fields) {
422
432
  };
423
433
  return JSON.stringify(payload);
424
434
  }
425
- function taskSubmissionPayload(candidate, workspaceDir, reason) {
435
+ function taskSubmissionPayload(candidate, workspaceDir, details) {
436
+ const { reason, canonicalPainId } = details;
426
437
  if (candidate.kind === 'user_correction') {
427
438
  // P1-1 privacy (review round 2): sanitize the FULL text before any
428
439
  // truncation — a token crossing the 200-char boundary must be redacted, not
@@ -436,9 +447,14 @@ function taskSubmissionPayload(candidate, workspaceDir, reason) {
436
447
  hostKind: candidate.hostKind,
437
448
  // WorkBuddy explicit intake has no transcript observation to promote.
438
449
  // The classified Owner statement is its input basis, not a pseudo-trace.
439
- evidence: candidate.hostKind === 'workbuddy' && candidate.explicitOwnerStatement === true
440
- ? []
441
- : [{ 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) }],
442
458
  // SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
443
459
  // Owner statement rides as classified correction evidence — never
444
460
  // summarized by an AI, never inside evidence[] pseudo-trace entries.
@@ -534,7 +550,7 @@ function admitCorrection({ db, candidate, workspaceDir, now, nowIso, keywordStor
534
550
  // detection, so `detection` is optional — no ruleVersion exists for them.
535
551
  ruleVersion: detection?.ruleVersion ?? null,
536
552
  reason,
537
- payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
553
+ payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason, canonicalPainId: painId }),
538
554
  nowIso,
539
555
  });
540
556
  })();
@@ -608,8 +624,11 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
608
624
  else {
609
625
  db.prepare(`INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at`)
610
626
  .run(candidate.rootSessionId, nowIso, nowIso);
611
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
612
- .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);
627
+ const projectDir = typeof candidate.projectDir === 'string' && path.isAbsolute(candidate.projectDir)
628
+ ? path.resolve(candidate.projectDir)
629
+ : null;
630
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
631
+ .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);
613
632
  reason = `tool=${candidate.toolName}; error=${outcome.error ?? `exit=${outcome.exitCode}`}; path=${relativePath}`;
614
633
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
615
634
  .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);
@@ -624,7 +643,7 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
624
643
  canonicalPainId: painId,
625
644
  ruleVersion: null,
626
645
  reason: sanitizeString(reason, workspaceDir),
627
- payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
646
+ payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason }),
628
647
  nowIso,
629
648
  });
630
649
  }
@@ -637,8 +656,8 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
637
656
  }
638
657
  return { disposition: 'admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: null };
639
658
  }
640
- function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation }) {
641
- 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))) {
642
661
  return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission accepts Codex observations or explicit WorkBuddy Owner statements' };
643
662
  }
644
663
  const marker = db.prepare('SELECT * FROM governance_signal_admissions WHERE logical_observation_key = ?').get(candidate.logicalObservationKey);
@@ -663,7 +682,7 @@ function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDe
663
682
  * trajectory.db transaction (marker + pain row + quota together), so one
664
683
  * failing candidate never rolls back another's admission.
665
684
  */
666
- export function admitGovernanceSignals(input) {
685
+ function admitGovernanceSignalsInternal(input, trustedWorkbuddyLive = false) {
667
686
  const opened = openAdmissionStore(input.workspaceDir, input.databaseFactory);
668
687
  if (!('db' in opened))
669
688
  return opened;
@@ -678,7 +697,7 @@ export function admitGovernanceSignals(input) {
678
697
  const outcomes = [];
679
698
  try {
680
699
  for (const candidate of input.candidates) {
681
- 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 }));
682
701
  }
683
702
  return { ok: true, outcomes };
684
703
  }
@@ -690,6 +709,87 @@ export function admitGovernanceSignals(input) {
690
709
  close();
691
710
  }
692
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
+ }
693
793
  // ─── Diagnostician continuation (one pain → exactly one pending task) ───────
694
794
  function isGovernanceTaskPayload(value) {
695
795
  if (!isRecord(value))
package/dist/index.d.ts CHANGED
@@ -87,5 +87,6 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
87
87
  }): HostRuntime;
88
88
  export * from './behavior-example-pack-assembler.js';
89
89
  export * from './behavior-history.js';
90
- export * from './workspace-behavior-evidence.js';
90
+ export * from './codex-rule-context-provider.js';
91
+ export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
91
92
  export * from './explicit-owner-statement-prefix.js';
package/dist/index.js CHANGED
@@ -181,6 +181,7 @@ export function createHostRuntime(options) {
181
181
  }
182
182
  export function createProductionHostRuntime(options = {}) {
183
183
  const productionGate = createProductionRuleHostGate({
184
+ ...(options.hostKind ? { hostKind: options.hostKind } : {}),
184
185
  ...(options.projectDir ? { projectDir: options.projectDir } : {}),
185
186
  ...(options.ruleContextProvider ? { ruleContextProvider: options.ruleContextProvider } : {}),
186
187
  ...(options.ruleInputEnrichmentProvider ? { ruleInputEnrichmentProvider: options.ruleInputEnrichmentProvider } : {}),
@@ -192,6 +193,7 @@ export function createProductionHostRuntime(options = {}) {
192
193
  ...(options.painEnrichmentProvider ? { painEnrichmentProvider: options.painEnrichmentProvider } : {}),
193
194
  ...(options.painDatabaseFactory ? { painDatabaseFactory: options.painDatabaseFactory } : {}),
194
195
  ...(options.hostKind ? { hostKind: options.hostKind } : {}),
196
+ ...(options.projectDir ? { projectDir: options.projectDir } : {}),
195
197
  ...(options.events ? { events: options.events } : {}),
196
198
  }),
197
199
  beforeToolCall: options.beforeToolCall ?? productionGate,
@@ -267,5 +269,6 @@ export function createProductionHostRuntime(options = {}) {
267
269
  }
268
270
  export * from './behavior-example-pack-assembler.js';
269
271
  export * from './behavior-history.js';
270
- export * from './workspace-behavior-evidence.js';
272
+ export * from './codex-rule-context-provider.js';
273
+ export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
271
274
  export * from './explicit-owner-statement-prefix.js';
@@ -693,7 +693,7 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
693
693
  break;
694
694
  case 'scribe':
695
695
  runner = new ScribeRunner({
696
- stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator(),
696
+ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator({ requireRuleCodeContracts: true }),
697
697
  // I2 — CHAIN STAMPING (Owner review of PR #1856, P1): the scribe
698
698
  // stamps the ledger principle UUID (minted at intake, keyed by the
699
699
  // dreamer task seed's candidateId) onto the artifact it writes, so
@@ -170,6 +170,9 @@ export function createEvaluatorRepairDeps(workspaceDir, stateManager, logger) {
170
170
  timeoutMs: params.inheritedTimeoutMs,
171
171
  inputArtifactRefs: [...params.inheritedInputArtifactRefs],
172
172
  outputArtifactRefs: [],
173
+ ...(params.ownerEvidenceSelection !== undefined
174
+ ? { ownerEvidenceSelection: params.ownerEvidenceSelection }
175
+ : {}),
173
176
  repairPayload: params.repairPayload,
174
177
  }),
175
178
  });
@@ -143,6 +143,7 @@ export declare function createProductionPainEvidenceHandler(options?: {
143
143
  painEnrichmentProvider?: PainEnrichmentProvider;
144
144
  painDatabaseFactory?: PainDatabaseFactory;
145
145
  hostKind?: GovernanceHostKind;
146
+ projectDir?: string;
146
147
  events?: HostEventEmitter;
147
148
  }): (event: HostEvent) => Promise<HostEventResult>;
148
149
  export declare function resetProductionPainCooldownForTest(): void;
@@ -225,7 +225,7 @@ export function deriveProductionCorrectionPainIdentity(fields) {
225
225
  }
226
226
  const REQUIRED_COLUMNS = {
227
227
  sessions: { session_id: 'TEXT', started_at: 'TEXT', updated_at: 'TEXT' },
228
- tool_calls: { session_id: 'TEXT', tool_name: 'TEXT', outcome: 'TEXT', duration_ms: 'INTEGER', exit_code: 'INTEGER', error_type: 'TEXT', error_message: 'TEXT', gfi_before: 'REAL', gfi_after: 'REAL', params_json: 'TEXT', result_preview: 'TEXT', created_at: 'TEXT' },
228
+ tool_calls: { session_id: 'TEXT', tool_name: 'TEXT', outcome: 'TEXT', duration_ms: 'INTEGER', exit_code: 'INTEGER', error_type: 'TEXT', error_message: 'TEXT', gfi_before: 'REAL', gfi_after: 'REAL', params_json: 'TEXT', result_preview: 'TEXT', created_at: 'TEXT', project_dir: 'TEXT' },
229
229
  pain_events: { session_id: 'TEXT', source: 'TEXT', score: 'REAL', reason: 'TEXT', severity: 'TEXT', origin: 'TEXT', confidence: 'REAL', text: 'TEXT', canonical_pain_id: 'TEXT', runtime_task_id: 'TEXT', host_kind: 'TEXT', created_at: 'TEXT' },
230
230
  };
231
231
  function pragmaField(row, key) {
@@ -277,7 +277,7 @@ function hasCanonicalSchema(db) {
277
277
  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 ?');
278
278
  db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?');
279
279
  db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at');
280
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
280
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
281
281
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
282
282
  return true;
283
283
  }
@@ -301,6 +301,19 @@ function ensureToolCallsToolUseIdColumn(db) {
301
301
  // the prepared-statement readiness check below fails loud if it is truly absent.
302
302
  }
303
303
  }
304
+ /** Additive migration for verified host cwd binding; legacy rows stay NULL. */
305
+ function ensureToolCallsProjectDirColumn(db) {
306
+ const columns = db.prepare('PRAGMA table_info(tool_calls)').all();
307
+ if (columns.some((row) => pragmaField(row, 'name') === 'project_dir'))
308
+ return;
309
+ try {
310
+ db.exec('ALTER TABLE tool_calls ADD COLUMN project_dir TEXT');
311
+ }
312
+ catch {
313
+ // A concurrent writer may have added it between the check and the ALTER;
314
+ // the prepared-statement readiness check below fails loud if absent.
315
+ }
316
+ }
304
317
  /**
305
318
  * SPEC-CODEX-PAIN-CLOSURE-001 CR-01: upgrade the live-hook observation of a
306
319
  * tool call with the host-verified final result (exit code) read through the
@@ -412,6 +425,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
412
425
  db = options.painDatabaseFactory ? options.painDatabaseFactory(dbPath) : new Database(dbPath);
413
426
  db.pragma('busy_timeout = 5000');
414
427
  ensureToolCallsToolUseIdColumn(db);
428
+ ensureToolCallsProjectDirColumn(db);
415
429
  if (!hasCanonicalSchema(db)) {
416
430
  db.close();
417
431
  db = undefined;
@@ -518,8 +532,8 @@ export function createProductionPainEvidenceHandler(options = {}) {
518
532
  return;
519
533
  }
520
534
  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);
521
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
522
- .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);
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));
523
537
  if (admitted) {
524
538
  // Raw observation only (Pain Diagnosis Persistence SPEC §8): the
525
539
  // pain row must record WHAT happened, not WHY. Attribution
@@ -1,4 +1,4 @@
1
- import { type ToolSemanticRegistry } from '@principles/core/runtime-v2';
1
+ import { type ToolSemanticRegistry, type GovernanceHostKind } from '@principles/core/runtime-v2';
2
2
  import type { HostEvent, HostEventResult } from '@principles/core/host';
3
3
  import { type RuleImplementationRuntime } from './rule-implementation-runtime.js';
4
4
  export interface ProductionRuleContextRequest {
@@ -17,6 +17,7 @@ export interface RuleInputEnrichment {
17
17
  export type RuleInputEnrichmentProvider = (request: ProductionRuleContextRequest) => unknown | Promise<unknown>;
18
18
  export interface ProductionRuleHostGateOptions {
19
19
  projectDir?: string;
20
+ hostKind?: GovernanceHostKind;
20
21
  ruleContextProvider?: RuleContextProvider;
21
22
  ruleInputEnrichmentProvider?: RuleInputEnrichmentProvider;
22
23
  implementationRuntime?: RuleImplementationRuntime;
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { buildRuleHostAction, buildToolSemanticRegistry, deriveToolHintsFromCanonicalKind, estimateLineChanges, mergeDecisions, SqliteConnection, UNAVAILABLE_RULE_CONTEXT, validateRuleContextV2, validateRuleHostResult, verifyPiArtifactRowDigest, } from '@principles/core/runtime-v2';
4
+ import { isSensitiveSystemPath } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
4
5
  import { scanRetiredContractSymbols } from './legacy-rule-contract-symbols.js';
5
6
  import { recordDegradedEnforcement } from './degraded-enforcement-marker.js';
6
7
  import { createNodeRuleImplementationRuntime, MAX_ACTIVE_RULES, RULE_BATCH_SOURCE_BYTES, RULE_SOURCE_BYTES, } from './rule-implementation-runtime.js';
@@ -174,6 +175,8 @@ export function createProductionRuleHostGate(options = {}) {
174
175
  SELECT a.activation_id, a.artifact_id, a.target_ref, a.action,
175
176
  c.enforcement, c.isolation_decision_id,
176
177
  p.source_rule_id, p.source_principle_id,
178
+ EXISTS (SELECT 1 FROM approvals ap WHERE ap.artifact_id = a.artifact_id
179
+ AND ap.channel = 'code_tool_hook' AND ap.status = 'approved') AS owner_approved,
177
180
  length(CAST(p.content_json AS BLOB)) AS content_bytes,
178
181
  (SELECT d.artifact_digest FROM activation_decisions d
179
182
  WHERE d.subject_kind = 'activation' AND d.activation_id = a.activation_id
@@ -232,8 +235,8 @@ export function createProductionRuleHostGate(options = {}) {
232
235
  // through the SAME budget/content/context validation and
233
236
  // evaluateBatch pipeline as live rows, but their results are
234
237
  // collected as observations only and never join mergeDecisions
235
- // (shadow observes, never enforces). The v2 gate below stays ahead
236
- // of any evaluation, so Codex v2 shadow remains suspended.
238
+ // (shadow observes, never enforces). The v2 qualification and context
239
+ // checks below remain ahead of evaluation for both activation modes.
237
240
  const activationMode = row.action === 'code_tool_hook_live_activate'
238
241
  ? 'live'
239
242
  : row.action === 'code_tool_hook_shadow_activate' ? 'shadow' : null;
@@ -325,6 +328,42 @@ export function createProductionRuleHostGate(options = {}) {
325
328
  addWarning(warnings, `unsupported_context_version: ${String(content.requiresContextVersion)}`, 'regenerate the rule artifact for context version 2');
326
329
  continue;
327
330
  }
331
+ // Codex V2 is opt-in per regenerated, Owner-approved artifact.
332
+ // This system-stamped contract is emitted only after current
333
+ // evaluator replay; merely wiring history must not revive old
334
+ // activations. Live execution additionally needs the existing
335
+ // promotion decision's binding to this exact artifact snapshot.
336
+ if (options.hostKind === 'codex') {
337
+ const contract = content.ruleHostInputContract;
338
+ const hosts = isRecord(contract) ? contract.hostKinds : undefined;
339
+ let contentBindingVerified = digestCheck.outcome === 'verified';
340
+ if (activationMode === 'shadow' && row.owner_approved === 1) {
341
+ // Old databases remain readable without writing a migration
342
+ // from this enforcement reader. A legacy approval without a
343
+ // content digest cannot grant Codex V2 execution eligibility.
344
+ const approvalColumns = connection.getDb().prepare('PRAGMA table_info(approvals)').all();
345
+ if (approvalColumns.some((column) => isRecord(column) && column.name === 'artifact_digest')) {
346
+ const approval = connection.getDb().prepare(`
347
+ SELECT artifact_digest FROM approvals
348
+ WHERE artifact_id = ? AND channel = 'code_tool_hook' AND status = 'approved'
349
+ ORDER BY decided_at DESC LIMIT 1
350
+ `).get(artifactId);
351
+ const approvalDigest = isRecord(approval) && typeof approval.artifact_digest === 'string'
352
+ ? approval.artifact_digest : null;
353
+ contentBindingVerified = verifyPiArtifactRowDigest(contentRow, contentJson, approvalDigest).outcome === 'verified';
354
+ }
355
+ else {
356
+ contentBindingVerified = false;
357
+ }
358
+ }
359
+ if (!isRecord(contract) || contract.version !== 1 || contract.contextVersion !== 2
360
+ || !Array.isArray(hosts) || !hosts.every((host) => typeof host === 'string')
361
+ || !hosts.includes('codex') || row.owner_approved !== 1
362
+ || !contentBindingVerified) {
363
+ 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');
364
+ continue;
365
+ }
366
+ }
328
367
  // SPEC-RULECODE-QUALITY-001 A3 / §7.1 (Q10): MANDATORY runtime
329
368
  // suspension of context-dependent rules. A v2 rule may only
330
369
  // execute when the provider resolved a schema-valid context
@@ -379,7 +418,7 @@ export function createProductionRuleHostGate(options = {}) {
379
418
  }
380
419
  const hostInput = {
381
420
  action,
382
- workspace: { isRiskPath: false },
421
+ workspace: { isRiskPath: isSensitiveSystemPath(action.normalizedPath) },
383
422
  session: { sessionId: event.context.sessionId, currentGfi: enrichment.currentGfi },
384
423
  evolution: { epTier: enrichment.epTier },
385
424
  derived: { estimatedLineChanges: estimateLineChanges({ toolName: input.toolName, params: input.params }), bashRisk: enrichment.bashRisk },
@@ -401,7 +440,9 @@ export function createProductionRuleHostGate(options = {}) {
401
440
  if (remaining <= 0) {
402
441
  return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'reduce active RuleCode count or source size and retry')], metadata: { evaluatedLiveRules: 0 } };
403
442
  }
404
- const liveBatch = implementationRuntime.evaluateBatch(liveCandidates.map((candidate) => ({ source: candidate.source, filename: `activation-${candidate.implId}` })), hostInput, remaining);
443
+ const liveBatch = liveCandidates.length === 0
444
+ ? { ok: true, results: [] }
445
+ : implementationRuntime.evaluateBatch(liveCandidates.map((candidate) => ({ source: candidate.source, filename: `activation-${candidate.implId}` })), hostInput, remaining);
405
446
  if (!liveBatch.ok || !liveBatch.results) {
406
447
  return { decision: 'allow', source: event.source, warnings: [boundedWarning(`${liveBatch.reason ?? 'rule_batch_failed'}: ${liveBatch.detail ?? 'unknown failure'}`, 'inspect active RuleCode resource use and repair or deactivate the unhealthy rule')], metadata: { evaluatedLiveRules: 0 } };
407
448
  }
@@ -1,3 +1,21 @@
1
1
  import type { BehaviorEvidencePort, ToolSemanticRegistry } from '@principles/core/runtime-v2';
2
+ import type { RuleHostContextRow } from '@principles/core/trajectory-types';
2
3
  /** Lazy storage access: missing labels need no DB, and a failed open remains observable. */
3
4
  export declare function createWorkspaceBehaviorEvidencePort(workspaceDir: string, toolSemantics?: ToolSemanticRegistry): BehaviorEvidencePort;
5
+ /**
6
+ * Read one bounded, same-session history window from the canonical trajectory
7
+ * database. The timestamp is the caller's pre-event cutoff; rows at or after
8
+ * it are excluded so replayed/future calls cannot enter a live RuleContext.
9
+ * This is read-only and never creates or migrates trajectory state.
10
+ */
11
+ export declare function readWorkspaceRuleContextRowsBefore(workspaceDir: string, input: {
12
+ sessionId: string;
13
+ projectDir: string;
14
+ beforeCreatedAt: string;
15
+ excludeToolUseId: string;
16
+ limit: number;
17
+ }): {
18
+ rows: readonly RuleHostContextRow[];
19
+ truncated: boolean;
20
+ unscopedPriorRows: boolean;
21
+ };
@@ -72,3 +72,50 @@ export function createWorkspaceBehaviorEvidencePort(workspaceDir, toolSemantics)
72
72
  },
73
73
  };
74
74
  }
75
+ /**
76
+ * Read one bounded, same-session history window from the canonical trajectory
77
+ * database. The timestamp is the caller's pre-event cutoff; rows at or after
78
+ * it are excluded so replayed/future calls cannot enter a live RuleContext.
79
+ * This is read-only and never creates or migrates trajectory state.
80
+ */
81
+ export function readWorkspaceRuleContextRowsBefore(workspaceDir, input) {
82
+ const { sessionId, projectDir, beforeCreatedAt, excludeToolUseId, limit } = input;
83
+ if (sessionId.trim().length === 0)
84
+ throw new Error('history_session_id_invalid');
85
+ if (!path.isAbsolute(projectDir))
86
+ throw new Error('history_project_dir_invalid');
87
+ if (excludeToolUseId.trim().length === 0)
88
+ throw new Error('history_tool_use_id_invalid');
89
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
90
+ throw new Error('history_limit_invalid');
91
+ if (!Number.isFinite(Date.parse(beforeCreatedAt)))
92
+ throw new Error('history_cutoff_invalid');
93
+ const db = new Database(path.join(workspaceDir, '.state', 'trajectory.db'), { readonly: true, fileMustExist: true });
94
+ try {
95
+ db.pragma('busy_timeout = 100');
96
+ return db.transaction(() => {
97
+ const rows = db.prepare(`
98
+ SELECT id, tool_name, outcome, params_json
99
+ FROM tool_calls
100
+ WHERE session_id = ? AND project_dir = ? AND created_at < ? AND (tool_use_id IS NULL OR tool_use_id <> ?)
101
+ ORDER BY created_at DESC, id DESC
102
+ LIMIT ?
103
+ `).all(sessionId, projectDir, beforeCreatedAt, excludeToolUseId, limit + 1);
104
+ const unscoped = db.prepare(`
105
+ SELECT 1 AS found FROM tool_calls
106
+ WHERE session_id = ? AND project_dir IS NULL AND created_at < ?
107
+ AND (tool_use_id IS NULL OR tool_use_id <> ?) LIMIT 1
108
+ `).get(sessionId, beforeCreatedAt, excludeToolUseId);
109
+ if (!Array.isArray(rows))
110
+ throw new Error('history_query_invalid');
111
+ return {
112
+ rows: rows.slice(0, limit).reverse().map(contextRow),
113
+ truncated: rows.length > limit,
114
+ unscopedPriorRows: unscoped !== undefined,
115
+ };
116
+ })();
117
+ }
118
+ finally {
119
+ db.close();
120
+ }
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.10.2",
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.4",
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"