@fitlab-ai/agent-infra 0.11.6 → 0.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +125 -7
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +132 -8
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- 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/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
package/lib/sandbox/config.ts
CHANGED
|
@@ -32,7 +32,7 @@ type WriteStderr = (chunk: string) => unknown;
|
|
|
32
32
|
type SandboxConfigInput = {
|
|
33
33
|
engine?: SandboxEngineInput;
|
|
34
34
|
runtimes?: string[];
|
|
35
|
-
tools?:
|
|
35
|
+
tools?: unknown;
|
|
36
36
|
customTools?: unknown;
|
|
37
37
|
refreshIntervalDays?: unknown;
|
|
38
38
|
dockerfile?: string | null;
|
|
@@ -115,10 +115,22 @@ function resolveSandboxToolIds(
|
|
|
115
115
|
sandbox: SandboxConfigInput,
|
|
116
116
|
defaults: readonly string[]
|
|
117
117
|
): string[] {
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
118
|
+
if (sandbox.tools === undefined) {
|
|
119
|
+
return [...defaults];
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
if (typeof sandbox.tools !== 'object' || sandbox.tools === null || Array.isArray(sandbox.tools)) {
|
|
122
|
+
throw new Error('sandbox.tools must be an object when configured');
|
|
123
|
+
}
|
|
124
|
+
const tools = sandbox.tools as Record<string, unknown>;
|
|
125
|
+
if (!Array.isArray(tools.ids)) {
|
|
126
|
+
throw new Error('sandbox.tools.ids must be an array');
|
|
127
|
+
}
|
|
128
|
+
return tools.ids.map((id, index) => {
|
|
129
|
+
if (typeof id !== 'string') {
|
|
130
|
+
throw new Error(`sandbox.tools.ids[${index}] must be a string`);
|
|
131
|
+
}
|
|
132
|
+
return id;
|
|
133
|
+
});
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
export function loadConfig({
|
|
@@ -180,7 +192,13 @@ export function loadConfig({
|
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
const
|
|
195
|
+
const toolsConfig = sandbox.tools && typeof sandbox.tools === 'object' && !Array.isArray(sandbox.tools)
|
|
196
|
+
? sandbox.tools as Record<string, unknown>
|
|
197
|
+
: undefined;
|
|
198
|
+
if (sandbox.customTools !== undefined) {
|
|
199
|
+
throw new Error('sandbox.customTools is no longer supported; use sandbox.tools.definitions');
|
|
200
|
+
}
|
|
201
|
+
const customTools = parseCustomTools(toolsConfig?.definitions, { home });
|
|
184
202
|
const tools = resolveSandboxToolIds(sandbox, defaults.tools);
|
|
185
203
|
|
|
186
204
|
return {
|
|
@@ -272,12 +272,15 @@ function taskCreateDomainEvidence(
|
|
|
272
272
|
? output.task as Record<string, unknown> : null;
|
|
273
273
|
const taskId = typeof task?.id === 'string' ? task.id : null;
|
|
274
274
|
const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
|
|
275
|
-
|
|
275
|
+
const state = task?.state;
|
|
276
|
+
if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state))) return { consistent: false };
|
|
277
|
+
if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null) return { consistent: false };
|
|
276
278
|
try {
|
|
277
279
|
const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
|
|
278
280
|
const shortIds = loadShortIdByTaskId(manifest.repoRoot);
|
|
279
281
|
return {
|
|
280
|
-
consistent: resolved.ok && resolved.taskId === taskId && resolved.state ===
|
|
282
|
+
consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
|
|
283
|
+
&& (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
|
|
281
284
|
taskId,
|
|
282
285
|
shortId
|
|
283
286
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const SANDBOX_GIT_COMMON_DIR = '/run/agent-infra/git';
|
|
6
|
+
|
|
7
|
+
export type SandboxGitMetadata = Readonly<{
|
|
8
|
+
commonDir: string;
|
|
9
|
+
worktreeGitFile: string;
|
|
10
|
+
worktreeGitFileContent: string;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
function gitText(worktreeRoot: string, args: string[]): string {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
for (const key of [
|
|
16
|
+
'GIT_DIR',
|
|
17
|
+
'GIT_COMMON_DIR',
|
|
18
|
+
'GIT_WORK_TREE',
|
|
19
|
+
'GIT_INDEX_FILE',
|
|
20
|
+
'GIT_PREFIX',
|
|
21
|
+
'GIT_NAMESPACE',
|
|
22
|
+
'GIT_OBJECT_DIRECTORY',
|
|
23
|
+
'GIT_CEILING_DIRECTORIES',
|
|
24
|
+
'GIT_DISCOVERY_ACROSS_FILESYSTEM'
|
|
25
|
+
]) {
|
|
26
|
+
delete env[key];
|
|
27
|
+
}
|
|
28
|
+
return execFileSync('git', ['-C', worktreeRoot, ...args], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
env
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveSandboxGitMetadata(
|
|
36
|
+
worktreeRoot: string,
|
|
37
|
+
controlDir: string
|
|
38
|
+
): SandboxGitMetadata {
|
|
39
|
+
const commonDir = fs.realpathSync.native(gitText(worktreeRoot, [
|
|
40
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
41
|
+
]));
|
|
42
|
+
const gitDir = fs.realpathSync.native(gitText(worktreeRoot, ['rev-parse', '--absolute-git-dir']));
|
|
43
|
+
const relativeGitDir = path.relative(commonDir, gitDir);
|
|
44
|
+
if (path.isAbsolute(relativeGitDir) || relativeGitDir.startsWith(`..${path.sep}`) || relativeGitDir === '..') {
|
|
45
|
+
throw new Error('SANDBOX_GIT_METADATA_INVALID: worktree git directory is outside its common directory');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const containerGitDir = relativeGitDir === '.'
|
|
49
|
+
? SANDBOX_GIT_COMMON_DIR
|
|
50
|
+
: path.posix.join(SANDBOX_GIT_COMMON_DIR, ...relativeGitDir.split(path.sep));
|
|
51
|
+
return {
|
|
52
|
+
commonDir,
|
|
53
|
+
worktreeGitFile: path.join(controlDir, 'workspace.git'),
|
|
54
|
+
worktreeGitFileContent: `gitdir: ${containerGitDir}\n`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function materializeSandboxGitMetadata(
|
|
59
|
+
worktreeRoot: string,
|
|
60
|
+
controlDir: string
|
|
61
|
+
): SandboxGitMetadata {
|
|
62
|
+
const metadata = resolveSandboxGitMetadata(worktreeRoot, controlDir);
|
|
63
|
+
fs.mkdirSync(controlDir, { recursive: true });
|
|
64
|
+
fs.writeFileSync(metadata.worktreeGitFile, metadata.worktreeGitFileContent, 'utf8');
|
|
65
|
+
return metadata;
|
|
66
|
+
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
rm <branch | TASK-id> | --unbound | --purge
|
|
21
21
|
Remove one sandbox; use a full TASK-id for task-bound
|
|
22
22
|
cleanup and a branch for branch-only cleanup. --unbound
|
|
23
|
-
removes
|
|
23
|
+
removes sandboxes not bound to an active task short id.
|
|
24
24
|
start [--recreate] <branch | TASK-id | N>
|
|
25
25
|
Verify or recover an existing sandbox container;
|
|
26
26
|
optionally replace only the container on failure
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './constants.ts';
|
|
8
8
|
import type { SandboxConfig } from './config.ts';
|
|
9
9
|
import { sandboxWorkspaceViewStatePaths } from './workspace-view.ts';
|
|
10
|
+
import { SANDBOX_GIT_COMMON_DIR } from './git-metadata.ts';
|
|
10
11
|
|
|
11
12
|
export type SandboxBindMountDeclaration = {
|
|
12
13
|
hostPaths: string[];
|
|
@@ -26,6 +27,8 @@ export function sandboxCoreBindMounts(
|
|
|
26
27
|
workspaceViewRoot: string;
|
|
27
28
|
controlDir: string;
|
|
28
29
|
controlStatusDir: string;
|
|
30
|
+
gitCommonDir?: string;
|
|
31
|
+
worktreeGitFile?: string;
|
|
29
32
|
runtimeDir?: string;
|
|
30
33
|
taskSources?: string[];
|
|
31
34
|
taskId?: string;
|
|
@@ -46,7 +49,17 @@ export function sandboxCoreBindMounts(
|
|
|
46
49
|
readOnly: true
|
|
47
50
|
}
|
|
48
51
|
);
|
|
52
|
+
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
53
|
+
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
54
|
+
}
|
|
55
|
+
const gitCommonMount = overrides.gitCommonDir
|
|
56
|
+
? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
|
|
57
|
+
: null;
|
|
58
|
+
const worktreeGitFileMount = overrides.worktreeGitFile
|
|
59
|
+
? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
|
|
60
|
+
: null;
|
|
49
61
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
62
|
+
...(gitCommonMount ? [gitCommonMount] : []),
|
|
50
63
|
{
|
|
51
64
|
hostPaths: overrides.worktree
|
|
52
65
|
? [overrides.worktree]
|
|
@@ -54,6 +67,7 @@ export function sandboxCoreBindMounts(
|
|
|
54
67
|
containerPath: '/workspace',
|
|
55
68
|
readOnly: false
|
|
56
69
|
},
|
|
70
|
+
...(worktreeGitFileMount ? [worktreeGitFileMount] : []),
|
|
57
71
|
...workspaceMounts,
|
|
58
72
|
];
|
|
59
73
|
if (taskBound) {
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
import type { SandboxConfig } from './config.ts';
|
|
25
25
|
import { toEnginePath } from './engines/wsl2-paths.ts';
|
|
26
26
|
import { sandboxCoreBindMounts } from './mounts.ts';
|
|
27
|
+
import { resolveSandboxGitMetadata } from './git-metadata.ts';
|
|
27
28
|
import {
|
|
28
29
|
assertSandboxTaskSource,
|
|
29
30
|
sandboxControlPaths,
|
|
@@ -764,7 +765,19 @@ function expectedMounts(params: {
|
|
|
764
765
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
765
766
|
: null;
|
|
766
767
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
768
|
+
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) =>
|
|
769
|
+
fs.existsSync(path.join(candidate, '.git'))
|
|
770
|
+
);
|
|
771
|
+
const gitMetadata = worktrees.length === 1
|
|
772
|
+
? resolveSandboxGitMetadata(worktrees[0]!, control.channelDir)
|
|
773
|
+
: null;
|
|
767
774
|
const core = sandboxCoreBindMounts(config, branch, {
|
|
775
|
+
...(gitMetadata === null
|
|
776
|
+
? {}
|
|
777
|
+
: {
|
|
778
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
779
|
+
worktreeGitFile: gitMetadata.worktreeGitFile
|
|
780
|
+
}),
|
|
768
781
|
workspaceViewRoot: view.root,
|
|
769
782
|
controlDir: control.channelDir,
|
|
770
783
|
controlStatusDir: control.statusDir,
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './sh
|
|
|
22
22
|
import {
|
|
23
23
|
parseSandboxWorkspaceIdentity,
|
|
24
24
|
resolveSandboxCleanupTarget,
|
|
25
|
+
resolveSandboxTarget,
|
|
25
26
|
sameSandboxWorkspaceIdentity,
|
|
26
27
|
type SandboxCleanupTarget,
|
|
27
28
|
type SandboxWorkspaceKey
|
|
@@ -1112,7 +1113,10 @@ async function rmUnbound(
|
|
|
1112
1113
|
tools: SandboxTool[],
|
|
1113
1114
|
options: { dryRun: boolean; assumeYes: boolean }
|
|
1114
1115
|
): Promise<void> {
|
|
1115
|
-
return
|
|
1116
|
+
return withRepositoryMutationLock(
|
|
1117
|
+
config.repoRoot,
|
|
1118
|
+
() => rmUnboundCore(config, tools, options)
|
|
1119
|
+
);
|
|
1116
1120
|
}
|
|
1117
1121
|
|
|
1118
1122
|
async function rmUnboundCore(
|
|
@@ -1127,32 +1131,66 @@ async function rmUnboundCore(
|
|
|
1127
1131
|
sandboxBranchLabel(config),
|
|
1128
1132
|
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
1129
1133
|
);
|
|
1130
|
-
const
|
|
1134
|
+
const discovered = [...listed.running, ...listed.nonRunning];
|
|
1135
|
+
const branchMismatches: Array<{ container: string; labelBranch: string; taskBranch: string }> = [];
|
|
1136
|
+
const classified = discovered.map((row) => {
|
|
1137
|
+
if (!row.branch) return { row, protected: true, cleanupTarget: null };
|
|
1138
|
+
if (row.workspaceMode === 'task-bound' && row.taskId) {
|
|
1139
|
+
try {
|
|
1140
|
+
resolveSandboxTarget(row.taskId, config.repoRoot);
|
|
1141
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1142
|
+
} catch {
|
|
1143
|
+
try {
|
|
1144
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
|
|
1145
|
+
if (cleanupTarget.taskState === 'blocked'
|
|
1146
|
+
|| cleanupTarget.taskState === 'completed'
|
|
1147
|
+
|| cleanupTarget.taskState === 'archive') {
|
|
1148
|
+
if (row.branch !== cleanupTarget.branch) {
|
|
1149
|
+
branchMismatches.push({ container: row.name, labelBranch: row.branch, taskBranch: cleanupTarget.branch });
|
|
1150
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1151
|
+
}
|
|
1152
|
+
const activeBranchTarget = resolveSandboxTarget(cleanupTarget.branch, config.repoRoot);
|
|
1153
|
+
if (activeBranchTarget.workspace.mode === 'branch-only') {
|
|
1154
|
+
return { row, protected: false, cleanupTarget };
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
// An unresolved task identity has no active short-id evidence, so keep it out of bulk cleanup.
|
|
1159
|
+
}
|
|
1160
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (row.workspaceMode !== 'branch-only') return { row, protected: true, cleanupTarget: null };
|
|
1164
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.branch, config.repoRoot);
|
|
1165
|
+
return cleanupTarget.workspace.mode === 'branch-only'
|
|
1166
|
+
? { row, protected: false, cleanupTarget }
|
|
1167
|
+
: { row, protected: true, cleanupTarget: null };
|
|
1168
|
+
});
|
|
1169
|
+
const protectedBranches = new Set(classified
|
|
1170
|
+
.filter((entry) => entry.protected)
|
|
1171
|
+
.map(({ row }) => row.branch));
|
|
1172
|
+
const targets = classified
|
|
1173
|
+
.filter((entry) => !entry.protected && !protectedBranches.has(entry.row.branch) && entry.cleanupTarget)
|
|
1174
|
+
.map(({ row, cleanupTarget }) => ({
|
|
1175
|
+
row,
|
|
1176
|
+
cleanupTarget: cleanupTarget!,
|
|
1177
|
+
target: resolveRmTarget(config, tools, cleanupTarget!, { discoveredContainers: [row.name] })
|
|
1178
|
+
}));
|
|
1131
1179
|
p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
|
|
1180
|
+
for (const mismatch of branchMismatches) {
|
|
1181
|
+
p.log.warn(`Skipping task-bound sandbox '${mismatch.container}': container branch '${mismatch.labelBranch}' differs from task record branch '${mismatch.taskBranch}'. Inspect the task record and container labels before retrying cleanup.`);
|
|
1182
|
+
}
|
|
1132
1183
|
if (options.dryRun) {
|
|
1133
|
-
p.outro(`Dry run: ${
|
|
1184
|
+
p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
|
|
1134
1185
|
return;
|
|
1135
1186
|
}
|
|
1136
|
-
for (const row of
|
|
1137
|
-
|
|
1138
|
-
runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
const cleanupTarget: SandboxCleanupTarget = {
|
|
1142
|
-
requestedRef: row.branch,
|
|
1143
|
-
branch: row.branch,
|
|
1144
|
-
workspace: row.workspaceMode === 'task-bound' && row.taskId
|
|
1145
|
-
? { mode: 'task-bound', taskId: row.taskId }
|
|
1146
|
-
: { mode: 'branch-only' },
|
|
1147
|
-
taskState: 'branch-only'
|
|
1148
|
-
};
|
|
1149
|
-
const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
|
|
1150
|
-
await rmOne(config, tools, cleanupTarget.branch, {
|
|
1187
|
+
for (const { row, cleanupTarget, target } of targets) {
|
|
1188
|
+
await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
|
|
1151
1189
|
assumeYes: options.assumeYes,
|
|
1152
1190
|
target,
|
|
1153
1191
|
cleanupTarget
|
|
1154
1192
|
});
|
|
1155
1193
|
}
|
|
1156
|
-
p.outro(pc.green(`Removed ${
|
|
1194
|
+
p.outro(pc.green(`Removed ${targets.length} sandbox(es)`));
|
|
1157
1195
|
}
|
|
1158
1196
|
export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
|
|
@@ -16,7 +16,7 @@ type SandboxTool = {
|
|
|
16
16
|
pathRewriteFiles?: string[];
|
|
17
17
|
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
|
|
18
18
|
postSetupCmds?: string[];
|
|
19
|
-
tmpfs?: { size?: string; seed?: string[] };
|
|
19
|
+
tmpfs?: { size?: string; seed?: string[]; exec?: boolean };
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
type SandboxAlias = Readonly<{
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -85,6 +85,16 @@ function asOptionalNonEmptyString(value: unknown, field: string, context: string
|
|
|
85
85
|
return value;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function asOptionalBoolean(value: unknown, field: string, context: string): boolean | undefined {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== 'boolean') {
|
|
93
|
+
throw new Error(`${context}: field "${field}" must be a boolean when provided`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
function asStringRecord(value: unknown, field: string, context: string): Record<string, string> | undefined {
|
|
89
99
|
if (value === undefined) {
|
|
90
100
|
return undefined;
|
|
@@ -193,18 +203,20 @@ function parseTmpfs(value: unknown, context: string): SandboxTool['tmpfs'] {
|
|
|
193
203
|
if (!isPlainObject(value)) {
|
|
194
204
|
throw new Error(`${context}: field "tmpfs" must be an object when provided`);
|
|
195
205
|
}
|
|
206
|
+
const exec = asOptionalBoolean(value.exec, 'tmpfs.exec', context);
|
|
196
207
|
return {
|
|
197
208
|
size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
|
|
198
|
-
seed: asStringArray(value.seed, 'tmpfs.seed', context)
|
|
209
|
+
seed: asStringArray(value.seed, 'tmpfs.seed', context),
|
|
210
|
+
...(exec === undefined ? {} : { exec })
|
|
199
211
|
};
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
export function parseCustomTool(
|
|
203
215
|
entry: unknown,
|
|
204
216
|
index: number,
|
|
205
|
-
options: { home: string }
|
|
217
|
+
options: { home: string },
|
|
218
|
+
context = `customTools[${index}]`
|
|
206
219
|
): SandboxTool {
|
|
207
|
-
const context = `customTools[${index}]`;
|
|
208
220
|
if (!isPlainObject(entry)) {
|
|
209
221
|
throw new Error(`${context} must be an object`);
|
|
210
222
|
}
|
|
@@ -243,13 +255,30 @@ export function parseCustomTool(
|
|
|
243
255
|
}
|
|
244
256
|
|
|
245
257
|
export function parseCustomTools(value: unknown, options: { home: string }): SandboxTool[] {
|
|
246
|
-
if (value === undefined
|
|
258
|
+
if (value === undefined) {
|
|
247
259
|
return [];
|
|
248
260
|
}
|
|
249
|
-
if (!
|
|
250
|
-
throw new Error('sandbox
|
|
261
|
+
if (!isPlainObject(value)) {
|
|
262
|
+
throw new Error('sandbox.tools.definitions must be an object');
|
|
251
263
|
}
|
|
252
|
-
return value.map((
|
|
264
|
+
return Object.entries(value).map(([id, definition], index) => {
|
|
265
|
+
const context = `sandbox.tools.definitions.${id}`;
|
|
266
|
+
if (!TOOL_ID_PATTERN.test(id)) {
|
|
267
|
+
throw new Error(`${context}: tool id must match ${TOOL_ID_PATTERN.source}`);
|
|
268
|
+
}
|
|
269
|
+
if (!isPlainObject(definition)) {
|
|
270
|
+
throw new Error(`${context} must be an object`);
|
|
271
|
+
}
|
|
272
|
+
if (Object.hasOwn(definition, 'id')) {
|
|
273
|
+
throw new Error(`${context}.id is not allowed; the definition key is the tool id`);
|
|
274
|
+
}
|
|
275
|
+
return parseCustomTool(
|
|
276
|
+
{ ...definition, id },
|
|
277
|
+
index,
|
|
278
|
+
options,
|
|
279
|
+
context
|
|
280
|
+
);
|
|
281
|
+
});
|
|
253
282
|
}
|
|
254
283
|
|
|
255
284
|
export function resolveTools(config: ToolsConfig): SandboxTool[] {
|
package/lib/server/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.ts';
|
|
6
6
|
|
|
7
7
|
export type ServerLogConfig = {
|
|
8
8
|
path: string;
|
|
@@ -52,17 +52,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
52
52
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
// directory, keyed by the .airc.json "project" AND a stable hash of the repo
|
|
57
|
-
// root path:
|
|
58
|
-
// ~/.agent-infra/logs/<project>/<repo-hash>/server.log
|
|
59
|
-
// ~/.agent-infra/run/<project>/<repo-hash>/server.pid
|
|
60
|
-
// The <project> segment groups a project's checkouts for readability; the
|
|
61
|
-
// <repo-hash> segment guarantees that two checkouts/worktrees of the same
|
|
62
|
-
// project (same "project" but different absolute path) get ISOLATED runtime
|
|
63
|
-
// dirs, so they never read/control each other's daemon. Using os.homedir() +
|
|
64
|
-
// path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
|
|
65
|
-
// An explicit log.path in server.json/.local/env still overrides the log default.
|
|
55
|
+
// Runtime state is shared by all worktrees through the primary checkout.
|
|
66
56
|
function resolveProjectKey(repoRoot: string): string {
|
|
67
57
|
try {
|
|
68
58
|
const airc = JSON.parse(
|
|
@@ -77,13 +67,8 @@ function resolveProjectKey(repoRoot: string): string {
|
|
|
77
67
|
return path.basename(repoRoot);
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runtimePath(repoRoot: string, projectKey: string, kind: 'logs' | 'run', file: string): string {
|
|
86
|
-
return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
|
|
70
|
+
function runtimePath(projectKey: string, kind: 'logs' | 'run', file: string): string {
|
|
71
|
+
return path.join(homedir(), '.agent-infra', kind, projectKey, file);
|
|
87
72
|
}
|
|
88
73
|
|
|
89
74
|
function detectRepoRoot(): string {
|
|
@@ -185,7 +170,7 @@ export function validateServerConfig(committed: Record<string, unknown>): Server
|
|
|
185
170
|
}
|
|
186
171
|
|
|
187
172
|
export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): ServerConfig {
|
|
188
|
-
const repoRoot = rootDir ?? detectRepoRoot();
|
|
173
|
+
const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
|
|
189
174
|
const agentsDir = path.join(repoRoot, '.agents');
|
|
190
175
|
|
|
191
176
|
const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
|
|
@@ -205,18 +190,11 @@ export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): Server
|
|
|
205
190
|
const projectKey = resolveProjectKey(repoRoot);
|
|
206
191
|
|
|
207
192
|
const log = isPlainObject(merged.log) ? merged.log : {};
|
|
208
|
-
// No explicit log.path → default under ~/.agent-infra/logs/<project>/.
|
|
209
|
-
// Explicit relative path resolves against the repo root; absolute is used as-is.
|
|
210
|
-
const explicitPath = typeof log.path === 'string' ? log.path : null;
|
|
211
|
-
const resolvedLogPath = explicitPath === null
|
|
212
|
-
? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
|
|
213
|
-
: (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
|
|
214
|
-
|
|
215
193
|
return {
|
|
216
194
|
repoRoot,
|
|
217
|
-
pidFile: runtimePath(
|
|
195
|
+
pidFile: runtimePath(projectKey, 'run', 'server.pid'),
|
|
218
196
|
log: {
|
|
219
|
-
path:
|
|
197
|
+
path: runtimePath(projectKey, 'logs', 'server.log'),
|
|
220
198
|
rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
|
|
221
199
|
},
|
|
222
200
|
heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
|
package/lib/server/daemon.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
|
|
|
83
83
|
chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
|
|
84
84
|
throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
|
|
85
85
|
},
|
|
86
|
-
(emit) => runAiImpl(plan.argv, { onChunk: emit }),
|
|
86
|
+
(emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }),
|
|
87
87
|
(outbound) => replyOutbound(message, outbound)
|
|
88
88
|
);
|
|
89
89
|
}
|
|
@@ -100,15 +100,17 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
|
|
|
100
100
|
// - runDaemon() awaits a shutdown promise that only resolves once a
|
|
101
101
|
// SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
|
|
102
102
|
// the only keep-alive timer (that would let the process exit immediately).
|
|
103
|
-
export async function runDaemon(): Promise<void> {
|
|
103
|
+
export async function runDaemon({ rootDir }: { rootDir?: string } = {}): Promise<void> {
|
|
104
104
|
let config;
|
|
105
105
|
try {
|
|
106
|
-
config = loadServerConfig();
|
|
106
|
+
config = loadServerConfig({ rootDir });
|
|
107
107
|
} catch (error) {
|
|
108
108
|
process.stderr.write(`${errorMessage(error)}\n`);
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
process.chdir(config.repoRoot);
|
|
113
|
+
|
|
112
114
|
const logger = createLogger(config.log);
|
|
113
115
|
logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
|
|
114
116
|
const ownStartTime = getProcessStartTime(process.pid);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { spawn, execFileSync } from 'node:child_process';
|
|
3
4
|
import { loadServerConfig } from './config.ts';
|
|
4
5
|
import type { ServerConfig } from './config.ts';
|
|
@@ -91,7 +92,7 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
if (foreground) {
|
|
94
|
-
await runDaemon();
|
|
95
|
+
await runDaemon({ rootDir: config.repoRoot });
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -103,8 +104,9 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
|
|
|
103
104
|
// Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
|
|
104
105
|
// (node --experimental-strip-types ./bin/cli.ts) and the built path
|
|
105
106
|
// (node dist/bin/cli.js) both work.
|
|
106
|
-
const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
|
|
107
|
+
const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
|
|
107
108
|
detached: true,
|
|
109
|
+
cwd: config.repoRoot,
|
|
108
110
|
stdio: 'ignore'
|
|
109
111
|
});
|
|
110
112
|
child.unref();
|
package/lib/server/runner.ts
CHANGED
|
@@ -10,8 +10,9 @@ export type RunnerResult = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export type RunnerOptions = {
|
|
13
|
-
spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void
|
|
13
|
+
spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void>, cwd?: string) => Promise<RunnerResult>;
|
|
14
14
|
onChunk?: (chunk: string) => void | Promise<void>;
|
|
15
|
+
cwd?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
function resolveCommand(file: string): string {
|
|
@@ -40,13 +41,15 @@ function needsShell(file: string): boolean {
|
|
|
40
41
|
function spawnCapture(
|
|
41
42
|
file: string,
|
|
42
43
|
args: string[],
|
|
43
|
-
onChunk?: (chunk: string) => void | Promise<void
|
|
44
|
+
onChunk?: (chunk: string) => void | Promise<void>,
|
|
45
|
+
cwd?: string
|
|
44
46
|
): Promise<RunnerResult> {
|
|
45
47
|
return new Promise((resolve, reject) => {
|
|
46
48
|
const resolvedFile = resolveCommand(file);
|
|
47
49
|
const child = spawn(resolvedFile, args, {
|
|
48
50
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
-
shell: needsShell(resolvedFile)
|
|
51
|
+
shell: needsShell(resolvedFile),
|
|
52
|
+
...(cwd === undefined ? {} : { cwd })
|
|
50
53
|
});
|
|
51
54
|
let stdout = '';
|
|
52
55
|
let stderr = '';
|
|
@@ -91,5 +94,5 @@ function spawnCapture(
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
export async function runAi(args: string[], options: RunnerOptions = {}): Promise<RunnerResult> {
|
|
94
|
-
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
|
|
97
|
+
return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
|
|
95
98
|
}
|
|
@@ -13,10 +13,6 @@
|
|
|
13
13
|
"type": "object",
|
|
14
14
|
"additionalProperties": false,
|
|
15
15
|
"properties": {
|
|
16
|
-
"path": {
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "Log file path. Defaults to ~/.agent-infra/logs/<project>/<repo-hash>/server.log (outside the repo; the repo-hash isolates checkouts that share a project). Relative paths resolve against the repo root; absolute paths are used as-is."
|
|
19
|
-
},
|
|
20
16
|
"rotateAtBytes": {
|
|
21
17
|
"type": "integer",
|
|
22
18
|
"minimum": 0,
|
|
@@ -108,6 +108,7 @@ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
|
|
|
108
108
|
const VERDICTS: ReadonlySet<string> = new Set(['approved', 'changes-requested', 'commented']);
|
|
109
109
|
const OUTCOMES: ReadonlySet<string> = new Set(['aborted', 'superseded']);
|
|
110
110
|
const SUCCESS_STATUSES: ReadonlySet<PrReviewArtifactStatus> = new Set(['applied', 'no-op']);
|
|
111
|
+
const LOCAL_COMPLETION_STATUSES: ReadonlySet<PrReviewArtifactStatus> = new Set(['applied', 'no-op', 'blocked', 'failed']);
|
|
111
112
|
const ARTIFACT_STATUSES: ReadonlySet<string> = new Set([
|
|
112
113
|
'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
|
|
113
114
|
]);
|
|
@@ -209,7 +210,7 @@ function readSnapshot(taskRef: string, repoRoot?: string):
|
|
|
209
210
|
if (terminal.length === 1) {
|
|
210
211
|
const note = terminal[0]!.note;
|
|
211
212
|
const terminalMatchesStatus =
|
|
212
|
-
(note.startsWith('Verdict: ') &&
|
|
213
|
+
(note.startsWith('Verdict: ') && LOCAL_COMPLETION_STATUSES.has(parsed.status))
|
|
213
214
|
|| (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
|
|
214
215
|
|| (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
|
|
215
216
|
|| (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
|
|
@@ -338,8 +339,8 @@ function applyLocked(intent: PrReviewActivityIntent, normalizedAgent: string, op
|
|
|
338
339
|
if (roundState.startedAgent === null) {
|
|
339
340
|
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
|
|
340
341
|
}
|
|
341
|
-
if (intent.kind === 'pr-review-complete' && !
|
|
342
|
-
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must
|
|
342
|
+
if (intent.kind === 'pr-review-complete' && !LOCAL_COMPLETION_STATUSES.has(roundState.status)) {
|
|
343
|
+
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must record a publication outcome before completion`, state.taskId);
|
|
343
344
|
}
|
|
344
345
|
if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
|
|
345
346
|
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
|