@fitlab-ai/agent-infra 0.11.5 → 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/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/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/pull-requests.js +29 -3
- package/dist/lib/platform/release-notes.js +7 -8
- 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/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/task-resolver.js +62 -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/resolve-ref.js +3 -3
- 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/lib/task/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- 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/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/platform/release-notes.ts +11 -12
- 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/lifecycle.ts +22 -13
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/task-resolver.ts +51 -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/resolve-ref.ts +3 -3
- 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/lib/task/write.ts +115 -1
- 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/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- 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/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/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-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- 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
|
@@ -5,17 +5,21 @@ import { extractSection, findSectionHeading, parseTable } from './sections.js';
|
|
|
5
5
|
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
|
|
6
6
|
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
|
|
7
7
|
const RECEIPT_EVENTS = new Set([
|
|
8
|
+
'analysis.completed',
|
|
9
|
+
'plan.completed',
|
|
8
10
|
'review-analysis.completed',
|
|
9
11
|
'review-plan.completed',
|
|
12
|
+
'code.completed',
|
|
10
13
|
'review-code.completed',
|
|
11
|
-
'code.completed'
|
|
12
14
|
]);
|
|
13
15
|
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
14
16
|
const RECEIPT_SHAPES = {
|
|
17
|
+
'analysis.completed': { output: 'analysis', input: 'review-analysis' },
|
|
18
|
+
'plan.completed': { output: 'plan', input: 'analysis' },
|
|
15
19
|
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
16
20
|
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
17
|
-
'
|
|
18
|
-
'code.completed': { output: 'code', input: '
|
|
21
|
+
'code.completed': { output: 'code', input: 'plan' },
|
|
22
|
+
'review-code.completed': { output: 'review-code', input: 'code' }
|
|
19
23
|
};
|
|
20
24
|
class ArtifactReceiptError extends Error {
|
|
21
25
|
code = 'ARTIFACT_RECEIPT_INVALID';
|
|
@@ -37,8 +41,16 @@ function validateReceiptShape(event, output, input) {
|
|
|
37
41
|
throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
38
42
|
const shape = RECEIPT_SHAPES[event];
|
|
39
43
|
const inputMatches = event === 'code.completed'
|
|
40
|
-
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
|
|
41
|
-
:
|
|
44
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan' || inputIdentity.family === 'review-code'
|
|
45
|
+
: event === 'plan.completed'
|
|
46
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan'
|
|
47
|
+
: event === 'analysis.completed'
|
|
48
|
+
? inputIdentity.family === 'review-analysis'
|
|
49
|
+
: event === 'review-plan.completed'
|
|
50
|
+
? inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis'
|
|
51
|
+
: event === 'review-code.completed'
|
|
52
|
+
? inputIdentity.family === 'code' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-plan'
|
|
53
|
+
: inputIdentity.family === shape.input;
|
|
42
54
|
if (outputIdentity.family !== shape.output || !inputMatches) {
|
|
43
55
|
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
44
56
|
}
|
|
@@ -52,7 +64,9 @@ function parseArtifactReceipts(content) {
|
|
|
52
64
|
table = parseTable(content, {
|
|
53
65
|
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
54
66
|
columns: [...RECEIPT_COLUMNS],
|
|
55
|
-
|
|
67
|
+
// A completed artifact can have multiple lifecycle inputs. Pair
|
|
68
|
+
// uniqueness is checked below because the table parser supports one key.
|
|
69
|
+
keyColumn: null
|
|
56
70
|
});
|
|
57
71
|
}
|
|
58
72
|
catch (error) {
|
|
@@ -84,29 +98,55 @@ function parseArtifactReceipts(content) {
|
|
|
84
98
|
completedAt
|
|
85
99
|
};
|
|
86
100
|
});
|
|
101
|
+
const edges = new Set();
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const edge = `${row.output}\0${row.input}`;
|
|
104
|
+
if (edges.has(edge))
|
|
105
|
+
throw new ArtifactReceiptError(`duplicate receipt edge '${row.output}' <- '${row.input}'`);
|
|
106
|
+
edges.add(edge);
|
|
107
|
+
}
|
|
87
108
|
return { present: true, rows };
|
|
88
109
|
}
|
|
110
|
+
function receiptsForOutput(content, output) {
|
|
111
|
+
return parseArtifactReceipts(content).rows.filter((row) => row.output === output);
|
|
112
|
+
}
|
|
89
113
|
function receiptForOutput(content, output) {
|
|
90
|
-
|
|
91
|
-
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
114
|
+
return receiptsForOutput(content, output)[0] ?? null;
|
|
92
115
|
}
|
|
93
116
|
function upsertArtifactReceipt(content, receipt) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
return upsertArtifactReceipts(content, [receipt]);
|
|
118
|
+
}
|
|
119
|
+
function upsertArtifactReceipts(content, receiptsToAdd) {
|
|
120
|
+
if (receiptsToAdd.length === 0)
|
|
121
|
+
throw new ArtifactReceiptError('at least one receipt is required');
|
|
122
|
+
const additions = new Map();
|
|
123
|
+
for (const receipt of receiptsToAdd) {
|
|
124
|
+
if (!RECEIPT_EVENTS.has(receipt.event))
|
|
125
|
+
throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
126
|
+
if (!receipt.output || !receipt.input)
|
|
127
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
128
|
+
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
129
|
+
if (!SHA256_RE.test(receipt.inputSha256))
|
|
130
|
+
throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
|
|
131
|
+
if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
|
|
132
|
+
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
133
|
+
}
|
|
134
|
+
const key = `${receipt.output}\0${receipt.input}`;
|
|
135
|
+
const duplicate = additions.get(key);
|
|
136
|
+
if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(receipt))
|
|
137
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' has conflicting evidence`);
|
|
138
|
+
additions.set(key, receipt);
|
|
103
139
|
}
|
|
104
140
|
const existing = parseArtifactReceipts(content).rows;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
for (const receipt of additions.values()) {
|
|
142
|
+
const previous = existing.find((row) => row.output === receipt.output && row.input === receipt.input);
|
|
143
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt))
|
|
144
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' already exists with different evidence`);
|
|
108
145
|
}
|
|
109
|
-
const rows =
|
|
146
|
+
const rows = [...existing];
|
|
147
|
+
for (const receipt of additions.values())
|
|
148
|
+
if (!rows.some((row) => row.output === receipt.output && row.input === receipt.input))
|
|
149
|
+
rows.push(receipt);
|
|
110
150
|
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
111
151
|
const body = [
|
|
112
152
|
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
@@ -115,5 +155,5 @@ function upsertArtifactReceipt(content, receipt) {
|
|
|
115
155
|
].join('\n');
|
|
116
156
|
return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
|
|
117
157
|
}
|
|
118
|
-
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
|
|
158
|
+
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, receiptsForOutput, sha256Bytes, sha256File, upsertArtifactReceipt, upsertArtifactReceipts };
|
|
119
159
|
//# sourceMappingURL=artifact-receipts.js.map
|
|
@@ -12,6 +12,8 @@ import { hasOpenLifecycleExecution } from './activity-log.js';
|
|
|
12
12
|
import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.js';
|
|
13
13
|
import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
|
|
14
14
|
import { parseImplementationInputs } from './implementation-inputs.js';
|
|
15
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
16
|
+
import { inspectReviewIdentity } from './review-identity.js';
|
|
15
17
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
16
18
|
const REASONS = new Set([
|
|
17
19
|
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
|
|
@@ -139,13 +141,15 @@ function canStart(action, facts, trigger) {
|
|
|
139
141
|
return allow('plan-review-approved');
|
|
140
142
|
}
|
|
141
143
|
if (action === 'review-code') {
|
|
142
|
-
return hasArtifact(facts, 'code') ?
|
|
144
|
+
return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
|
|
143
145
|
}
|
|
144
146
|
if (action === 'manual-validation' || action === 'validation-run') {
|
|
145
147
|
if (!hasArtifact(facts, 'review-code'))
|
|
146
148
|
return deny('CODE_REVIEW_REQUIRED');
|
|
147
|
-
if (!reviewMatchesLatest(facts, 'code', 'review-code'))
|
|
149
|
+
if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
|
|
148
150
|
return deny('CODE_REVIEW_NOT_LATEST');
|
|
151
|
+
if (!facts.reviewCodeIdentityValid)
|
|
152
|
+
return deny('CODE_REVIEW_IDENTITY_INVALID');
|
|
149
153
|
if (facts.reviews['review-code'] !== 'approved')
|
|
150
154
|
return deny('CODE_REVIEW_NOT_APPROVED');
|
|
151
155
|
if (facts.unresolvedLedger.code > 0)
|
|
@@ -309,7 +313,13 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
309
313
|
continue;
|
|
310
314
|
}
|
|
311
315
|
const audit = parseQualificationAudit(auditContent);
|
|
312
|
-
if (!audit.ok
|
|
316
|
+
if (!audit.ok) {
|
|
317
|
+
qualificationStaleArtifacts.push(name);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!audit.audit.present)
|
|
321
|
+
continue;
|
|
322
|
+
if (!audit.audit.snapshot) {
|
|
313
323
|
qualificationStaleArtifacts.push(name);
|
|
314
324
|
continue;
|
|
315
325
|
}
|
|
@@ -324,6 +334,7 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
324
334
|
}
|
|
325
335
|
const reviews = {};
|
|
326
336
|
const reviewedInputs = {};
|
|
337
|
+
let reviewCodeIdentityValid = false;
|
|
327
338
|
for (const family of ['review-analysis', 'review-plan', 'review-code']) {
|
|
328
339
|
const latest = latestArtifact(artifacts[family] ?? []);
|
|
329
340
|
if (!latest)
|
|
@@ -337,6 +348,12 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
337
348
|
&& (artifacts[expectedFamily] ?? []).includes(input)
|
|
338
349
|
&& artifactHashes[input] === receipt.inputSha256)
|
|
339
350
|
reviewedInputs[family] = input;
|
|
351
|
+
if (family === 'review-code') {
|
|
352
|
+
const identity = inspectReviewIdentity(taskDir, reviewContent);
|
|
353
|
+
reviewCodeIdentityValid = identity.status === 'matched'
|
|
354
|
+
&& hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
|
|
355
|
+
&& hasArtifactCompletionLog(content, latest, 'review-code.completed');
|
|
356
|
+
}
|
|
340
357
|
const parsed = parseReviewSummary(reviewContent);
|
|
341
358
|
if (!parsed.ok)
|
|
342
359
|
continue;
|
|
@@ -407,7 +424,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
407
424
|
qualificationStaleArtifacts,
|
|
408
425
|
pathState,
|
|
409
426
|
reworkClassificationRequired,
|
|
410
|
-
resolvedHumanDecisions
|
|
427
|
+
resolvedHumanDecisions,
|
|
428
|
+
reviewCodeIdentityValid
|
|
411
429
|
};
|
|
412
430
|
facts.recommendedAction = recommendNext(facts).action;
|
|
413
431
|
return { ok: true, facts };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { canonicalSemanticDigest } from './artifact-operations.js';
|
|
3
|
+
import { inspectActivityLog } from './activity-log.js';
|
|
4
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
|
+
import { parseArtifactName } from './artifact-name.js';
|
|
6
|
+
import { sha256File } from './artifact-receipts.js';
|
|
7
|
+
export function parseCompletionFacts(value) {
|
|
8
|
+
if (typeof value !== 'string')
|
|
9
|
+
return [];
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(value);
|
|
12
|
+
if (!Array.isArray(parsed))
|
|
13
|
+
return [];
|
|
14
|
+
return parsed.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
|
|
15
|
+
&& typeof fact.event === 'string'
|
|
16
|
+
&& typeof fact.output === 'string'
|
|
17
|
+
&& /^[a-f0-9]{64}$/u.test(fact.outputSha256)
|
|
18
|
+
&& /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
|
|
19
|
+
&& typeof fact.requestId === 'string'
|
|
20
|
+
&& typeof fact.result === 'string'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
|
|
27
|
+
try {
|
|
28
|
+
const frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
29
|
+
const output = artifactPath.split(/[\\/]/u).at(-1) ?? '';
|
|
30
|
+
const content = fs.readFileSync(artifactPath, 'utf8');
|
|
31
|
+
const outputSha256 = sha256File(artifactPath);
|
|
32
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
33
|
+
return parseCompletionFacts(frontmatter.completion_facts).some((fact) => fact.event === event && fact.output === output
|
|
34
|
+
&& fact.outputSha256 === outputSha256 && fact.semanticDigest === semanticDigest
|
|
35
|
+
&& fact.requestId.length > 0);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function hasArtifactCompletionLog(taskContent, artifact, event) {
|
|
42
|
+
const identity = parseArtifactName(artifact);
|
|
43
|
+
if (!identity)
|
|
44
|
+
return false;
|
|
45
|
+
const action = event === 'review-code.completed' ? 'Review Code'
|
|
46
|
+
: event === 'review-plan.completed' ? 'Review Plan'
|
|
47
|
+
: event === 'review-analysis.completed' ? 'Review Analysis' : null;
|
|
48
|
+
if (!action)
|
|
49
|
+
return false;
|
|
50
|
+
const { section, invalidEntries } = inspectActivityLog(taskContent);
|
|
51
|
+
if (!section || invalidEntries.length > 0)
|
|
52
|
+
return false;
|
|
53
|
+
const expected = `${action} (Round ${identity.round})`;
|
|
54
|
+
return section.entries.some((entry) => entry.step === expected && entry.note.includes(`→ ${artifact}`));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=completion-facts.js.map
|
|
@@ -48,9 +48,10 @@ export function parseTaskCreateResult(value) {
|
|
|
48
48
|
if (!['applied', 'no-op', 'degraded', 'failed', 'blocked'].includes(result.status)
|
|
49
49
|
|| typeof result.changed !== 'boolean'
|
|
50
50
|
|| !result.task || typeof result.task !== 'object' || Array.isArray(result.task)
|
|
51
|
-
|| !exactKeys(result.task, ['id', 'shortId'])
|
|
51
|
+
|| !exactKeys(result.task, ['id', 'shortId', 'state'])
|
|
52
52
|
|| !validTaskReference(result.task.id)
|
|
53
53
|
|| !validTaskReference(result.task.shortId)
|
|
54
|
+
|| ![null, 'active', 'blocked', 'completed', 'archive'].includes(result.task.state)
|
|
54
55
|
|| !Array.isArray(result.operations)
|
|
55
56
|
|| !result.operations.every((operation) => {
|
|
56
57
|
if (!operation || typeof operation !== 'object' || Array.isArray(operation))
|
|
@@ -93,7 +94,7 @@ export function taskCreateFailure(error, control) {
|
|
|
93
94
|
return {
|
|
94
95
|
status: error.retryable ? 'blocked' : 'failed',
|
|
95
96
|
changed: false,
|
|
96
|
-
task: { id: null, shortId: null },
|
|
97
|
+
task: { id: null, shortId: null, state: null },
|
|
97
98
|
issue: null,
|
|
98
99
|
operations: [],
|
|
99
100
|
warnings: [],
|
|
@@ -156,6 +157,9 @@ async function createTask(value, options) {
|
|
|
156
157
|
const operations = [{
|
|
157
158
|
name: 'task:local', status: local.status, reasonCode: null
|
|
158
159
|
}];
|
|
160
|
+
if (local.status === 'no-op' && local.task.state !== 'active') {
|
|
161
|
+
return { status: 'no-op', changed: false, task: local.task, issue: null, operations, warnings: [], error: null };
|
|
162
|
+
}
|
|
159
163
|
let issue = null;
|
|
160
164
|
const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
161
165
|
operations.push(...platformOperations('platform-create', created.operations));
|
package/dist/lib/task/create.js
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
|
import { isValidAgentInfraVersion, VERSION } from '../version.js';
|
|
5
6
|
import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
|
|
@@ -7,6 +8,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
7
8
|
import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.js';
|
|
8
9
|
import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.js';
|
|
9
10
|
import { CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, parseTaskQualification } from './qualification-audit.js';
|
|
11
|
+
import { enumerateAllTaskDirs } from './resolve-ref.js';
|
|
12
|
+
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
10
13
|
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'];
|
|
11
14
|
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'];
|
|
12
15
|
const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'];
|
|
@@ -292,10 +295,27 @@ function ensureRealDirectory(directory, code) {
|
|
|
292
295
|
}
|
|
293
296
|
fs.mkdirSync(directory, { mode: 0o700 });
|
|
294
297
|
}
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
function createLockRoot(project) {
|
|
299
|
+
const root = path.join(os.homedir(), '.agent-infra');
|
|
300
|
+
const directories = [root, path.join(root, 'run'), path.join(root, 'run', project), path.join(root, 'run', project, 'task-create-locks')];
|
|
301
|
+
for (const directory of directories) {
|
|
302
|
+
if (fs.existsSync(directory)) {
|
|
303
|
+
assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error.code !== 'EEXIST')
|
|
311
|
+
throw error;
|
|
312
|
+
assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return directories.at(-1);
|
|
316
|
+
}
|
|
317
|
+
function withCreateLock(repoRoot, project, operation) {
|
|
318
|
+
const lockRoot = createLockRoot(project);
|
|
299
319
|
let callbackError;
|
|
300
320
|
try {
|
|
301
321
|
return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
|
|
@@ -306,7 +326,7 @@ function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
|
306
326
|
callbackError = error;
|
|
307
327
|
throw error;
|
|
308
328
|
}
|
|
309
|
-
}, { lockRoot });
|
|
329
|
+
}, { lockRoot, identityScope: `task-create\0${project}` });
|
|
310
330
|
}
|
|
311
331
|
catch (error) {
|
|
312
332
|
if (callbackError !== undefined)
|
|
@@ -318,45 +338,24 @@ function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
|
318
338
|
throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
|
|
319
339
|
}
|
|
320
340
|
}
|
|
321
|
-
function
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
fs.renameSync(temporary, receiptPath);
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
try {
|
|
329
|
-
fs.unlinkSync(temporary);
|
|
330
|
-
}
|
|
331
|
-
catch { /* best effort */ }
|
|
332
|
-
throw error;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
function recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest) {
|
|
336
|
-
for (const name of fs.readdirSync(activeRoot)) {
|
|
337
|
-
if (!/^TASK-\d{8}-\d{6}$/.test(name))
|
|
338
|
-
continue;
|
|
339
|
-
const taskDir = path.join(activeRoot, name);
|
|
340
|
-
const stat = fs.lstatSync(taskDir);
|
|
341
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
342
|
-
continue;
|
|
343
|
-
const taskMd = path.join(taskDir, 'task.md');
|
|
344
|
-
if (!fs.existsSync(taskMd))
|
|
345
|
-
continue;
|
|
341
|
+
function recoverPublishedTask(repoRoot, keyDigest, candidateDigest) {
|
|
342
|
+
for (const task of enumerateAllTaskDirs(repoRoot)) {
|
|
343
|
+
const taskMd = path.join(task.taskDir, 'task.md');
|
|
346
344
|
const taskStat = fs.lstatSync(taskMd);
|
|
347
345
|
if (!taskStat.isFile() || taskStat.isSymbolicLink())
|
|
346
|
+
throw new Error('TASK_CREATE_TASK_INVALID');
|
|
347
|
+
const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMd, 'utf8'));
|
|
348
|
+
if (frontmatter.task_create_key_digest !== keyDigest)
|
|
348
349
|
continue;
|
|
349
|
-
|
|
350
|
-
if (!content.includes(`task_create_key_digest: ${keyDigest}`))
|
|
351
|
-
continue;
|
|
352
|
-
if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
|
|
350
|
+
if (frontmatter.task_create_candidate_digest !== candidateDigest) {
|
|
353
351
|
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
354
352
|
}
|
|
355
|
-
const shortId =
|
|
356
|
-
?? mutateShortIdRegistry(repoRoot,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
353
|
+
const shortId = task.state === 'active'
|
|
354
|
+
? loadShortIdByTaskId(repoRoot).get(task.taskId) ?? mutateShortIdRegistry(repoRoot, task.taskId, 'alloc').shortId
|
|
355
|
+
: null;
|
|
356
|
+
if (task.state === 'active' && !shortId)
|
|
357
|
+
throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
358
|
+
return { taskId: task.taskId, shortId, state: task.state };
|
|
360
359
|
}
|
|
361
360
|
return null;
|
|
362
361
|
}
|
|
@@ -376,30 +375,10 @@ function createLocalTask(value, options) {
|
|
|
376
375
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
377
376
|
const candidateDigest = sha256(canonical);
|
|
378
377
|
const keyDigest = sha256(candidate.idempotencyKey);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (fs.existsSync(receipts))
|
|
382
|
-
assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
383
|
-
const receiptPath = path.join(receipts, `${keyDigest}.json`);
|
|
384
|
-
if (fs.existsSync(receiptPath)) {
|
|
385
|
-
const stat = fs.lstatSync(receiptPath);
|
|
386
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
387
|
-
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
388
|
-
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
|
389
|
-
if (receipt.candidateDigest !== candidateDigest)
|
|
390
|
-
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
391
|
-
if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
|
|
392
|
-
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
393
|
-
}
|
|
394
|
-
return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
|
|
395
|
-
}
|
|
396
|
-
const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
|
|
378
|
+
return withCreateLock(repoRoot, projectConfig.project, () => {
|
|
379
|
+
const recovered = recoverPublishedTask(repoRoot, keyDigest, candidateDigest);
|
|
397
380
|
if (recovered) {
|
|
398
|
-
|
|
399
|
-
writeReceipt(receiptPath, {
|
|
400
|
-
version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
|
|
401
|
-
});
|
|
402
|
-
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
|
|
381
|
+
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId, state: recovered.state } };
|
|
403
382
|
}
|
|
404
383
|
const base = options.now?.() ?? new Date();
|
|
405
384
|
let taskDate = new Date(base.getTime());
|
|
@@ -418,32 +397,17 @@ function createLocalTask(value, options) {
|
|
|
418
397
|
agentInfraVersion, keyDigest, candidateDigest
|
|
419
398
|
});
|
|
420
399
|
validateRenderedQualification(rendered, candidate);
|
|
421
|
-
ensureRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
422
400
|
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
423
401
|
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
424
402
|
fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
|
|
425
403
|
const destination = path.join(activeRoot, taskId);
|
|
426
404
|
fs.renameSync(temporary, destination);
|
|
427
405
|
let shortId;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
fs.rmSync(destination, { recursive: true, force: true });
|
|
436
|
-
throw error;
|
|
437
|
-
}
|
|
438
|
-
try {
|
|
439
|
-
writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
mutateShortIdRegistry(repoRoot, taskId, 'release');
|
|
443
|
-
fs.rmSync(destination, { recursive: true, force: true });
|
|
444
|
-
throw error;
|
|
445
|
-
}
|
|
446
|
-
return { status: 'applied', changed: true, task: { id: taskId, shortId } };
|
|
406
|
+
const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
|
|
407
|
+
if (!allocated.shortId)
|
|
408
|
+
throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
409
|
+
shortId = allocated.shortId;
|
|
410
|
+
return { status: 'applied', changed: true, task: { id: taskId, shortId, state: 'active' } };
|
|
447
411
|
});
|
|
448
412
|
}
|
|
449
413
|
export { canonicalTaskCreateCandidate, createLocalTask, validateTaskCreateCandidate };
|