@fitlab-ai/agent-infra 0.7.7 → 0.8.1

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 (142) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +42 -15
  4. package/dist/bin/cli.js +46 -16
  5. package/dist/lib/decide.js +110 -0
  6. package/dist/lib/run/host.js +39 -0
  7. package/dist/lib/run/index.js +193 -0
  8. package/dist/lib/run/prompt.js +2 -0
  9. package/dist/lib/run/skills.js +29 -0
  10. package/dist/lib/run/tui.js +35 -0
  11. package/dist/lib/sandbox/capture.js +158 -0
  12. package/dist/lib/sandbox/commands/create.js +58 -9
  13. package/dist/lib/sandbox/commands/enter.js +7 -3
  14. package/dist/lib/sandbox/credentials.js +42 -3
  15. package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
  16. package/dist/lib/server/adapters/_contract.js +9 -0
  17. package/dist/lib/server/adapters/feishu/index.js +40 -0
  18. package/dist/lib/server/adapters/feishu/transport.js +95 -0
  19. package/dist/lib/server/auth.js +19 -0
  20. package/dist/lib/server/config.js +183 -0
  21. package/dist/lib/server/daemon.js +96 -0
  22. package/dist/lib/server/index.js +47 -0
  23. package/dist/lib/server/logger.js +34 -0
  24. package/dist/lib/server/plugin-loader.js +73 -0
  25. package/dist/lib/server/process-control.js +200 -0
  26. package/dist/lib/server/protocol.js +69 -0
  27. package/dist/lib/server/redact.js +12 -0
  28. package/dist/lib/server/runner.js +75 -0
  29. package/dist/lib/server/streamer.js +44 -0
  30. package/dist/lib/task/commands/decisions.js +258 -0
  31. package/dist/lib/task/commands/log.js +38 -41
  32. package/dist/lib/task/commands/status.js +144 -45
  33. package/dist/lib/task/index.js +9 -0
  34. package/dist/lib/task/ledger.js +66 -0
  35. package/dist/lib/task/sections.js +30 -1
  36. package/lib/decide.ts +124 -0
  37. package/lib/run/host.ts +47 -0
  38. package/lib/run/index.ts +237 -0
  39. package/lib/run/prompt.ts +1 -0
  40. package/lib/run/skills.ts +34 -0
  41. package/lib/run/tui.ts +40 -0
  42. package/lib/sandbox/capture.ts +215 -0
  43. package/lib/sandbox/commands/create.ts +64 -12
  44. package/lib/sandbox/commands/enter.ts +8 -2
  45. package/lib/sandbox/credentials.ts +57 -3
  46. package/lib/sandbox/runtimes/base.dockerfile +13 -0
  47. package/lib/server/adapters/_contract.ts +42 -0
  48. package/lib/server/adapters/feishu/index.ts +47 -0
  49. package/lib/server/adapters/feishu/transport.ts +128 -0
  50. package/lib/server/auth.ts +28 -0
  51. package/lib/server/config.ts +228 -0
  52. package/lib/server/daemon.ts +110 -0
  53. package/lib/server/index.ts +50 -0
  54. package/lib/server/logger.ts +48 -0
  55. package/lib/server/plugin-loader.ts +84 -0
  56. package/lib/server/process-control.ts +225 -0
  57. package/lib/server/protocol.ts +79 -0
  58. package/lib/server/redact.ts +12 -0
  59. package/lib/server/runner.ts +95 -0
  60. package/lib/server/server.schema.json +127 -0
  61. package/lib/server/streamer.ts +57 -0
  62. package/lib/task/commands/decisions.ts +272 -0
  63. package/lib/task/commands/log.ts +38 -39
  64. package/lib/task/commands/status.ts +200 -58
  65. package/lib/task/index.ts +9 -0
  66. package/lib/task/ledger.ts +73 -0
  67. package/lib/task/sections.ts +30 -1
  68. package/package.json +8 -3
  69. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  70. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  71. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  72. package/templates/.agents/rules/next-step-output.en.md +5 -1
  73. package/templates/.agents/rules/next-step-output.zh-CN.md +5 -1
  74. package/templates/.agents/rules/no-mid-flow-questions.en.md +7 -2
  75. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +7 -2
  76. package/templates/.agents/rules/pr-sync.github.en.md +3 -3
  77. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
  78. package/templates/.agents/rules/review-handshake.en.md +5 -1
  79. package/templates/.agents/rules/review-handshake.zh-CN.md +5 -1
  80. package/templates/.agents/rules/task-management.en.md +2 -2
  81. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  82. package/templates/.agents/scripts/lib/post-review-commit.js +15 -18
  83. package/templates/.agents/scripts/validate-artifact.js +8 -0
  84. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -3
  85. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +15 -3
  86. package/templates/.agents/skills/analyze-task/config/verify.en.json +2 -1
  87. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +2 -1
  88. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  89. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  90. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  91. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  92. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
  93. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
  94. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -0
  95. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -0
  96. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -1
  97. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -1
  98. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  99. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  100. package/templates/.agents/skills/create-task/SKILL.en.md +1 -3
  101. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -3
  102. package/templates/.agents/skills/import-codescan/SKILL.en.md +0 -3
  103. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +0 -3
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +0 -1
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +0 -1
  106. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -2
  107. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -2
  108. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -2
  109. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -2
  110. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  111. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  112. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
  115. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
  116. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
  117. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
  118. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
  119. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
  120. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  121. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  122. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  123. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  124. package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
  125. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
  126. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
  127. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
  128. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
  129. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
  130. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  131. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  132. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  134. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
  135. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
  136. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
  137. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
  138. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
  139. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
  140. package/templates/.agents/templates/task.en.md +3 -3
  141. package/templates/.agents/templates/task.zh-CN.md +3 -3
  142. package/templates/.github/workflows/metadata-sync.yml +0 -18
@@ -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/task/index.ts CHANGED
@@ -2,6 +2,7 @@ const USAGE = `Usage: ai task <command> [options]
2
2
 
3
3
  Commands:
4
4
  cat <ref> <artifact | N> Print a task artifact (by name or number)
5
+ decisions, d <ref> [selector] List human-decision (HD-) items, or show one's detail
5
6
  files <ref> List artifacts in a task dir (numbered)
6
7
  grep <pattern> [ref] [artifact | N] Literal search across task artifacts (omit ref to scan all)
7
8
  issue-body <ref> [--template <path>] Print a deterministic Issue body from task.md (描述 + 需求, or an Issue Form)
@@ -13,6 +14,8 @@ Commands:
13
14
  Examples:
14
15
  ai task cat 11 analysis
15
16
  ai task cat 11 3
17
+ ai task decisions 11
18
+ ai task d 11 HD-3 --format markdown
16
19
  ai task files 11
17
20
  ai task grep resolveArtifact
18
21
  ai task grep resolveArtifact 11
@@ -46,6 +49,12 @@ export async function runTask(args: string[]): Promise<void> {
46
49
  cat(rest);
47
50
  break;
48
51
  }
52
+ case 'decisions':
53
+ case 'd': {
54
+ const { decisions } = await import('./commands/decisions.ts');
55
+ decisions(rest);
56
+ break;
57
+ }
49
58
  case 'files': {
50
59
  const { files } = await import('./commands/files.ts');
51
60
  files(rest);
@@ -0,0 +1,73 @@
1
+ // Shared parser for the task.md `## 审查分歧账本` (Review Disagreement Ledger).
2
+ // Single source of truth for ledger row parsing across `ai task` commands
3
+ // (log, decisions) — see the plan's D1 constraint "no third parser". The gate
4
+ // parser in `.agents/scripts/validate-artifact.js` is a separate concern and
5
+ // must be kept semantically in sync by hand (noted in review-handshake.md).
6
+
7
+ // The ledger H2 heading is language-dependent (zh template / en template).
8
+ const LEDGER_HEADING_RE = /^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/;
9
+ const NEXT_H2_RE = /^##\s/;
10
+
11
+ type LedgerRow = {
12
+ id: string;
13
+ stage: string;
14
+ round: string;
15
+ severity: string;
16
+ status: string;
17
+ evidence: string;
18
+ };
19
+
20
+ // Terminal statuses the completion gates treat as resolved.
21
+ const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
22
+ // Statuses that represent an executor-raised human-decision row (pending or done).
23
+ const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
24
+
25
+ // Parse all rows of the disagreement ledger table. Skips the heading, the
26
+ // header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
27
+ // Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
28
+ function parseLedger(content: string): LedgerRow[] {
29
+ const lines = content.split('\n');
30
+ let i = 0;
31
+ while (i < lines.length && !LEDGER_HEADING_RE.test(lines[i]!)) i += 1;
32
+ if (i >= lines.length) return [];
33
+
34
+ const rows: LedgerRow[] = [];
35
+ for (let j = i + 1; j < lines.length; j += 1) {
36
+ if (NEXT_H2_RE.test(lines[j]!)) break;
37
+ const line = lines[j]!.trim();
38
+ if (!line.startsWith('|')) continue;
39
+ const cells = line
40
+ .split('|')
41
+ .slice(1, -1)
42
+ .map((cell) => cell.trim());
43
+ if (cells.length < 6) continue;
44
+ if (cells[0] === 'id' || /^-+$/.test(cells[0] ?? '')) continue;
45
+ rows.push({
46
+ id: cells[0]!,
47
+ stage: cells[1]!,
48
+ round: cells[2]!,
49
+ severity: cells[3]!,
50
+ status: cells[4]!,
51
+ evidence: cells[5]!
52
+ });
53
+ }
54
+ return rows;
55
+ }
56
+
57
+ // Allocate the next globally-unique human-decision id. Scans every `HD-<n>`
58
+ // already present in the ledger (across all stages) and returns `HD-{max+1}`,
59
+ // or `HD-1` when none exist. Global monotonic allocation prevents id collisions
60
+ // across the analysis / plan / code stages (plan PL-2).
61
+ function nextHdId(rows: readonly LedgerRow[]): string {
62
+ let max = 0;
63
+ for (const row of rows) {
64
+ const m = /^HD-(\d+)$/.exec(row.id);
65
+ if (!m) continue;
66
+ const n = Number.parseInt(m[1]!, 10);
67
+ if (n > max) max = n;
68
+ }
69
+ return `HD-${max + 1}`;
70
+ }
71
+
72
+ export { parseLedger, nextHdId, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
73
+ export type { LedgerRow };
@@ -41,4 +41,33 @@ function findSectionHeading(content: string, aliases: string[]): string {
41
41
  return aliases[0]!;
42
42
  }
43
43
 
44
- export { extractSection, findSectionHeading };
44
+ /**
45
+ * Return the body of the first `### {headingPrefix}` sub-section, from the
46
+ * heading line (inclusive) to the next `### ` / `## ` heading or EOF. Used to
47
+ * pull a single `### HD-N` human-decision detail block out of an artifact. The
48
+ * prefix must be followed by a word boundary so `HD-1` does not match `HD-10`
49
+ * (e.g. `### HD-1`, `### HD-1:标题`, `### HD-1 [needs-human-decision]`). Leading
50
+ * and trailing blank lines are trimmed. Returns '' when no match is present.
51
+ */
52
+ function extractSubSection(content: string, headingPrefix: string): string {
53
+ const lines = content.split('\n');
54
+ const headRe = new RegExp(`^###\\s+${escapeRegExp(headingPrefix)}(?![\\w-])`);
55
+ let start = -1;
56
+ for (let i = 0; i < lines.length; i += 1) {
57
+ if (headRe.test(lines[i]!.trim())) {
58
+ start = i;
59
+ break;
60
+ }
61
+ }
62
+ if (start === -1) return '';
63
+ let end = lines.length;
64
+ for (let i = start + 1; i < lines.length; i += 1) {
65
+ if (/^###?\s+/.test(lines[i]!)) {
66
+ end = i;
67
+ break;
68
+ }
69
+ }
70
+ return lines.slice(start, end).join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
71
+ }
72
+
73
+ export { extractSection, findSectionHeading, extractSubSection };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
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",
@@ -43,7 +43,8 @@
43
43
  "installer"
44
44
  ],
45
45
  "dependencies": {
46
- "@clack/prompts": "1.5.1",
46
+ "@clack/prompts": "1.7.0",
47
+ "@larksuiteoapi/node-sdk": "^1.68.0",
47
48
  "cross-spawn": "^7.0.6",
48
49
  "picocolors": "1.1.1",
49
50
  "semver": "^7.8.1",
@@ -57,17 +58,21 @@
57
58
  "typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
58
59
  "test:smoke": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/unit/**/*.test.ts\"",
59
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",
60
62
  "test": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\"",
61
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\"",
62
64
  "prepublishOnly": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\""
63
65
  },
64
66
  "devDependencies": {
65
67
  "@types/cross-spawn": "^6.0.6",
66
- "@types/node": "^25.9.1",
68
+ "@types/node": "^22.20.0",
67
69
  "@types/semver": "^7.7.1",
68
70
  "typescript": "~6.0"
69
71
  },
70
72
  "optionalDependencies": {
71
73
  "@lydell/node-pty": "^1.2.0-beta.12"
74
+ },
75
+ "overrides": {
76
+ "axios": "1.18.1"
72
77
  }
73
78
  }
@@ -47,8 +47,8 @@ cp .agents/templates/task.md .agents/workspace/active/task-001.md
47
47
 
48
48
  ```yaml
49
49
  id: task-001
50
- type: feature # feature | bugfix | refactor | docs | review
51
- status: open # open | in-progress | review | blocked | completed
50
+ type: feature # feature | bugfix | refactor | docs | chore
51
+ status: active # active | blocked | completed
52
52
  assigned_to: claude # claude | codex | gemini | opencode | human
53
53
  ```
54
54
 
@@ -29,7 +29,7 @@ Localized option input may be normalized before writing:
29
29
  | `中` | `Medium` |
30
30
  | `低` | `Low` |
31
31
 
32
- AI agents may infer `priority` and `effort` from the title and description when creating or refining tasks, but must keep date fields empty unless the user or source Issue provides explicit dates. Human edits in `task.md` take precedence.
32
+ AI agents may infer `priority` and `effort` from the title and description when creating or refining tasks. Date fields are factual, not estimated: `start_date` is written by the analyze stage (the analysis start date) and `target_date` by the complete stage (the completion date). Human edits in `task.md` take precedence.
33
33
 
34
34
  ## GraphQL Reference
35
35
 
@@ -29,7 +29,7 @@
29
29
  | `中` | `Medium` |
30
30
  | `低` | `Low` |
31
31
 
32
- AI agent 在创建或修订任务时可根据标题与描述推断 `priority` 和 `effort`,但除非用户或来源 Issue 明确提供日期,否则必须保持日期字段为空。`task.md` 中人工填写的值优先。
32
+ AI agent 在创建或修订任务时可根据标题与描述推断 `priority` 和 `effort`。日期字段为事实值、不估算:`start_date` analyze 阶段写入(= 分析开始日),`target_date` 由 complete 阶段写入(= 完成日)。`task.md` 中人工填写的值优先。
33
33
 
34
34
  ## GraphQL 参考
35
35
 
@@ -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)
@@ -77,6 +77,10 @@ This section is a **third standalone rule, co-equal with the two above**, used o
77
77
  Location: the matching row in task.md `## 审查分歧账本` · Evidence: {evidence}
78
78
  …(one entry per status=needs-human-decision row of this stage in task.md `## 审查分歧账本`)
79
79
 
80
+ View details:
81
+ - All pending decisions: ai task decisions {task-ref}
82
+ - A single item's full background/options/impact/recommendation: ai task decisions {task-ref} <ordinal|HD-id>
83
+
80
84
  To resolve:
81
85
  1. In the task.md `## 人工裁决` section, record your ruling and rationale for each item above.
82
86
  2. Flip the status of the matching row in `## 审查分歧账本` from `needs-human-decision` to `human-decided`.
@@ -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 时)
@@ -77,6 +77,10 @@ Completed at: YYYY-MM-DD HH:mm:ss
77
77
  位置:task.md `## 审查分歧账本` 对应行 · 证据:{evidence}
78
78
  …(task.md `## 审查分歧账本` 中本阶段每个 status=needs-human-decision 行一条)
79
79
 
80
+ 查看详情:
81
+ - 全部待裁决项:ai task decisions {task-ref}
82
+ - 单项完整背景/选项/影响/建议:ai task decisions {task-ref} <序号|HD-id>
83
+
80
84
  完成裁决:
81
85
  1. 在 task.md `## 人工裁决` 段,逐项记录你对上述裁决项的裁定与理由。
82
86
  2. 把 `## 审查分歧账本` 中对应行的 status 由 `needs-human-decision` 翻为 `human-decided`。
@@ -55,7 +55,10 @@ For every SKILL execution context not covered by any exemption above, the defaul
55
55
 
56
56
  ## Key Design Decision Marking And Ledgering
57
57
 
58
- When an open question is a key design decision that needs human judgment, the executor must mark the item with `[needs-human-decision]` and write the matching `HD-` row to task.md `## Review Disagreement Ledger` according to `.agents/rules/review-handshake.md`.
58
+ When an open question is a key design decision that needs human judgment, the executor must:
59
+
60
+ 1. Write the decision's **detail block** (background / options / impact / recommendation) into the artifact's `## 人工裁决待办` (Pending Human Decisions) section, with a heading like `### HD-N:<title> [needs-human-decision]`; ordinary assumptions / open questions still go under `## Assumptions` / `## Open Questions` (`## 假设` / `## 未决问题`), and `## Open Questions` may keep a one-line pointer to the matching `### HD-N`.
61
+ 2. Upsert the matching `HD-` row in task.md `## Review Disagreement Ledger` according to `.agents/rules/review-handshake.md`, with `evidence` pointing to the stable anchor `<artifact>#HD-N`. The `HD-N` number is **globally unique** (scan existing `HD-(\d+)` in the ledger and take max+1, monotonically increasing across analysis / plan / code, never reused); see review-handshake.md for the allocation rule.
59
62
 
60
63
  Use these checks together:
61
64
 
@@ -64,6 +67,8 @@ Use these checks together:
64
67
  - **Small-impact exemption**: if it is only a local, reversible, low-cost execution detail, record it under `## Assumptions` instead of upgrading it to a human ruling.
65
68
  - **Fallback**: when unsure whether it is key, treat it as key; `review-*` must check whether the executor missed any `[needs-human-decision]` markings that should have been upgraded.
66
69
 
70
+ > Use `ai task decisions <task-ref>` to view all pending decisions of a task and their detail blocks.
71
+
67
72
  ## Human Review Checkpoint Semantics
68
73
 
69
74
  A mandatory human review checkpoint means:
@@ -71,7 +76,7 @@ A mandatory human review checkpoint means:
71
76
  - Stop after producing the artifact: once the skill finishes an artifact such as `plan.md`, end the current invocation and wait for the user to explicitly trigger the next skill command
72
77
  - Do not pause mid-process to ask for input: do not insert interruptions such as "Do you prefer option A or B?" between execution steps
73
78
 
74
- If a key decision needs human judgment during execution, follow the assumptions and open questions rule above: record it in the artifact's "Open Questions" / `未决问题` section for the user to address at the review checkpoint.
79
+ If a key decision needs human judgment during execution, follow the "Key Design Decision Marking And Ledgering" rule above: write the detail block into the artifact's `## 人工裁决待办` (Pending Human Decisions) section as `### HD-N` and upsert the `HD-` ledger row, for the user to address at the review checkpoint; ordinary open questions still go to `## Open Questions` / `未决问题`.
75
80
 
76
81
  ## Anchor Location
77
82
 
@@ -55,7 +55,10 @@
55
55
 
56
56
  ## 关键设计决策标记与落账
57
57
 
58
- 当未决问题属于需要人工裁定的关键设计决策时,执行方必须在该条目前标记 `[needs-human-decision]`,并按 `.agents/rules/review-handshake.md` 在 task.md `## 审查分歧账本` 写入 `HD-` 行。
58
+ 当未决问题属于需要人工裁定的关键设计决策时,执行方必须:
59
+
60
+ 1. 把该决策的**详情块**(背景 / 选项 / 影响 / 推荐)写入产物的 `## 人工裁决待办` 段,标题形如 `### HD-N:<标题> [needs-human-decision]`;普通假设 / 未决问题仍分别写 `## 假设` / `## 未决问题`(`## Assumptions` / `## Open Questions`),`## 未决问题` 可保留一行指针指向对应 `### HD-N`。
61
+ 2. 按 `.agents/rules/review-handshake.md` 在 task.md `## 审查分歧账本` upsert 对应 `HD-` 行,evidence 指向稳定锚点 `<artifact>#HD-N`。`HD-N` 编号**全局唯一**(扫描账本已有 `HD-(\d+)` 取 max+1,跨 analysis / plan / code 单调递增、禁止复用),分配规则详见 review-handshake.md。
59
62
 
60
63
  判定时同时使用以下检查:
61
64
 
@@ -64,6 +67,8 @@
64
67
  - **小影响豁免**:若它只是局部、可逆、低成本的执行细节,写入 `## 假设` 即可,不升级为人工裁决。
65
68
  - **兜底**:无法判断是否关键时按关键处理;`review-*` 需要复核执行方是否漏标应升级的 `[needs-human-decision]`。
66
69
 
70
+ > 用户可用 `ai task decisions <task-ref>` 查看某任务全部待裁决项及其详情块。
71
+
67
72
  ## 人工审查检查点语义
68
73
 
69
74
  「强制性人工审查检查点」(mandatory human review checkpoint)的语义是:
@@ -71,7 +76,7 @@
71
76
  - **产出后停止**:技能完成产物(如 `plan.md`)后立即结束本轮调用,等待用户主动触发下一个技能命令
72
77
  - **不是过程中暂停征求意见**:不允许在执行步骤之间插入「请问您倾向 A 还是 B?」之类的中断
73
78
 
74
- 如果在执行过程中发现需要用户裁定的关键决策,按上文「假设与未决问题」处理,记录到产物的「未决问题」/`Open Questions` 段落,由用户在审查检查点统一回应。
79
+ 如果在执行过程中发现需要用户裁定的关键决策,按上文「关键设计决策标记与落账」处理:详情块写入产物 `## 人工裁决待办` 的 `### HD-N`、回写 `HD-` 账本行,由用户在审查检查点统一回应;普通未决问题仍写 `## 未决问题` / `Open Questions`。
75
80
 
76
81
  ## 锚点位置
77
82