@fitlab-ai/agent-infra 0.7.5 → 0.7.7
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/bin/cli.ts +11 -3
- package/dist/bin/cli.js +10 -3
- package/dist/lib/sandbox/commands/create.js +26 -4
- package/dist/lib/sandbox/commands/ls.js +4 -33
- package/dist/lib/sandbox/commands/show.js +67 -0
- package/dist/lib/sandbox/index.js +7 -0
- package/dist/lib/sandbox/tools.js +20 -1
- package/dist/lib/task/commands/issue-body.js +94 -0
- package/dist/lib/task/commands/log.js +139 -6
- package/dist/lib/task/index.js +8 -0
- package/dist/lib/task/issue-form.js +66 -0
- package/dist/lib/task/sections.js +44 -0
- package/lib/sandbox/commands/create.ts +33 -4
- package/lib/sandbox/commands/ls.ts +4 -36
- package/lib/sandbox/commands/show.ts +80 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/sandbox/tools.ts +28 -1
- package/lib/task/commands/issue-body.ts +102 -0
- package/lib/task/commands/log.ts +146 -6
- package/lib/task/index.ts +8 -0
- package/lib/task/issue-form.ts +77 -0
- package/lib/task/sections.ts +44 -0
- package/package.json +3 -2
- package/templates/.agents/README.en.md +3 -1
- package/templates/.agents/README.zh-CN.md +3 -1
- package/templates/.agents/rules/create-issue.github.en.md +20 -29
- package/templates/.agents/rules/create-issue.github.zh-CN.md +20 -29
- package/templates/.agents/rules/issue-pr-commands.github.en.md +6 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/next-step-output.en.md +26 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +26 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +11 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +11 -0
- package/templates/.agents/rules/review-handshake.en.md +15 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +15 -1
- package/templates/.agents/rules/task-management.en.md +27 -0
- package/templates/.agents/rules/task-management.zh-CN.md +31 -0
- package/templates/.agents/scripts/validate-artifact.js +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +3 -1
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +13 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +13 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +13 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +14 -2
- package/templates/.agents/skills/check-task/SKILL.en.md +30 -81
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +30 -80
- package/templates/.agents/skills/close-codescan/SKILL.en.md +13 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +13 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +13 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +13 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +7 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +7 -2
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +6 -14
- package/templates/.agents/skills/commit/SKILL.en.md +37 -5
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +37 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +13 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +13 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +23 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +23 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +19 -2
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +19 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +14 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +14 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +35 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +35 -2
- package/templates/.agents/skills/init-labels/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -1
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +15 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +15 -1
- package/templates/.agents/skills/post-release/SKILL.en.md +3 -1
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.en.md +3 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/release/SKILL.en.md +3 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +13 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +13 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +16 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +16 -2
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +15 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +15 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +16 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +16 -2
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +1 -2
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +1 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +1 -0
- package/templates/.agents/skills/test/SKILL.en.md +3 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +3 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +3 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +3 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -1
- package/templates/.agents/templates/task.en.md +5 -0
- package/templates/.agents/templates/task.zh-CN.md +5 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
// Field id -> task value mapping (single source of truth, per HD-1).
|
|
3
|
+
// Only ids that map cleanly to title / description / requirements get a value;
|
|
4
|
+
// every other text field gets `N/A`. This deliberately tightens the older
|
|
5
|
+
// "suggested" mapping table (impact / context / alternatives / steps / expected
|
|
6
|
+
// no longer flow into requirements) so the requirements checklist is never
|
|
7
|
+
// pushed into an unrelated field.
|
|
8
|
+
const TITLE_IDS = new Set(['summary', 'title']);
|
|
9
|
+
const DESCRIPTION_IDS = new Set([
|
|
10
|
+
'description',
|
|
11
|
+
'problem',
|
|
12
|
+
'what-happened',
|
|
13
|
+
'question',
|
|
14
|
+
'current-content',
|
|
15
|
+
'issue-description',
|
|
16
|
+
'detailed-description'
|
|
17
|
+
]);
|
|
18
|
+
const REQUIREMENTS_IDS = new Set(['requirements', 'solution', 'suggested-content']);
|
|
19
|
+
// Only free-text field types carry task content; structural / static fields are skipped.
|
|
20
|
+
const TEXT_FIELD_TYPES = new Set(['input', 'textarea']);
|
|
21
|
+
const PLACEHOLDER = 'N/A';
|
|
22
|
+
function mapFieldValue(id, fields) {
|
|
23
|
+
if (TITLE_IDS.has(id))
|
|
24
|
+
return fields.title;
|
|
25
|
+
if (DESCRIPTION_IDS.has(id))
|
|
26
|
+
return fields.description;
|
|
27
|
+
if (REQUIREMENTS_IDS.has(id))
|
|
28
|
+
return fields.requirements;
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render the final Issue body for a GitHub Issue Form (scenario A).
|
|
33
|
+
*
|
|
34
|
+
* Walks the form's `body[]` in order, skips `markdown` / `dropdown` /
|
|
35
|
+
* `checkboxes`, and renders each `input` / `textarea` as `### {label}` followed
|
|
36
|
+
* by the deterministically-mapped task value (or `N/A` when the field has no
|
|
37
|
+
* reliable source). The template structure is preserved; the whole task.md and
|
|
38
|
+
* its scaffolding sections are never emitted.
|
|
39
|
+
*
|
|
40
|
+
* Throws on unreadable / non-object YAML or a missing `body[]` list so the
|
|
41
|
+
* caller can fall back to the default body.
|
|
42
|
+
*/
|
|
43
|
+
function renderTemplateBody(formText, fields) {
|
|
44
|
+
const doc = parse(formText);
|
|
45
|
+
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.body)) {
|
|
46
|
+
throw new Error('Issue Form has no body[] list');
|
|
47
|
+
}
|
|
48
|
+
const sections = [];
|
|
49
|
+
for (const raw of doc.body) {
|
|
50
|
+
if (!raw || typeof raw !== 'object')
|
|
51
|
+
continue;
|
|
52
|
+
const type = typeof raw.type === 'string' ? raw.type : '';
|
|
53
|
+
if (!TEXT_FIELD_TYPES.has(type))
|
|
54
|
+
continue;
|
|
55
|
+
const label = typeof raw.attributes?.label === 'string' ? raw.attributes.label.trim() : '';
|
|
56
|
+
if (!label)
|
|
57
|
+
continue;
|
|
58
|
+
const id = typeof raw.id === 'string' ? raw.id : '';
|
|
59
|
+
const mapped = mapFieldValue(id, fields);
|
|
60
|
+
const value = mapped.trim() === '' ? PLACEHOLDER : mapped;
|
|
61
|
+
sections.push(`### ${label}\n\n${value}`);
|
|
62
|
+
}
|
|
63
|
+
return `${sections.join('\n\n')}\n`;
|
|
64
|
+
}
|
|
65
|
+
export { renderTemplateBody, mapFieldValue, PLACEHOLDER };
|
|
66
|
+
//# sourceMappingURL=issue-form.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function escapeRegExp(value) {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Return the body of the first `## {alias}` section (any alias matches), from
|
|
6
|
+
* the heading line to the next `## ` heading or EOF. Lines are preserved
|
|
7
|
+
* verbatim (checkbox text is never normalized); only leading/trailing blank
|
|
8
|
+
* lines are trimmed. Returns '' when no alias heading is present.
|
|
9
|
+
*/
|
|
10
|
+
function extractSection(content, aliases) {
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
let start = -1;
|
|
13
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
14
|
+
const line = lines[i].trim();
|
|
15
|
+
if (aliases.some((alias) => new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`).test(line))) {
|
|
16
|
+
start = i + 1;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (start === -1)
|
|
21
|
+
return '';
|
|
22
|
+
let end = lines.length;
|
|
23
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
24
|
+
if (/^##\s+/.test(lines[i])) {
|
|
25
|
+
end = i;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return the heading alias that actually appears as a `## {alias}` line, so a
|
|
33
|
+
* rendered section can mirror the source language. Falls back to the first
|
|
34
|
+
* alias when none is present.
|
|
35
|
+
*/
|
|
36
|
+
function findSectionHeading(content, aliases) {
|
|
37
|
+
for (const alias of aliases) {
|
|
38
|
+
if (new RegExp(`^##\\s+${escapeRegExp(alias)}\\s*$`, 'm').test(content))
|
|
39
|
+
return alias;
|
|
40
|
+
}
|
|
41
|
+
return aliases[0];
|
|
42
|
+
}
|
|
43
|
+
export { extractSection, findSectionHeading };
|
|
44
|
+
//# sourceMappingURL=sections.js.map
|
|
@@ -1084,6 +1084,13 @@ function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts:
|
|
|
1084
1084
|
return runTaskCommand(command.cmd, command.args, opts);
|
|
1085
1085
|
}
|
|
1086
1086
|
|
|
1087
|
+
// `docker run` args for mounting a tool's containerMount as an in-container
|
|
1088
|
+
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
1089
|
+
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string }): string[] {
|
|
1090
|
+
const size = tmpfs.size ?? '512m';
|
|
1091
|
+
return ['--tmpfs', `${containerMount}:rw,size=${size}`];
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1087
1094
|
export function buildImage(
|
|
1088
1095
|
config: Pick<SandboxCreateConfig, 'project' | 'imageName' | 'repoRoot'> & { engine?: string | null },
|
|
1089
1096
|
tools: SandboxTool[],
|
|
@@ -1397,10 +1404,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1397
1404
|
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1398
1405
|
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1399
1406
|
}
|
|
1400
|
-
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1401
|
-
'-v',
|
|
1402
|
-
|
|
1403
|
-
|
|
1407
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1408
|
+
tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]
|
|
1409
|
+
);
|
|
1410
|
+
const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1411
|
+
tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []
|
|
1412
|
+
);
|
|
1404
1413
|
const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
|
|
1405
1414
|
hostShellConfig = prepareHostShellConfig({
|
|
1406
1415
|
home: effectiveConfig.home,
|
|
@@ -1412,6 +1421,24 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1412
1421
|
'-v',
|
|
1413
1422
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1414
1423
|
]);
|
|
1424
|
+
// A tmpfs containerMount starts empty, so the config seeded into the
|
|
1425
|
+
// host dir before launch would be invisible in-container. Bind only
|
|
1426
|
+
// the explicitly declared seed entries (config.toml, model-catalogs)
|
|
1427
|
+
// back over the tmpfs as nested mounts — the same proven mechanism as
|
|
1428
|
+
// hostLiveMounts/auth.json, established at `docker run` time (no
|
|
1429
|
+
// post-start `docker cp`, which can land under a freshly-mounted
|
|
1430
|
+
// tmpfs instead of inside it). The allowlist is deliberate: any
|
|
1431
|
+
// runtime files left in the host dir (e.g. a stale logs_2.sqlite or
|
|
1432
|
+
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1433
|
+
// or the high-churn writes would land on the host SSD again.
|
|
1434
|
+
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1435
|
+
(tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1436
|
+
const hostPath = path.join(dir, entry);
|
|
1437
|
+
return fs.existsSync(hostPath)
|
|
1438
|
+
? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
|
|
1439
|
+
: [];
|
|
1440
|
+
})
|
|
1441
|
+
);
|
|
1415
1442
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1416
1443
|
(tool.hostLiveMounts ?? [])
|
|
1417
1444
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
@@ -1466,6 +1493,8 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1466
1493
|
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1467
1494
|
...dotfilesMount,
|
|
1468
1495
|
...toolVolumes,
|
|
1496
|
+
...tmpfsArgs,
|
|
1497
|
+
...tmpfsSeedVolumes,
|
|
1469
1498
|
...liveMountVolumes,
|
|
1470
1499
|
...shellConfigVolumes,
|
|
1471
1500
|
...envFile.dockerArgs,
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import * as p from '@clack/prompts';
|
|
4
2
|
import pc from 'picocolors';
|
|
5
3
|
import { loadConfig } from '../config.ts';
|
|
6
4
|
import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
7
5
|
import { detectEngine } from '../engine.ts';
|
|
8
|
-
import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
|
|
9
6
|
import { formatTable } from '../../table.ts';
|
|
10
7
|
import { lookupShortIdByBranch } from '../../task/short-id.ts';
|
|
11
8
|
import { fetchSandboxRows } from './list-running.ts';
|
|
@@ -20,7 +17,10 @@ id bound to each container's branch (via
|
|
|
20
17
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
21
18
|
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
22
19
|
A '-' means no active task is bound to that branch, so the sandbox is free
|
|
23
|
-
to remove with "ai sandbox rm <branch>"
|
|
20
|
+
to remove with "ai sandbox rm <branch>".
|
|
21
|
+
|
|
22
|
+
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
23
|
+
state paths.`;
|
|
24
24
|
|
|
25
25
|
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
26
26
|
|
|
@@ -40,14 +40,6 @@ export function formatContainerTable(rows: ContainerTableRow[], zebra = false):
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function listChildren(dir: string): string[] {
|
|
44
|
-
if (!fs.existsSync(dir)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return fs.readdirSync(dir).sort().map((entry) => path.join(dir, entry));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
43
|
export function ls(args: string[] = []): void {
|
|
52
44
|
if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
|
|
53
45
|
process.stdout.write(`${USAGE}\n`);
|
|
@@ -56,7 +48,6 @@ export function ls(args: string[] = []): void {
|
|
|
56
48
|
|
|
57
49
|
const config = loadConfig();
|
|
58
50
|
const engine = detectEngine(config);
|
|
59
|
-
const tools = resolveTools(config);
|
|
60
51
|
const label = sandboxLabel(config);
|
|
61
52
|
const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
|
|
62
53
|
|
|
@@ -87,27 +78,4 @@ export function ls(args: string[] = []): void {
|
|
|
87
78
|
);
|
|
88
79
|
}
|
|
89
80
|
}
|
|
90
|
-
|
|
91
|
-
p.log.step('Worktrees');
|
|
92
|
-
const worktrees = listChildren(config.worktreeBase);
|
|
93
|
-
if (worktrees.length === 0) {
|
|
94
|
-
p.log.warn(' No sandbox worktrees');
|
|
95
|
-
} else {
|
|
96
|
-
for (const worktree of worktrees) {
|
|
97
|
-
process.stdout.write(` ${worktree}\n`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for (const tool of tools) {
|
|
102
|
-
p.log.step(`${tool.name} state`);
|
|
103
|
-
const entries = toolProjectDirCandidates(tool, config.project)
|
|
104
|
-
.flatMap((dir) => listChildren(dir));
|
|
105
|
-
if (entries.length === 0) {
|
|
106
|
-
p.log.warn(` No ${tool.name} sandbox state`);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
for (const entry of entries) {
|
|
110
|
-
process.stdout.write(` ${entry}\n`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
81
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import type { SandboxConfig } from '../config.ts';
|
|
5
|
+
import { loadConfig } from '../config.ts';
|
|
6
|
+
import { assertValidBranchName, worktreeDirCandidates } from '../constants.ts';
|
|
7
|
+
import { resolveBranchArg } from './list-running.ts';
|
|
8
|
+
import { resolveTools, toolConfigDirCandidates } from '../tools.ts';
|
|
9
|
+
|
|
10
|
+
const USAGE = `Usage: ai sandbox show <branch | TASK-id | N | '#N'>
|
|
11
|
+
|
|
12
|
+
Shows one sandbox's worktree path and per-tool state paths (Claude Code,
|
|
13
|
+
Codex, Gemini CLI, OpenCode). The argument follows the same contract as
|
|
14
|
+
'ai sandbox exec' and 'ai sandbox start': N (bare) is the recommended form
|
|
15
|
+
for task short ids (e.g. 'ai sandbox show 11'); '#N', a TASK-id, or a plain
|
|
16
|
+
branch name are also accepted. Use 'ai sandbox ls' for the container list.`;
|
|
17
|
+
|
|
18
|
+
export type SandboxDetail = {
|
|
19
|
+
worktrees: string[];
|
|
20
|
+
toolStates: { name: string; entries: string[] }[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function existingDirs(candidates: string[]): string[] {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
const result: string[] = [];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
28
|
+
seen.add(candidate);
|
|
29
|
+
result.push(candidate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function collectSandboxDetail(config: SandboxConfig, branch: string): SandboxDetail {
|
|
36
|
+
const worktrees = existingDirs(worktreeDirCandidates(config, branch));
|
|
37
|
+
const toolStates = resolveTools(config).map((tool) => ({
|
|
38
|
+
name: tool.name,
|
|
39
|
+
entries: existingDirs(toolConfigDirCandidates(tool, config.project, branch))
|
|
40
|
+
}));
|
|
41
|
+
return { worktrees, toolStates };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function show(args: string[] = []): void {
|
|
45
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
46
|
+
process.stdout.write(`${USAGE}\n`);
|
|
47
|
+
if (args.length === 0) {
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const branch = resolveBranchArg(args[0]!, { repoRoot: config.repoRoot });
|
|
55
|
+
assertValidBranchName(branch);
|
|
56
|
+
|
|
57
|
+
const detail = collectSandboxDetail(config, branch);
|
|
58
|
+
|
|
59
|
+
p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
|
|
60
|
+
|
|
61
|
+
p.log.step('Worktree');
|
|
62
|
+
if (detail.worktrees.length === 0) {
|
|
63
|
+
p.log.warn(' No worktree for this branch');
|
|
64
|
+
} else {
|
|
65
|
+
for (const worktree of detail.worktrees) {
|
|
66
|
+
process.stdout.write(` ${worktree}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const tool of detail.toolStates) {
|
|
71
|
+
p.log.step(`${tool.name} state`);
|
|
72
|
+
if (tool.entries.length === 0) {
|
|
73
|
+
p.log.warn(` No ${tool.name} sandbox state`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of tool.entries) {
|
|
77
|
+
process.stdout.write(` ${entry}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ Commands:
|
|
|
9
9
|
ls List sandboxes for the current project (the '#'
|
|
10
10
|
column is a display-only row number; the 'SHORT'
|
|
11
11
|
column shows the active task short id, '-' if none)
|
|
12
|
+
show <branch | TASK-id | N | '#N'>
|
|
13
|
+
Show one sandbox's worktree and per-tool state paths
|
|
12
14
|
prune [--dry-run] Remove orphaned per-branch state dirs
|
|
13
15
|
rebuild [--quiet] [--refresh]
|
|
14
16
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
@@ -56,6 +58,11 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
56
58
|
ls(rest);
|
|
57
59
|
break;
|
|
58
60
|
}
|
|
61
|
+
case 'show': {
|
|
62
|
+
const { show } = await import('./commands/show.ts');
|
|
63
|
+
show(rest);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
59
66
|
case 'prune': {
|
|
60
67
|
const { prune } = await import('./commands/prune.ts');
|
|
61
68
|
await prune(rest);
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -19,6 +19,13 @@ export type SandboxTool = {
|
|
|
19
19
|
pathRewriteFiles?: string[];
|
|
20
20
|
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
|
|
21
21
|
postSetupCmds?: string[];
|
|
22
|
+
// When set, containerMount is mounted as an in-container tmpfs (RAM) instead
|
|
23
|
+
// of bind-mounting the host config dir, keeping high-churn tool logs off the
|
|
24
|
+
// host disk. `seed` lists the host-dir entries (relative to the tool's config
|
|
25
|
+
// dir) to bind back over the tmpfs so seeded config stays visible — it is an
|
|
26
|
+
// explicit allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in
|
|
27
|
+
// the host dir are NOT re-mounted, which would defeat the tmpfs.
|
|
28
|
+
tmpfs?: { size?: string; seed?: string[] };
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
type ToolsConfig = {
|
|
@@ -70,6 +77,12 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
70
77
|
containerMount: '/home/devuser/.codex',
|
|
71
78
|
versionCmd: 'codex --version',
|
|
72
79
|
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
80
|
+
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
81
|
+
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
82
|
+
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
83
|
+
// Only the seeded config (config.toml, model-catalogs) is bound back over
|
|
84
|
+
// the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
|
|
85
|
+
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
73
86
|
hostLiveMounts: [
|
|
74
87
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
75
88
|
],
|
|
@@ -259,6 +272,19 @@ function parseHostLiveMounts(value: unknown, context: string): SandboxTool['host
|
|
|
259
272
|
});
|
|
260
273
|
}
|
|
261
274
|
|
|
275
|
+
function parseTmpfs(value: unknown, context: string): SandboxTool['tmpfs'] {
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
if (!isPlainObject(value)) {
|
|
280
|
+
throw new Error(`${context}: field "tmpfs" must be an object when provided`);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
|
|
284
|
+
seed: asStringArray(value.seed, 'tmpfs.seed', context)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
export function parseCustomTool(
|
|
263
289
|
entry: unknown,
|
|
264
290
|
index: number,
|
|
@@ -294,7 +320,8 @@ export function parseCustomTool(
|
|
|
294
320
|
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
295
321
|
pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
|
|
296
322
|
hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
|
|
297
|
-
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
|
|
323
|
+
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context),
|
|
324
|
+
tmpfs: parseTmpfs(entry.tmpfs, context)
|
|
298
325
|
};
|
|
299
326
|
|
|
300
327
|
validateTool(tool);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { resolveTaskRef } from '../resolve-ref.ts';
|
|
3
|
+
import { extractTitle } from '../frontmatter.ts';
|
|
4
|
+
import { extractSection, findSectionHeading } from '../sections.ts';
|
|
5
|
+
import { renderTemplateBody, PLACEHOLDER } from '../issue-form.ts';
|
|
6
|
+
import type { TaskFields } from '../issue-form.ts';
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: ai task issue-body <N | #N | TASK-id> [--template <path>]
|
|
9
|
+
|
|
10
|
+
Print a deterministic Issue body extracted from a task's task.md.
|
|
11
|
+
<ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
|
|
12
|
+
--template <path> Render the final body for the given GitHub Issue Form (scenario A);
|
|
13
|
+
without it, print the default '描述 + 需求' body (scenario B).
|
|
14
|
+
|
|
15
|
+
Only the task title, '## 描述' and '## 需求' sections are ever emitted; the rest of
|
|
16
|
+
task.md (scaffolding sections, placeholders) is never written to the body.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const DESCRIPTION_ALIASES = ['描述', 'Description'];
|
|
20
|
+
const REQUIREMENTS_ALIASES = ['需求', 'Requirements'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the scenario B default body, mirroring whichever heading language the
|
|
24
|
+
* task.md actually uses, with empty sections falling back to `N/A`.
|
|
25
|
+
*/
|
|
26
|
+
function buildDefaultBody(content: string): string {
|
|
27
|
+
const descHeading = findSectionHeading(content, DESCRIPTION_ALIASES);
|
|
28
|
+
const reqHeading = findSectionHeading(content, REQUIREMENTS_ALIASES);
|
|
29
|
+
const description = extractSection(content, DESCRIPTION_ALIASES) || PLACEHOLDER;
|
|
30
|
+
const requirements = extractSection(content, REQUIREMENTS_ALIASES) || PLACEHOLDER;
|
|
31
|
+
return `## ${descHeading}\n\n${description}\n\n## ${reqHeading}\n\n${requirements}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readTaskFields(content: string): TaskFields {
|
|
35
|
+
return {
|
|
36
|
+
title: extractTitle(content),
|
|
37
|
+
description: extractSection(content, DESCRIPTION_ALIASES),
|
|
38
|
+
requirements: extractSection(content, REQUIREMENTS_ALIASES)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function issueBody(args: string[] = []): void {
|
|
43
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
44
|
+
process.stdout.write(USAGE);
|
|
45
|
+
if (args.length === 0) process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let ref: string | undefined;
|
|
50
|
+
let templatePath: string | undefined;
|
|
51
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
52
|
+
const arg = args[i]!;
|
|
53
|
+
if (arg === '--template') {
|
|
54
|
+
templatePath = args[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (ref === undefined) {
|
|
57
|
+
ref = arg;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!ref) {
|
|
62
|
+
process.stderr.write('ai task issue-body: missing task ref\n');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (templatePath === undefined && args.includes('--template')) {
|
|
67
|
+
process.stderr.write('ai task issue-body: --template requires a path\n');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolved = resolveTaskRef(ref);
|
|
73
|
+
if (!resolved.ok) {
|
|
74
|
+
process.stderr.write(`ai task issue-body: ${resolved.message}\n`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
80
|
+
|
|
81
|
+
if (templatePath !== undefined) {
|
|
82
|
+
let formText: string;
|
|
83
|
+
try {
|
|
84
|
+
formText = fs.readFileSync(templatePath, 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
process.stderr.write(`ai task issue-body: cannot read template '${templatePath}': ${(e as Error).message}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
process.stdout.write(renderTemplateBody(formText, readTaskFields(content)));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
process.stderr.write(`ai task issue-body: cannot render template '${templatePath}': ${(e as Error).message}\n`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.stdout.write(buildDefaultBody(content));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { issueBody, buildDefaultBody };
|