@fitlab-ai/agent-infra 0.9.10 → 0.9.11
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 +13 -3
- package/README.zh-CN.md +13 -3
- package/bin/cli.ts +17 -4
- package/dist/bin/cli.js +19 -4
- package/dist/bin/internal-cli.js +50 -4
- package/dist/lib/agent-client.js +0 -1
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
- package/dist/lib/agent-clients/adapters/traecli.js +7 -8
- package/dist/lib/agent-clients/config.js +14 -99
- package/dist/lib/agent-clients/reconcile.js +1 -11
- package/dist/lib/agent-clients/schema.js +5 -4
- package/dist/lib/agent-clients/tokens.js +5 -15
- package/dist/lib/decide.js +14 -30
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/release-workflow.js +0 -1
- package/dist/lib/internal/task-finalization.js +19 -9
- package/dist/lib/internal/task-ledger.js +8 -2
- package/dist/lib/internal/task-lifecycle.js +17 -93
- package/dist/lib/internal/task-orchestration.js +30 -216
- package/dist/lib/internal/task-validate.js +2 -1
- package/dist/lib/merge.js +205 -103
- package/dist/lib/process-data/normalize.js +2 -9
- package/dist/lib/run/host.js +46 -16
- package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
- package/dist/lib/sandbox/commands/create.js +3 -3
- package/dist/lib/sandbox/commands/enter.js +18 -4
- package/dist/lib/sandbox/config.js +3 -12
- package/dist/lib/sandbox/control/executor.js +131 -39
- package/dist/lib/sandbox/control/lifecycle.js +29 -20
- package/dist/lib/sandbox/control/server.js +18 -16
- package/dist/lib/sandbox/mounts.js +3 -2
- package/dist/lib/sandbox/recovery.js +123 -22
- package/dist/lib/sandbox/workspace-identity.js +14 -0
- package/dist/lib/sandbox/workspace-view.js +8 -9
- package/dist/lib/self-update.js +278 -0
- package/dist/lib/server/protocol.js +5 -5
- package/dist/lib/task/activity-log.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +4 -2
- package/dist/lib/task/codex-orchestration.js +1 -1
- package/dist/lib/task/command-options.js +1 -3
- package/dist/lib/task/commands/cat.js +0 -5
- package/dist/lib/task/commands/decisions.js +76 -92
- package/dist/lib/task/commands/files.js +6 -6
- package/dist/lib/task/commands/grep.js +25 -61
- package/dist/lib/task/commands/issue-body.js +6 -6
- package/dist/lib/task/commands/log.js +25 -10
- package/dist/lib/task/commands/show.js +5 -6
- package/dist/lib/task/commands/status.js +8 -8
- package/dist/lib/task/commit-operation.js +21 -25
- package/dist/lib/task/commit-policy.js +1 -1
- package/dist/lib/task/control-authority.js +431 -0
- package/dist/lib/task/create.js +6 -1
- package/dist/lib/task/current-contract.js +35 -0
- package/dist/lib/task/decision-details.js +263 -0
- package/dist/lib/task/decision-intents.js +5 -2
- package/dist/lib/task/events.js +11 -5
- package/dist/lib/task/index.js +19 -20
- package/dist/lib/task/ledger-intents.js +8 -9
- package/dist/lib/task/ledger.js +13 -6
- package/dist/lib/task/lifecycle.js +16 -0
- package/dist/lib/task/orchestration.js +50 -13
- package/dist/lib/task/review-artifacts.js +31 -12
- package/dist/lib/task/review-finalization.js +19 -6
- package/dist/lib/task/verification-engine.js +38 -15
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/task/workspace-snapshot.js +5 -11
- package/dist/lib/task/write.js +11 -1
- package/dist/lib/update.js +6 -37
- package/dist/lib/version.js +5 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +0 -1
- package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
- package/lib/agent-clients/adapters/traecli.ts +7 -8
- package/lib/agent-clients/config.ts +14 -144
- package/lib/agent-clients/reconcile.ts +1 -12
- package/lib/agent-clients/schema.ts +5 -4
- package/lib/agent-clients/tokens.ts +5 -18
- package/lib/decide.ts +12 -22
- package/lib/defaults.json +4 -0
- package/lib/init.ts +2 -2
- package/lib/internal/release-workflow.ts +0 -1
- package/lib/internal/task-finalization.ts +27 -13
- package/lib/internal/task-ledger.ts +8 -2
- package/lib/internal/task-lifecycle.ts +33 -76
- package/lib/internal/task-orchestration.ts +33 -190
- package/lib/internal/task-validate.ts +3 -2
- package/lib/merge.ts +236 -120
- package/lib/process-data/normalize.ts +2 -8
- package/lib/run/host.ts +63 -14
- package/lib/sandbox/agent-client-reconciler.ts +5 -15
- package/lib/sandbox/commands/create.ts +4 -4
- package/lib/sandbox/commands/enter.ts +24 -4
- package/lib/sandbox/config.ts +2 -14
- package/lib/sandbox/control/container-identity.ts +2 -2
- package/lib/sandbox/control/executor.ts +148 -51
- package/lib/sandbox/control/lifecycle.ts +34 -28
- package/lib/sandbox/control/protocol.ts +1 -3
- package/lib/sandbox/control/server.ts +15 -12
- package/lib/sandbox/control/state.ts +7 -7
- package/lib/sandbox/mounts.ts +4 -3
- package/lib/sandbox/recovery.ts +162 -34
- package/lib/sandbox/tools.ts +1 -1
- package/lib/sandbox/workspace-identity.ts +28 -0
- package/lib/sandbox/workspace-view.ts +14 -10
- package/lib/self-update.ts +341 -0
- package/lib/server/protocol.ts +5 -5
- package/lib/task/activity-log.ts +6 -1
- package/lib/task/artifact-lifecycle.ts +5 -3
- package/lib/task/codex-orchestration.ts +2 -1
- package/lib/task/command-options.ts +1 -2
- package/lib/task/commands/cat.ts +0 -4
- package/lib/task/commands/decisions.ts +80 -75
- package/lib/task/commands/files.ts +6 -6
- package/lib/task/commands/grep.ts +23 -51
- package/lib/task/commands/issue-body.ts +6 -6
- package/lib/task/commands/log.ts +22 -10
- package/lib/task/commands/show.ts +5 -6
- package/lib/task/commands/status.ts +8 -8
- package/lib/task/commit-operation.ts +25 -30
- package/lib/task/commit-policy.ts +1 -2
- package/lib/task/control-authority.ts +563 -0
- package/lib/task/create.ts +6 -1
- package/lib/task/current-contract.ts +49 -0
- package/lib/task/decision-details.ts +340 -0
- package/lib/task/decision-intents.ts +7 -2
- package/lib/task/events.ts +10 -5
- package/lib/task/index.ts +19 -20
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/ledger.ts +17 -7
- package/lib/task/lifecycle.ts +20 -0
- package/lib/task/orchestration.ts +40 -15
- package/lib/task/review-artifacts.ts +53 -15
- package/lib/task/review-finalization.ts +33 -7
- package/lib/task/verification-engine.ts +33 -18
- package/lib/task/verification-review.ts +1 -1
- package/lib/task/workspace-snapshot.ts +4 -13
- package/lib/task/write.ts +20 -1
- package/lib/update.ts +6 -40
- package/lib/version.ts +6 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +5 -3
- package/templates/.agents/README.zh-CN.md +5 -3
- package/templates/.agents/rules/compatibility-policy.en.md +5 -4
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +9 -7
- package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
- package/templates/.agents/rules/issue-fields.github.en.md +1 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +1 -1
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +3 -3
- package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/release-commands.github.en.md +6 -0
- package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/review-handshake.en.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +1 -2
- package/templates/.agents/rules/task-management.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/SKILL.en.md +27 -7
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
- package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/analyze-task.en.md +1 -1
- package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
- package/templates/.claude/commands/block-task.en.md +1 -1
- package/templates/.claude/commands/block-task.zh-CN.md +1 -1
- package/templates/.claude/commands/cancel-task.en.md +1 -1
- package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
- package/templates/.claude/commands/check-task.en.md +1 -1
- package/templates/.claude/commands/check-task.zh-CN.md +1 -1
- package/templates/.claude/commands/code-task.en.md +1 -1
- package/templates/.claude/commands/code-task.zh-CN.md +1 -1
- package/templates/.claude/commands/commit.en.md +1 -1
- package/templates/.claude/commands/commit.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-task.en.md +1 -1
- package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
- package/templates/.claude/commands/create-pr.en.md +1 -1
- package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
- package/templates/.claude/commands/plan-task.en.md +1 -1
- package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
- package/templates/.claude/commands/review-analysis.en.md +1 -1
- package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
- package/templates/.claude/commands/review-code.en.md +1 -1
- package/templates/.claude/commands/review-code.zh-CN.md +1 -1
- package/templates/.claude/commands/review-plan.en.md +1 -1
- package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/run-task.en.md +1 -1
- package/templates/.claude/commands/run-task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
- package/dist/lib/builtin-tuis.js +0 -40
- package/lib/builtin-tuis.ts +0 -56
package/lib/merge.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { info, ok } from './log.ts';
|
|
4
3
|
import { removeDirRecursive } from './remove-dir.ts';
|
|
4
|
+
import { validateCurrentTaskContract } from './task/current-contract.ts';
|
|
5
5
|
|
|
6
6
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
7
7
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
8
8
|
const TITLE_RE = /^# (.+)$/m;
|
|
9
|
-
const DATE_FROM_PATH_RE = /(?:^|[/\\])(\d{4})[/\\](\d{2})[/\\](\d{2})(?:[/\\]|$)/;
|
|
10
9
|
const MUTABLE_SECTIONS = ['active', 'blocked', 'completed'] as const;
|
|
11
10
|
const ALL_SECTIONS = [...MUTABLE_SECTIONS, 'archive'] as const;
|
|
12
11
|
type MutableSection = typeof MUTABLE_SECTIONS[number];
|
|
13
12
|
type Section = typeof ALL_SECTIONS[number];
|
|
14
13
|
type MutableAction = 'copied' | 'updated' | 'moved' | 'skipped';
|
|
15
14
|
type ArchiveAction = 'copied' | 'skipped';
|
|
16
|
-
type SourceMode = 'workspace'
|
|
15
|
+
type SourceMode = 'workspace';
|
|
17
16
|
type DateParts = {
|
|
18
17
|
year: string;
|
|
19
18
|
month: string;
|
|
@@ -129,40 +128,6 @@ function normalizeTaskRecord(taskDir: string, taskFile: string, dateParts: DateP
|
|
|
129
128
|
};
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
function fallbackDateParts(taskDir: string, content: string): DateParts | null {
|
|
133
|
-
const pathMatch = taskDir.match(DATE_FROM_PATH_RE);
|
|
134
|
-
if (pathMatch) {
|
|
135
|
-
const [, year, month, day] = pathMatch;
|
|
136
|
-
if (!year || !month || !day) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
year,
|
|
141
|
-
month,
|
|
142
|
-
day
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const completedAt = extractField(content, 'completed_at');
|
|
147
|
-
const updatedAt = extractField(content, 'updated_at');
|
|
148
|
-
const source = completedAt || updatedAt;
|
|
149
|
-
const dateMatch = source?.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
150
|
-
|
|
151
|
-
if (dateMatch) {
|
|
152
|
-
const [, year, month, day] = dateMatch;
|
|
153
|
-
if (!year || !month || !day) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
year,
|
|
158
|
-
month,
|
|
159
|
-
day
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
131
|
function scanSourceTasks(sourceDir: string): TaskRecord[] {
|
|
167
132
|
const tasks: TaskRecord[] = [];
|
|
168
133
|
const years = fs.existsSync(sourceDir) ? fs.readdirSync(sourceDir, { withFileTypes: true }) : [];
|
|
@@ -206,44 +171,6 @@ function scanSourceTasks(sourceDir: string): TaskRecord[] {
|
|
|
206
171
|
}
|
|
207
172
|
}
|
|
208
173
|
|
|
209
|
-
if (tasks.length > 0) {
|
|
210
|
-
return tasks;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Fall back to a deeper scan if the source layout is unusual but still contains archived tasks.
|
|
214
|
-
const stack = [sourceDir];
|
|
215
|
-
while (stack.length > 0) {
|
|
216
|
-
const currentDir = stack.pop();
|
|
217
|
-
if (!currentDir || !fs.existsSync(currentDir)) {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
222
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
223
|
-
if (!entry.isDirectory()) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (TASK_ID_RE.test(entry.name)) {
|
|
228
|
-
const taskFile = path.join(entryPath, 'task.md');
|
|
229
|
-
if (!fs.existsSync(taskFile)) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const content = fs.readFileSync(taskFile, 'utf8');
|
|
234
|
-
const dateParts = fallbackDateParts(entryPath, content);
|
|
235
|
-
if (!dateParts) {
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
tasks.push(normalizeTaskRecord(entryPath, taskFile, dateParts));
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
stack.push(entryPath);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
174
|
return tasks.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
248
175
|
}
|
|
249
176
|
|
|
@@ -624,15 +551,230 @@ function copyTaskToSection(sourceTask: WorkspaceRecord, workspaceDir: string): s
|
|
|
624
551
|
return destinationDir;
|
|
625
552
|
}
|
|
626
553
|
|
|
554
|
+
function invalidSource(message: string): never {
|
|
555
|
+
throw new Error(`Invalid merge source: ${message}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function sourceRelativePath(sourceRoot: string, targetPath: string): string {
|
|
559
|
+
return path.relative(sourceRoot, targetPath).split(path.sep).join('/') || '.';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function isMissingPathError(error: unknown): boolean {
|
|
563
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function assertSourcePathContained(sourceRoot: string, targetPath: string): void {
|
|
567
|
+
let sourceRealPath: string;
|
|
568
|
+
let targetRealPath: string;
|
|
569
|
+
try {
|
|
570
|
+
sourceRealPath = fs.realpathSync(sourceRoot);
|
|
571
|
+
targetRealPath = fs.realpathSync(targetPath);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} cannot be resolved: ${error instanceof Error ? error.message : String(error)}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const relativePath = path.relative(sourceRealPath, targetRealPath);
|
|
577
|
+
if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
578
|
+
invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} resolves outside the source workspace`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function sourcePathStat(sourceRoot: string, targetPath: string): fs.Stats | null {
|
|
583
|
+
let stat: fs.Stats;
|
|
584
|
+
try {
|
|
585
|
+
stat = fs.lstatSync(targetPath);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (isMissingPathError(error)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (stat.isSymbolicLink()) {
|
|
594
|
+
invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} must not be a symbolic link`);
|
|
595
|
+
}
|
|
596
|
+
assertSourcePathContained(sourceRoot, targetPath);
|
|
597
|
+
return stat;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function validateTaskTree(sourceRoot: string, taskDir: string): void {
|
|
601
|
+
for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
|
|
602
|
+
const entryPath = path.join(taskDir, entry.name);
|
|
603
|
+
const entryStat = sourcePathStat(sourceRoot, entryPath);
|
|
604
|
+
if (!entryStat) {
|
|
605
|
+
invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} disappeared during validation`);
|
|
606
|
+
}
|
|
607
|
+
if (entryStat.isDirectory()) {
|
|
608
|
+
validateTaskTree(sourceRoot, entryPath);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!entryStat.isFile()) {
|
|
612
|
+
invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} must be a regular file or directory`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function validateSourceRoot(sourceRoot: string): void {
|
|
618
|
+
const stat = sourcePathStat(sourceRoot, sourceRoot);
|
|
619
|
+
if (!stat || !stat.isDirectory()) {
|
|
620
|
+
invalidSource('source path must be a real directory');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function validateTaskDirectory(
|
|
625
|
+
sourceRoot: string,
|
|
626
|
+
taskDir: string,
|
|
627
|
+
seenTaskIds: Map<string, string>
|
|
628
|
+
): void {
|
|
629
|
+
const taskId = path.basename(taskDir);
|
|
630
|
+
const taskFile = path.join(taskDir, 'task.md');
|
|
631
|
+
const taskDirStat = sourcePathStat(sourceRoot, taskDir);
|
|
632
|
+
if (!taskDirStat || !taskDirStat.isDirectory()) {
|
|
633
|
+
invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
|
|
634
|
+
}
|
|
635
|
+
const taskFileStat = sourcePathStat(sourceRoot, taskFile);
|
|
636
|
+
if (!taskFileStat || !taskFileStat.isFile()) {
|
|
637
|
+
invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} requires a regular task.md file`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const content = fs.readFileSync(taskFile, 'utf8');
|
|
641
|
+
const contract = validateCurrentTaskContract(content);
|
|
642
|
+
if (!contract.ok) {
|
|
643
|
+
invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} is not a valid current task: ${contract.message}`);
|
|
644
|
+
}
|
|
645
|
+
if (contract.metadata.id !== taskId) {
|
|
646
|
+
invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
|
|
647
|
+
}
|
|
648
|
+
validateTaskTree(sourceRoot, taskDir);
|
|
649
|
+
|
|
650
|
+
const previousPath = seenTaskIds.get(taskId);
|
|
651
|
+
if (previousPath) {
|
|
652
|
+
invalidSource(`duplicate task ID ${taskId} at ${previousPath} and ${sourceRelativePath(sourceRoot, taskDir)}`);
|
|
653
|
+
}
|
|
654
|
+
seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function validateMutableSection(
|
|
658
|
+
sourceRoot: string,
|
|
659
|
+
section: MutableSection,
|
|
660
|
+
seenTaskIds: Map<string, string>
|
|
661
|
+
): void {
|
|
662
|
+
const sectionDir = path.join(sourceRoot, section);
|
|
663
|
+
const sectionStat = sourcePathStat(sourceRoot, sectionDir);
|
|
664
|
+
if (!sectionStat) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!sectionStat.isDirectory()) {
|
|
668
|
+
invalidSource(`${section} must be a directory`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) {
|
|
672
|
+
const entryPath = path.join(sectionDir, entry.name);
|
|
673
|
+
const entryStat = sourcePathStat(sourceRoot, entryPath);
|
|
674
|
+
if (!entryStat) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (!entryStat.isDirectory()) {
|
|
678
|
+
if (TASK_ID_RE.test(entry.name)) {
|
|
679
|
+
invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} is not a directory`);
|
|
680
|
+
}
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!TASK_ID_RE.test(entry.name)) {
|
|
684
|
+
invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} is not a direct TASK-YYYYMMDD-HHMMSS directory`);
|
|
685
|
+
}
|
|
686
|
+
validateTaskDirectory(sourceRoot, entryPath, seenTaskIds);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function validateArchiveSection(
|
|
691
|
+
sourceRoot: string,
|
|
692
|
+
seenTaskIds: Map<string, string>
|
|
693
|
+
): void {
|
|
694
|
+
const archiveDir = path.join(sourceRoot, 'archive');
|
|
695
|
+
const archiveStat = sourcePathStat(sourceRoot, archiveDir);
|
|
696
|
+
if (!archiveStat) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (!archiveStat.isDirectory()) {
|
|
700
|
+
invalidSource('archive must be a directory');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const allowManifest = (directory: string, entry: fs.Dirent): boolean => {
|
|
704
|
+
const entryPath = path.join(directory, entry.name);
|
|
705
|
+
const entryStat = sourcePathStat(sourceRoot, entryPath);
|
|
706
|
+
return Boolean(entryStat?.isFile() && entry.name === 'manifest.md');
|
|
707
|
+
};
|
|
708
|
+
const years = fs.readdirSync(archiveDir, { withFileTypes: true });
|
|
709
|
+
for (const yearEntry of years) {
|
|
710
|
+
const yearPath = path.join(archiveDir, yearEntry.name);
|
|
711
|
+
if (allowManifest(archiveDir, yearEntry)) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const yearStat = sourcePathStat(sourceRoot, yearPath);
|
|
715
|
+
if (!yearStat?.isDirectory() || !/^\d{4}$/.test(yearEntry.name)) {
|
|
716
|
+
invalidSource(`${sourceRelativePath(sourceRoot, yearPath)} is not a YYYY directory or manifest.md`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const yearDir = yearPath;
|
|
720
|
+
for (const monthEntry of fs.readdirSync(yearDir, { withFileTypes: true })) {
|
|
721
|
+
const monthPath = path.join(yearDir, monthEntry.name);
|
|
722
|
+
if (allowManifest(yearDir, monthEntry)) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const monthStat = sourcePathStat(sourceRoot, monthPath);
|
|
726
|
+
if (!monthStat?.isDirectory() || !/^\d{2}$/.test(monthEntry.name)) {
|
|
727
|
+
invalidSource(`${sourceRelativePath(sourceRoot, monthPath)} is not a MM directory or manifest.md`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const monthDir = monthPath;
|
|
731
|
+
for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
|
|
732
|
+
const dayPath = path.join(monthDir, dayEntry.name);
|
|
733
|
+
if (allowManifest(monthDir, dayEntry)) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const dayStat = sourcePathStat(sourceRoot, dayPath);
|
|
737
|
+
if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
|
|
738
|
+
invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const dayDir = dayPath;
|
|
742
|
+
for (const taskEntry of fs.readdirSync(dayDir, { withFileTypes: true })) {
|
|
743
|
+
const taskPath = path.join(dayDir, taskEntry.name);
|
|
744
|
+
const taskStat = sourcePathStat(sourceRoot, taskPath);
|
|
745
|
+
if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
|
|
746
|
+
invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
|
|
747
|
+
}
|
|
748
|
+
validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function validateSourceWorkspace(sourcePath: string): void {
|
|
756
|
+
validateSourceRoot(sourcePath);
|
|
757
|
+
const seenTaskIds = new Map<string, string>();
|
|
758
|
+
for (const section of MUTABLE_SECTIONS) {
|
|
759
|
+
validateMutableSection(sourcePath, section, seenTaskIds);
|
|
760
|
+
}
|
|
761
|
+
validateArchiveSection(sourcePath, seenTaskIds);
|
|
762
|
+
}
|
|
763
|
+
|
|
627
764
|
function detectSourceMode(sourcePath: string): SourceMode {
|
|
765
|
+
validateSourceRoot(sourcePath);
|
|
628
766
|
for (const section of ALL_SECTIONS) {
|
|
629
767
|
const sectionDir = path.join(sourcePath, section);
|
|
630
|
-
|
|
768
|
+
const sectionStat = sourcePathStat(sourcePath, sectionDir);
|
|
769
|
+
if (sectionStat?.isDirectory()) {
|
|
631
770
|
return 'workspace';
|
|
632
771
|
}
|
|
772
|
+
if (sectionStat) {
|
|
773
|
+
invalidSource(`${section} must be a directory`);
|
|
774
|
+
}
|
|
633
775
|
}
|
|
634
776
|
|
|
635
|
-
|
|
777
|
+
invalidSource('expected a current workspace containing active, blocked, completed, or archive sections');
|
|
636
778
|
}
|
|
637
779
|
|
|
638
780
|
function createReport(sourcePath: string, backupRoot: string): MergeReport {
|
|
@@ -778,29 +920,6 @@ function mergeArchiveSection(sourceArchive: string, localArchive: string, report
|
|
|
778
920
|
return sourceTasks.length;
|
|
779
921
|
}
|
|
780
922
|
|
|
781
|
-
function printLegacyArchiveMessages(report: MergeReport, sourcePath: string): void {
|
|
782
|
-
const merged = report.sections.archive.copied;
|
|
783
|
-
const skipped = report.sections.archive.skipped;
|
|
784
|
-
|
|
785
|
-
if (merged.length === 0 && skipped.length === 0) {
|
|
786
|
-
info(`No archived tasks found in ${sourcePath}`);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
for (const task of merged) {
|
|
790
|
-
ok(`Merged ${task.taskId} -> ${task.relativePath}`);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
for (const task of skipped) {
|
|
794
|
-
info(`Skipped ${task.taskId} (already exists at ${task.relativePath})`);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
process.stdout.write('\n');
|
|
798
|
-
info('Merge summary');
|
|
799
|
-
info(`- Merged: ${merged.length}`);
|
|
800
|
-
info(`- Skipped: ${skipped.length}`);
|
|
801
|
-
process.stdout.write('\n');
|
|
802
|
-
}
|
|
803
|
-
|
|
804
923
|
function printSection(lines: string[], name: MutableSection, counts: MutableCounts): void {
|
|
805
924
|
const title = `${SECTION_LABELS[name].padEnd(9, ' ')} (.agents/workspace/${name}/):`;
|
|
806
925
|
lines.push(title);
|
|
@@ -910,7 +1029,11 @@ async function cmdMerge(args: string[]): Promise<void> {
|
|
|
910
1029
|
throw new Error(`Source path does not exist: ${sourcePath}`);
|
|
911
1030
|
}
|
|
912
1031
|
|
|
913
|
-
if (
|
|
1032
|
+
if (fs.lstatSync(resolvedSource).isSymbolicLink()) {
|
|
1033
|
+
throw new Error(`Invalid merge source: source path must be a real directory: ${sourcePath}`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!fs.lstatSync(resolvedSource).isDirectory()) {
|
|
914
1037
|
throw new Error(`Source path is not a directory: ${sourcePath}`);
|
|
915
1038
|
}
|
|
916
1039
|
|
|
@@ -920,35 +1043,28 @@ async function cmdMerge(args: string[]): Promise<void> {
|
|
|
920
1043
|
const backupRootRelative = `.agents/workspace/.merge-backup/${backupStamp}/`;
|
|
921
1044
|
const backupRoot = path.join(workspaceDir, '.merge-backup', backupStamp);
|
|
922
1045
|
const report = createReport(resolvedSource, backupRootRelative);
|
|
923
|
-
|
|
1046
|
+
detectSourceMode(resolvedSource);
|
|
1047
|
+
validateSourceWorkspace(resolvedSource);
|
|
924
1048
|
|
|
925
1049
|
for (const section of ALL_SECTIONS) {
|
|
926
1050
|
fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
|
|
927
1051
|
}
|
|
928
1052
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
localWorkspace: workspaceDir,
|
|
936
|
-
backupRoot,
|
|
937
|
-
report
|
|
938
|
-
});
|
|
1053
|
+
mergeMutableSections({
|
|
1054
|
+
sourceWorkspace: resolvedSource,
|
|
1055
|
+
localWorkspace: workspaceDir,
|
|
1056
|
+
backupRoot,
|
|
1057
|
+
report
|
|
1058
|
+
});
|
|
939
1059
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1060
|
+
const sourceArchive = path.join(resolvedSource, 'archive');
|
|
1061
|
+
const sourceArchiveStat = sourcePathStat(resolvedSource, sourceArchive);
|
|
1062
|
+
if (sourceArchiveStat?.isDirectory()) {
|
|
1063
|
+
mergeArchiveSection(sourceArchive, archiveDir, report);
|
|
944
1064
|
}
|
|
945
1065
|
|
|
946
1066
|
rebuildManifests(archiveDir);
|
|
947
1067
|
|
|
948
|
-
if (mode === 'legacy-archive') {
|
|
949
|
-
printLegacyArchiveMessages(report, sourcePath);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
1068
|
printReport(report);
|
|
953
1069
|
}
|
|
954
1070
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256 } from './store.ts';
|
|
2
|
+
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
2
3
|
|
|
3
4
|
import type { CapturedObject, JsonValue, NormalizedKind, NormalizedRecord } from './types.ts';
|
|
4
5
|
|
|
@@ -14,14 +15,7 @@ function recordId(kind: NormalizedKind, identity: string, suffix = ''): string {
|
|
|
14
15
|
|
|
15
16
|
function canonicalActor(raw: string): string | null {
|
|
16
17
|
const lowered = raw.toLowerCase();
|
|
17
|
-
|
|
18
|
-
'claude-code': 'claude',
|
|
19
|
-
'antigravity-cli': 'antigravity',
|
|
20
|
-
'gemini-cli': 'gemini',
|
|
21
|
-
gemini: 'gemini'
|
|
22
|
-
};
|
|
23
|
-
if (['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'human'].includes(lowered)) return lowered;
|
|
24
|
-
return aliases[lowered] ?? null;
|
|
18
|
+
return normalizeAgentToken(lowered);
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
function nextOccurrence(occurrences: Map<string, number>, identity: string): string {
|
package/lib/run/host.ts
CHANGED
|
@@ -9,27 +9,74 @@ export type RunProcessResult = {
|
|
|
9
9
|
stderr?: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
type CommandResolution = Readonly<{
|
|
13
|
+
path: string;
|
|
14
|
+
source: 'absolute' | 'path' | 'current-directory' | 'unresolved';
|
|
15
|
+
pathEntry?: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
function resolveCommandDetails(
|
|
19
|
+
file: string,
|
|
20
|
+
platform: NodeJS.Platform = process.platform,
|
|
21
|
+
env: NodeJS.ProcessEnv = process.env
|
|
22
|
+
): CommandResolution {
|
|
23
|
+
if (path.isAbsolute(file)) return { path: file, source: 'absolute' };
|
|
16
24
|
|
|
17
|
-
const pathValue =
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
const pathValue = platform === 'win32'
|
|
26
|
+
? env.Path || env.PATH || ''
|
|
27
|
+
: env.PATH || '';
|
|
28
|
+
const extensions = platform === 'win32'
|
|
29
|
+
? (path.extname(file)
|
|
30
|
+
? ['']
|
|
31
|
+
: (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean))
|
|
32
|
+
: [''];
|
|
33
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
20
34
|
for (const extension of extensions) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
const candidates = extension
|
|
36
|
+
? [`${file}${extension.toLowerCase()}`, `${file}${extension.toUpperCase()}`]
|
|
37
|
+
: [file];
|
|
38
|
+
for (const candidateName of candidates) {
|
|
39
|
+
const candidate = path.resolve(dir || '.', candidateName);
|
|
40
|
+
try {
|
|
41
|
+
const stat = fs.statSync(candidate);
|
|
42
|
+
if (!stat.isFile()) continue;
|
|
43
|
+
if (platform !== 'win32' && (stat.mode & 0o111) === 0) continue;
|
|
44
|
+
return {
|
|
45
|
+
path: candidate,
|
|
46
|
+
source: dir ? 'path' : 'current-directory',
|
|
47
|
+
pathEntry: dir
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
// Keep searching the remaining PATH entries.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
25
53
|
}
|
|
26
54
|
}
|
|
27
55
|
|
|
28
|
-
return file;
|
|
56
|
+
return { path: file, source: 'unresolved' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveCommand(
|
|
60
|
+
file: string,
|
|
61
|
+
platform: NodeJS.Platform = process.platform,
|
|
62
|
+
env: NodeJS.ProcessEnv = process.env
|
|
63
|
+
): string {
|
|
64
|
+
return resolveCommandDetails(file, platform, env).path;
|
|
29
65
|
}
|
|
30
66
|
|
|
31
|
-
function
|
|
32
|
-
|
|
67
|
+
function resolveCommandFromAbsolutePath(
|
|
68
|
+
file: string,
|
|
69
|
+
platform: NodeJS.Platform = process.platform,
|
|
70
|
+
env: NodeJS.ProcessEnv = process.env
|
|
71
|
+
): string | null {
|
|
72
|
+
const resolution = resolveCommandDetails(file, platform, env);
|
|
73
|
+
return resolution.source === 'path' && resolution.pathEntry && path.isAbsolute(resolution.pathEntry)
|
|
74
|
+
? resolution.path
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function needsShell(file: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
79
|
+
return platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
|
|
33
80
|
}
|
|
34
81
|
|
|
35
82
|
export async function runHostCommand(command: string[]): Promise<RunProcessResult> {
|
|
@@ -45,3 +92,5 @@ export async function runHostCommand(command: string[]): Promise<RunProcessResul
|
|
|
45
92
|
child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
46
93
|
});
|
|
47
94
|
}
|
|
95
|
+
|
|
96
|
+
export { needsShell, resolveCommand, resolveCommandFromAbsolutePath };
|
|
@@ -28,7 +28,7 @@ type SandboxCapabilityConfig = Readonly<{
|
|
|
28
28
|
project: string;
|
|
29
29
|
tools: readonly string[];
|
|
30
30
|
customTools?: readonly SandboxTool[];
|
|
31
|
-
agentClientState
|
|
31
|
+
agentClientState: AgentClientState;
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
type SandboxRuntimeCapabilityProjection = Readonly<{
|
|
@@ -98,18 +98,6 @@ function agentInfraTool(home: string): SandboxTool {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function compatAgentClientState(tools: readonly string[]): AgentClientState {
|
|
102
|
-
return Object.fromEntries(
|
|
103
|
-
listAgentClientAdapters().map((adapter) => [
|
|
104
|
-
adapter.id,
|
|
105
|
-
{
|
|
106
|
-
enabled: true,
|
|
107
|
-
installInSandbox: tools.includes(adapter.id)
|
|
108
|
-
}
|
|
109
|
-
])
|
|
110
|
-
) as AgentClientState;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
101
|
function stableValue(value: unknown): unknown {
|
|
114
102
|
if (Array.isArray(value)) {
|
|
115
103
|
return value.map(stableValue);
|
|
@@ -206,7 +194,7 @@ function hooksByPhase(
|
|
|
206
194
|
function createSandboxCapabilityPlan(
|
|
207
195
|
config: SandboxCapabilityConfig
|
|
208
196
|
): SandboxCapabilityPlan {
|
|
209
|
-
const state = config.agentClientState
|
|
197
|
+
const state = config.agentClientState;
|
|
210
198
|
const selectedAgentClients = listInstalledAgentClientAdapters(state);
|
|
211
199
|
const context = { home: config.home, project: config.project };
|
|
212
200
|
const selectedTools = selectedAgentClients.map((adapter) =>
|
|
@@ -228,7 +216,9 @@ function createSandboxCapabilityPlan(
|
|
|
228
216
|
const nonClientTools: SandboxTool[] = [];
|
|
229
217
|
const selectedCustomTools: SandboxTool[] = [];
|
|
230
218
|
for (const id of config.tools) {
|
|
231
|
-
if (isAgentClientId(id))
|
|
219
|
+
if (isAgentClientId(id)) {
|
|
220
|
+
throw new Error(`Agent Client '${id}' must be provided by agentClientState`);
|
|
221
|
+
}
|
|
232
222
|
if (id === 'agent-infra') {
|
|
233
223
|
if (!nonClientTools.some((tool) => tool.id === id)) {
|
|
234
224
|
nonClientTools.push(agentInfraTool(config.home));
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
commitSandboxControlReplacement,
|
|
78
78
|
quiesceSandboxControlRoot,
|
|
79
79
|
recoverSandboxControlReplacement,
|
|
80
|
-
|
|
80
|
+
readSandboxControlManifest
|
|
81
81
|
} from '../control/lifecycle.ts';
|
|
82
82
|
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
83
83
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
@@ -1250,7 +1250,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1250
1250
|
const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1251
1251
|
const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
|
|
1252
1252
|
const previousManifest = fs.existsSync(controlPaths.manifestPath)
|
|
1253
|
-
?
|
|
1253
|
+
? readSandboxControlManifest(controlPaths.manifestPath)
|
|
1254
1254
|
: null;
|
|
1255
1255
|
if (hadExistingControlRoot) {
|
|
1256
1256
|
await quiesceSandboxControlRoot(controlPaths.root);
|
|
@@ -1423,10 +1423,10 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1423
1423
|
...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
|
|
1424
1424
|
...(target.workspace.mode === 'task-bound'
|
|
1425
1425
|
? {
|
|
1426
|
-
|
|
1426
|
+
taskSources: [assertSandboxTaskSource(
|
|
1427
1427
|
effectiveConfig.repoRoot,
|
|
1428
1428
|
target.workspace.taskId
|
|
1429
|
-
),
|
|
1429
|
+
)],
|
|
1430
1430
|
taskId: target.workspace.taskId
|
|
1431
1431
|
}
|
|
1432
1432
|
: {})
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
assertValidBranchName,
|
|
4
4
|
containerNameCandidates,
|
|
5
5
|
sandboxBranchLabel,
|
|
6
|
-
sandboxLabel
|
|
6
|
+
sandboxLabel,
|
|
7
|
+
sandboxTaskIdLabel,
|
|
8
|
+
sandboxWorkspaceModeLabel
|
|
7
9
|
} from '../constants.ts';
|
|
8
10
|
import { detectEngine } from '../engine.ts';
|
|
9
11
|
import { ensureSandboxReady } from '../recovery.ts';
|
|
@@ -21,7 +23,10 @@ import {
|
|
|
21
23
|
fetchSandboxRows,
|
|
22
24
|
selectSandboxContainer,
|
|
23
25
|
} from './list-running.ts';
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
resolveSandboxReentryContext,
|
|
28
|
+
resolveSandboxTarget
|
|
29
|
+
} from '../workspace-identity.ts';
|
|
25
30
|
|
|
26
31
|
const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
|
|
27
32
|
|
|
@@ -128,7 +133,11 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
128
133
|
const { running, nonRunning } = fetchSandboxRows(
|
|
129
134
|
engine,
|
|
130
135
|
sandboxLabel(config),
|
|
131
|
-
sandboxBranchLabel(config)
|
|
136
|
+
sandboxBranchLabel(config),
|
|
137
|
+
{
|
|
138
|
+
mode: sandboxWorkspaceModeLabel(config),
|
|
139
|
+
taskId: sandboxTaskIdLabel(config)
|
|
140
|
+
}
|
|
132
141
|
);
|
|
133
142
|
const found = selectSandboxContainer(
|
|
134
143
|
[...running, ...nonRunning],
|
|
@@ -140,11 +149,22 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
140
149
|
`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`
|
|
141
150
|
);
|
|
142
151
|
}
|
|
152
|
+
const containerWorkspace = found.workspaceMode === 'task-bound' && found.taskId
|
|
153
|
+
? { mode: 'task-bound' as const, taskId: found.taskId }
|
|
154
|
+
: found.workspaceMode === 'branch-only'
|
|
155
|
+
? { mode: 'branch-only' as const }
|
|
156
|
+
: { mode: 'legacy-invalid' as const };
|
|
157
|
+
const reentry = resolveSandboxReentryContext({
|
|
158
|
+
target,
|
|
159
|
+
containerWorkspace,
|
|
160
|
+
repoRoot: config.repoRoot
|
|
161
|
+
});
|
|
143
162
|
const ready = await ensureSandboxReady({
|
|
144
163
|
config,
|
|
145
164
|
engine,
|
|
146
165
|
branch,
|
|
147
|
-
workspace:
|
|
166
|
+
workspace: reentry.workspace,
|
|
167
|
+
reentry: reentry.reentry,
|
|
148
168
|
row: found,
|
|
149
169
|
allowRecreate: parsed.recreate,
|
|
150
170
|
recreate: async () => {
|