@fitlab-ai/agent-infra 0.11.4 → 0.11.5

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 (111) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/platform/pr-summary.js +4 -23
  15. package/dist/lib/platform/verification-sync.js +12 -49
  16. package/dist/lib/sandbox/control/client.js +4 -1
  17. package/dist/lib/sandbox/control/executor.js +12 -4
  18. package/dist/lib/sandbox/control/protocol.js +2 -1
  19. package/dist/lib/sandbox/removal.js +15 -8
  20. package/dist/lib/task/activity-intent.js +1 -2
  21. package/dist/lib/task/artifact-operations.js +36 -2
  22. package/dist/lib/task/artifact-schema.js +7 -7
  23. package/dist/lib/task/control-authority.js +9 -166
  24. package/dist/lib/task/decision-intents.js +2 -4
  25. package/dist/lib/task/events.js +7 -28
  26. package/dist/lib/task/finalization-handoff.js +92 -0
  27. package/dist/lib/task/finalization.js +123 -12
  28. package/dist/lib/task/intermediate-cleanup.js +9 -148
  29. package/dist/lib/task/ledger-intents.js +3 -6
  30. package/dist/lib/task/lifecycle.js +50 -11
  31. package/dist/lib/task/local-artifact-finalization.js +13 -28
  32. package/dist/lib/task/review-command.js +1 -2
  33. package/dist/lib/task/review-finalization.js +1 -2
  34. package/dist/lib/task/snapshot.js +6 -6
  35. package/dist/lib/task/verification-engine.js +16 -11
  36. package/dist/lib/task/verification.js +1 -13
  37. package/dist/lib/task/workflow-warning-intents.js +2 -4
  38. package/dist/lib/task/write.js +1 -2
  39. package/dist/lifecycle-build-manifest.json +3 -3
  40. package/lib/decide.ts +5 -35
  41. package/lib/git/branch-worktree.ts +41 -0
  42. package/lib/internal/cli-route-inventory.ts +0 -2
  43. package/lib/internal/task-activity.ts +6 -34
  44. package/lib/internal/task-event.ts +4 -37
  45. package/lib/internal/task-finalization.ts +9 -2
  46. package/lib/internal/task-ledger.ts +5 -38
  47. package/lib/internal/task-lifecycle.ts +2 -2
  48. package/lib/internal/task-operation-registry.ts +1 -4
  49. package/lib/internal/task-review.ts +5 -31
  50. package/lib/internal/task-verify.ts +3 -39
  51. package/lib/internal/task-warning.ts +3 -32
  52. package/lib/platform/pr-summary.ts +4 -25
  53. package/lib/platform/verification-sync.ts +13 -53
  54. package/lib/sandbox/control/client.ts +5 -1
  55. package/lib/sandbox/control/executor.ts +13 -4
  56. package/lib/sandbox/control/protocol.ts +3 -2
  57. package/lib/sandbox/removal.ts +23 -7
  58. package/lib/task/activity-intent.ts +1 -2
  59. package/lib/task/artifact-operations.ts +58 -2
  60. package/lib/task/artifact-schema.ts +9 -7
  61. package/lib/task/control-authority.ts +14 -188
  62. package/lib/task/decision-intents.ts +2 -4
  63. package/lib/task/events.ts +8 -27
  64. package/lib/task/finalization-handoff.ts +104 -0
  65. package/lib/task/finalization.ts +120 -12
  66. package/lib/task/intermediate-cleanup.ts +9 -125
  67. package/lib/task/ledger-intents.ts +3 -6
  68. package/lib/task/lifecycle.ts +45 -13
  69. package/lib/task/local-artifact-finalization.ts +16 -35
  70. package/lib/task/review-command.ts +2 -5
  71. package/lib/task/review-finalization.ts +1 -4
  72. package/lib/task/snapshot.ts +6 -6
  73. package/lib/task/verification-engine.ts +20 -10
  74. package/lib/task/verification.ts +1 -15
  75. package/lib/task/workflow-warning-intents.ts +2 -4
  76. package/lib/task/write.ts +1 -4
  77. package/package.json +1 -1
  78. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  79. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  82. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  83. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  84. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  85. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  86. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  87. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  88. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  89. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  90. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  91. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  92. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  93. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  94. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  95. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  96. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  97. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  99. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  100. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  101. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  102. package/dist/lib/internal/task-override.js +0 -138
  103. package/dist/lib/task/artifact-recovery.js +0 -476
  104. package/dist/lib/task/artifact-repair-intent.js +0 -181
  105. package/dist/lib/task/guard-override.js +0 -31
  106. package/dist/lib/task/human-override.js +0 -1259
  107. package/lib/internal/task-override.ts +0 -144
  108. package/lib/task/artifact-recovery.ts +0 -572
  109. package/lib/task/artifact-repair-intent.ts +0 -227
  110. package/lib/task/guard-override.ts +0 -50
  111. package/lib/task/human-override.ts +0 -1522
@@ -1,1522 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createHash, randomUUID } from 'node:crypto';
4
-
5
- import { parseArtifactName } from './artifact-name.ts';
6
- import { locateActivityLog } from './activity-log.ts';
7
- import { applyPrReviewActivityIntent } from './activity-intent.ts';
8
- import { applyHumanDecision } from './decision-intents.ts';
9
- import { applyTaskEvent } from './events.ts';
10
- import { parseTypedTaskFrontmatter } from './frontmatter.ts';
11
- import { applyTaskLifecycle, lifecycleProducerCatalog } from './lifecycle.ts';
12
- import type { TaskLifecycleIntent, TaskLifecycleRequest } from './lifecycle.ts';
13
- import { guardFailureId, guardProducerCatalog } from './guard-override.ts';
14
- import type { GuardProducer, ManualOverrideCapability } from './guard-override.ts';
15
- import { inspectLifecycleExecution } from './lifecycle-execution.ts';
16
- import { applyLedgerIntent } from './ledger-intents.ts';
17
- import { bindPlatformIssue } from '../platform/issues.ts';
18
- import { bindPlatformPullRequest } from '../platform/pull-requests.ts';
19
- import type { PlatformClient } from '../platform/context.ts';
20
- import { finalizeReviewSummary } from './review-finalization.ts';
21
- import { resolveTaskRef } from './resolve-ref.ts';
22
- import { DocumentMutationError, parseTable } from './sections.ts';
23
- import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
24
- import { verifyTaskEvent } from './verification.ts';
25
- import { verifyInProcess } from './verification-engine.ts';
26
- import { canonicalTimestamp, writeTask } from './write.ts';
27
- import type { TaskFileSystem, TaskWriteMetadata, TaskWriteResult } from './write.ts';
28
- import { readPrDeliveryFact } from './pr-delivery-fact.ts';
29
- import { resourceIdentity, resourceIdentityNumber } from '../platform/provider-bridge.ts';
30
- import type { ResourceIdentity } from '../platform/resource-identity.ts';
31
- import { taskIssueIdentity } from '../platform/task-identities.ts';
32
-
33
- type Eligibility = 'eligible' | 'repair-only' | 'runtime-recovery-only' | 'never-overridable';
34
- type OutcomeEffect = 'apply-target' | 'retry-same-intent' | 'record-only' | 'no-write';
35
- type OutcomeResult = 'human-approved' | 'safe-closed' | 'recovery-required' | 'preserve-failure';
36
-
37
- type FailureIdentity = Readonly<{
38
- id: string;
39
- producerId: string;
40
- guardId: string;
41
- code: string;
42
- }>;
43
-
44
- type OutcomeContext = Readonly<{
45
- id: string;
46
- facts: readonly string[];
47
- }>;
48
-
49
- type OutcomeRule = Readonly<{
50
- target: string;
51
- contextId: string;
52
- effect: OutcomeEffect;
53
- result: OutcomeResult;
54
- residual: string;
55
- }>;
56
-
57
- type OutcomeFallback = Readonly<{
58
- effect: 'no-write' | 'record-only';
59
- result: 'recovery-required' | 'preserve-failure';
60
- residual: string;
61
- }>;
62
-
63
- type FailurePolicy = FailureIdentity & Readonly<{
64
- eligibility: Eligibility;
65
- capability: string;
66
- targets: readonly string[];
67
- contexts: readonly OutcomeContext[];
68
- targetContexts: Readonly<Record<string, readonly string[]>>;
69
- outcomes: readonly OutcomeRule[];
70
- fallback: OutcomeFallback;
71
- probeId: string;
72
- }>;
73
-
74
- type Outcome = Readonly<{
75
- effect: OutcomeEffect;
76
- result: OutcomeResult;
77
- residual: string;
78
- policyId: string;
79
- target: string;
80
- contextId: string;
81
- }>;
82
-
83
- type HumanOverrideIdentity = Readonly<{
84
- source: 'local-declared';
85
- operator: string;
86
- verified: false;
87
- }>;
88
-
89
- type HumanOverrideOptions = Readonly<{
90
- repoRoot?: string;
91
- now?: () => string;
92
- randomId?: () => string;
93
- metadataProvider?: () => TaskWriteMetadata;
94
- randomSuffix?: () => string;
95
- taskFileSystem?: Partial<TaskFileSystem>;
96
- probeIntent?: TaskLifecycleIntent;
97
- probeOperator?: string;
98
- probeReason?: string;
99
- probeAlertNumber?: number;
100
- probeIssueNumber?: number;
101
- probePullRequestNumber?: number;
102
- probeStagingDir?: string;
103
- platformClient?: PlatformClient;
104
- effectExecutor?: (capability: ManualOverrideCapability) => OverrideError | null | Promise<OverrideError | null>;
105
- }>;
106
-
107
- type FailureSnapshot = Readonly<{
108
- schema: '1';
109
- taskId: string;
110
- failureId: string;
111
- producerId: string;
112
- code: string;
113
- target: string;
114
- state: string;
115
- digest: string;
116
- observedFacts: readonly string[];
117
- message: string;
118
- capturedAt: string;
119
- source: 'producer-result';
120
- }>;
121
-
122
- type HumanOverrideRequest = Readonly<{
123
- taskRef: string;
124
- failureId: string;
125
- target: string;
126
- operator: string;
127
- reason: string;
128
- scope: string;
129
- expiresAt: string;
130
- intent?: TaskLifecycleIntent;
131
- alertNumber?: number;
132
- issueNumber?: number;
133
- pullRequestNumber?: number;
134
- stagingDir?: string;
135
- failureDigest?: string;
136
- failureSnapshot?: FailureSnapshot;
137
- identitySource?: string;
138
- }>;
139
-
140
- type ConsumeHumanOverrideRequest = Readonly<{
141
- taskRef: string;
142
- ticketId: string;
143
- failureId: string;
144
- target: string;
145
- scope: string;
146
- intent?: TaskLifecycleIntent;
147
- alertNumber?: number;
148
- issueNumber?: number;
149
- pullRequestNumber?: number;
150
- stagingDir?: string;
151
- failureDigest?: string;
152
- observedFacts?: readonly string[];
153
- failureSnapshot?: FailureSnapshot;
154
- }>;
155
-
156
- type OverrideError = Readonly<{ code: string; message: string }>;
157
-
158
- type HumanOverrideIssueResult =
159
- | Readonly<{
160
- status: 'applied' | 'no-op';
161
- changed: boolean;
162
- ticketId: string;
163
- taskId: string;
164
- identity: HumanOverrideIdentity;
165
- error: null;
166
- }>
167
- | Readonly<{
168
- status: 'failed';
169
- changed: false;
170
- ticketId: string | null;
171
- taskId: string | null;
172
- identity: HumanOverrideIdentity | null;
173
- error: OverrideError;
174
- }>;
175
-
176
- type HumanOverrideConsumeResult =
177
- | Readonly<{
178
- status: 'applied' | 'no-op';
179
- changed: boolean;
180
- ticketId: string;
181
- taskId: string;
182
- outcome: Outcome;
183
- manualOverride?: ManualOverrideCapability;
184
- error: null;
185
- }>
186
- | Readonly<{
187
- status: 'failed';
188
- changed: false;
189
- ticketId: string | null;
190
- taskId: string | null;
191
- outcome: Outcome | null;
192
- error: OverrideError;
193
- }>;
194
-
195
- const LIFECYCLE_PRODUCER = 'lifecycle.apply';
196
- const HUMAN_OVERRIDE_SECTION_ALIASES = ['人工豁免账本', 'Human Override Ledger'] as const;
197
- const HUMAN_OVERRIDE_COLUMNS = [
198
- 'id', 'schema', 'failure_id', 'producer_id', 'code', 'target', 'operator',
199
- 'identity_source', 'verified', 'reason', 'scope', 'intent', 'failure_message',
200
- 'failure_digest', 'context_id', 'effect', 'source_state', 'issued_at', 'expires_at', 'status',
201
- 'result', 'consumed_at', 'residual', 'alert_number', 'issue_number', 'pull_request_number', 'staging_dir'
202
- ] as const;
203
- const LEGACY_HUMAN_OVERRIDE_COLUMNS = HUMAN_OVERRIDE_COLUMNS.filter((column) => column !== 'pull_request_number');
204
- const PULL_REQUEST_NUMBER_COLUMN = 'pull_request_number';
205
-
206
- const SHORT_ID_CONFIRMED = 'identity-confirmed-and-safe-close-proven';
207
- const SHORT_ID_UNCONFIRMED = 'identity-unconfirmed';
208
- const REGISTRY_RECOVERABLE = 'registry-recoverable';
209
-
210
- function failureId(producerId: string, code: string): string {
211
- if (!singleLine(producerId) || !singleLine(code) || producerId.includes(':') || code.includes(':')) {
212
- throw new Error('failure identity producer and code must be non-empty and colon-free');
213
- }
214
- return `${producerId}:${code}`;
215
- }
216
-
217
- function identity(producerId: string, code: string, guardId = code): FailureIdentity {
218
- return { id: failureId(producerId, code), producerId, guardId, code };
219
- }
220
-
221
- function context(id: string, ...facts: string[]): OutcomeContext {
222
- return { id, facts };
223
- }
224
-
225
- function makeSimplePolicy(code: string, eligibility: Eligibility, target: string, result: OutcomeResult): FailurePolicy {
226
- const id = failureId(LIFECYCLE_PRODUCER, code);
227
- const contextId = 'identity-confirmed';
228
- const effect: OutcomeEffect = result === 'preserve-failure' ? 'record-only' : 'apply-target';
229
- return {
230
- ...identity(LIFECYCLE_PRODUCER, code, 'G-03'),
231
- eligibility,
232
- capability: eligibility === 'never-overridable' ? 'audit-only' : 'local-task-repair',
233
- targets: [target],
234
- contexts: [context(contextId, contextId)],
235
- targetContexts: { [target]: [contextId] },
236
- outcomes: [{
237
- target,
238
- contextId,
239
- effect,
240
- result,
241
- residual: result === 'preserve-failure' ? 'original failure remains authoritative' : 'local operator action was applied to the task'
242
- }],
243
- fallback: {
244
- effect: 'no-write',
245
- result: result === 'preserve-failure' ? 'preserve-failure' : 'recovery-required',
246
- residual: 'outcome context was not confirmed; no target state was written'
247
- },
248
- probeId: `probe.${code.toLowerCase()}`
249
- };
250
- }
251
-
252
- function simplePolicy(code: string): FailurePolicy {
253
- if (code === 'LIFECYCLE_SOURCE_INVALID') return makeSimplePolicy(code, 'eligible', 'continue-local', 'human-approved');
254
- if (code === 'LIFECYCLE_LOG_MISSING' || code === 'LIFECYCLE_METADATA_FAILED' || code === 'LIFECYCLE_DOCUMENT_INVALID') {
255
- return makeSimplePolicy(code, 'repair-only', 'repair-task', 'recovery-required');
256
- }
257
- if (code === 'LIFECYCLE_IDENTITY_CONFLICT' || code === 'LIFECYCLE_IDENTITY_INVALID' || code === 'LIFECYCLE_PAYLOAD_INVALID' || code === 'LIFECYCLE_STAGING_IDENTITY_INVALID' || code === 'LIFECYCLE_STAGING_INVALID') {
258
- return makeSimplePolicy(code, 'never-overridable', 'record-only', 'preserve-failure');
259
- }
260
- return makeSimplePolicy(code, 'runtime-recovery-only', 'retry-same-intent', 'recovery-required');
261
- }
262
-
263
- function genericGuardPolicy(producer: GuardProducer): FailurePolicy {
264
- const preserve = producer.guardId === 'G-01' || producer.guardId === 'G-09' || producer.guardId === 'G-10';
265
- const contextId = 'guard-observed';
266
- const result: OutcomeResult = preserve ? 'preserve-failure' : 'human-approved';
267
- const effect: OutcomeEffect = preserve ? 'record-only' : 'apply-target';
268
- return {
269
- ...identity(producer.producerId, producer.code, producer.guardId),
270
- eligibility: preserve ? 'never-overridable' : 'eligible',
271
- capability: preserve ? 'audit-only' : `local-${producer.guardId.toLowerCase()}-gate`,
272
- targets: ['continue-local'],
273
- contexts: [context(contextId, contextId)],
274
- targetContexts: { 'continue-local': [contextId] },
275
- outcomes: [{
276
- target: 'continue-local',
277
- contextId,
278
- effect,
279
- result,
280
- residual: preserve ? 'external or identity evidence remains authoritative' : `${producer.guardId} local guard was explicitly overridden`
281
- }],
282
- fallback: {
283
- effect: 'record-only',
284
- result: 'preserve-failure',
285
- residual: 'guard facts were not confirmed; the original failure remains authoritative'
286
- },
287
- probeId: `probe.${producer.producerId}.${producer.code.toLowerCase()}`
288
- };
289
- }
290
-
291
- const shortIdPolicy: FailurePolicy = {
292
- ...identity(LIFECYCLE_PRODUCER, 'SHORT_ID_CAPACITY_EXCEEDED', 'G-03'),
293
- eligibility: 'repair-only',
294
- capability: 'safe-close-or-registry-recovery',
295
- targets: ['safe-close', 'recover-registry'],
296
- contexts: [
297
- context(SHORT_ID_CONFIRMED, SHORT_ID_CONFIRMED),
298
- context(SHORT_ID_UNCONFIRMED, SHORT_ID_UNCONFIRMED),
299
- context(REGISTRY_RECOVERABLE, REGISTRY_RECOVERABLE)
300
- ],
301
- targetContexts: {
302
- 'safe-close': [SHORT_ID_CONFIRMED, SHORT_ID_UNCONFIRMED],
303
- 'recover-registry': [REGISTRY_RECOVERABLE]
304
- },
305
- outcomes: [
306
- {
307
- target: 'safe-close',
308
- contextId: SHORT_ID_CONFIRMED,
309
- effect: 'apply-target',
310
- result: 'safe-closed',
311
- residual: 'task was atomically safe-closed without entering active or expanding short-id capacity'
312
- },
313
- {
314
- target: 'safe-close',
315
- contextId: SHORT_ID_UNCONFIRMED,
316
- effect: 'no-write',
317
- result: 'recovery-required',
318
- residual: 'task identity could not be confirmed; no terminal state was written'
319
- },
320
- {
321
- target: 'recover-registry',
322
- contextId: REGISTRY_RECOVERABLE,
323
- effect: 'retry-same-intent',
324
- result: 'recovery-required',
325
- residual: 'registry recovery must complete before a new diagnosis and ticket are issued'
326
- }
327
- ],
328
- fallback: {
329
- effect: 'no-write',
330
- result: 'recovery-required',
331
- residual: 'short-id failure facts did not match a registered safe outcome context; original failure remains authoritative'
332
- },
333
- probeId: 'probe.short-id-capacity'
334
- };
335
-
336
- const HUMAN_OVERRIDE_FAILURE_REGISTRY: readonly FailurePolicy[] = [
337
- shortIdPolicy,
338
- ...lifecycleProducerCatalog
339
- .filter(({ code }) => code !== 'SHORT_ID_CAPACITY_EXCEEDED')
340
- .map(({ code }) => simplePolicy(code)),
341
- ...guardProducerCatalog.map((producer) => genericGuardPolicy(producer))
342
- ];
343
-
344
- const stableFailureCatalog = [
345
- ...lifecycleProducerCatalog,
346
- ...guardProducerCatalog
347
- ].map(({ producerId, guardId, code }) => ({
348
- id: guardFailureId(producerId, code),
349
- producerId,
350
- guardId,
351
- code
352
- }));
353
-
354
- function singleLine(value: unknown): value is string {
355
- return typeof value === 'string' && value.trim().length > 0 && !/[\r\n]/.test(value);
356
- }
357
-
358
- function validatePolicy(policy: FailurePolicy): void {
359
- if (!singleLine(policy.id) || !singleLine(policy.producerId) || !singleLine(policy.guardId) || !singleLine(policy.code)) {
360
- throw new Error('failure policy identity is invalid');
361
- }
362
- if (policy.id !== failureId(policy.producerId, policy.code)) throw new Error(`policy ${policy.id} has an invalid identity`);
363
- if (!Array.isArray(policy.targets) || new Set(policy.targets).size !== policy.targets.length || policy.targets.length === 0) {
364
- throw new Error(`policy ${policy.id} targets are invalid`);
365
- }
366
- if (!Array.isArray(policy.contexts) || policy.contexts.length === 0) throw new Error(`policy ${policy.id} contexts are missing`);
367
- const contexts = new Map<string, OutcomeContext>();
368
- for (const item of policy.contexts) {
369
- if (!singleLine(item.id) || contexts.has(item.id) || item.facts.length === 0 || new Set(item.facts).size !== item.facts.length) {
370
- throw new Error(`policy ${policy.id} contains invalid or duplicate contexts`);
371
- }
372
- contexts.set(item.id, item);
373
- }
374
- const contextFactsByTarget = new Map<string, Set<string>>();
375
- for (const target of policy.targets) {
376
- const facts = contextFactsByTarget.get(target) ?? new Set<string>();
377
- for (const contextId of policy.targetContexts[target] ?? []) {
378
- const item = contexts.get(contextId);
379
- if (!item) continue;
380
- const key = item.facts.slice().sort().join('\0');
381
- if (facts.has(key)) throw new Error(`policy ${policy.id} has overlapping context facts for target ${target}`);
382
- facts.add(key);
383
- }
384
- contextFactsByTarget.set(target, facts);
385
- }
386
- const outcomes = new Set<string>();
387
- for (const target of policy.targets) {
388
- const contextIds = policy.targetContexts[target];
389
- if (!contextIds || contextIds.length === 0) throw new Error(`policy ${policy.id} target ${target} has no contexts`);
390
- for (const contextId of contextIds) {
391
- if (!contexts.has(contextId)) throw new Error(`policy ${policy.id} references unknown context ${contextId}`);
392
- const key = `${target}\0${contextId}`;
393
- if (outcomes.has(key)) throw new Error(`policy ${policy.id} has overlapping outcome ${key}`);
394
- outcomes.add(key);
395
- }
396
- }
397
- if (!Array.isArray(policy.outcomes) || policy.outcomes.length !== outcomes.size) {
398
- throw new Error(`policy ${policy.id} outcome table does not cover every target/context`);
399
- }
400
- for (const rule of policy.outcomes) {
401
- const key = `${rule.target}\0${rule.contextId}`;
402
- if (!outcomes.has(key) || outcomes.delete(key) === false || !singleLine(rule.residual)) {
403
- throw new Error(`policy ${policy.id} contains an invalid or duplicate outcome`);
404
- }
405
- if (rule.result === 'human-approved' && (policy.eligibility !== 'eligible' || rule.effect !== 'apply-target')) {
406
- throw new Error(`policy ${policy.id} grants human-approved outside an eligible apply-target rule`);
407
- }
408
- if (rule.result === 'safe-closed' && rule.effect !== 'apply-target') {
409
- throw new Error(`policy ${policy.id} grants safe-closed without applying a target`);
410
- }
411
- if (rule.effect === 'record-only' && rule.result === 'human-approved') {
412
- throw new Error(`policy ${policy.id} treats record-only as human-approved`);
413
- }
414
- }
415
- if (outcomes.size !== 0) throw new Error(`policy ${policy.id} has missing outcomes`);
416
- if (!['no-write', 'record-only'].includes(policy.fallback.effect) || !['recovery-required', 'preserve-failure'].includes(policy.fallback.result) || !singleLine(policy.fallback.residual)) {
417
- throw new Error(`policy ${policy.id} fallback is invalid`);
418
- }
419
- }
420
-
421
- function validateRegistry(registry: readonly FailurePolicy[] = HUMAN_OVERRIDE_FAILURE_REGISTRY): void {
422
- const ids = new Set<string>();
423
- for (const policy of registry) {
424
- validatePolicy(policy);
425
- if (ids.has(policy.id)) throw new Error(`duplicate failure policy ${policy.id}`);
426
- ids.add(policy.id);
427
- }
428
- const catalogIds = new Set(stableFailureCatalog.map((item) => item.id));
429
- if (ids.size !== catalogIds.size || [...catalogIds].some((id) => !ids.has(id))) {
430
- throw new Error('human override registry does not match the lifecycle producer catalog');
431
- }
432
- for (const item of stableFailureCatalog) {
433
- const policy = registry.find((candidate) => candidate.id === item.id);
434
- if (!policy || policy.producerId !== item.producerId || policy.guardId !== item.guardId || policy.code !== item.code) {
435
- throw new Error(`human override policy ${item.id} does not match its producer catalog entry`);
436
- }
437
- }
438
- }
439
-
440
- validateRegistry();
441
-
442
- function policyFor(value: string | FailurePolicy): FailurePolicy | null {
443
- if (typeof value !== 'string') return value;
444
- return HUMAN_OVERRIDE_FAILURE_REGISTRY.find((policy) => policy.id === value) ?? null;
445
- }
446
-
447
- function resolveOutcome(policyOrId: string | FailurePolicy, requestedTarget: string, observedFacts: readonly string[]): Outcome {
448
- const policy = policyFor(policyOrId);
449
- if (!policy || !singleLine(requestedTarget) || !Array.isArray(observedFacts) || observedFacts.some((fact) => !singleLine(fact))) {
450
- return {
451
- effect: 'no-write',
452
- result: 'recovery-required',
453
- residual: 'failure policy or observed facts were not registered; no target state was written',
454
- policyId: typeof policyOrId === 'string' ? policyOrId : policyOrId.id,
455
- target: requestedTarget,
456
- contextId: 'context-unmatched'
457
- };
458
- }
459
- const allowedContexts = policy.targetContexts[requestedTarget] ?? [];
460
- const matches = policy.contexts.filter((item) => allowedContexts.includes(item.id)
461
- && item.facts.length === observedFacts.length
462
- && item.facts.every((fact) => observedFacts.includes(fact))
463
- && observedFacts.every((fact) => item.facts.includes(fact)));
464
- if (matches.length !== 1) {
465
- return { ...policy.fallback, policyId: policy.id, target: requestedTarget, contextId: 'context-unmatched' };
466
- }
467
- const rule = policy.outcomes.find((item) => item.target === requestedTarget && item.contextId === matches[0]!.id);
468
- if (!rule) return { ...policy.fallback, policyId: policy.id, target: requestedTarget, contextId: 'context-unmatched' };
469
- return { ...rule, policyId: policy.id };
470
- }
471
-
472
- function error(code: string, message: string): OverrideError {
473
- return { code, message };
474
- }
475
-
476
- type ParsedHumanOverrideLedger = Readonly<{
477
- table: NonNullable<ReturnType<typeof parseTable>>;
478
- columns: readonly string[];
479
- }>;
480
-
481
- function parseHumanOverrideLedger(content: string): ParsedHumanOverrideLedger | null {
482
- try {
483
- const table = parseTable(content, {
484
- sectionAliases: HUMAN_OVERRIDE_SECTION_ALIASES,
485
- columns: HUMAN_OVERRIDE_COLUMNS,
486
- keyColumn: 'id'
487
- });
488
- return table ? { table, columns: HUMAN_OVERRIDE_COLUMNS } : null;
489
- } catch (cause) {
490
- if (!(cause instanceof DocumentMutationError) || cause.code !== 'TABLE_NOT_FOUND') throw cause;
491
- const table = parseTable(content, {
492
- sectionAliases: HUMAN_OVERRIDE_SECTION_ALIASES,
493
- columns: LEGACY_HUMAN_OVERRIDE_COLUMNS,
494
- keyColumn: 'id'
495
- });
496
- return table ? { table, columns: LEGACY_HUMAN_OVERRIDE_COLUMNS } : null;
497
- }
498
- }
499
-
500
- function escapeTableCell(value: string): string {
501
- return value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
502
- }
503
-
504
- function migrateHumanOverrideLedger(ledger: ParsedHumanOverrideLedger) {
505
- if (ledger.columns.includes(PULL_REQUEST_NUMBER_COLUMN)) return null;
506
- const header = `| ${HUMAN_OVERRIDE_COLUMNS.join(' | ')} |`;
507
- const separator = `| ${HUMAN_OVERRIDE_COLUMNS.map(() => '---').join(' | ')} |`;
508
- const rows = ledger.table.rows.map(({ values }) => `| ${HUMAN_OVERRIDE_COLUMNS
509
- .map((column) => escapeTableCell(values[column] ?? ''))
510
- .join(' | ')} |`);
511
- return {
512
- kind: 'section' as const,
513
- aliases: HUMAN_OVERRIDE_SECTION_ALIASES,
514
- heading: ledger.table.heading,
515
- body: [header, separator, ...rows].join('\n')
516
- };
517
- }
518
-
519
- function validatePullRequestBinding(
520
- policy: FailurePolicy,
521
- row: Readonly<Record<string, string>>,
522
- request: ConsumeHumanOverrideRequest
523
- ): OverrideError | null {
524
- if (policy.producerId !== 'platform.pull-request') return null;
525
- const bound = toPositiveInteger(row[PULL_REQUEST_NUMBER_COLUMN]);
526
- if (bound === null) {
527
- return error('OVERRIDE_RESOURCE_BINDING_MISSING', 'override ticket has no bound pull request number; issue a new ticket');
528
- }
529
- if (request.pullRequestNumber !== undefined && request.pullRequestNumber !== bound) {
530
- return error('OVERRIDE_RESOURCE_MISMATCH', `override ticket is bound to pull request #${bound}`);
531
- }
532
- return null;
533
- }
534
-
535
- function overrideDryRunConflict(values: Readonly<Record<string, unknown>>): OverrideError | null {
536
- const hasOverride = ['overrideTicket', 'overrideTarget', 'overrideScope'].some((key) => values[key] !== undefined && values[key] !== '');
537
- return values.dryRun && hasOverride
538
- ? error('OVERRIDE_DRY_RUN_CONFLICT', '--dry-run cannot be combined with --override-*')
539
- : null;
540
- }
541
-
542
- function resolveTask(taskRef: string, options: HumanOverrideOptions) {
543
- return resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
544
- }
545
-
546
- function failedIssue(code: string, message: string, taskId: string | null = null): HumanOverrideIssueResult {
547
- return { status: 'failed', changed: false, ticketId: null, taskId, identity: null, error: error(code, message) };
548
- }
549
-
550
- function failedConsume(code: string, message: string, ticketId: string | null = null, taskId: string | null = null): HumanOverrideConsumeResult {
551
- return { status: 'failed', changed: false, ticketId, taskId, outcome: null, error: error(code, message) };
552
- }
553
-
554
- function readTask(taskMdPath: string): { content: string; frontmatter: ReturnType<typeof parseTypedTaskFrontmatter> } | OverrideError {
555
- try {
556
- const content = fs.readFileSync(taskMdPath, 'utf8');
557
- const frontmatter = parseTypedTaskFrontmatter(content);
558
- return { content, frontmatter };
559
- } catch (cause) {
560
- return error('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause));
561
- }
562
- }
563
-
564
- function writeOptions(options: HumanOverrideOptions) {
565
- return {
566
- repoRoot: options.repoRoot,
567
- metadataProvider: options.metadataProvider,
568
- randomSuffix: options.randomSuffix,
569
- fileSystem: options.taskFileSystem
570
- };
571
- }
572
-
573
- function taskWriteError(result: TaskWriteResult): OverrideError | null {
574
- return result.status === 'failed' ? error(result.error.code, result.error.message) : null;
575
- }
576
-
577
- type ProducerFailure = Readonly<{
578
- code: string;
579
- message: string;
580
- observedFacts: readonly string[];
581
- state: string;
582
- }>;
583
-
584
- function snapshotEvidence(
585
- taskId: string,
586
- failureIdValue: string,
587
- target: string,
588
- content: string,
589
- frontmatter: Record<string, unknown> | null,
590
- producer: ProducerFailure,
591
- resource: Readonly<{ kind: 'issue' | 'pull-request'; identity: ResourceIdentity; number: number | null }> | null
592
- ): string {
593
- const stableFrontmatter = frontmatter
594
- ? Object.fromEntries(Object.entries(frontmatter).filter(([key]) => !['updated_at', 'agent_infra_version', 'status', 'blocked_at', 'completed_at', 'cancelled_at'].includes(key)))
595
- : { parse: 'invalid' };
596
- return JSON.stringify({
597
- taskId,
598
- failureId: failureIdValue,
599
- target,
600
- producer: {
601
- code: producer.code,
602
- message: producer.message,
603
- observedFacts: producer.observedFacts,
604
- state: producer.state
605
- },
606
- resource,
607
- stableFrontmatter,
608
- activityLogPresent: locateActivityLog(content) !== null
609
- });
610
- }
611
-
612
- function resourceForSnapshot(
613
- policy: FailurePolicy,
614
- options: HumanOverrideOptions,
615
- frontmatter: Record<string, unknown> | null
616
- ): Readonly<{ kind: 'issue' | 'pull-request'; identity: ResourceIdentity; number: number | null }> | null {
617
- if (policy.producerId === 'platform.issue') {
618
- const identity = options.probeIssueNumber !== undefined
619
- ? resourceIdentity(options.probeIssueNumber)
620
- : taskIssueIdentity((frontmatter || {}) as Record<string, string | number | boolean | null>);
621
- return identity ? { kind: 'issue', identity, number: resourceIdentityNumber(identity) } : null;
622
- }
623
- if (policy.producerId === 'platform.pull-request') {
624
- const fact = frontmatter ? readPrDeliveryFact(frontmatter) : null;
625
- const identity = options.probePullRequestNumber !== undefined
626
- ? resourceIdentity(options.probePullRequestNumber)
627
- : fact?.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
628
- return identity ? { kind: 'pull-request', identity, number: resourceIdentityNumber(identity) } : null;
629
- }
630
- return null;
631
- }
632
-
633
- function probeLifecycleFailure(
634
- taskRef: string,
635
- policy: FailurePolicy,
636
- resolved: { taskId: string; state: string },
637
- options: HumanOverrideOptions
638
- ): ProducerFailure | OverrideError {
639
- if (!options.probeIntent) return error('OVERRIDE_PROBE_INPUT_MISSING', 'an original lifecycle intent is required to prove the producer failure');
640
- const request = lifecycleRequestFor(
641
- taskRef,
642
- options.probeIntent,
643
- options.probeOperator ?? 'local-operator',
644
- options.probeReason ?? 'human override producer probe',
645
- {
646
- alert_number: options.probeAlertNumber === undefined ? undefined : String(options.probeAlertNumber),
647
- issue_number: options.probeIssueNumber === undefined ? undefined : String(options.probeIssueNumber),
648
- staging_dir: options.probeStagingDir
649
- }
650
- );
651
- if ('code' in request) return request;
652
- const result = applyTaskLifecycle({ ...request, dryRun: true }, {
653
- repoRoot: options.repoRoot,
654
- metadataProvider: options.metadataProvider
655
- });
656
- if (result.status !== 'failed' || result.error?.code !== policy.code) {
657
- return error(
658
- 'OVERRIDE_FAILURE_NOT_PRESENT',
659
- `producer probe returned ${result.error?.code ?? 'no failure'}; expected ${policy.code}`
660
- );
661
- }
662
- const identityConfirmed = result.taskId === resolved.taskId;
663
- const observedFacts = policy.code === 'SHORT_ID_CAPACITY_EXCEEDED'
664
- ? identityConfirmed && ['active', 'blocked'].includes(resolved.state)
665
- ? [SHORT_ID_CONFIRMED]
666
- : [SHORT_ID_UNCONFIRMED]
667
- : identityConfirmed ? ['identity-confirmed'] : [];
668
- return {
669
- code: result.error.code,
670
- message: result.error.message,
671
- observedFacts,
672
- state: result.sourceState ?? resolved.state
673
- };
674
- }
675
-
676
- type ProbeTask = Readonly<{
677
- taskId: string;
678
- state: string;
679
- taskDir: string;
680
- repoRoot: string;
681
- }>;
682
-
683
- function probeResult(
684
- policy: FailurePolicy,
685
- result: unknown,
686
- state: string
687
- ): ProducerFailure | null {
688
- if (!result || typeof result !== 'object') return null;
689
- const errorValue = (result as { error?: unknown }).error;
690
- if (!errorValue || typeof errorValue !== 'object') return null;
691
- const code = String((errorValue as { code?: unknown }).code ?? '');
692
- if (code !== policy.code) return null;
693
- return {
694
- code,
695
- message: String((errorValue as { message?: unknown }).message ?? code),
696
- observedFacts: ['guard-observed'],
697
- state
698
- };
699
- }
700
-
701
- function probeNotObserved(policy: FailurePolicy): OverrideError {
702
- return error('OVERRIDE_FAILURE_NOT_PRESENT', `producer ${policy.producerId} did not produce ${policy.code}`);
703
- }
704
-
705
- async function probePlatformFailure(
706
- taskRef: string,
707
- policy: FailurePolicy,
708
- resolved: ProbeTask,
709
- options: HumanOverrideOptions
710
- ): Promise<ProducerFailure | OverrideError> {
711
- let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
712
- try {
713
- frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(path.join(resolved.taskDir, 'task.md'), 'utf8'));
714
- } catch (cause) {
715
- return error('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause));
716
- }
717
- const issueIdentity = options.probeIssueNumber !== undefined
718
- ? resourceIdentity(options.probeIssueNumber)
719
- : taskIssueIdentity(frontmatter);
720
- const fact = readPrDeliveryFact(frontmatter);
721
- if (fact.status === 'invalid') {
722
- return error(fact.error.code, fact.error.message);
723
- }
724
- const pullRequestIdentity = options.probePullRequestNumber !== undefined
725
- ? resourceIdentity(options.probePullRequestNumber)
726
- : fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
727
- const token = (identity: ResourceIdentity | null): string | null => identity ? String(identity.value) : null;
728
- const issueToken = token(issueIdentity);
729
- const pullRequestToken = token(pullRequestIdentity);
730
- const operation = await (policy.producerId === 'platform.issue'
731
- ? issueToken === null
732
- ? null
733
- : bindPlatformIssue(taskRef, {
734
- issue: issueToken,
735
- agent: options.probeOperator ?? 'local-operator',
736
- dryRun: true,
737
- cwd: resolved.repoRoot,
738
- ...(options.platformClient ? { client: options.platformClient } : {})
739
- })
740
- : pullRequestToken === null
741
- ? null
742
- : bindPlatformPullRequest(taskRef, {
743
- pr: pullRequestToken,
744
- agent: options.probeOperator ?? 'local-operator',
745
- dryRun: true,
746
- cwd: resolved.repoRoot,
747
- ...(options.platformClient ? { client: options.platformClient } : {})
748
- })
749
- );
750
- if (!operation) return error('OVERRIDE_PROBE_INPUT_MISSING', `${policy.producerId} requires a bound issue or pull request identity`);
751
- if (operation.status !== 'failed' && operation.status !== 'blocked') return probeNotObserved(policy);
752
- if (!operation.error) return error('OVERRIDE_PROBE_INPUT_MISSING', `${policy.producerId} returned no platform error evidence`);
753
- return {
754
- code: policy.code,
755
- message: `${operation.error.code}: ${operation.error.message}`,
756
- observedFacts: ['guard-observed'],
757
- state: resolved.state
758
- };
759
- }
760
-
761
- function toPositiveInteger(value: unknown): number | null {
762
- const number = typeof value === 'number' ? value : Number(value);
763
- return Number.isSafeInteger(number) && number > 0 ? number : null;
764
- }
765
-
766
- function probeTaskEventFailure(taskRef: string, policy: FailurePolicy, resolved: ProbeTask): ProducerFailure | OverrideError {
767
- const starts = [
768
- 'analyze.started', 'review-analysis.started', 'plan.started', 'review-plan.started',
769
- 'code.started', 'review-code.started', 'manual-validation.started', 'validation-run.started'
770
- ];
771
- const completions = [
772
- ['analyze.completed', 'analysis.md'],
773
- ['review-analysis.completed', 'review-analysis.md'],
774
- ['plan.completed', 'plan.md'],
775
- ['review-plan.completed', 'review-plan.md'],
776
- ['code.completed', 'code.md'],
777
- ['review-code.completed', 'review-code.md']
778
- ] as const;
779
- const candidates: Array<{ event: string; artifact?: string }> = policy.code === 'EVENT_START_MISSING'
780
- ? completions.map(([event, artifact]) => ({ event, artifact }))
781
- : starts.map((event) => ({ event }));
782
- for (const candidate of candidates) {
783
- const result = applyTaskEvent({
784
- taskRef,
785
- event: candidate.event,
786
- agent: 'codex',
787
- dryRun: true,
788
- initiator: 'model',
789
- requestId: `override-probe:${taskRef}:${candidate.event}`,
790
- reasonCode: 'user-request',
791
- ...(candidate.artifact ? { artifact: candidate.artifact } : {})
792
- }, { repoRoot: resolved.repoRoot, lockAlreadyHeld: true });
793
- const matched = probeResult(policy, result, resolved.state);
794
- if (matched) return matched;
795
- }
796
- return probeNotObserved(policy);
797
- }
798
-
799
- async function probeVerificationFailure(taskRef: string, policy: FailurePolicy, resolved: ProbeTask): Promise<ProducerFailure | OverrideError> {
800
- const result = await verifyTaskEvent(
801
- {
802
- taskRef,
803
- event: 'review-code.completed',
804
- artifact: policy.code === 'VERIFY_ARTIFACT_INVALID' ? 'invalid-artifact.md' : 'review-code.md'
805
- },
806
- { repoRoot: resolved.repoRoot }
807
- );
808
- const matched = probeResult(policy, result, resolved.state);
809
- return matched ?? probeNotObserved(policy);
810
- }
811
-
812
- function latestArtifact(taskDir: string, family: string): string | undefined {
813
- let names: string[];
814
- try { names = fs.readdirSync(taskDir); } catch { return undefined; }
815
- return names.map(parseArtifactName).filter((identity) => identity !== null)
816
- .filter((identity) => identity.family === family).sort((left, right) => right.round - left.round)[0]?.name;
817
- }
818
-
819
- async function probeVerificationEngineFailure(policy: FailurePolicy, resolved: ProbeTask): Promise<ProducerFailure | OverrideError> {
820
- const candidates = [
821
- { skillName: 'review-code', family: 'review-code' },
822
- { skillName: 'review-plan', family: 'review-plan' },
823
- { skillName: 'review-analysis', family: 'review-analysis' },
824
- { skillName: 'code-task', family: 'code' },
825
- { skillName: 'plan-task', family: 'plan' },
826
- { skillName: 'analyze-task', family: 'analysis' },
827
- { skillName: 'complete-task', family: null }
828
- ];
829
- for (const candidate of candidates) {
830
- const artifactFile = candidate.family ? latestArtifact(resolved.taskDir, candidate.family) : undefined;
831
- if (candidate.family && !artifactFile) continue;
832
- try {
833
- const payload = await verifyInProcess({
834
- mode: 'gate',
835
- skillName: candidate.skillName,
836
- taskDir: resolved.taskDir,
837
- artifactFile,
838
- checks: [],
839
- repositoryRoot: resolved.repoRoot
840
- }) as { gate?: unknown };
841
- const code = payload.gate === 'blocked' ? 'CHECK_BLOCKED' : payload.gate === 'fail' ? 'CHECK_FAILED' : '';
842
- if (code === policy.code) {
843
- return {
844
- code,
845
- message: `${candidate.skillName} verification gate returned ${payload.gate}`,
846
- observedFacts: ['guard-observed'],
847
- state: resolved.state
848
- };
849
- }
850
- } catch {
851
- // A probe failure is not evidence that the requested verification failure occurred.
852
- }
853
- }
854
- return probeNotObserved(policy);
855
- }
856
-
857
- async function probeGenericFailure(
858
- taskRef: string,
859
- policy: FailurePolicy,
860
- resolved: ProbeTask,
861
- options: HumanOverrideOptions = {}
862
- ): Promise<ProducerFailure | OverrideError> {
863
- switch (policy.producerId) {
864
- case 'task.resolve':
865
- return probeNotObserved(policy);
866
- case 'task.write':
867
- return probeResult(policy, writeTask({ taskRef, expectedState: 'active', mutations: [], dryRun: true }, { repoRoot: resolved.repoRoot }), resolved.state) ?? probeNotObserved(policy);
868
- case 'task-event':
869
- return probeTaskEventFailure(taskRef, policy, resolved);
870
- case 'task-verify':
871
- return await probeVerificationFailure(taskRef, policy, resolved);
872
- case 'verification-engine':
873
- return await probeVerificationEngineFailure(policy, resolved);
874
- case 'activity-intent':
875
- return probeResult(policy, applyPrReviewActivityIntent({
876
- kind: 'pr-review-start', taskRef, agent: 'codex', artifact: 'pr-review.md', head: '0'.repeat(40), dryRun: true
877
- }, { repoRoot: resolved.repoRoot, lockAlreadyHeld: true }), resolved.state) ?? probeNotObserved(policy);
878
- case 'ledger-intent':
879
- return probeResult(policy, applyLedgerIntent({
880
- kind: 'finding-respond', taskRef, id: 'CD-1', round: 1, status: 'accepted', evidence: 'review-code.md#CD-1', dryRun: true
881
- }), resolved.state) ?? probeNotObserved(policy);
882
- case 'decision-intent':
883
- return probeResult(policy, applyHumanDecision({ taskRef, selector: 'CD-1', decision: 'probe', dryRun: true }), resolved.state) ?? probeNotObserved(policy);
884
- case 'workflow-warning':
885
- return probeResult(policy, applyWorkflowWarningIntent({
886
- kind: 'add', taskRef, step: 'probe', severity: 'IMPORTANT', code: 'PROBE', target: 'probe', message: 'probe', action: 'probe', dryRun: true
887
- }), resolved.state) ?? probeNotObserved(policy);
888
- case 'review-finalization':
889
- return probeResult(policy, finalizeReviewSummary({
890
- taskRef, stage: 'code', artifact: 'review-code.md', dryRun: true
891
- }, { repoRoot: resolved.repoRoot, lockAlreadyHeld: true }), resolved.state) ?? probeNotObserved(policy);
892
- case 'lifecycle-execution': {
893
- const identities = {
894
- stage: 'code' as const, round: 1, artifact: 'code.md', role: 'executor' as const
895
- };
896
- const results = [
897
- inspectLifecycleExecution(taskRef, { mode: 'standalone', identity: identities, dryRun: true }, { repoRoot: resolved.repoRoot }),
898
- inspectLifecycleExecution(taskRef, { mode: 'orchestrated', identity: identities, dryRun: true }, { repoRoot: resolved.repoRoot })
899
- ];
900
- for (const result of results) {
901
- const matched = probeResult(policy, result, resolved.state);
902
- if (matched) return matched;
903
- }
904
- return probeNotObserved(policy);
905
- }
906
- case 'platform.issue':
907
- case 'platform.pull-request':
908
- return await probePlatformFailure(taskRef, policy, resolved, options);
909
- default:
910
- return error('OVERRIDE_PROBE_INPUT_MISSING', `no producer probe is registered for ${policy.producerId}`);
911
- }
912
- }
913
-
914
- function isProducerFailure(value: ProducerFailure | OverrideError): value is ProducerFailure {
915
- return 'observedFacts' in value && 'state' in value && Array.isArray(value.observedFacts);
916
- }
917
-
918
- async function captureFailureSnapshot(taskRef: string, failureIdValue: string, target: string, options: HumanOverrideOptions = {}): Promise<FailureSnapshot | OverrideError> {
919
- const policy = policyFor(failureIdValue);
920
- if (!policy) return error('OVERRIDE_FAILURE_UNKNOWN', `failure policy '${failureIdValue}' is not registered`);
921
- if (!policy.targets.includes(target)) return error('OVERRIDE_TARGET_INVALID', `target '${target}' is not registered for ${failureIdValue}`);
922
- const resolved = resolveTask(taskRef, options);
923
- if (!resolved.ok) return error(resolved.code, resolved.message);
924
- let content: string;
925
- try {
926
- content = fs.readFileSync(resolved.taskMdPath, 'utf8');
927
- } catch (cause) {
928
- return error('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause));
929
- }
930
- let frontmatter: Record<string, unknown> | null = null;
931
- try { frontmatter = parseTypedTaskFrontmatter(content); } catch { /* probe reports an unconfirmed identity */ }
932
- const producer: ProducerFailure | OverrideError = policy.producerId === LIFECYCLE_PRODUCER
933
- ? probeLifecycleFailure(taskRef, policy, resolved, options)
934
- : await probeGenericFailure(taskRef, policy, resolved, options);
935
- if (!isProducerFailure(producer)) return producer;
936
- const digest = computeFailureDigest(snapshotEvidence(
937
- resolved.taskId,
938
- failureIdValue,
939
- target,
940
- content,
941
- frontmatter,
942
- producer,
943
- resourceForSnapshot(policy, options, frontmatter)
944
- ));
945
- const [producerId, ...codeParts] = failureIdValue.split(':');
946
- const code = codeParts.join(':');
947
- return {
948
- schema: '1',
949
- taskId: resolved.taskId,
950
- failureId: failureIdValue,
951
- producerId: producerId ?? '',
952
- code,
953
- target,
954
- state: producer.state,
955
- digest,
956
- observedFacts: producer.observedFacts,
957
- message: producer.message,
958
- capturedAt: (options.now ?? canonicalTimestamp)(),
959
- source: 'producer-result'
960
- };
961
- }
962
-
963
- function isOverrideError(value: FailureSnapshot | OverrideError): value is OverrideError {
964
- return !('schema' in value);
965
- }
966
-
967
- function validateSnapshot(snapshot: FailureSnapshot, expected: FailureSnapshot): OverrideError | null {
968
- if (
969
- snapshot.schema !== '1' || snapshot.source !== 'producer-result' ||
970
- snapshot.taskId !== expected.taskId || snapshot.failureId !== expected.failureId ||
971
- snapshot.producerId !== expected.producerId || snapshot.code !== expected.code ||
972
- snapshot.target !== expected.target || snapshot.digest !== expected.digest ||
973
- snapshot.state !== expected.state || snapshot.message !== expected.message ||
974
- snapshot.observedFacts.length !== expected.observedFacts.length ||
975
- snapshot.observedFacts.some((fact, index) => fact !== expected.observedFacts[index])
976
- ) return error('OVERRIDE_SNAPSHOT_MISMATCH', 'failure snapshot is not the canonical producer probe result');
977
- return null;
978
- }
979
-
980
- function validateCommon(request: HumanOverrideRequest): OverrideError | null {
981
- const fields = [request.taskRef, request.failureId, request.target, request.operator, request.reason, request.scope, request.expiresAt];
982
- if (fields.some((value) => !singleLine(value))) return error('OVERRIDE_PAYLOAD_INVALID', 'override fields must be non-empty single-line values');
983
- if (request.identitySource !== undefined && request.identitySource !== 'local-declared') {
984
- return error('OVERRIDE_IDENTITY_SOURCE_INVALID', 'the local override entrypoint owns identity_source=local-declared');
985
- }
986
- return null;
987
- }
988
-
989
- function validateResourceNumbers(request: HumanOverrideRequest): OverrideError | null {
990
- const values = [
991
- ['alertNumber', request.alertNumber],
992
- ['issueNumber', request.issueNumber],
993
- ['pullRequestNumber', request.pullRequestNumber]
994
- ] as const;
995
- for (const [name, value] of values) {
996
- if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
997
- return error('OVERRIDE_RESOURCE_INVALID', `${name} must be a safe positive integer`);
998
- }
999
- }
1000
- return null;
1001
- }
1002
-
1003
- async function issueHumanOverride(request: HumanOverrideRequest, options: HumanOverrideOptions = {}): Promise<HumanOverrideIssueResult> {
1004
- const invalid = validateCommon(request);
1005
- if (invalid) return failedIssue(invalid.code, invalid.message);
1006
- const policy = policyFor(request.failureId);
1007
- if (!policy) return failedIssue('OVERRIDE_FAILURE_UNKNOWN', `failure policy '${request.failureId}' is not registered`);
1008
- if (!policy.targets.includes(request.target)) return failedIssue('OVERRIDE_TARGET_INVALID', `target '${request.target}' is not registered for ${request.failureId}`);
1009
- const invalidResource = validateResourceNumbers(request);
1010
- if (invalidResource) return failedIssue(invalidResource.code, invalidResource.message);
1011
- const recordOnly = policy.outcomes.some((rule) => rule.target === request.target && rule.effect === 'record-only' && rule.result === 'preserve-failure');
1012
- if (policy.eligibility === 'never-overridable' && !recordOnly) return failedIssue('OVERRIDE_NOT_ELIGIBLE', `failure '${request.failureId}' is never overridable`);
1013
- const issuedAt = (options.now ?? canonicalTimestamp)();
1014
- const issuedMs = Date.parse(issuedAt);
1015
- const expiresMs = Date.parse(request.expiresAt);
1016
- if (!Number.isFinite(issuedMs) || !Number.isFinite(expiresMs) || expiresMs <= issuedMs) {
1017
- return failedIssue('OVERRIDE_EXPIRY_INVALID', 'expiresAt must be a valid time after issuedAt');
1018
- }
1019
- const resolved = resolveTask(request.taskRef, options);
1020
- if (!resolved.ok) return failedIssue(resolved.code, resolved.message, resolved.taskId);
1021
- const document = readTask(resolved.taskMdPath);
1022
- if ('code' in document) return failedIssue(document.code, document.message, resolved.taskId);
1023
- if (document.frontmatter.id !== resolved.taskId) return failedIssue('OVERRIDE_DOCUMENT_INVALID', 'task frontmatter id does not match resolved task', resolved.taskId);
1024
- const snapshotResult = await captureFailureSnapshot(request.taskRef, request.failureId, request.target, {
1025
- ...options,
1026
- probeIntent: request.intent,
1027
- probeOperator: request.operator,
1028
- probeReason: request.reason,
1029
- probeAlertNumber: request.alertNumber,
1030
- probeIssueNumber: request.issueNumber,
1031
- probePullRequestNumber: request.pullRequestNumber,
1032
- probeStagingDir: request.stagingDir
1033
- });
1034
- if (isOverrideError(snapshotResult)) return failedIssue(snapshotResult.code, snapshotResult.message, resolved.taskId);
1035
- const snapshot = snapshotResult as FailureSnapshot;
1036
- if (request.failureSnapshot) {
1037
- const snapshotError = validateSnapshot(request.failureSnapshot, snapshot);
1038
- if (snapshotError) return failedIssue(snapshotError.code, snapshotError.message, resolved.taskId);
1039
- }
1040
- if (request.failureDigest !== undefined && request.failureDigest !== snapshot.digest) {
1041
- return failedIssue('OVERRIDE_DIGEST_MISMATCH', 'caller-provided failure digest does not match the producer probe', resolved.taskId);
1042
- }
1043
- let ledger: ParsedHumanOverrideLedger | null = null;
1044
- try {
1045
- ledger = parseHumanOverrideLedger(document.content);
1046
- } catch (cause) {
1047
- return failedIssue('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause), resolved.taskId);
1048
- }
1049
- const ticketId = (options.randomId ?? randomUUID)();
1050
- if (!singleLine(ticketId)) return failedIssue('OVERRIDE_PAYLOAD_INVALID', 'ticket id generator returned an invalid id', resolved.taskId);
1051
- const mutations = [] as Array<
1052
- | { kind: 'section'; aliases: readonly string[]; heading: string; body: string }
1053
- | { kind: 'table-row'; sectionAliases: readonly string[]; columns: readonly string[]; keyColumn: string; key: string; action: 'upsert'; values: Readonly<Record<string, string>> }
1054
- >;
1055
- if (!ledger) {
1056
- const header = `| ${HUMAN_OVERRIDE_COLUMNS.join(' | ')} |`;
1057
- const separator = `| ${HUMAN_OVERRIDE_COLUMNS.map(() => '---').join(' | ')} |`;
1058
- mutations.push({
1059
- kind: 'section',
1060
- aliases: HUMAN_OVERRIDE_SECTION_ALIASES,
1061
- heading: 'Human Override Ledger',
1062
- body: `${header}\n${separator}`
1063
- });
1064
- } else {
1065
- const migration = migrateHumanOverrideLedger(ledger);
1066
- if (migration) mutations.push(migration);
1067
- }
1068
- const [producerId, ...codeParts] = request.failureId.split(':');
1069
- const code = codeParts.join(':');
1070
- mutations.push({
1071
- kind: 'table-row',
1072
- sectionAliases: HUMAN_OVERRIDE_SECTION_ALIASES,
1073
- columns: HUMAN_OVERRIDE_COLUMNS,
1074
- keyColumn: 'id',
1075
- key: ticketId,
1076
- action: 'upsert',
1077
- values: {
1078
- schema: '1',
1079
- failure_id: request.failureId,
1080
- producer_id: producerId ?? '',
1081
- code,
1082
- target: request.target,
1083
- operator: request.operator,
1084
- identity_source: 'local-declared',
1085
- verified: 'false',
1086
- reason: request.reason,
1087
- scope: request.scope,
1088
- intent: request.intent ?? '',
1089
- failure_message: snapshot.message,
1090
- failure_digest: snapshot.digest,
1091
- context_id: snapshot.observedFacts[0] ?? 'context-unmatched',
1092
- effect: '',
1093
- source_state: resolved.state,
1094
- issued_at: issuedAt,
1095
- expires_at: request.expiresAt,
1096
- status: 'issued',
1097
- result: '',
1098
- consumed_at: '',
1099
- residual: '',
1100
- alert_number: request.alertNumber === undefined ? '' : String(request.alertNumber),
1101
- issue_number: request.issueNumber === undefined ? '' : String(request.issueNumber),
1102
- pull_request_number: request.pullRequestNumber === undefined ? '' : String(request.pullRequestNumber),
1103
- staging_dir: request.stagingDir ?? ''
1104
- }
1105
- });
1106
- const result = writeTask({ taskRef: request.taskRef, expectedState: resolved.state, mutations }, writeOptions(options));
1107
- const writeError = taskWriteError(result);
1108
- if (writeError) return failedIssue(writeError.code, writeError.message, resolved.taskId);
1109
- if (result.status === 'failed') return failedIssue(result.error.code, result.error.message, resolved.taskId);
1110
- if (result.status === 'planned') return failedIssue('OVERRIDE_WRITE_INVALID', 'override writes cannot be planned', resolved.taskId);
1111
- return {
1112
- status: result.status,
1113
- changed: result.changed,
1114
- ticketId,
1115
- taskId: resolved.taskId,
1116
- identity: { source: 'local-declared', operator: request.operator, verified: false },
1117
- error: null
1118
- };
1119
- }
1120
-
1121
- function lifecycleRequestFor(
1122
- taskRef: string,
1123
- intent: string,
1124
- operator: string,
1125
- reason: string,
1126
- values: Readonly<Record<string, string | undefined>> = {}
1127
- ): TaskLifecycleRequest | OverrideError {
1128
- if (!['block', 'activate', 'cancel', 'complete', 'restore', 'close-codescan', 'close-dependabot'].includes(intent)) {
1129
- return error('OVERRIDE_EFFECT_INPUT_MISSING', 'a lifecycle intent is required to apply this override outcome');
1130
- }
1131
- if (intent === 'block') return { taskRef, intent: 'block', agent: operator, reason, unblockCondition: 'manual override recovery' };
1132
- if (intent === 'activate') return { taskRef, intent: 'activate', agent: operator, note: `manual override: ${reason}` };
1133
- if (intent === 'cancel') return { taskRef, intent: 'cancel', agent: operator, reason };
1134
- if (intent === 'complete') return { taskRef, intent: 'complete', agent: operator };
1135
- if (intent === 'close-codescan' || intent === 'close-dependabot') {
1136
- const alertNumber = Number(values.alert_number);
1137
- if (!Number.isSafeInteger(alertNumber) || alertNumber < 1) return error('OVERRIDE_EFFECT_INPUT_MISSING', `${intent} requires the original alert number`);
1138
- return { taskRef, intent, agent: operator, alertNumber, reason };
1139
- }
1140
- const issueNumber = Number(values.issue_number);
1141
- const stagingDir = values.staging_dir;
1142
- if (!Number.isSafeInteger(issueNumber) || issueNumber < 1 || !stagingDir) return error('OVERRIDE_EFFECT_INPUT_MISSING', 'restore requires the original staging directory and issue number');
1143
- return { taskRef, intent: 'restore', agent: operator, stagingDir, issueNumber };
1144
- }
1145
-
1146
- async function applyOutcomeEffect(
1147
- request: ConsumeHumanOverrideRequest,
1148
- row: Readonly<Record<string, string>>,
1149
- snapshot: FailureSnapshot,
1150
- outcome: Outcome,
1151
- options: HumanOverrideOptions
1152
- ): Promise<OverrideError | null> {
1153
- if (outcome.effect === 'no-write' || outcome.effect === 'record-only') return null;
1154
- const intent = outcome.target === 'safe-close' ? 'cancel' : row.intent || request.intent;
1155
- if (!intent && outcome.target === 'continue-local') {
1156
- if (!options.effectExecutor) return error('OVERRIDE_EFFECT_EXECUTOR_MISSING', 'the producer must supply an effect executor for a continue-local outcome');
1157
- return await options.effectExecutor({
1158
- failureId: request.failureId,
1159
- operator: row.operator ?? 'local-operator',
1160
- reason: row.reason ?? snapshot.message
1161
- });
1162
- }
1163
- if (!intent) return error('OVERRIDE_EFFECT_INPUT_MISSING', 'the ticket has no original lifecycle intent for its retry effect');
1164
- const lifecycleRequest = lifecycleRequestFor(request.taskRef, intent, row.operator ?? 'local-operator', row.reason ?? snapshot.message, row);
1165
- if ('code' in lifecycleRequest) return lifecycleRequest;
1166
- const result = applyTaskLifecycle(lifecycleRequest, {
1167
- repoRoot: options.repoRoot,
1168
- metadataProvider: options.metadataProvider,
1169
- manualOverride: { failureId: request.failureId, operator: row.operator ?? 'local-operator', reason: row.reason ?? snapshot.message }
1170
- });
1171
- if (result.status === 'failed') {
1172
- return error('OVERRIDE_EFFECT_FAILED', `${result.error?.code ?? 'LIFECYCLE_FAILED'}: ${result.error?.message ?? 'manual outcome effect failed'}`);
1173
- }
1174
- return null;
1175
- }
1176
-
1177
- function ticketMutation(
1178
- ticketId: string,
1179
- row: Readonly<Record<string, string>>,
1180
- outcome: Outcome,
1181
- status: 'consuming' | 'consumed',
1182
- timestamp: string,
1183
- residual: string,
1184
- columns: readonly string[]
1185
- ) {
1186
- const values: Record<string, string> = {
1187
- schema: row.schema ?? '',
1188
- failure_id: row.failure_id ?? '',
1189
- producer_id: row.producer_id ?? '',
1190
- code: row.code ?? '',
1191
- target: row.target ?? '',
1192
- operator: row.operator ?? '',
1193
- identity_source: row.identity_source ?? '',
1194
- verified: row.verified ?? '',
1195
- reason: row.reason ?? '',
1196
- scope: row.scope ?? '',
1197
- intent: row.intent ?? '',
1198
- failure_message: row.failure_message ?? '',
1199
- failure_digest: row.failure_digest ?? '',
1200
- context_id: outcome.contextId,
1201
- effect: outcome.effect,
1202
- source_state: row.source_state ?? '',
1203
- issued_at: row.issued_at ?? '',
1204
- expires_at: row.expires_at ?? '',
1205
- status,
1206
- result: outcome.result,
1207
- consumed_at: status === 'consumed' ? timestamp : '',
1208
- residual,
1209
- alert_number: row.alert_number ?? '',
1210
- issue_number: row.issue_number ?? '',
1211
- staging_dir: row.staging_dir ?? ''
1212
- };
1213
- if (columns.includes(PULL_REQUEST_NUMBER_COLUMN)) values[PULL_REQUEST_NUMBER_COLUMN] = row[PULL_REQUEST_NUMBER_COLUMN] ?? '';
1214
- return {
1215
- kind: 'table-row' as const,
1216
- sectionAliases: HUMAN_OVERRIDE_SECTION_ALIASES,
1217
- columns,
1218
- keyColumn: 'id',
1219
- key: ticketId,
1220
- action: 'upsert' as const,
1221
- values
1222
- };
1223
- }
1224
-
1225
- function outcomeFromConsumingRow(policy: FailurePolicy, row: Readonly<Record<string, string>>, target: string): Outcome | OverrideError {
1226
- const contextId = row.context_id;
1227
- if (!contextId || !row.effect || !row.result || !row.residual) {
1228
- return error('OVERRIDE_RECOVERY_INVALID', 'consuming override ticket has incomplete effect state');
1229
- }
1230
- const outcome = resolveOutcome(policy, target, [contextId]);
1231
- if (
1232
- outcome.contextId !== contextId || outcome.effect !== row.effect || outcome.result !== row.result ||
1233
- outcome.residual !== row.residual.replace(/^effect pending: /, '')
1234
- ) {
1235
- return error('OVERRIDE_RECOVERY_INVALID', 'consuming override ticket does not match its registered outcome');
1236
- }
1237
- return outcome;
1238
- }
1239
-
1240
- function snapshotFromTicket(taskId: string, row: Readonly<Record<string, string>>): FailureSnapshot | OverrideError {
1241
- const [producerId, ...codeParts] = (row.failure_id ?? '').split(':');
1242
- if (!producerId || codeParts.length === 0 || !row.failure_digest || !row.failure_message || !row.source_state) {
1243
- return error('OVERRIDE_RECOVERY_INVALID', 'consuming override ticket has incomplete failure evidence');
1244
- }
1245
- return {
1246
- schema: '1',
1247
- taskId,
1248
- failureId: row.failure_id,
1249
- producerId,
1250
- code: codeParts.join(':'),
1251
- target: row.target,
1252
- state: row.source_state,
1253
- digest: row.failure_digest,
1254
- observedFacts: row.context_id ? [row.context_id] : [],
1255
- message: row.failure_message,
1256
- capturedAt: row.issued_at,
1257
- source: 'producer-result'
1258
- };
1259
- }
1260
-
1261
- async function consumeHumanOverride(request: ConsumeHumanOverrideRequest, options: HumanOverrideOptions = {}): Promise<HumanOverrideConsumeResult> {
1262
- const values = [request.taskRef, request.ticketId, request.failureId, request.target, request.scope];
1263
- if (values.some((value) => !singleLine(value))) {
1264
- return failedConsume('OVERRIDE_PAYLOAD_INVALID', 'consume fields must be non-empty single-line values', request.ticketId || null);
1265
- }
1266
- const policy = policyFor(request.failureId);
1267
- if (!policy) return failedConsume('OVERRIDE_FAILURE_UNKNOWN', `failure policy '${request.failureId}' is not registered`, request.ticketId);
1268
- if (!policy.targets.includes(request.target)) return failedConsume('OVERRIDE_TARGET_INVALID', `target '${request.target}' is not registered for ${request.failureId}`, request.ticketId);
1269
- const resolved = resolveTask(request.taskRef, options);
1270
- if (!resolved.ok) return failedConsume(resolved.code, resolved.message, request.ticketId, resolved.taskId);
1271
- const document = readTask(resolved.taskMdPath);
1272
- if ('code' in document) return failedConsume(document.code, document.message, request.ticketId, resolved.taskId);
1273
- if (document.frontmatter.id !== resolved.taskId) return failedConsume('OVERRIDE_DOCUMENT_INVALID', 'task frontmatter id does not match resolved task', request.ticketId, resolved.taskId);
1274
- let ledger: ParsedHumanOverrideLedger | null;
1275
- try {
1276
- ledger = parseHumanOverrideLedger(document.content);
1277
- } catch (cause) {
1278
- return failedConsume('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause), request.ticketId, resolved.taskId);
1279
- }
1280
- if (!ledger) return failedConsume('OVERRIDE_NOT_FOUND', `override ledger is missing for ticket '${request.ticketId}'`, request.ticketId, resolved.taskId);
1281
- const row = ledger.table.rows.find((item) => item.values.id === request.ticketId);
1282
- if (!row) return failedConsume('OVERRIDE_NOT_FOUND', `override ticket '${request.ticketId}' was not found`, request.ticketId, resolved.taskId);
1283
- if (row.values.status === 'consumed') return failedConsume('OVERRIDE_REPLAY', `override ticket '${request.ticketId}' was already consumed`, request.ticketId, resolved.taskId);
1284
- const recovering = row.values.status === 'consuming';
1285
- if (row.values.status !== 'issued' && !recovering) return failedConsume('OVERRIDE_STATUS_INVALID', `override ticket '${request.ticketId}' is not issued`, request.ticketId, resolved.taskId);
1286
- if (row.values.failure_id !== request.failureId) return failedConsume('OVERRIDE_FAILURE_MISMATCH', 'ticket failure identity does not match consume request', request.ticketId, resolved.taskId);
1287
- if (row.values.target !== request.target) return failedConsume('OVERRIDE_TARGET_INVALID', 'ticket target does not match consume request', request.ticketId, resolved.taskId);
1288
- if (row.values.scope !== request.scope) return failedConsume('OVERRIDE_SCOPE_MISMATCH', 'ticket scope does not match consume request', request.ticketId, resolved.taskId);
1289
- const resourceError = validatePullRequestBinding(policy, row.values, request);
1290
- if (resourceError) return failedConsume(resourceError.code, resourceError.message, request.ticketId, resolved.taskId);
1291
- let snapshot: FailureSnapshot;
1292
- if (recovering) {
1293
- const recoverySnapshot = snapshotFromTicket(resolved.taskId, row.values);
1294
- if (isOverrideError(recoverySnapshot)) return failedConsume(recoverySnapshot.code, recoverySnapshot.message, request.ticketId, resolved.taskId);
1295
- snapshot = recoverySnapshot;
1296
- } else {
1297
- const snapshotResult = await captureFailureSnapshot(request.taskRef, request.failureId, request.target, {
1298
- ...options,
1299
- probeIntent: row.values.intent ? row.values.intent as TaskLifecycleIntent : request.intent,
1300
- probeOperator: row.values.operator,
1301
- probeReason: row.values.reason,
1302
- probeAlertNumber: row.values.alert_number ? Number(row.values.alert_number) : undefined,
1303
- probeIssueNumber: row.values.issue_number ? Number(row.values.issue_number) : undefined,
1304
- probePullRequestNumber: row.values.pull_request_number ? Number(row.values.pull_request_number) : undefined,
1305
- probeStagingDir: row.values.staging_dir || undefined
1306
- });
1307
- if (isOverrideError(snapshotResult)) return failedConsume(snapshotResult.code, snapshotResult.message, request.ticketId, resolved.taskId);
1308
- snapshot = snapshotResult as FailureSnapshot;
1309
- if (request.failureSnapshot) {
1310
- const snapshotError = validateSnapshot(request.failureSnapshot, snapshot);
1311
- if (snapshotError) return failedConsume(snapshotError.code, snapshotError.message, request.ticketId, resolved.taskId);
1312
- }
1313
- if (request.failureDigest !== undefined && request.failureDigest !== snapshot.digest) return failedConsume('OVERRIDE_DIGEST_MISMATCH', 'caller-provided failure digest does not match the producer result', request.ticketId, resolved.taskId);
1314
- if (row.values.failure_digest !== snapshot.digest) return failedConsume('OVERRIDE_DIGEST_MISMATCH', 'ticket failure digest does not match the producer result', request.ticketId, resolved.taskId);
1315
- if (request.observedFacts && (request.observedFacts.length !== snapshot.observedFacts.length || request.observedFacts.some((fact, index) => fact !== snapshot.observedFacts[index]))) {
1316
- return failedConsume('OVERRIDE_FACTS_MISMATCH', 'caller-provided facts do not match the producer result', request.ticketId, resolved.taskId);
1317
- }
1318
- if (row.values.source_state && row.values.source_state !== snapshot.state) {
1319
- return failedConsume('OVERRIDE_SNAPSHOT_STALE', 'the task no longer has the state captured when the ticket was issued', request.ticketId, resolved.taskId);
1320
- }
1321
- }
1322
- const now = (options.now ?? canonicalTimestamp)();
1323
- const nowMs = Date.parse(now);
1324
- const expiresMs = Date.parse(row.values.expires_at ?? '');
1325
- if (!recovering && (!Number.isFinite(nowMs) || !Number.isFinite(expiresMs) || expiresMs <= nowMs)) return failedConsume('OVERRIDE_EXPIRED', `override ticket '${request.ticketId}' is expired`, request.ticketId, resolved.taskId);
1326
- const resolvedOutcome = recovering ? outcomeFromConsumingRow(policy, row.values, request.target) : resolveOutcome(policy, request.target, snapshot.observedFacts);
1327
- if ('code' in resolvedOutcome) return failedConsume(resolvedOutcome.code, resolvedOutcome.message, request.ticketId, resolved.taskId);
1328
- const outcome = resolvedOutcome;
1329
- let effectRow = row.values;
1330
- if (!recovering && outcome.effect !== 'no-write' && outcome.effect !== 'record-only') {
1331
- const staged = writeTask({
1332
- taskRef: request.taskRef,
1333
- expectedState: resolved.state,
1334
- mutations: [ticketMutation(request.ticketId, row.values, outcome, 'consuming', now, `effect pending: ${outcome.residual}`, ledger.columns)]
1335
- }, writeOptions(options));
1336
- const stagedError = taskWriteError(staged);
1337
- if (stagedError) return failedConsume(stagedError.code, stagedError.message, request.ticketId, resolved.taskId);
1338
- if (staged.status === 'failed') return failedConsume(staged.error.code, staged.error.message, request.ticketId, resolved.taskId);
1339
- if (staged.status === 'planned') return failedConsume('OVERRIDE_WRITE_INVALID', 'override writes cannot be planned', request.ticketId, resolved.taskId);
1340
- effectRow = { ...row.values, status: 'consuming', effect: outcome.effect, result: outcome.result, context_id: outcome.contextId, residual: `effect pending: ${outcome.residual}` };
1341
- }
1342
- const effectError = await applyOutcomeEffect(request, effectRow, snapshot, outcome, options);
1343
- if (effectError) return failedConsume(effectError.code, effectError.message, request.ticketId, resolved.taskId);
1344
- const after = resolveTask(request.taskRef, options);
1345
- if (!after.ok) return failedConsume(after.code, after.message, request.ticketId, after.taskId);
1346
- const mutation = ticketMutation(request.ticketId, effectRow, outcome, 'consumed', now, outcome.residual, ledger.columns);
1347
- const result = writeTask({ taskRef: request.taskRef, expectedState: after.state, mutations: [mutation] }, writeOptions(options));
1348
- const writeError = taskWriteError(result);
1349
- if (writeError) return failedConsume(writeError.code, writeError.message, request.ticketId, resolved.taskId);
1350
- if (result.status === 'failed') return failedConsume(result.error.code, result.error.message, request.ticketId, resolved.taskId);
1351
- if (result.status === 'planned') return failedConsume('OVERRIDE_WRITE_INVALID', 'override writes cannot be planned', request.ticketId, resolved.taskId);
1352
- return {
1353
- status: result.status,
1354
- changed: result.changed,
1355
- ticketId: request.ticketId,
1356
- taskId: resolved.taskId,
1357
- outcome,
1358
- ...(outcome.effect !== 'record-only' && outcome.effect !== 'no-write'
1359
- ? { manualOverride: { failureId: request.failureId, operator: row.values.operator ?? 'local-operator', reason: row.values.reason ?? snapshot.message } }
1360
- : {}),
1361
- error: null
1362
- };
1363
- }
1364
-
1365
- function diagnoseHumanOverride(failure: string, target?: string) {
1366
- const policy = policyFor(failure);
1367
- if (!policy) return { status: 'failed' as const, changed: false as const, error: error('OVERRIDE_FAILURE_UNKNOWN', `failure policy '${failure}' is not registered`) };
1368
- return {
1369
- status: 'ready' as const,
1370
- changed: false as const,
1371
- failure: policy,
1372
- target: target ?? null,
1373
- outcomes: target ? policy.outcomes.filter((rule) => rule.target === target) : policy.outcomes,
1374
- error: null
1375
- };
1376
- }
1377
-
1378
- async function diagnoseHumanOverrideForTask(
1379
- taskRef: string,
1380
- failure?: string,
1381
- target?: string,
1382
- options: HumanOverrideOptions = {}
1383
- ) {
1384
- const resolved = resolveTask(taskRef, options);
1385
- if (!resolved.ok) {
1386
- return {
1387
- status: 'failed' as const,
1388
- changed: false as const,
1389
- taskId: resolved.taskId,
1390
- state: null,
1391
- blockedBy: [],
1392
- probes: [],
1393
- error: error(resolved.code, resolved.message)
1394
- };
1395
- }
1396
- const document = readTask(resolved.taskMdPath);
1397
- if ('code' in document) {
1398
- return {
1399
- status: 'failed' as const,
1400
- changed: false as const,
1401
- taskId: resolved.taskId,
1402
- state: resolved.state,
1403
- blockedBy: [],
1404
- probes: [],
1405
- error: document
1406
- };
1407
- }
1408
- const policies = failure
1409
- ? [policyFor(failure)].filter((policy): policy is FailurePolicy => policy !== null)
1410
- : HUMAN_OVERRIDE_FAILURE_REGISTRY;
1411
- if (failure && policies.length === 0) {
1412
- return {
1413
- status: 'failed' as const,
1414
- changed: false as const,
1415
- taskId: resolved.taskId,
1416
- state: resolved.state,
1417
- blockedBy: [],
1418
- probes: [],
1419
- error: error('OVERRIDE_FAILURE_UNKNOWN', `failure policy '${failure}' is not registered`)
1420
- };
1421
- }
1422
- const probes = await Promise.all(policies
1423
- .filter((policy) => !target || policy.targets.includes(target))
1424
- .map(async (policy) => {
1425
- const producer = policy.producerId === LIFECYCLE_PRODUCER
1426
- ? probeLifecycleFailure(taskRef, policy, resolved, options)
1427
- : await probeGenericFailure(taskRef, policy, resolved, options);
1428
- const observed = isProducerFailure(producer);
1429
- return {
1430
- failureId: policy.id,
1431
- producerId: policy.producerId,
1432
- guardId: policy.guardId,
1433
- code: policy.code,
1434
- target: target && policy.targets.includes(target) ? target : policy.targets[0]!,
1435
- eligibility: policy.eligibility,
1436
- status: observed ? 'observed' as const : producer.code === 'OVERRIDE_PROBE_INPUT_MISSING' ? 'unproven' as const : 'not-observed' as const,
1437
- observedFacts: observed ? producer.observedFacts : [],
1438
- message: observed ? producer.message : producer.message,
1439
- errorCode: observed ? null : producer.code
1440
- };
1441
- }));
1442
- const blockedBy = probes.filter((probe) => probe.status === 'observed');
1443
- return {
1444
- status: 'ready' as const,
1445
- changed: false as const,
1446
- taskId: resolved.taskId,
1447
- state: resolved.state,
1448
- snapshot: {
1449
- taskId: resolved.taskId,
1450
- state: resolved.state,
1451
- documentDigest: computeFailureDigest(document.content)
1452
- },
1453
- blockedBy,
1454
- probes,
1455
- error: null
1456
- };
1457
- }
1458
-
1459
- function renderHumanOverrideAudit(content: string): string {
1460
- let ledger: ParsedHumanOverrideLedger | null;
1461
- try {
1462
- ledger = parseHumanOverrideLedger(content);
1463
- } catch {
1464
- return '';
1465
- }
1466
- if (!ledger || ledger.table.rows.length === 0) return '';
1467
- const rows = ledger.table.rows.map(({ values }) => [
1468
- `- ${values.status || 'unknown'}: ${values.failure_id || 'unknown failure'}`,
1469
- values.pull_request_number ? `resource=pull-request#${values.pull_request_number}` : values.issue_number ? `resource=issue#${values.issue_number}` : 'resource=local',
1470
- `target=${values.target || 'unknown'}`,
1471
- `context=${values.context_id || 'unknown'}`,
1472
- `effect=${values.effect || 'unknown'}`,
1473
- `result=${values.result || 'pending'}`,
1474
- `operator=${values.operator || 'unknown'}`,
1475
- `identity_source=${values.identity_source || 'unknown'}`,
1476
- `reason=${values.reason || 'not recorded'}`,
1477
- `residual=${values.residual || 'pending'}`
1478
- ].join('; '));
1479
- return `## Human Override Audit\n\n${rows.join('\n')}`;
1480
- }
1481
-
1482
- function computeFailureDigest(value: string): string {
1483
- return createHash('sha256').update(value).digest('hex');
1484
- }
1485
-
1486
- export {
1487
- HUMAN_OVERRIDE_COLUMNS,
1488
- HUMAN_OVERRIDE_FAILURE_REGISTRY,
1489
- HUMAN_OVERRIDE_SECTION_ALIASES,
1490
- LIFECYCLE_PRODUCER,
1491
- computeFailureDigest,
1492
- captureFailureSnapshot,
1493
- consumeHumanOverride,
1494
- diagnoseHumanOverride,
1495
- diagnoseHumanOverrideForTask,
1496
- failureId,
1497
- issueHumanOverride,
1498
- overrideDryRunConflict,
1499
- renderHumanOverrideAudit,
1500
- resolveOutcome,
1501
- stableFailureCatalog,
1502
- validatePolicy,
1503
- validateRegistry
1504
- };
1505
- export type {
1506
- ConsumeHumanOverrideRequest,
1507
- Eligibility,
1508
- FailureIdentity,
1509
- FailurePolicy,
1510
- HumanOverrideConsumeResult,
1511
- HumanOverrideIdentity,
1512
- HumanOverrideIssueResult,
1513
- HumanOverrideOptions,
1514
- HumanOverrideRequest,
1515
- FailureSnapshot,
1516
- Outcome,
1517
- OutcomeContext,
1518
- OutcomeEffect,
1519
- OutcomeFallback,
1520
- OutcomeResult,
1521
- OutcomeRule
1522
- };