@principles/host-runtime 0.10.0 → 0.10.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.
- package/dist/explicit-owner-statement-prefix.d.ts +17 -0
- package/dist/explicit-owner-statement-prefix.js +21 -0
- package/dist/governance-signal-admission.d.ts +2 -2
- package/dist/governance-signal-admission.js +15 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internalization-consumer-cycle.d.ts +10 -1
- package/dist/internalization-consumer-cycle.js +85 -4
- package/dist/production-rulehost-gate.js +40 -3
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The exact prefix (fullwidth colon) defined by SPEC §6.4. */
|
|
2
|
+
export declare const OWNER_STATEMENT_PREFIX = "PD\u8BB0\u5F55\u75DB\u82E6\uFF1A";
|
|
3
|
+
/** SPEC §6.1: reject instead of truncating the Owner's intent. */
|
|
4
|
+
export declare const MAX_OWNER_STATEMENT_CHARS = 500;
|
|
5
|
+
export type OwnerStatementPrefixParse = {
|
|
6
|
+
matched: false;
|
|
7
|
+
} | {
|
|
8
|
+
matched: true;
|
|
9
|
+
text: string | null;
|
|
10
|
+
reason?: 'statement_empty' | 'statement_too_long';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic prefix parse of the CURRENT user prompt. `text === null` means
|
|
14
|
+
* the prefix matched but the remainder was empty/blank — an explicit request
|
|
15
|
+
* with no content is a bounded rejection, never a fabricated statement.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseOwnerStatementPrefix(prompt: unknown): OwnerStatementPrefixParse;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The exact prefix (fullwidth colon) defined by SPEC §6.4. */
|
|
2
|
+
export const OWNER_STATEMENT_PREFIX = 'PD记录痛苦:';
|
|
3
|
+
/** SPEC §6.1: reject instead of truncating the Owner's intent. */
|
|
4
|
+
export const MAX_OWNER_STATEMENT_CHARS = 500;
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic prefix parse of the CURRENT user prompt. `text === null` means
|
|
7
|
+
* the prefix matched but the remainder was empty/blank — an explicit request
|
|
8
|
+
* with no content is a bounded rejection, never a fabricated statement.
|
|
9
|
+
*/
|
|
10
|
+
export function parseOwnerStatementPrefix(prompt) {
|
|
11
|
+
if (typeof prompt !== 'string')
|
|
12
|
+
return { matched: false };
|
|
13
|
+
if (!prompt.startsWith(OWNER_STATEMENT_PREFIX))
|
|
14
|
+
return { matched: false };
|
|
15
|
+
const remainder = prompt.slice(OWNER_STATEMENT_PREFIX.length).trim();
|
|
16
|
+
if (remainder.length === 0)
|
|
17
|
+
return { matched: true, text: null, reason: 'statement_empty' };
|
|
18
|
+
if (remainder.length > MAX_OWNER_STATEMENT_CHARS)
|
|
19
|
+
return { matched: true, text: null, reason: 'statement_too_long' };
|
|
20
|
+
return { matched: true, text: remainder };
|
|
21
|
+
}
|
|
@@ -63,7 +63,7 @@ export declare function evaluateCorrectionSignal(input: {
|
|
|
63
63
|
export type GovernanceSignalKind = 'user_correction' | 'tool_failure';
|
|
64
64
|
export interface GovernanceCorrectionCandidate {
|
|
65
65
|
readonly kind: 'user_correction';
|
|
66
|
-
readonly hostKind: 'codex';
|
|
66
|
+
readonly hostKind: 'codex' | 'workbuddy';
|
|
67
67
|
readonly logicalObservationKey: string;
|
|
68
68
|
readonly rolloutIdentity: string;
|
|
69
69
|
readonly rootSessionId: string;
|
|
@@ -72,7 +72,7 @@ export interface GovernanceCorrectionCandidate {
|
|
|
72
72
|
readonly observedAt: string;
|
|
73
73
|
/**
|
|
74
74
|
* SPEC-CODEX-PAIN-CLOSURE-001 CR-02/CR-07: the text is an EXPLICIT Owner
|
|
75
|
-
* statement (
|
|
75
|
+
* statement (native current-user `PD记录痛苦:` prefix). Explicit submissions bypass the
|
|
76
76
|
* STRONG+high-precision keyword gate — the Owner's explicit request is the
|
|
77
77
|
* signal; nothing is inferred. Rate limiting and the unique observation-key
|
|
78
78
|
* dedup still apply unchanged.
|
|
@@ -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, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
|
|
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';
|
|
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';
|
|
@@ -385,7 +385,7 @@ function tryConsumeRateLimit({ db, rootSessionId, ruleVersion, now }) {
|
|
|
385
385
|
}
|
|
386
386
|
function insertCorrectionPain({ db, candidate, workspaceDir, painId, detection, nowIso }) {
|
|
387
387
|
const reason = candidate.explicitOwnerStatement === true
|
|
388
|
-
? 'Codex explicit owner statement (PD记录痛苦 prefix)
|
|
388
|
+
? `${candidate.hostKind === 'workbuddy' ? 'WorkBuddy' : 'Codex'} explicit owner statement (PD记录痛苦 prefix)`
|
|
389
389
|
: detection !== undefined && detection.matchedTerms.length > 0
|
|
390
390
|
? `User correction detected: ${detection.matchedTerms.join(', ')}`
|
|
391
391
|
: 'User correction detected';
|
|
@@ -433,13 +433,19 @@ function taskSubmissionPayload(candidate, workspaceDir, reason) {
|
|
|
433
433
|
reason: reason || 'User correction detected',
|
|
434
434
|
score: GOVERNANCE_STRONG_PAIN_SCORE,
|
|
435
435
|
sessionId: candidate.rootSessionId,
|
|
436
|
-
hostKind:
|
|
437
|
-
|
|
436
|
+
hostKind: candidate.hostKind,
|
|
437
|
+
// WorkBuddy explicit intake has no transcript observation to promote.
|
|
438
|
+
// 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) }],
|
|
438
442
|
// SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
|
|
439
443
|
// Owner statement rides as classified correction evidence — never
|
|
440
444
|
// summarized by an AI, never inside evidence[] pseudo-trace entries.
|
|
441
445
|
...(candidate.explicitOwnerStatement === true
|
|
442
|
-
? { correctionEvidence:
|
|
446
|
+
? { correctionEvidence: candidate.hostKind === 'workbuddy'
|
|
447
|
+
? { text: boundCorrectionEvidenceText({ text: candidate.text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }, workspaceDir).text, classification: 'owner_statement', channel: 'workbuddy_explicit_prefix' }
|
|
448
|
+
: { text: sanitizeString(candidate.text, workspaceDir), classification: 'owner_statement', channel: 'codex_explicit_prefix' } }
|
|
443
449
|
: {}),
|
|
444
450
|
};
|
|
445
451
|
return JSON.stringify(payload);
|
|
@@ -632,8 +638,8 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
|
|
|
632
638
|
return { disposition: 'admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: null };
|
|
633
639
|
}
|
|
634
640
|
function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation }) {
|
|
635
|
-
if (candidate.hostKind !== 'codex') {
|
|
636
|
-
return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission
|
|
641
|
+
if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && candidate.explicitOwnerStatement === true)) {
|
|
642
|
+
return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission accepts Codex observations or explicit WorkBuddy Owner statements' };
|
|
637
643
|
}
|
|
638
644
|
const marker = db.prepare('SELECT * FROM governance_signal_admissions WHERE logical_observation_key = ?').get(candidate.logicalObservationKey);
|
|
639
645
|
if (isRecord(marker)) {
|
|
@@ -710,7 +716,8 @@ function isGovernanceTaskPayload(value) {
|
|
|
710
716
|
const correctionEvidence = own(value, 'correctionEvidence');
|
|
711
717
|
const hasOwnerStatement = isRecord(correctionEvidence)
|
|
712
718
|
&& own(correctionEvidence, 'classification') === 'owner_statement'
|
|
713
|
-
&& own(correctionEvidence, 'channel') === 'codex_explicit_prefix'
|
|
719
|
+
&& (own(correctionEvidence, 'channel') === 'codex_explicit_prefix' && hostKind === 'codex'
|
|
720
|
+
|| own(correctionEvidence, 'channel') === 'workbuddy_explicit_prefix' && hostKind === 'workbuddy')
|
|
714
721
|
&& typeof own(correctionEvidence, 'text') === 'string'
|
|
715
722
|
&& own(correctionEvidence, 'text').length > 0;
|
|
716
723
|
if (correctionEvidence !== undefined && !hasOwnerStatement)
|
package/dist/index.d.ts
CHANGED
|
@@ -88,3 +88,4 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
|
|
|
88
88
|
export * from './behavior-example-pack-assembler.js';
|
|
89
89
|
export * from './behavior-history.js';
|
|
90
90
|
export * from './workspace-behavior-evidence.js';
|
|
91
|
+
export * from './explicit-owner-statement-prefix.js';
|
package/dist/index.js
CHANGED
|
@@ -268,3 +268,4 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
268
268
|
export * from './behavior-example-pack-assembler.js';
|
|
269
269
|
export * from './behavior-history.js';
|
|
270
270
|
export * from './workspace-behavior-evidence.js';
|
|
271
|
+
export * from './explicit-owner-statement-prefix.js';
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Hosts inject: a logger, a structured-event sink, their tool catalog, and an
|
|
21
21
|
* env getter. Everything else is the existing production path.
|
|
22
22
|
*/
|
|
23
|
-
import { type ToolSemanticRegistry, type BehaviorEvidencePort } from '@principles/core/runtime-v2';
|
|
23
|
+
import { type ToolSemanticRegistry, type BehaviorEvidencePort, type ArtificerTrustedFactsPort } from '@principles/core/runtime-v2';
|
|
24
24
|
import { type ConsumerGovernanceLogger } from './internalization-consumer-governance.js';
|
|
25
25
|
export declare const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = "internalization_auto_consumer";
|
|
26
26
|
/** Structural logger port — OpenClaw PluginLogger and worker loggers satisfy this. */
|
|
@@ -71,6 +71,15 @@ export interface InternalizationConsumerCyclePorts {
|
|
|
71
71
|
* and the runner is never constructed evidence-less.
|
|
72
72
|
*/
|
|
73
73
|
readonly behaviorEvidence?: BehaviorEvidencePort;
|
|
74
|
+
/**
|
|
75
|
+
* SPEC-RULECODE-QUALITY-001 A1 (PRI-995): host-injected trusted-facts port
|
|
76
|
+
* for the artificer intent-basis preflight (SPEC 8.3 seam). Optional:
|
|
77
|
+
* absent + a scribe-declared required fact degrades OBSERVABLY
|
|
78
|
+
* (trusted_fact_provider_absent needs_human_review) and the runner is
|
|
79
|
+
* never constructed — a host capability gap never reads as an Owner fault
|
|
80
|
+
* or a transient error.
|
|
81
|
+
*/
|
|
82
|
+
readonly trustedFacts?: ArtificerTrustedFactsPort;
|
|
74
83
|
/** Log label used in human log prefixes ('AutoConsumer' | 'CodexWorker' | …). */
|
|
75
84
|
readonly logLabel: string;
|
|
76
85
|
readonly envGetter?: (name: string) => string | undefined;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE, PrincipleTreeLedgerAdapter,
|
|
24
24
|
// PRI-714: resolve outputLanguage from the same effective config the
|
|
25
25
|
// runners already receive (EP-07: canonical resolved value, not raw input).
|
|
26
|
-
resolveOutputLanguage, hydratePITaskRecord, preflightArtificerOwnerEvidence, } from '@principles/core/runtime-v2';
|
|
26
|
+
resolveOutputLanguage, hydratePITaskRecord, preflightArtificerOwnerEvidence, preflightArtificerIntentBasis, consumeSourceFaultBackoffSkip, advanceSourceFaultBackoff, } from '@principles/core/runtime-v2';
|
|
27
27
|
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
28
28
|
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from './pd-config.js';
|
|
29
29
|
import { createEvaluatorRepairDeps, createRolloutGovernanceDeps } from './internalization-consumer-governance.js';
|
|
@@ -384,6 +384,21 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
384
384
|
preflightSkipReason = 'code_rule_capability_disabled';
|
|
385
385
|
continue;
|
|
386
386
|
}
|
|
387
|
+
// SPEC-RULECODE-QUALITY-001 §9 (Q21): a pending source-fault backoff
|
|
388
|
+
// consumes one skip cycle per encounter — the task stays pending,
|
|
389
|
+
// zero model cost, and the technical retry happens only after the
|
|
390
|
+
// bounded 1-then-2-cycle backoff elapses (restart-durable).
|
|
391
|
+
if (await consumeSourceFaultBackoffSkip(candidate.taskId, leasedPiTask, stateManager)) {
|
|
392
|
+
const backoffInfo = JSON.stringify({
|
|
393
|
+
reason: 'source_fault_backoff_skip',
|
|
394
|
+
taskId: candidate.taskId,
|
|
395
|
+
nextAction: 'Transient source fault backoff in effect; the task retries automatically at zero model cost.',
|
|
396
|
+
});
|
|
397
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', backoffInfo);
|
|
398
|
+
logger.info(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped this cycle: source-fault backoff.`);
|
|
399
|
+
preflightSkipReason = 'source_fault_backoff_skip';
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
387
402
|
artificerEvidence = await preflightArtificerOwnerEvidence({
|
|
388
403
|
taskId: candidate.taskId,
|
|
389
404
|
piTask: leasedPiTask,
|
|
@@ -415,16 +430,21 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
415
430
|
// assembly at zero model cost), the runner is never constructed,
|
|
416
431
|
// and the preflight already emitted the structured degradation
|
|
417
432
|
// telemetry (reason=evidence_source_error, reviewPersisted=false).
|
|
433
|
+
// SPEC-RULECODE-QUALITY-001 §9 (Q21/Q22): the transient fault now
|
|
434
|
+
// advances the SHARED bounded source-fault backoff (restart-
|
|
435
|
+
// durable counter; 3rd consecutive fault ends auto-retry in a
|
|
436
|
+
// source_fault_persistent review terminal — never an Owner label).
|
|
437
|
+
const persistentFault = await advanceSourceFaultBackoff({ taskId: candidate.taskId, piTask: leasedPiTask, stateManager }, `evidence source error: ${artificerEvidence.detail ?? 'trajectory store I/O fault'}`);
|
|
418
438
|
const storageInfo = JSON.stringify({
|
|
419
|
-
reason: 'evidence_source_error',
|
|
439
|
+
reason: persistentFault ? 'source_fault_persistent' : 'evidence_source_error',
|
|
420
440
|
taskId: candidate.taskId,
|
|
421
441
|
taskKind: candidate.taskKind,
|
|
422
442
|
nextAction: artificerEvidence.nextAction,
|
|
423
443
|
...(artificerEvidence.detail !== undefined ? { detail: artificerEvidence.detail } : {}),
|
|
424
444
|
});
|
|
425
445
|
emitEvent('INTERNALIZATION_CONSUMER_SKIP', storageInfo);
|
|
426
|
-
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: evidence source error (storage fault preserved; task stays wake-eligible).`);
|
|
427
|
-
preflightSkipReason = 'evidence_source_error';
|
|
446
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: evidence source error (storage fault preserved; task stays wake-eligible${persistentFault ? '; auto-retry ended after repeated faults' : ''}).`);
|
|
447
|
+
preflightSkipReason = persistentFault ? 'source_fault_persistent' : 'evidence_source_error';
|
|
428
448
|
continue;
|
|
429
449
|
}
|
|
430
450
|
if (artificerEvidence.decision === 'blocked') {
|
|
@@ -444,6 +464,61 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
444
464
|
preflightSkipReason = 'artificer_blocked_on_owner_evidence';
|
|
445
465
|
continue;
|
|
446
466
|
}
|
|
467
|
+
// SPEC-RULECODE-QUALITY-001 A1 (Q01/Q20): intent-basis preflight —
|
|
468
|
+
// evidence-ready is necessary but NOT sufficient. The scribe
|
|
469
|
+
// artifact must carry a valid intentContract AND machine-readable
|
|
470
|
+
// executionConditions, and every declared required trusted fact
|
|
471
|
+
// must be resolvable through ports.trustedFacts. Any stop verdict
|
|
472
|
+
// moves the task to needs_human_review with a stable reasonCode
|
|
473
|
+
// and the runner is NEVER constructed (zero generation calls).
|
|
474
|
+
// The gate re-derives from the scribe artifact every run — a
|
|
475
|
+
// stored verification record is never a pass token.
|
|
476
|
+
const intentBasis = await preflightArtificerIntentBasis({
|
|
477
|
+
taskId: candidate.taskId,
|
|
478
|
+
piTask: leasedPiTask,
|
|
479
|
+
owner,
|
|
480
|
+
workspaceDir,
|
|
481
|
+
stateManager,
|
|
482
|
+
eventEmitter: workspaceEmitter,
|
|
483
|
+
...(ports.trustedFacts !== undefined ? { trustedFacts: ports.trustedFacts } : {}),
|
|
484
|
+
});
|
|
485
|
+
if (intentBasis.decision === 'source_error') {
|
|
486
|
+
// Q21: transient artifact-store fault — the preflight already
|
|
487
|
+
// advanced the shared bounded backoff; task stays wake-eligible.
|
|
488
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({
|
|
489
|
+
reason: 'intent_source_error',
|
|
490
|
+
taskId: candidate.taskId,
|
|
491
|
+
nextAction: intentBasis.nextAction,
|
|
492
|
+
detail: intentBasis.detail,
|
|
493
|
+
}));
|
|
494
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: intent-basis source error (transient; bounded backoff applies).`);
|
|
495
|
+
preflightSkipReason = 'intent_source_error';
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (intentBasis.decision === 'source_fault_persistent') {
|
|
499
|
+
emitEvent('INTERNALIZATION_CONSUMER_INTENT_BLOCKED', JSON.stringify({
|
|
500
|
+
taskId: candidate.taskId,
|
|
501
|
+
reasonCode: intentBasis.reasonCode,
|
|
502
|
+
nextAction: intentBasis.nextAction,
|
|
503
|
+
detail: intentBasis.detail,
|
|
504
|
+
reviewPersisted: intentBasis.reviewPersisted,
|
|
505
|
+
}));
|
|
506
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} blocked: repeated source faults ended auto-retry (${intentBasis.reasonCode}).`);
|
|
507
|
+
preflightSkipReason = 'source_fault_persistent';
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (intentBasis.decision === 'blocked') {
|
|
511
|
+
emitEvent('INTERNALIZATION_CONSUMER_INTENT_BLOCKED', JSON.stringify({
|
|
512
|
+
taskId: candidate.taskId,
|
|
513
|
+
reasonCode: intentBasis.reasonCode,
|
|
514
|
+
nextAction: intentBasis.nextAction,
|
|
515
|
+
detail: intentBasis.detail,
|
|
516
|
+
reviewPersisted: intentBasis.reviewPersisted,
|
|
517
|
+
}));
|
|
518
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} blocked on intent basis (${intentBasis.reasonCode}, reviewPersisted=${intentBasis.reviewPersisted}).`);
|
|
519
|
+
preflightSkipReason = 'artificer_blocked_on_intent_basis';
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
447
522
|
}
|
|
448
523
|
}
|
|
449
524
|
wakeResult = candidate;
|
|
@@ -643,6 +718,12 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
643
718
|
...(artificerEvidence !== null && artificerEvidence.decision === 'ready'
|
|
644
719
|
? { behaviorExamplePack: artificerEvidence.pack }
|
|
645
720
|
: {}),
|
|
721
|
+
// SPEC-RULECODE-QUALITY-001 A1 (Owner review P2): the SAME
|
|
722
|
+
// trusted-facts port the intent preflight used must ride into the
|
|
723
|
+
// runner so the defensive mirror sees the same fact resolutions —
|
|
724
|
+
// without this, a port-resolved task would pass the entry gate and
|
|
725
|
+
// then be refused by the runner's "provider absent" guard.
|
|
726
|
+
...(ports.trustedFacts !== undefined ? { trustedFacts: ports.trustedFacts } : {}),
|
|
646
727
|
},
|
|
647
728
|
// PRI-741: thread the host semantic projection into generation.
|
|
648
729
|
{ ...runnerOptions, ...(artificerHostSemanticContext !== undefined ? { hostSemanticContext: artificerHostSemanticContext } : {}) });
|
|
@@ -85,6 +85,13 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
85
85
|
if (action.normalizedPath === null)
|
|
86
86
|
return { decision: 'allow', source: event.source, metadata: { evaluatedLiveRules: 0 } };
|
|
87
87
|
let context;
|
|
88
|
+
/**
|
|
89
|
+
* SPEC-RULECODE-QUALITY-001 A3 (Q10): the precise reason a v2 rule is
|
|
90
|
+
* suspended — carried into the skip warning so each fault kind (absent /
|
|
91
|
+
* undefined / invalid / failed / unavailable) is distinguishable in host
|
|
92
|
+
* telemetry instead of merging into one opaque 'unavailable'.
|
|
93
|
+
*/
|
|
94
|
+
let contextFault;
|
|
88
95
|
let enrichment = { currentGfi: 0, epTier: 0, bashRisk: 'unknown' };
|
|
89
96
|
const request = {
|
|
90
97
|
workspaceDir: event.context.workspaceDir, sessionId: event.context.sessionId,
|
|
@@ -110,6 +117,7 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
110
117
|
const candidate = await withinGateDeadline(options.ruleContextProvider(request), startedAt);
|
|
111
118
|
if (candidate === undefined) {
|
|
112
119
|
context = undefined;
|
|
120
|
+
contextFault = 'provider_returned_undefined';
|
|
113
121
|
}
|
|
114
122
|
else {
|
|
115
123
|
const validation = validateRuleContextV2(candidate);
|
|
@@ -117,18 +125,27 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
117
125
|
context = candidate;
|
|
118
126
|
else {
|
|
119
127
|
context = UNAVAILABLE_RULE_CONTEXT;
|
|
128
|
+
contextFault = 'provider_invalid';
|
|
120
129
|
addWarning(warnings, `rule_context_invalid: ${validation.errors.join('; ')}`, 'repair the host context provider');
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
catch (error) {
|
|
125
134
|
context = UNAVAILABLE_RULE_CONTEXT;
|
|
135
|
+
contextFault = 'provider_failed';
|
|
126
136
|
addWarning(warnings, `rule_context_provider_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect the host context provider and retry');
|
|
127
137
|
}
|
|
128
138
|
}
|
|
139
|
+
else {
|
|
140
|
+
contextFault = 'provider_absent';
|
|
141
|
+
}
|
|
129
142
|
const dbPath = path.join(event.context.workspaceDir, '.pd', 'state.db');
|
|
130
143
|
if (remainingGateMs(startedAt) <= 0) {
|
|
131
|
-
|
|
144
|
+
// SPEC-RULECODE-QUALITY-001 Q24: a deadline-exhausted allow must be
|
|
145
|
+
// observable as "no rule executed" (evaluatedLiveRules=0), never read
|
|
146
|
+
// as successful governance — isolated probe finding (the early return
|
|
147
|
+
// previously carried no evaluation facts at all).
|
|
148
|
+
return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'inspect host context providers and active RuleCode resource use')], metadata: { evaluatedLiveRules: 0 } };
|
|
132
149
|
}
|
|
133
150
|
if (!fs.existsSync(dbPath)) {
|
|
134
151
|
// Security audit run-1 (gate-failopen-allow-on-state-corruption):
|
|
@@ -308,8 +325,28 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
308
325
|
addWarning(warnings, `unsupported_context_version: ${String(content.requiresContextVersion)}`, 'regenerate the rule artifact for context version 2');
|
|
309
326
|
continue;
|
|
310
327
|
}
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
// SPEC-RULECODE-QUALITY-001 A3 / §7.1 (Q10): MANDATORY runtime
|
|
329
|
+
// suspension of context-dependent rules. A v2 rule may only
|
|
330
|
+
// execute when the provider resolved a schema-valid context
|
|
331
|
+
// whose history.status is 'available'. The old check (`!context`)
|
|
332
|
+
// let the truthy UNAVAILABLE_RULE_CONTEXT sentinel (provider
|
|
333
|
+
// invalid/thrown) and schema-valid unavailable contexts through,
|
|
334
|
+
// so a v2 rule without its own self-protection still executed on
|
|
335
|
+
// untrusted input. Skipping happens per-call, per-rule — healthy
|
|
336
|
+
// sibling rules (v1 or well-fed v2) keep evaluating, and no
|
|
337
|
+
// governance state changes. Intentional behavior change vs the
|
|
338
|
+
// pre-fix "legal unavailable still executed" path.
|
|
339
|
+
if (context === undefined || context.history.status !== 'available') {
|
|
340
|
+
// Reference identity against the frozen sentinel distinguishes
|
|
341
|
+
// "the provider faulted" from "the provider legitimately
|
|
342
|
+
// declared history unavailable" (same wire shape, different
|
|
343
|
+
// fault class — Q10 asserts them separately).
|
|
344
|
+
const fault = context === undefined
|
|
345
|
+
? (contextFault ?? 'provider_absent')
|
|
346
|
+
: context === UNAVAILABLE_RULE_CONTEXT
|
|
347
|
+
? (contextFault ?? 'provider_invalid')
|
|
348
|
+
: 'history_unavailable';
|
|
349
|
+
addWarning(warnings, `rule_context_v2_unavailable: ${fault} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
|
|
313
350
|
continue;
|
|
314
351
|
}
|
|
315
352
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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.
|
|
34
|
+
"@principles/core": "^1.293.4",
|
|
35
35
|
"@principles/install-layout": "^0.4.0",
|
|
36
36
|
"better-sqlite3": "^13.0.3",
|
|
37
37
|
"js-yaml": "^5.4.3"
|