@fitlab-ai/agent-infra 0.8.0 → 0.8.2

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 (86) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/cli.ts +4 -4
  4. package/dist/bin/cli.js +4 -4
  5. package/dist/lib/defaults.json +1 -0
  6. package/dist/lib/run/index.js +96 -5
  7. package/dist/lib/sandbox/capture.js +113 -4
  8. package/dist/lib/sandbox/commands/create.js +37 -4
  9. package/dist/lib/sandbox/commands/enter.js +7 -3
  10. package/dist/lib/sandbox/config.js +1 -1
  11. package/dist/lib/sandbox/credentials.js +42 -3
  12. package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
  13. package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
  14. package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
  15. package/dist/lib/sandbox/tools.js +9 -0
  16. package/dist/lib/server/adapters/feishu/index.js +5 -3
  17. package/dist/lib/server/adapters/feishu/transport.js +20 -2
  18. package/dist/lib/server/protocol.js +20 -4
  19. package/dist/lib/task/commands/log.js +36 -14
  20. package/dist/lib/task/commands/status.js +144 -45
  21. package/dist/lib/update.js +31 -0
  22. package/lib/defaults.json +1 -0
  23. package/lib/run/index.ts +119 -6
  24. package/lib/sandbox/capture.ts +136 -5
  25. package/lib/sandbox/commands/create.ts +42 -4
  26. package/lib/sandbox/commands/enter.ts +8 -2
  27. package/lib/sandbox/config.ts +1 -1
  28. package/lib/sandbox/credentials.ts +57 -3
  29. package/lib/sandbox/runtimes/base.dockerfile +13 -0
  30. package/lib/sandbox/runtimes/node20.dockerfile +1 -1
  31. package/lib/sandbox/runtimes/node22.dockerfile +1 -1
  32. package/lib/sandbox/tools.ts +9 -0
  33. package/lib/server/adapters/feishu/index.ts +5 -3
  34. package/lib/server/adapters/feishu/transport.ts +32 -3
  35. package/lib/server/protocol.ts +20 -4
  36. package/lib/task/commands/log.ts +36 -14
  37. package/lib/task/commands/status.ts +200 -58
  38. package/lib/update.ts +34 -0
  39. package/package.json +5 -4
  40. package/templates/.agents/README.en.md +3 -3
  41. package/templates/.agents/README.zh-CN.md +3 -3
  42. package/templates/.agents/rules/next-step-output.en.md +1 -1
  43. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  44. package/templates/.agents/rules/pr-sync.github.en.md +3 -3
  45. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
  46. package/templates/.agents/rules/task-management.en.md +1 -1
  47. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  48. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  49. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  50. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  51. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  52. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
  53. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
  54. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  55. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  56. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  57. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  58. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  59. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  60. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
  61. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
  62. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
  63. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
  64. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
  65. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
  66. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  67. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  68. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  69. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  70. package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
  71. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
  72. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
  73. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
  74. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
  75. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
  76. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  77. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  78. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  79. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  80. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
  81. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
  82. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
  83. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
  84. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
  85. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
  86. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +26 -1
@@ -1,24 +1,27 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { execFileSync } from 'node:child_process';
4
+ import { commandForEngine } from '../../sandbox/shell.ts';
3
5
  import { resolveTaskRef } from '../resolve-ref.ts';
4
6
  import { enumerateArtifacts, type Artifact } from '../artifacts.ts';
5
7
  import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontmatter.ts';
6
8
  import { loadShortIdByTaskId } from '../short-id.ts';
9
+ import { parseActivityLog, pairEntries } from './log.ts';
7
10
 
8
11
  const USAGE = `Usage: ai task status <N | #N | TASK-id>
9
12
 
10
- Prints an aggregated "health check" view for a task: header, metadata, an
11
- artifacts summary, git branch state, and best-effort GitHub issue/PR status.
13
+ Prints an aggregated "health check" view for a task: header, metadata,
14
+ artifacts, workflow/runtime execution state, and git branch state.
12
15
  <ref> Bare numeric / '#N' short id, or a full TASK-YYYYMMDD-HHMMSS id.
13
16
 
14
- Git and Platform rows are best-effort: a failed git/gh call degrades that row to
15
- '-' without failing the command.
17
+ Git rows are best-effort: a failed git call degrades that row to '-' without
18
+ failing the command.
16
19
  `;
17
20
 
18
21
  const DASH = '-';
19
22
 
20
23
  // Subprocess boundary: the single place this command shells out. Injectable so
21
- // the collectors below can be unit-tested without spawning git/gh. Returns the
24
+ // the collectors below can be unit-tested without spawning git. Returns the
22
25
  // command's stdout; throws (like execFileSync) on a non-zero exit or spawn error.
23
26
  type Runner = (file: string, args: string[]) => string;
24
27
 
@@ -27,7 +30,7 @@ function makeRunner(cwd: string): Runner {
27
30
  execFileSync(file, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
28
31
  }
29
32
 
30
- // Run `run` and swallow any failure into null, so a single failing git/gh call
33
+ // Run `run` and swallow any failure into null, so a single failing git call
31
34
  // degrades only its own field instead of aborting the whole view.
32
35
  function tryRun(run: Runner, file: string, args: string[]): string | null {
33
36
  try {
@@ -47,9 +50,7 @@ const METADATA_KEYS = [
47
50
  'branch',
48
51
  'assigned_to',
49
52
  'created_at',
50
- 'updated_at',
51
- 'issue_number',
52
- 'pr_status'
53
+ 'updated_at'
53
54
  ] as const;
54
55
 
55
56
  function collectMetadata(fm: Frontmatter): [string, string][] {
@@ -155,55 +156,176 @@ function collectGit(frontmatterBranch: string, run: Runner): GitInfo {
155
156
  return { current, frontmatter, match, exists, uncommitted, aheadBehind };
156
157
  }
157
158
 
158
- type PlatformInfo = { issue: string; pr: string };
159
-
160
- function collectPlatform(fm: Frontmatter, run: Runner): PlatformInfo {
161
- let issue = DASH;
162
- if (fm.issue_number && /^\d+$/.test(fm.issue_number)) {
163
- const out = tryRun(run, 'gh', ['issue', 'view', fm.issue_number, '--json', 'state,labels']);
164
- if (out !== null) {
165
- try {
166
- const data = JSON.parse(out);
167
- const labels = Array.isArray(data.labels)
168
- ? data.labels.map((label: { name: string }) => label.name).join(', ')
169
- : '';
170
- issue = labels ? `${data.state} [${labels}]` : `${data.state}`;
171
- } catch {
172
- issue = DASH;
173
- }
174
- }
159
+ type WorkflowInfo = {
160
+ state: string;
161
+ step: string;
162
+ agent: string;
163
+ startedAt: string;
164
+ doneAt: string;
165
+ stale: string;
166
+ };
167
+
168
+ const STALE_MS = 60 * 60 * 1000;
169
+
170
+ function parseActivityTime(value: string): number {
171
+ const epoch = Date.parse(value.replace(' ', 'T'));
172
+ return Number.isFinite(epoch) ? epoch : Number.NaN;
173
+ }
174
+
175
+ function collectWorkflow(content: string, now: Date = new Date()): WorkflowInfo {
176
+ const parsed = parseActivityLog(content);
177
+ if (!parsed.sectionFound || parsed.entries.length === 0) {
178
+ return { state: 'unknown', step: DASH, agent: DASH, startedAt: DASH, doneAt: DASH, stale: DASH };
179
+ }
180
+
181
+ const rows = pairEntries(parsed.entries);
182
+ const latest = rows.at(-1);
183
+ if (!latest) {
184
+ return { state: 'unknown', step: DASH, agent: DASH, startedAt: DASH, doneAt: DASH, stale: DASH };
175
185
  }
176
186
 
177
- let pr = DASH;
178
- if (fm.pr_status === 'created' && fm.pr_number && /^\d+$/.test(fm.pr_number)) {
179
- const out = tryRun(run, 'gh', ['pr', 'view', fm.pr_number, '--json', 'state,statusCheckRollup']);
180
- if (out !== null) {
181
- try {
182
- const data = JSON.parse(out);
183
- const rollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : [];
184
- const passed = rollup.filter(
185
- (check: { conclusion?: string; state?: string }) =>
186
- check.conclusion === 'SUCCESS' || check.state === 'SUCCESS'
187
- ).length;
188
- pr = rollup.length > 0 ? `${data.state}, checks: ${passed}/${rollup.length}` : `${data.state}`;
189
- } catch {
190
- pr = DASH;
187
+ const inProgress = latest.started !== '' && latest.done === '';
188
+ const state = inProgress ? 'in-progress' : latest.done ? 'idle' : 'unknown';
189
+ let stale = DASH;
190
+ if (inProgress) {
191
+ const started = parseActivityTime(latest.started);
192
+ stale = Number.isFinite(started) ? (now.getTime() - started > STALE_MS ? 'yes' : 'no') : 'unknown';
193
+ }
194
+
195
+ return {
196
+ state,
197
+ step: latest.step || DASH,
198
+ agent: latest.agent || DASH,
199
+ startedAt: latest.started || DASH,
200
+ doneAt: latest.done || DASH,
201
+ stale
202
+ };
203
+ }
204
+
205
+ type RuntimeInfo = {
206
+ mode: string;
207
+ status: string;
208
+ run: string;
209
+ tmux: string;
210
+ startedAt: string;
211
+ finishedAt: string;
212
+ exitCode: string;
213
+ log: string;
214
+ };
215
+
216
+ type ManagedRunRecord = {
217
+ run_id: string;
218
+ engine: string;
219
+ container: string;
220
+ run_dir: string;
221
+ status_file: string;
222
+ log_file: string;
223
+ };
224
+
225
+ function latestRunRecord(taskDir: string): ManagedRunRecord | null {
226
+ const runsDir = path.join(taskDir, 'runs');
227
+ if (!fs.existsSync(runsDir)) return null;
228
+ const candidates: { path: string; mtimeMs: number }[] = [];
229
+ for (const entry of fs.readdirSync(runsDir)) {
230
+ if (!entry.endsWith('.json')) continue;
231
+ const filePath = path.join(runsDir, entry);
232
+ const stat = fs.statSync(filePath);
233
+ if (stat.isFile()) candidates.push({ path: filePath, mtimeMs: stat.mtimeMs });
234
+ }
235
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
236
+
237
+ for (const candidate of candidates) {
238
+ try {
239
+ const data = JSON.parse(fs.readFileSync(candidate.path, 'utf8'));
240
+ if (
241
+ typeof data?.run_id === 'string' &&
242
+ typeof data?.engine === 'string' &&
243
+ typeof data?.container === 'string' &&
244
+ typeof data?.run_dir === 'string'
245
+ ) {
246
+ return {
247
+ run_id: data.run_id,
248
+ engine: data.engine,
249
+ container: data.container,
250
+ run_dir: data.run_dir,
251
+ status_file:
252
+ typeof data.status_file === 'string' ? data.status_file : `${data.run_dir}/status`,
253
+ log_file:
254
+ typeof data.log_file === 'string' ? data.log_file : `${data.run_dir}/output.log`
255
+ };
191
256
  }
257
+ } catch {
258
+ // Ignore malformed local records and try the next newest file.
192
259
  }
193
260
  }
194
261
 
195
- return { issue, pr };
262
+ return null;
263
+ }
264
+
265
+ function readRuntimeFile(record: ManagedRunRecord, filePath: string, run: Runner): string | null {
266
+ const command = commandForEngine(record.engine, 'docker', ['exec', record.container, 'cat', filePath]);
267
+ const output = tryRun(run, command.cmd, command.args);
268
+ return output === null ? null : output.trim();
269
+ }
270
+
271
+ function runtimeValue(record: ManagedRunRecord, name: string, run: Runner): string {
272
+ const output = readRuntimeFile(record, `${record.run_dir}/${name}`, run);
273
+ return output ? output : DASH;
274
+ }
275
+
276
+ function collectRuntime(taskDir: string, workflow: WorkflowInfo, run: Runner): RuntimeInfo {
277
+ const record = latestRunRecord(taskDir);
278
+ if (!record) {
279
+ return workflow.state === 'in-progress'
280
+ ? {
281
+ mode: 'unmanaged',
282
+ status: 'inferred-from-workflow',
283
+ run: DASH,
284
+ tmux: DASH,
285
+ startedAt: DASH,
286
+ finishedAt: DASH,
287
+ exitCode: DASH,
288
+ log: DASH
289
+ }
290
+ : {
291
+ mode: 'none',
292
+ status: DASH,
293
+ run: DASH,
294
+ tmux: DASH,
295
+ startedAt: DASH,
296
+ finishedAt: DASH,
297
+ exitCode: DASH,
298
+ log: DASH
299
+ };
300
+ }
301
+
302
+ const status = readRuntimeFile(record, record.status_file, run)?.trim() || 'unknown';
303
+ const session = runtimeValue(record, 'session', run);
304
+ const window = runtimeValue(record, 'window', run);
305
+ const pane = runtimeValue(record, 'pane', run);
306
+ const tmux = session !== DASH && window !== DASH && pane !== DASH ? `${session}:${window}:${pane}` : DASH;
307
+
308
+ return {
309
+ mode: 'managed-tmux',
310
+ status,
311
+ run: record.run_id,
312
+ tmux,
313
+ startedAt: runtimeValue(record, 'started_at', run),
314
+ finishedAt: runtimeValue(record, 'finished_at', run),
315
+ exitCode: runtimeValue(record, 'exit_code', run),
316
+ log: record.log_file
317
+ };
196
318
  }
197
319
 
198
320
  type StatusModel = {
199
321
  taskId: string;
200
322
  shortId: string;
201
323
  title: string;
202
- issueNumber: string;
203
324
  metadata: [string, string][];
204
325
  artifacts: { count: number; groups: { stage: string; files: string[] }[] };
326
+ workflow: WorkflowInfo;
327
+ runtime: RuntimeInfo;
205
328
  git: GitInfo;
206
- platform: PlatformInfo;
207
329
  };
208
330
 
209
331
  // Indent each label/value pair by two spaces and pad labels to a common width so
@@ -228,6 +350,34 @@ function renderStatus(model: StatusModel): string[] {
228
350
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
229
351
  }
230
352
 
353
+ lines.push(
354
+ '',
355
+ 'Workflow',
356
+ ...renderPairs([
357
+ ['state', model.workflow.state],
358
+ ['step', model.workflow.step],
359
+ ['agent', model.workflow.agent],
360
+ ['started_at', model.workflow.startedAt],
361
+ ['done_at', model.workflow.doneAt],
362
+ ['stale', model.workflow.stale]
363
+ ])
364
+ );
365
+
366
+ lines.push(
367
+ '',
368
+ 'Runtime',
369
+ ...renderPairs([
370
+ ['mode', model.runtime.mode],
371
+ ['status', model.runtime.status],
372
+ ['run', model.runtime.run],
373
+ ['tmux', model.runtime.tmux],
374
+ ['started_at', model.runtime.startedAt],
375
+ ['finished_at', model.runtime.finishedAt],
376
+ ['exit_code', model.runtime.exitCode],
377
+ ['log', model.runtime.log]
378
+ ])
379
+ );
380
+
231
381
  lines.push(
232
382
  '',
233
383
  'Git',
@@ -241,16 +391,6 @@ function renderStatus(model: StatusModel): string[] {
241
391
  ])
242
392
  );
243
393
 
244
- const issueLabel = model.issueNumber ? `issue #${model.issueNumber}` : 'issue';
245
- lines.push(
246
- '',
247
- 'Platform',
248
- ...renderPairs([
249
- [issueLabel, model.platform.issue],
250
- ['pr', model.platform.pr]
251
- ])
252
- );
253
-
254
394
  return lines;
255
395
  }
256
396
 
@@ -272,16 +412,17 @@ function status(args: string[] = []): void {
272
412
  const fm = parseTaskFrontmatter(content);
273
413
  const run = makeRunner(resolved.repoRoot);
274
414
  const artifacts = enumerateArtifacts(resolved.taskDir);
415
+ const workflow = collectWorkflow(content);
275
416
 
276
417
  const model: StatusModel = {
277
418
  taskId: resolved.taskId,
278
419
  shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
279
420
  title: extractTitle(content),
280
- issueNumber: fm.issue_number && /^\d+$/.test(fm.issue_number) ? fm.issue_number : '',
281
421
  metadata: collectMetadata(fm),
282
422
  artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
283
- git: collectGit(fm.branch ?? '', run),
284
- platform: collectPlatform(fm, run)
423
+ workflow,
424
+ runtime: collectRuntime(resolved.taskDir, workflow, run),
425
+ git: collectGit(fm.branch ?? '', run)
285
426
  };
286
427
 
287
428
  for (const line of renderStatus(model)) {
@@ -295,8 +436,9 @@ export {
295
436
  collectMetadata,
296
437
  groupArtifacts,
297
438
  collectGit,
298
- collectPlatform,
439
+ collectWorkflow,
440
+ collectRuntime,
299
441
  renderStatus,
300
442
  METADATA_KEYS
301
443
  };
302
- export type { Runner, GitInfo, PlatformInfo, StatusModel };
444
+ export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel };
package/lib/update.ts CHANGED
@@ -40,6 +40,9 @@ const defaults = JSON.parse(
40
40
 
41
41
  const CONFIG_DIR = '.agents';
42
42
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
43
+ const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
44
+ const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
45
+ const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
43
46
 
44
47
  // One-time migration of the legacy project-level PR switch to the three-state
45
48
  // `prFlow` preference. `true` (the old default / "PR flow on") maps to the
@@ -60,6 +63,26 @@ function migratePrFlow(config: UpdateConfig): 'required' | 'disabled' | null {
60
63
  return null;
61
64
  }
62
65
 
66
+ function isLegacyDefaultSandboxTools(value: unknown): value is string[] {
67
+ if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
68
+ return false;
69
+ }
70
+ const tools = new Set(value);
71
+ return LEGACY_DEFAULT_SANDBOX_TOOLS.every((tool) => tools.has(tool));
72
+ }
73
+
74
+ function migrateSandboxTools(config: UpdateConfig): boolean {
75
+ const tools = config.sandbox?.tools;
76
+ if (!isLegacyDefaultSandboxTools(tools)) {
77
+ return false;
78
+ }
79
+ config.sandbox = {
80
+ ...config.sandbox,
81
+ tools: [...DEFAULT_SANDBOX_TOOLS]
82
+ };
83
+ return true;
84
+ }
85
+
63
86
  function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
64
87
  const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
65
88
  if (!top.startsWith('.')) return false;
@@ -215,6 +238,7 @@ async function cmdUpdate(): Promise<void> {
215
238
  const taskAdded = !config.task;
216
239
  const labelsAdded = !config.labels;
217
240
  const prFlowMigrated = migratePrFlow(config);
241
+ const sandboxToolsMigrated = !sandboxAdded && migrateSandboxTools(config);
218
242
  let configChanged = changed;
219
243
 
220
244
  if (platformAdded) {
@@ -241,6 +265,10 @@ async function cmdUpdate(): Promise<void> {
241
265
  configChanged = true;
242
266
  }
243
267
 
268
+ if (sandboxToolsMigrated) {
269
+ configChanged = true;
270
+ }
271
+
244
272
  if (configChanged) {
245
273
  console.log('');
246
274
  if (hasNewEntries) {
@@ -267,6 +295,9 @@ async function cmdUpdate(): Promise<void> {
267
295
  if (prFlowMigrated) {
268
296
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
269
297
  }
298
+ if (sandboxToolsMigrated) {
299
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
300
+ }
270
301
  } else {
271
302
  info(`File registry changed in ${CONFIG_PATH}.`);
272
303
  }
@@ -285,6 +316,9 @@ async function cmdUpdate(): Promise<void> {
285
316
  if (hasNewEntries && prFlowMigrated) {
286
317
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
287
318
  }
319
+ if (hasNewEntries && sandboxToolsMigrated) {
320
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
321
+ }
288
322
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
289
323
  ok(`Updated ${CONFIG_PATH}`);
290
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "templates/"
30
30
  ],
31
31
  "engines": {
32
- "node": ">=22"
32
+ "node": ">=22.9.0"
33
33
  },
34
34
  "keywords": [
35
35
  "ai",
@@ -43,7 +43,7 @@
43
43
  "installer"
44
44
  ],
45
45
  "dependencies": {
46
- "@clack/prompts": "1.6.0",
46
+ "@clack/prompts": "1.7.0",
47
47
  "@larksuiteoapi/node-sdk": "^1.68.0",
48
48
  "cross-spawn": "^7.0.6",
49
49
  "picocolors": "1.1.1",
@@ -58,13 +58,14 @@
58
58
  "typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
59
59
  "test:smoke": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/unit/**/*.test.ts\"",
60
60
  "test:core": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/unit/**/*.test.ts\" \"tests/integration/**/*.test.ts\"",
61
+ "check": "npm run typecheck && npm test",
61
62
  "test": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\"",
62
63
  "test:coverage": "npm run build && node --experimental-strip-types --no-warnings --test --experimental-test-coverage \"--test-coverage-exclude=tests/**\" --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov \"tests/**/*.test.ts\"",
63
64
  "prepublishOnly": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\""
64
65
  },
65
66
  "devDependencies": {
66
67
  "@types/cross-spawn": "^6.0.6",
67
- "@types/node": "^24.0.0",
68
+ "@types/node": "^22.20.0",
68
69
  "@types/semver": "^7.7.1",
69
70
  "typescript": "~6.0"
70
71
  },
@@ -260,7 +260,7 @@ Supported `invoke` placeholders:
260
260
  | Placeholder | Replaced with | Example |
261
261
  |-------------|---------------|---------|
262
262
  | `${skillName}` | The skill command name, such as `review-code` or `commit`. | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
263
- | `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
263
+ | `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/your-project:review-code` |
264
264
 
265
265
  Non-namespaced custom TUI:
266
266
 
@@ -280,7 +280,7 @@ Namespaced custom TUI:
280
280
 
281
281
  ```json
282
282
  {
283
- "project": "agent-infra",
283
+ "project": "your-project",
284
284
  "customTUIs": [
285
285
  {
286
286
  "name": "<your-tui-name>",
@@ -295,7 +295,7 @@ Namespaced custom TUI:
295
295
 
296
296
  ## Sandbox Custom Tools
297
297
 
298
- `customTUIs` (above) generates slash-command files but does not change the sandbox image. To install a non-npm TUI (pip / cargo / curl-based / pre-built binary) into the sandbox image and live-mount its credentials, declare it under `sandbox.customTools` in `.agents/.airc.json`. Built-in tools (`claude-code`, `codex`, `opencode`, `gemini-cli`) keep working unchanged.
298
+ `customTUIs` generates slash-command files but does not change the sandbox image. To install a non-npm CLI or tool (pip / cargo / curl-based / pre-built binary) into the sandbox image and live-mount its credentials, declare it under `sandbox.customTools` in `.agents/.airc.json`. Built-in sandbox tools (`claude-code`, `codex`, `opencode`, `gemini-cli`, `agent-infra`) keep working unchanged; `agent-infra` only provides the in-sandbox `ai` / `agent-infra` CLI and is not part of `tuis` or `customTUIs`.
299
299
 
300
300
  ### Required fields
301
301
 
@@ -260,7 +260,7 @@ args: "<task-id>" # 可选
260
260
  | 占位符 | 替换为 | 示例 |
261
261
  |--------|--------|------|
262
262
  | `${skillName}` | skill 命令名,例如 `review-code` 或 `commit`。 | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
263
- | `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
263
+ | `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/your-project:review-code` |
264
264
 
265
265
  不带命名空间的自定义 TUI:
266
266
 
@@ -280,7 +280,7 @@ args: "<task-id>" # 可选
280
280
 
281
281
  ```json
282
282
  {
283
- "project": "agent-infra",
283
+ "project": "your-project",
284
284
  "customTUIs": [
285
285
  {
286
286
  "name": "<your-tui-name>",
@@ -295,7 +295,7 @@ args: "<task-id>" # 可选
295
295
 
296
296
  ## 沙箱自定义工具(Sandbox Custom Tools)
297
297
 
298
- 上文 `customTUIs` 只负责生成 slash-command 文件,**不影响沙箱镜像**。如果要把一个非 npm 分发的 TUI(pip / cargo / curl 脚本 / 裸二进制)装进沙箱镜像、并 live-mount 它的凭证目录,需要在 `.agents/.airc.json` 的 `sandbox.customTools` 中声明。内建的四个工具(`claude-code` / `codex` / `opencode` / `gemini-cli`)行为保持不变。
298
+ `customTUIs` 只负责生成 slash-command 文件,**不影响沙箱镜像**。如果要把一个非 npm 分发的 CLI 或工具(pip / cargo / curl 脚本 / 裸二进制)装进沙箱镜像、并 live-mount 它的凭证目录,需要在 `.agents/.airc.json` 的 `sandbox.customTools` 中声明。内建 sandbox 工具(`claude-code` / `codex` / `opencode` / `gemini-cli` / `agent-infra`)行为保持不变;其中 `agent-infra` 只提供沙箱内 `ai` / `agent-infra` CLI,不属于 `tuis` 或 `customTUIs` 配置。
299
299
 
300
300
  ### 必填字段
301
301
 
@@ -59,7 +59,7 @@ Completed at: YYYY-MM-DD HH:mm:ss
59
59
  ```
60
60
 
61
61
  - Value command (local timezone, no offset): `date "+%Y-%m-%d %H:%M:%S"`
62
- - Position: it must be the last line of the entire user-facing output, after all "Next steps" commands. If a scenario has a conditional reminder line after the commands (e.g. the env-blocked reminder), the completion line goes after that reminder.
62
+ - Position: it must be the last line of the entire user-facing output, after all "Next steps" commands. If a scenario has a conditional reminder line after the commands (e.g. the manual-validation reminder), the completion line goes after that reminder.
63
63
  - This line is for terminal scanning only; it is never written to any artifact file or Issue/PR comment. The single source of truth for completion time remains the Activity Log in task.md.
64
64
 
65
65
  ## Pending human-decision pre-block (review-* only, when {h} > 0)
@@ -59,7 +59,7 @@ Completed at: YYYY-MM-DD HH:mm:ss
59
59
  ```
60
60
 
61
61
  - 取值命令(本地时区、不带偏移):`date "+%Y-%m-%d %H:%M:%S"`
62
- - 位置:必须是整段面向用户输出的最后一行,排在所有「下一步」命令之后。若某场景在命令之后还有条件性提醒行(如 env-blocked 提醒),收尾行排在该提醒行之后。
62
+ - 位置:必须是整段面向用户输出的最后一行,排在所有「下一步」命令之后。若某场景在命令之后还有条件性提醒行(如 manual-validation 提醒),收尾行排在该提醒行之后。
63
63
  - 该行只用于终端扫视,不写入任何产物文件或 Issue/PR 评论;完成时刻的单一事实源仍是 task.md 的 Activity Log。
64
64
 
65
65
  ## 人工裁决待办前置块(review-* 专用,{h} > 0 时)
@@ -34,7 +34,7 @@ Aggregation rules:
34
34
  - if one artifact class is missing, treat it as "no data for this stage" and continue
35
35
  - Manual verification section: include only post-code-stage checks that still require a human to execute or judge and that the AI cannot close on its own.
36
36
  - **Admission boundary**: the verification result depends on a real environment, permissions, account, external system, or human judgment, and cannot be closed by an agent rerunning tests, adding checks, or continuing the fix loop.
37
- - **Sources**: `review-code*` "Environment-Blocked Findings", plus `code*` items that satisfy the boundary above.
37
+ - **Sources**: `review-code*` "Manual Validation Items", plus `code*` items that satisfy the boundary above.
38
38
  - **Wording**: each retained item must state at least "what to verify + location (file/change/scope) + why only a human can verify it".
39
39
  - **Empty rendering**: when there are no retained items, do NOT use the ⚠️ alarm style (it falsely implies a problem). Render the whole block as: heading `### ✅ No Manual Verification Needed` and a single line `No items in this change require manual confirmation.`, with no item list. Only use the `### ⚠️ Manual Verification Required` heading + item list when retained items exist.
40
40
 
@@ -51,7 +51,7 @@ Use this canonical comment body template:
51
51
 
52
52
  **Updated At**: {current-time}
53
53
 
54
- {manual-verify-section}
54
+ {manual-validation-section}
55
55
 
56
56
  ### Key Technical Decisions
57
57
 
@@ -72,7 +72,7 @@ Use this canonical comment body template:
72
72
  *Generated by {agent} · Internal tracking: {task-id}*
73
73
  ```
74
74
 
75
- > Render `{manual-verify-section}` per the "manual verification section" aggregation rule above: with retained items → `### ⚠️ Manual Verification Required` heading + quote + item list; with none → `### ✅ No Manual Verification Needed` heading + a single line `No items in this change require manual confirmation.` (no ⚠️, no list).
75
+ > Render `{manual-validation-section}` per the "manual verification section" aggregation rule above: with retained items → `### ⚠️ Manual Verification Required` heading + quote + item list; with none → `### ✅ No Manual Verification Needed` heading + a single line `No items in this change require manual confirmation.` (no ⚠️, no list).
76
76
 
77
77
  ## Comment Lookup And Update
78
78
 
@@ -34,7 +34,7 @@
34
34
  - 某一类产物缺失时,按“无该阶段数据”处理并继续生成
35
35
  - 需人工校验段落:只收进入 code 阶段后仍需人实际执行或判断、AI 无法自行关闭的校验点。
36
36
  - **准入边界**:校验结论依赖真实环境、权限、账号、外部系统或人工判断,且无法通过 agent 重跑测试、补充检查或继续修复自行关闭。
37
- - **来源**:`review-code*` 的「环境性遗留」,以及 `code*` 中满足上述边界的校验点。
37
+ - **来源**:`review-code*` 的「人工校验项」,以及 `code*` 中满足上述边界的校验点。
38
38
  - **写法**:每条保留项至少写明「校验什么 + 定位(文件/改动/范围)+ 为什么只能由人校验」。
39
39
  - **空集渲染**:无保留项时,不要使用 ⚠️ 告警样式(会让人误以为有问题)。整段降级渲染为:标题 `### ✅ 无需人工校验`,正文一行 `本次改动无需人工确认事项。`,不带条目列表。有保留项时才用 `### ⚠️ 需人工校验` 标题 + 条目列表。
40
40
 
@@ -51,7 +51,7 @@
51
51
 
52
52
  **更新时间**:{当前时间}
53
53
 
54
- {manual-verify-section}
54
+ {manual-validation-section}
55
55
 
56
56
  ### 关键技术决策
57
57
 
@@ -72,7 +72,7 @@
72
72
  *由 {agent} 自动生成 · 内部追踪:{task-id}*
73
73
  ```
74
74
 
75
- > `{manual-verify-section}` 按上文「需人工校验段落」聚合规则渲染:有保留项 → `### ⚠️ 需人工校验` 标题 + 引用说明 + 条目列表;无保留项 → `### ✅ 无需人工校验` 标题 + 一行 `本次改动无需人工确认事项。`(不带 ⚠️、不带列表)。
75
+ > `{manual-validation-section}` 按上文「需人工校验段落」聚合规则渲染:有保留项 → `### ⚠️ 需人工校验` 标题 + 引用说明 + 条目列表;无保留项 → `### ✅ 无需人工校验` 标题 + 一行 `本次改动无需人工确认事项。`(不带 ⚠️、不带列表)。
76
76
 
77
77
  ## 评论查找与更新
78
78
 
@@ -52,7 +52,7 @@ Map user intent to the corresponding workflow command:
52
52
 
53
53
  **Pairing and rendering** (`ai task log`): a started entry pairs with the next same-`{base}` done entry onto one row (repeated executions of the same base pair FIFO by ascending time). The STARTED column shows the start time, DONE the completion time; started with no done = in progress (DONE shows `(in progress)`); done with no started (legacy logs) = a standalone completed row. All three shapes are valid and never error.
54
54
 
55
- **Human counts** (`ai task log`): review step rows fold two human counts into the NOTE verdict text, comma-separated, right after `blockers/major/minor`, with fixed English labels `Manual-verify: {e}, Human-decision: {h}`. `Human-decision` (`{h}`) maps canonical step prefixes (`Review Analysis` / `Review Plan` / `Review Code`) to `analysis` / `plan` / `code`, then counts current rows in `## Review Disagreement Ledger` / `## 审查分歧账本` for that stage with `status ∈ {needs-human-decision, human-decided}`. `Manual-verify` (`{e}`) parses `(+ {n} env-blocked)` from the review done note (de-duplicated from the display), defaulting to `0`. Non-review rows carry no human counts.
55
+ **Human counts** (`ai task log`): review step rows fold two human counts into the NOTE verdict text, comma-separated, right after `blockers/major/minor`, with fixed English labels `Manual-validation: {e}, Human-decision: {h}`. `Human-decision` (`{h}`) maps canonical step prefixes (`Review Analysis` / `Review Plan` / `Review Code`) to `analysis` / `plan` / `code`, then counts current rows in `## Review Disagreement Ledger` / `## 审查分歧账本` for that stage with `status ∈ {needs-human-decision, human-decided}`. `Manual-validation` (`{e}`) parses the `Manual-validation: {n}` source field from the review done note (normalized to the same field when displayed), defaulting to `0`. Non-review rows carry no human counts.
56
56
 
57
57
  **Gate** (`checkActivityLog`): when computing the "latest action / freshness" it skips `[started]` lines (ascending-order and format checks still cover every line), so a started marker never satisfies a skill's `expected_action_pattern`.
58
58
 
@@ -56,7 +56,7 @@
56
56
 
57
57
  **配对与渲染**(`ai task log`):按 `{基名}` 把 started 与其后最近的同名 done 配成一行(同基名多次执行按时间升序 FIFO 配对)。STARTED 列显示 started 时间、DONE 列显示 done 时间;只有 started 无 done = 进行中(DONE 显示 `(in progress)`);只有 done 无 started(历史日志)= 单态完成行。三种形态都合法、不报错。
58
58
 
59
- **人工计数**(`ai task log`):审查步骤行把两项人工计数并入 NOTE 的 verdict 文本,逗号分隔、紧随 `blockers/major/minor`,固定英文标签 `Manual-verify: {e}, Human-decision: {h}`。`Human-decision`(`{h}`)按 canonical 步骤名前缀(`Review Analysis` / `Review Plan` / `Review Code`)映射到 `analysis` / `plan` / `code`,统计 `## 审查分歧账本` 中对应阶段 `status ∈ {needs-human-decision, human-decided}` 的当前累计数量;`Manual-verify`(`{e}`)解析 review done note 中的 `(+ {n} env-blocked)`(解析后从展示中去重),缺失为 `0`。非审查步骤不附加人工计数。
59
+ **人工计数**(`ai task log`):审查步骤行把两项人工计数并入 NOTE 的 verdict 文本,逗号分隔、紧随 `blockers/major/minor`,固定英文标签 `Manual-validation: {e}, Human-decision: {h}`。`Human-decision`(`{h}`)按 canonical 步骤名前缀(`Review Analysis` / `Review Plan` / `Review Code`)映射到 `analysis` / `plan` / `code`,统计 `## 审查分歧账本` 中对应阶段 `status ∈ {needs-human-decision, human-decided}` 的当前累计数量;`Manual-validation`(`{e}`)解析 review done note 中的 `Manual-validation: {n}` 源字段(展示时归一化为同一字段),缺失为 `0`。非审查步骤不附加人工计数。
60
60
 
61
61
  **gate**(`checkActivityLog`):计算「最新 action / freshness」时跳过 `[started]` 行(升序与格式校验仍覆盖全部行),故 started 标记不会污染各 SKILL 的 `expected_action_pattern`。
62
62
 
@@ -12,7 +12,7 @@ Implement the approved plan and produce `code.md` or `code-r{N}.md`. This skill
12
12
  ## Boundary / Critical Rules
13
13
 
14
14
  - Follow the latest plan artifact: `plan.md` or `plan-r{N}.md`
15
- - Fix mode verifies each finding of the latest `review-code` one by one: fix it if it holds, or rebut it and record it under unresolved if it is unfounded/hallucinated; do not expand to issues the review did not list; env-blocked items are out of scope
15
+ - Fix mode verifies each finding of the latest `review-code` one by one: fix it if it holds, or rebut it and record it under unresolved if it is unfounded/hallucinated; do not expand to issues the review did not list; manual-validation items are out of scope
16
16
  - If implementation encounters a key design decision not covered by the plan, per `.agents/rules/no-mid-flow-questions.md` write its detail block into the implementation report's `## 人工裁决待办` (Pending Human Decisions) section as `### HD-N:<title> [needs-human-decision]` (`HD-N` is globally unique, see `.agents/rules/review-handshake.md`) and write the matching `HD-` ledger row, instead of asking mid-flow or expanding scope silently
17
17
  - Never auto-run `git add` or `git commit`
18
18
  - Create a new code artifact for each round and never overwrite an older one
@@ -12,7 +12,7 @@ description: >
12
12
  ## 行为边界 / 关键规则
13
13
 
14
14
  - 严格遵循最新方案产物:`plan.md` 或 `plan-r{N}.md`
15
- - 修复模式逐条核实最新 `review-code` 的发现:成立则修复,判定为不成立/幻觉则在报告中反驳并记入 unresolved;不擅自扩大到审查未列出的问题;env-blocked 项不在修复范围
15
+ - 修复模式逐条核实最新 `review-code` 的发现:成立则修复,判定为不成立/幻觉则在报告中反驳并记入 unresolved;不擅自扩大到审查未列出的问题;manual-validation 项不在修复范围
16
16
  - 实现中遇到方案未覆盖的关键设计决策时,按 `.agents/rules/no-mid-flow-questions.md` 判据,把详情块写入实现报告的 `## 人工裁决待办` 段 `### HD-N:<标题> [needs-human-decision]`(`HD-N` 全局唯一,见 `.agents/rules/review-handshake.md`)并回写 `HD-` 账本行,不中途提问或擅自扩范围
17
17
  - 绝不自动执行 `git add` 或 `git commit`
18
18
  - 每轮实现都创建新的实现产物,不覆盖旧文件
@@ -168,7 +168,7 @@ date "+%Y-%m-%d %H:%M:%S%:z"
168
168
  - 记录 Round `{code-round}` 的 `{code-artifact}`
169
169
  - 追加:
170
170
  - 初次实现:`- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Code Task (Round {N})** by {agent} — Code implemented, {n} files modified, {n} tests passed → {code-artifact}`
171
- - 修复模式:`- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Code Task (Round {N}, fix for {review-artifact})** by {agent} — Fixed {n} blockers, {n} major, {n} minor issues[, skipped {n} env-blocked] → {code-artifact}`
171
+ - 修复模式:`- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Code Task (Round {N}, fix for {review-artifact})** by {agent} — Fixed {n} blockers, {n} major, {n} minor issues[, skipped {n} manual-validation] → {code-artifact}`
172
172
 
173
173
  如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续;执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测):
174
174
  - 按 issue-sync.md 设置 `status: in-progress`