@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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