@fitlab-ai/agent-infra 0.11.9 → 0.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -6
- package/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +14 -25
- package/dist/lib/internal/task-operation-registry.js +6 -141
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +83 -16
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +99 -17
- package/dist/lib/sandbox/control/executor.js +12 -14
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +18 -3
- package/dist/lib/task/artifact-lifecycle.js +121 -63
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +59 -22
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -6
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +9 -275
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +43 -86
- package/dist/lib/task/frontmatter.js +25 -2
- package/dist/lib/task/ledger-intents.js +6 -8
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +29 -10
- package/dist/lib/task/platform-operation-recovery.js +109 -35
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +16 -26
- package/lib/internal/task-operation-registry.ts +6 -153
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +78 -15
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +107 -19
- package/lib/sandbox/control/executor.ts +13 -15
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/removal.ts +2 -10
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +19 -3
- package/lib/task/artifact-lifecycle.ts +102 -53
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +52 -26
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +9 -248
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +40 -82
- package/lib/task/frontmatter.ts +26 -2
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +47 -13
- package/lib/task/platform-operation-recovery.ts +102 -37
- package/lib/task/resolve-ref.ts +8 -3
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.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 +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/finalization-handoff.ts +0 -105
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -8,7 +8,7 @@ function selectArtifactDisposition(request) {
|
|
|
8
8
|
if (!request.latest) {
|
|
9
9
|
return {
|
|
10
10
|
disposition: 'create',
|
|
11
|
-
reasonCode:
|
|
11
|
+
reasonCode: 'no-completed-history',
|
|
12
12
|
artifact: request.next, writeRequired: true
|
|
13
13
|
};
|
|
14
14
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseArtifactName } from './artifact-name.js';
|
|
4
|
-
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
|
|
5
4
|
import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.js';
|
|
6
5
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
6
|
import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
@@ -14,9 +13,11 @@ import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
|
|
|
14
13
|
import { parseImplementationInputs } from './implementation-inputs.js';
|
|
15
14
|
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
16
15
|
import { inspectReviewIdentity } from './review-identity.js';
|
|
16
|
+
import { inspectArtifactDirectory } from './artifact-lifecycle.js';
|
|
17
|
+
import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.js';
|
|
17
18
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
18
19
|
const REASONS = new Set([
|
|
19
|
-
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
|
|
20
|
+
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'
|
|
20
21
|
]);
|
|
21
22
|
function deny(reasonCode, ...evidence) {
|
|
22
23
|
return { allowed: false, reasonCode, evidence };
|
|
@@ -39,6 +40,9 @@ function canStart(action, facts, trigger) {
|
|
|
39
40
|
return deny('TRIGGER_REQUEST_ID_REQUIRED');
|
|
40
41
|
if (!REASONS.has(trigger.reasonCode))
|
|
41
42
|
return deny('TRIGGER_REASON_INVALID');
|
|
43
|
+
if (trigger.reasonCode === 'manual-review-supplement' && (action !== 'review-code' || trigger.initiator !== 'human')) {
|
|
44
|
+
return deny('MANUAL_REVIEW_SUPPLEMENT_REQUIRES_HUMAN_REVIEW_CODE');
|
|
45
|
+
}
|
|
42
46
|
if (trigger.sourceArtifact && !trigger.sourceSha256)
|
|
43
47
|
return deny('SOURCE_ARTIFACT_HASH_REQUIRED');
|
|
44
48
|
if (trigger.sourceSha256 && !trigger.sourceArtifact)
|
|
@@ -52,8 +56,6 @@ function canStart(action, facts, trigger) {
|
|
|
52
56
|
}
|
|
53
57
|
if (facts.taskState !== 'active')
|
|
54
58
|
return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
|
|
55
|
-
if (invalidationBlocks(facts.invalidation))
|
|
56
|
-
return deny('INVALIDATION_INCOMPLETE');
|
|
57
59
|
if (facts.executionBusy)
|
|
58
60
|
return deny('EXECUTION_BUSY');
|
|
59
61
|
const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
|
|
@@ -102,7 +104,9 @@ function canStart(action, facts, trigger) {
|
|
|
102
104
|
return allow('analysis-review-approved');
|
|
103
105
|
}
|
|
104
106
|
if (action === 'review-plan') {
|
|
105
|
-
|
|
107
|
+
if (!hasArtifact(facts, 'plan'))
|
|
108
|
+
return deny('PLAN_ARTIFACT_REQUIRED');
|
|
109
|
+
return executionInputsMatchLatest(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_INPUT_STALE');
|
|
106
110
|
}
|
|
107
111
|
if (action === 'code') {
|
|
108
112
|
if (trigger.implementationInput) {
|
|
@@ -141,11 +145,15 @@ function canStart(action, facts, trigger) {
|
|
|
141
145
|
return allow('plan-review-approved');
|
|
142
146
|
}
|
|
143
147
|
if (action === 'review-code') {
|
|
148
|
+
if (hasArtifact(facts, 'code') && !executionInputsMatchLatest(facts, 'code'))
|
|
149
|
+
return deny('CODE_INPUT_STALE');
|
|
144
150
|
return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
|
|
145
151
|
}
|
|
146
152
|
if (action === 'manual-validation' || action === 'validation-run') {
|
|
147
153
|
if (!hasArtifact(facts, 'review-code'))
|
|
148
154
|
return deny('CODE_REVIEW_REQUIRED');
|
|
155
|
+
if (!executionInputsMatchLatest(facts, 'code'))
|
|
156
|
+
return deny('CODE_INPUT_STALE');
|
|
149
157
|
if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
|
|
150
158
|
return deny('CODE_REVIEW_NOT_LATEST');
|
|
151
159
|
if (!facts.reviewCodeIdentityValid)
|
|
@@ -159,13 +167,22 @@ function canStart(action, facts, trigger) {
|
|
|
159
167
|
return deny('CAPABILITY_ACTION_UNKNOWN');
|
|
160
168
|
}
|
|
161
169
|
function latestArtifact(names) {
|
|
162
|
-
return names.
|
|
163
|
-
.sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
|
|
170
|
+
return names.at(-1) ?? null;
|
|
164
171
|
}
|
|
165
172
|
function reviewMatchesLatest(facts, source, review) {
|
|
166
173
|
const latestSource = latestArtifact(facts.artifacts[source] ?? []);
|
|
167
174
|
return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
|
|
168
175
|
}
|
|
176
|
+
function executionInputsMatchLatest(facts, stage) {
|
|
177
|
+
if (!facts.pathState || facts.pathState.status !== 'valid' || facts.executionInputs === undefined)
|
|
178
|
+
return true;
|
|
179
|
+
return executionReceiptsMatchLatest(stage, facts.pathState, {
|
|
180
|
+
analysis: latestArtifact(facts.artifacts.analysis ?? []) ?? undefined,
|
|
181
|
+
'review-analysis': latestArtifact(facts.artifacts['review-analysis'] ?? []) ?? undefined,
|
|
182
|
+
plan: latestArtifact(facts.artifacts.plan ?? []) ?? undefined,
|
|
183
|
+
'review-plan': latestArtifact(facts.artifacts['review-plan'] ?? []) ?? undefined
|
|
184
|
+
}, facts.executionInputs[stage] ?? []);
|
|
185
|
+
}
|
|
169
186
|
function qualificationCandidateSnapshotMatches(audit, qualification) {
|
|
170
187
|
const current = new Map(qualification.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
171
188
|
return audit.candidateQualifications.length === current.size && audit.candidateQualifications.every((row) => {
|
|
@@ -201,6 +218,8 @@ function recommendNext(facts) {
|
|
|
201
218
|
}
|
|
202
219
|
if (stage === 'plan' && !hasArtifact(facts, stage))
|
|
203
220
|
return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
|
|
221
|
+
if (stage === 'plan' && !executionInputsMatchLatest(facts, stage))
|
|
222
|
+
return { action: stage, reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
|
|
204
223
|
if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
|
|
205
224
|
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
|
|
206
225
|
return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
|
|
@@ -211,6 +230,8 @@ function recommendNext(facts) {
|
|
|
211
230
|
}
|
|
212
231
|
if (stage === 'code' && !hasArtifact(facts, stage))
|
|
213
232
|
return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
|
|
233
|
+
if (stage === 'code' && !executionInputsMatchLatest(facts, stage))
|
|
234
|
+
return { action: stage, reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
|
|
214
235
|
if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
|
|
215
236
|
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
|
|
216
237
|
return facts.resolvedHumanDecisions.code === 'implementation'
|
|
@@ -234,6 +255,8 @@ function recommendNext(facts) {
|
|
|
234
255
|
}
|
|
235
256
|
if (!hasArtifact(facts, 'plan'))
|
|
236
257
|
return { action: 'plan', reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
|
|
258
|
+
if (!executionInputsMatchLatest(facts, 'plan'))
|
|
259
|
+
return { action: 'plan', reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
|
|
237
260
|
if (!hasArtifact(facts, 'review-plan') || !reviewMatchesLatest(facts, 'plan', 'review-plan')) {
|
|
238
261
|
return { action: 'review-plan', reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
|
|
239
262
|
}
|
|
@@ -242,6 +265,8 @@ function recommendNext(facts) {
|
|
|
242
265
|
}
|
|
243
266
|
if (!hasArtifact(facts, 'code'))
|
|
244
267
|
return { action: 'code', reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
|
|
268
|
+
if (!executionInputsMatchLatest(facts, 'code'))
|
|
269
|
+
return { action: 'code', reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
|
|
245
270
|
if (!hasArtifact(facts, 'review-code') || !reviewMatchesLatest(facts, 'code', 'review-code')) {
|
|
246
271
|
return { action: 'review-code', reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
|
|
247
272
|
}
|
|
@@ -253,9 +278,6 @@ function recommendNext(facts) {
|
|
|
253
278
|
function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBusy = false) {
|
|
254
279
|
try {
|
|
255
280
|
const metadata = parseTypedTaskFrontmatter(content);
|
|
256
|
-
const invalidation = parseInvalidationDocument(content);
|
|
257
|
-
if (!invalidation.ok)
|
|
258
|
-
return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalidation.message };
|
|
259
281
|
const rework = parseReworkIntentDocument(content);
|
|
260
282
|
if (!rework.ok)
|
|
261
283
|
return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: rework.message };
|
|
@@ -276,22 +298,38 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
276
298
|
const family = parseArtifactName(name)?.family;
|
|
277
299
|
return artifactFamilies.find((candidate) => candidate === family) ?? null;
|
|
278
300
|
};
|
|
279
|
-
const activeFiles = files.filter((name) => {
|
|
280
|
-
const family = familyFor(name);
|
|
281
|
-
return !family || !isArtifactInvalidated(invalidation.document, family, name);
|
|
282
|
-
});
|
|
283
301
|
const allArtifactHashes = {};
|
|
284
302
|
for (const name of files)
|
|
285
303
|
allArtifactHashes[name] = sha256File(path.join(taskDir, name));
|
|
286
304
|
const artifactHashes = {};
|
|
287
|
-
for (const name of
|
|
305
|
+
for (const name of files)
|
|
288
306
|
artifactHashes[name] = allArtifactHashes[name];
|
|
307
|
+
const completedByFamily = new Map();
|
|
308
|
+
for (const family of artifactFamilies) {
|
|
309
|
+
const inventory = inspectArtifactDirectory(taskDir, family);
|
|
310
|
+
if (inventory.status !== 'ready')
|
|
311
|
+
continue;
|
|
312
|
+
completedByFamily.set(family, inventory.completed.map((artifact) => artifact.name));
|
|
313
|
+
}
|
|
314
|
+
const completedFiles = new Set([...completedByFamily.values()].flat());
|
|
315
|
+
const activeFiles = [
|
|
316
|
+
...files.filter((name) => !familyFor(name)),
|
|
317
|
+
...artifactFamilies.flatMap((family) => completedByFamily.get(family) ?? [])
|
|
318
|
+
];
|
|
289
319
|
const artifacts = Object.fromEntries(artifactFamilies.map((family) => [
|
|
290
320
|
family, activeFiles.filter((name) => familyFor(name) === family)
|
|
291
321
|
]));
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
])
|
|
322
|
+
const receipts = parseArtifactReceipts(content).rows;
|
|
323
|
+
const executionInputs = {};
|
|
324
|
+
for (const family of ['plan', 'code']) {
|
|
325
|
+
const output = latestArtifact(artifacts[family] ?? []);
|
|
326
|
+
if (!output)
|
|
327
|
+
continue;
|
|
328
|
+
executionInputs[family] = receipts.filter((receipt) => receipt.output === output
|
|
329
|
+
&& receipt.event === `${family}.completed`
|
|
330
|
+
&& completedFiles.has(receipt.input)
|
|
331
|
+
&& artifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => receipt.input);
|
|
332
|
+
}
|
|
295
333
|
const qualification = parseTaskQualification(content);
|
|
296
334
|
if (!qualification.ok)
|
|
297
335
|
return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: qualification.message };
|
|
@@ -362,9 +400,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
362
400
|
reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
|
|
363
401
|
}
|
|
364
402
|
const reworkClassificationRequired = [];
|
|
365
|
-
const receipts = parseArtifactReceipts(content).rows;
|
|
366
403
|
for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']]) {
|
|
367
|
-
const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` &&
|
|
404
|
+
const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && completedFiles.has(receipt.output)
|
|
368
405
|
&& allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
|
|
369
406
|
const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
|
|
370
407
|
const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
|
|
@@ -415,8 +452,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
|
|
|
415
452
|
}
|
|
416
453
|
const facts = {
|
|
417
454
|
taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
|
|
418
|
-
|
|
419
|
-
|
|
455
|
+
reviewedInputs, executionInputs, artifactHashes,
|
|
456
|
+
reworkIntents: rework.intents,
|
|
420
457
|
unresolvedLedger,
|
|
421
458
|
executionBusy: executionBusy || hasOpenLifecycleExecution(content),
|
|
422
459
|
recommendedAction: null,
|
|
@@ -11,8 +11,6 @@ import { getOpenWorkflowWarnings, formatWorkflowWarningSummary } from '../workfl
|
|
|
11
11
|
import { parseActivityLog, pairEntries, startedBackedRows } from '../activity-log.js';
|
|
12
12
|
import { readRun } from '../orchestration.js';
|
|
13
13
|
import { statusCard } from '../../server/display.js';
|
|
14
|
-
import { invalidationBlocks, parseInvalidationDocument } from '../invalidation.js';
|
|
15
|
-
import { parseReworkIntentDocument } from '../rework-intent.js';
|
|
16
14
|
import { buildLifecycleFacts, recommendNext } from '../capabilities.js';
|
|
17
15
|
const USAGE = `Usage: ai task status [--task <ref> | -t <ref>]
|
|
18
16
|
|
|
@@ -320,11 +318,6 @@ function renderStatus(model) {
|
|
|
320
318
|
else {
|
|
321
319
|
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
322
320
|
}
|
|
323
|
-
lines.push('', 'Invalidation', ...renderPairs([
|
|
324
|
-
['status', model.invalidation?.status ?? 'unknown'],
|
|
325
|
-
['processed', `${model.invalidation?.processed ?? 0}/${model.invalidation?.total ?? 0}`],
|
|
326
|
-
['pending_intents', String(model.invalidation?.pendingIntents ?? 0)]
|
|
327
|
-
]));
|
|
328
321
|
lines.push('', 'Recommendation', ...renderPairs([
|
|
329
322
|
['path', model.lifecyclePath ?? DASH],
|
|
330
323
|
['action', model.recommendation ?? DASH],
|
|
@@ -374,10 +367,6 @@ function renderStatus(model) {
|
|
|
374
367
|
return lines;
|
|
375
368
|
}
|
|
376
369
|
function modelTone(model) {
|
|
377
|
-
if (model.invalidation?.status === 'invalid')
|
|
378
|
-
return 'danger';
|
|
379
|
-
if (model.invalidation?.status === 'pending' || model.invalidation?.status === 'in-progress')
|
|
380
|
-
return 'warning';
|
|
381
370
|
if (model.orchestration.status === 'paused')
|
|
382
371
|
return 'warning';
|
|
383
372
|
if (model.orchestration.status === 'running')
|
|
@@ -399,21 +388,6 @@ function buildFromResolved(input) {
|
|
|
399
388
|
const run = input.run ?? makeRunner(input.repoRoot);
|
|
400
389
|
const artifacts = enumerateArtifacts(input.taskDir);
|
|
401
390
|
const workflow = collectWorkflow(content, input.now);
|
|
402
|
-
const invalidation = parseInvalidationDocument(content);
|
|
403
|
-
const rework = parseReworkIntentDocument(content);
|
|
404
|
-
const pendingIntents = rework.ok ? rework.intents.filter((intent) => intent.status === 'pending').length : 0;
|
|
405
|
-
const invalidationInfo = !invalidation.ok
|
|
406
|
-
? { status: 'invalid', processed: 0, total: 0, pendingIntents }
|
|
407
|
-
: {
|
|
408
|
-
status: invalidation.present
|
|
409
|
-
? invalidationBlocks(invalidation.document)
|
|
410
|
-
? invalidation.document.operations.some((operation) => operation.status === 'in-progress') ? 'in-progress' : 'pending'
|
|
411
|
-
: 'completed'
|
|
412
|
-
: 'none',
|
|
413
|
-
processed: invalidation.document.operations.reduce((sum, operation) => sum + operation.processed, 0),
|
|
414
|
-
total: invalidation.document.operations.reduce((sum, operation) => sum + operation.total, 0),
|
|
415
|
-
pendingIntents
|
|
416
|
-
};
|
|
417
391
|
const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
|
|
418
392
|
const next = facts.ok ? recommendNext(facts.facts) : null;
|
|
419
393
|
const recommendation = next?.action ?? DASH;
|
|
@@ -427,7 +401,6 @@ function buildFromResolved(input) {
|
|
|
427
401
|
workflow,
|
|
428
402
|
runtime: collectRuntime(input.taskDir, workflow, run),
|
|
429
403
|
orchestration: collectOrchestration(input.taskDir),
|
|
430
|
-
invalidation: invalidationInfo,
|
|
431
404
|
recommendation,
|
|
432
405
|
recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
|
|
433
406
|
lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,
|
|
@@ -6,19 +6,34 @@ function gitText(repoRoot, args) {
|
|
|
6
6
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
7
7
|
}).trim();
|
|
8
8
|
}
|
|
9
|
+
function gitOutput(repoRoot, args) {
|
|
10
|
+
return execFileSync('git', [...args], {
|
|
11
|
+
cwd: repoRoot,
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function commitPaths(repoRoot, head, pathspec) {
|
|
17
|
+
const args = ['diff-tree', '--no-commit-id', '--name-only', '-r', '-z', head];
|
|
18
|
+
if (pathspec && pathspec.length > 0)
|
|
19
|
+
args.push('--', ...pathspec);
|
|
20
|
+
return gitOutput(repoRoot, args).split('\0').filter(Boolean).sort();
|
|
21
|
+
}
|
|
22
|
+
function canonicalMessage(message) {
|
|
23
|
+
return message.replace(/\r\n/g, '\n').replace(/\n+$/, '');
|
|
24
|
+
}
|
|
9
25
|
function checkpointCommitMatches(repoRoot, intent, head) {
|
|
10
26
|
try {
|
|
11
27
|
const parent = gitText(repoRoot, ['rev-parse', `${head}^`]);
|
|
12
28
|
const tree = gitText(repoRoot, ['rev-parse', `${head}^{tree}`]);
|
|
13
|
-
const message =
|
|
14
|
-
const changed =
|
|
15
|
-
|
|
16
|
-
.filter(Boolean)
|
|
17
|
-
.sort();
|
|
29
|
+
const message = canonicalMessage(gitOutput(repoRoot, ['show', '-s', '--format=%B', head]));
|
|
30
|
+
const changed = commitPaths(repoRoot, head);
|
|
31
|
+
const selected = commitPaths(repoRoot, head, intent.paths);
|
|
18
32
|
return parent === intent.expectedHead
|
|
19
33
|
&& tree === intent.expectedTree
|
|
20
|
-
&& message === intent.message
|
|
21
|
-
&&
|
|
34
|
+
&& message === canonicalMessage(intent.message)
|
|
35
|
+
&& changed.length > 0
|
|
36
|
+
&& JSON.stringify(changed) === JSON.stringify(selected);
|
|
22
37
|
}
|
|
23
38
|
catch {
|
|
24
39
|
return false;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const MAX_INTENT_SIZE = 1024 * 1024;
|
|
5
|
+
const TASK_ID_PATTERN = /^TASK-\d{8}-\d{6}$/;
|
|
6
|
+
function intentPath(taskDir) {
|
|
7
|
+
return path.join(taskDir, '.checkpoint-intent.json');
|
|
6
8
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
+
function assertTaskDirectory(taskDir, taskId) {
|
|
10
|
+
if (!TASK_ID_PATTERN.test(taskId) || path.basename(path.resolve(taskDir)) !== taskId) {
|
|
11
|
+
throw new Error('COMMIT_INTENT_INVALID: task directory does not match task ID');
|
|
12
|
+
}
|
|
13
|
+
const stat = fs.statSync(taskDir);
|
|
14
|
+
if (!stat.isDirectory())
|
|
15
|
+
throw new Error('COMMIT_INTENT_INVALID: task directory is not a directory');
|
|
9
16
|
}
|
|
10
17
|
function checkpointIntentDigest(identity) {
|
|
11
18
|
return createHash('sha256').update(JSON.stringify({
|
|
@@ -23,8 +30,13 @@ function isIntent(value) {
|
|
|
23
30
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
24
31
|
return false;
|
|
25
32
|
const intent = value;
|
|
33
|
+
const validState = ['prepared', 'committed', 'synced'].includes(String(intent.state));
|
|
34
|
+
const validHead = intent.committedHead === null
|
|
35
|
+
? intent.state === 'prepared'
|
|
36
|
+
: typeof intent.committedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.committedHead)
|
|
37
|
+
&& intent.state !== 'prepared';
|
|
26
38
|
return intent.version === 1
|
|
27
|
-
&& typeof intent.taskId === 'string' && intent.taskId
|
|
39
|
+
&& typeof intent.taskId === 'string' && TASK_ID_PATTERN.test(intent.taskId)
|
|
28
40
|
&& typeof intent.branch === 'string' && intent.branch.length > 0
|
|
29
41
|
&& intent.mode === 'local'
|
|
30
42
|
&& typeof intent.expectedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.expectedHead)
|
|
@@ -33,28 +45,57 @@ function isIntent(value) {
|
|
|
33
45
|
&& typeof intent.message === 'string' && intent.message.length > 0
|
|
34
46
|
&& Number.isSafeInteger(intent.round) && Number(intent.round) > 0
|
|
35
47
|
&& typeof intent.digest === 'string' && /^[a-f0-9]{64}$/.test(intent.digest)
|
|
36
|
-
&&
|
|
37
|
-
&& (intent.committedHead === null || (typeof intent.committedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.committedHead)))
|
|
48
|
+
&& validState && validHead
|
|
38
49
|
&& typeof intent.createdAt === 'string' && intent.createdAt.length > 0
|
|
39
50
|
&& typeof intent.updatedAt === 'string' && intent.updatedAt.length > 0
|
|
40
51
|
&& intent.digest === checkpointIntentDigest(intent);
|
|
41
52
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
function invalidIntent() {
|
|
54
|
+
throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
|
|
55
|
+
}
|
|
56
|
+
function readCheckpointIntent(taskDir, taskId) {
|
|
57
|
+
assertTaskDirectory(taskDir, taskId);
|
|
58
|
+
const target = intentPath(taskDir);
|
|
59
|
+
let before;
|
|
60
|
+
try {
|
|
61
|
+
before = fs.lstatSync(target);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code === 'ENOENT')
|
|
65
|
+
return null;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_INTENT_SIZE)
|
|
69
|
+
return invalidIntent();
|
|
70
|
+
let descriptor;
|
|
71
|
+
try {
|
|
72
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
73
|
+
descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
|
|
74
|
+
const opened = fs.fstatSync(descriptor);
|
|
75
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
76
|
+
|| opened.size !== before.size || opened.size > MAX_INTENT_SIZE)
|
|
77
|
+
return invalidIntent();
|
|
78
|
+
const value = JSON.parse(fs.readFileSync(descriptor, 'utf8'));
|
|
79
|
+
if (!isIntent(value) || value.taskId !== taskId)
|
|
80
|
+
return invalidIntent();
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof Error && error.message.startsWith('COMMIT_INTENT_INVALID:'))
|
|
85
|
+
throw error;
|
|
86
|
+
return invalidIntent();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (descriptor !== undefined)
|
|
90
|
+
fs.closeSync(descriptor);
|
|
91
|
+
}
|
|
50
92
|
}
|
|
51
|
-
function writeCheckpointIntent(
|
|
93
|
+
function writeCheckpointIntent(taskDir, value) {
|
|
94
|
+
assertTaskDirectory(taskDir, value.taskId);
|
|
52
95
|
if (!isIntent(value))
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const target = intentPath(repoRoot, value.taskId);
|
|
57
|
-
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
96
|
+
return invalidIntent();
|
|
97
|
+
const target = intentPath(taskDir);
|
|
98
|
+
const temporary = `${target}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
58
99
|
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
59
100
|
try {
|
|
60
101
|
fs.renameSync(temporary, target);
|
|
@@ -75,10 +116,10 @@ function updateCheckpointIntent(value, patch) {
|
|
|
75
116
|
throw new Error('COMMIT_INTENT_INVALID: committed intent requires a committed head');
|
|
76
117
|
return next;
|
|
77
118
|
}
|
|
78
|
-
function removeCheckpointIntent(
|
|
79
|
-
|
|
119
|
+
function removeCheckpointIntent(taskDir, taskId) {
|
|
120
|
+
assertTaskDirectory(taskDir, taskId);
|
|
80
121
|
try {
|
|
81
|
-
fs.unlinkSync(
|
|
122
|
+
fs.unlinkSync(intentPath(taskDir));
|
|
82
123
|
}
|
|
83
124
|
catch (error) {
|
|
84
125
|
if (error.code !== 'ENOENT')
|
|
@@ -96,5 +137,5 @@ function sameCheckpointIntent(left, right) {
|
|
|
96
137
|
&& left.round === right.round
|
|
97
138
|
&& JSON.stringify(left.paths) === JSON.stringify(right.paths);
|
|
98
139
|
}
|
|
99
|
-
export { checkpointIntentDigest, intentPath,
|
|
140
|
+
export { checkpointIntentDigest, intentPath, isIntent, readCheckpointIntent, removeCheckpointIntent, sameCheckpointIntent, updateCheckpointIntent, writeCheckpointIntent };
|
|
100
141
|
//# sourceMappingURL=commit-intent.js.map
|
|
@@ -206,7 +206,7 @@ function executeUnlocked(input, task, mode) {
|
|
|
206
206
|
? (() => {
|
|
207
207
|
const identity = checkpointIdentity(input, task, delivery);
|
|
208
208
|
try {
|
|
209
|
-
pendingIntent = readCheckpointIntent(task.
|
|
209
|
+
pendingIntent = readCheckpointIntent(task.taskDir, task.taskId);
|
|
210
210
|
}
|
|
211
211
|
catch (error) {
|
|
212
212
|
return {
|
|
@@ -215,6 +215,11 @@ function executeUnlocked(input, task, mode) {
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
|
|
218
|
+
if (pendingIntent.state !== 'prepared')
|
|
219
|
+
return {
|
|
220
|
+
status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
|
|
221
|
+
error: { code: 'COMMIT_INTENT_INVALID', message: 'A committed checkpoint intent must be recovered with its original request' }
|
|
222
|
+
};
|
|
218
223
|
const timestamp = checkpointTimestamp();
|
|
219
224
|
pendingIntent = {
|
|
220
225
|
version: 1, ...identity,
|
|
@@ -222,7 +227,7 @@ function executeUnlocked(input, task, mode) {
|
|
|
222
227
|
state: 'prepared', committedHead: null, createdAt: timestamp, updatedAt: timestamp
|
|
223
228
|
};
|
|
224
229
|
try {
|
|
225
|
-
writeCheckpointIntent(task.
|
|
230
|
+
writeCheckpointIntent(task.taskDir, pendingIntent);
|
|
226
231
|
}
|
|
227
232
|
catch (error) {
|
|
228
233
|
return {
|
|
@@ -231,6 +236,12 @@ function executeUnlocked(input, task, mode) {
|
|
|
231
236
|
};
|
|
232
237
|
}
|
|
233
238
|
}
|
|
239
|
+
if (pendingIntent && pendingIntent.state !== 'prepared'
|
|
240
|
+
&& !checkpointCommitMatches(task.repoRoot, pendingIntent, pendingIntent.committedHead))
|
|
241
|
+
return {
|
|
242
|
+
status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
|
|
243
|
+
error: { code: 'COMMIT_INTENT_INVALID', message: 'Committed checkpoint intent does not match its Git commit' }
|
|
244
|
+
};
|
|
234
245
|
if (!pendingIntent) {
|
|
235
246
|
const timestamp = checkpointTimestamp();
|
|
236
247
|
pendingIntent = {
|
|
@@ -238,7 +249,7 @@ function executeUnlocked(input, task, mode) {
|
|
|
238
249
|
committedHead: null, createdAt: timestamp, updatedAt: timestamp
|
|
239
250
|
};
|
|
240
251
|
try {
|
|
241
|
-
writeCheckpointIntent(task.
|
|
252
|
+
writeCheckpointIntent(task.taskDir, pendingIntent);
|
|
242
253
|
}
|
|
243
254
|
catch (error) {
|
|
244
255
|
return {
|
|
@@ -264,7 +275,7 @@ function executeUnlocked(input, task, mode) {
|
|
|
264
275
|
pendingIntent = updateCheckpointIntent(pendingIntent, {
|
|
265
276
|
state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
266
277
|
});
|
|
267
|
-
writeCheckpointIntent(task.
|
|
278
|
+
writeCheckpointIntent(task.taskDir, pendingIntent);
|
|
268
279
|
return {
|
|
269
280
|
status: 'no-op', changed: false, snapshot: inspected.snapshot,
|
|
270
281
|
operations: [{ name: 'commit', status: 'no-op' }], error: null
|
|
@@ -284,10 +295,10 @@ function executeUnlocked(input, task, mode) {
|
|
|
284
295
|
pendingIntent = updateCheckpointIntent(pendingIntent, {
|
|
285
296
|
state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
286
297
|
});
|
|
287
|
-
writeCheckpointIntent(task.
|
|
298
|
+
writeCheckpointIntent(task.taskDir, pendingIntent);
|
|
288
299
|
}
|
|
289
300
|
else if (created.status === 'no-op') {
|
|
290
|
-
removeCheckpointIntent(task.
|
|
301
|
+
removeCheckpointIntent(task.taskDir, task.taskId);
|
|
291
302
|
pendingIntent = null;
|
|
292
303
|
}
|
|
293
304
|
return created;
|
|
@@ -377,8 +388,8 @@ function executeUnlocked(input, task, mode) {
|
|
|
377
388
|
const synced = updateCheckpointIntent(pendingIntent, {
|
|
378
389
|
state: 'synced', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
379
390
|
});
|
|
380
|
-
writeCheckpointIntent(task.
|
|
381
|
-
removeCheckpointIntent(task.
|
|
391
|
+
writeCheckpointIntent(task.taskDir, synced);
|
|
392
|
+
removeCheckpointIntent(task.taskDir, task.taskId);
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
395
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { canonicalSemanticDigest } from './artifact-operations.js';
|
|
3
|
-
import { inspectActivityLog } from './activity-log.js';
|
|
3
|
+
import { inspectActivityLog, pairEntries } from './activity-log.js';
|
|
4
4
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
5
|
import { parseArtifactName } from './artifact-name.js';
|
|
6
6
|
import { sha256File } from './artifact-receipts.js';
|
|
@@ -39,18 +39,40 @@ export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
export function hasArtifactCompletionLog(taskContent, artifact, event) {
|
|
42
|
+
return artifactCompletionOrder(taskContent, artifact, event) !== null;
|
|
43
|
+
}
|
|
44
|
+
/** Returns the stable Activity Log position of a paired completion row. */
|
|
45
|
+
export function artifactCompletionOrder(taskContent, artifact, event) {
|
|
42
46
|
const identity = parseArtifactName(artifact);
|
|
43
47
|
if (!identity)
|
|
44
|
-
return
|
|
45
|
-
const
|
|
46
|
-
: event === 'review-
|
|
47
|
-
: event === '
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
return null;
|
|
49
|
+
const family = event === 'analysis.completed' || event === 'analyze.completed' ? 'analysis'
|
|
50
|
+
: event === 'review-analysis.completed' ? 'review-analysis'
|
|
51
|
+
: event === 'plan.completed' ? 'plan'
|
|
52
|
+
: event === 'review-plan.completed' ? 'review-plan'
|
|
53
|
+
: event === 'code.completed' ? 'code'
|
|
54
|
+
: event === 'review-code.completed' ? 'review-code' : null;
|
|
55
|
+
if (!family || family !== identity.family)
|
|
56
|
+
return null;
|
|
57
|
+
const action = family === 'analysis' ? 'Analyze Task'
|
|
58
|
+
: family === 'review-analysis' ? 'Review Analysis'
|
|
59
|
+
: family === 'plan' ? 'Plan Task'
|
|
60
|
+
: family === 'review-plan' ? 'Review Plan'
|
|
61
|
+
: family === 'code' ? 'Code Task' : 'Review Code';
|
|
50
62
|
const { section, invalidEntries } = inspectActivityLog(taskContent);
|
|
51
63
|
if (!section || invalidEntries.length > 0)
|
|
52
|
-
return
|
|
53
|
-
const
|
|
54
|
-
|
|
64
|
+
return null;
|
|
65
|
+
const ordered = section.entries.map((entry, sourceOrder) => ({ entry, sourceOrder }))
|
|
66
|
+
.sort((left, right) => Date.parse(left.entry.time.replace(' ', 'T')) - Date.parse(right.entry.time.replace(' ', 'T')) || left.sourceOrder - right.sourceOrder);
|
|
67
|
+
const entries = ordered.map(({ entry }) => entry);
|
|
68
|
+
const roundPattern = new RegExp(`^${action} \\(Round ${identity.round}(?:, [^)]+)?\\)$`);
|
|
69
|
+
const row = pairEntries(entries).find((candidate) => candidate.started !== ''
|
|
70
|
+
&& candidate.done !== '' && roundPattern.test(candidate.step)
|
|
71
|
+
&& candidate.note.includes(`→ ${artifact}`));
|
|
72
|
+
if (!row)
|
|
73
|
+
return null;
|
|
74
|
+
const doneIndex = ordered.findIndex(({ entry }) => entry.time === row.done
|
|
75
|
+
&& entry.step === row.step && entry.note === row.note);
|
|
76
|
+
return doneIndex < 0 ? null : doneIndex;
|
|
55
77
|
}
|
|
56
78
|
//# sourceMappingURL=completion-facts.js.map
|
|
@@ -4,7 +4,7 @@ import { normalizeAgentToken } from '../agent-clients/tokens.js';
|
|
|
4
4
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
5
5
|
import { getAgentClientAdapter } from '../agent-clients/registry.js';
|
|
6
6
|
import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, awaitOrchestrationDelegationActivation, beginOrResumeOrchestration, dispatchOrchestrationDelegation, OrchestrationStateError, pauseOrchestration, prepareOrchestrationDelegation, recoverPreparedOrchestrationDelegation, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusOrchestration } from './orchestration.js';
|
|
7
|
-
import { applyTaskFinalization
|
|
7
|
+
import { applyTaskFinalization } from './finalization.js';
|
|
8
8
|
import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
|
|
9
9
|
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
|
|
10
10
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
@@ -274,7 +274,6 @@ export function createSandboxExecutorExecutionContext(params) {
|
|
|
274
274
|
generation: params.generation,
|
|
275
275
|
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
276
276
|
requestId: params.requestId,
|
|
277
|
-
handoffDirectory: absolute('handoffDirectory', params.handoffDirectory ?? path.join(params.worktreeRoot, '.agent-infra-handoff')),
|
|
278
277
|
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
279
278
|
...(params.lifecycleRecoveryAttestation === undefined ? {} : {
|
|
280
279
|
lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
|
|
@@ -302,7 +301,6 @@ export function assertTaskControlExecutionContext(context) {
|
|
|
302
301
|
absolute('worktreeRoot', context.worktreeRoot);
|
|
303
302
|
absolute('runtimeDir', context.runtimeDir);
|
|
304
303
|
absolute('manifestPath', context.manifestPath);
|
|
305
|
-
absolute('handoffDirectory', context.handoffDirectory);
|
|
306
304
|
if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
|
|
307
305
|
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
308
306
|
}
|
|
@@ -434,12 +432,10 @@ export function dispatchTaskControlOperation(context, operation) {
|
|
|
434
432
|
}
|
|
435
433
|
if (operation.family === 'task-finalization') {
|
|
436
434
|
operationTaskId(context, operation.request.taskRef);
|
|
437
|
-
|
|
438
|
-
return finalize(operation.request, {
|
|
435
|
+
return applyTaskFinalization(operation.request, {
|
|
439
436
|
repoRoot: context.repoRoot,
|
|
440
437
|
...(context.source === 'sandbox-executor' ? {
|
|
441
438
|
controlBinding: { generation: context.generation, requestId: context.requestId },
|
|
442
|
-
handoffDirectory: context.handoffDirectory,
|
|
443
439
|
manifestPath: context.manifestPath
|
|
444
440
|
} : {}),
|
|
445
441
|
preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
|