@fitlab-ai/agent-infra 0.11.6 → 0.11.7
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/git/primary-worktree.js +58 -0
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +6 -2
- package/dist/lib/task/create.js +46 -82
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +7 -3
- package/lib/task/create.ts +44 -71
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.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 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +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/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
|
@@ -7,24 +7,30 @@ import { extractSection, findSectionHeading, parseTable } from './sections.ts';
|
|
|
7
7
|
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'] as const;
|
|
8
8
|
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'] as const;
|
|
9
9
|
const RECEIPT_EVENTS = new Set([
|
|
10
|
+
'analysis.completed',
|
|
11
|
+
'plan.completed',
|
|
10
12
|
'review-analysis.completed',
|
|
11
13
|
'review-plan.completed',
|
|
14
|
+
'code.completed',
|
|
12
15
|
'review-code.completed',
|
|
13
|
-
'code.completed'
|
|
14
16
|
]);
|
|
15
17
|
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
16
18
|
const RECEIPT_SHAPES = {
|
|
19
|
+
'analysis.completed': { output: 'analysis', input: 'review-analysis' },
|
|
20
|
+
'plan.completed': { output: 'plan', input: 'analysis' },
|
|
17
21
|
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
18
22
|
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
19
|
-
'
|
|
20
|
-
'code.completed': { output: 'code', input: '
|
|
23
|
+
'code.completed': { output: 'code', input: 'plan' },
|
|
24
|
+
'review-code.completed': { output: 'review-code', input: 'code' }
|
|
21
25
|
} as const;
|
|
22
26
|
|
|
23
27
|
type ArtifactReceiptEvent =
|
|
28
|
+
| 'analysis.completed'
|
|
29
|
+
| 'plan.completed'
|
|
24
30
|
| 'review-analysis.completed'
|
|
25
31
|
| 'review-plan.completed'
|
|
26
|
-
| '
|
|
27
|
-
| 'code.completed';
|
|
32
|
+
| 'code.completed'
|
|
33
|
+
| 'review-code.completed';
|
|
28
34
|
type ArtifactReceipt = {
|
|
29
35
|
event: ArtifactReceiptEvent;
|
|
30
36
|
output: string;
|
|
@@ -65,8 +71,16 @@ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input
|
|
|
65
71
|
if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
66
72
|
const shape = RECEIPT_SHAPES[event];
|
|
67
73
|
const inputMatches = event === 'code.completed'
|
|
68
|
-
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
|
|
69
|
-
:
|
|
74
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan' || inputIdentity.family === 'review-code'
|
|
75
|
+
: event === 'plan.completed'
|
|
76
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan'
|
|
77
|
+
: event === 'analysis.completed'
|
|
78
|
+
? inputIdentity.family === 'review-analysis'
|
|
79
|
+
: event === 'review-plan.completed'
|
|
80
|
+
? inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis'
|
|
81
|
+
: event === 'review-code.completed'
|
|
82
|
+
? inputIdentity.family === 'code' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-plan'
|
|
83
|
+
: inputIdentity.family === shape.input;
|
|
70
84
|
if (outputIdentity.family !== shape.output || !inputMatches) {
|
|
71
85
|
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
72
86
|
}
|
|
@@ -81,7 +95,9 @@ function parseArtifactReceipts(content: string): ArtifactReceiptParseResult {
|
|
|
81
95
|
table = parseTable(content, {
|
|
82
96
|
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
83
97
|
columns: [...RECEIPT_COLUMNS],
|
|
84
|
-
|
|
98
|
+
// A completed artifact can have multiple lifecycle inputs. Pair
|
|
99
|
+
// uniqueness is checked below because the table parser supports one key.
|
|
100
|
+
keyColumn: null
|
|
85
101
|
});
|
|
86
102
|
} catch (error) {
|
|
87
103
|
throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
|
|
@@ -109,15 +125,31 @@ function parseArtifactReceipts(content: string): ArtifactReceiptParseResult {
|
|
|
109
125
|
completedAt
|
|
110
126
|
};
|
|
111
127
|
});
|
|
128
|
+
const edges = new Set<string>();
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const edge = `${row.output}\0${row.input}`;
|
|
131
|
+
if (edges.has(edge)) throw new ArtifactReceiptError(`duplicate receipt edge '${row.output}' <- '${row.input}'`);
|
|
132
|
+
edges.add(edge);
|
|
133
|
+
}
|
|
112
134
|
return { present: true, rows };
|
|
113
135
|
}
|
|
114
136
|
|
|
137
|
+
function receiptsForOutput(content: string, output: string): readonly ArtifactReceipt[] {
|
|
138
|
+
return parseArtifactReceipts(content).rows.filter((row) => row.output === output);
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
function receiptForOutput(content: string, output: string): ArtifactReceipt | null {
|
|
116
|
-
|
|
117
|
-
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
142
|
+
return receiptsForOutput(content, output)[0] ?? null;
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
function upsertArtifactReceipt(content: string, receipt: ArtifactReceipt): ReceiptSectionMutation {
|
|
146
|
+
return upsertArtifactReceipts(content, [receipt]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function upsertArtifactReceipts(content: string, receiptsToAdd: readonly ArtifactReceipt[]): ReceiptSectionMutation {
|
|
150
|
+
if (receiptsToAdd.length === 0) throw new ArtifactReceiptError('at least one receipt is required');
|
|
151
|
+
const additions = new Map<string, ArtifactReceipt>();
|
|
152
|
+
for (const receipt of receiptsToAdd) {
|
|
121
153
|
if (!RECEIPT_EVENTS.has(receipt.event)) throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
122
154
|
if (!receipt.output || !receipt.input) throw new ArtifactReceiptError('receipt output and input are required');
|
|
123
155
|
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
@@ -126,12 +158,18 @@ function upsertArtifactReceipt(content: string, receipt: ArtifactReceipt): Recei
|
|
|
126
158
|
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
127
159
|
}
|
|
128
160
|
|
|
161
|
+
const key = `${receipt.output}\0${receipt.input}`;
|
|
162
|
+
const duplicate = additions.get(key);
|
|
163
|
+
if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(receipt)) throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' has conflicting evidence`);
|
|
164
|
+
additions.set(key, receipt);
|
|
165
|
+
}
|
|
129
166
|
const existing = parseArtifactReceipts(content).rows;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
|
|
167
|
+
for (const receipt of additions.values()) {
|
|
168
|
+
const previous = existing.find((row) => row.output === receipt.output && row.input === receipt.input);
|
|
169
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' already exists with different evidence`);
|
|
133
170
|
}
|
|
134
|
-
const rows =
|
|
171
|
+
const rows = [...existing];
|
|
172
|
+
for (const receipt of additions.values()) if (!rows.some((row) => row.output === receipt.output && row.input === receipt.input)) rows.push(receipt);
|
|
135
173
|
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
136
174
|
const body = [
|
|
137
175
|
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
@@ -146,8 +184,10 @@ export {
|
|
|
146
184
|
RECEIPT_SECTION_ALIASES,
|
|
147
185
|
parseArtifactReceipts,
|
|
148
186
|
receiptForOutput,
|
|
187
|
+
receiptsForOutput,
|
|
149
188
|
sha256Bytes,
|
|
150
189
|
sha256File,
|
|
151
|
-
upsertArtifactReceipt
|
|
190
|
+
upsertArtifactReceipt,
|
|
191
|
+
upsertArtifactReceipts
|
|
152
192
|
};
|
|
153
193
|
export type { ArtifactReceipt, ArtifactReceiptEvent, ArtifactReceiptParseResult, ReceiptSectionMutation };
|
package/lib/task/capabilities.ts
CHANGED
|
@@ -17,6 +17,8 @@ import type { QualificationAudit, TaskQualification } from './qualification-audi
|
|
|
17
17
|
import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.ts';
|
|
18
18
|
import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
19
19
|
import { parseImplementationInputs } from './implementation-inputs.ts';
|
|
20
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
|
|
21
|
+
import { inspectReviewIdentity } from './review-identity.ts';
|
|
20
22
|
|
|
21
23
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
22
24
|
|
|
@@ -55,6 +57,7 @@ type LifecycleFacts = {
|
|
|
55
57
|
pathState?: LifecyclePathState;
|
|
56
58
|
reworkClassificationRequired?: readonly ('analysis' | 'plan' | 'code')[];
|
|
57
59
|
resolvedHumanDecisions?: Partial<Record<'analysis' | 'plan' | 'code', 'review' | 'implementation'>>;
|
|
60
|
+
reviewCodeIdentityValid?: boolean;
|
|
58
61
|
};
|
|
59
62
|
type CapabilityResult = {
|
|
60
63
|
allowed: boolean;
|
|
@@ -176,11 +179,12 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
176
179
|
return allow('plan-review-approved');
|
|
177
180
|
}
|
|
178
181
|
if (action === 'review-code') {
|
|
179
|
-
return hasArtifact(facts, 'code') ?
|
|
182
|
+
return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
|
|
180
183
|
}
|
|
181
184
|
if (action === 'manual-validation' || action === 'validation-run') {
|
|
182
185
|
if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
|
|
183
|
-
if (!reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
|
|
186
|
+
if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
|
|
187
|
+
if (!facts.reviewCodeIdentityValid) return deny('CODE_REVIEW_IDENTITY_INVALID');
|
|
184
188
|
if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
|
|
185
189
|
if (facts.unresolvedLedger.code > 0) return deny('CODE_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.code}`);
|
|
186
190
|
return allow('code-review-approved');
|
|
@@ -334,7 +338,9 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
334
338
|
try { auditContent = fs.readFileSync(path.join(taskDir, name), 'utf8'); }
|
|
335
339
|
catch { qualificationStaleArtifacts.push(name); continue; }
|
|
336
340
|
const audit = parseQualificationAudit(auditContent);
|
|
337
|
-
if (!audit.ok
|
|
341
|
+
if (!audit.ok) { qualificationStaleArtifacts.push(name); continue; }
|
|
342
|
+
if (!audit.audit.present) continue;
|
|
343
|
+
if (!audit.audit.snapshot) { qualificationStaleArtifacts.push(name); continue; }
|
|
338
344
|
const constraintsChanged = audit.audit.constraintDependencies.some((dependency) => constraints.get(dependency.constraintId) !== dependency.constraintDigest);
|
|
339
345
|
const taskInputChanged = audit.audit.snapshot.taskInputDigest !== qualification.qualification.taskInputDigest;
|
|
340
346
|
if (audit.audit.snapshot.nonConstraintInputDigest !== qualification.qualification.nonConstraintInputDigest
|
|
@@ -345,6 +351,7 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
345
351
|
}
|
|
346
352
|
const reviews: LifecycleFacts['reviews'] = {};
|
|
347
353
|
const reviewedInputs: NonNullable<LifecycleFacts['reviewedInputs']> = {};
|
|
354
|
+
let reviewCodeIdentityValid = false;
|
|
348
355
|
for (const family of ['review-analysis', 'review-plan', 'review-code'] as const) {
|
|
349
356
|
const latest = latestArtifact(artifacts[family] ?? []);
|
|
350
357
|
if (!latest) continue;
|
|
@@ -358,6 +365,12 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
358
365
|
&& (artifacts[expectedFamily] ?? []).includes(input)
|
|
359
366
|
&& artifactHashes[input] === receipt.inputSha256
|
|
360
367
|
) reviewedInputs[family] = input;
|
|
368
|
+
if (family === 'review-code') {
|
|
369
|
+
const identity = inspectReviewIdentity(taskDir, reviewContent);
|
|
370
|
+
reviewCodeIdentityValid = identity.status === 'matched'
|
|
371
|
+
&& hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
|
|
372
|
+
&& hasArtifactCompletionLog(content, latest, 'review-code.completed');
|
|
373
|
+
}
|
|
361
374
|
const parsed = parseReviewSummary(reviewContent);
|
|
362
375
|
if (!parsed.ok) continue;
|
|
363
376
|
const verdict = resolveCanonicalVerdict(parsed.summary);
|
|
@@ -423,6 +436,7 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
423
436
|
, pathState
|
|
424
437
|
, reworkClassificationRequired
|
|
425
438
|
, resolvedHumanDecisions
|
|
439
|
+
, reviewCodeIdentityValid
|
|
426
440
|
};
|
|
427
441
|
facts.recommendedAction = recommendNext(facts).action;
|
|
428
442
|
return { ok: true, facts };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { canonicalSemanticDigest } from './artifact-operations.ts';
|
|
4
|
+
import { inspectActivityLog } from './activity-log.ts';
|
|
5
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
6
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
7
|
+
import { sha256File } from './artifact-receipts.ts';
|
|
8
|
+
|
|
9
|
+
export type CompletionFact = Readonly<{
|
|
10
|
+
event: string;
|
|
11
|
+
output: string;
|
|
12
|
+
outputSha256: string;
|
|
13
|
+
semanticDigest: string;
|
|
14
|
+
requestId: string;
|
|
15
|
+
result: string;
|
|
16
|
+
lifecycleInputs?: readonly Readonly<{ name: string; sha256: string }>[];
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export function parseCompletionFacts(value: unknown): CompletionFact[] {
|
|
20
|
+
if (typeof value !== 'string') return [];
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(value);
|
|
23
|
+
if (!Array.isArray(parsed)) return [];
|
|
24
|
+
return parsed.filter((fact): fact is CompletionFact => Boolean(
|
|
25
|
+
fact && typeof fact === 'object' && !Array.isArray(fact)
|
|
26
|
+
&& typeof (fact as CompletionFact).event === 'string'
|
|
27
|
+
&& typeof (fact as CompletionFact).output === 'string'
|
|
28
|
+
&& /^[a-f0-9]{64}$/u.test((fact as CompletionFact).outputSha256)
|
|
29
|
+
&& /^[a-f0-9]{64}$/u.test((fact as CompletionFact).semanticDigest)
|
|
30
|
+
&& typeof (fact as CompletionFact).requestId === 'string'
|
|
31
|
+
&& typeof (fact as CompletionFact).result === 'string'
|
|
32
|
+
));
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasArtifactCompletionFact(taskContent: string, artifactPath: string, event: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
const frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
41
|
+
const output = artifactPath.split(/[\\/]/u).at(-1) ?? '';
|
|
42
|
+
const content = fs.readFileSync(artifactPath, 'utf8');
|
|
43
|
+
const outputSha256 = sha256File(artifactPath);
|
|
44
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
45
|
+
return parseCompletionFacts(frontmatter.completion_facts).some((fact) =>
|
|
46
|
+
fact.event === event && fact.output === output
|
|
47
|
+
&& fact.outputSha256 === outputSha256 && fact.semanticDigest === semanticDigest
|
|
48
|
+
&& fact.requestId.length > 0
|
|
49
|
+
);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasArtifactCompletionLog(taskContent: string, artifact: string, event: string): boolean {
|
|
56
|
+
const identity = parseArtifactName(artifact);
|
|
57
|
+
if (!identity) return false;
|
|
58
|
+
const action = event === 'review-code.completed' ? 'Review Code'
|
|
59
|
+
: event === 'review-plan.completed' ? 'Review Plan'
|
|
60
|
+
: event === 'review-analysis.completed' ? 'Review Analysis' : null;
|
|
61
|
+
if (!action) return false;
|
|
62
|
+
const { section, invalidEntries } = inspectActivityLog(taskContent);
|
|
63
|
+
if (!section || invalidEntries.length > 0) return false;
|
|
64
|
+
const expected = `${action} (Round ${identity.round})`;
|
|
65
|
+
return section.entries.some((entry) => entry.step === expected && entry.note.includes(`→ ${artifact}`));
|
|
66
|
+
}
|
|
@@ -19,7 +19,7 @@ type TaskCreateError = { code: string; message: string; retryable: boolean };
|
|
|
19
19
|
type TaskCreateResult = Readonly<{
|
|
20
20
|
status: TaskCreateStatus;
|
|
21
21
|
changed: boolean;
|
|
22
|
-
task: { id: string | null; shortId: string | null };
|
|
22
|
+
task: { id: string | null; shortId: string | null; state: 'active' | 'blocked' | 'completed' | 'archive' | null };
|
|
23
23
|
issue: { number: number; url: string } | null;
|
|
24
24
|
operations: readonly TaskCreateOperation[];
|
|
25
25
|
warnings: readonly TaskCreateWarning[];
|
|
@@ -87,9 +87,10 @@ export function parseTaskCreateResult(value: unknown): TaskCreateResult {
|
|
|
87
87
|
if (!['applied', 'no-op', 'degraded', 'failed', 'blocked'].includes(result.status as string)
|
|
88
88
|
|| typeof result.changed !== 'boolean'
|
|
89
89
|
|| !result.task || typeof result.task !== 'object' || Array.isArray(result.task)
|
|
90
|
-
|| !exactKeys(result.task as Record<string, unknown>, ['id', 'shortId'])
|
|
90
|
+
|| !exactKeys(result.task as Record<string, unknown>, ['id', 'shortId', 'state'])
|
|
91
91
|
|| !validTaskReference((result.task as { id?: unknown }).id)
|
|
92
92
|
|| !validTaskReference((result.task as { shortId?: unknown }).shortId)
|
|
93
|
+
|| ![null, 'active', 'blocked', 'completed', 'archive'].includes((result.task as { state?: unknown }).state as string | null)
|
|
93
94
|
|| !Array.isArray(result.operations)
|
|
94
95
|
|| !result.operations.every((operation) => {
|
|
95
96
|
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) return false;
|
|
@@ -136,7 +137,7 @@ export function taskCreateFailure(
|
|
|
136
137
|
return {
|
|
137
138
|
status: error.retryable ? 'blocked' : 'failed',
|
|
138
139
|
changed: false,
|
|
139
|
-
task: { id: null, shortId: null },
|
|
140
|
+
task: { id: null, shortId: null, state: null },
|
|
140
141
|
issue: null,
|
|
141
142
|
operations: [],
|
|
142
143
|
warnings: [],
|
|
@@ -206,6 +207,9 @@ async function createTask(value: unknown, options: CreateTaskOptions): Promise<T
|
|
|
206
207
|
const operations: TaskCreateOperation[] = [{
|
|
207
208
|
name: 'task:local', status: local.status, reasonCode: null
|
|
208
209
|
}];
|
|
210
|
+
if (local.status === 'no-op' && local.task.state !== 'active') {
|
|
211
|
+
return { status: 'no-op', changed: false, task: local.task, issue: null, operations, warnings: [], error: null };
|
|
212
|
+
}
|
|
209
213
|
let issue: TaskCreateResult['issue'] = null;
|
|
210
214
|
const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
211
215
|
operations.push(...platformOperations('platform-create', created.operations));
|
package/lib/task/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { isValidAgentInfraVersion, VERSION } from '../version.ts';
|
|
@@ -8,6 +9,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
8
9
|
import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.ts';
|
|
9
10
|
import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.ts';
|
|
10
11
|
import { CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, parseTaskQualification } from './qualification-audit.ts';
|
|
12
|
+
import { enumerateAllTaskDirs } from './resolve-ref.ts';
|
|
13
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
11
14
|
|
|
12
15
|
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'] as const;
|
|
13
16
|
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'] as const;
|
|
@@ -41,7 +44,7 @@ type TaskCreateCandidateV1 = Readonly<{
|
|
|
41
44
|
type LocalTaskCreateResult = Readonly<{
|
|
42
45
|
status: 'applied' | 'no-op';
|
|
43
46
|
changed: boolean;
|
|
44
|
-
task: { id: string; shortId: string };
|
|
47
|
+
task: { id: string; shortId: string | null; state: 'active' | 'blocked' | 'completed' | 'archive' };
|
|
45
48
|
}>;
|
|
46
49
|
|
|
47
50
|
type LocalTaskCreateOptions = Readonly<{
|
|
@@ -345,16 +348,32 @@ function ensureRealDirectory(directory: string, code: string): void {
|
|
|
345
348
|
fs.mkdirSync(directory, { mode: 0o700 });
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
351
|
+
function createLockRoot(project: string): string {
|
|
352
|
+
const root = path.join(os.homedir(), '.agent-infra');
|
|
353
|
+
const directories = [root, path.join(root, 'run'), path.join(root, 'run', project), path.join(root, 'run', project, 'task-create-locks')];
|
|
354
|
+
for (const directory of directories) {
|
|
355
|
+
if (fs.existsSync(directory)) {
|
|
356
|
+
assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
363
|
+
assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return directories.at(-1)!;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function withCreateLock<T>(repoRoot: string, project: string, operation: () => T): T {
|
|
370
|
+
const lockRoot = createLockRoot(project);
|
|
352
371
|
let callbackError: unknown;
|
|
353
372
|
try {
|
|
354
373
|
return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
|
|
355
374
|
try { return operation(); }
|
|
356
375
|
catch (error) { callbackError = error; throw error; }
|
|
357
|
-
}, { lockRoot });
|
|
376
|
+
}, { lockRoot, identityScope: `task-create\0${project}` });
|
|
358
377
|
} catch (error) {
|
|
359
378
|
if (callbackError !== undefined) throw callbackError;
|
|
360
379
|
if (error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY') {
|
|
@@ -365,35 +384,21 @@ function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: (
|
|
|
365
384
|
}
|
|
366
385
|
}
|
|
367
386
|
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
try { fs.renameSync(temporary, receiptPath); }
|
|
372
|
-
catch (error) {
|
|
373
|
-
try { fs.unlinkSync(temporary); } catch { /* best effort */ }
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function recoverPublishedTask(activeRoot: string, repoRoot: string, keyDigest: string, candidateDigest: string): { taskId: string; shortId: string } | null {
|
|
379
|
-
for (const name of fs.readdirSync(activeRoot)) {
|
|
380
|
-
if (!/^TASK-\d{8}-\d{6}$/.test(name)) continue;
|
|
381
|
-
const taskDir = path.join(activeRoot, name);
|
|
382
|
-
const stat = fs.lstatSync(taskDir);
|
|
383
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
384
|
-
const taskMd = path.join(taskDir, 'task.md');
|
|
385
|
-
if (!fs.existsSync(taskMd)) continue;
|
|
387
|
+
function recoverPublishedTask(repoRoot: string, keyDigest: string, candidateDigest: string): { taskId: string; shortId: string | null; state: 'active' | 'blocked' | 'completed' | 'archive' } | null {
|
|
388
|
+
for (const task of enumerateAllTaskDirs(repoRoot)) {
|
|
389
|
+
const taskMd = path.join(task.taskDir, 'task.md');
|
|
386
390
|
const taskStat = fs.lstatSync(taskMd);
|
|
387
|
-
if (!taskStat.isFile() || taskStat.isSymbolicLink())
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
if (
|
|
391
|
+
if (!taskStat.isFile() || taskStat.isSymbolicLink()) throw new Error('TASK_CREATE_TASK_INVALID');
|
|
392
|
+
const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMd, 'utf8'));
|
|
393
|
+
if (frontmatter.task_create_key_digest !== keyDigest) continue;
|
|
394
|
+
if (frontmatter.task_create_candidate_digest !== candidateDigest) {
|
|
391
395
|
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
392
396
|
}
|
|
393
|
-
const shortId =
|
|
394
|
-
?? mutateShortIdRegistry(repoRoot,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
const shortId = task.state === 'active'
|
|
398
|
+
? loadShortIdByTaskId(repoRoot).get(task.taskId) ?? mutateShortIdRegistry(repoRoot, task.taskId, 'alloc').shortId
|
|
399
|
+
: null;
|
|
400
|
+
if (task.state === 'active' && !shortId) throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
401
|
+
return { taskId: task.taskId, shortId, state: task.state };
|
|
397
402
|
}
|
|
398
403
|
return null;
|
|
399
404
|
}
|
|
@@ -414,29 +419,10 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
414
419
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
415
420
|
const candidateDigest = sha256(canonical);
|
|
416
421
|
const keyDigest = sha256(candidate.idempotencyKey);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return withCreateLock(repoRoot, workspaceRoot, () => {
|
|
420
|
-
if (fs.existsSync(receipts)) assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
421
|
-
const receiptPath = path.join(receipts, `${keyDigest}.json`);
|
|
422
|
-
if (fs.existsSync(receiptPath)) {
|
|
423
|
-
const stat = fs.lstatSync(receiptPath);
|
|
424
|
-
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
425
|
-
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')) as { candidateDigest?: string; taskId?: string; shortId?: string };
|
|
426
|
-
if (receipt.candidateDigest !== candidateDigest) throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
427
|
-
if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
|
|
428
|
-
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
429
|
-
}
|
|
430
|
-
return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
|
|
422
|
+
return withCreateLock(repoRoot, projectConfig.project, () => {
|
|
423
|
+
const recovered = recoverPublishedTask(repoRoot, keyDigest, candidateDigest);
|
|
434
424
|
if (recovered) {
|
|
435
|
-
|
|
436
|
-
writeReceipt(receiptPath, {
|
|
437
|
-
version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
|
|
438
|
-
});
|
|
439
|
-
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
|
|
425
|
+
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId, state: recovered.state } };
|
|
440
426
|
}
|
|
441
427
|
|
|
442
428
|
const base = options.now?.() ?? new Date();
|
|
@@ -455,29 +441,16 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
455
441
|
agentInfraVersion, keyDigest, candidateDigest
|
|
456
442
|
});
|
|
457
443
|
validateRenderedQualification(rendered, candidate);
|
|
458
|
-
ensureRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
459
444
|
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
460
445
|
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
461
446
|
fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
|
|
462
447
|
const destination = path.join(activeRoot, taskId);
|
|
463
448
|
fs.renameSync(temporary, destination);
|
|
464
449
|
let shortId: string;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
} catch (error) {
|
|
470
|
-
fs.rmSync(destination, { recursive: true, force: true });
|
|
471
|
-
throw error;
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
|
|
475
|
-
} catch (error) {
|
|
476
|
-
mutateShortIdRegistry(repoRoot, taskId, 'release');
|
|
477
|
-
fs.rmSync(destination, { recursive: true, force: true });
|
|
478
|
-
throw error;
|
|
479
|
-
}
|
|
480
|
-
return { status: 'applied', changed: true, task: { id: taskId, shortId } };
|
|
450
|
+
const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
|
|
451
|
+
if (!allocated.shortId) throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
452
|
+
shortId = allocated.shortId;
|
|
453
|
+
return { status: 'applied', changed: true, task: { id: taskId, shortId, state: 'active' } };
|
|
481
454
|
});
|
|
482
455
|
}
|
|
483
456
|
|