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.
Files changed (71) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/models/CodexGovernanceHealthModel.d.ts +39 -2
  3. package/console/dist/server/models/CodexGovernanceHealthModel.js +228 -29
  4. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.d.ts.map +1 -1
  5. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js +28 -9
  6. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js.map +1 -1
  7. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts.map +1 -1
  8. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js +29 -9
  9. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js.map +1 -1
  10. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts +6 -0
  11. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  12. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +105 -1
  13. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  14. package/core/dist/runtime-v2/index.d.ts +6 -2
  15. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  16. package/core/dist/runtime-v2/index.js +3 -1
  17. package/core/dist/runtime-v2/index.js.map +1 -1
  18. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts +154 -0
  19. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -0
  20. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js +686 -0
  21. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -0
  22. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +21 -3
  23. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  24. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +48 -5
  25. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  26. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +17 -0
  27. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  28. package/core/dist/runtime-v2/internalization/artificer-runner.js +45 -2
  29. package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  30. package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts +0 -19
  31. package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/internalization/behavior-example-pack.js +31 -0
  33. package/core/dist/runtime-v2/internalization/behavior-example-pack.js.map +1 -1
  34. package/core/dist/runtime-v2/internalization/intent-contract.d.ts +31 -0
  35. package/core/dist/runtime-v2/internalization/intent-contract.d.ts.map +1 -1
  36. package/core/dist/runtime-v2/internalization/intent-contract.js +42 -0
  37. package/core/dist/runtime-v2/internalization/intent-contract.js.map +1 -1
  38. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +14 -1
  39. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  40. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  41. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +64 -0
  42. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  43. package/core/dist/runtime-v2/internalization/pitask-metadata.js +84 -0
  44. package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  45. package/core/dist/runtime-v2/internalization/scribe-output.d.ts +13 -1
  46. package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  47. package/core/dist/runtime-v2/internalization/scribe-output.js +13 -1
  48. package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  49. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +15 -1
  50. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
  51. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +29 -1
  52. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
  53. package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts +2 -1
  54. package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts.map +1 -1
  55. package/core/dist/runtime-v2/tools/agent-tool-contract.js +1 -1
  56. package/core/dist/runtime-v2/tools/agent-tool-contract.js.map +1 -1
  57. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts +1 -1
  58. package/core/dist/telemetry-event.d.ts +4 -2
  59. package/core/dist/telemetry-event.d.ts.map +1 -1
  60. package/core/dist/telemetry-event.js +12 -0
  61. package/core/dist/telemetry-event.js.map +1 -1
  62. package/core/package.json +3 -3
  63. package/host-runtime/dist/internalization-consumer-cycle.d.ts +10 -1
  64. package/host-runtime/dist/internalization-consumer-cycle.js +85 -4
  65. package/host-runtime/dist/production-rulehost-gate.js +40 -3
  66. package/host-runtime/package.json +1 -1
  67. package/package.json +1 -1
  68. package/plugin/dist/bundle.js +655 -781
  69. package/plugin/dist/openclaw.plugin.json +1 -1
  70. package/plugin/dist/rulehost-evidence.js +27 -27
  71. 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