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