@fitlab-ai/agent-infra 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/bin/cli.ts +13 -11
  2. package/dist/bin/cli.js +13 -11
  3. package/dist/lib/init.js +1 -1
  4. package/dist/lib/merge.js +1 -1
  5. package/dist/lib/sandbox/commands/create.js +26 -4
  6. package/dist/lib/sandbox/index.js +21 -21
  7. package/dist/lib/sandbox/tools.js +20 -1
  8. package/dist/lib/task/commands/log.js +56 -6
  9. package/dist/lib/task/index.js +13 -13
  10. package/dist/lib/update.js +1 -1
  11. package/lib/init.ts +1 -1
  12. package/lib/merge.ts +1 -1
  13. package/lib/sandbox/commands/create.ts +33 -4
  14. package/lib/sandbox/index.ts +21 -21
  15. package/lib/sandbox/tools.ts +28 -1
  16. package/lib/task/commands/log.ts +59 -6
  17. package/lib/task/index.ts +13 -13
  18. package/lib/update.ts +1 -1
  19. package/package.json +1 -1
  20. package/templates/.agents/rules/README.en.md +7 -3
  21. package/templates/.agents/rules/README.zh-CN.md +7 -3
  22. package/templates/.agents/rules/cli-help-format.en.md +49 -0
  23. package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
  24. package/templates/.agents/rules/no-mid-flow-questions.en.md +25 -2
  25. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +25 -2
  26. package/templates/.agents/rules/pr-sync.github.en.md +8 -6
  27. package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
  28. package/templates/.agents/rules/review-handshake.en.md +97 -0
  29. package/templates/.agents/rules/review-handshake.zh-CN.md +97 -0
  30. package/templates/.agents/rules/task-management.en.md +25 -0
  31. package/templates/.agents/rules/task-management.zh-CN.md +29 -0
  32. package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
  33. package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
  34. package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
  35. package/templates/.agents/scripts/validate-artifact.js +251 -2
  36. package/templates/.agents/skills/analyze-task/SKILL.en.md +63 -6
  37. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +63 -6
  38. package/templates/.agents/skills/block-task/SKILL.en.md +10 -0
  39. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +10 -0
  40. package/templates/.agents/skills/cancel-task/SKILL.en.md +10 -0
  41. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +11 -1
  42. package/templates/.agents/skills/close-codescan/SKILL.en.md +10 -0
  43. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +10 -0
  44. package/templates/.agents/skills/close-dependabot/SKILL.en.md +10 -0
  45. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +10 -0
  46. package/templates/.agents/skills/code-task/SKILL.en.md +11 -0
  47. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -0
  48. package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
  49. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
  50. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
  51. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
  52. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
  53. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
  54. package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
  55. package/templates/.agents/skills/commit/SKILL.en.md +16 -0
  56. package/templates/.agents/skills/commit/SKILL.zh-CN.md +16 -0
  57. package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
  58. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
  59. package/templates/.agents/skills/complete-task/SKILL.en.md +20 -0
  60. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -0
  61. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
  62. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
  63. package/templates/.agents/skills/create-pr/SKILL.en.md +20 -1
  64. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +20 -1
  65. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  66. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  67. package/templates/.agents/skills/create-release-note/SKILL.en.md +16 -1
  68. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -1
  69. package/templates/.agents/skills/create-task/SKILL.en.md +11 -0
  70. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -3
  71. package/templates/.agents/skills/import-codescan/SKILL.en.md +11 -0
  72. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +11 -0
  73. package/templates/.agents/skills/import-dependabot/SKILL.en.md +11 -0
  74. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +11 -0
  75. package/templates/.agents/skills/import-issue/SKILL.en.md +16 -0
  76. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +16 -0
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -1
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -1
  79. package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
  80. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
  81. package/templates/.agents/skills/restore-task/SKILL.en.md +10 -0
  82. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -0
  83. package/templates/.agents/skills/review-analysis/SKILL.en.md +10 -0
  84. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +10 -0
  85. package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
  86. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
  87. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
  88. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
  89. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
  90. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
  91. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +1 -0
  92. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +1 -0
  93. package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
  94. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
  95. package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
  96. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
  97. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
  98. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
  99. package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
  100. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
  101. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +1 -0
  102. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +1 -0
  103. package/templates/.agents/skills/review-plan/SKILL.en.md +10 -0
  104. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +10 -0
  105. package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
  106. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
  107. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
  108. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
  109. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
  110. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
  111. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +1 -0
  112. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +1 -0
  113. package/templates/.agents/skills/watch-pr/SKILL.en.md +10 -0
  114. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +10 -0
  115. package/templates/.agents/templates/task.en.md +12 -0
  116. package/templates/.agents/templates/task.zh-CN.md +12 -0
  117. package/templates/.github/workflows/metadata-sync.yml +1 -1
  118. package/templates/.github/workflows/pr-label.yml +1 -1
  119. package/templates/.github/workflows/status-label.yml +1 -1
package/bin/cli.ts CHANGED
@@ -12,17 +12,19 @@ if (major < 22) {
12
12
 
13
13
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
14
14
 
15
- Usage:
16
- agent-infra cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
17
- agent-infra help Show this help message
18
- agent-infra init Initialize a new project with update-agent-infra seed command
19
- agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
20
- agent-infra sandbox Manage Docker-based AI sandboxes
21
- agent-infra task Read-only views over .agents/workspace tasks (ls / show / files / cat / status / log / grep)
22
- agent-infra update Update seed files and sync file registry for an existing project
23
- agent-infra version Show version
15
+ Usage: ai <command> [options]
24
16
 
25
- Shorthand: ai (e.g. ai init)
17
+ Commands:
18
+ cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
19
+ help Show this help message
20
+ init Initialize a new project with update-agent-infra seed command
21
+ merge Merge tasks from another workspace directory (active/blocked/completed/archive)
22
+ sandbox Manage Docker-based AI sandboxes
23
+ task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
24
+ update Update seed files and sync file registry for an existing project
25
+ version Show version
26
+
27
+ 'ai' and 'agent-infra' are interchangeable; 'ai' is the shorter form.
26
28
 
27
29
  Install methods:
28
30
  npm: npm install -g @fitlab-ai/agent-infra
@@ -31,7 +33,7 @@ Install methods:
31
33
  curl: curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.sh | sh (runs npm install -g internally)
32
34
 
33
35
  Examples:
34
- cd my-project && agent-infra init
36
+ cd my-project && ai init
35
37
  npx @fitlab-ai/agent-infra init
36
38
  `;
37
39
 
package/dist/bin/cli.js CHANGED
@@ -16,17 +16,19 @@ if (major < 22) {
16
16
  }
17
17
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
18
18
 
19
- Usage:
20
- agent-infra cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
21
- agent-infra help Show this help message
22
- agent-infra init Initialize a new project with update-agent-infra seed command
23
- agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
24
- agent-infra sandbox Manage Docker-based AI sandboxes
25
- agent-infra task Read-only views over .agents/workspace tasks (ls / show / files / cat / status / log / grep)
26
- agent-infra update Update seed files and sync file registry for an existing project
27
- agent-infra version Show version
19
+ Usage: ai <command> [options]
28
20
 
29
- Shorthand: ai (e.g. ai init)
21
+ Commands:
22
+ cp <ssh-alias> Copy local clipboard image to a remote macOS NSPasteboard
23
+ help Show this help message
24
+ init Initialize a new project with update-agent-infra seed command
25
+ merge Merge tasks from another workspace directory (active/blocked/completed/archive)
26
+ sandbox Manage Docker-based AI sandboxes
27
+ task Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
28
+ update Update seed files and sync file registry for an existing project
29
+ version Show version
30
+
31
+ 'ai' and 'agent-infra' are interchangeable; 'ai' is the shorter form.
30
32
 
31
33
  Install methods:
32
34
  npm: npm install -g @fitlab-ai/agent-infra
@@ -35,7 +37,7 @@ Install methods:
35
37
  curl: curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.sh | sh (runs npm install -g internally)
36
38
 
37
39
  Examples:
38
- cd my-project && agent-infra init
40
+ cd my-project && ai init
39
41
  npx @fitlab-ai/agent-infra init
40
42
  `;
41
43
  const command = process.argv[2] || '';
package/dist/lib/init.js CHANGED
@@ -70,7 +70,7 @@ function parseLocalSources(input) {
70
70
  }
71
71
  async function cmdInit() {
72
72
  console.log('');
73
- console.log(' agent-infra init');
73
+ console.log(' ai init');
74
74
  console.log(' ================================');
75
75
  console.log(' Optional template and skill sources can be added now or later in .agents/.airc.json.');
76
76
  console.log('');
package/dist/lib/merge.js CHANGED
@@ -702,7 +702,7 @@ function printReport(report) {
702
702
  async function cmdMerge(args) {
703
703
  const sourcePath = args[0];
704
704
  if (!sourcePath) {
705
- throw new Error('Usage: agent-infra merge <source-path>');
705
+ throw new Error('Usage: ai merge <source-path>');
706
706
  }
707
707
  const resolvedSource = path.resolve(sourcePath);
708
708
  if (!fs.existsSync(resolvedSource)) {
@@ -820,6 +820,12 @@ function runEngineTaskCommand(engine, cmd, args, opts = {}) {
820
820
  const command = commandForEngine(engine, cmd, args);
821
821
  return runTaskCommand(command.cmd, command.args, opts);
822
822
  }
823
+ // `docker run` args for mounting a tool's containerMount as an in-container
824
+ // tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
825
+ export function buildTmpfsRunArgs(containerMount, tmpfs) {
826
+ const size = tmpfs.size ?? '512m';
827
+ return ['--tmpfs', `${containerMount}:rw,size=${size}`];
828
+ }
823
829
  export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
824
830
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
825
831
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
@@ -1076,10 +1082,8 @@ export async function create(args) {
1076
1082
  // The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
1077
1083
  ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
1078
1084
  }
1079
- const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => [
1080
- '-v',
1081
- volumeArg(engine, dir, tool.containerMount)
1082
- ]);
1085
+ const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]);
1086
+ const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) => tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []);
1083
1087
  const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
1084
1088
  hostShellConfig = prepareHostShellConfig({
1085
1089
  home: effectiveConfig.home,
@@ -1091,6 +1095,22 @@ export async function create(args) {
1091
1095
  '-v',
1092
1096
  volumeArg(engine, hostPath, containerPath, ':ro')
1093
1097
  ]);
1098
+ // A tmpfs containerMount starts empty, so the config seeded into the
1099
+ // host dir before launch would be invisible in-container. Bind only
1100
+ // the explicitly declared seed entries (config.toml, model-catalogs)
1101
+ // back over the tmpfs as nested mounts — the same proven mechanism as
1102
+ // hostLiveMounts/auth.json, established at `docker run` time (no
1103
+ // post-start `docker cp`, which can land under a freshly-mounted
1104
+ // tmpfs instead of inside it). The allowlist is deliberate: any
1105
+ // runtime files left in the host dir (e.g. a stale logs_2.sqlite or
1106
+ // sessions/ from a previous bind-mount era) must NOT be re-mounted,
1107
+ // or the high-churn writes would land on the host SSD again.
1108
+ const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
1109
+ const hostPath = path.join(dir, entry);
1110
+ return fs.existsSync(hostPath)
1111
+ ? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
1112
+ : [];
1113
+ }));
1094
1114
  const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
1095
1115
  .filter(({ hostPath }) => fs.existsSync(hostPath))
1096
1116
  .flatMap(({ hostPath, containerSubpath }) => [
@@ -1133,6 +1153,8 @@ export async function create(args) {
1133
1153
  volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
1134
1154
  ...dotfilesMount,
1135
1155
  ...toolVolumes,
1156
+ ...tmpfsArgs,
1157
+ ...tmpfsSeedVolumes,
1136
1158
  ...liveMountVolumes,
1137
1159
  ...shellConfigVolumes,
1138
1160
  ...envFile.dockerArgs,
@@ -6,9 +6,6 @@ Commands:
6
6
  Enter sandbox or run a command. N (bare) is the
7
7
  recommended form for task short ids (e.g.
8
8
  'ai sandbox exec 11'); '#N' is also accepted.
9
- start <branch | TASK-id | N | '#N'>
10
- Start an existing stopped sandbox container
11
- (e.g. after the Docker daemon restarted)
12
9
  ls List sandboxes for the current project (the '#'
13
10
  column is a display-only row number; the 'SHORT'
14
11
  column shows the active task short id, '-' if none)
@@ -19,6 +16,9 @@ Commands:
19
16
  rm <branch> | --all | --purge
20
17
  Remove one sandbox, all sandboxes not bound to an
21
18
  active task (--all), or tear down everything (--purge)
19
+ start <branch | TASK-id | N | '#N'>
20
+ Start an existing stopped sandbox container
21
+ (e.g. after the Docker daemon restarted)
22
22
  vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
23
23
 
24
24
  Run 'ai sandbox <command> --help' for details.`;
@@ -47,6 +47,21 @@ export async function runSandbox(args) {
47
47
  }
48
48
  break;
49
49
  }
50
+ case 'ls': {
51
+ const { ls } = await import("./commands/ls.js");
52
+ ls(rest);
53
+ break;
54
+ }
55
+ case 'prune': {
56
+ const { prune } = await import("./commands/prune.js");
57
+ await prune(rest);
58
+ break;
59
+ }
60
+ case 'rebuild': {
61
+ const { rebuild } = await import("./commands/rebuild.js");
62
+ await rebuild(rest);
63
+ break;
64
+ }
50
65
  case 'refresh': {
51
66
  const { refresh } = await import("./commands/refresh.js");
52
67
  const exitCode = await refresh(rest);
@@ -55,24 +70,14 @@ export async function runSandbox(args) {
55
70
  }
56
71
  break;
57
72
  }
58
- case 'start': {
59
- const { start } = await import("./commands/start.js");
60
- await start(rest);
61
- break;
62
- }
63
- case 'ls': {
64
- const { ls } = await import("./commands/ls.js");
65
- ls(rest);
66
- break;
67
- }
68
73
  case 'rm': {
69
74
  const { rm } = await import("./commands/rm.js");
70
75
  await rm(rest);
71
76
  break;
72
77
  }
73
- case 'prune': {
74
- const { prune } = await import("./commands/prune.js");
75
- await prune(rest);
78
+ case 'start': {
79
+ const { start } = await import("./commands/start.js");
80
+ await start(rest);
76
81
  break;
77
82
  }
78
83
  case 'vm': {
@@ -80,11 +85,6 @@ export async function runSandbox(args) {
80
85
  await vm(rest);
81
86
  break;
82
87
  }
83
- case 'rebuild': {
84
- const { rebuild } = await import("./commands/rebuild.js");
85
- await rebuild(rest);
86
- break;
87
- }
88
88
  default:
89
89
  throw new Error(`Unknown sandbox command: ${subcommand}`);
90
90
  }
@@ -41,6 +41,12 @@ function createBuiltinTools(home, project) {
41
41
  containerMount: '/home/devuser/.codex',
42
42
  versionCmd: 'codex --version',
43
43
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
44
+ // codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
45
+ // a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
46
+ // codex home as tmpfs so those logs stay in RAM and die with the container.
47
+ // Only the seeded config (config.toml, model-catalogs) is bound back over
48
+ // the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
49
+ tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
44
50
  hostLiveMounts: [
45
51
  { hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
46
52
  ],
@@ -218,6 +224,18 @@ function parseHostLiveMounts(value, context) {
218
224
  };
219
225
  });
220
226
  }
227
+ function parseTmpfs(value, context) {
228
+ if (value === undefined) {
229
+ return undefined;
230
+ }
231
+ if (!isPlainObject(value)) {
232
+ throw new Error(`${context}: field "tmpfs" must be an object when provided`);
233
+ }
234
+ return {
235
+ size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
236
+ seed: asStringArray(value.seed, 'tmpfs.seed', context)
237
+ };
238
+ }
221
239
  export function parseCustomTool(entry, index, options) {
222
240
  const context = `customTools[${index}]`;
223
241
  if (!isPlainObject(entry)) {
@@ -246,7 +264,8 @@ export function parseCustomTool(entry, index, options) {
246
264
  hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
247
265
  pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
248
266
  hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
249
- postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
267
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context),
268
+ tmpfs: parseTmpfs(entry.tmpfs, context)
250
269
  };
251
270
  validateTool(tool);
252
271
  return tool;
@@ -3,12 +3,14 @@ import { formatTable } from "../../table.js";
3
3
  import { resolveTaskRef } from "../resolve-ref.js";
4
4
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
5
5
 
6
- Renders a task's activity log as a chronological timeline table.
6
+ Renders a task's activity log as a per-step status table. A step's start and
7
+ completion are paired onto one row: STARTED holds the start time, DONE the
8
+ completion time (or '(in progress)' while still running).
7
9
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
8
10
 
9
- Columns: # (timeline position) / TIME / STEP / AGENT / NOTE
11
+ Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
10
12
  `;
11
- const TABLE_HEADERS = ['#', 'TIME', 'STEP', 'AGENT', 'NOTE'];
13
+ const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
12
14
  // The activity-log H2 heading is language-dependent (zh template / en template).
13
15
  const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
14
16
  const NEXT_H2_RE = /^##\s/;
@@ -16,6 +18,11 @@ const NEXT_H2_RE = /^##\s/;
16
18
  // (U+2014). STEP/AGENT are non-greedy so a note that itself contains ' — ' or
17
19
  // '→' is not mis-split; NOTE greedily takes the rest of the line.
18
20
  const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
21
+ // A start marker reuses the normal entry grammar and only suffixes its action
22
+ // with ` [started]`; the matching done entry carries the identical base action
23
+ // without the suffix. Pairing therefore keys on the base action (including any
24
+ // `(Round N)`), so every round and every repeated execution pairs on its own.
25
+ const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
19
26
  function parseActivityLog(content) {
20
27
  const lines = content.split('\n');
21
28
  let i = 0;
@@ -40,6 +47,41 @@ function parseActivityLog(content) {
40
47
  parsed.sort((a, b) => a.epoch - b.epoch || a.order - b.order);
41
48
  return { sectionFound: true, entries: parsed.map((p) => p.entry) };
42
49
  }
50
+ // Collapse a chronological entry list into per-step rows: a `[started]` marker
51
+ // opens a row, the next matching done entry fills it in place (FIFO per base
52
+ // action). Started-only rows stay in flight; done-only entries (legacy logs with
53
+ // no start marker) render as standalone rows. Result order = first-seen order,
54
+ // which is already ascending because `entries` is sorted ascending.
55
+ function pairEntries(entries) {
56
+ const rows = [];
57
+ const open = new Map();
58
+ for (const e of entries) {
59
+ const isStarted = STARTED_SUFFIX_RE.test(e.step);
60
+ const base = e.step.replace(STARTED_SUFFIX_RE, '');
61
+ if (isStarted) {
62
+ const row = { step: base, agent: e.agent, started: e.time, done: '', note: e.note };
63
+ rows.push(row);
64
+ const queue = open.get(base);
65
+ if (queue)
66
+ queue.push(row);
67
+ else
68
+ open.set(base, [row]);
69
+ }
70
+ else {
71
+ const pending = open.get(base)?.shift();
72
+ if (pending) {
73
+ // Done fills the open row; the done entry carries the meaningful note.
74
+ pending.done = e.time;
75
+ pending.agent = e.agent;
76
+ pending.note = e.note;
77
+ }
78
+ else {
79
+ rows.push({ step: base, agent: e.agent, started: '', done: e.time, note: e.note });
80
+ }
81
+ }
82
+ }
83
+ return rows;
84
+ }
43
85
  function log(args = []) {
44
86
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
45
87
  process.stdout.write(USAGE);
@@ -65,11 +107,19 @@ function log(args = []) {
65
107
  process.exitCode = 1;
66
108
  return;
67
109
  }
68
- const rows = entries.map((e, idx) => [String(idx + 1), e.time, e.step, e.agent, e.note]);
110
+ const steps = pairEntries(entries);
111
+ const rows = steps.map((s, idx) => [
112
+ String(idx + 1),
113
+ s.step,
114
+ s.agent,
115
+ s.started,
116
+ s.done || (s.started ? '(in progress)' : ''),
117
+ s.note
118
+ ]);
69
119
  for (const line of formatTable(TABLE_HEADERS, rows, { zebra: Boolean(process.stdout.isTTY) })) {
70
120
  process.stdout.write(`${line}\n`);
71
121
  }
72
- process.stdout.write(`Total: ${entries.length} entries\n`);
122
+ process.stdout.write(`Total: ${steps.length} steps\n`);
73
123
  }
74
- export { log, parseActivityLog };
124
+ export { log, parseActivityLog, pairEntries };
75
125
  //# sourceMappingURL=log.js.map
@@ -34,14 +34,9 @@ export async function runTask(args) {
34
34
  return;
35
35
  }
36
36
  switch (subcommand) {
37
- case 'ls': {
38
- const { ls } = await import("./commands/ls.js");
39
- ls(rest);
40
- break;
41
- }
42
- case 'show': {
43
- const { show } = await import("./commands/show.js");
44
- show(rest);
37
+ case 'cat': {
38
+ const { cat } = await import("./commands/cat.js");
39
+ cat(rest);
45
40
  break;
46
41
  }
47
42
  case 'files': {
@@ -49,11 +44,6 @@ export async function runTask(args) {
49
44
  files(rest);
50
45
  break;
51
46
  }
52
- case 'cat': {
53
- const { cat } = await import("./commands/cat.js");
54
- cat(rest);
55
- break;
56
- }
57
47
  case 'grep': {
58
48
  const { grep } = await import("./commands/grep.js");
59
49
  grep(rest);
@@ -64,6 +54,16 @@ export async function runTask(args) {
64
54
  log(rest);
65
55
  break;
66
56
  }
57
+ case 'ls': {
58
+ const { ls } = await import("./commands/ls.js");
59
+ ls(rest);
60
+ break;
61
+ }
62
+ case 'show': {
63
+ const { show } = await import("./commands/show.js");
64
+ show(rest);
65
+ break;
66
+ }
67
67
  case 'status': {
68
68
  const { status } = await import("./commands/status.js");
69
69
  status(rest);
@@ -83,7 +83,7 @@ function syncFileRegistry(config, platformType, enabledTUIs) {
83
83
  }
84
84
  async function cmdUpdate() {
85
85
  console.log('');
86
- console.log(' agent-infra update');
86
+ console.log(' ai update');
87
87
  console.log(' ==================================');
88
88
  console.log('');
89
89
  // check config exists
package/lib/init.ts CHANGED
@@ -119,7 +119,7 @@ function parseLocalSources(input: string): SourceEntry[] {
119
119
 
120
120
  async function cmdInit(): Promise<void> {
121
121
  console.log('');
122
- console.log(' agent-infra init');
122
+ console.log(' ai init');
123
123
  console.log(' ================================');
124
124
  console.log(' Optional template and skill sources can be added now or later in .agents/.airc.json.');
125
125
  console.log('');
package/lib/merge.ts CHANGED
@@ -901,7 +901,7 @@ function printReport(report: MergeReport): void {
901
901
  async function cmdMerge(args: string[]): Promise<void> {
902
902
  const sourcePath = args[0];
903
903
  if (!sourcePath) {
904
- throw new Error('Usage: agent-infra merge <source-path>');
904
+ throw new Error('Usage: ai merge <source-path>');
905
905
  }
906
906
 
907
907
  const resolvedSource = path.resolve(sourcePath);
@@ -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
- volumeArg(engine, dir, tool.containerMount)
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,
@@ -6,9 +6,6 @@ Commands:
6
6
  Enter sandbox or run a command. N (bare) is the
7
7
  recommended form for task short ids (e.g.
8
8
  'ai sandbox exec 11'); '#N' is also accepted.
9
- start <branch | TASK-id | N | '#N'>
10
- Start an existing stopped sandbox container
11
- (e.g. after the Docker daemon restarted)
12
9
  ls List sandboxes for the current project (the '#'
13
10
  column is a display-only row number; the 'SHORT'
14
11
  column shows the active task short id, '-' if none)
@@ -19,6 +16,9 @@ Commands:
19
16
  rm <branch> | --all | --purge
20
17
  Remove one sandbox, all sandboxes not bound to an
21
18
  active task (--all), or tear down everything (--purge)
19
+ start <branch | TASK-id | N | '#N'>
20
+ Start an existing stopped sandbox container
21
+ (e.g. after the Docker daemon restarted)
22
22
  vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
23
23
 
24
24
  Run 'ai sandbox <command> --help' for details.`;
@@ -51,6 +51,21 @@ export async function runSandbox(args: string[]): Promise<void> {
51
51
  }
52
52
  break;
53
53
  }
54
+ case 'ls': {
55
+ const { ls } = await import('./commands/ls.ts');
56
+ ls(rest);
57
+ break;
58
+ }
59
+ case 'prune': {
60
+ const { prune } = await import('./commands/prune.ts');
61
+ await prune(rest);
62
+ break;
63
+ }
64
+ case 'rebuild': {
65
+ const { rebuild } = await import('./commands/rebuild.ts');
66
+ await rebuild(rest);
67
+ break;
68
+ }
54
69
  case 'refresh': {
55
70
  const { refresh } = await import('./commands/refresh.ts');
56
71
  const exitCode = await refresh(rest);
@@ -59,24 +74,14 @@ export async function runSandbox(args: string[]): Promise<void> {
59
74
  }
60
75
  break;
61
76
  }
62
- case 'start': {
63
- const { start } = await import('./commands/start.ts');
64
- await start(rest);
65
- break;
66
- }
67
- case 'ls': {
68
- const { ls } = await import('./commands/ls.ts');
69
- ls(rest);
70
- break;
71
- }
72
77
  case 'rm': {
73
78
  const { rm } = await import('./commands/rm.ts');
74
79
  await rm(rest);
75
80
  break;
76
81
  }
77
- case 'prune': {
78
- const { prune } = await import('./commands/prune.ts');
79
- await prune(rest);
82
+ case 'start': {
83
+ const { start } = await import('./commands/start.ts');
84
+ await start(rest);
80
85
  break;
81
86
  }
82
87
  case 'vm': {
@@ -84,11 +89,6 @@ export async function runSandbox(args: string[]): Promise<void> {
84
89
  await vm(rest);
85
90
  break;
86
91
  }
87
- case 'rebuild': {
88
- const { rebuild } = await import('./commands/rebuild.ts');
89
- await rebuild(rest);
90
- break;
91
- }
92
92
  default:
93
93
  throw new Error(`Unknown sandbox command: ${subcommand}`);
94
94
  }
@@ -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);