@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.
Files changed (102) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -1
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-finalization.js +41 -1
  7. package/dist/lib/internal/task-ledger.js +13 -5
  8. package/dist/lib/internal/task-operation-registry.js +1 -0
  9. package/dist/lib/platform/completion-backfill.js +63 -17
  10. package/dist/lib/sandbox/control/server.js +26 -143
  11. package/dist/lib/sandbox/task-cleanup.js +13 -20
  12. package/dist/lib/task/artifact-command.js +11 -4
  13. package/dist/lib/task/artifact-lifecycle.js +217 -85
  14. package/dist/lib/task/artifact-operations.js +1 -26
  15. package/dist/lib/task/artifact-receipts.js +4 -1
  16. package/dist/lib/task/artifact-schema.js +1 -0
  17. package/dist/lib/task/artifact-selection.js +25 -0
  18. package/dist/lib/task/capabilities.js +158 -39
  19. package/dist/lib/task/commands/status.js +9 -2
  20. package/dist/lib/task/control-authority.js +3 -2
  21. package/dist/lib/task/decision-intents.js +12 -0
  22. package/dist/lib/task/delivery.js +1 -43
  23. package/dist/lib/task/events.js +80 -64
  24. package/dist/lib/task/finalization.js +301 -143
  25. package/dist/lib/task/ledger-intents.js +157 -15
  26. package/dist/lib/task/lifecycle-path.js +75 -0
  27. package/dist/lib/task/lifecycle.js +10 -0
  28. package/dist/lib/task/local-artifact-finalization.js +1 -20
  29. package/dist/lib/task/orchestration.js +17 -7
  30. package/dist/lib/task/review-finalization.js +3 -16
  31. package/dist/lib/task/rework-intent.js +70 -4
  32. package/dist/lib/task/verification-engine.js +20 -30
  33. package/dist/lib/task/verification.js +1 -0
  34. package/dist/lib/task/workflow-warning-intents.js +21 -6
  35. package/dist/lib/task/workspace-snapshot.js +7 -2
  36. package/dist/lifecycle-build-manifest.json +3 -3
  37. package/lib/internal/cli-route-inventory.ts +1 -1
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +40 -1
  40. package/lib/internal/task-ledger.ts +12 -5
  41. package/lib/internal/task-operation-registry.ts +1 -0
  42. package/lib/platform/completion-backfill.ts +78 -16
  43. package/lib/sandbox/control/server.ts +21 -146
  44. package/lib/sandbox/task-cleanup.ts +12 -20
  45. package/lib/task/artifact-command.ts +14 -4
  46. package/lib/task/artifact-lifecycle.ts +236 -85
  47. package/lib/task/artifact-operations.ts +1 -26
  48. package/lib/task/artifact-receipts.ts +4 -1
  49. package/lib/task/artifact-schema.ts +1 -0
  50. package/lib/task/artifact-selection.ts +53 -0
  51. package/lib/task/capabilities.ts +154 -40
  52. package/lib/task/commands/status.ts +11 -2
  53. package/lib/task/control-authority.ts +3 -1
  54. package/lib/task/decision-intents.ts +9 -0
  55. package/lib/task/delivery.ts +1 -40
  56. package/lib/task/events.ts +82 -64
  57. package/lib/task/finalization.ts +269 -111
  58. package/lib/task/ledger-intents.ts +155 -15
  59. package/lib/task/lifecycle-path.ts +98 -0
  60. package/lib/task/lifecycle.ts +13 -0
  61. package/lib/task/local-artifact-finalization.ts +2 -25
  62. package/lib/task/orchestration.ts +26 -8
  63. package/lib/task/review-finalization.ts +3 -16
  64. package/lib/task/rework-intent.ts +75 -5
  65. package/lib/task/verification-engine.ts +19 -30
  66. package/lib/task/verification.ts +2 -1
  67. package/lib/task/workflow-warning-intents.ts +18 -6
  68. package/lib/task/workspace-snapshot.ts +3 -2
  69. package/package.json +1 -1
  70. package/templates/.agents/rules/review-handshake.en.md +4 -4
  71. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  72. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  73. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  74. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  75. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  76. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  77. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  78. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  79. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  82. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  83. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  84. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  85. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  86. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  87. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  88. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  89. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  90. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  91. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  92. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  93. package/templates/.agents/templates/task.en.md +2 -2
  94. package/templates/.agents/templates/task.zh-CN.md +2 -2
  95. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  96. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  97. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  98. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  99. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  100. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  101. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  102. 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, round: roundRaw, status, evidence } = row;
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
- gitRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
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
- if (resolved.state !== 'completed')
122
- return { status: 'failed', changed: false, error: { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected completed` } };
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' ? 'Resolved by a successful finalization retry' : ''
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' ? 'Resolved by a successful finalization retry' : ''
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: 'completed',
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: 'completed' },
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
- git(repoRoot, indexFile, ['read-tree', 'HEAD']);
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.3",
4
- "sourceInputHash": "99efbc575d475879773bf1676cc2de5ea628b9c193f69a9812a723313ba5ae7f",
5
- "internalExecutableBuildHash": "6912a66863d013c476699b88cc1d4154082971164cbae3f98ea79dda39a46e7a"
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>\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 a workflow artifact without changing task state.\n`;
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', '--target': 'target',
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', 'target'],
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 excludedPrReview(taskDir: string, message: string): string | null {
32
- const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])(pr-review(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
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') return null;
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
- return stat.isFile() && !stat.isSymbolicLink() ? parsed.name : null;
41
- } catch {
42
- return null;
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, excluded } of matching) {
164
+ for (const { warning, artifact } of matching.matches) {
103
165
  metadata ??= captureTaskWriteMetadata();
104
166
  const resolution = [
105
- `excluded=${excluded}`,
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 };