@fitlab-ai/agent-infra 0.11.8 → 0.11.9

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 (45) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  4. package/dist/lib/agent-clients/next-steps.js +1 -1
  5. package/dist/lib/agent-clients/reconcile.js +3 -3
  6. package/dist/lib/agent-clients/schema.js +18 -10
  7. package/dist/lib/init.js +2 -2
  8. package/dist/lib/internal/agent-client.js +3 -3
  9. package/dist/lib/sandbox/recovery.js +1 -22
  10. package/dist/lib/sandbox/tools.js +2 -0
  11. package/dist/lib/task/finalization.js +90 -93
  12. package/dist/lib/task/verification.js +0 -1
  13. package/dist/lib/update.js +2 -2
  14. package/dist/lifecycle-build-manifest.json +3 -3
  15. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  16. package/lib/agent-clients/next-steps.ts +3 -3
  17. package/lib/agent-clients/reconcile.ts +3 -3
  18. package/lib/agent-clients/schema.ts +18 -10
  19. package/lib/init.ts +2 -2
  20. package/lib/internal/agent-client.ts +3 -3
  21. package/lib/sandbox/recovery.ts +0 -25
  22. package/lib/sandbox/tool-types.ts +1 -0
  23. package/lib/sandbox/tools.ts +2 -0
  24. package/lib/task/finalization.ts +91 -88
  25. package/lib/task/verification.ts +1 -2
  26. package/lib/update.ts +2 -3
  27. package/package.json +1 -1
  28. package/templates/.agents/README.en.md +2 -50
  29. package/templates/.agents/README.zh-CN.md +2 -50
  30. package/templates/.agents/rules/next-step-output.en.md +1 -1
  31. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  32. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  33. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  34. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  35. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  36. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  37. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  38. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  39. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  40. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  41. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  42. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  43. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
  44. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  45. package/lib/agent-clients/custom-tuis.ts +0 -149
package/README.md CHANGED
@@ -240,7 +240,7 @@ In-depth guides live under [`docs/en/`](./docs/en/README.md):
240
240
  - [Feishu Bridge](./docs/en/feishu-bridge.md) — configure the Feishu long-connection adapter, command routing, permissions, and TUI selection
241
241
  - [Built-in AI Skills](./docs/en/skills.md) — the full skill catalog by use case
242
242
  - [Custom Skills](./docs/en/custom-skills.md) — create and sync project-specific skills
243
- - [Custom TUI Configuration](./docs/en/custom-tui.md) — adapt agent-infra to non-built-in AI TUIs
243
+ - [Custom Tool Invocations](./docs/en/custom-tool-invocations.md) — configure custom-tool next-step commands
244
244
  - [Prebuilt Workflows](./docs/en/workflows.md) — the gated delivery lifecycle and example flow
245
245
  - [Configuration Reference](./docs/en/configuration.md) — `.agents/.airc.json`, external sources, version management
246
246
  - [File Management Strategies](./docs/en/file-management.md) — managed / merged / ejected update strategies
package/README.zh-CN.md CHANGED
@@ -239,7 +239,7 @@ my-project/
239
239
  - [飞书桥接](./docs/zh-CN/feishu-bridge.md) — 配置飞书长连接 adapter、命令路由、权限和 TUI 选择
240
240
  - [内置 AI Skills](./docs/zh-CN/skills.md) — 按使用场景分组的完整 skill 清单
241
241
  - [自定义 Skills](./docs/zh-CN/custom-skills.md) — 创建并同步项目专属 skill
242
- - [自定义 TUI 配置](./docs/zh-CN/custom-tui.md) — 适配非内置的 AI TUI
242
+ - [自定义工具调用](./docs/zh-CN/custom-tool-invocations.md) — 配置自定义工具的下一步命令
243
243
  - [预置工作流](./docs/zh-CN/workflows.md) — 分阶段交付链路与示例流程
244
244
  - [配置参考](./docs/zh-CN/configuration.md) — `.agents/.airc.json`、外部源、版本管理
245
245
  - [文件管理策略](./docs/zh-CN/file-management.md) — managed / merged / ejected 更新策略
@@ -0,0 +1,92 @@
1
+ const ALLOWED_PLACEHOLDERS = Object.freeze(['skillName', 'projectName']);
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function isNonEmptySingleLine(value) {
6
+ return typeof value === 'string'
7
+ && value.trim() !== ''
8
+ && !/[\r\n]/.test(value);
9
+ }
10
+ function hasValidPlaceholders(invocation) {
11
+ const placeholders = [
12
+ ...invocation.matchAll(/\$\{([^}]+)\}/g)
13
+ ].map((match) => match[1]);
14
+ return placeholders.includes('skillName')
15
+ && placeholders.every((placeholder) => ALLOWED_PLACEHOLDERS.includes(placeholder))
16
+ && !invocation
17
+ .replaceAll('${skillName}', '')
18
+ .replaceAll('${projectName}', '')
19
+ .includes('${');
20
+ }
21
+ function normalizeCustomToolInvocations(config) {
22
+ const items = [];
23
+ const diagnostics = [];
24
+ const root = isRecord(config) ? config : {};
25
+ const sandbox = isRecord(root.sandbox) ? root.sandbox : {};
26
+ const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : { ids: ['agent-infra'] };
27
+ const definitions = sandboxTools.definitions ?? {};
28
+ const selectedIds = sandboxTools.ids;
29
+ if (!Array.isArray(selectedIds)) {
30
+ return Object.freeze({
31
+ items: Object.freeze([]),
32
+ diagnostics: Object.freeze([
33
+ Object.freeze({
34
+ code: 'INVALID_CUSTOM_TOOL_CONFIG',
35
+ path: 'sandbox.tools.ids'
36
+ })
37
+ ])
38
+ });
39
+ }
40
+ if (!isRecord(definitions)) {
41
+ return Object.freeze({
42
+ items: Object.freeze([]),
43
+ diagnostics: Object.freeze([
44
+ Object.freeze({
45
+ code: 'INVALID_CUSTOM_TOOL_CONFIG',
46
+ path: 'sandbox.tools.definitions'
47
+ })
48
+ ])
49
+ });
50
+ }
51
+ for (const [index, id] of selectedIds.entries()) {
52
+ if (typeof id !== 'string') {
53
+ diagnostics.push({
54
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION',
55
+ path: `sandbox.tools.ids[${index}]`
56
+ });
57
+ continue;
58
+ }
59
+ const candidate = definitions[id];
60
+ if (!isRecord(candidate))
61
+ continue;
62
+ if (candidate.invoke === undefined)
63
+ continue;
64
+ const base = `sandbox.tools.definitions.${id}`;
65
+ const name = candidate.name === undefined ? id : candidate.name;
66
+ if (!isNonEmptySingleLine(name)) {
67
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.name` });
68
+ continue;
69
+ }
70
+ if (!isNonEmptySingleLine(candidate.invoke)) {
71
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.invoke` });
72
+ continue;
73
+ }
74
+ if (!hasValidPlaceholders(candidate.invoke)) {
75
+ diagnostics.push({
76
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER',
77
+ path: `${base}.invoke`
78
+ });
79
+ continue;
80
+ }
81
+ items.push(Object.freeze({
82
+ name,
83
+ invocation: candidate.invoke
84
+ }));
85
+ }
86
+ return Object.freeze({
87
+ items: Object.freeze(items),
88
+ diagnostics: Object.freeze(diagnostics.map((diagnostic) => Object.freeze(diagnostic)))
89
+ });
90
+ }
91
+ export { normalizeCustomToolInvocations };
92
+ //# sourceMappingURL=custom-tool-invocations.js.map
@@ -37,7 +37,7 @@ function renderNextStepCommands(input) {
37
37
  args
38
38
  })
39
39
  }));
40
- const custom = input.customTUIs.map((tool) => Object.freeze({
40
+ const custom = input.customToolInvocations.map((tool) => Object.freeze({
41
41
  source: 'custom',
42
42
  displayName: tool.name,
43
43
  command: renderAgentClientInvocation(tool.invocation, {
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import defaults from '../defaults.json' with { type: 'json' };
5
5
  import { normalizeAgentClients, serializeAgentClients } from './config.js';
6
- import { normalizeCustomTUIs } from './custom-tuis.js';
6
+ import { normalizeCustomToolInvocations } from './custom-tool-invocations.js';
7
7
  import { renderNextStepCommands } from './next-steps.js';
8
8
  import { planAgentClientProjectAssets } from './project-assets.js';
9
9
  import { listAgentClientAdapters, listEnabledAgentClientAdapters } from './registry.js';
@@ -141,11 +141,11 @@ function planAgentClientReconciliation(input) {
141
141
  if (Object.keys(baselines).length === 0) {
142
142
  delete nextConfig.files.managedBaselines;
143
143
  }
144
- const custom = normalizeCustomTUIs(input.projectRoot, input.config.customTUIs ?? []);
144
+ const custom = normalizeCustomToolInvocations(input.config);
145
145
  const nextSteps = renderNextStepCommands({
146
146
  projectName,
147
147
  state: desired,
148
- customTUIs: custom.items,
148
+ customToolInvocations: custom.items,
149
149
  skillName: 'update-agent-infra'
150
150
  });
151
151
  const actionableSeed = seedOperations.some((operation) => operation.kind === 'write' || operation.kind === 'remove');
@@ -38,16 +38,24 @@ const AGENT_CLIENTS_SCHEMA = {
38
38
  }
39
39
  }))
40
40
  },
41
- customTUIs: {
42
- type: 'array',
43
- items: {
44
- type: 'object',
45
- additionalProperties: true,
46
- required: ['name', 'dir', 'invoke'],
47
- properties: {
48
- name: { type: 'string' },
49
- dir: { type: 'string' },
50
- invoke: { type: 'string' }
41
+ sandbox: {
42
+ type: 'object',
43
+ properties: {
44
+ tools: {
45
+ type: 'object',
46
+ properties: {
47
+ ids: { type: 'array', items: { type: 'string' } },
48
+ definitions: {
49
+ type: 'object',
50
+ additionalProperties: {
51
+ type: 'object',
52
+ properties: {
53
+ name: { type: 'string' },
54
+ invoke: { type: 'string' }
55
+ }
56
+ }
57
+ }
58
+ }
51
59
  }
52
60
  }
53
61
  }
package/dist/lib/init.js CHANGED
@@ -228,8 +228,8 @@ async function cmdInit() {
228
228
  console.log(' npm install -g @fitlab-ai/agent-infra');
229
229
  console.log('');
230
230
  if (workflowPlan.nextSteps.length === 0) {
231
- console.log(' No Agent Client project integration enabled.');
232
- console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
231
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
232
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${configPath}.`);
233
233
  console.log('');
234
234
  }
235
235
  else {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { AgentClientConfigError, normalizeAgentClients } from '../agent-clients/config.js';
4
- import { normalizeCustomTUIs } from '../agent-clients/custom-tuis.js';
4
+ import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.js';
5
5
  import { renderNextStepCommands } from '../agent-clients/next-steps.js';
6
6
  import { getAgentClientModelSelection } from '../agent-clients/registry.js';
7
7
  import { isAgentClientId } from '../agent-clients/types.js';
@@ -139,11 +139,11 @@ function agentClient(args = []) {
139
139
  }
140
140
  try {
141
141
  const clients = normalizeAgentClients(config);
142
- const custom = normalizeCustomTUIs(process.cwd(), config.customTUIs ?? []);
142
+ const custom = normalizeCustomToolInvocations(config);
143
143
  const commands = renderNextStepCommands({
144
144
  projectName: String(config.project ?? ''),
145
145
  state: clients.state,
146
- customTUIs: custom.items,
146
+ customToolInvocations: custom.items,
147
147
  skillName: parsed.skillName,
148
148
  ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
149
149
  ...(parsed.version ? { version: parsed.version } : {})
@@ -4,8 +4,7 @@ import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { nodeEntryArgs } from './control/executor.js';
7
- import { containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxRuntimeCapabilityLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, worktreeDirCandidates } from './constants.js';
8
- import { isAgentClientId } from '../agent-clients/types.js';
7
+ import { containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, worktreeDirCandidates } from './constants.js';
9
8
  import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
10
9
  import { toEnginePath } from './engines/wsl2-paths.js';
11
10
  import { sandboxCoreBindMounts } from './mounts.js';
@@ -277,12 +276,6 @@ export function classifySandboxRecovery(snapshot) {
277
276
  : `Container identity ${actualIdentity} on branch ${snapshot.actualBranch ?? 'unknown'} does not match requested ${expectedIdentity} on branch ${snapshot.expectedBranch}.`
278
277
  });
279
278
  }
280
- if (snapshot.runtimeCapabilityOk === false) {
281
- findings.push({
282
- repairKind: 'hard-failure',
283
- message: 'Container runtime capability signature does not match the current sandbox plan.'
284
- });
285
- }
286
279
  if (snapshot.workspaceTopology === 'legacy-parent') {
287
280
  findings.push({
288
281
  repairKind: 'hard-failure',
@@ -307,13 +300,6 @@ export function classifySandboxRecovery(snapshot) {
307
300
  path: '/run/agent-infra/runtime'
308
301
  });
309
302
  }
310
- for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
311
- findings.push({
312
- repairKind: 'hard-failure',
313
- message: `Disabled Agent Client capability remains mounted at ${mountPath}.`,
314
- path: mountPath
315
- });
316
- }
317
303
  for (const mount of snapshot.mounts) {
318
304
  if (snapshot.workspaceTopology === 'legacy-parent'
319
305
  && mount.path.startsWith('/workspace/.agents/workspace'))
@@ -679,11 +665,6 @@ export function collectSandboxRecoverySnapshot(params) {
679
665
  mode: sandboxWorkspaceModeLabel(params.config),
680
666
  taskId: sandboxTaskIdLabel(params.config)
681
667
  });
682
- const selectedToolIds = new Set(tools.map((tool) => tool.id));
683
- const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
684
- .filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
685
- .map((tool) => tool.containerMount)
686
- .filter((mountPath) => mountsByDestination.has(mountPath));
687
668
  const mountSnapshots = expectedMounts({
688
669
  config: params.config,
689
670
  branch: params.branch,
@@ -736,8 +717,6 @@ export function collectSandboxRecoverySnapshot(params) {
736
717
  identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
737
718
  && branchLabel === params.branch
738
719
  && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
739
- runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
740
- unexpectedCapabilityMounts,
741
720
  mounts: mountSnapshots,
742
721
  tmpfs,
743
722
  seeds,
@@ -187,6 +187,7 @@ export function parseCustomTool(entry, index, options, context = `customTools[${
187
187
  if (!containerMount.startsWith('/')) {
188
188
  throw new Error(`${context}: field "containerMount" must be an absolute path`);
189
189
  }
190
+ const invoke = asOptionalNonEmptyString(entry.invoke, 'invoke', context);
190
191
  const tool = {
191
192
  id,
192
193
  name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
@@ -196,6 +197,7 @@ export function parseCustomTool(entry, index, options, context = `customTools[${
196
197
  versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
197
198
  setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
198
199
  ?? `Run \`${id}\` inside the container to set up.`,
200
+ ...(invoke === undefined ? {} : { invoke }),
199
201
  envVars: asStringRecord(entry.envVars, 'envVars', context),
200
202
  hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
201
203
  hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
@@ -619,9 +619,12 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
619
619
  const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
620
620
  const warnings = openWarnings(receipt);
621
621
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
622
- const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
622
+ const terminalVerificationPending = receipt.lifecycle === 'done' && receipt.verification === 'pending';
623
+ const resultError = error ?? (terminalVerificationPending ? steps.verification?.error : null) ?? null;
624
+ const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID'
625
+ || terminalVerificationPending;
623
626
  return {
624
- status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
627
+ status: hardError ? (resultError?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
625
628
  changed,
626
629
  taskId,
627
630
  backfill: steps.backfill ?? null,
@@ -632,7 +635,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
632
635
  completedSteps: completedSteps(receipt),
633
636
  pendingSteps: pending,
634
637
  result: hardError
635
- ? (error?.retryable ? 'blocked' : 'failed')
638
+ ? (resultError?.retryable ? 'blocked' : 'failed')
636
639
  : postLifecyclePending
637
640
  ? 'completed_with_warnings'
638
641
  : pending.length === 0
@@ -641,7 +644,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
641
644
  ? 'blocked'
642
645
  : 'failed',
643
646
  warnings,
644
- error: hardError ? error : postLifecyclePending ? null : error
647
+ error: hardError ? resultError : postLifecyclePending ? null : resultError
645
648
  };
646
649
  }
647
650
  function preparedResult(taskId, receipt, steps, changed) {
@@ -663,7 +666,6 @@ async function prepareUnderLock(request, taskId, options) {
663
666
  const commentSync = options.commentSync ?? syncPlatformComment;
664
667
  const issueSync = options.issueSync ?? syncPlatformIssue;
665
668
  const issueInspect = options.issueInspect ?? inspectPlatformIssue;
666
- const verify = options.verify ?? verifyTaskEvent;
667
669
  const consumedCapabilities = new Set();
668
670
  let receipt;
669
671
  try {
@@ -732,7 +734,8 @@ async function prepareUnderLock(request, taskId, options) {
732
734
  }
733
735
  const issueOperations = synced.operations;
734
736
  const badOperation = issueOperations?.find((operation) => ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
735
- && operation.status !== 'applied' && operation.status !== 'no-op');
737
+ && operation.status !== 'applied' && operation.status !== 'no-op'
738
+ && !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR'));
736
739
  if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
737
740
  const error = synced.error ?? {
738
741
  code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
@@ -907,88 +910,9 @@ async function prepareUnderLock(request, taskId, options) {
907
910
  metadataProvider: () => ({ timestamp: projection.timestamp, agentInfraVersion: projection.agentInfraVersion })
908
911
  })
909
912
  : undefined;
910
- let verification = null;
911
- if (receipt.verification !== 'pending') {
912
- verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
913
- }
914
- else {
915
- try {
916
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
917
- const verificationState = resolveTaskRef(taskId, { repoRoot });
918
- const verificationEvent = verificationState.ok && verificationState.state === 'completed'
919
- ? 'complete-task.completed'
920
- : 'complete-task.prepared';
921
- const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
922
- verification = verificationStep(result);
923
- if (result.status === 'pass') {
924
- const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
925
- const warnings = applyVerificationWarnings(receipt, result);
926
- receipt = updateReceipt(repoRoot, receipt, {
927
- verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
928
- warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
929
- });
930
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
931
- const finalComment = await syncPendingTaskComment({
932
- repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
933
- });
934
- receipt = finalComment.receipt;
935
- if (finalComment.step.status !== 'no-op')
936
- taskComment = finalComment.step;
937
- changed = changed || finalComment.changed;
938
- if (finalComment.error) {
939
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
940
- }
941
- }
942
- else {
943
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
944
- const warnings = applyVerificationWarnings(receipt, result);
945
- receipt = updateReceipt(repoRoot, receipt, {
946
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
947
- });
948
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
949
- const warningComment = await syncPendingTaskComment({
950
- repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
951
- });
952
- receipt = warningComment.receipt;
953
- taskComment = warningComment.step;
954
- changed = changed || warningComment.changed;
955
- if (warningComment.error) {
956
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
957
- }
958
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
959
- }
960
- }
961
- catch (error) {
962
- const detail = errorOf(error, 'VERIFY_FAILED', true);
963
- try {
964
- const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
965
- receipt = updateReceipt(repoRoot, receipt, {
966
- verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
967
- });
968
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
969
- }
970
- catch (writeError) {
971
- const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
972
- const hardError = {
973
- code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
974
- message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
975
- retryable: true
976
- };
977
- return terminalResult(taskId, receipt, {
978
- backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
979
- }, changed, hardError);
980
- }
981
- try {
982
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities });
983
- receipt = warningComment.receipt;
984
- if (warningComment.step.status !== 'no-op')
985
- taskComment = warningComment.step;
986
- changed = changed || warningComment.changed;
987
- }
988
- catch { /* preserve the primary error */ }
989
- return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
990
- }
991
- }
913
+ const verification = receipt.verification === 'done'
914
+ ? { status: 'no-op', changed: false, error: null }
915
+ : { status: 'skipped', changed: false, error: null };
992
916
  let summary = receipt.summary === 'pending'
993
917
  ? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
994
918
  : { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
@@ -1039,7 +963,7 @@ async function commitPreparedTaskFinalization(request, options) {
1039
963
  if (!resolved.ok)
1040
964
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
1041
965
  try {
1042
- return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
966
+ return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', async () => {
1043
967
  if (options.controlBinding && options.manifestPath) {
1044
968
  try {
1045
969
  const manifest = readSandboxControlManifest(options.manifestPath);
@@ -1091,7 +1015,7 @@ async function commitPreparedTaskFinalization(request, options) {
1091
1015
  return failed(resolved.taskId, {
1092
1016
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
1093
1017
  });
1094
- if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
1018
+ if (receipt.lifecycle === 'done' && receipt.verification !== 'pending' && resolved.state === 'completed') {
1095
1019
  return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
1096
1020
  }
1097
1021
  if (options.controlBinding) {
@@ -1130,7 +1054,7 @@ async function commitPreparedTaskFinalization(request, options) {
1130
1054
  }
1131
1055
  }
1132
1056
  }
1133
- if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1057
+ if (receipt.taskComment === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1134
1058
  return failed(resolved.taskId, {
1135
1059
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
1136
1060
  }, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
@@ -1142,6 +1066,7 @@ async function commitPreparedTaskFinalization(request, options) {
1142
1066
  message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
1143
1067
  retryable: false
1144
1068
  });
1069
+ receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
1145
1070
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1146
1071
  const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
1147
1072
  const step = lifecycleStep(result);
@@ -1149,8 +1074,80 @@ async function commitPreparedTaskFinalization(request, options) {
1149
1074
  const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
1150
1075
  return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
1151
1076
  }
1152
- const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
1153
- return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
1077
+ let verificationResult;
1078
+ try {
1079
+ verificationResult = await (options.verify ?? verifyTaskEvent)({ taskRef: resolved.taskId, event: 'complete-task.completed' }, { repoRoot });
1080
+ }
1081
+ catch (error) {
1082
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
1083
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
1084
+ let updated;
1085
+ try {
1086
+ updated = updateReceipt(repoRoot, receipt, {
1087
+ lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
1088
+ warnings, lastError: detail
1089
+ });
1090
+ }
1091
+ catch (persistCause) {
1092
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1093
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1094
+ persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
1095
+ return failed(resolved.taskId, persistError, {
1096
+ changed: result.changed,
1097
+ lifecycle: step,
1098
+ verification: { status: 'blocked', changed: false, error: persistError },
1099
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1100
+ });
1101
+ }
1102
+ const verification = { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail };
1103
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
1104
+ }
1105
+ const verification = verificationStep(verificationResult);
1106
+ if (verificationResult.status === 'pass') {
1107
+ const resolvedVerificationWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
1108
+ const warnings = applyVerificationWarnings(receipt, verificationResult);
1109
+ const hasOpenWarnings = warnings.some((warning) => warning.status === 'open');
1110
+ let updated;
1111
+ try {
1112
+ updated = updateReceipt(repoRoot, receipt, {
1113
+ lifecycle: 'done', verification: 'done',
1114
+ taskComment: resolvedVerificationWarning ? 'pending' : receipt.taskComment,
1115
+ warningProjection: hasOpenWarnings ? 'pending' : 'done', warnings, lastError: null
1116
+ });
1117
+ }
1118
+ catch (persistCause) {
1119
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1120
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1121
+ return failed(resolved.taskId, persistError, {
1122
+ changed: result.changed,
1123
+ lifecycle: step,
1124
+ verification,
1125
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1126
+ });
1127
+ }
1128
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed);
1129
+ }
1130
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
1131
+ const warnings = applyVerificationWarnings(receipt, verificationResult);
1132
+ let updated;
1133
+ try {
1134
+ updated = updateReceipt(repoRoot, receipt, {
1135
+ lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
1136
+ warnings, lastError: detail
1137
+ });
1138
+ }
1139
+ catch (persistCause) {
1140
+ const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
1141
+ persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
1142
+ persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
1143
+ return failed(resolved.taskId, persistError, {
1144
+ changed: result.changed,
1145
+ lifecycle: step,
1146
+ verification: { status: 'blocked', changed: false, error: persistError },
1147
+ completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
1148
+ });
1149
+ }
1150
+ return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
1154
1151
  });
1155
1152
  }
1156
1153
  catch (error) {
@@ -22,7 +22,6 @@ const VERIFICATION_CATALOG = {
22
22
  'commit.completed': gate('commit', 'active'),
23
23
  'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
24
24
  'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
25
- 'complete-task.prepared': gate('complete-task', 'active'),
26
25
  'complete-task.completed': gate('complete-task', 'completed'),
27
26
  'create-pr.completed': gate('create-pr', 'active'),
28
27
  'create-task.completed': gate('create-task', 'active'),
@@ -132,8 +132,8 @@ async function cmdSync() {
132
132
  ok('Seed files synced successfully!');
133
133
  console.log('');
134
134
  if (workflowPlan.nextSteps.length === 0) {
135
- console.log(' No Agent Client project integration enabled.');
136
- console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
135
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
136
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${CONFIG_PATH}.`);
137
137
  console.log('');
138
138
  }
139
139
  else {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.8",
4
- "sourceInputHash": "6ddb72f8b50137d5148a47172bd7eb2628b17a937ae647ac38350e582ba08124",
5
- "internalExecutableBuildHash": "c886e015506879620bb490f1ea5977d9952f2bbb4515d3a0b49e0ec184b48028"
3
+ "packageVersion": "0.11.9",
4
+ "sourceInputHash": "17fe6c91408c93e3aa5185e816025d590ded7db7e6a305fdce00751e78723abc",
5
+ "internalExecutableBuildHash": "8de9870e4ece1050564d7a3e77ee211a789dd975e3f94593300b7362ed815a00"
6
6
  }