@principles/core 1.86.0 → 1.88.0

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 (33) hide show
  1. package/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js +245 -1
  2. package/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js +425 -2
  4. package/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js.map +1 -1
  5. package/dist/runtime-v2/__tests__/telemetry-event.test.js +13 -0
  6. package/dist/runtime-v2/__tests__/telemetry-event.test.js.map +1 -1
  7. package/dist/runtime-v2/internalization/evaluator-output.d.ts +2 -2
  8. package/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
  9. package/dist/runtime-v2/internalization/evaluator-output.js +29 -23
  10. package/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
  11. package/dist/runtime-v2/internalization/evaluator-runner.d.ts +74 -48
  12. package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  13. package/dist/runtime-v2/internalization/evaluator-runner.js +164 -444
  14. package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  15. package/dist/runtime-v2/internalization/scribe-output.d.ts +8 -2
  16. package/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  17. package/dist/runtime-v2/internalization/scribe-output.js +33 -17
  18. package/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  19. package/dist/runtime-v2/internalization/scribe-runner.d.ts +56 -49
  20. package/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  21. package/dist/runtime-v2/internalization/scribe-runner.js +116 -398
  22. package/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  23. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +119 -0
  24. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -1
  25. package/dist/runtime-v2/runner/base-peer-runner.d.ts +1 -1
  26. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  27. package/dist/runtime-v2/runner/base-peer-runner.js +8 -0
  28. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  29. package/dist/telemetry-event.d.ts +2 -2
  30. package/dist/telemetry-event.d.ts.map +1 -1
  31. package/dist/telemetry-event.js +1 -0
  32. package/dist/telemetry-event.js.map +1 -1
  33. package/package.json +1 -1
@@ -1,15 +1,15 @@
1
- import { PDRuntimeError } from '../error-categories.js';
1
+ import { PDRuntimeError, isPDErrorCategory } from '../error-categories.js';
2
2
  import { hydratePITaskRecord } from './pitask-metadata.js';
3
- import { RunnerPhase } from '../runner/runner-phase.js';
4
3
  import { ScribePromptBuilder } from './scribe-prompt-builder.js';
5
4
  import { injectRunnerLineageIfAbsent } from './peer-runner-contracts.js';
6
- const DEFAULT_SCRIBE_RUNNER_OPTIONS = {
5
+ import { BasePeerRunner } from '../runner/base-peer-runner.js';
6
+ export const DEFAULT_SCRIBE_RUNNER_OPTIONS = {
7
7
  pollIntervalMs: 5_000,
8
8
  timeoutMs: 300_000,
9
9
  defaultMaxAttempts: 3,
10
10
  agentId: 'scribe',
11
11
  };
12
- function resolveScribeRunnerOptions(options) {
12
+ export function resolveScribeRunnerOptions(options) {
13
13
  return {
14
14
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_SCRIBE_RUNNER_OPTIONS.pollIntervalMs,
15
15
  timeoutMs: options.timeoutMs ?? DEFAULT_SCRIBE_RUNNER_OPTIONS.timeoutMs,
@@ -19,117 +19,22 @@ function resolveScribeRunnerOptions(options) {
19
19
  agentId: options.agentId ?? DEFAULT_SCRIBE_RUNNER_OPTIONS.agentId,
20
20
  };
21
21
  }
22
- export class ScribeRunner {
23
- phase = RunnerPhase.Idle;
24
- resolvedOptions;
25
- stateManager;
26
- runtimeAdapter;
27
- eventEmitter;
22
+ // ── ScribeRunner ─────────────────────────────────────────────────────────────
23
+ export class ScribeRunner extends BasePeerRunner {
28
24
  validator;
29
- artifactStore;
30
25
  constructor(deps, options) {
31
- this.stateManager = deps.stateManager;
32
- this.runtimeAdapter = deps.runtimeAdapter;
33
- this.eventEmitter = deps.eventEmitter;
34
- this.validator = deps.validator;
35
- this.artifactStore = deps.artifactStore;
36
- this.resolvedOptions = resolveScribeRunnerOptions(options);
37
- }
38
- get currentPhase() {
39
- return this.phase;
40
- }
41
- emitScribeEvent(eventType, taskId, payload) {
42
- this.eventEmitter.emitTelemetry({
43
- eventType: eventType,
44
- traceId: taskId,
45
- timestamp: new Date().toISOString(),
46
- sessionId: this.resolvedOptions.owner,
47
- agentId: this.resolvedOptions.agentId,
48
- payload,
26
+ super(deps, options, {
27
+ runnerName: 'scribe',
28
+ expectedTaskKind: 'scribe',
29
+ defaultAgentId: 'scribe',
30
+ resultRefPrefix: 'scribe',
49
31
  });
32
+ this.validator = deps.validator;
50
33
  }
51
- async run(taskId) {
52
- this.phase = RunnerPhase.Idle;
53
- let leasedTask;
54
- try {
55
- leasedTask = await this.stateManager.acquireLease({
56
- taskId,
57
- owner: this.resolvedOptions.owner,
58
- runtimeKind: this.resolvedOptions.runtimeKind,
59
- });
60
- }
61
- catch (error) {
62
- return await this.handleLeaseOrPhaseError(taskId, error);
63
- }
64
- if (leasedTask.taskKind !== 'scribe') {
65
- this.emitScribeEvent('scribe_wrong_task_kind', taskId, {
66
- expectedKind: 'scribe',
67
- actualKind: leasedTask.taskKind,
68
- });
69
- await this.stateManager.markTaskFailed(taskId, 'input_invalid');
70
- return {
71
- status: 'failed',
72
- taskId,
73
- errorCategory: 'input_invalid',
74
- failureReason: `Task kind must be 'scribe', got '${leasedTask.taskKind}'`,
75
- attemptCount: leasedTask.attemptCount,
76
- };
77
- }
78
- this.emitScribeEvent('scribe_task_leased', taskId, {
79
- taskKind: 'scribe',
80
- attemptCount: leasedTask.attemptCount,
81
- });
82
- try {
83
- const storeRunId = await this.resolveStoreRunId(taskId);
84
- this.phase = RunnerPhase.BuildingContext;
85
- const { contextHash, philosopherArtifact, sourcePhilosopherArtifactId } = await this.buildContext(taskId);
86
- if (!philosopherArtifact) {
87
- return this.retryOrFail({
88
- taskId,
89
- task: leasedTask,
90
- errorCategory: 'input_invalid',
91
- failureReason: 'Philosopher dependency artifact not found',
92
- });
93
- }
94
- this.emitScribeEvent('scribe_context_built', taskId, { contextHash });
95
- this.phase = RunnerPhase.Invoking;
96
- const runHandle = await this.invokeRuntime({ taskId, contextHash, philosopherArtifact, sourcePhilosopherArtifactId });
97
- this.emitScribeEvent('scribe_run_started', taskId, {
98
- runtimeKind: this.resolvedOptions.runtimeKind,
99
- });
100
- this.phase = RunnerPhase.Polling;
101
- const finalStatus = await this.pollUntilTerminal(runHandle);
102
- if (finalStatus.status !== 'succeeded') {
103
- return await this.handleRuntimeFailure(taskId, leasedTask, finalStatus);
104
- }
105
- this.phase = RunnerPhase.FetchingOutput;
106
- const output = await this.fetchAndParseOutput(runHandle.runId);
107
- // Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
108
- injectRunnerLineageIfAbsent(output, 'taskId', taskId);
109
- this.phase = RunnerPhase.Validating;
110
- const validationResult = await this.validator.validate(output, taskId, sourcePhilosopherArtifactId ?? undefined);
111
- if (!validationResult.valid) {
112
- return await this.handleValidationError({
113
- taskId,
114
- task: leasedTask,
115
- errors: validationResult.errors,
116
- errorCategory: validationResult.errorCategory,
117
- });
118
- }
119
- this.emitScribeEvent('scribe_output_validated', taskId, {
120
- principleTitle: output.principleDraft.title,
121
- });
122
- return await this.succeedTask({
123
- taskId,
124
- runId: storeRunId,
125
- output,
126
- task: leasedTask,
127
- contextHash,
128
- });
129
- }
130
- catch (error) {
131
- return await this.handlePostLeaseError(taskId, leasedTask, error);
132
- }
34
+ // ── Abstract implementations ───────────────────────────────────────────────
35
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
36
+ get permanentErrorCategories() {
37
+ return new Set(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid']);
133
38
  }
134
39
  async buildContext(taskId) {
135
40
  const task = await this.stateManager.getTask(taskId);
@@ -139,8 +44,7 @@ export class ScribeRunner {
139
44
  const piTask = hydratePITaskRecord(task);
140
45
  const deps = piTask?.dependencyTaskIds ?? [];
141
46
  if (deps.length === 0) {
142
- this.emitScribeEvent('scribe_no_dependencies', taskId, {});
143
- return { contextHash: 'empty', philosopherArtifact: null, sourcePhilosopherArtifactId: null };
47
+ throw new PDRuntimeError('input_invalid', 'Philosopher dependency artifact not found');
144
48
  }
145
49
  for (const depId of deps) {
146
50
  const depTask = await this.stateManager.getTask(depId);
@@ -149,7 +53,7 @@ export class ScribeRunner {
149
53
  if (depTask.taskKind !== 'philosopher')
150
54
  continue;
151
55
  if (depTask.status !== 'succeeded') {
152
- this.emitScribeEvent('scribe_dependency_not_succeeded', taskId, {
56
+ this.emitEvent('dependency_not_succeeded', taskId, {
153
57
  depTaskId: depId,
154
58
  depStatus: depTask.status,
155
59
  });
@@ -163,358 +67,172 @@ export class ScribeRunner {
163
67
  continue;
164
68
  const artifactRef = depPiTask?.outputArtifactRefs?.[0]?.ref ?? `pi-artifact://${depId}`;
165
69
  return {
166
- contextHash: ScribeRunner.hashContextRefs([artifactRef]),
70
+ contextHash: BasePeerRunner.hashContextRefs([artifactRef]),
167
71
  philosopherArtifact: firstArtifact.contentJson,
168
72
  sourcePhilosopherArtifactId: firstArtifact.artifactId,
169
73
  };
170
74
  }
171
75
  }
172
- this.emitScribeEvent('scribe_no_philosopher_artifact', taskId, {});
173
- return { contextHash: 'empty', philosopherArtifact: null, sourcePhilosopherArtifactId: null };
76
+ throw new PDRuntimeError('input_invalid', 'Philosopher dependency artifact not found');
174
77
  }
175
- static hashContextRefs(refs) {
176
- if (refs.length === 0)
177
- return 'empty';
178
- const str = refs.join('|');
179
- let hash = 0;
180
- for (let i = 0; i < str.length; i++) {
181
- hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
182
- }
183
- return `ctx-${Math.abs(hash).toString(16)}`;
184
- }
185
- async resolveStoreRunId(taskId) {
186
- const runs = await this.stateManager.getRunsByTask(taskId);
187
- const latestRun = runs[runs.length - 1];
188
- if (!latestRun) {
189
- throw new PDRuntimeError('execution_failed', `No run records found for task ${taskId} after lease acquisition`);
78
+ async invokeRuntime(taskId, context) {
79
+ let parsedPhilosopherArtifact;
80
+ try {
81
+ parsedPhilosopherArtifact = JSON.parse(context.philosopherArtifact);
190
82
  }
191
- return latestRun.runId;
192
- }
193
- async invokeRuntime(params) {
194
- let parsedPhilosopherArtifact = null;
195
- if (params.philosopherArtifact) {
196
- try {
197
- parsedPhilosopherArtifact = JSON.parse(params.philosopherArtifact);
198
- }
199
- catch {
200
- parsedPhilosopherArtifact = params.philosopherArtifact;
201
- }
83
+ catch {
84
+ parsedPhilosopherArtifact = context.philosopherArtifact;
202
85
  }
203
86
  const builder = new ScribePromptBuilder();
204
87
  const { message } = builder.buildPrompt({
205
- taskId: params.taskId,
206
- contextHash: params.contextHash,
88
+ taskId,
89
+ contextHash: context.contextHash,
207
90
  philosopherArtifact: parsedPhilosopherArtifact,
208
- sourcePhilosopherArtifactId: params.sourcePhilosopherArtifactId ?? '',
91
+ sourcePhilosopherArtifactId: context.sourcePhilosopherArtifactId,
209
92
  });
210
- const startInput = {
93
+ return this.runtimeAdapter.startRun({
211
94
  agentSpec: { agentId: this.resolvedOptions.agentId, schemaVersion: 'v1' },
212
- taskRef: { taskId: params.taskId },
95
+ taskRef: { taskId },
213
96
  inputPayload: message,
214
97
  contextItems: [],
215
98
  outputSchemaRef: 'scribe-output-v1',
216
99
  timeoutMs: this.resolvedOptions.timeoutMs,
217
- };
218
- return this.runtimeAdapter.startRun(startInput);
100
+ });
219
101
  }
220
- async pollUntilTerminal(runHandle) {
221
- const deadline = Date.now() + this.resolvedOptions.timeoutMs;
222
- const terminalStatuses = ['succeeded', 'failed', 'timed_out', 'cancelled'];
223
- while (Date.now() < deadline) {
224
- const status = await this.runtimeAdapter.pollRun(runHandle.runId);
225
- if (terminalStatuses.includes(status.status)) {
226
- return status;
227
- }
228
- await this.sleep(this.resolvedOptions.pollIntervalMs);
102
+ async validateOutput(output, taskId, context) {
103
+ const result = await this.validator.validate(output, taskId, context.sourcePhilosopherArtifactId);
104
+ // Trust-boundary: validator is an injected dependency returning `string | undefined`
105
+ // for errorCategory. We must not `as`-cast; validate at runtime (ERR-001, ERR-005).
106
+ const rawCategory = result.errorCategory;
107
+ let errorCategory;
108
+ if (rawCategory == null) {
109
+ errorCategory = undefined;
229
110
  }
230
- let cancelFailed = false;
231
- try {
232
- await this.runtimeAdapter.cancelRun(runHandle.runId);
111
+ else if (isPDErrorCategory(rawCategory)) {
112
+ errorCategory = rawCategory;
233
113
  }
234
- catch (cancelErr) {
235
- cancelFailed = true;
236
- this.emitScribeEvent('scribe_cancel_run_failed', runHandle.runId, {
237
- runId: runHandle.runId,
238
- errorMessage: cancelErr instanceof Error ? cancelErr.message : String(cancelErr),
239
- });
114
+ else {
115
+ // Invalid errorCategory from validator — fail loud, do not pass through
116
+ return {
117
+ valid: false,
118
+ errors: [...result.errors, `invalid errorCategory: ${rawCategory}`],
119
+ errorCategory: 'output_invalid',
120
+ };
240
121
  }
241
- const cancelNote = cancelFailed ? ' (cancelRun also failed)' : '';
242
- throw new PDRuntimeError('timeout', `Run ${runHandle.runId} timed out after ${this.resolvedOptions.timeoutMs}ms${cancelNote}`);
122
+ return {
123
+ valid: result.valid,
124
+ errors: result.errors,
125
+ errorCategory,
126
+ };
243
127
  }
244
- async fetchAndParseOutput(runId) {
245
- const result = await this.runtimeAdapter.fetchOutput(runId);
246
- if (!result || !result.payload) {
247
- throw new PDRuntimeError('output_invalid', `No output available for run ${runId}`);
128
+ // eslint-disable-next-line @typescript-eslint/max-params
129
+ async succeedTask(taskId, runId, output, task, contextHash, context) {
130
+ // Lineage consistency: sourcePhilosopherArtifactId must match buildContext result (ERR-004).
131
+ if (output.sourcePhilosopherArtifactId !== context.sourcePhilosopherArtifactId) {
132
+ throw new PDRuntimeError('output_invalid', `sourcePhilosopherArtifactId mismatch: expected ${context.sourcePhilosopherArtifactId}, got ${output.sourcePhilosopherArtifactId}`);
248
133
  }
249
- return result.payload;
250
- }
251
- async succeedTask(ctx) {
134
+ // Store output before marking succeeded
252
135
  try {
253
- await this.stateManager.updateRunOutput(ctx.runId, JSON.stringify(ctx.output));
136
+ await this.stateManager.updateRunOutput(runId, JSON.stringify(output));
254
137
  }
255
138
  catch (updateErr) {
256
- this.emitScribeEvent('scribe_update_output_failed', ctx.taskId, {
257
- runId: ctx.runId,
139
+ this.emitEvent('update_output_failed', taskId, {
140
+ runId,
258
141
  errorMessage: updateErr instanceof Error ? updateErr.message : String(updateErr),
259
142
  });
260
143
  throw updateErr;
261
144
  }
262
- const artifactId = `pi-art-${ctx.taskId}-${ctx.runId}`;
263
- const now = new Date().toISOString();
145
+ // Resolve lineage artifact IDs
264
146
  let lineageArtifactIds = [];
147
+ let lineageHasRejected = false;
265
148
  try {
266
- const piTask = hydratePITaskRecord(ctx.task);
267
- const deps = piTask?.dependencyTaskIds ?? [];
268
- const results = await Promise.allSettled(deps.map((depId) => this.artifactStore.listBySourceTaskId(depId)));
269
- for (const result of results) {
270
- if (result.status === 'fulfilled') {
271
- for (const artifact of result.value) {
272
- lineageArtifactIds.push(artifact.artifactId);
273
- }
274
- }
275
- }
149
+ const lineageResult = await this.resolveLineageArtifactIds(taskId);
150
+ lineageArtifactIds = lineageResult.ids;
151
+ lineageHasRejected = lineageResult.hasRejected;
152
+ }
153
+ catch (lineageErr) {
154
+ this.emitEvent('lineage_resolve_failed', taskId, {
155
+ runId,
156
+ errorMessage: lineageErr instanceof Error ? lineageErr.message : String(lineageErr),
157
+ });
158
+ }
159
+ if (lineageHasRejected) {
160
+ this.emitEvent('lineage_partial', taskId, {
161
+ runId,
162
+ resolvedCount: lineageArtifactIds.length,
163
+ warning: 'Some dependency artifact queries were rejected; lineage may be incomplete',
164
+ });
276
165
  }
277
- catch { /* lineage resolution failure is non-fatal */ }
166
+ // Write PIArtifact via artifactStore (idempotent upsert)
167
+ const artifactId = `pi-art-${taskId}-${runId}`;
168
+ const now = new Date().toISOString();
278
169
  try {
279
170
  await this.artifactStore.upsertArtifact({
280
171
  artifactId,
281
172
  artifactKind: 'principle',
282
- sourceTaskId: ctx.taskId,
173
+ sourceTaskId: taskId,
283
174
  lineageArtifactIds,
284
175
  validationStatus: 'pending',
285
- contentJson: JSON.stringify(ctx.output),
176
+ contentJson: JSON.stringify(output),
286
177
  createdAt: now,
287
178
  updatedAt: now,
288
179
  });
289
180
  }
290
181
  catch (artifactErr) {
291
- this.emitScribeEvent('scribe_artifact_write_failed', ctx.taskId, {
292
- runId: ctx.runId,
182
+ this.emitEvent('artifact_write_failed', taskId, {
183
+ runId,
293
184
  errorMessage: artifactErr instanceof Error ? artifactErr.message : String(artifactErr),
294
185
  });
295
186
  return this.retryOrFail({
296
- taskId: ctx.taskId,
297
- task: ctx.task,
187
+ taskId,
188
+ task,
298
189
  errorCategory: 'artifact_commit_failed',
299
190
  failureReason: `PIArtifact write failed: ${artifactErr instanceof Error ? artifactErr.message : String(artifactErr)}`,
300
191
  });
301
192
  }
302
- const resultRef = `scribe://${ctx.runId}`;
193
+ // Mark task succeeded
194
+ const resultRef = `${this.config.resultRefPrefix}://${runId}`;
303
195
  try {
304
- await this.stateManager.markTaskSucceeded(ctx.taskId, resultRef);
196
+ await this.stateManager.markTaskSucceeded(taskId, resultRef);
305
197
  }
306
198
  catch (stateErr) {
307
- this.emitScribeEvent('scribe_mark_succeeded_failed', ctx.taskId, {
308
- taskId: ctx.taskId,
309
- runId: ctx.runId,
199
+ this.emitEvent('mark_succeeded_failed', taskId, {
200
+ taskId,
201
+ runId,
310
202
  errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
311
203
  });
312
204
  throw stateErr;
313
205
  }
314
- this.emitScribeEvent('scribe_task_succeeded', ctx.taskId, {
315
- attemptCount: ctx.task.attemptCount,
206
+ this.emitEvent('task_succeeded', taskId, {
207
+ attemptCount: task.attemptCount,
316
208
  resultRef,
317
- principleTitle: ctx.output.principleDraft.title,
209
+ principleTitle: output.principleDraft.title,
318
210
  });
319
- this.phase = RunnerPhase.Completed;
320
211
  return {
321
212
  status: 'succeeded',
322
- taskId: ctx.taskId,
323
- runId: ctx.runId,
213
+ taskId,
214
+ runId,
324
215
  artifactId,
325
216
  resultRef,
326
- contextHash: ctx.contextHash,
327
- output: ctx.output,
328
- attemptCount: ctx.task.attemptCount,
217
+ contextHash,
218
+ output,
219
+ attemptCount: task.attemptCount,
329
220
  };
330
221
  }
331
- async handleRuntimeFailure(taskId, task, runStatus) {
332
- const errorCategory = this.mapRunStatusToErrorCategory(runStatus.status);
333
- this.emitScribeEvent('scribe_run_failed', taskId, {
334
- runStatus: runStatus.status,
335
- errorCategory,
336
- });
337
- return this.retryOrFail({
338
- taskId,
339
- task,
340
- errorCategory,
341
- failureReason: `Runtime execution ended with status: ${runStatus.status}`,
342
- });
343
- }
344
- async handleValidationError(ctx) {
345
- const category = (ctx.errorCategory ?? 'output_invalid');
346
- this.emitScribeEvent('scribe_output_invalid', ctx.taskId, {
347
- errorCount: ctx.errors.length,
348
- errorCategory: category,
349
- });
350
- return this.retryOrFail({
351
- taskId: ctx.taskId,
352
- task: ctx.task,
353
- errorCategory: category,
354
- failureReason: `Validation failed: ${ctx.errors.join('; ')}`,
355
- });
356
- }
357
- async handleLeaseOrPhaseError(taskId, error) {
358
- const classified = this.classifyError(error);
359
- if (classified.category === 'lease_conflict') {
360
- this.emitScribeEvent('scribe_run_failed', taskId, {
361
- errorCategory: 'lease_conflict',
362
- errorMessage: classified.message,
363
- });
364
- return {
365
- status: 'failed',
366
- taskId,
367
- errorCategory: 'lease_conflict',
368
- failureReason: classified.message,
369
- attemptCount: 1,
370
- };
371
- }
372
- this.emitScribeEvent('scribe_run_failed', taskId, {
373
- errorCategory: classified.category,
374
- errorMessage: classified.message,
375
- });
376
- const task = {
377
- taskId,
378
- taskKind: 'scribe',
379
- status: 'leased',
380
- createdAt: new Date().toISOString(),
381
- updatedAt: new Date().toISOString(),
382
- attemptCount: 1,
383
- maxAttempts: this.resolvedOptions.defaultMaxAttempts,
384
- };
385
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
386
- }
387
- async handlePostLeaseError(taskId, task, error) {
388
- const classified = this.classifyError(error);
389
- this.emitScribeEvent('scribe_run_failed', taskId, {
390
- errorCategory: classified.category,
391
- errorMessage: classified.message,
392
- });
393
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
394
- }
395
- async retryOrFail(ctx) {
396
- if (this.isPermanentError(ctx.errorCategory)) {
397
- try {
398
- await this.stateManager.markTaskFailed(ctx.taskId, ctx.errorCategory);
399
- }
400
- catch (stateErr) {
401
- this.emitScribeEvent('scribe_mark_failed_error', ctx.taskId, {
402
- errorCategory: 'storage_unavailable',
403
- attemptCount: ctx.task.attemptCount,
404
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
405
- });
406
- return {
407
- status: 'failed',
408
- taskId: ctx.taskId,
409
- errorCategory: 'storage_unavailable',
410
- failureReason: `State manager error: ${ctx.failureReason}`,
411
- attemptCount: ctx.task.attemptCount,
412
- };
413
- }
414
- this.emitScribeEvent('scribe_task_failed', ctx.taskId, {
415
- errorCategory: ctx.errorCategory,
416
- attemptCount: ctx.task.attemptCount,
417
- failureReason: ctx.failureReason,
418
- });
419
- this.phase = RunnerPhase.Failed;
420
- return {
421
- status: 'failed',
422
- taskId: ctx.taskId,
423
- errorCategory: ctx.errorCategory,
424
- failureReason: ctx.failureReason,
425
- attemptCount: ctx.task.attemptCount,
426
- };
427
- }
428
- const shouldRetry = this.stateManager.getRetryPolicy().shouldRetry(ctx.task);
429
- if (shouldRetry) {
430
- try {
431
- await this.stateManager.markTaskRetryWait(ctx.taskId, ctx.errorCategory);
432
- }
433
- catch (stateErr) {
434
- this.emitScribeEvent('scribe_mark_retry_error', ctx.taskId, {
435
- errorCategory: 'storage_unavailable',
436
- attemptCount: ctx.task.attemptCount,
437
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
438
- });
439
- return {
440
- status: 'failed',
441
- taskId: ctx.taskId,
442
- errorCategory: 'storage_unavailable',
443
- failureReason: `State manager error: ${ctx.failureReason}`,
444
- attemptCount: ctx.task.attemptCount,
445
- };
446
- }
447
- this.emitScribeEvent('scribe_task_retried', ctx.taskId, {
448
- errorCategory: ctx.errorCategory,
449
- attemptCount: ctx.task.attemptCount,
450
- });
451
- this.phase = RunnerPhase.RetryWaiting;
452
- return {
453
- status: 'retried',
454
- taskId: ctx.taskId,
455
- errorCategory: ctx.errorCategory,
456
- failureReason: ctx.failureReason,
457
- attemptCount: ctx.task.attemptCount,
458
- };
459
- }
460
- try {
461
- await this.stateManager.markTaskFailed(ctx.taskId, 'max_attempts_exceeded');
462
- }
463
- catch (stateErr) {
464
- this.emitScribeEvent('scribe_mark_failed_error', ctx.taskId, {
465
- errorCategory: 'storage_unavailable',
466
- attemptCount: ctx.task.attemptCount,
467
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
468
- });
469
- return {
470
- status: 'failed',
471
- taskId: ctx.taskId,
472
- errorCategory: 'storage_unavailable',
473
- failureReason: `State manager error: ${ctx.failureReason}`,
474
- attemptCount: ctx.task.attemptCount,
475
- };
476
- }
477
- this.emitScribeEvent('scribe_task_failed', ctx.taskId, {
478
- errorCategory: 'max_attempts_exceeded',
479
- attemptCount: ctx.task.attemptCount,
480
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
481
- });
482
- this.phase = RunnerPhase.Failed;
483
- return {
484
- status: 'failed',
485
- taskId: ctx.taskId,
486
- errorCategory: 'max_attempts_exceeded',
487
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
488
- attemptCount: ctx.task.attemptCount,
489
- };
490
- }
491
- PERMANENT_ERROR_CATEGORIES = new Set(Object.freeze(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid', 'output_invalid']));
492
- isPermanentError(category) {
493
- return this.PERMANENT_ERROR_CATEGORIES.has(category);
494
- }
222
+ // ── Optional hooks ─────────────────────────────────────────────────────────
223
+ /**
224
+ * Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
225
+ * Only fill when absent via Object.hasOwn — present-but-falsy values
226
+ * must reach validation and fail loud (Runtime Contract Rule 3).
227
+ */
495
228
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
496
- classifyError(error) {
497
- if (error instanceof PDRuntimeError) {
498
- return { category: error.category, message: error.message };
499
- }
500
- if (error instanceof Error) {
501
- return { category: 'execution_failed', message: error.message };
502
- }
503
- return { category: 'execution_failed', message: String(error) };
229
+ postFetchTransform(taskId, untrustedOutput) {
230
+ injectRunnerLineageIfAbsent(untrustedOutput, 'taskId', taskId);
504
231
  }
505
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
506
- mapRunStatusToErrorCategory(status) {
507
- switch (status) {
508
- case 'failed': return 'execution_failed';
509
- case 'timed_out': return 'timeout';
510
- case 'cancelled': return 'cancelled';
511
- default: return 'execution_failed';
512
- }
513
- }
514
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
515
- sleep(ms) {
516
- return new Promise((resolve) => setTimeout(resolve, ms));
232
+ emitSuccessTelemetry(taskId, output) {
233
+ this.emitEvent('principle_draft_generated', taskId, {
234
+ principleTitle: output.principleDraft.title,
235
+ });
517
236
  }
518
237
  }
519
- export { resolveScribeRunnerOptions, DEFAULT_SCRIBE_RUNNER_OPTIONS };
520
238
  //# sourceMappingURL=scribe-runner.js.map