@fitlab-ai/agent-infra 0.11.3 → 0.11.4
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/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +3 -2
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization.js +301 -143
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +10 -0
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +3 -1
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization.ts +269 -111
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +13 -0
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- 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-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/lib/sandbox/control/finalization-response.ts +0 -35
|
@@ -26,7 +26,6 @@ import { OrchestrationStateError, readRun } from "./orchestration.js";
|
|
|
26
26
|
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
27
27
|
import { readPrDeliveryFact } from "./pr-delivery-fact.js";
|
|
28
28
|
import { validateLocalArtifact } from "./local-artifact-finalization.js";
|
|
29
|
-
import { validateQualificationAudit } from "./qualification-audit.js";
|
|
30
29
|
import { getArtifactSchema } from "./artifact-schema.js";
|
|
31
30
|
import { inspectArtifactContract } from "./artifact-operations.js";
|
|
32
31
|
import { normalizeVerificationRecord } from './gate-policy.js';
|
|
@@ -59,7 +58,6 @@ const ACTIVITY_LOG_STARTED_RE = /\s*\[started\]\s*$/;
|
|
|
59
58
|
const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
60
59
|
// Review disagreement ledger (see .agents/rules/review-handshake.md).
|
|
61
60
|
const LEDGER_SECTION_NAMES = ["审查分歧账本", "Review Disagreement Ledger"];
|
|
62
|
-
const DEFAULT_MAX_HANDSHAKE_ROUNDS = 3;
|
|
63
61
|
const POST_REVIEW_COMMIT_STAGE = "post-review-commit";
|
|
64
62
|
const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
65
63
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
@@ -441,21 +439,6 @@ function checkArtifact({ taskDir, config, artifactFile, skillName }) {
|
|
|
441
439
|
return failResult("artifact", `Artifact is empty: ${path.basename(artifactPath)}`);
|
|
442
440
|
}
|
|
443
441
|
const content = fs.readFileSync(artifactPath, "utf8");
|
|
444
|
-
let taskContent = "";
|
|
445
|
-
const taskPath = path.join(taskDir, "task.md");
|
|
446
|
-
if (fs.existsSync(taskPath)) {
|
|
447
|
-
try {
|
|
448
|
-
taskContent = fs.readFileSync(taskPath, "utf8");
|
|
449
|
-
}
|
|
450
|
-
catch (error) {
|
|
451
|
-
return failResult("artifact", `Cannot read task qualification contract: ${error instanceof Error ? error.message : String(error)}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (taskContent && /^review-(?:analysis|plan|code)$/.test(skillName)) {
|
|
455
|
-
const qualification = validateQualificationAudit(taskContent, content, { artifact: path.basename(artifactPath) });
|
|
456
|
-
if (!qualification.ok)
|
|
457
|
-
return failResult("artifact", `${path.basename(artifactPath)} has qualification audit errors: ${qualification.code}: ${qualification.message}`);
|
|
458
|
-
}
|
|
459
442
|
const schemaFamily = skillName === "analyze-task"
|
|
460
443
|
? "analysis"
|
|
461
444
|
: skillName === "review-analysis"
|
|
@@ -479,9 +462,7 @@ function checkArtifact({ taskDir, config, artifactFile, skillName }) {
|
|
|
479
462
|
}
|
|
480
463
|
if (localFamily) {
|
|
481
464
|
const local = validateLocalArtifact(content, {
|
|
482
|
-
family: localFamily
|
|
483
|
-
...(taskContent ? { taskContent } : {}),
|
|
484
|
-
artifact: path.basename(artifactPath)
|
|
465
|
+
family: localFamily
|
|
485
466
|
});
|
|
486
467
|
if (!local.ok) {
|
|
487
468
|
return failResult("artifact", `${path.basename(artifactPath)} has local structural errors: ${local.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')}`);
|
|
@@ -715,11 +696,10 @@ function checkReviewLedger({ taskDir, config, repositoryRoot }) {
|
|
|
715
696
|
return passResult("review-ledger", "Disagreement ledger has no entries");
|
|
716
697
|
}
|
|
717
698
|
const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
|
|
718
|
-
const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS, repositoryRoot));
|
|
719
699
|
const problems = [];
|
|
720
700
|
let inScopeCount = 0;
|
|
721
701
|
for (const row of rows) {
|
|
722
|
-
const { id, stage,
|
|
702
|
+
const { id, stage, status, evidence } = row;
|
|
723
703
|
const stageScoped = stageScope ? stageScope.includes(stage) : true;
|
|
724
704
|
// post-review-commit exemption rows are consumed by the post-review-commit
|
|
725
705
|
// check, not enforced here.
|
|
@@ -738,13 +718,6 @@ function checkReviewLedger({ taskDir, config, repositoryRoot }) {
|
|
|
738
718
|
if (status !== "open" && evidence === "") {
|
|
739
719
|
problems.push(`${id}: status '${status}' requires evidence`);
|
|
740
720
|
}
|
|
741
|
-
const round = Number.parseInt(roundRaw, 10);
|
|
742
|
-
if (Number.isFinite(round) &&
|
|
743
|
-
round >= maxRounds &&
|
|
744
|
-
!LEDGER_TERMINAL.has(status) &&
|
|
745
|
-
status !== "needs-human-decision") {
|
|
746
|
-
problems.push(`${id}: round ${round} reached limit ${maxRounds} without convergence; escalate to needs-human-decision`);
|
|
747
|
-
}
|
|
748
721
|
if (!LEDGER_TERMINAL.has(status)) {
|
|
749
722
|
problems.push(`${id}: unresolved (status '${status}')`);
|
|
750
723
|
}
|
|
@@ -1051,7 +1024,8 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
|
|
|
1051
1024
|
let targetHead;
|
|
1052
1025
|
let diffBase;
|
|
1053
1026
|
try {
|
|
1054
|
-
|
|
1027
|
+
const taskRepositoryRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1028
|
+
gitRoot = reviewWorktreeForTask(taskRepositoryRoot, String(task.metadata.branch || "").trim()) ?? taskRepositoryRoot;
|
|
1055
1029
|
head = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
1056
1030
|
const target = resolveDeliveryTarget(gitRoot, { remote: deliveryRemote, baseRef: deliveryBaseRef });
|
|
1057
1031
|
if (!target.ok)
|
|
@@ -1101,6 +1075,22 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
|
|
|
1101
1075
|
}
|
|
1102
1076
|
return passResult("review-fact", `Review fact valid for ${path.basename(resolvedArtifact.path)} at ${baseline.slice(0, 8)}`);
|
|
1103
1077
|
}
|
|
1078
|
+
function reviewWorktreeForTask(repositoryRoot, branch) {
|
|
1079
|
+
if (!branch)
|
|
1080
|
+
return null;
|
|
1081
|
+
const expectedRef = `refs/heads/${branch}`;
|
|
1082
|
+
const records = execFileSync("git", ["-C", repositoryRoot, "worktree", "list", "--porcelain"], {
|
|
1083
|
+
encoding: "utf8",
|
|
1084
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1085
|
+
}).trim().split(/\r?\n\r?\n/);
|
|
1086
|
+
for (const record of records) {
|
|
1087
|
+
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1088
|
+
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1089
|
+
if (worktree && branchRef === expectedRef)
|
|
1090
|
+
return worktree;
|
|
1091
|
+
}
|
|
1092
|
+
throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
|
|
1093
|
+
}
|
|
1104
1094
|
function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewArtifact }) {
|
|
1105
1095
|
if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
|
|
1106
1096
|
return { ok: true, sha: lastReviewedCommit };
|
|
@@ -23,6 +23,7 @@ const VERIFICATION_CATALOG = {
|
|
|
23
23
|
'commit.completed': gate('commit', 'active'),
|
|
24
24
|
'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
|
|
25
25
|
'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
|
|
26
|
+
'complete-task.prepared': gate('complete-task', 'active'),
|
|
26
27
|
'complete-task.completed': gate('complete-task', 'completed'),
|
|
27
28
|
'create-pr.completed': gate('create-pr', 'active'),
|
|
28
29
|
'create-task.completed': gate('create-task', 'active'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
3
|
+
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
3
4
|
import { WORKFLOW_WARNING_COLUMNS, WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_SEVERITIES, WORKFLOW_WARNING_STATUSES, parseWorkflowWarnings } from './workflow-warnings.js';
|
|
4
5
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
5
6
|
import { allowsManualOverride } from './guard-override.js';
|
|
@@ -118,8 +119,18 @@ function projectFinalizationWarning(taskRef, warning, options = {}) {
|
|
|
118
119
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
119
120
|
if (!resolved.ok)
|
|
120
121
|
return { status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } };
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
let prepared = resolved.state === 'completed';
|
|
123
|
+
if (!prepared && resolved.state === 'active') {
|
|
124
|
+
try {
|
|
125
|
+
prepared = parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
prepared = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!prepared) {
|
|
132
|
+
return { status: 'failed', changed: false, error: { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected a prepared or completed task` } };
|
|
133
|
+
}
|
|
123
134
|
let content;
|
|
124
135
|
let rows;
|
|
125
136
|
try {
|
|
@@ -139,23 +150,27 @@ function projectFinalizationWarning(taskRef, warning, options = {}) {
|
|
|
139
150
|
message: warning.message,
|
|
140
151
|
action: retryHintForWarning(warning),
|
|
141
152
|
resolvedAt: warning.resolvedAt || (warning.status === 'resolved' ? metadata.timestamp : ''),
|
|
142
|
-
resolution: warning.status === 'resolved'
|
|
153
|
+
resolution: warning.status === 'resolved'
|
|
154
|
+
? options.resolution === 'superseded' ? 'Superseded by a later verification observation' : 'Resolved by a successful finalization retry'
|
|
155
|
+
: ''
|
|
143
156
|
}
|
|
144
157
|
: {
|
|
145
158
|
id: nextWarningId(rows), time: metadata.timestamp, step: warning.step,
|
|
146
159
|
severity: warning.severity, code: warning.code, status: warning.status,
|
|
147
160
|
target: warning.target, message: warning.message, action: retryHintForWarning(warning),
|
|
148
|
-
resolvedAt: warning.resolvedAt || '', resolution: warning.status === 'resolved'
|
|
161
|
+
resolvedAt: warning.resolvedAt || '', resolution: warning.status === 'resolved'
|
|
162
|
+
? options.resolution === 'superseded' ? 'Superseded by a later verification observation' : 'Resolved by a successful finalization retry'
|
|
163
|
+
: ''
|
|
149
164
|
};
|
|
150
165
|
if (existing && JSON.stringify(existing) === JSON.stringify(after))
|
|
151
166
|
return { status: 'no-op', changed: false, error: null };
|
|
152
167
|
const written = writeTask({
|
|
153
168
|
taskRef: resolved.taskId,
|
|
154
|
-
expectedState:
|
|
169
|
+
expectedState: resolved.state,
|
|
155
170
|
mutations: [...sectionMutation(content), rowMutation(after)]
|
|
156
171
|
}, {
|
|
157
172
|
...options,
|
|
158
|
-
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state:
|
|
173
|
+
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
|
159
174
|
metadataProvider: () => metadata
|
|
160
175
|
});
|
|
161
176
|
if (written.status === 'failed')
|
|
@@ -15,7 +15,12 @@ function captureWorktreeTree(repoRoot, forcedPath) {
|
|
|
15
15
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-orchestration-index-'));
|
|
16
16
|
const indexFile = path.join(tempDir, 'index');
|
|
17
17
|
try {
|
|
18
|
-
|
|
18
|
+
try {
|
|
19
|
+
git(repoRoot, indexFile, ['read-tree', 'HEAD']);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
git(repoRoot, indexFile, ['read-tree', '--empty']);
|
|
23
|
+
}
|
|
19
24
|
git(repoRoot, indexFile, ['add', '-A', '--', ':/']);
|
|
20
25
|
if (forcedPath && fs.existsSync(path.join(repoRoot, forcedPath))) {
|
|
21
26
|
git(repoRoot, indexFile, ['add', '-f', '--', forcedPath]);
|
|
@@ -111,5 +116,5 @@ function diffWorkspaceSnapshots(repoRoot, before, after) {
|
|
|
111
116
|
}
|
|
112
117
|
return [...changed].sort();
|
|
113
118
|
}
|
|
114
|
-
export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
|
|
119
|
+
export { captureRepositorySnapshot, captureWorkspaceSnapshot, captureWorktreeTree, diffWorkspaceSnapshots };
|
|
115
120
|
//# sourceMappingURL=workspace-snapshot.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.11.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.11.4",
|
|
4
|
+
"sourceInputHash": "46c0a39f354bff8c80fe19354cdda89a91a4acad060b1f134b6d599bc0ba6806",
|
|
5
|
+
"internalExecutableBuildHash": "1bc9e5755c857ee9f13a702452b2ce7c85da94444dc5a8d1305ae80d5899b001"
|
|
6
6
|
}
|
|
@@ -53,7 +53,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
53
53
|
'pr-review-grade': ['decide', 'resolve-host', 'verify-artifact'],
|
|
54
54
|
'platform-checks': ['inspect', 'watch', 'resolve-run', 'logs'],
|
|
55
55
|
'task-context': ['resolve'],
|
|
56
|
-
'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert'],
|
|
56
|
+
'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert', 'rework-intent-rebuild'],
|
|
57
57
|
'task-warning': ['list', 'add', 'set-status'],
|
|
58
58
|
'task-activity': ['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate'],
|
|
59
59
|
'task-artifact': ['inspect', 'init', 'preflight', 'finalize-local'],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.ts';
|
|
2
2
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
|
|
3
3
|
|
|
4
|
-
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family
|
|
4
|
+
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
function taskArtifact(args: string[] = []): void {
|
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
dispatchTaskControlOperation,
|
|
4
4
|
parseTaskControlOperation
|
|
5
5
|
} from '../task/control-authority.ts';
|
|
6
|
-
import { applyTaskFinalization } from '../task/finalization.ts';
|
|
6
|
+
import { applyTaskFinalization, prepareTaskFinalization } from '../task/finalization.ts';
|
|
7
7
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
|
|
8
|
+
import { verifyTaskEvent } from '../task/verification.ts';
|
|
9
|
+
import { resolveSandboxControlTransport } from './task-operation-registry.ts';
|
|
10
|
+
import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.ts';
|
|
8
11
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
9
12
|
|
|
10
13
|
const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
|
|
@@ -73,10 +76,46 @@ async function taskFinalization(args: string[] = []): Promise<void> {
|
|
|
73
76
|
...operation,
|
|
74
77
|
request: { ...operation.request, taskRef: resolved.taskId }
|
|
75
78
|
};
|
|
79
|
+
const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
|
|
80
|
+
if (transport.kind === 'sandbox-local') {
|
|
81
|
+
const prepared = await prepareTaskFinalization(boundOperation.request, {
|
|
82
|
+
repoRoot,
|
|
83
|
+
preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
|
|
84
|
+
});
|
|
85
|
+
if (prepared.status !== 'prepared') {
|
|
86
|
+
process.stdout.write(envelope(prepared.status, prepared.changed, true, prepared, prepared.error));
|
|
87
|
+
process.exitCode = exitCode(prepared.status);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
|
|
92
|
+
process.stdout.write(response.stdout);
|
|
93
|
+
process.stderr.write(response.stderr);
|
|
94
|
+
process.exitCode = response.exitCode ?? 1;
|
|
95
|
+
return;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const detail = error instanceof SandboxControlClientError
|
|
98
|
+
? error.detail
|
|
99
|
+
: { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
100
|
+
process.stdout.write(envelope(detail.retryable ? 'blocked' : 'failed', false, false, null, detail));
|
|
101
|
+
process.exitCode = detail.retryable ? 2 : 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
76
105
|
const result = await dispatchTaskControlOperation(
|
|
77
106
|
createDirectHostExecutionContext({ repoRoot }),
|
|
78
107
|
boundOperation
|
|
79
108
|
);
|
|
109
|
+
if (result.status === 'prepared') {
|
|
110
|
+
const detail = {
|
|
111
|
+
code: 'TASK_FINALIZATION_DIRECT_COMMIT_REQUIRED',
|
|
112
|
+
message: 'direct finalization prepared external work without committing the lifecycle transition',
|
|
113
|
+
retryable: false
|
|
114
|
+
};
|
|
115
|
+
process.stdout.write(envelope('failed', result.changed, true, result, detail));
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
80
119
|
process.stdout.write(envelope(result.status, result.changed, true, result, result.error));
|
|
81
120
|
process.exitCode = exitCode(result.status);
|
|
82
121
|
}
|
|
@@ -8,17 +8,18 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
|
8
8
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
9
9
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
10
10
|
|
|
11
|
-
const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, rework-intent-upsert, stage-status\n`;
|
|
11
|
+
const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, rework-intent-upsert, rework-intent-rebuild, stage-status\n`;
|
|
12
12
|
const FLAGS: Record<string, string> = {
|
|
13
13
|
'--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
|
|
14
14
|
'--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
|
|
15
15
|
'--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation',
|
|
16
16
|
'--intent-id': 'intentId', '--finding-id': 'findingId', '--source-artifact': 'sourceArtifact',
|
|
17
|
-
'--source-sha256': 'sourceSha256', '--
|
|
17
|
+
'--source-sha256': 'sourceSha256', '--classification': 'classification',
|
|
18
18
|
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
19
19
|
};
|
|
20
20
|
const NUMERIC = new Set(['ordinal', 'round']);
|
|
21
21
|
const BOOLEAN = new Set(['needsImplementation']);
|
|
22
|
+
const REWORK_CLASSIFICATIONS = new Set(['scope-or-requirement', 'design', 'implementation', 'human-decision', 'insufficient-evidence']);
|
|
22
23
|
|
|
23
24
|
function usageFailure(message: string): void {
|
|
24
25
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LEDGER_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -38,6 +39,7 @@ async function taskLedger(args: string[] = []): Promise<void> {
|
|
|
38
39
|
internalHandlerRoute('task-ledger', 'decision-next-id', kind),
|
|
39
40
|
internalHandlerRoute('task-ledger', 'decision-upsert', kind),
|
|
40
41
|
internalHandlerRoute('task-ledger', 'rework-intent-upsert', kind),
|
|
42
|
+
internalHandlerRoute('task-ledger', 'rework-intent-rebuild', kind),
|
|
41
43
|
internalHandlerRoute('task-ledger', 'stage-status', kind)
|
|
42
44
|
].some(Boolean)) {
|
|
43
45
|
usageFailure(`unknown ledger intent '${kind}'`); return;
|
|
@@ -67,14 +69,16 @@ async function taskLedger(args: string[] = []): Promise<void> {
|
|
|
67
69
|
'finding-review': ['id', 'status', 'evidence'],
|
|
68
70
|
'decision-next-id': [],
|
|
69
71
|
'decision-upsert': ['id', 'stage', 'artifact'],
|
|
70
|
-
'rework-intent-upsert': ['intentId', 'findingId', 'sourceArtifact', 'sourceSha256', '
|
|
72
|
+
'rework-intent-upsert': ['intentId', 'findingId', 'sourceArtifact', 'sourceSha256', 'classification'],
|
|
73
|
+
'rework-intent-rebuild': [],
|
|
71
74
|
'stage-status': ['stage']
|
|
72
75
|
};
|
|
73
76
|
const optional: Record<string, string[]> = {
|
|
74
77
|
'finding-review': ['needsImplementation'],
|
|
75
|
-
'decision-upsert': ['needsImplementation']
|
|
78
|
+
'decision-upsert': ['needsImplementation'],
|
|
79
|
+
'rework-intent-rebuild': ['findingId', 'sourceArtifact', 'sourceSha256', 'classification']
|
|
76
80
|
};
|
|
77
|
-
const overrideFields = kind === 'stage-status' || kind === 'decision-next-id' || kind === 'rework-intent-upsert'
|
|
81
|
+
const overrideFields = kind === 'stage-status' || kind === 'decision-next-id' || kind === 'rework-intent-upsert' || kind === 'rework-intent-rebuild'
|
|
78
82
|
? []
|
|
79
83
|
: ['overrideTicket', 'overrideTarget', 'overrideScope'];
|
|
80
84
|
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? []), ...overrideFields]);
|
|
@@ -84,6 +88,9 @@ async function taskLedger(args: string[] = []): Promise<void> {
|
|
|
84
88
|
if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
|
|
85
89
|
const dryRunConflict = overrideDryRunConflict(values);
|
|
86
90
|
if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
|
|
91
|
+
if (values.classification !== undefined && !REWORK_CLASSIFICATIONS.has(String(values.classification))) {
|
|
92
|
+
usageFailure(`invalid rework classification '${values.classification}'`); return;
|
|
93
|
+
}
|
|
87
94
|
if (kind === 'stage-status') {
|
|
88
95
|
if (!isReviewStage(String(values.stage))) { usageFailure(`stage-status requires a valid review stage`); return; }
|
|
89
96
|
const resolved = resolveTaskRef(taskRef);
|
|
@@ -125,6 +125,7 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
|
|
|
125
125
|
descriptor('internal', 'task-ledger', 'finding-review', 'task-bound', 'progress'),
|
|
126
126
|
descriptor('internal', 'task-ledger', 'decision-upsert', 'task-bound', 'progress'),
|
|
127
127
|
descriptor('internal', 'task-ledger', 'rework-intent-upsert', 'task-bound', 'progress'),
|
|
128
|
+
descriptor('internal', 'task-ledger', 'rework-intent-rebuild', 'task-bound', 'progress'),
|
|
128
129
|
descriptor('internal', 'task-warning', 'list', 'task-bound', 'diagnostic'),
|
|
129
130
|
descriptor('internal', 'task-warning', 'add', 'task-bound', 'progress'),
|
|
130
131
|
descriptor('internal', 'task-warning', 'set-status', 'task-bound', 'progress'),
|
|
@@ -19,6 +19,24 @@ type CompletionBackfillResult = PlatformResult & {
|
|
|
19
19
|
artifacts: Array<{ artifact: string; status: PlatformResult['status'] }>;
|
|
20
20
|
warnings: Array<{ id: string; status: string }>;
|
|
21
21
|
};
|
|
22
|
+
type CompletionBackfillEligibility = Readonly<{
|
|
23
|
+
status: 'ready' | 'failed';
|
|
24
|
+
eligible: boolean;
|
|
25
|
+
error: { code: string; message: string; retryable: boolean } | null;
|
|
26
|
+
}>;
|
|
27
|
+
type BackfillArtifactReference = Readonly<
|
|
28
|
+
| { status: 'ignored'; artifact: null; error: null }
|
|
29
|
+
| { status: 'ready'; artifact: string; error: null }
|
|
30
|
+
| { status: 'failed'; artifact: null; error: { code: string; message: string; retryable: false } }
|
|
31
|
+
>;
|
|
32
|
+
type BackfillWarningMatch = Readonly<{
|
|
33
|
+
warning: ReturnType<typeof getOpenWorkflowWarnings>[number];
|
|
34
|
+
artifact: string;
|
|
35
|
+
}>;
|
|
36
|
+
type BackfillWarningMatches = Readonly<
|
|
37
|
+
| { status: 'ready'; matches: BackfillWarningMatch[]; error: null }
|
|
38
|
+
| { status: 'failed'; matches: []; error: { code: string; message: string; retryable: false } }
|
|
39
|
+
>;
|
|
22
40
|
|
|
23
41
|
function result(
|
|
24
42
|
base: PlatformResult,
|
|
@@ -28,18 +46,65 @@ function result(
|
|
|
28
46
|
return { ...base, artifacts, warnings };
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
function
|
|
32
|
-
const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])(
|
|
49
|
+
function referencedBackfillArtifact(taskDir: string, message: string): BackfillArtifactReference {
|
|
50
|
+
const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])([a-z][a-z-]*(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
|
|
33
51
|
.map((match) => match[1]!);
|
|
34
|
-
if (names.length !== 1) return null;
|
|
52
|
+
if (names.length !== 1) return { status: 'ignored', artifact: null, error: null };
|
|
35
53
|
const parsed = parseArtifactName(names[0]!);
|
|
36
|
-
if (!parsed || parsed.family !== 'pr-review')
|
|
54
|
+
if (!parsed || (parsed.family !== 'pr-review' && !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family as typeof COMPLETION_BACKFILL_FAMILIES[number]))) {
|
|
55
|
+
return { status: 'ignored', artifact: null, error: null };
|
|
56
|
+
}
|
|
37
57
|
const artifactPath = path.join(taskDir, parsed.name);
|
|
38
58
|
try {
|
|
39
59
|
const stat = fs.lstatSync(artifactPath);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
if (stat.isSymbolicLink()) return {
|
|
61
|
+
status: 'failed', artifact: null,
|
|
62
|
+
error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `SYMBOLIC_LINK: symbolic links are not workflow artifacts: ${parsed.name}`, retryable: false }
|
|
63
|
+
};
|
|
64
|
+
if (!stat.isFile()) return {
|
|
65
|
+
status: 'failed', artifact: null,
|
|
66
|
+
error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `NON_REGULAR_FILE: artifact is not a regular file: ${parsed.name}`, retryable: false }
|
|
67
|
+
};
|
|
68
|
+
fs.accessSync(artifactPath, fs.constants.R_OK);
|
|
69
|
+
return { status: 'ready', artifact: parsed.name, error: null };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'failed', artifact: null,
|
|
73
|
+
error: { code: 'ARTIFACT_READ_FAILED', message: `Unable to inspect referenced artifact '${parsed.name}': ${error instanceof Error ? error.message : String(error)}`, retryable: false }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchingBackfillWarnings(taskDir: string, content: string): BackfillWarningMatches {
|
|
79
|
+
const matches: BackfillWarningMatch[] = [];
|
|
80
|
+
for (const warning of getOpenWorkflowWarnings(content)) {
|
|
81
|
+
if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact') continue;
|
|
82
|
+
const reference = referencedBackfillArtifact(taskDir, warning.message);
|
|
83
|
+
if (reference.status === 'failed') return { status: 'failed', matches: [], error: reference.error };
|
|
84
|
+
if (reference.status === 'ready') matches.push({ warning, artifact: reference.artifact });
|
|
85
|
+
}
|
|
86
|
+
return { status: 'ready', matches, error: null };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function inspectCompletionBackfillEligibility(
|
|
90
|
+
taskRef: string,
|
|
91
|
+
options: Pick<CompletionBackfillOptions, 'cwd'> = {}
|
|
92
|
+
): CompletionBackfillEligibility {
|
|
93
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
94
|
+
if (!resolved.ok) return {
|
|
95
|
+
status: 'failed', eligible: false,
|
|
96
|
+
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
100
|
+
const matching = matchingBackfillWarnings(resolved.taskDir, content);
|
|
101
|
+
if (matching.status === 'failed') return { status: 'failed', eligible: false, error: matching.error };
|
|
102
|
+
return { status: 'ready', eligible: matching.matches.length > 0, error: null };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
status: 'failed', eligible: false,
|
|
106
|
+
error: { code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
107
|
+
};
|
|
43
108
|
}
|
|
44
109
|
}
|
|
45
110
|
|
|
@@ -60,6 +125,8 @@ async function backfillCompletionComments(
|
|
|
60
125
|
}
|
|
61
126
|
}), [], []);
|
|
62
127
|
const initialContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
128
|
+
const matching = matchingBackfillWarnings(resolved.taskDir, initialContent);
|
|
129
|
+
if (matching.status === 'failed') return result(platformResult('failed', { error: matching.error }), [], []);
|
|
63
130
|
const issueIdentity = taskIssueIdentity(parseTaskFrontmatter(initialContent));
|
|
64
131
|
if (!issueIdentity) return result(platformResult('no-op', {
|
|
65
132
|
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
@@ -92,17 +159,12 @@ async function backfillCompletionComments(
|
|
|
92
159
|
}), artifacts, []);
|
|
93
160
|
}
|
|
94
161
|
|
|
95
|
-
const matching = getOpenWorkflowWarnings(initialContent).flatMap((warning) => {
|
|
96
|
-
if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact') return [];
|
|
97
|
-
const excluded = excludedPrReview(resolved.taskDir, warning.message);
|
|
98
|
-
return excluded ? [{ warning, excluded }] : [];
|
|
99
|
-
});
|
|
100
162
|
const warnings: CompletionBackfillResult['warnings'] = [];
|
|
101
163
|
let metadata: ReturnType<typeof captureTaskWriteMetadata> | null = null;
|
|
102
|
-
for (const { warning,
|
|
164
|
+
for (const { warning, artifact } of matching.matches) {
|
|
103
165
|
metadata ??= captureTaskWriteMetadata();
|
|
104
166
|
const resolution = [
|
|
105
|
-
`
|
|
167
|
+
`artifact=${artifact}`,
|
|
106
168
|
`completionFamilies=${COMPLETION_BACKFILL_FAMILIES.join(',')}`,
|
|
107
169
|
`synchronized=${inventory.artifacts.map((artifact) => artifact.name).join(',') || 'none'}`,
|
|
108
170
|
`completedAt=${metadata.timestamp}`
|
|
@@ -131,5 +193,5 @@ async function backfillCompletionComments(
|
|
|
131
193
|
}), artifacts, warnings);
|
|
132
194
|
}
|
|
133
195
|
|
|
134
|
-
export { backfillCompletionComments };
|
|
135
|
-
export type { CompletionBackfillOptions, CompletionBackfillResult };
|
|
196
|
+
export { backfillCompletionComments, inspectCompletionBackfillEligibility };
|
|
197
|
+
export type { CompletionBackfillEligibility, CompletionBackfillOptions, CompletionBackfillResult };
|