@fitlab-ai/agent-infra 0.11.4 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.js';
|
|
2
|
-
import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
|
|
3
2
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
3
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
5
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
6
5
|
const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-status|list> [intent flags] [--dry-run]\n`;
|
|
7
6
|
const FLAGS = {
|
|
8
7
|
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
9
|
-
'--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
|
|
10
|
-
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
8
|
+
'--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
|
|
11
9
|
};
|
|
12
10
|
function usageFailure(message) {
|
|
13
11
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'WARNING_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -66,9 +64,6 @@ async function taskWarning(args = []) {
|
|
|
66
64
|
};
|
|
67
65
|
const optional = kind === 'list' ? ['status'] : [];
|
|
68
66
|
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...optional]);
|
|
69
|
-
allowed.add('overrideTicket');
|
|
70
|
-
allowed.add('overrideTarget');
|
|
71
|
-
allowed.add('overrideScope');
|
|
72
67
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
73
68
|
const missing = required[kind].find((key) => values[key] === undefined);
|
|
74
69
|
if (unexpected) {
|
|
@@ -79,43 +74,14 @@ async function taskWarning(args = []) {
|
|
|
79
74
|
usageFailure(`${kind} requires '${missing}'`);
|
|
80
75
|
return;
|
|
81
76
|
}
|
|
82
|
-
const dryRunConflict = overrideDryRunConflict(values);
|
|
83
|
-
if (dryRunConflict) {
|
|
84
|
-
usageFailure(dryRunConflict.message);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
77
|
const resolved = resolveTaskRef(taskRef);
|
|
88
78
|
if (!resolved.ok) {
|
|
89
79
|
usageFailure(resolved.message);
|
|
90
80
|
return;
|
|
91
81
|
}
|
|
92
82
|
let result;
|
|
93
|
-
let humanOverride = null;
|
|
94
83
|
try {
|
|
95
|
-
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () =>
|
|
96
|
-
let current = applyWorkflowWarningIntent(values);
|
|
97
|
-
if (current.status !== 'failed' || !values.overrideTicket)
|
|
98
|
-
return current;
|
|
99
|
-
if (!values.overrideTarget || !values.overrideScope) {
|
|
100
|
-
usageFailure('override ticket requires target and scope');
|
|
101
|
-
return current;
|
|
102
|
-
}
|
|
103
|
-
const consumed = await consumeHumanOverride({
|
|
104
|
-
taskRef, ticketId: String(values.overrideTicket),
|
|
105
|
-
failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
|
|
106
|
-
target: String(values.overrideTarget), scope: String(values.overrideScope)
|
|
107
|
-
}, {
|
|
108
|
-
effectExecutor: (capability) => {
|
|
109
|
-
const retried = applyWorkflowWarningIntent(values, { manualOverride: capability });
|
|
110
|
-
current = retried;
|
|
111
|
-
return retried.status === 'failed' || retried.status === 'planned'
|
|
112
|
-
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no workflow warning effect was committed' : `${retried.error?.code ?? 'WARNING_FAILED'}: ${retried.error?.message ?? 'manual warning effect failed'}` }
|
|
113
|
-
: null;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
humanOverride = consumed;
|
|
117
|
-
return current;
|
|
118
|
-
});
|
|
84
|
+
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => applyWorkflowWarningIntent(values));
|
|
119
85
|
}
|
|
120
86
|
catch (error) {
|
|
121
87
|
if (!(error instanceof TaskExecutionLockError))
|
|
@@ -124,10 +90,8 @@ async function taskWarning(args = []) {
|
|
|
124
90
|
process.exitCode = 1;
|
|
125
91
|
return;
|
|
126
92
|
}
|
|
127
|
-
process.stdout.write(`${JSON.stringify(
|
|
128
|
-
|
|
129
|
-
humanOverride.status === 'failed');
|
|
130
|
-
if (result.status === 'failed' || overrideFailed)
|
|
93
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
94
|
+
if (result.status === 'failed')
|
|
131
95
|
process.exitCode = 1;
|
|
132
96
|
}
|
|
133
97
|
export { taskWarning };
|
|
@@ -2,13 +2,12 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { enumerateArtifacts } from '../task/artifacts.js';
|
|
4
4
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
5
|
-
import { renderHumanOverrideAudit } from '../task/human-override.js';
|
|
6
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
7
6
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
8
7
|
import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
|
|
9
8
|
import { resolvePlatformProviderContext } from './context.js';
|
|
10
9
|
import { normalizeCommentContent } from './issue-comments.js';
|
|
11
|
-
import { CONTROL_MARKER_PATTERN,
|
|
10
|
+
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
|
|
12
11
|
import { platformResult } from './types.js';
|
|
13
12
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
|
|
14
13
|
import { resourceIdentityEquals } from './resource-identity.js';
|
|
@@ -18,23 +17,6 @@ import { readManualValidationCompletion } from '../task/manual-validation-comple
|
|
|
18
17
|
function summaryMarker(taskId) {
|
|
19
18
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
20
19
|
}
|
|
21
|
-
function escapeSummaryAuditText(value) {
|
|
22
|
-
return escapeHtmlText(value).replace(/\r\n?/g, '\n');
|
|
23
|
-
}
|
|
24
|
-
function renderSummaryAuditLine(value) {
|
|
25
|
-
const safe = escapeSummaryAuditText(value);
|
|
26
|
-
const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
|
|
27
|
-
const delimiter = '`'.repeat(longestRun + 1);
|
|
28
|
-
return `${delimiter} ${safe} ${delimiter}`;
|
|
29
|
-
}
|
|
30
|
-
function renderSafeHumanOverrideAudit(value) {
|
|
31
|
-
const normalized = value.trim().replace(/\r\n?/g, '\n');
|
|
32
|
-
if (!normalized)
|
|
33
|
-
return '';
|
|
34
|
-
const [heading, ...rows] = normalized.split('\n');
|
|
35
|
-
const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
|
|
36
|
-
return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
|
|
37
|
-
}
|
|
38
20
|
function isSafeSummaryEnvelope(value, taskId) {
|
|
39
21
|
return value.includes(summaryMarker(taskId))
|
|
40
22
|
&& (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
|
|
@@ -71,9 +53,8 @@ function warningResultForPrimary(primaryResult) {
|
|
|
71
53
|
function platformError(error) {
|
|
72
54
|
return { code: error.code, message: error.message, retryable: error.retryable ?? false };
|
|
73
55
|
}
|
|
74
|
-
function buildPullRequestSummary(taskId, body, headSha
|
|
75
|
-
|
|
76
|
-
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
|
|
56
|
+
function buildPullRequestSummary(taskId, body, headSha) {
|
|
57
|
+
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
|
|
77
58
|
}
|
|
78
59
|
function reconcileSummaryComment(comments, taskId, desired) {
|
|
79
60
|
const marker = summaryMarker(taskId);
|
|
@@ -586,7 +567,7 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
586
567
|
const renderedBody = manual?.phase === 'final'
|
|
587
568
|
? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
|
|
588
569
|
: replaced.value;
|
|
589
|
-
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha
|
|
570
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
|
|
590
571
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId))
|
|
591
572
|
return fail('failed', context, {
|
|
592
573
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { resolveBranchWorktree } from "../git/branch-worktree.js";
|
|
6
7
|
import { resolvePlatformProviderContext } from "./context.js";
|
|
7
8
|
import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.js";
|
|
8
9
|
import { requirementSectionAnchors } from "./issues.js";
|
|
@@ -807,7 +808,10 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
|
|
|
807
808
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
808
809
|
}
|
|
809
810
|
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
810
|
-
const gitCwd = branch ?
|
|
811
|
+
const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
|
|
812
|
+
if (!gitCwd) {
|
|
813
|
+
return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
|
|
814
|
+
}
|
|
811
815
|
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
812
816
|
if (!changedFilesResult.ok) {
|
|
813
817
|
return { ...changedFilesResult, type: "network_error" };
|
|
@@ -831,26 +835,6 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
|
|
|
831
835
|
}
|
|
832
836
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
833
837
|
}
|
|
834
|
-
function worktreeForBranch(repositoryRoot, branch) {
|
|
835
|
-
try {
|
|
836
|
-
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
837
|
-
cwd: repositoryRoot,
|
|
838
|
-
encoding: "utf8",
|
|
839
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
840
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
841
|
-
const expectedRef = `refs/heads/${branch}`;
|
|
842
|
-
for (const record of records) {
|
|
843
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
844
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
845
|
-
if (worktree && branchRef === expectedRef)
|
|
846
|
-
return worktree;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
catch {
|
|
850
|
-
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
851
|
-
}
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
838
|
function loadInLabelMapping(shared) {
|
|
855
839
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
856
840
|
if (!fs.existsSync(configPath)) {
|
|
@@ -888,36 +872,15 @@ function resolvePrHeadSha(context) {
|
|
|
888
872
|
if (!branch) {
|
|
889
873
|
return fallback();
|
|
890
874
|
}
|
|
891
|
-
const
|
|
892
|
-
if (!worktreeList.ok) {
|
|
893
|
-
return fallback();
|
|
894
|
-
}
|
|
895
|
-
const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
|
|
875
|
+
const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
|
|
896
876
|
if (!matchedWorktree) {
|
|
897
|
-
return
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
return headInWorktree;
|
|
904
|
-
}
|
|
905
|
-
function findWorktreeForBranch(porcelainOutput, branch) {
|
|
906
|
-
let currentWorktree = "";
|
|
907
|
-
for (const rawLine of String(porcelainOutput || "").split("\n")) {
|
|
908
|
-
const line = rawLine.trimEnd();
|
|
909
|
-
if (line.startsWith("worktree ")) {
|
|
910
|
-
currentWorktree = line.slice("worktree ".length).trim();
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
914
|
-
const usedBranch = line.slice("branch refs/heads/".length).trim();
|
|
915
|
-
if (usedBranch === branch && currentWorktree) {
|
|
916
|
-
return currentWorktree;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
877
|
+
return {
|
|
878
|
+
ok: false,
|
|
879
|
+
type: "check_failed",
|
|
880
|
+
message: `Task branch '${branch}' is not checked out in a usable worktree`
|
|
881
|
+
};
|
|
919
882
|
}
|
|
920
|
-
return
|
|
883
|
+
return gitText(["rev-parse", "HEAD"], matchedWorktree);
|
|
921
884
|
}
|
|
922
885
|
function interpolate(template, taskDir, artifactFile) {
|
|
923
886
|
const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
|
|
@@ -340,15 +340,18 @@ export function requestSandboxTaskFinalization(params) {
|
|
|
340
340
|
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
341
341
|
const auth = authority(params);
|
|
342
342
|
const issuedAt = Date.now();
|
|
343
|
+
const id = randomUUID();
|
|
344
|
+
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
343
345
|
const request = {
|
|
344
346
|
version: 3,
|
|
345
|
-
id
|
|
347
|
+
id,
|
|
346
348
|
...auth,
|
|
347
349
|
issuedAt,
|
|
348
350
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
349
351
|
family: 'task-finalization',
|
|
350
352
|
operation: 'complete',
|
|
351
353
|
agent,
|
|
354
|
+
handoffSha256,
|
|
352
355
|
args: [],
|
|
353
356
|
controllerProcess: null,
|
|
354
357
|
controllerProof: null
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
6
7
|
import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
|
|
7
8
|
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
@@ -13,6 +14,10 @@ import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, pa
|
|
|
13
14
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
|
|
14
15
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
15
16
|
import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
|
|
17
|
+
import { shareBranchDir } from '../constants.js';
|
|
18
|
+
function handoffDirectory(manifest) {
|
|
19
|
+
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
20
|
+
}
|
|
16
21
|
function sameBrokerOwner(left, right) {
|
|
17
22
|
return left.version === right.version
|
|
18
23
|
&& left.pid === right.pid
|
|
@@ -295,9 +300,10 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
if (request.family === 'task-finalization') {
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
303
|
+
const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
|
|
304
|
+
if (parsed.family !== 'task-finalization')
|
|
300
305
|
throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
306
|
+
const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
|
|
301
307
|
const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
|
|
302
308
|
repoRoot: manifest.repoRoot,
|
|
303
309
|
worktreeRoot: manifest.worktreeRoot,
|
|
@@ -305,7 +311,8 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
305
311
|
taskId: manifest.taskId,
|
|
306
312
|
generation: manifest.generation,
|
|
307
313
|
manifestPath,
|
|
308
|
-
requestId: request.id
|
|
314
|
+
requestId: request.id,
|
|
315
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
309
316
|
}), operation);
|
|
310
317
|
return finalizationResult(await result);
|
|
311
318
|
}
|
|
@@ -326,7 +333,8 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
326
333
|
taskId: manifest.taskId,
|
|
327
334
|
generation: manifest.generation,
|
|
328
335
|
manifestPath,
|
|
329
|
-
requestId: request.id
|
|
336
|
+
requestId: request.id,
|
|
337
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
330
338
|
});
|
|
331
339
|
const format = (value) => {
|
|
332
340
|
const result = value;
|
|
@@ -158,11 +158,12 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
158
158
|
return request;
|
|
159
159
|
}
|
|
160
160
|
if (request.family === 'task-finalization') {
|
|
161
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
161
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
162
162
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
163
163
|
|| request.operation !== 'complete'
|
|
164
164
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
165
165
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
166
|
+
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
166
167
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
167
168
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
168
169
|
}
|
|
@@ -17,6 +17,7 @@ import { acquireSandboxResourceLock } from './control/native-file-lock.js';
|
|
|
17
17
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.js';
|
|
18
18
|
import { getProcessStartTime } from '../server/process-state.js';
|
|
19
19
|
import { releaseStaleShortIdRegistry } from '../task/short-id.js';
|
|
20
|
+
import { withRepositoryMutationLock } from '../task/task-execution-lock.js';
|
|
20
21
|
import { fetchSandboxRows } from './commands/list-running.js';
|
|
21
22
|
import { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup } from '../task/intermediate-cleanup.js';
|
|
22
23
|
import { createSandboxControlBindingEvidence } from './task-cleanup.js';
|
|
@@ -670,7 +671,7 @@ async function runRmOneUnderRepositoryLock(config, tools, branch, options = {})
|
|
|
670
671
|
return rmOneCore(config, tools, branch, { ...options, target });
|
|
671
672
|
}
|
|
672
673
|
async function rmOne(config, tools, branch, options = {}) {
|
|
673
|
-
await runRmOneUnderRepositoryLock(config, tools, branch, options);
|
|
674
|
+
await withRepositoryMutationLock(config.repoRoot, () => runRmOneUnderRepositoryLock(config, tools, branch, options));
|
|
674
675
|
}
|
|
675
676
|
async function removeUncheckedSandbox(config, target, options) {
|
|
676
677
|
const confirm = options.prompt?.confirm ?? p.confirm;
|
|
@@ -703,8 +704,12 @@ async function removeUncheckedSandbox(config, target, options) {
|
|
|
703
704
|
p.outro('Cancelled');
|
|
704
705
|
return null;
|
|
705
706
|
}
|
|
707
|
+
const worktreePermits = options.permits ?? await authorizeWorktrees(existingWorktrees, { allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) }, { ...options.prompt, interactive: options.interactive });
|
|
706
708
|
for (const container of matchedContainers)
|
|
707
709
|
runSafeEngine(engine, 'docker', ['rm', '-f', container]);
|
|
710
|
+
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
711
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
712
|
+
}
|
|
708
713
|
for (const root of controlRoots) {
|
|
709
714
|
fs.rmSync(root, { recursive: true, force: true });
|
|
710
715
|
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
@@ -720,14 +725,16 @@ async function removeUncheckedSandbox(config, target, options) {
|
|
|
720
725
|
if (shouldRemoveShare)
|
|
721
726
|
fs.rmSync(sharePath, { recursive: true, force: true });
|
|
722
727
|
if (shouldRemoveWorktree) {
|
|
723
|
-
for (const worktree of existingWorktrees)
|
|
724
|
-
|
|
725
|
-
|
|
728
|
+
for (const worktree of existingWorktrees) {
|
|
729
|
+
const permit = worktreePermits.get(path.resolve(worktree));
|
|
730
|
+
if (!permit)
|
|
731
|
+
throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
|
|
732
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
|
|
733
|
+
}
|
|
726
734
|
}
|
|
727
|
-
if (shouldDeleteBranch)
|
|
735
|
+
if (shouldDeleteBranch) {
|
|
736
|
+
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
728
737
|
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
729
|
-
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
730
|
-
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
731
738
|
}
|
|
732
739
|
if (target.workspace.mode === 'task-bound') {
|
|
733
740
|
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
@@ -741,7 +748,7 @@ async function rmOneCore(config, tools, branch, options = {}) {
|
|
|
741
748
|
return removeUncheckedSandbox(config, target, options);
|
|
742
749
|
}
|
|
743
750
|
async function rmPurge(config, tools, prompt = {}) {
|
|
744
|
-
return rmPurgeCore(config, tools, prompt);
|
|
751
|
+
return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
|
|
745
752
|
}
|
|
746
753
|
async function rmPurgeCore(config, tools, prompt = {}) {
|
|
747
754
|
const engine = detectEngine(config);
|
|
@@ -6,7 +6,6 @@ import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts
|
|
|
6
6
|
import { parseArtifactName } from './artifact-name.js';
|
|
7
7
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
8
8
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
9
|
-
import { allowsManualOverride } from './guard-override.js';
|
|
10
9
|
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
11
10
|
const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
|
|
12
11
|
const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
|
|
@@ -294,7 +293,7 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
|
|
|
294
293
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
295
294
|
if (!resolved.ok)
|
|
296
295
|
return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
297
|
-
if (resolved.state !== 'active'
|
|
296
|
+
if (resolved.state !== 'active') {
|
|
298
297
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
299
298
|
}
|
|
300
299
|
if (options.lockAlreadyHeld)
|
|
@@ -166,10 +166,44 @@ function inspectArtifactPatterns(content, schema) {
|
|
|
166
166
|
diagnostics
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
-
function
|
|
169
|
+
function inspectArtifactStateCheck(content, schema, scopeToStateCheckSection) {
|
|
170
|
+
const stateCheck = schema.sections.find((section) => section.id === 'state-check');
|
|
171
|
+
if (!stateCheck || schema.stateCheckPatterns.length === 0) {
|
|
172
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
173
|
+
}
|
|
174
|
+
if (!scopeToStateCheckSection) {
|
|
175
|
+
const diagnostics = schema.stateCheckPatterns
|
|
176
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(content))
|
|
177
|
+
.map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `artifact is missing required state-check command '${pattern}'`, stateCheck.id, null));
|
|
178
|
+
return {
|
|
179
|
+
ok: diagnostics.length === 0,
|
|
180
|
+
family: schema.family,
|
|
181
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
182
|
+
diagnostics
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const scanned = scanVisibleMarkdown(content);
|
|
186
|
+
const headings = scanned.headings.filter((heading) => (heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)));
|
|
187
|
+
if (headings.length !== 1) {
|
|
188
|
+
return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
|
|
189
|
+
}
|
|
190
|
+
const bodyBounds = sectionBodyBounds(content, headings[0], scanned);
|
|
191
|
+
const body = content.slice(bodyBounds.start, bodyBounds.end);
|
|
192
|
+
const diagnostics = schema.stateCheckPatterns
|
|
193
|
+
.filter((pattern) => !new RegExp(pattern, 'm').test(body))
|
|
194
|
+
.map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `state-check section is missing required command '${pattern}'`, stateCheck.id, lineNumber(content, headings[0].start)));
|
|
195
|
+
return {
|
|
196
|
+
ok: diagnostics.length === 0,
|
|
197
|
+
family: schema.family,
|
|
198
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
199
|
+
diagnostics
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function inspectArtifactContract(content, schema, options = {}) {
|
|
170
203
|
const structure = inspectArtifactStructure(content, schema);
|
|
204
|
+
const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
|
|
171
205
|
const patterns = inspectArtifactPatterns(content, schema);
|
|
172
|
-
const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
|
|
206
|
+
const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
|
|
173
207
|
return {
|
|
174
208
|
ok: diagnostics.length === 0,
|
|
175
209
|
family: schema.family,
|
|
@@ -81,21 +81,21 @@ function buildSections(family) {
|
|
|
81
81
|
marker: `artifact-section:${family}:${id}`
|
|
82
82
|
}));
|
|
83
83
|
}
|
|
84
|
+
const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
|
|
84
85
|
const ARTIFACT_SCHEMAS = [
|
|
85
|
-
{ family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: [
|
|
86
|
-
{ family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$'
|
|
87
|
-
{ family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: [
|
|
88
|
-
{ family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$'
|
|
89
|
-
{ family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: [
|
|
86
|
+
{ family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
87
|
+
{ family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
|
|
88
|
+
{ family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
89
|
+
{ family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
|
|
90
|
+
{ family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
90
91
|
{ family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
|
|
91
92
|
'^### (?:审查决定|Approval Decision)$',
|
|
92
|
-
'^\\$ ',
|
|
93
93
|
'^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
|
|
94
94
|
'^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
|
|
95
95
|
'^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
|
|
96
96
|
'^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
|
|
97
97
|
'^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
|
|
98
|
-
] }
|
|
98
|
+
], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
|
|
99
99
|
];
|
|
100
100
|
function getArtifactSchema(family) {
|
|
101
101
|
return ARTIFACT_SCHEMAS.find((schema) => schema.family === family) ?? null;
|