@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
- package/dist/lib/agent-clients/next-steps.js +1 -1
- package/dist/lib/agent-clients/reconcile.js +3 -3
- package/dist/lib/agent-clients/schema.js +18 -10
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/agent-client.js +3 -3
- package/dist/lib/sandbox/recovery.js +1 -22
- package/dist/lib/sandbox/tools.js +2 -0
- package/dist/lib/task/finalization.js +90 -93
- package/dist/lib/task/verification.js +0 -1
- package/dist/lib/update.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/custom-tool-invocations.ts +129 -0
- package/lib/agent-clients/next-steps.ts +3 -3
- package/lib/agent-clients/reconcile.ts +3 -3
- package/lib/agent-clients/schema.ts +18 -10
- package/lib/init.ts +2 -2
- package/lib/internal/agent-client.ts +3 -3
- package/lib/sandbox/recovery.ts +0 -25
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/tools.ts +2 -0
- package/lib/task/finalization.ts +91 -88
- package/lib/task/verification.ts +1 -2
- package/lib/update.ts +2 -3
- package/package.json +1 -1
- package/templates/.agents/README.en.md +2 -50
- package/templates/.agents/README.zh-CN.md +2 -50
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
- package/dist/lib/agent-clients/custom-tuis.js +0 -99
- package/lib/agent-clients/custom-tuis.ts +0 -149
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
type CustomToolInvocation = Readonly<{
|
|
2
|
+
name: string;
|
|
3
|
+
invocation: string;
|
|
4
|
+
}>;
|
|
5
|
+
|
|
6
|
+
type CustomToolInvocationDiagnosticCode =
|
|
7
|
+
| 'INVALID_CUSTOM_TOOL_CONFIG'
|
|
8
|
+
| 'INVALID_CUSTOM_TOOL_INVOCATION'
|
|
9
|
+
| 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER';
|
|
10
|
+
|
|
11
|
+
type CustomToolInvocationDiagnostic = Readonly<{
|
|
12
|
+
code: CustomToolInvocationDiagnosticCode;
|
|
13
|
+
path: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
type NormalizeCustomToolInvocationsResult = Readonly<{
|
|
17
|
+
items: readonly CustomToolInvocation[];
|
|
18
|
+
diagnostics: readonly CustomToolInvocationDiagnostic[];
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
const ALLOWED_PLACEHOLDERS = Object.freeze(['skillName', 'projectName']);
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isNonEmptySingleLine(value: unknown): value is string {
|
|
28
|
+
return typeof value === 'string'
|
|
29
|
+
&& value.trim() !== ''
|
|
30
|
+
&& !/[\r\n]/.test(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasValidPlaceholders(invocation: string): boolean {
|
|
34
|
+
const placeholders = [
|
|
35
|
+
...invocation.matchAll(/\$\{([^}]+)\}/g)
|
|
36
|
+
].map((match) => match[1]!);
|
|
37
|
+
return placeholders.includes('skillName')
|
|
38
|
+
&& placeholders.every((placeholder) =>
|
|
39
|
+
ALLOWED_PLACEHOLDERS.includes(placeholder)
|
|
40
|
+
)
|
|
41
|
+
&& !invocation
|
|
42
|
+
.replaceAll('${skillName}', '')
|
|
43
|
+
.replaceAll('${projectName}', '')
|
|
44
|
+
.includes('${');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeCustomToolInvocations(config: unknown): NormalizeCustomToolInvocationsResult {
|
|
48
|
+
const items: CustomToolInvocation[] = [];
|
|
49
|
+
const diagnostics: CustomToolInvocationDiagnostic[] = [];
|
|
50
|
+
|
|
51
|
+
const root = isRecord(config) ? config : {};
|
|
52
|
+
const sandbox = isRecord(root.sandbox) ? root.sandbox : {};
|
|
53
|
+
const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : { ids: ['agent-infra'] };
|
|
54
|
+
const definitions = sandboxTools.definitions ?? {};
|
|
55
|
+
const selectedIds = sandboxTools.ids;
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(selectedIds)) {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
items: Object.freeze([]),
|
|
60
|
+
diagnostics: Object.freeze([
|
|
61
|
+
Object.freeze({
|
|
62
|
+
code: 'INVALID_CUSTOM_TOOL_CONFIG' as const,
|
|
63
|
+
path: 'sandbox.tools.ids'
|
|
64
|
+
})
|
|
65
|
+
])
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isRecord(definitions)) {
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
items: Object.freeze([]),
|
|
72
|
+
diagnostics: Object.freeze([
|
|
73
|
+
Object.freeze({
|
|
74
|
+
code: 'INVALID_CUSTOM_TOOL_CONFIG' as const,
|
|
75
|
+
path: 'sandbox.tools.definitions'
|
|
76
|
+
})
|
|
77
|
+
])
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [index, id] of selectedIds.entries()) {
|
|
82
|
+
if (typeof id !== 'string') {
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
code: 'INVALID_CUSTOM_TOOL_INVOCATION',
|
|
85
|
+
path: `sandbox.tools.ids[${index}]`
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const candidate = definitions[id];
|
|
90
|
+
if (!isRecord(candidate)) continue;
|
|
91
|
+
if (candidate.invoke === undefined) continue;
|
|
92
|
+
const base = `sandbox.tools.definitions.${id}`;
|
|
93
|
+
const name = candidate.name === undefined ? id : candidate.name;
|
|
94
|
+
if (!isNonEmptySingleLine(name)) {
|
|
95
|
+
diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.name` });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!isNonEmptySingleLine(candidate.invoke)) {
|
|
99
|
+
diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.invoke` });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!hasValidPlaceholders(candidate.invoke)) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
code: 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER',
|
|
105
|
+
path: `${base}.invoke`
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
items.push(Object.freeze({
|
|
110
|
+
name,
|
|
111
|
+
invocation: candidate.invoke
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
items: Object.freeze(items),
|
|
117
|
+
diagnostics: Object.freeze(
|
|
118
|
+
diagnostics.map((diagnostic) => Object.freeze(diagnostic))
|
|
119
|
+
)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { normalizeCustomToolInvocations };
|
|
124
|
+
export type {
|
|
125
|
+
CustomToolInvocation,
|
|
126
|
+
CustomToolInvocationDiagnostic,
|
|
127
|
+
CustomToolInvocationDiagnosticCode,
|
|
128
|
+
NormalizeCustomToolInvocationsResult
|
|
129
|
+
};
|
|
@@ -2,7 +2,7 @@ import { listEnabledAgentClientAdapters } from './registry.ts';
|
|
|
2
2
|
import { renderAgentClientInvocation } from './invocation.ts';
|
|
3
3
|
import semver from 'semver';
|
|
4
4
|
import { isTaskScopeSkill } from '../task/skill-scope.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { CustomToolInvocation } from './custom-tool-invocations.ts';
|
|
6
6
|
import type { AgentClientId, AgentClientState } from './types.ts';
|
|
7
7
|
|
|
8
8
|
type NextStepCommand = Readonly<{
|
|
@@ -15,7 +15,7 @@ type NextStepCommand = Readonly<{
|
|
|
15
15
|
type RenderNextStepsInput = Readonly<{
|
|
16
16
|
projectName: string;
|
|
17
17
|
state: AgentClientState;
|
|
18
|
-
|
|
18
|
+
customToolInvocations: readonly CustomToolInvocation[];
|
|
19
19
|
skillName: string;
|
|
20
20
|
taskRef?: string;
|
|
21
21
|
version?: string;
|
|
@@ -69,7 +69,7 @@ function renderNextStepCommands(
|
|
|
69
69
|
)
|
|
70
70
|
})
|
|
71
71
|
);
|
|
72
|
-
const custom = input.
|
|
72
|
+
const custom = input.customToolInvocations.map(
|
|
73
73
|
(tool): NextStepCommand => Object.freeze({
|
|
74
74
|
source: 'custom',
|
|
75
75
|
displayName: tool.name,
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import defaults from '../defaults.json' with { type: 'json' };
|
|
6
6
|
import { normalizeAgentClients, serializeAgentClients } from './config.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeCustomToolInvocations } from './custom-tool-invocations.ts';
|
|
8
8
|
import { renderNextStepCommands } from './next-steps.ts';
|
|
9
9
|
import { planAgentClientProjectAssets } from './project-assets.ts';
|
|
10
10
|
import {
|
|
@@ -216,11 +216,11 @@ function planAgentClientReconciliation(input: Readonly<{
|
|
|
216
216
|
delete (nextConfig.files as Record<string, unknown>).managedBaselines;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const custom =
|
|
219
|
+
const custom = normalizeCustomToolInvocations(input.config);
|
|
220
220
|
const nextSteps = renderNextStepCommands({
|
|
221
221
|
projectName,
|
|
222
222
|
state: desired,
|
|
223
|
-
|
|
223
|
+
customToolInvocations: custom.items,
|
|
224
224
|
skillName: 'update-agent-infra'
|
|
225
225
|
});
|
|
226
226
|
const actionableSeed = seedOperations.some((operation) =>
|
|
@@ -43,16 +43,24 @@ const AGENT_CLIENTS_SCHEMA = {
|
|
|
43
43
|
}
|
|
44
44
|
}))
|
|
45
45
|
},
|
|
46
|
-
|
|
47
|
-
type: '
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
sandbox: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
tools: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
ids: { type: 'array', items: { type: 'string' } },
|
|
53
|
+
definitions: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
additionalProperties: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
name: { type: 'string' },
|
|
59
|
+
invoke: { type: 'string' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
}
|
package/lib/init.ts
CHANGED
|
@@ -314,8 +314,8 @@ async function cmdInit(): Promise<void> {
|
|
|
314
314
|
console.log(' npm install -g @fitlab-ai/agent-infra');
|
|
315
315
|
console.log('');
|
|
316
316
|
if (workflowPlan.nextSteps.length === 0) {
|
|
317
|
-
console.log(' No Agent Client
|
|
318
|
-
console.log(` Configure "
|
|
317
|
+
console.log(' No Agent Client or sandbox custom tool integration enabled.');
|
|
318
|
+
console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${configPath}.`);
|
|
319
319
|
console.log('');
|
|
320
320
|
} else {
|
|
321
321
|
console.log(' Next step: open this project in any AI TUI and run:');
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
AgentClientConfigError,
|
|
6
6
|
normalizeAgentClients
|
|
7
7
|
} from '../agent-clients/config.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.ts';
|
|
9
9
|
import { renderNextStepCommands } from '../agent-clients/next-steps.ts';
|
|
10
10
|
import { getAgentClientModelSelection } from '../agent-clients/registry.ts';
|
|
11
11
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
@@ -148,11 +148,11 @@ function agentClient(args: string[] = []): void {
|
|
|
148
148
|
|
|
149
149
|
try {
|
|
150
150
|
const clients = normalizeAgentClients(config);
|
|
151
|
-
const custom =
|
|
151
|
+
const custom = normalizeCustomToolInvocations(config);
|
|
152
152
|
const commands = renderNextStepCommands({
|
|
153
153
|
projectName: String(config.project ?? ''),
|
|
154
154
|
state: clients.state,
|
|
155
|
-
|
|
155
|
+
customToolInvocations: custom.items,
|
|
156
156
|
skillName: parsed.skillName,
|
|
157
157
|
...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
|
|
158
158
|
...(parsed.version ? { version: parsed.version } : {})
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -8,12 +8,10 @@ import {
|
|
|
8
8
|
containerNameCandidates,
|
|
9
9
|
sandboxBranchLabel,
|
|
10
10
|
sandboxLabel,
|
|
11
|
-
sandboxRuntimeCapabilityLabel,
|
|
12
11
|
sandboxTaskIdLabel,
|
|
13
12
|
sandboxWorkspaceModeLabel,
|
|
14
13
|
worktreeDirCandidates
|
|
15
14
|
} from './constants.ts';
|
|
16
|
-
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
17
15
|
import {
|
|
18
16
|
createSandboxCapabilityPlan
|
|
19
17
|
} from './agent-client-reconciler.ts';
|
|
@@ -100,8 +98,6 @@ export type SandboxRecoverySnapshot = {
|
|
|
100
98
|
workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
|
|
101
99
|
taskView?: { path: string; readable: boolean };
|
|
102
100
|
runtimeStoreOk?: boolean;
|
|
103
|
-
runtimeCapabilityOk?: boolean;
|
|
104
|
-
unexpectedCapabilityMounts?: string[];
|
|
105
101
|
mounts: Array<{
|
|
106
102
|
path: string;
|
|
107
103
|
expectedType: string;
|
|
@@ -452,12 +448,6 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
452
448
|
: `Container identity ${actualIdentity} on branch ${snapshot.actualBranch ?? 'unknown'} does not match requested ${expectedIdentity} on branch ${snapshot.expectedBranch}.`
|
|
453
449
|
});
|
|
454
450
|
}
|
|
455
|
-
if (snapshot.runtimeCapabilityOk === false) {
|
|
456
|
-
findings.push({
|
|
457
|
-
repairKind: 'hard-failure',
|
|
458
|
-
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
451
|
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
462
452
|
findings.push({
|
|
463
453
|
repairKind: 'hard-failure',
|
|
@@ -482,14 +472,6 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
482
472
|
path: '/run/agent-infra/runtime'
|
|
483
473
|
});
|
|
484
474
|
}
|
|
485
|
-
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
486
|
-
findings.push({
|
|
487
|
-
repairKind: 'hard-failure',
|
|
488
|
-
message: `Disabled Agent Client capability remains mounted at ${mountPath}.`,
|
|
489
|
-
path: mountPath
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
475
|
for (const mount of snapshot.mounts) {
|
|
494
476
|
if (
|
|
495
477
|
snapshot.workspaceTopology === 'legacy-parent'
|
|
@@ -964,11 +946,6 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
964
946
|
mode: sandboxWorkspaceModeLabel(params.config),
|
|
965
947
|
taskId: sandboxTaskIdLabel(params.config)
|
|
966
948
|
});
|
|
967
|
-
const selectedToolIds = new Set(tools.map((tool) => tool.id));
|
|
968
|
-
const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
|
|
969
|
-
.filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
|
|
970
|
-
.map((tool) => tool.containerMount)
|
|
971
|
-
.filter((mountPath) => mountsByDestination.has(mountPath));
|
|
972
949
|
const mountSnapshots = expectedMounts({
|
|
973
950
|
config: params.config,
|
|
974
951
|
branch: params.branch,
|
|
@@ -1035,8 +1012,6 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
1035
1012
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
1036
1013
|
&& branchLabel === params.branch
|
|
1037
1014
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
1038
|
-
runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
|
|
1039
|
-
unexpectedCapabilityMounts,
|
|
1040
1015
|
mounts: mountSnapshots,
|
|
1041
1016
|
tmpfs,
|
|
1042
1017
|
seeds,
|
|
@@ -10,6 +10,7 @@ type SandboxTool = {
|
|
|
10
10
|
containerMount: string;
|
|
11
11
|
versionCmd: string;
|
|
12
12
|
setupHint: string;
|
|
13
|
+
invoke?: string;
|
|
13
14
|
envVars?: Record<string, string>;
|
|
14
15
|
hostPreSeedFiles?: Array<{ hostPath: string; sandboxName: string }>;
|
|
15
16
|
hostPreSeedDirs?: Array<{ hostDir: string; sandboxSubdir: string }>;
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -231,6 +231,7 @@ export function parseCustomTool(
|
|
|
231
231
|
if (!containerMount.startsWith('/')) {
|
|
232
232
|
throw new Error(`${context}: field "containerMount" must be an absolute path`);
|
|
233
233
|
}
|
|
234
|
+
const invoke = asOptionalNonEmptyString(entry.invoke, 'invoke', context);
|
|
234
235
|
|
|
235
236
|
const tool: SandboxTool = {
|
|
236
237
|
id,
|
|
@@ -241,6 +242,7 @@ export function parseCustomTool(
|
|
|
241
242
|
versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
|
|
242
243
|
setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
|
|
243
244
|
?? `Run \`${id}\` inside the container to set up.`,
|
|
245
|
+
...(invoke === undefined ? {} : { invoke }),
|
|
244
246
|
envVars: asStringRecord(entry.envVars, 'envVars', context),
|
|
245
247
|
hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
|
|
246
248
|
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
package/lib/task/finalization.ts
CHANGED
|
@@ -795,9 +795,12 @@ function terminalResult(
|
|
|
795
795
|
const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
|
|
796
796
|
const warnings = openWarnings(receipt);
|
|
797
797
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
798
|
-
const
|
|
798
|
+
const terminalVerificationPending = receipt.lifecycle === 'done' && receipt.verification === 'pending';
|
|
799
|
+
const resultError = error ?? (terminalVerificationPending ? steps.verification?.error : null) ?? null;
|
|
800
|
+
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID'
|
|
801
|
+
|| terminalVerificationPending;
|
|
799
802
|
return {
|
|
800
|
-
status: hardError ? (
|
|
803
|
+
status: hardError ? (resultError?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
801
804
|
changed,
|
|
802
805
|
taskId,
|
|
803
806
|
backfill: steps.backfill ?? null,
|
|
@@ -808,7 +811,7 @@ function terminalResult(
|
|
|
808
811
|
completedSteps: completedSteps(receipt),
|
|
809
812
|
pendingSteps: pending,
|
|
810
813
|
result: hardError
|
|
811
|
-
? (
|
|
814
|
+
? (resultError?.retryable ? 'blocked' : 'failed')
|
|
812
815
|
: postLifecyclePending
|
|
813
816
|
? 'completed_with_warnings'
|
|
814
817
|
: pending.length === 0
|
|
@@ -817,7 +820,7 @@ function terminalResult(
|
|
|
817
820
|
? 'blocked'
|
|
818
821
|
: 'failed',
|
|
819
822
|
warnings,
|
|
820
|
-
error: hardError ?
|
|
823
|
+
error: hardError ? resultError : postLifecyclePending ? null : resultError
|
|
821
824
|
};
|
|
822
825
|
}
|
|
823
826
|
|
|
@@ -850,7 +853,6 @@ async function prepareUnderLock(
|
|
|
850
853
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
851
854
|
const issueSync = options.issueSync ?? syncPlatformIssue;
|
|
852
855
|
const issueInspect = options.issueInspect ?? inspectPlatformIssue;
|
|
853
|
-
const verify = options.verify ?? verifyTaskEvent;
|
|
854
856
|
const consumedCapabilities = new Set<string>();
|
|
855
857
|
let receipt: TaskFinalizationReceipt;
|
|
856
858
|
try {
|
|
@@ -922,6 +924,7 @@ async function prepareUnderLock(
|
|
|
922
924
|
const badOperation = issueOperations?.find((operation) =>
|
|
923
925
|
['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
|
|
924
926
|
&& operation.status !== 'applied' && operation.status !== 'no-op'
|
|
927
|
+
&& !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR')
|
|
925
928
|
);
|
|
926
929
|
if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
|
|
927
930
|
const error = synced.error ?? {
|
|
@@ -1080,84 +1083,9 @@ async function prepareUnderLock(
|
|
|
1080
1083
|
})
|
|
1081
1084
|
: undefined;
|
|
1082
1085
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
} else {
|
|
1087
|
-
try {
|
|
1088
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1089
|
-
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
1090
|
-
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
1091
|
-
? 'complete-task.completed'
|
|
1092
|
-
: 'complete-task.prepared';
|
|
1093
|
-
const result = await verify(
|
|
1094
|
-
{ taskRef: taskId, event: verificationEvent },
|
|
1095
|
-
{ repoRoot }
|
|
1096
|
-
);
|
|
1097
|
-
verification = verificationStep(result);
|
|
1098
|
-
if (result.status === 'pass') {
|
|
1099
|
-
const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
1100
|
-
const warnings = applyVerificationWarnings(receipt, result);
|
|
1101
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
1102
|
-
verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
|
|
1103
|
-
warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
|
|
1104
|
-
});
|
|
1105
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1106
|
-
const finalComment = await syncPendingTaskComment({
|
|
1107
|
-
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
1108
|
-
});
|
|
1109
|
-
receipt = finalComment.receipt;
|
|
1110
|
-
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
1111
|
-
changed = changed || finalComment.changed;
|
|
1112
|
-
if (finalComment.error) {
|
|
1113
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
1114
|
-
}
|
|
1115
|
-
} else {
|
|
1116
|
-
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
1117
|
-
const warnings = applyVerificationWarnings(receipt, result);
|
|
1118
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
1119
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
1120
|
-
});
|
|
1121
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1122
|
-
const warningComment = await syncPendingTaskComment({
|
|
1123
|
-
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
1124
|
-
});
|
|
1125
|
-
receipt = warningComment.receipt;
|
|
1126
|
-
taskComment = warningComment.step;
|
|
1127
|
-
changed = changed || warningComment.changed;
|
|
1128
|
-
if (warningComment.error) {
|
|
1129
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
|
|
1130
|
-
}
|
|
1131
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
1132
|
-
}
|
|
1133
|
-
} catch (error) {
|
|
1134
|
-
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
1135
|
-
try {
|
|
1136
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
1137
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
1138
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
1139
|
-
});
|
|
1140
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1141
|
-
} catch (writeError) {
|
|
1142
|
-
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1143
|
-
const hardError: FinalizationError = {
|
|
1144
|
-
code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
|
|
1145
|
-
message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
|
|
1146
|
-
retryable: true
|
|
1147
|
-
};
|
|
1148
|
-
return terminalResult(taskId, receipt, {
|
|
1149
|
-
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
1150
|
-
}, changed, hardError);
|
|
1151
|
-
}
|
|
1152
|
-
try {
|
|
1153
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities });
|
|
1154
|
-
receipt = warningComment.receipt;
|
|
1155
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
1156
|
-
changed = changed || warningComment.changed;
|
|
1157
|
-
} catch { /* preserve the primary error */ }
|
|
1158
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1086
|
+
const verification: TaskFinalizationStep = receipt.verification === 'done'
|
|
1087
|
+
? { status: 'no-op', changed: false, error: null }
|
|
1088
|
+
: { status: 'skipped', changed: false, error: null };
|
|
1161
1089
|
|
|
1162
1090
|
let summary = receipt.summary === 'pending'
|
|
1163
1091
|
? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
|
|
@@ -1208,7 +1136,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1208
1136
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
1209
1137
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1210
1138
|
try {
|
|
1211
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
1139
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', async () => {
|
|
1212
1140
|
if (options.controlBinding && options.manifestPath) {
|
|
1213
1141
|
try {
|
|
1214
1142
|
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
@@ -1255,7 +1183,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1255
1183
|
if (!receipt) return failed(resolved.taskId, {
|
|
1256
1184
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
1257
1185
|
});
|
|
1258
|
-
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
1186
|
+
if (receipt.lifecycle === 'done' && receipt.verification !== 'pending' && resolved.state === 'completed') {
|
|
1259
1187
|
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
1260
1188
|
}
|
|
1261
1189
|
if (options.controlBinding) {
|
|
@@ -1291,7 +1219,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1291
1219
|
}
|
|
1292
1220
|
}
|
|
1293
1221
|
}
|
|
1294
|
-
if (receipt.taskComment === 'pending' || receipt.
|
|
1222
|
+
if (receipt.taskComment === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1295
1223
|
return failed(resolved.taskId, {
|
|
1296
1224
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
1297
1225
|
}, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
|
|
@@ -1302,6 +1230,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1302
1230
|
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
1303
1231
|
retryable: false
|
|
1304
1232
|
});
|
|
1233
|
+
receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
|
|
1305
1234
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
1306
1235
|
const result = lifecycle(
|
|
1307
1236
|
{ taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
|
|
@@ -1312,8 +1241,82 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1312
1241
|
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
|
|
1313
1242
|
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
|
|
1314
1243
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1244
|
+
|
|
1245
|
+
let verificationResult: TaskVerificationResult;
|
|
1246
|
+
try {
|
|
1247
|
+
verificationResult = await (options.verify ?? verifyTaskEvent)(
|
|
1248
|
+
{ taskRef: resolved.taskId, event: 'complete-task.completed' },
|
|
1249
|
+
{ repoRoot }
|
|
1250
|
+
);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
1253
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
1254
|
+
let updated: TaskFinalizationReceipt;
|
|
1255
|
+
try {
|
|
1256
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1257
|
+
lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
|
|
1258
|
+
warnings, lastError: detail
|
|
1259
|
+
});
|
|
1260
|
+
} catch (persistCause) {
|
|
1261
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1262
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1263
|
+
persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
|
|
1264
|
+
return failed(resolved.taskId, persistError, {
|
|
1265
|
+
changed: result.changed,
|
|
1266
|
+
lifecycle: step,
|
|
1267
|
+
verification: { status: 'blocked', changed: false, error: persistError },
|
|
1268
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const verification: TaskFinalizationStep = { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail };
|
|
1272
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const verification = verificationStep(verificationResult);
|
|
1276
|
+
if (verificationResult.status === 'pass') {
|
|
1277
|
+
const resolvedVerificationWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
1278
|
+
const warnings = applyVerificationWarnings(receipt, verificationResult);
|
|
1279
|
+
const hasOpenWarnings = warnings.some((warning) => warning.status === 'open');
|
|
1280
|
+
let updated: TaskFinalizationReceipt;
|
|
1281
|
+
try {
|
|
1282
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1283
|
+
lifecycle: 'done', verification: 'done',
|
|
1284
|
+
taskComment: resolvedVerificationWarning ? 'pending' : receipt.taskComment,
|
|
1285
|
+
warningProjection: hasOpenWarnings ? 'pending' : 'done', warnings, lastError: null
|
|
1286
|
+
});
|
|
1287
|
+
} catch (persistCause) {
|
|
1288
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1289
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1290
|
+
return failed(resolved.taskId, persistError, {
|
|
1291
|
+
changed: result.changed,
|
|
1292
|
+
lifecycle: step,
|
|
1293
|
+
verification,
|
|
1294
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
1301
|
+
const warnings = applyVerificationWarnings(receipt, verificationResult);
|
|
1302
|
+
let updated: TaskFinalizationReceipt;
|
|
1303
|
+
try {
|
|
1304
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1305
|
+
lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
|
|
1306
|
+
warnings, lastError: detail
|
|
1307
|
+
});
|
|
1308
|
+
} catch (persistCause) {
|
|
1309
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1310
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1311
|
+
persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
|
|
1312
|
+
return failed(resolved.taskId, persistError, {
|
|
1313
|
+
changed: result.changed,
|
|
1314
|
+
lifecycle: step,
|
|
1315
|
+
verification: { status: 'blocked', changed: false, error: persistError },
|
|
1316
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
|
|
1317
1320
|
});
|
|
1318
1321
|
} catch (error) {
|
|
1319
1322
|
const detail = error instanceof TaskExecutionLockError
|
package/lib/task/verification.ts
CHANGED
|
@@ -13,7 +13,7 @@ type VerificationEvent =
|
|
|
13
13
|
| 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
|
|
14
14
|
| 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
|
|
15
15
|
| 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
|
|
16
|
-
| 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.
|
|
16
|
+
| 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.completed'
|
|
17
17
|
| 'create-pr.completed' | 'create-task.completed'
|
|
18
18
|
| 'import-codescan.completed' | 'import-dependabot.completed'
|
|
19
19
|
| 'import-issue.completed' | 'watch-pr.completed'
|
|
@@ -68,7 +68,6 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
|
|
|
68
68
|
'commit.completed': gate('commit', 'active'),
|
|
69
69
|
'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
|
|
70
70
|
'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
|
|
71
|
-
'complete-task.prepared': gate('complete-task', 'active'),
|
|
72
71
|
'complete-task.completed': gate('complete-task', 'completed'),
|
|
73
72
|
'create-pr.completed': gate('create-pr', 'active'),
|
|
74
73
|
'create-task.completed': gate('create-task', 'active'),
|
package/lib/update.ts
CHANGED
|
@@ -27,7 +27,6 @@ type UpdateConfig = {
|
|
|
27
27
|
labels?: Record<string, unknown>;
|
|
28
28
|
files?: Partial<FileRegistry>;
|
|
29
29
|
agentClients?: unknown;
|
|
30
|
-
customTUIs?: unknown;
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
type Defaults = {
|
|
@@ -189,8 +188,8 @@ async function cmdSync(): Promise<void> {
|
|
|
189
188
|
ok('Seed files synced successfully!');
|
|
190
189
|
console.log('');
|
|
191
190
|
if (workflowPlan.nextSteps.length === 0) {
|
|
192
|
-
console.log(' No Agent Client
|
|
193
|
-
console.log(` Configure "
|
|
191
|
+
console.log(' No Agent Client or sandbox custom tool integration enabled.');
|
|
192
|
+
console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${CONFIG_PATH}.`);
|
|
194
193
|
console.log('');
|
|
195
194
|
} else {
|
|
196
195
|
console.log(' Next step: run the full update in your AI TUI:');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.9",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|