@fitlab-ai/agent-infra 0.8.2 → 0.8.4

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 (198) hide show
  1. package/README.zh-CN.md +10 -0
  2. package/bin/cli.ts +1 -1
  3. package/dist/bin/cli.js +2 -2
  4. package/dist/lib/cp.js +57 -16
  5. package/dist/lib/decide.js +2 -2
  6. package/dist/lib/defaults.json +2 -0
  7. package/dist/lib/init.js +7 -7
  8. package/dist/lib/merge.js +1 -1
  9. package/dist/lib/prompt.js +1 -1
  10. package/dist/lib/run/index.js +7 -7
  11. package/dist/lib/run/prompt.js +1 -1
  12. package/dist/lib/sandbox/capture.js +10 -8
  13. package/dist/lib/sandbox/clipboard/bridge.js +59 -21
  14. package/dist/lib/sandbox/clipboard/inbox.js +82 -0
  15. package/dist/lib/sandbox/clipboard/index.js +5 -6
  16. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  17. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  18. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  19. package/dist/lib/sandbox/commands/create.js +106 -88
  20. package/dist/lib/sandbox/commands/enter.js +9 -9
  21. package/dist/lib/sandbox/commands/list-running.js +2 -2
  22. package/dist/lib/sandbox/commands/ls.js +7 -7
  23. package/dist/lib/sandbox/commands/prune.js +8 -8
  24. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  25. package/dist/lib/sandbox/commands/refresh.js +2 -2
  26. package/dist/lib/sandbox/commands/rm.js +11 -11
  27. package/dist/lib/sandbox/commands/show.js +4 -4
  28. package/dist/lib/sandbox/commands/start.js +4 -4
  29. package/dist/lib/sandbox/commands/vm.js +4 -4
  30. package/dist/lib/sandbox/config.js +10 -4
  31. package/dist/lib/sandbox/constants.js +4 -1
  32. package/dist/lib/sandbox/credentials.js +1 -1
  33. package/dist/lib/sandbox/dotfiles.js +1 -1
  34. package/dist/lib/sandbox/engine.js +3 -3
  35. package/dist/lib/sandbox/engines/index.js +5 -5
  36. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  37. package/dist/lib/sandbox/image-build.js +80 -0
  38. package/dist/lib/sandbox/image-prune.js +2 -2
  39. package/dist/lib/sandbox/index.js +10 -10
  40. package/dist/lib/sandbox/managed-fs.js +1 -1
  41. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  42. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
  43. package/dist/lib/sandbox/shell.js +9 -1
  44. package/dist/lib/sandbox/tools.js +4 -4
  45. package/dist/lib/server/adapters/feishu/index.js +10 -3
  46. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  47. package/dist/lib/server/adapters/feishu/transport.js +2 -13
  48. package/dist/lib/server/daemon.js +64 -43
  49. package/dist/lib/server/display.js +83 -0
  50. package/dist/lib/server/index.js +2 -2
  51. package/dist/lib/server/process-control.js +2 -2
  52. package/dist/lib/server/protocol.js +2 -2
  53. package/dist/lib/server/streamer.js +5 -4
  54. package/dist/lib/task/commands/cat.js +2 -2
  55. package/dist/lib/task/commands/decisions.js +4 -4
  56. package/dist/lib/task/commands/files.js +3 -3
  57. package/dist/lib/task/commands/grep.js +3 -3
  58. package/dist/lib/task/commands/issue-body.js +4 -4
  59. package/dist/lib/task/commands/log.js +5 -5
  60. package/dist/lib/task/commands/ls.js +3 -3
  61. package/dist/lib/task/commands/show.js +1 -1
  62. package/dist/lib/task/commands/status.js +71 -25
  63. package/dist/lib/task/index.js +9 -9
  64. package/dist/lib/task/resolve-ref.js +1 -1
  65. package/dist/lib/task/workflow-warnings.js +94 -0
  66. package/dist/lib/update.js +4 -4
  67. package/lib/cp.ts +56 -15
  68. package/lib/defaults.json +2 -0
  69. package/lib/sandbox/capture.ts +5 -3
  70. package/lib/sandbox/clipboard/bridge.ts +62 -18
  71. package/lib/sandbox/clipboard/inbox.ts +83 -0
  72. package/lib/sandbox/clipboard/index.ts +5 -6
  73. package/lib/sandbox/clipboard/linux.ts +124 -0
  74. package/lib/sandbox/clipboard/win32.ts +173 -0
  75. package/lib/sandbox/commands/create.ts +132 -88
  76. package/lib/sandbox/commands/rebuild.ts +42 -54
  77. package/lib/sandbox/config.ts +13 -1
  78. package/lib/sandbox/constants.ts +4 -0
  79. package/lib/sandbox/image-build.ts +134 -0
  80. package/lib/sandbox/runtimes/base.dockerfile +3 -0
  81. package/lib/sandbox/shell.ts +11 -2
  82. package/lib/sandbox/tools.ts +5 -5
  83. package/lib/server/adapters/_contract.ts +3 -0
  84. package/lib/server/adapters/feishu/index.ts +11 -3
  85. package/lib/server/adapters/feishu/renderer.ts +99 -0
  86. package/lib/server/adapters/feishu/transport.ts +5 -18
  87. package/lib/server/daemon.ts +74 -38
  88. package/lib/server/display.ts +136 -0
  89. package/lib/server/streamer.ts +5 -4
  90. package/lib/task/commands/log.ts +3 -3
  91. package/lib/task/commands/status.ts +102 -21
  92. package/lib/task/workflow-warnings.ts +121 -0
  93. package/package.json +2 -2
  94. package/templates/.agents/README.en.md +3 -0
  95. package/templates/.agents/README.zh-CN.md +3 -0
  96. package/templates/.agents/rules/create-issue.github.en.md +4 -4
  97. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -4
  98. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  99. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  100. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  101. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  102. package/templates/.agents/rules/next-step-output.en.md +15 -1
  103. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  104. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  105. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  106. package/templates/.agents/rules/review-handshake.en.md +2 -2
  107. package/templates/.agents/rules/review-handshake.zh-CN.md +2 -2
  108. package/templates/.agents/rules/task-management.en.md +1 -1
  109. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  110. package/templates/.agents/rules/testing-discipline.en.md +3 -41
  111. package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
  112. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  113. package/templates/.agents/scripts/validate-artifact.js +213 -0
  114. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  115. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  116. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
  118. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  119. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  120. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  121. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  122. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
  123. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  124. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  125. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  127. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
  130. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
  131. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
  132. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  133. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  134. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  135. package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
  136. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
  137. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +142 -0
  138. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +142 -0
  139. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  140. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  141. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  142. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  143. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  144. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  145. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  148. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  149. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  150. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  151. package/templates/.agents/skills/create-task/SKILL.en.md +6 -3
  152. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -6
  153. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
  155. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  157. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
  158. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  159. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  160. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -2
  161. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
  162. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
  163. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  164. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  165. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  166. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  167. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  168. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  169. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  170. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  171. package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
  172. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
  173. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  174. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  175. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
  176. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
  177. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -2
  178. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -2
  179. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  180. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  181. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  182. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  183. package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
  184. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  185. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +36 -6
  186. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  187. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  188. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
  190. package/templates/.agents/templates/task.en.md +7 -0
  191. package/templates/.agents/templates/task.zh-CN.md +7 -0
  192. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  193. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  194. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  195. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  196. package/templates/.github/workflows/metadata-sync.yml +5 -0
  197. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  198. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -1,87 +1,123 @@
1
1
  import { VERSION } from '../version.ts';
2
2
  import { loadServerConfig } from './config.ts';
3
+ import type { ServerConfig } from './config.ts';
3
4
  import { createLogger } from './logger.ts';
5
+ import type { Logger } from './logger.ts';
4
6
  import { loadAdapters, unloadAdapters } from './plugin-loader.ts';
5
7
  import type { InboundMessage } from './adapters/_contract.ts';
6
8
  import { authorize } from './auth.ts';
7
9
  import { commandHelp, parseCommand } from './protocol.ts';
8
10
  import { runAi } from './runner.ts';
11
+ import type { RunnerOptions, RunnerResult } from './runner.ts';
9
12
  import { streamCommand } from './streamer.ts';
13
+ import { markdownMessage, replyOutbound, textMessage } from './display.ts';
14
+ import { buildStatusModel, statusModelToDisplay, type StatusModel } from '../task/commands/status.ts';
10
15
 
11
16
  function errorMessage(error: unknown): string {
12
17
  return error instanceof Error ? error.message : String(error);
13
18
  }
14
19
 
15
- // The daemon main loop. Runs in the detached child spawned by
16
- // process-control.start(), or in the foreground for debugging.
17
- //
18
- // Lifecycle (keep-alive / shutdown model):
19
- // - The heartbeat interval is kept *ref'd*. It is both the keep-alive that
20
- // holds the event loop open while subtask A has no adapters, and the
21
- // observable signal that `ai server logs -f` shows.
22
- // - runDaemon() awaits a shutdown promise that only resolves once a
23
- // SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
24
- // the only keep-alive timer (that would let the process exit immediately).
25
- export async function runDaemon(): Promise<void> {
26
- let config;
27
- try {
28
- config = loadServerConfig();
29
- } catch (error) {
30
- process.stderr.write(`${errorMessage(error)}\n`);
31
- process.exit(1);
32
- }
20
+ type MessageDispatcherOptions = {
21
+ config: ServerConfig;
22
+ logger: Pick<Logger, 'info'>;
23
+ runAi?: (args: string[], options?: RunnerOptions) => Promise<RunnerResult>;
24
+ buildStatusModel?: (ref: string) => StatusModel;
25
+ statusModelToDisplay?: (model: StatusModel) => ReturnType<typeof statusModelToDisplay>;
26
+ };
33
27
 
34
- const logger = createLogger(config.log);
35
- logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
28
+ function isTaskStatus(argv: string[]): boolean {
29
+ return argv[0] === 'task' && argv[1] === 'status' && typeof argv[2] === 'string';
30
+ }
36
31
 
37
- const abortController = new AbortController();
38
- let resolveShutdown: () => void = () => {};
39
- const shutdown = new Promise<void>((resolve) => {
40
- resolveShutdown = resolve;
41
- });
32
+ export function createMessageDispatcher(options: MessageDispatcherOptions): (message: InboundMessage) => Promise<void> {
33
+ const runAiImpl = options.runAi ?? runAi;
34
+ const buildStatusModelImpl = options.buildStatusModel ?? buildStatusModel;
35
+ const statusModelToDisplayImpl = options.statusModelToDisplay ?? statusModelToDisplay;
42
36
 
43
- const dispatch = async (message: InboundMessage): Promise<void> => {
37
+ return async (message: InboundMessage): Promise<void> => {
44
38
  const plan = parseCommand(message.text);
45
39
  if (plan.kind === 'ignore') return;
46
40
  if (plan.kind === 'error') {
47
- await message.reply(plan.message);
48
- logger.info(`command rejected from ${message.adapter}:${message.userId}: ${plan.message}`);
41
+ await replyOutbound(message, textMessage(plan.message));
42
+ options.logger.info(`command rejected from ${message.adapter}:${message.userId}: ${plan.message}`);
49
43
  return;
50
44
  }
51
45
  if (plan.kind === 'builtin' && plan.name === 'ping') {
52
- await message.reply(`pong ${VERSION}`);
46
+ await replyOutbound(message, textMessage(`pong ${VERSION}`));
53
47
  return;
54
48
  }
55
49
  if (plan.kind === 'builtin' && plan.name === 'help') {
56
- await message.reply(commandHelp());
50
+ await replyOutbound(message, markdownMessage(commandHelp()));
57
51
  return;
58
52
  }
59
53
  if (plan.kind === 'builtin' && plan.name === 'version') {
60
- await message.reply(`agent-infra ${VERSION}`);
54
+ await replyOutbound(message, textMessage(`agent-infra ${VERSION}`));
61
55
  return;
62
56
  }
63
57
  if (plan.kind === 'ai') {
64
58
  const allowed = authorize(
65
59
  { adapter: message.adapter, userId: message.userId },
66
60
  plan.role,
67
- config.auth
61
+ options.config.auth
68
62
  );
69
63
  if (!allowed.ok) {
70
- await message.reply(allowed.message);
71
- logger.info(`unauthorized command from ${message.adapter}:${message.userId}: ${allowed.message}`);
64
+ await replyOutbound(message, textMessage(allowed.message));
65
+ options.logger.info(`unauthorized command from ${message.adapter}:${message.userId}: ${allowed.message}`);
72
66
  return;
73
67
  }
68
+
69
+ if (isTaskStatus(plan.argv)) {
70
+ try {
71
+ await replyOutbound(message, statusModelToDisplayImpl(buildStatusModelImpl(plan.argv[2]!)));
72
+ return;
73
+ } catch {
74
+ // Fall back to the existing CLI streaming path. This preserves the
75
+ // old behavior for invalid refs and any status-model collection error.
76
+ }
77
+ }
78
+
74
79
  await streamCommand(
75
80
  {
76
81
  title: `ai ${plan.argv.join(' ')}`,
77
- chunkChars: typeof config.stream?.chunkChars === 'number' ? config.stream.chunkChars : 4000,
78
- throttleMs: typeof config.stream?.throttleMs === 'number' ? config.stream.throttleMs : 1500
82
+ chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
83
+ throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
79
84
  },
80
- (emit) => runAi(plan.argv, { onChunk: emit }),
81
- (text) => message.reply(text)
85
+ (emit) => runAiImpl(plan.argv, { onChunk: emit }),
86
+ (outbound) => replyOutbound(message, outbound)
82
87
  );
83
88
  }
84
89
  };
90
+ }
91
+
92
+ // The daemon main loop. Runs in the detached child spawned by
93
+ // process-control.start(), or in the foreground for debugging.
94
+ //
95
+ // Lifecycle (keep-alive / shutdown model):
96
+ // - The heartbeat interval is kept *ref'd*. It is both the keep-alive that
97
+ // holds the event loop open while subtask A has no adapters, and the
98
+ // observable signal that `ai server logs -f` shows.
99
+ // - runDaemon() awaits a shutdown promise that only resolves once a
100
+ // SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
101
+ // the only keep-alive timer (that would let the process exit immediately).
102
+ export async function runDaemon(): Promise<void> {
103
+ let config;
104
+ try {
105
+ config = loadServerConfig();
106
+ } catch (error) {
107
+ process.stderr.write(`${errorMessage(error)}\n`);
108
+ process.exit(1);
109
+ }
110
+
111
+ const logger = createLogger(config.log);
112
+ logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
113
+
114
+ const abortController = new AbortController();
115
+ let resolveShutdown: () => void = () => {};
116
+ const shutdown = new Promise<void>((resolve) => {
117
+ resolveShutdown = resolve;
118
+ });
119
+
120
+ const dispatch = createMessageDispatcher({ config, logger });
85
121
 
86
122
  const ctx = { config, logger, dispatch, signal: abortController.signal };
87
123
  const adapters = await loadAdapters(config, ctx);
@@ -0,0 +1,136 @@
1
+ export type OutboundMessage = string | DisplayMessage;
2
+
3
+ export type DisplayTone = 'info' | 'success' | 'warning' | 'danger' | 'running';
4
+
5
+ export type DisplayMessage =
6
+ | { kind: 'text'; text: string }
7
+ | { kind: 'markdown'; markdown: string }
8
+ | { kind: 'table'; title?: string; columns: string[]; rows: string[][] }
9
+ | { kind: 'status-card'; title: string; tone: DisplayTone; fields?: [string, string][]; body?: string }
10
+ | {
11
+ kind: 'stream-event';
12
+ title: string;
13
+ phase: 'started' | 'chunk' | 'finished';
14
+ text?: string;
15
+ exitCode?: number | null;
16
+ signal?: NodeJS.Signals | null;
17
+ }
18
+ | {
19
+ kind: 'command-result';
20
+ title: string;
21
+ exitCode: number | null;
22
+ signal: NodeJS.Signals | null;
23
+ stdout?: string;
24
+ stderr?: string;
25
+ };
26
+
27
+ export function textMessage(text: string): DisplayMessage {
28
+ return { kind: 'text', text };
29
+ }
30
+
31
+ export function markdownMessage(markdown: string): DisplayMessage {
32
+ return { kind: 'markdown', markdown };
33
+ }
34
+
35
+ export function tableMessage(columns: string[], rows: string[][], title?: string): DisplayMessage {
36
+ return { kind: 'table', title, columns, rows };
37
+ }
38
+
39
+ export function statusCard(
40
+ title: string,
41
+ tone: DisplayTone,
42
+ fields?: [string, string][],
43
+ body?: string
44
+ ): DisplayMessage {
45
+ return { kind: 'status-card', title, tone, fields, body };
46
+ }
47
+
48
+ export function streamEvent(
49
+ title: string,
50
+ phase: 'started' | 'chunk' | 'finished',
51
+ text?: string,
52
+ exitCode?: number | null,
53
+ signal?: NodeJS.Signals | null
54
+ ): DisplayMessage {
55
+ const message: Extract<DisplayMessage, { kind: 'stream-event' }> = { kind: 'stream-event', title, phase };
56
+ if (text !== undefined) message.text = text;
57
+ if (exitCode !== undefined) message.exitCode = exitCode;
58
+ if (signal !== undefined) message.signal = signal;
59
+ return message;
60
+ }
61
+
62
+ export function commandResult(
63
+ title: string,
64
+ exitCode: number | null,
65
+ signal: NodeJS.Signals | null,
66
+ stdout?: string,
67
+ stderr?: string
68
+ ): DisplayMessage {
69
+ return { kind: 'command-result', title, exitCode, signal, stdout, stderr };
70
+ }
71
+
72
+ export function normalizeOutbound(message: OutboundMessage): DisplayMessage {
73
+ return typeof message === 'string' ? textMessage(message) : message;
74
+ }
75
+
76
+ function tableToText(message: Extract<DisplayMessage, { kind: 'table' }>): string {
77
+ const lines: string[] = [];
78
+ if (message.title) lines.push(message.title);
79
+ lines.push(message.columns.join(' | '));
80
+ lines.push(message.columns.map(() => '---').join(' | '));
81
+ for (const row of message.rows) {
82
+ lines.push(row.join(' | '));
83
+ }
84
+ return lines.join('\n');
85
+ }
86
+
87
+ function compact(lines: Array<string | undefined>): string {
88
+ return lines.filter((line): line is string => typeof line === 'string' && line !== '').join('\n');
89
+ }
90
+
91
+ function unknownDisplayKind(message: never): string {
92
+ return `[unknown: ${String((message as { kind?: unknown }).kind)}]`;
93
+ }
94
+
95
+ export function outboundToText(message: OutboundMessage): string {
96
+ const normalized = normalizeOutbound(message);
97
+ switch (normalized.kind) {
98
+ case 'text':
99
+ return normalized.text;
100
+ case 'markdown':
101
+ return normalized.markdown;
102
+ case 'table':
103
+ return tableToText(normalized);
104
+ case 'status-card':
105
+ return compact([
106
+ normalized.title,
107
+ ...(normalized.fields ?? []).map(([label, value]) => `${label}: ${value}`),
108
+ normalized.body
109
+ ]);
110
+ case 'stream-event':
111
+ if (normalized.phase === 'started') return `started ${normalized.title}`;
112
+ if (normalized.phase === 'chunk') return normalized.text ?? '';
113
+ return `finished ${normalized.title} exitCode=${normalized.exitCode ?? 'null'} signal=${normalized.signal ?? 'null'}`;
114
+ case 'command-result':
115
+ return compact([
116
+ `finished ${normalized.title} exitCode=${normalized.exitCode ?? 'null'} signal=${normalized.signal ?? 'null'}`,
117
+ normalized.stdout,
118
+ normalized.stderr
119
+ ]);
120
+ }
121
+ return unknownDisplayKind(normalized);
122
+ }
123
+
124
+ export async function replyOutbound(
125
+ inbound: {
126
+ reply: (text: string) => Promise<void>;
127
+ replyDisplay?: (message: OutboundMessage) => Promise<void>;
128
+ },
129
+ message: OutboundMessage
130
+ ): Promise<void> {
131
+ if (inbound.replyDisplay) {
132
+ await inbound.replyDisplay(message);
133
+ return;
134
+ }
135
+ await inbound.reply(outboundToText(message));
136
+ }
@@ -1,3 +1,4 @@
1
+ import { streamEvent, type OutboundMessage } from './display.ts';
1
2
  import { redactSecrets } from './redact.ts';
2
3
  import type { RunnerResult } from './runner.ts';
3
4
 
@@ -19,9 +20,9 @@ function chunks(text: string, size: number): string[] {
19
20
  export async function streamCommand(
20
21
  options: StreamOptions,
21
22
  run: (emit?: (chunk: string) => Promise<void>) => Promise<RunnerResult>,
22
- send: (text: string) => Promise<void>
23
+ send: (message: OutboundMessage) => Promise<void>
23
24
  ): Promise<RunnerResult> {
24
- await send(`started ${options.title}`);
25
+ await send(streamEvent(options.title, 'started'));
25
26
  const size = options.chunkChars ?? 4000;
26
27
  const throttleMs = options.throttleMs ?? 0;
27
28
  let streamed = false;
@@ -34,7 +35,7 @@ export async function streamCommand(
34
35
  buffer = '';
35
36
  lastFlush = Date.now();
36
37
  for (const chunk of chunks(text, size)) {
37
- await send(chunk);
38
+ await send(streamEvent(options.title, 'chunk', chunk));
38
39
  }
39
40
  };
40
41
 
@@ -52,6 +53,6 @@ export async function streamCommand(
52
53
  buffer += [result.stdout, result.stderr].filter(Boolean).join('\n');
53
54
  }
54
55
  await flush();
55
- await send(`finished ${options.title} exitCode=${result.exitCode} signal=${result.signal ?? 'null'}`);
56
+ await send(streamEvent(options.title, 'finished', undefined, result.exitCode, result.signal));
56
57
  return result;
57
58
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { formatTable } from '../../table.ts';
3
3
  import { resolveTaskRef } from '../resolve-ref.ts';
4
- import { parseLedger, HUMAN_DECISION_STATUSES, type LedgerRow } from '../ledger.ts';
4
+ import { parseLedger, type LedgerRow } from '../ledger.ts';
5
5
 
6
6
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
7
7
 
@@ -14,7 +14,7 @@ Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
14
14
  A human-executed step shows AGENT as 'human' and, when it has no start marker,
15
15
  a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
16
16
  the verdict list, right after blockers/major/minor: manual-validation
17
- and human-decision (current ledger stage total).
17
+ and human-decision (current pending ledger stage total).
18
18
  `;
19
19
 
20
20
  const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'] as const;
@@ -108,7 +108,7 @@ function pairEntries(entries: LogEntry[]): StepRow[] {
108
108
  function countHumanDecisionsByStage(rows: LedgerRow[]): Map<ReviewStage, number> {
109
109
  const counts = new Map<ReviewStage, number>();
110
110
  for (const row of rows) {
111
- if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status)) continue;
111
+ if (!isReviewStage(row.stage) || row.status !== 'needs-human-decision') continue;
112
112
  counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
113
113
  }
114
114
  return counts;
@@ -6,7 +6,9 @@ import { resolveTaskRef } from '../resolve-ref.ts';
6
6
  import { enumerateArtifacts, type Artifact } from '../artifacts.ts';
7
7
  import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontmatter.ts';
8
8
  import { loadShortIdByTaskId } from '../short-id.ts';
9
+ import { getOpenWorkflowWarnings, formatWorkflowWarningSummary, type WorkflowWarning } from '../workflow-warnings.ts';
9
10
  import { parseActivityLog, pairEntries } from './log.ts';
11
+ import { statusCard, type DisplayMessage } from '../../server/display.ts';
10
12
 
11
13
  const USAGE = `Usage: ai task status <N | #N | TASK-id>
12
14
 
@@ -322,12 +324,28 @@ type StatusModel = {
322
324
  shortId: string;
323
325
  title: string;
324
326
  metadata: [string, string][];
327
+ workflowWarnings: WorkflowWarning[];
325
328
  artifacts: { count: number; groups: { stage: string; files: string[] }[] };
326
329
  workflow: WorkflowInfo;
327
330
  runtime: RuntimeInfo;
328
331
  git: GitInfo;
329
332
  };
330
333
 
334
+ type BuildStatusModelInput = {
335
+ taskId: string;
336
+ taskDir: string;
337
+ taskMdPath: string;
338
+ repoRoot: string;
339
+ shortId?: string;
340
+ run?: Runner;
341
+ now?: Date;
342
+ };
343
+
344
+ type BuildStatusModelOptions = {
345
+ run?: Runner;
346
+ now?: Date;
347
+ };
348
+
331
349
  // Indent each label/value pair by two spaces and pad labels to a common width so
332
350
  // every section reads as an aligned "key value" block.
333
351
  function renderPairs(rows: [string, string][]): string[] {
@@ -343,6 +361,14 @@ function renderStatus(model: StatusModel): string[] {
343
361
 
344
362
  lines.push('', 'Metadata', ...renderPairs(model.metadata));
345
363
 
364
+ if (model.workflowWarnings.length > 0) {
365
+ lines.push(
366
+ '',
367
+ `Workflow Warnings (${model.workflowWarnings.length} open)`,
368
+ ...formatWorkflowWarningSummary(model.workflowWarnings).map((line) => ` ${line}`)
369
+ );
370
+ }
371
+
346
372
  lines.push('', `Artifacts (${model.artifacts.count})`);
347
373
  if (model.artifacts.groups.length === 0) {
348
374
  lines.push(' (none)');
@@ -394,36 +420,89 @@ function renderStatus(model: StatusModel): string[] {
394
420
  return lines;
395
421
  }
396
422
 
397
- function status(args: string[] = []): void {
398
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
399
- process.stdout.write(USAGE);
400
- if (args.length === 0) process.exitCode = 1;
401
- return;
402
- }
403
-
404
- const resolved = resolveTaskRef(args[0]!);
405
- if (!resolved.ok) {
406
- process.stderr.write(`ai task status: ${resolved.message}\n`);
407
- process.exitCode = 1;
408
- return;
423
+ function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
424
+ if (model.workflow.state === 'in-progress') return model.workflow.stale === 'yes' ? 'warning' : 'running';
425
+ if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed')) return 'success';
426
+ if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
427
+ return 'danger';
409
428
  }
429
+ return 'info';
430
+ }
410
431
 
411
- const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
432
+ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
433
+ const content = fs.readFileSync(input.taskMdPath, 'utf8');
412
434
  const fm = parseTaskFrontmatter(content);
413
- const run = makeRunner(resolved.repoRoot);
414
- const artifacts = enumerateArtifacts(resolved.taskDir);
415
- const workflow = collectWorkflow(content);
435
+ const run = input.run ?? makeRunner(input.repoRoot);
436
+ const artifacts = enumerateArtifacts(input.taskDir);
437
+ const workflow = collectWorkflow(content, input.now);
416
438
 
417
- const model: StatusModel = {
418
- taskId: resolved.taskId,
419
- shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
439
+ return {
440
+ taskId: input.taskId,
441
+ shortId: input.shortId ?? loadShortIdByTaskId(input.repoRoot).get(input.taskId) ?? DASH,
420
442
  title: extractTitle(content),
421
443
  metadata: collectMetadata(fm),
444
+ workflowWarnings: getOpenWorkflowWarnings(content),
422
445
  artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
423
446
  workflow,
424
- runtime: collectRuntime(resolved.taskDir, workflow, run),
447
+ runtime: collectRuntime(input.taskDir, workflow, run),
425
448
  git: collectGit(fm.branch ?? '', run)
426
449
  };
450
+ }
451
+
452
+ function buildStatusModel(ref: string, options?: BuildStatusModelOptions): StatusModel;
453
+ function buildStatusModel(input: BuildStatusModelInput): StatusModel;
454
+ function buildStatusModel(
455
+ refOrInput: string | BuildStatusModelInput,
456
+ options: BuildStatusModelOptions = {}
457
+ ): StatusModel {
458
+ if (typeof refOrInput !== 'string') {
459
+ return buildFromResolved(refOrInput);
460
+ }
461
+
462
+ const resolved = resolveTaskRef(refOrInput);
463
+ if (!resolved.ok) {
464
+ throw new Error(resolved.message);
465
+ }
466
+
467
+ return buildFromResolved({
468
+ taskId: resolved.taskId,
469
+ taskDir: resolved.taskDir,
470
+ taskMdPath: resolved.taskMdPath,
471
+ repoRoot: resolved.repoRoot,
472
+ run: options.run,
473
+ now: options.now
474
+ });
475
+ }
476
+
477
+ function statusModelToDisplay(model: StatusModel): DisplayMessage {
478
+ return statusCard(
479
+ `Task ${model.taskId} (${model.shortId})`,
480
+ modelTone(model),
481
+ [
482
+ ['workflow', model.workflow.state],
483
+ ['step', model.workflow.step],
484
+ ['runtime', model.runtime.status],
485
+ ['git', model.git.uncommitted]
486
+ ],
487
+ renderStatus(model).join('\n')
488
+ );
489
+ }
490
+
491
+ function status(args: string[] = []): void {
492
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
493
+ process.stdout.write(USAGE);
494
+ if (args.length === 0) process.exitCode = 1;
495
+ return;
496
+ }
497
+
498
+ let model: StatusModel;
499
+ try {
500
+ model = buildStatusModel(args[0]!);
501
+ } catch (error) {
502
+ process.stderr.write(`ai task status: ${error instanceof Error ? error.message : String(error)}\n`);
503
+ process.exitCode = 1;
504
+ return;
505
+ }
427
506
 
428
507
  for (const line of renderStatus(model)) {
429
508
  process.stdout.write(`${line}\n`);
@@ -433,12 +512,14 @@ function status(args: string[] = []): void {
433
512
  export {
434
513
  status,
435
514
  makeRunner,
515
+ buildStatusModel,
436
516
  collectMetadata,
437
517
  groupArtifacts,
438
518
  collectGit,
439
519
  collectWorkflow,
440
520
  collectRuntime,
441
521
  renderStatus,
522
+ statusModelToDisplay,
442
523
  METADATA_KEYS
443
524
  };
444
- export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel };
525
+ export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel, BuildStatusModelInput };
@@ -0,0 +1,121 @@
1
+ import { extractSection } from './sections.ts';
2
+
3
+ const WORKFLOW_WARNING_HEADINGS = ['工作流告警', 'Workflow Warnings'];
4
+ const WORKFLOW_WARNING_STATUSES = new Set(['open', 'resolved', 'ignored']);
5
+ const WORKFLOW_WARNING_SEVERITIES = new Set(['IMPORTANT', 'ACTION_REQUIRED']);
6
+
7
+ type WorkflowWarning = {
8
+ id: string;
9
+ time: string;
10
+ step: string;
11
+ severity: string;
12
+ code: string;
13
+ status: string;
14
+ target: string;
15
+ message: string;
16
+ action: string;
17
+ resolvedAt: string;
18
+ resolution: string;
19
+ };
20
+
21
+ function splitTableRow(line: string): string[] {
22
+ let value = line.trim();
23
+ if (!value.startsWith('|')) return [];
24
+ value = value.replace(/^\|/, '').replace(/\|$/, '');
25
+
26
+ const cells: string[] = [];
27
+ let cell = '';
28
+ for (let index = 0; index < value.length; index += 1) {
29
+ const char = value[index]!;
30
+ if (char === '|' && !isEscapedAt(value, index)) {
31
+ cells.push(unescapeTableCell(cell.trim()));
32
+ cell = '';
33
+ continue;
34
+ }
35
+ cell += char;
36
+ }
37
+ cells.push(unescapeTableCell(cell.trim()));
38
+ return cells;
39
+ }
40
+
41
+ function unescapeTableCell(value: string): string {
42
+ let output = '';
43
+ for (let index = 0; index < value.length; index += 1) {
44
+ const char = value[index]!;
45
+ const next = value[index + 1];
46
+ if (char === '\\' && (next === '\\' || next === '|')) {
47
+ output += next;
48
+ index += 1;
49
+ continue;
50
+ }
51
+ output += char;
52
+ }
53
+ return output;
54
+ }
55
+
56
+ function isEscapedAt(value: string, index: number): boolean {
57
+ let backslashes = 0;
58
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor -= 1) {
59
+ backslashes += 1;
60
+ }
61
+ return backslashes % 2 === 1;
62
+ }
63
+
64
+ function isSeparatorRow(cells: string[]): boolean {
65
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
66
+ }
67
+
68
+ /**
69
+ * Parses well-formed workflow warning rows. Malformed rows are intentionally
70
+ * ignored here; validate-artifact is responsible for reporting structure errors.
71
+ */
72
+ function parseWorkflowWarnings(content: string): WorkflowWarning[] {
73
+ const section = extractSection(content, WORKFLOW_WARNING_HEADINGS);
74
+ if (!section) return [];
75
+
76
+ const rows: WorkflowWarning[] = [];
77
+ for (const rawLine of section.split(/\r?\n/)) {
78
+ const cells = splitTableRow(rawLine);
79
+ if (cells.length === 0) continue;
80
+ if ((cells[0] ?? '').toLowerCase() === 'id') continue;
81
+ if (isSeparatorRow(cells)) continue;
82
+ if (cells.length < 11) continue;
83
+
84
+ rows.push({
85
+ id: cells[0] ?? '',
86
+ time: cells[1] ?? '',
87
+ step: cells[2] ?? '',
88
+ severity: cells[3] ?? '',
89
+ code: cells[4] ?? '',
90
+ status: cells[5] ?? '',
91
+ target: cells[6] ?? '',
92
+ message: cells[7] ?? '',
93
+ action: cells[8] ?? '',
94
+ resolvedAt: cells[9] ?? '',
95
+ resolution: cells[10] ?? ''
96
+ });
97
+ }
98
+ return rows;
99
+ }
100
+
101
+ function getOpenWorkflowWarnings(content: string): WorkflowWarning[] {
102
+ return parseWorkflowWarnings(content).filter((warning) => warning.status === 'open');
103
+ }
104
+
105
+ function formatWorkflowWarningSummary(warnings: readonly WorkflowWarning[]): string[] {
106
+ return warnings.map((warning) => {
107
+ const target = warning.target ? ` ${warning.target}` : '';
108
+ const action = warning.action ? ` - ${warning.action}` : '';
109
+ return `${warning.id} [${warning.severity}] ${warning.code}${target}${action}`;
110
+ });
111
+ }
112
+
113
+ export {
114
+ WORKFLOW_WARNING_HEADINGS,
115
+ WORKFLOW_WARNING_STATUSES,
116
+ WORKFLOW_WARNING_SEVERITIES,
117
+ parseWorkflowWarnings,
118
+ getOpenWorkflowWarnings,
119
+ formatWorkflowWarningSummary
120
+ };
121
+ export type { WorkflowWarning };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -67,7 +67,7 @@
67
67
  "@types/cross-spawn": "^6.0.6",
68
68
  "@types/node": "^22.20.0",
69
69
  "@types/semver": "^7.7.1",
70
- "typescript": "~6.0"
70
+ "typescript": "~7.0"
71
71
  },
72
72
  "optionalDependencies": {
73
73
  "@lydell/node-pty": "^1.2.0-beta.12"