@fitlab-ai/agent-infra 0.9.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +0 -1
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/issue-comments.js +16 -10
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +6 -2
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/workflow-executor.js +4 -14
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/task/artifact-command.js +6 -28
- package/dist/lib/task/artifact-operations.js +16 -230
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +73 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +38 -7
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +164 -22
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +66 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +148 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +1 -0
- package/dist/lib/task/review-finalization.js +104 -77
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +0 -1
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +0 -1
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/issue-comments.ts +18 -10
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +6 -2
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/protocol.ts +5 -0
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/workflow-executor.ts +4 -14
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/task/artifact-command.ts +8 -27
- package/lib/task/artifact-operations.ts +21 -251
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +99 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +47 -14
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +187 -23
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +50 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +176 -132
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +1 -0
- package/lib/task/review-finalization.ts +106 -87
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +0 -1
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- 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/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ResourceIdentity } from '../platform/resource-identity.ts';
|
|
2
|
-
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
|
|
3
2
|
import {
|
|
4
3
|
isResourceIdentity,
|
|
5
4
|
parseResourceIdentity,
|
|
@@ -17,14 +16,6 @@ type PrDeliveryIdentity = {
|
|
|
17
16
|
base: { repository: string; ref: string; sha: string };
|
|
18
17
|
};
|
|
19
18
|
|
|
20
|
-
type LegacyPrDeliveryIdentity = {
|
|
21
|
-
repository: string;
|
|
22
|
-
number: number;
|
|
23
|
-
nodeId: string;
|
|
24
|
-
url: string;
|
|
25
|
-
head: { repository: string; ref: string; sha: string };
|
|
26
|
-
base: { repository: string; ref: string; sha: string };
|
|
27
|
-
};
|
|
28
19
|
type CurrentPrDeliveryIdentityInput = {
|
|
29
20
|
resource: ResourceIdentity;
|
|
30
21
|
repository: string;
|
|
@@ -93,10 +84,6 @@ function timestamp(value: unknown, label: string): string {
|
|
|
93
84
|
if (Number.isNaN(Date.parse(result))) throw factError(`${label} must be a valid timestamp`);
|
|
94
85
|
return new Date(result).toISOString();
|
|
95
86
|
}
|
|
96
|
-
function positiveNumber(value: unknown, label: string): number {
|
|
97
|
-
if (!Number.isSafeInteger(value) || (value as number) <= 0) throw factError(`${label} must be a positive integer`);
|
|
98
|
-
return value as number;
|
|
99
|
-
}
|
|
100
87
|
function parseRef(value: unknown, label: string): { repository: string; ref: string; sha: string } {
|
|
101
88
|
if (!isRecord(value)) throw factError(`${label} must be an object`);
|
|
102
89
|
exactKeys(value, ['repository', 'ref', 'sha'], label);
|
|
@@ -115,35 +102,19 @@ function canonicalIdentity(value: unknown): PrDeliveryIdentity {
|
|
|
115
102
|
};
|
|
116
103
|
}
|
|
117
104
|
|
|
118
|
-
function decodeLegacyIdentity(value: unknown): PrDeliveryIdentity {
|
|
119
|
-
if (!isRecord(value)) throw factError('legacy identity must be an object');
|
|
120
|
-
// TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
|
|
121
|
-
exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
|
|
122
|
-
const legacy = {
|
|
123
|
-
number: positiveNumber(value.number, 'legacy identity.number'),
|
|
124
|
-
repository: text(value.repository, 'identity.repository'),
|
|
125
|
-
nodeId: text(value.nodeId, 'legacy identity.nodeId'),
|
|
126
|
-
url: text(value.url, 'identity.url'),
|
|
127
|
-
head: parseRef(value.head, 'identity.head'),
|
|
128
|
-
base: parseRef(value.base, 'identity.base')
|
|
129
|
-
} satisfies LegacyPrDeliveryIdentity;
|
|
130
|
-
return {
|
|
131
|
-
resource: { kind: 'number', value: legacy.number },
|
|
132
|
-
repository: legacy.repository,
|
|
133
|
-
url: legacy.url,
|
|
134
|
-
head: legacy.head,
|
|
135
|
-
base: legacy.base
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
105
|
function provenanceForSource(source: PrDeliveryBindingSource): PrDeliveryProvenance { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
|
|
140
106
|
|
|
141
|
-
function
|
|
107
|
+
function legacyFactError(): PrDeliveryFactError {
|
|
108
|
+
return Object.assign(
|
|
109
|
+
new Error('PLATFORM_IDENTITY_LEGACY_UNSUPPORTED: v1 PR delivery fact is unsupported; use the previous tool version to view this task or migrate its fact to version 2 before using this version'),
|
|
110
|
+
{ code: 'PLATFORM_IDENTITY_LEGACY_UNSUPPORTED' }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseFact(value: unknown): PrDeliveryFact {
|
|
142
115
|
if (!isRecord(value)) throw factError('fact must be an object');
|
|
143
|
-
if (
|
|
144
|
-
if (value.version
|
|
145
|
-
throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
|
|
146
|
-
}
|
|
116
|
+
if (value.version === 1) throw legacyFactError();
|
|
117
|
+
if (value.version !== 2 || typeof value.state !== 'string') throw factError('version or state is invalid');
|
|
147
118
|
if (value.state === 'unbound') {
|
|
148
119
|
exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
|
|
149
120
|
if (value.reason !== 'initial') throw factError('unbound reason is invalid');
|
|
@@ -155,24 +126,6 @@ function parseFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
|
|
|
155
126
|
return { version: 2, state: 'skipped', reason: 'explicit', decidedAt: timestamp(value.decidedAt, 'skipped.decidedAt') };
|
|
156
127
|
}
|
|
157
128
|
if (value.state !== 'bound') throw factError('state is invalid');
|
|
158
|
-
if (value.version === 1) {
|
|
159
|
-
// TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
|
|
160
|
-
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
|
|
161
|
-
if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('legacy binding or provenance is invalid');
|
|
162
|
-
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
|
|
163
|
-
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
|
|
164
|
-
return parseFact({
|
|
165
|
-
version: 2,
|
|
166
|
-
state: 'bound',
|
|
167
|
-
identity: decodeLegacyIdentity(value.identity),
|
|
168
|
-
binding: {
|
|
169
|
-
status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
|
|
170
|
-
issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
|
|
171
|
-
mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
|
|
172
|
-
},
|
|
173
|
-
provenance: value.provenance
|
|
174
|
-
}, runtimeVersion);
|
|
175
|
-
}
|
|
176
129
|
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
|
|
177
130
|
if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('binding or provenance is invalid');
|
|
178
131
|
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueIdentity', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
|
|
@@ -195,19 +148,19 @@ function parseFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
|
|
|
195
148
|
};
|
|
196
149
|
}
|
|
197
150
|
|
|
198
|
-
function decodePrDeliveryFact(value: unknown,
|
|
151
|
+
function decodePrDeliveryFact(value: unknown, _runtimeVersion?: string): PrDeliveryFact {
|
|
199
152
|
if (typeof value === 'string') {
|
|
200
153
|
let decoded: unknown;
|
|
201
154
|
try { decoded = JSON.parse(value); }
|
|
202
155
|
catch { throw factError('value is not valid JSON'); }
|
|
203
|
-
return parseFact(decoded
|
|
156
|
+
return parseFact(decoded);
|
|
204
157
|
}
|
|
205
|
-
return parseFact(value
|
|
158
|
+
return parseFact(value);
|
|
206
159
|
}
|
|
207
160
|
function encodePrDeliveryFact(fact: PrDeliveryFact): string { return JSON.stringify(parseFact(fact)); }
|
|
208
|
-
function readPrDeliveryFact(metadata: Record<string, unknown>,
|
|
161
|
+
function readPrDeliveryFact(metadata: Record<string, unknown>, _runtimeVersion?: string): PrDeliveryFactReadResult {
|
|
209
162
|
if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '') return { status: 'missing', fact: null };
|
|
210
|
-
try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]
|
|
163
|
+
try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) }; }
|
|
211
164
|
catch (error) { return { status: 'invalid', fact: null, error: asFactError(error) }; }
|
|
212
165
|
}
|
|
213
166
|
function buildUnboundFact(): PrDeliveryFact { return { version: 2, state: 'unbound', reason: 'initial' }; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type RecoveryWarning = Readonly<{ code: string; message: string; action: string }>;
|
|
2
|
+
|
|
3
|
+
function warningText(value: unknown): value is string {
|
|
4
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value && !/[\r\n]/u.test(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isRecoveryWarning(value: unknown): value is RecoveryWarning {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
9
|
+
const warning = value as Record<string, unknown>;
|
|
10
|
+
return Object.keys(warning).sort().join(',') === 'action,code,message'
|
|
11
|
+
&& warningText(warning.code)
|
|
12
|
+
&& warningText(warning.message)
|
|
13
|
+
&& warningText(warning.action);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sameRecoveryWarning(left: unknown, right: unknown): boolean {
|
|
17
|
+
return isRecoveryWarning(left) && isRecoveryWarning(right)
|
|
18
|
+
&& left.code === right.code
|
|
19
|
+
&& left.message === right.message
|
|
20
|
+
&& left.action === right.action;
|
|
21
|
+
}
|
|
@@ -7,6 +7,7 @@ export function parseReviewCommand(args: readonly string[]): ReviewFinalizationR
|
|
|
7
7
|
const values: Record<string, string | boolean> = {};
|
|
8
8
|
const flags = {
|
|
9
9
|
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
|
|
10
|
+
'--recovery-id': 'recoveryId',
|
|
10
11
|
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
11
12
|
} as const;
|
|
12
13
|
for (let index = 2; index < args.length; index += 1) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
2
|
|
|
5
3
|
import { parseArtifactName } from './artifact-name.ts';
|
|
6
4
|
import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.ts';
|
|
@@ -16,9 +14,15 @@ import { allowsManualOverride } from './guard-override.ts';
|
|
|
16
14
|
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
17
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
18
16
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
|
|
17
|
+
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
18
|
+
import {
|
|
19
|
+
beginArtifactRecovery,
|
|
20
|
+
commitArtifactRecovery,
|
|
21
|
+
prepareArtifactRecoveryCommit,
|
|
22
|
+
recoveryContextFromIntent,
|
|
23
|
+
stageArtifactCandidate
|
|
24
|
+
} from './artifact-recovery.ts';
|
|
25
|
+
import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
|
|
22
26
|
|
|
23
27
|
type ReviewFinalizationErrorCode =
|
|
24
28
|
| ResolveTaskRefErrorCode
|
|
@@ -35,8 +39,7 @@ type ReviewFinalizationErrorCode =
|
|
|
35
39
|
| 'REVIEW_DECISION_DETAIL_INVALID'
|
|
36
40
|
| 'REVIEW_ARTIFACT_CONFLICT'
|
|
37
41
|
| 'REVIEW_PROVENANCE_INVALID'
|
|
38
|
-
| '
|
|
39
|
-
| 'REVIEW_RENAME_FAILED';
|
|
42
|
+
| 'REVIEW_RECOVERY_COMMIT_FAILED';
|
|
40
43
|
type ReviewFinalizationError = { code: ReviewFinalizationErrorCode; message: string };
|
|
41
44
|
type ReviewFinalizationRequest = {
|
|
42
45
|
taskRef: string;
|
|
@@ -44,6 +47,7 @@ type ReviewFinalizationRequest = {
|
|
|
44
47
|
artifact: string;
|
|
45
48
|
orchestrated?: boolean;
|
|
46
49
|
dryRun?: boolean;
|
|
50
|
+
recoveryId?: string;
|
|
47
51
|
};
|
|
48
52
|
type ReviewFinalizationResult = {
|
|
49
53
|
status: 'planned' | 'applied' | 'no-op' | 'failed';
|
|
@@ -56,8 +60,12 @@ type ReviewFinalizationResult = {
|
|
|
56
60
|
stageStatus: LedgerStageStatus | null;
|
|
57
61
|
artifactSha256: string | null;
|
|
58
62
|
semanticDigest: string | null;
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
recovery?: Readonly<{
|
|
64
|
+
recoveryId: string;
|
|
65
|
+
candidatePath: string;
|
|
66
|
+
baselineSha256: string;
|
|
67
|
+
baselineSemanticDigest: string;
|
|
68
|
+
}>;
|
|
61
69
|
operations: readonly {
|
|
62
70
|
kind: 'artifact';
|
|
63
71
|
artifact: string;
|
|
@@ -67,17 +75,13 @@ type ReviewFinalizationResult = {
|
|
|
67
75
|
};
|
|
68
76
|
type ReviewFileSystem = {
|
|
69
77
|
readFileSync: (file: string) => string;
|
|
70
|
-
statModeSync: (file: string) => number;
|
|
71
|
-
writeFileSync: (file: string, content: string, mode: number) => void;
|
|
72
|
-
renameSync: (from: string, to: string) => void;
|
|
73
|
-
unlinkSync: (file: string) => void;
|
|
74
78
|
};
|
|
75
79
|
type ReviewFinalizationOptions = {
|
|
76
80
|
repoRoot?: string;
|
|
77
|
-
randomSuffix?: () => string;
|
|
78
81
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
79
82
|
manualOverride?: ManualOverrideCapability;
|
|
80
83
|
lockAlreadyHeld?: boolean;
|
|
84
|
+
startRecovery?: boolean;
|
|
81
85
|
};
|
|
82
86
|
|
|
83
87
|
const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
|
|
@@ -86,11 +90,7 @@ const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' |
|
|
|
86
90
|
code: { family: 'review-code' }
|
|
87
91
|
};
|
|
88
92
|
const DEFAULT_FILE_SYSTEM: ReviewFileSystem = {
|
|
89
|
-
readFileSync: (file) => fs.readFileSync(file, 'utf8')
|
|
90
|
-
statModeSync: (file) => fs.statSync(file).mode,
|
|
91
|
-
writeFileSync: (file, content, mode) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: 'wx', mode }),
|
|
92
|
-
renameSync: (from, to) => fs.renameSync(from, to),
|
|
93
|
-
unlinkSync: (file) => fs.unlinkSync(file)
|
|
93
|
+
readFileSync: (file) => fs.readFileSync(file, 'utf8')
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
function failed(
|
|
@@ -112,27 +112,17 @@ function failed(
|
|
|
112
112
|
stageStatus,
|
|
113
113
|
artifactSha256: null,
|
|
114
114
|
semanticDigest: null,
|
|
115
|
-
repairable: false,
|
|
116
|
-
operation: null,
|
|
117
115
|
operations: [],
|
|
118
116
|
error: { code, message },
|
|
119
117
|
...extra
|
|
120
118
|
};
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
function cleanupTemp(fileSystem: ReviewFileSystem, tempPath: string): void {
|
|
124
|
-
try {
|
|
125
|
-
fileSystem.unlinkSync(tempPath);
|
|
126
|
-
} catch {
|
|
127
|
-
// Best-effort cleanup preserves the primary failure.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
type ReviewSummaryCandidatePreparation = Readonly<{
|
|
132
122
|
result: ReviewFinalizationResult;
|
|
133
123
|
content: string;
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
recovery?: ArtifactRecoveryContext;
|
|
125
|
+
lockAlreadyHeld?: boolean;
|
|
136
126
|
}>;
|
|
137
127
|
|
|
138
128
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
@@ -163,39 +153,28 @@ function prepareReviewSummaryCandidate(
|
|
|
163
153
|
let taskContent: string;
|
|
164
154
|
try { taskContent = (options.fileSystem?.readFileSync ?? DEFAULT_FILE_SYSTEM.readFileSync)(resolved.taskMdPath); }
|
|
165
155
|
catch (error) { return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error)); }
|
|
156
|
+
let recovery: ArtifactRecoveryContext | undefined;
|
|
157
|
+
if (request.recoveryId) {
|
|
158
|
+
try {
|
|
159
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
|
|
160
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
161
|
+
if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
162
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
163
|
+
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
164
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
165
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
166
|
+
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error)); }
|
|
167
|
+
}
|
|
166
168
|
const artifactSha256 = sha256Content(artifactContent);
|
|
167
169
|
const semanticDigest = canonicalSemanticDigest(artifactContent);
|
|
168
170
|
const digests = { artifactSha256, semanticDigest };
|
|
169
171
|
let repairIntent;
|
|
170
|
-
try { repairIntent =
|
|
172
|
+
try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
|
|
171
173
|
catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const repairable = structure.repair !== null && structure.diagnostics.length === 1 && structure.diagnostics[0]?.repairable === true;
|
|
176
|
-
if (repairable && repairIntent && (repairIntent.state !== 'awaiting-repair'
|
|
177
|
-
|| repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== structure.semanticDigest)) {
|
|
178
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'a different review repair baseline is already recorded for this artifact', digests);
|
|
174
|
+
if (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed') {
|
|
175
|
+
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
176
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
179
177
|
}
|
|
180
|
-
return {
|
|
181
|
-
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
182
|
-
artifactSha256, semanticDigest: structure.semanticDigest, repairable, operation: structure.repair
|
|
183
|
-
}),
|
|
184
|
-
...(repairable && !repairIntent ? { provenance: {
|
|
185
|
-
version: 2 as const, taskId: taskId!, family: spec.family, artifact: request.artifact,
|
|
186
|
-
state: 'awaiting-repair' as const, baselineSemanticDigest: structure.semanticDigest,
|
|
187
|
-
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
188
|
-
recoveryOperationId: null, phase: null, authorityDigest: null,
|
|
189
|
-
requestId: `review-finalize:${taskId!}`, createdAt: Date.now(), updatedAt: Date.now()
|
|
190
|
-
}, expectedIntent: null } : {})
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
if (repairIntent?.state === 'awaiting-repair' && repairIntent.baselineSemanticDigest !== semanticDigest) {
|
|
194
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the repaired review artifact semantic digest does not match the recorded repair baseline', digests);
|
|
195
|
-
}
|
|
196
|
-
if ((repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')
|
|
197
|
-
&& (repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== semanticDigest)) {
|
|
198
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
199
178
|
}
|
|
200
179
|
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
201
180
|
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
@@ -217,9 +196,54 @@ function prepareReviewSummaryCandidate(
|
|
|
217
196
|
} catch (error) { return reject('REVIEW_LEDGER_INVALID', String(error)); }
|
|
218
197
|
const detail = inspectDecisionDetailDuplicates(artifactContent);
|
|
219
198
|
if (!detail.ok) return reject('REVIEW_DECISION_DETAIL_INVALID', `${detail.code}: ${detail.message}`);
|
|
199
|
+
const schema = getArtifactSchema(spec.family)!;
|
|
200
|
+
const structure = inspectArtifactContract(artifactContent, schema);
|
|
201
|
+
if (!structure.ok) {
|
|
202
|
+
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
203
|
+
try {
|
|
204
|
+
recovery = beginArtifactRecovery({
|
|
205
|
+
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
206
|
+
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
207
|
+
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
208
|
+
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
212
|
+
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
213
|
+
...(recovery ? { recovery: {
|
|
214
|
+
recoveryId: recovery.recoveryId,
|
|
215
|
+
candidatePath: recovery.stagingPath,
|
|
216
|
+
baselineSha256: recovery.baselineSha256,
|
|
217
|
+
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
218
|
+
} } : {})
|
|
219
|
+
}),
|
|
220
|
+
...(recovery ? { recovery } : {})
|
|
221
|
+
};
|
|
222
|
+
}
|
|
220
223
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
|
|
221
224
|
if (!transformed.ok) return reject(transformed.code, transformed.message);
|
|
222
225
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
226
|
+
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
227
|
+
if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
228
|
+
return {
|
|
229
|
+
content: transformed.content,
|
|
230
|
+
result: {
|
|
231
|
+
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
|
|
232
|
+
status: 'no-op',
|
|
233
|
+
changed: false,
|
|
234
|
+
error: null
|
|
235
|
+
},
|
|
236
|
+
lockAlreadyHeld: options.lockAlreadyHeld
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
240
|
+
try {
|
|
241
|
+
recovery = beginArtifactRecovery({
|
|
242
|
+
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
243
|
+
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
244
|
+
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
245
|
+
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests); }
|
|
246
|
+
}
|
|
223
247
|
return {
|
|
224
248
|
content: transformed.content,
|
|
225
249
|
result: {
|
|
@@ -229,10 +253,8 @@ function prepareReviewSummaryCandidate(
|
|
|
229
253
|
operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
|
|
230
254
|
error: null
|
|
231
255
|
},
|
|
232
|
-
...(
|
|
233
|
-
|
|
234
|
-
expectedIntent: repairIntent
|
|
235
|
-
} : {})
|
|
256
|
+
...(recovery ? { recovery } : {}),
|
|
257
|
+
lockAlreadyHeld: options.lockAlreadyHeld
|
|
236
258
|
};
|
|
237
259
|
}
|
|
238
260
|
|
|
@@ -240,14 +262,28 @@ function commitReviewSummaryProvenance(
|
|
|
240
262
|
prepared: ReviewSummaryCandidatePreparation,
|
|
241
263
|
repoRoot: string
|
|
242
264
|
): ReviewFinalizationResult {
|
|
265
|
+
if (prepared.result.status === 'failed' || !prepared.recovery) return prepared.result;
|
|
243
266
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
|
|
268
|
+
if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
269
|
+
let committed;
|
|
270
|
+
if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
|
|
271
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
272
|
+
} else {
|
|
273
|
+
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
274
|
+
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
275
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
246
276
|
}
|
|
247
|
-
|
|
277
|
+
if (committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
|
|
278
|
+
return {
|
|
279
|
+
...prepared.result,
|
|
280
|
+
artifactSha256: committed.finalArtifactSha256,
|
|
281
|
+
semanticDigest: committed.finalSemanticDigest,
|
|
282
|
+
error: null
|
|
283
|
+
};
|
|
248
284
|
} catch (error) {
|
|
249
285
|
return { ...prepared.result, status: 'failed', error: {
|
|
250
|
-
code: '
|
|
286
|
+
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
251
287
|
} };
|
|
252
288
|
}
|
|
253
289
|
}
|
|
@@ -270,24 +306,6 @@ function finalizeReviewSummaryUnlocked(
|
|
|
270
306
|
catch (error) { return failed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
271
307
|
const prepared = prepareReviewSummaryCandidate(request, content, options);
|
|
272
308
|
if (request.dryRun) return prepared.result;
|
|
273
|
-
if (prepared.result.status !== 'applied') return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
|
|
274
|
-
const tempPath = path.join(resolved.taskDir, `.${request.artifact}.tmp-${process.pid}-${(options.randomSuffix ?? randomUUID)()}`);
|
|
275
|
-
try {
|
|
276
|
-
fileSystem.writeFileSync(tempPath, prepared.content, fileSystem.statModeSync(validated.artifact.path));
|
|
277
|
-
} catch (error) {
|
|
278
|
-
cleanupTemp(fileSystem, tempPath);
|
|
279
|
-
return failed(request, 'REVIEW_TEMP_WRITE_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
if (fileSystem.readFileSync(validated.artifact.path) !== content) {
|
|
283
|
-
cleanupTemp(fileSystem, tempPath);
|
|
284
|
-
return failed(request, 'REVIEW_ARTIFACT_CONFLICT', `${request.artifact} changed during finalization`, resolved.taskId, prepared.result.stageStatus);
|
|
285
|
-
}
|
|
286
|
-
fileSystem.renameSync(tempPath, validated.artifact.path);
|
|
287
|
-
} catch (error) {
|
|
288
|
-
cleanupTemp(fileSystem, tempPath);
|
|
289
|
-
return failed(request, 'REVIEW_RENAME_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
|
|
290
|
-
}
|
|
291
309
|
return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
|
|
292
310
|
}
|
|
293
311
|
|
|
@@ -295,15 +313,16 @@ function finalizeReviewSummary(
|
|
|
295
313
|
request: ReviewFinalizationRequest,
|
|
296
314
|
options: ReviewFinalizationOptions = {}
|
|
297
315
|
): ReviewFinalizationResult {
|
|
298
|
-
|
|
316
|
+
const effectiveOptions = { ...options, startRecovery: !request.dryRun };
|
|
317
|
+
if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
299
318
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
300
|
-
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request,
|
|
319
|
+
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
301
320
|
try {
|
|
302
321
|
return withTaskExecutionLock(
|
|
303
322
|
resolved.repoRoot,
|
|
304
323
|
resolved.taskId,
|
|
305
324
|
'task-review.finalize-summary',
|
|
306
|
-
() => finalizeReviewSummaryUnlocked(request,
|
|
325
|
+
() => finalizeReviewSummaryUnlocked(request, { ...effectiveOptions, lockAlreadyHeld: true })
|
|
307
326
|
);
|
|
308
327
|
} catch (error) {
|
|
309
328
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
package/lib/task/short-id.ts
CHANGED
|
@@ -329,14 +329,18 @@ function mutateShortIdRegistryAt(
|
|
|
329
329
|
activeDir: string,
|
|
330
330
|
width: number,
|
|
331
331
|
taskId: string,
|
|
332
|
-
requested: ShortIdMutationEffect
|
|
332
|
+
requested: ShortIdMutationEffect,
|
|
333
|
+
onlyReleaseWhenInactive = false,
|
|
334
|
+
pruneOrphans = true
|
|
333
335
|
): ShortIdMutationResult {
|
|
334
336
|
const registryPath = path.join(activeDir, REGISTRY_NAME);
|
|
335
337
|
return withRegistryLock(activeDir, () => {
|
|
336
338
|
const registry = readRegistryStrict(registryPath, width);
|
|
337
339
|
const original = JSON.stringify(registry.ids);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
if (pruneOrphans) {
|
|
341
|
+
for (const [key, candidate] of Object.entries(registry.ids)) {
|
|
342
|
+
if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))) delete registry.ids[key];
|
|
343
|
+
}
|
|
340
344
|
}
|
|
341
345
|
const existing = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId);
|
|
342
346
|
let result: ShortIdMutationResult;
|
|
@@ -356,8 +360,13 @@ function mutateShortIdRegistryAt(
|
|
|
356
360
|
result = { effect: 'allocated', shortId: key, changed: true };
|
|
357
361
|
}
|
|
358
362
|
} else if (requested === 'release') {
|
|
359
|
-
|
|
360
|
-
|
|
363
|
+
const taskIsActive = fs.existsSync(path.join(activeDir, taskId, 'task.md'));
|
|
364
|
+
if (!existing || (onlyReleaseWhenInactive && taskIsActive)) {
|
|
365
|
+
result = { effect: 'unchanged', shortId: existing?.[0] ?? null, changed: false };
|
|
366
|
+
} else {
|
|
367
|
+
delete registry.ids[existing[0]];
|
|
368
|
+
result = { effect: 'released', shortId: existing[0], changed: true };
|
|
369
|
+
}
|
|
361
370
|
} else result = { effect: 'unchanged', shortId: existing ? existing[0] : null, changed: false };
|
|
362
371
|
const changed = original !== JSON.stringify(registry.ids);
|
|
363
372
|
if (changed) writeRegistry(registryPath, registry);
|
|
@@ -365,6 +374,18 @@ function mutateShortIdRegistryAt(
|
|
|
365
374
|
});
|
|
366
375
|
}
|
|
367
376
|
|
|
377
|
+
function releaseStaleShortIdRegistry(repoRoot: string, taskId: string): ShortIdMutationResult {
|
|
378
|
+
const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
|
|
379
|
+
return mutateShortIdRegistryAt(
|
|
380
|
+
activeDir,
|
|
381
|
+
configuredShortIdLength(repoRoot),
|
|
382
|
+
taskId,
|
|
383
|
+
'release',
|
|
384
|
+
true,
|
|
385
|
+
false
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
368
389
|
function executeShortIdCommand(request: ShortIdCommandRequest): ShortIdCommandResult {
|
|
369
390
|
try {
|
|
370
391
|
if (!Number.isInteger(request.shortIdLength) || request.shortIdLength < 1) {
|
|
@@ -550,6 +571,7 @@ export {
|
|
|
550
571
|
inspectShortIdRegistry,
|
|
551
572
|
configuredShortIdLength,
|
|
552
573
|
mutateShortIdRegistry,
|
|
574
|
+
releaseStaleShortIdRegistry,
|
|
553
575
|
executeShortIdCommand
|
|
554
576
|
};
|
|
555
577
|
export type {
|
|
@@ -293,7 +293,7 @@ function withTaskExecutionLock<T>(
|
|
|
293
293
|
return value;
|
|
294
294
|
} catch (error) {
|
|
295
295
|
if (error instanceof TaskExecutionLockError) throw error;
|
|
296
|
-
if (error instanceof Error && error.name
|
|
296
|
+
if (error instanceof Error && ['OrchestrationStateError', 'ArtifactRecoveryError'].includes(error.name)) throw error;
|
|
297
297
|
throw new TaskExecutionLockError(
|
|
298
298
|
'ORCHESTRATION_LOCK_FAILED',
|
|
299
299
|
'lifecycle task lock operation failed',
|
|
@@ -48,6 +48,7 @@ import { manualValidationFinalSummaryProjectionMatches } from "./manual-validati
|
|
|
48
48
|
import { readManualValidationCompletion } from "./manual-validation-completion.ts";
|
|
49
49
|
import { sha256File } from "./artifact-receipts.ts";
|
|
50
50
|
import { summaryCommentState } from "../platform/pr-summary.ts";
|
|
51
|
+
import { taskIssueIdentity } from "../platform/task-identities.ts";
|
|
51
52
|
|
|
52
53
|
const TASK_ENUMS = {
|
|
53
54
|
type: ["feature", "bugfix", "refactor", "docs", "chore"],
|
|
@@ -100,8 +101,6 @@ const sharedUtils = {
|
|
|
100
101
|
failResult,
|
|
101
102
|
blockedResult,
|
|
102
103
|
safeStat,
|
|
103
|
-
parseIssueNumber,
|
|
104
|
-
parsePrNumber
|
|
105
104
|
};
|
|
106
105
|
|
|
107
106
|
async function runCheck(type: any, context: any, shared: any): Promise<any> {
|
|
@@ -361,8 +360,8 @@ function checkTaskMeta({ taskDir, config, repositoryRoot }: any): any {
|
|
|
361
360
|
);
|
|
362
361
|
}
|
|
363
362
|
|
|
364
|
-
if (config.
|
|
365
|
-
return failResult("task-meta", "Expected a valid
|
|
363
|
+
if (config.require_platform_issue_identity && !taskIssueIdentity(metadata)) {
|
|
364
|
+
return failResult("task-meta", "Expected a valid platform_issue_identity in task metadata");
|
|
366
365
|
}
|
|
367
366
|
|
|
368
367
|
if (config.require_completed_at && isBlank(metadata.completed_at)) {
|
|
@@ -1376,19 +1375,6 @@ function getCheckedRequirements(content: any): any {
|
|
|
1376
1375
|
.map((match: any) => match[1].trim());
|
|
1377
1376
|
}
|
|
1378
1377
|
|
|
1379
|
-
function parseIssueNumber(value: any): any {
|
|
1380
|
-
if (isBlank(value) || value === "N/A") {
|
|
1381
|
-
return null;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
const match = String(value).match(/\d+/);
|
|
1385
|
-
return match ? Number(match[0]) : null;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function parsePrNumber(value: any): any {
|
|
1389
|
-
return parseIssueNumber(value);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
1378
|
// === Utilities ===
|
|
1393
1379
|
|
|
1394
1380
|
function normalizeContent(text: any): any {
|
|
@@ -11,8 +11,6 @@ export type VerificationShared = {
|
|
|
11
11
|
failResult: (type: string, message: string, failType?: string) => VerificationCheckResult;
|
|
12
12
|
blockedResult: (type: string, message: string, failType?: string) => VerificationCheckResult;
|
|
13
13
|
safeStat: (filePath: string) => fs.Stats | null;
|
|
14
|
-
parseIssueNumber: (value: unknown) => number | null;
|
|
15
|
-
parsePrNumber: (value: unknown) => number | null;
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
type VerificationStatus = 'pass' | 'fail' | 'blocked';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export const TASK_WORKFLOW_COMMANDS = Object.freeze({
|
|
2
2
|
'artifact-inspect': ['task-artifact', 'inspect'],
|
|
3
3
|
'artifact-init': ['task-artifact', 'init'],
|
|
4
|
-
'artifact-repair': ['task-artifact', 'repair'],
|
|
5
4
|
'artifact-finalize-local': ['task-artifact', 'finalize-local'],
|
|
6
5
|
'review-finalize-summary': ['task-review', 'finalize-summary'],
|
|
7
6
|
event: ['task-event', null],
|
package/lib/version.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import semver from 'semver';
|
|
3
2
|
|
|
4
3
|
const packageJsonUrl = [
|
|
5
4
|
new URL('../package.json', import.meta.url),
|
|
@@ -13,22 +12,9 @@ if (!packageJsonUrl) {
|
|
|
13
12
|
const { version } = JSON.parse(readFileSync(packageJsonUrl, 'utf8'));
|
|
14
13
|
const VERSION = `v${version}`;
|
|
15
14
|
const AGENT_INFRA_VERSION_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
16
|
-
const LEGACY_COMPATIBILITY_CUTOFF_VERSION = '1.0.0';
|
|
17
15
|
|
|
18
16
|
function isValidAgentInfraVersion(value: unknown): value is string {
|
|
19
17
|
return typeof value === 'string' && AGENT_INFRA_VERSION_PATTERN.test(value);
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
const normalized = runtimeVersion.startsWith('v') ? runtimeVersion.slice(1) : runtimeVersion;
|
|
24
|
-
const parsed = semver.valid(normalized);
|
|
25
|
-
return parsed !== null && semver.lt(parsed, LEGACY_COMPATIBILITY_CUTOFF_VERSION);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function legacyCompatibilityError(resource: string, runtimeVersion = VERSION): Error & { code: string } {
|
|
29
|
-
return Object.assign(new Error(
|
|
30
|
-
`PLATFORM_IDENTITY_LEGACY_UNSUPPORTED: ${resource} requires migration to the current schema before v${LEGACY_COMPATIBILITY_CUTOFF_VERSION} runtime ${runtimeVersion}`
|
|
31
|
-
), { code: 'PLATFORM_IDENTITY_LEGACY_UNSUPPORTED' });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export { LEGACY_COMPATIBILITY_CUTOFF_VERSION, VERSION, isLegacyCompatibilityEnabled, isValidAgentInfraVersion, legacyCompatibilityError };
|
|
20
|
+
export { VERSION, isValidAgentInfraVersion };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|