create-principles-disciple 1.145.6 → 1.145.7
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/_release/product-identity.json +2 -2
- package/console/dist/server/models/CodexGovernanceHealthModel.d.ts +39 -2
- package/console/dist/server/models/CodexGovernanceHealthModel.js +228 -29
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js +28 -9
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js +29 -9
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts +6 -0
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +105 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +6 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +3 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts +154 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js +686 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -0
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +21 -3
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +48 -5
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +17 -0
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.js +45 -2
- package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts +0 -19
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js +31 -0
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js.map +1 -1
- package/core/dist/runtime-v2/internalization/intent-contract.d.ts +31 -0
- package/core/dist/runtime-v2/internalization/intent-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/intent-contract.js +42 -0
- package/core/dist/runtime-v2/internalization/intent-contract.js.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +14 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +64 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.js +84 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts +13 -1
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-output.js +13 -1
- package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +15 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +29 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts +2 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.js +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.js.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts +1 -1
- package/core/dist/telemetry-event.d.ts +4 -2
- package/core/dist/telemetry-event.d.ts.map +1 -1
- package/core/dist/telemetry-event.js +12 -0
- package/core/dist/telemetry-event.js.map +1 -1
- package/core/package.json +3 -3
- package/host-runtime/dist/internalization-consumer-cycle.d.ts +10 -1
- package/host-runtime/dist/internalization-consumer-cycle.js +85 -4
- package/host-runtime/dist/production-rulehost-gate.js +40 -3
- package/host-runtime/package.json +1 -1
- package/package.json +1 -1
- package/plugin/dist/bundle.js +655 -781
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +27 -27
- package/release-manager/package.json +1 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPEC-RULECODE-QUALITY-001 A1 (PRI-995) — artificer intent-basis preflight.
|
|
3
|
+
*
|
|
4
|
+
* The authoritative PRE-MODEL gate for the INTENT BASIS of Artificer tasks on
|
|
5
|
+
* the `code_tool_hook` channel, invoked by the shared consumer cycle AFTER
|
|
6
|
+
* the Owner-evidence preflight and BEFORE any runner construction:
|
|
7
|
+
*
|
|
8
|
+
* 1. resolve the scribe artifact the artificer would consume (the SAME
|
|
9
|
+
* dependency-walk semantics as ArtificerRunner.buildContext);
|
|
10
|
+
* 2. verify the intent basis deterministically:
|
|
11
|
+
* - intentContract present + valid (the prose anchor);
|
|
12
|
+
* - executionConditions present + valid (the machine-readable
|
|
13
|
+
* declaration of which trusted external facts the principle's
|
|
14
|
+
* enforcement depends on);
|
|
15
|
+
* - every declared required fact resolvable through a host
|
|
16
|
+
* trusted-facts port (SPEC 7.2 four states);
|
|
17
|
+
* 3. verdict mapping (SPEC 5.3):
|
|
18
|
+
* executable → generation may proceed; a versioned
|
|
19
|
+
* derived record lands in task metadata
|
|
20
|
+
* intent_unverified / intent_conflict / unsupported /
|
|
21
|
+
* needs_evidence → atomic needs_human_review stop point
|
|
22
|
+
* with a stable reasonCode; ZERO
|
|
23
|
+
* generation calls (Q01/Q20)
|
|
24
|
+
* source_error → transient store I/O: bounded
|
|
25
|
+
* technical retry with restart-durable
|
|
26
|
+
* backoff, ZERO model cost, NEVER an
|
|
27
|
+
* Owner-fault label (Q21)
|
|
28
|
+
* source_fault_persistent → 3rd consecutive transient fault:
|
|
29
|
+
* auto-retry ends in a reviewable
|
|
30
|
+
* terminal that names the source fault
|
|
31
|
+
* (Q22)
|
|
32
|
+
*
|
|
33
|
+
* The stored `intentBasisVerification` record is DERIVED EVIDENCE, never a
|
|
34
|
+
* pass token: every invocation re-derives the assessment from the CURRENT
|
|
35
|
+
* scribe artifact, so any material change invalidates a prior record (Q13)
|
|
36
|
+
* and the generator can never self-declare its own gate satisfied (Q20 —
|
|
37
|
+
* the gate reads the scribe artifact from the store, not model output).
|
|
38
|
+
*
|
|
39
|
+
* A defensive mirror of the executable/stop decision lives inside
|
|
40
|
+
* ArtificerRunner.invokeRuntime (PRI-981 precedent) so direct run-once/CLI
|
|
41
|
+
* invocation cannot bypass the entry gate.
|
|
42
|
+
*/
|
|
43
|
+
/* eslint-disable @typescript-eslint/no-use-before-define -- helper functions declared after the main entry, matching the adversarial-loop.ts convention */
|
|
44
|
+
import { createHash } from 'node:crypto';
|
|
45
|
+
import { extractIntentContract, extractExecutionConditions } from './intent-contract.js';
|
|
46
|
+
import { buildArtificerRunCorrelation } from './artificer-run-correlation.js';
|
|
47
|
+
import { PDRuntimeError } from '../error-categories.js';
|
|
48
|
+
import { createPITaskDiagnosticJson, hydratePITaskRecord, mergePITaskMetadata, SOURCE_FAULT_MAX_CONSECUTIVE_FAILURES, SOURCE_FAULT_PERSISTENT_REASON_CODE, } from './pitask-metadata.js';
|
|
49
|
+
/**
|
|
50
|
+
* Lineage marker for intent NHR transitions (honest by construction — same
|
|
51
|
+
* convention as ARTIFICER_EVIDENCE_NHR_SOURCE_RUN_ID: no lease-minted run
|
|
52
|
+
* exists for a pure preflight block).
|
|
53
|
+
*/
|
|
54
|
+
export const ARTIFICER_INTENT_NHR_SOURCE_RUN_ID = 'intent-preflight';
|
|
55
|
+
const WAKE_ELIGIBLE_STATUSES = ['pending', 'retry_wait'];
|
|
56
|
+
/** rc-8: bounded single-line detail for metadata/telemetry payloads. */
|
|
57
|
+
function boundedDetail(value, max = 480) {
|
|
58
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
59
|
+
return flat.length > max ? `${flat.slice(0, max)}…[truncated]` : flat;
|
|
60
|
+
}
|
|
61
|
+
function withNhrOutEdge(nextAction) {
|
|
62
|
+
return `${nextAction} Out-edge once the intent basis is repaired (or to abandon this review): pd runtime internalization retry --confirm.`;
|
|
63
|
+
}
|
|
64
|
+
function sha256Of(value) {
|
|
65
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(value), 'utf8').digest('hex')}`;
|
|
66
|
+
}
|
|
67
|
+
function readDiagnosticJson(task) {
|
|
68
|
+
const raw = task.diagnosticJson;
|
|
69
|
+
return typeof raw === 'string' && raw.trim() !== '' ? raw : null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Atomic needs_human_review transition (evidence-preflight precedent): status
|
|
73
|
+
* + humanReviewContext (+ any same-write metadata overrides) in ONE
|
|
74
|
+
* CAS-guarded write, then the read-back invariant. Idempotent for a task
|
|
75
|
+
* already in needs_human_review (Q14: a repeat stop never duplicates or
|
|
76
|
+
* rewrites the review context — the lost CAS returns the durable state).
|
|
77
|
+
*/
|
|
78
|
+
async function transitionToNeedsHumanReview(params, context, metadataOverrides = {}) {
|
|
79
|
+
const { taskId, piTask, stateManager } = params;
|
|
80
|
+
const merged = mergePITaskMetadata(piTask, { humanReviewContext: context, ...metadataOverrides });
|
|
81
|
+
const patch = {
|
|
82
|
+
status: 'needs_human_review',
|
|
83
|
+
diagnosticJson: createPITaskDiagnosticJson(merged),
|
|
84
|
+
};
|
|
85
|
+
const expectedDiagnosticJson = readDiagnosticJson(piTask);
|
|
86
|
+
let updated = null;
|
|
87
|
+
for (const expectedStatus of WAKE_ELIGIBLE_STATUSES) {
|
|
88
|
+
updated = await stateManager.updateTaskIfDiagnosticJsonUnchanged(taskId, expectedDiagnosticJson, patch, { expectedStatus });
|
|
89
|
+
if (updated !== null)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (!updated) {
|
|
93
|
+
const current = await stateManager.getTask(taskId);
|
|
94
|
+
return current?.status === 'needs_human_review';
|
|
95
|
+
}
|
|
96
|
+
const current = await stateManager.getTask(taskId);
|
|
97
|
+
if (!current || current.status !== 'needs_human_review') {
|
|
98
|
+
throw new PDRuntimeError('storage_unavailable', `needs_human_review effect not durable for task ${taskId} (read-back status: ${current?.status ?? 'task_missing'})`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** Best-effort metadata patch from a WAKE-ELIGIBLE state; false = CAS lost (observable, never fabricated). */
|
|
103
|
+
async function patchTaskMetadata(params, overrides) {
|
|
104
|
+
const { taskId, piTask, stateManager } = params;
|
|
105
|
+
const merged = mergePITaskMetadata(piTask, overrides);
|
|
106
|
+
for (const expectedStatus of WAKE_ELIGIBLE_STATUSES) {
|
|
107
|
+
const updated = await stateManager.updateTaskIfDiagnosticJsonUnchanged(taskId, readDiagnosticJson(piTask), { diagnosticJson: createPITaskDiagnosticJson(merged) }, { expectedStatus });
|
|
108
|
+
if (updated !== null)
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the scribe artifact with the SAME dependency-walk semantics as
|
|
115
|
+
* ArtificerRunner.buildContext: dependencyTaskIds → succeeded scribe task →
|
|
116
|
+
* first artifact. Store errors bubble as source faults; a clean absence
|
|
117
|
+
* returns null (unresolved dependency — a material stop, not transient).
|
|
118
|
+
*/
|
|
119
|
+
async function resolveScribeArtifact(params) {
|
|
120
|
+
const { piTask, stateManager, artifactStore } = params;
|
|
121
|
+
for (const depId of piTask.dependencyTaskIds) {
|
|
122
|
+
const depTask = await stateManager.getTask(depId);
|
|
123
|
+
if (!depTask || depTask.taskKind !== 'scribe' || depTask.status !== 'succeeded')
|
|
124
|
+
continue;
|
|
125
|
+
const store = artifactStore ?? stateManager.piArtifactStore;
|
|
126
|
+
const artifacts = await store.listBySourceTaskId(depId);
|
|
127
|
+
const [first] = artifacts;
|
|
128
|
+
if (first !== undefined) {
|
|
129
|
+
// The scribe task's OWN dependencies carry the upstream intent material
|
|
130
|
+
// (philosopher stage) — the second, independent leg of the intent basis
|
|
131
|
+
// (Owner review R3: a verdict may never rest on the scribe artifact
|
|
132
|
+
// alone, because every audited surface would then come from one model
|
|
133
|
+
// output).
|
|
134
|
+
const hydrated = hydratePITaskRecord(depTask);
|
|
135
|
+
const scribeDependencyTaskIds = hydrated?.dependencyTaskIds ?? [];
|
|
136
|
+
return { artifactId: first.artifactId, contentJson: first.contentJson, scribeDependencyTaskIds };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the UPSTREAM intent material (philosopher stage) — the independent
|
|
143
|
+
* second leg the intent verdict must be bound to. Walks the scribe task's
|
|
144
|
+
* dependencies for a succeeded philosopher task and flattens its artifact's
|
|
145
|
+
* string content into a normalised material pool. Returns null when no
|
|
146
|
+
* philosopher artifact is resolvable — a condition the caller must treat as a
|
|
147
|
+
* STOP (a generation verdict that rests on the scribe artifact alone is
|
|
148
|
+
* exactly the under-reporting shape the R3 review reproduced).
|
|
149
|
+
*/
|
|
150
|
+
async function resolveUpstreamMaterial(params, scribeDependencyTaskIds) {
|
|
151
|
+
const { stateManager, artifactStore } = params;
|
|
152
|
+
const store = artifactStore ?? stateManager.piArtifactStore;
|
|
153
|
+
return collectUpstreamNormalisedText(async (depId) => stateManager.getTask(depId), (sourceTaskId) => store.listBySourceTaskId(sourceTaskId), scribeDependencyTaskIds);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Shared upstream-material resolver (entry preflight AND the runner's
|
|
157
|
+
* defensive mirror use the SAME walk so the mirror can never be weaker than
|
|
158
|
+
* the gate).
|
|
159
|
+
*/
|
|
160
|
+
export async function collectUpstreamNormalisedText(getTask, listBySourceTaskId, scribeDependencyTaskIds) {
|
|
161
|
+
for (const depId of scribeDependencyTaskIds) {
|
|
162
|
+
const depTask = await getTask(depId);
|
|
163
|
+
if (!depTask || depTask.taskKind !== 'philosopher' || depTask.status !== 'succeeded')
|
|
164
|
+
continue;
|
|
165
|
+
const artifacts = await listBySourceTaskId(depId);
|
|
166
|
+
const [first] = artifacts;
|
|
167
|
+
if (first === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
const parsed = safeParseJson(first.contentJson);
|
|
170
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
171
|
+
return null;
|
|
172
|
+
const texts = [];
|
|
173
|
+
// Only intent-bearing fields count as upstream material. Artifact IDs
|
|
174
|
+
// and timestamps cannot turn an empty/corrupt analysis into evidence.
|
|
175
|
+
// `analysis` retains the existing compact philosopher material shape.
|
|
176
|
+
for (const key of ['thesis', 'principleCandidate', 'risks', 'analysis']) {
|
|
177
|
+
if (Object.hasOwn(parsed, key))
|
|
178
|
+
collectAllStrings(Reflect.get(parsed, key), texts, 0);
|
|
179
|
+
}
|
|
180
|
+
const normalisedText = normaliseForAnchor(texts.join(' '));
|
|
181
|
+
return normalisedText.length > 0 ? { normalisedText } : null;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function safeParseJson(contentJson) {
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(contentJson);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Depth-bounded string collector for the upstream material pool (rc-8 bound). */
|
|
194
|
+
function collectAllStrings(value, out, depth) {
|
|
195
|
+
if (depth > 6 || out.length > 400)
|
|
196
|
+
return;
|
|
197
|
+
if (typeof value === 'string') {
|
|
198
|
+
if (value.trim() !== '')
|
|
199
|
+
out.push(value);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
for (const entry of value)
|
|
204
|
+
collectAllStrings(entry, out, depth + 1);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === 'object' && value !== null) {
|
|
208
|
+
for (const key of Object.keys(value)) {
|
|
209
|
+
collectAllStrings(value[key], out, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Normalise text for anchoring: lowercase, collapse whitespace, strip
|
|
215
|
+
* punctuation to a comparable spine. Deliberately language-mixed (the scribe
|
|
216
|
+
* contract can carry zh-CN or en material).
|
|
217
|
+
*/
|
|
218
|
+
function normaliseForAnchor(text) {
|
|
219
|
+
return text.toLowerCase().replace(/[\s`'"“”‘’、,。:;!?()\]{}()【】,.;:!?\-—_/\\*#]+/gu, '');
|
|
220
|
+
}
|
|
221
|
+
/** Approval-semantics signal vocabulary (substring, EN+ZH). */
|
|
222
|
+
const APPROVAL_SIGNAL_WORDS = ['confirm', 'approv', '批准', '确认'];
|
|
223
|
+
/** Which vocabulary words appear in a raw text (for upstream audit detail). */
|
|
224
|
+
function approvalSignalsInText(text) {
|
|
225
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
226
|
+
return [];
|
|
227
|
+
const lower = text.toLowerCase();
|
|
228
|
+
return APPROVAL_SIGNAL_WORDS.filter((word) => lower.includes(word));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Collect the intent-distillation material from the parsed scribe artifact:
|
|
232
|
+
* the four Owner-intent contract fields plus the principle draft statement —
|
|
233
|
+
* exactly the surfaces the scribe instructions require to be grounded in
|
|
234
|
+
* Owner-attributed text. Absent/ill-typed fields are skipped (the
|
|
235
|
+
* intentContract presence itself is already a precondition upstream).
|
|
236
|
+
*/
|
|
237
|
+
function collectIntentMaterial(parsed) {
|
|
238
|
+
const fields = [];
|
|
239
|
+
const push = (name, value) => {
|
|
240
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
241
|
+
fields.push({ name, text: value });
|
|
242
|
+
};
|
|
243
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
244
|
+
const record = parsed;
|
|
245
|
+
const draft = typeof record.principleDraft === 'object' && record.principleDraft !== null
|
|
246
|
+
? record.principleDraft
|
|
247
|
+
: {};
|
|
248
|
+
push('principleDraft.statement', draft.statement);
|
|
249
|
+
if (typeof record.intentContract === 'object' && record.intentContract !== null) {
|
|
250
|
+
const contract = record.intentContract;
|
|
251
|
+
push('intentContract.ownerIntent', contract.ownerIntent);
|
|
252
|
+
push('intentContract.targetBehavior', contract.targetBehavior);
|
|
253
|
+
push('intentContract.forbiddenBehavior', contract.forbiddenBehavior);
|
|
254
|
+
push('intentContract.validationExpectation', contract.validationExpectation);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const normalisedText = normaliseForAnchor(fields.map((field) => field.text).join(' '));
|
|
258
|
+
const approvalSignalFields = fields
|
|
259
|
+
.filter((field) => APPROVAL_SIGNAL_WORDS.some((word) => field.text.toLowerCase().includes(word)))
|
|
260
|
+
.map((field) => field.name);
|
|
261
|
+
return {
|
|
262
|
+
normalisedText,
|
|
263
|
+
approvalSignalFields,
|
|
264
|
+
hasApprovalSignal: approvalSignalFields.length > 0,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A declared basis is anchored when a bounded normalised prefix of it appears
|
|
269
|
+
* in the normalised intent material (quote-or-precise-cite contract). Very
|
|
270
|
+
* short bases (<8 normalised chars) must appear in full — too little text to
|
|
271
|
+
* trust a partial anchor.
|
|
272
|
+
*/
|
|
273
|
+
function isTextualBasisAnchored(textualBasis, normalisedMaterial) {
|
|
274
|
+
const normalisedBasis = normaliseForAnchor(textualBasis);
|
|
275
|
+
if (normalisedBasis.length < 4)
|
|
276
|
+
return false;
|
|
277
|
+
if (normalisedMaterial.length === 0)
|
|
278
|
+
return false;
|
|
279
|
+
const anchor = normalisedBasis.length >= 8 ? normalisedBasis.slice(0, 12) : normalisedBasis;
|
|
280
|
+
return normalisedMaterial.includes(anchor);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The pure decision core (no I/O): assess the intent basis from the resolved
|
|
284
|
+
* scribe artifact. Deterministic — no LLM, no keyword approval detection; the
|
|
285
|
+
* condition basis is the scribe's structured declaration, never prose parsed
|
|
286
|
+
* at this gate.
|
|
287
|
+
*/
|
|
288
|
+
export function assessIntentBasis(input) {
|
|
289
|
+
const { scribeArtifact, upstreamMaterial, trustedFacts } = input;
|
|
290
|
+
if (scribeArtifact === null) {
|
|
291
|
+
return {
|
|
292
|
+
status: 'intent_unverified',
|
|
293
|
+
reasonCode: 'scribe_dependency_unresolved',
|
|
294
|
+
detail: 'no succeeded scribe dependency artifact resolvable for this artificer task',
|
|
295
|
+
nextAction: 'Inspect the task lineage; the scribe stage must succeed and persist its artifact before code generation.',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Owner review R3 — UPSTREAM BINDING: every audited surface below would be
|
|
299
|
+
// one model output grading itself if the scribe artifact were the only
|
|
300
|
+
// material. No philosopher material → no verdict.
|
|
301
|
+
if (upstreamMaterial === null || upstreamMaterial.normalisedText.trim() === '') {
|
|
302
|
+
return {
|
|
303
|
+
status: 'intent_unverified',
|
|
304
|
+
reasonCode: 'upstream_material_unavailable',
|
|
305
|
+
detail: "no valid, non-empty philosopher material resolvable from the scribe task's dependencies — missing, malformed, or empty upstream artifacts cannot establish the intent basis",
|
|
306
|
+
nextAction: 'Inspect the task lineage and philosopher artifact content; restore valid non-empty intent material before retrying generation.',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(scribeArtifact.contentJson);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return {
|
|
315
|
+
status: 'intent_unverified',
|
|
316
|
+
reasonCode: 'scribe_artifact_unparseable',
|
|
317
|
+
detail: `scribe artifact ${scribeArtifact.artifactId} contentJson is not parseable JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
318
|
+
nextAction: 'Inspect and repair the scribe artifact row; a corrupted anchor must not reach generation.',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const intentContract = extractIntentContract(parsed);
|
|
322
|
+
if (intentContract === null) {
|
|
323
|
+
const hadKey = typeof parsed === 'object' && parsed !== null && Object.hasOwn(parsed, 'intentContract');
|
|
324
|
+
return {
|
|
325
|
+
status: 'intent_unverified',
|
|
326
|
+
reasonCode: hadKey ? 'intent_contract_invalid' : 'intent_contract_missing',
|
|
327
|
+
detail: hadKey
|
|
328
|
+
? `scribe artifact ${scribeArtifact.artifactId} carries a malformed intentContract`
|
|
329
|
+
: `scribe artifact ${scribeArtifact.artifactId} predates the intent contract (no intentContract key)`,
|
|
330
|
+
nextAction: 'Re-run the scribe stage so the Owner-intent contract is distilled and carried on the artifact.',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const { conditions, corrupted } = extractExecutionConditions(parsed);
|
|
334
|
+
if (conditions === null) {
|
|
335
|
+
return {
|
|
336
|
+
status: 'intent_unverified',
|
|
337
|
+
reasonCode: corrupted ? 'execution_conditions_invalid' : 'execution_conditions_missing',
|
|
338
|
+
detail: corrupted
|
|
339
|
+
? `scribe artifact ${scribeArtifact.artifactId} carries a malformed executionConditions declaration`
|
|
340
|
+
: `scribe artifact ${scribeArtifact.artifactId} predates executionConditions (legacy material cannot support pre-generation verification)`,
|
|
341
|
+
nextAction: 'Re-run the scribe stage so the machine-readable execution conditions are declared (requiredFacts: [] is valid when no trusted external fact is needed).',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// Conflict check: same factKind declared with contradictory `required` flags.
|
|
345
|
+
const requiredByKind = new Map();
|
|
346
|
+
for (const fact of conditions.requiredFacts) {
|
|
347
|
+
const previous = requiredByKind.get(fact.factKind);
|
|
348
|
+
if (previous !== undefined && previous !== fact.required) {
|
|
349
|
+
return {
|
|
350
|
+
status: 'intent_conflict',
|
|
351
|
+
reasonCode: 'required_fact_conflict',
|
|
352
|
+
detail: `executionConditions declare contradictory requirements for factKind '${fact.factKind}'`,
|
|
353
|
+
nextAction: 'Re-run the scribe stage; the contradictory declarations must be resolved at the source, never by the generator.',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
requiredByKind.set(fact.factKind, fact.required);
|
|
357
|
+
}
|
|
358
|
+
// SPEC-RULECODE-QUALITY-001 A1 (Owner review R2/R3) — two-direction audit
|
|
359
|
+
// of the declaration against BOTH material pools (scribe + upstream).
|
|
360
|
+
// 口径 (R3 exact-bypass fix): only required:true declarations count as
|
|
361
|
+
// claimed — a required:false entry is semantically unclaimed (downstream
|
|
362
|
+
// filters it out), so design_approval + required:false must NOT satisfy
|
|
363
|
+
// the name check and then filter to an empty dependency set.
|
|
364
|
+
// The keyword sweep is a SECONDARY, stop-only layer; the PRIMARY guarantee
|
|
365
|
+
// is the upstream binding above. Known disclosed boundary (NOT closed):
|
|
366
|
+
// synonym phrasings absent from the small vocabulary can evade the sweep
|
|
367
|
+
// when BOTH layers use them — the structural answers are the upstream
|
|
368
|
+
// binding and the structured declaration, not vocabulary growth.
|
|
369
|
+
const scribeMaterial = collectIntentMaterial(parsed);
|
|
370
|
+
const effectiveFacts = conditions.requiredFacts.filter((fact) => fact.required);
|
|
371
|
+
const effectiveFactKinds = effectiveFacts.map((fact) => fact.factKind.toLowerCase()).join(' ');
|
|
372
|
+
const upstreamSignalFields = approvalSignalsInText(upstreamMaterial.normalisedText);
|
|
373
|
+
const pools = [
|
|
374
|
+
{ label: 'scribe', hasSignal: scribeMaterial.hasApprovalSignal, fields: scribeMaterial.approvalSignalFields },
|
|
375
|
+
{ label: 'upstream', hasSignal: upstreamSignalFields.length > 0, fields: upstreamSignalFields },
|
|
376
|
+
];
|
|
377
|
+
for (const pool of pools) {
|
|
378
|
+
if (pool.hasSignal && !/approv/.test(effectiveFactKinds)) {
|
|
379
|
+
return {
|
|
380
|
+
status: 'intent_unverified',
|
|
381
|
+
reasonCode: 'approval_dependency_undeclared',
|
|
382
|
+
detail: `${pool.label} material carries approval semantics (signal in: ${pool.fields.join(', ')}) but executionConditions declare no REQUIRED approval-kind fact — an under-reported approval dependency must not reach generation`,
|
|
383
|
+
nextAction: 'Re-run the scribe stage: declare the approval dependency in executionConditions (factKind e.g. design_approval with required: true) with its textual basis, or correct the material if it overstates the requirement.',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// textualBasis anchoring — same required:true口径; the anchor pool is the
|
|
388
|
+
// UNION of the scribe intent material and the upstream material, so a fact
|
|
389
|
+
// requirement can be bound to the upstream intent it was distilled from.
|
|
390
|
+
const combinedPool = scribeMaterial.normalisedText + ' ' + upstreamMaterial.normalisedText;
|
|
391
|
+
for (const fact of effectiveFacts) {
|
|
392
|
+
if (!isTextualBasisAnchored(fact.textualBasis, combinedPool)) {
|
|
393
|
+
return {
|
|
394
|
+
status: 'intent_unverified',
|
|
395
|
+
reasonCode: 'textual_basis_unanchored',
|
|
396
|
+
detail: `required fact '${fact.factKind}' textualBasis ("${fact.textualBasis.replace(/\s+/g, ' ').slice(0, 120)}") cannot be anchored in the intent material (scribe intentContract + statement + upstream philosopher material)`,
|
|
397
|
+
nextAction: 'Re-run the scribe stage: quote or precisely cite the Owner-attributed text the fact requirement was read from.',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const requiredFacts = effectiveFacts;
|
|
402
|
+
if (requiredFacts.length > 0 && trustedFacts === undefined) {
|
|
403
|
+
return {
|
|
404
|
+
status: 'unsupported',
|
|
405
|
+
reasonCode: 'trusted_fact_provider_absent',
|
|
406
|
+
detail: `executionConditions require trusted fact(s) [${requiredFacts.map((fact) => fact.factKind).join(', ')}] but this host injects no trusted-facts provider`,
|
|
407
|
+
nextAction: 'Host capability gap, not a transient error: generation waits until a trusted-facts source exists (SPEC §9 缺provider).',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const resolutions = [];
|
|
411
|
+
for (const fact of requiredFacts) {
|
|
412
|
+
const outcome = trustedFacts?.resolveRequiredFact({ factKind: fact.factKind, taskId: input.taskId });
|
|
413
|
+
if (outcome === undefined || outcome.resolution === 'unknown') {
|
|
414
|
+
return {
|
|
415
|
+
status: 'needs_evidence',
|
|
416
|
+
reasonCode: 'trusted_fact_unknown',
|
|
417
|
+
detail: `trusted fact '${fact.factKind}' resolved to unknown${outcome?.detail !== undefined ? `: ${outcome.detail}` : ''}`,
|
|
418
|
+
nextAction: 'The trusted-facts source cannot confirm this fact yet; generation waits for the authoritative material.',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
resolutions.push({ factKind: fact.factKind, resolution: outcome.resolution });
|
|
422
|
+
}
|
|
423
|
+
return { status: 'executable', conditions, requiredFacts: resolutions };
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Run the intent-basis preflight for one code_tool_hook Artificer task.
|
|
427
|
+
* The caller owns channel resolution; this module never re-guesses channels.
|
|
428
|
+
*/
|
|
429
|
+
export async function preflightArtificerIntentBasis(params) {
|
|
430
|
+
const { taskId, piTask, owner, workspaceDir, eventEmitter, trustedFacts } = params;
|
|
431
|
+
let scribeArtifact;
|
|
432
|
+
try {
|
|
433
|
+
scribeArtifact = await resolveScribeArtifact(params);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
// Source I/O fault: bounded technical retry, never an Owner label (Q21/Q22).
|
|
437
|
+
const faultDetail = `artifact store read failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
438
|
+
return applySourceFault(params, faultDetail);
|
|
439
|
+
}
|
|
440
|
+
// Owner review R3: resolve the UPSTREAM (philosopher) material — the second
|
|
441
|
+
// independent leg. Store I/O on this leg is a source fault like any other
|
|
442
|
+
// (Q21); a clean unresolvable result is a material stop, handled inside
|
|
443
|
+
// assessIntentBasis.
|
|
444
|
+
let upstreamMaterial = null;
|
|
445
|
+
if (scribeArtifact !== null) {
|
|
446
|
+
try {
|
|
447
|
+
upstreamMaterial = await resolveUpstreamMaterial(params, scribeArtifact.scribeDependencyTaskIds);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const faultDetail = `upstream artifact read failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
451
|
+
return applySourceFault(params, faultDetail);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// The assessment re-derives from the CURRENT artifact on every run — a
|
|
455
|
+
// stored record is never consulted as a pass token (Q13/Q20).
|
|
456
|
+
const assessment = assessIntentBasis({ scribeArtifact, upstreamMaterial, trustedFacts, taskId });
|
|
457
|
+
eventEmitter.emitTelemetry({
|
|
458
|
+
eventType: 'artificer_intent_basis_checked',
|
|
459
|
+
traceId: taskId,
|
|
460
|
+
timestamp: new Date().toISOString(),
|
|
461
|
+
sessionId: owner,
|
|
462
|
+
agentId: 'artificer',
|
|
463
|
+
payload: {
|
|
464
|
+
...buildArtificerRunCorrelation({
|
|
465
|
+
taskId,
|
|
466
|
+
attemptNumber: piTask.attemptCount,
|
|
467
|
+
executionMode: 'preflight',
|
|
468
|
+
reasonCode: assessment.status === 'executable' ? 'executable' : assessment.reasonCode,
|
|
469
|
+
workspaceDir,
|
|
470
|
+
}),
|
|
471
|
+
channel: 'code_tool_hook',
|
|
472
|
+
gateStatus: assessment.status,
|
|
473
|
+
...(assessment.status !== 'executable' ? { detail: boundedDetail(assessment.detail) } : {}),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
// A successful read clears any lingering technical-fault state (§9) — in
|
|
477
|
+
// the SAME metadata write as the record / review context (atomic).
|
|
478
|
+
const backoffExisted = piTask.sourceFaultBackoff !== undefined;
|
|
479
|
+
const clearBackoff = backoffExisted ? { sourceFaultBackoff: undefined } : {};
|
|
480
|
+
if (assessment.status === 'executable') {
|
|
481
|
+
const parsed = scribeArtifact !== null ? safeParse(scribeArtifact.contentJson) : null;
|
|
482
|
+
const intentContract = parsed !== null ? extractIntentContract(parsed) : null;
|
|
483
|
+
const record = {
|
|
484
|
+
version: 1,
|
|
485
|
+
checkedAt: new Date().toISOString(),
|
|
486
|
+
checkedBy: 'deterministic-preflight',
|
|
487
|
+
sourceScribeArtifactId: scribeArtifact?.artifactId ?? '',
|
|
488
|
+
scribeArtifactDigest: sha256Of(scribeArtifact?.contentJson ?? ''),
|
|
489
|
+
...(intentContract !== null ? { intentContractDigest: sha256Of(intentContract) } : {}),
|
|
490
|
+
...(assessment.conditions !== undefined ? { executionConditionsDigest: sha256Of(assessment.conditions) } : {}),
|
|
491
|
+
requiredFacts: assessment.requiredFacts,
|
|
492
|
+
result: 'executable',
|
|
493
|
+
};
|
|
494
|
+
const recordPersisted = await patchTaskMetadata(params, { intentBasisVerification: record, ...clearBackoff });
|
|
495
|
+
// rc-9: a lost record CAS (concurrent writer) degrades observably — the
|
|
496
|
+
// verdict carries recordPersisted and the telemetry payload repeats it,
|
|
497
|
+
// so a silent "recorded" is never implied.
|
|
498
|
+
eventEmitter.emitTelemetry({
|
|
499
|
+
eventType: 'artificer_intent_basis_checked',
|
|
500
|
+
traceId: taskId,
|
|
501
|
+
timestamp: new Date().toISOString(),
|
|
502
|
+
sessionId: owner,
|
|
503
|
+
agentId: 'artificer',
|
|
504
|
+
payload: {
|
|
505
|
+
...buildArtificerRunCorrelation({ taskId, attemptNumber: piTask.attemptCount, executionMode: 'preflight', reasonCode: 'executable', workspaceDir }),
|
|
506
|
+
channel: 'code_tool_hook',
|
|
507
|
+
gateStatus: 'executable',
|
|
508
|
+
recordPersisted,
|
|
509
|
+
scribeArtifactDigest: record.scribeArtifactDigest,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
return { decision: 'executable', record, recordPersisted };
|
|
513
|
+
}
|
|
514
|
+
const reviewContext = {
|
|
515
|
+
reasonCode: assessment.reasonCode,
|
|
516
|
+
sourceRunId: ARTIFICER_INTENT_NHR_SOURCE_RUN_ID,
|
|
517
|
+
revisionEpoch: piTask.revisionCount ?? 0,
|
|
518
|
+
detail: boundedDetail(assessment.detail),
|
|
519
|
+
createdAt: new Date().toISOString(),
|
|
520
|
+
};
|
|
521
|
+
const reviewPersisted = await transitionToNeedsHumanReview(params, reviewContext, clearBackoff);
|
|
522
|
+
eventEmitter.emitTelemetry({
|
|
523
|
+
eventType: 'artificer_blocked_on_intent_basis',
|
|
524
|
+
traceId: taskId,
|
|
525
|
+
timestamp: new Date().toISOString(),
|
|
526
|
+
sessionId: owner,
|
|
527
|
+
agentId: 'artificer',
|
|
528
|
+
payload: {
|
|
529
|
+
...buildArtificerRunCorrelation({
|
|
530
|
+
taskId,
|
|
531
|
+
attemptNumber: piTask.attemptCount,
|
|
532
|
+
executionMode: 'preflight',
|
|
533
|
+
reasonCode: assessment.reasonCode,
|
|
534
|
+
workspaceDir,
|
|
535
|
+
}),
|
|
536
|
+
detail: boundedDetail(assessment.detail),
|
|
537
|
+
nextAction: withNhrOutEdge(assessment.nextAction),
|
|
538
|
+
reviewPersisted,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
decision: 'blocked',
|
|
543
|
+
logicalResult: assessment.status,
|
|
544
|
+
reasonCode: assessment.reasonCode,
|
|
545
|
+
detail: assessment.detail,
|
|
546
|
+
nextAction: withNhrOutEdge(assessment.nextAction),
|
|
547
|
+
reviewPersisted,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function safeParse(contentJson) {
|
|
551
|
+
try {
|
|
552
|
+
return JSON.parse(contentJson);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Apply one transient source fault: bounded counter + backoff, or the persistent terminal (Q21/Q22). */
|
|
559
|
+
async function applySourceFault(params, faultDetail) {
|
|
560
|
+
const { taskId, piTask, owner, workspaceDir, eventEmitter } = params;
|
|
561
|
+
const backoff = buildBackoffRecord((piTask.sourceFaultBackoff?.consecutiveFailures ?? 0) + 1, faultDetail);
|
|
562
|
+
eventEmitter.emitTelemetry({
|
|
563
|
+
eventType: 'artificer_intent_basis_checked',
|
|
564
|
+
traceId: taskId,
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
sessionId: owner,
|
|
567
|
+
agentId: 'artificer',
|
|
568
|
+
payload: {
|
|
569
|
+
...buildArtificerRunCorrelation({
|
|
570
|
+
taskId,
|
|
571
|
+
attemptNumber: piTask.attemptCount,
|
|
572
|
+
executionMode: 'preflight',
|
|
573
|
+
reasonCode: backoff.consecutiveFailures >= SOURCE_FAULT_MAX_CONSECUTIVE_FAILURES
|
|
574
|
+
? SOURCE_FAULT_PERSISTENT_REASON_CODE
|
|
575
|
+
: 'source_error',
|
|
576
|
+
workspaceDir,
|
|
577
|
+
}),
|
|
578
|
+
channel: 'code_tool_hook',
|
|
579
|
+
gateStatus: 'source_error',
|
|
580
|
+
consecutiveFailures: backoff.consecutiveFailures,
|
|
581
|
+
detail: boundedDetail(faultDetail),
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
const persistent = await advanceSourceFaultBackoff(params, faultDetail);
|
|
585
|
+
if (persistent) {
|
|
586
|
+
eventEmitter.emitTelemetry({
|
|
587
|
+
eventType: 'artificer_blocked_on_intent_basis',
|
|
588
|
+
traceId: taskId,
|
|
589
|
+
timestamp: new Date().toISOString(),
|
|
590
|
+
sessionId: owner,
|
|
591
|
+
agentId: 'artificer',
|
|
592
|
+
payload: {
|
|
593
|
+
...buildArtificerRunCorrelation({
|
|
594
|
+
taskId,
|
|
595
|
+
attemptNumber: piTask.attemptCount,
|
|
596
|
+
executionMode: 'preflight',
|
|
597
|
+
reasonCode: SOURCE_FAULT_PERSISTENT_REASON_CODE,
|
|
598
|
+
workspaceDir,
|
|
599
|
+
}),
|
|
600
|
+
detail: boundedDetail(faultDetail),
|
|
601
|
+
nextAction: 'Auto-retry ended after repeated transient source faults; inspect the artifact/trajectory store, then recover via pd runtime internalization retry --confirm.',
|
|
602
|
+
reviewPersisted: true,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
return {
|
|
606
|
+
decision: 'source_fault_persistent',
|
|
607
|
+
reasonCode: SOURCE_FAULT_PERSISTENT_REASON_CODE,
|
|
608
|
+
detail: faultDetail,
|
|
609
|
+
nextAction: 'Auto-retry ended after repeated transient source faults; inspect the artifact/trajectory store, then recover via pd runtime internalization retry --confirm.',
|
|
610
|
+
reviewPersisted: true,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
decision: 'source_error',
|
|
615
|
+
reason: 'source_error',
|
|
616
|
+
detail: faultDetail,
|
|
617
|
+
nextAction: 'Transient source fault; the task stays pending and later consumer cycles retry at zero model cost with a bounded backoff.',
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Shared bounded source-fault advancement (SPEC §9) — used by the intent
|
|
622
|
+
* preflight AND the consumer cycle's evidence_source_error branch, so BOTH
|
|
623
|
+
* pre-model source faults share one restart-durable counter/terminal. Pure
|
|
624
|
+
* state layer: telemetry belongs to the caller.
|
|
625
|
+
*
|
|
626
|
+
* Returns true when the fault count reached the cap and the task durably
|
|
627
|
+
* landed in the source_fault_persistent needs_human_review terminal (the
|
|
628
|
+
* final backoff record rides the SAME atomic write); false while the task
|
|
629
|
+
* stays wake-eligible with an incremented counter and 1-then-2-cycle backoff.
|
|
630
|
+
*/
|
|
631
|
+
export async function advanceSourceFaultBackoff(params, faultDetail) {
|
|
632
|
+
const { piTask } = params;
|
|
633
|
+
const consecutiveFailures = (piTask.sourceFaultBackoff?.consecutiveFailures ?? 0) + 1;
|
|
634
|
+
const backoff = buildBackoffRecord(consecutiveFailures, faultDetail);
|
|
635
|
+
if (consecutiveFailures >= SOURCE_FAULT_MAX_CONSECUTIVE_FAILURES) {
|
|
636
|
+
const reviewContext = {
|
|
637
|
+
reasonCode: SOURCE_FAULT_PERSISTENT_REASON_CODE,
|
|
638
|
+
sourceRunId: ARTIFICER_INTENT_NHR_SOURCE_RUN_ID,
|
|
639
|
+
revisionEpoch: piTask.revisionCount ?? 0,
|
|
640
|
+
detail: boundedDetail(`${SOURCE_FAULT_PERSISTENT_REASON_CODE}: ${SOURCE_FAULT_MAX_CONSECUTIVE_FAILURES} consecutive transient source faults; last: ${faultDetail}`),
|
|
641
|
+
createdAt: new Date().toISOString(),
|
|
642
|
+
};
|
|
643
|
+
// The final backoff record rides the SAME atomic NHR write (metadata
|
|
644
|
+
// overrides), so the terminal state is self-describing and the restart
|
|
645
|
+
// reads the full count (Q22). The CAS expected value stays the ORIGINAL
|
|
646
|
+
// diagnosticJson — the merge happens inside the transition.
|
|
647
|
+
return transitionToNeedsHumanReview(params, reviewContext, { sourceFaultBackoff: backoff });
|
|
648
|
+
}
|
|
649
|
+
const persisted = await patchTaskMetadata(params, { sourceFaultBackoff: backoff });
|
|
650
|
+
if (!persisted) {
|
|
651
|
+
// CAS lost to a concurrent writer — the counter stays at its previous
|
|
652
|
+
// value; the caller's telemetry still shows the fault (rc-9: observable,
|
|
653
|
+
// never fabricated). The next attempt re-reads current state (rc-7).
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
function buildBackoffRecord(consecutiveFailures, faultDetail) {
|
|
658
|
+
return {
|
|
659
|
+
version: 1,
|
|
660
|
+
consecutiveFailures,
|
|
661
|
+
skipCycles: consecutiveFailures,
|
|
662
|
+
lastReason: boundedDetail(faultDetail, 240),
|
|
663
|
+
updatedAt: new Date().toISOString(),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Consume one backoff skip cycle (consumer-cycle entry point): returns true
|
|
668
|
+
* when the task must be SKIPPED this cycle (a skip was pending and has been
|
|
669
|
+
* durably decremented). A failed decrement (CAS loss) reads as "do not skip"
|
|
670
|
+
* — the next preflight re-assesses with fresh state (rc-7).
|
|
671
|
+
*/
|
|
672
|
+
export async function consumeSourceFaultBackoffSkip(taskId, piTask, stateManager) {
|
|
673
|
+
const backoff = piTask.sourceFaultBackoff;
|
|
674
|
+
if (backoff === undefined || backoff.skipCycles <= 0)
|
|
675
|
+
return false;
|
|
676
|
+
const merged = mergePITaskMetadata(piTask, {
|
|
677
|
+
sourceFaultBackoff: { ...backoff, skipCycles: backoff.skipCycles - 1, updatedAt: new Date().toISOString() },
|
|
678
|
+
});
|
|
679
|
+
for (const expectedStatus of WAKE_ELIGIBLE_STATUSES) {
|
|
680
|
+
const updated = await stateManager.updateTaskIfDiagnosticJsonUnchanged(taskId, readDiagnosticJson(piTask), { diagnosticJson: createPITaskDiagnosticJson(merged) }, { expectedStatus });
|
|
681
|
+
if (updated !== null)
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
//# sourceMappingURL=artificer-intent-preflight.js.map
|