agentflowctl 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,6 +97,21 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
97
97
 
98
98
  `status` 會列出任務。進行中的任務會標出正在寫測試還是正在寫實作。
99
99
 
100
+ ### 執行中的終端機輸出
101
+
102
+ agent 每次使用工具,都會印出完整的指令或主要參數,不截斷。多行指令的後續行會縮排對齊。install、測試、verify 這些專案指令也會以 `$ ` 開頭印出來:
103
+
104
+ ```
105
+ 💬 [claude] 先讀現有的表單元件
106
+ 🔧 [claude] Read: /repo/.agentflowctl/worktrees/f-xxxx/src/LoginForm.tsx
107
+ 🔧 [claude] Bash: pnpm vitest run src/LoginForm.test.tsx
108
+ 🔧 [codex] shell: bash -lc 'pnpm test'
109
+ 🔧 [gemini] run_shell_command: npm run lint
110
+ $ pnpm install
111
+ ```
112
+
113
+ 工具參數依序取 command、檔案路徑、path、pattern、url、query,都沒有時印出整包 JSON。每次執行的完整輸出都在 `agentflowctl logs <id>`。
114
+
100
115
  ## 設定
101
116
 
102
117
  專案根目錄的 `flow.config.json`。完整範例見 `examples/flow.config.json`。未提供的欄位使用內建預設(安裝指令、測試指令、檢查清單預設對應 Vite + TypeScript + Vitest)。
@@ -126,6 +141,7 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
126
141
  | `maxAgentRuns` | `60` | 單一 run 最多執行幾次 agent |
127
142
  | `install` / `test` / `checks` | 見 `src/schemas.ts` | verify 階段實際執行的指令 |
128
143
  | `agents` | 內建 claude、codex、gemini | 覆寫內建 agent,或用 `command` adapter 接上其他 CLI |
144
+ | `removedAgents` | `[]` | 移除的內建 agent,不會被自動偵測、不能放進輪替。通常用 `agent remove` 寫入 |
129
145
 
130
146
  verify 失敗(型別、lint、建置)一律交回最後作者。審查意見才依 `fixStrategy` 決定修正者。
131
147
 
@@ -140,13 +156,15 @@ agentflowctl agent add aider --adapter command -- aider --yes-always --message {
140
156
  agentflowctl agent set codex --model 你要用的模型 --extra-arg=--search
141
157
  agentflowctl agent set aider --adapter gemini # 換 adapter
142
158
  agentflowctl agent remove aider
159
+ agentflowctl agent remove gemini # 內建的也能移除
143
160
  agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯示目前的順序
144
161
  ```
145
162
 
146
163
  修改會連帶更新相關設定,並在終端機列出:
147
164
 
148
165
  - `set --adapter` 換 adapter 時,會清掉舊 adapter 的 `model`、`extraArgs`、`command`,這次有重新指定的除外。
149
- - `remove` 刪除自訂 agent 時,會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回自動偵測。對內建 agent 用 `remove`,只會刪掉覆寫設定。
166
+ - `remove` 會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回自動偵測。
167
+ - 內建的 claude、codex、gemini 也能 `remove`:覆寫設定會一起刪掉,名稱記在 `removedAgents`,之後自動偵測會跳過它,也不能放進 `cycle`。要加回來用 `agent add gemini --adapter gemini`。
150
168
  - `--extra-arg` 可以重複指定,會整個取代原本的 `extraArgs`。參數以 `-` 開頭時,寫成 `--extra-arg=--sandbox`。
151
169
 
152
170
  已建立的 run 會沿用建立時的輪替順序,不受這些修改影響。
@@ -1,12 +1,20 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
- import { DEFAULT_CYCLE } from "./agents/index.js";
3
+ import { builtinAgents, DEFAULT_CYCLE } from "./agents/index.js";
4
4
  import { AgentDef, RepoConfig } from "./schemas.js";
5
5
  /** 這些欄位的意義取決於 adapter,換 adapter 時要清掉 */
6
6
  const ADAPTER_FIELDS = ["model", "extraArgs", "command"];
7
7
  const isBuiltin = (name) => DEFAULT_CYCLE.includes(name);
8
8
  const agentsOf = (cfg) => ({ ...(cfg.agents ?? {}) });
9
- const isDefined = (cfg, name) => isBuiltin(name) || name in agentsOf(cfg);
9
+ const removedOf = (cfg) => (cfg.removedAgents ?? []);
10
+ const isDefined = (cfg, name) => builtinAgents(removedOf(cfg)).includes(name) || name in agentsOf(cfg);
11
+ /** 寫回 removedAgents;清單變空就刪掉欄位 */
12
+ function withRemoved(cfg, removed) {
13
+ const next = { ...cfg, removedAgents: removed };
14
+ if (!removed.length)
15
+ delete next.removedAgents;
16
+ return next;
17
+ }
10
18
  /** 只留下有值的欄位,驗證後回傳 */
11
19
  function buildAgent(base, patch) {
12
20
  const next = { ...base };
@@ -29,7 +37,10 @@ export function addAgent(cfg, name, def) {
29
37
  if (isDefined(cfg, name))
30
38
  throw new Error(`agent ${name} 已存在,要修改請用 agent set`);
31
39
  const { adapter, ...patch } = def;
32
- return { cfg: { ...cfg, agents: { ...agentsOf(cfg), [name]: buildAgent({ adapter }, patch) } }, changes: [] };
40
+ const next = { ...cfg, agents: { ...agentsOf(cfg), [name]: buildAgent({ adapter }, patch) } };
41
+ // 加回先前移除的內建 agent
42
+ const removed = removedOf(cfg);
43
+ return { cfg: removed.includes(name) ? withRemoved(next, removed.filter((n) => n !== name)) : next, changes: [] };
33
44
  }
34
45
  export function setAgent(cfg, name, patch) {
35
46
  if (!isDefined(cfg, name))
@@ -51,16 +62,17 @@ export function setAgent(cfg, name, patch) {
51
62
  return { cfg: { ...cfg, agents: { ...agents, [name]: buildAgent(base, patch) } }, changes };
52
63
  }
53
64
  export function removeAgent(cfg, name) {
65
+ if (!isDefined(cfg, name))
66
+ throw new Error(`未定義的 agent:${name}`);
54
67
  const agents = agentsOf(cfg);
55
- if (!(name in agents)) {
56
- throw new Error(isBuiltin(name) ? `${name} 是內建 agent,沒有覆寫設定可以刪除` : `未定義的 agent:${name}`);
57
- }
58
68
  delete agents[name];
59
- const next = { ...cfg, agents };
69
+ // 內建 agent 沒有設定也存在,要記在 removedAgents 才算移除;覆寫設定一併刪掉
70
+ let next = { ...cfg, agents };
71
+ if (isBuiltin(name))
72
+ next = withRemoved(next, [...removedOf(cfg), name]);
60
73
  const changes = [];
61
74
  const cycle = cfg.cycle;
62
- // 自訂 agent 刪掉後就不存在了,一併從輪替順序移除;內建 agent 只是恢復預設,仍可留在輪替裡
63
- if (cycle?.includes(name) && !isBuiltin(name)) {
75
+ if (cycle?.includes(name)) {
64
76
  const rest = cycle.filter((n) => n !== name);
65
77
  if (rest.length) {
66
78
  next.cycle = rest;
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { num, str, tryJson } from "./types.js";
3
+ import { num, str, toolDetail, tryJson } from "./types.js";
4
4
  /**
5
5
  * Claude Code 的權限設定:acceptEdits 只自動允許工作目錄內的修改,
6
6
  * 版本控制交給 agentflowctl,並啟用內建沙箱限制 shell 指令。
@@ -46,7 +46,7 @@ export const claude = {
46
46
  if (b.type === "text" && str(b.text)?.trim())
47
47
  out.push({ kind: "text", text: str(b.text) });
48
48
  if (b.type === "tool_use")
49
- out.push({ kind: "tool", name: str(b.name) ?? "tool" });
49
+ out.push({ kind: "tool", name: str(b.name) ?? "tool", detail: toolDetail(b.input) });
50
50
  }
51
51
  }
52
52
  else if (ev.type === "result") {
@@ -21,9 +21,12 @@ export const codex = {
21
21
  if (item.type === "agent_message" && str(item.text)?.trim())
22
22
  out.push({ kind: "text", text: str(item.text) });
23
23
  else if (item.type === "command_execution")
24
- out.push({ kind: "tool", name: `shell: ${str(item.command) ?? ""}`.slice(0, 80) });
25
- else if (item.type === "file_change")
26
- out.push({ kind: "tool", name: "edit" });
24
+ out.push({ kind: "tool", name: "shell", detail: str(item.command) });
25
+ else if (item.type === "file_change") {
26
+ const changes = Array.isArray(item.changes) ? item.changes : [];
27
+ const paths = changes.map((c) => str(c.path)).filter(Boolean);
28
+ out.push({ kind: "tool", name: "edit", detail: paths.length ? paths.join(", ") : undefined });
29
+ }
27
30
  }
28
31
  else if (ev.type === "turn.completed") {
29
32
  const usage = (ev.usage ?? {});
@@ -1,4 +1,4 @@
1
- import { num, str, tryJson } from "./types.js";
1
+ import { num, str, toolDetail, tryJson } from "./types.js";
2
2
  /**
3
3
  * Google Gemini CLI:`gemini -p ... --output-format stream-json`。
4
4
  * Gemini CLI 沒有「只允許工作目錄內的 shell 指令」這種細緻權限,
@@ -21,7 +21,7 @@ export const gemini = {
21
21
  out.push({ kind: "text", text: str(ev.content) });
22
22
  }
23
23
  else if (ev.type === "tool_use") {
24
- out.push({ kind: "tool", name: str(ev.tool_name) ?? "tool" });
24
+ out.push({ kind: "tool", name: str(ev.tool_name) ?? "tool", detail: toolDetail(ev.parameters) });
25
25
  }
26
26
  else if (ev.type === "result") {
27
27
  const stats = (ev.stats ?? {});
@@ -5,4 +5,6 @@ import { gemini } from "./gemini.js";
5
5
  export const ADAPTERS = { claude, codex, gemini, command };
6
6
  /** 沒有設定時,依序偵測這些已安裝的 CLI 組成輪替順序 */
7
7
  export const DEFAULT_CYCLE = ["claude", "codex", "gemini"];
8
+ /** 扣掉設定裡 removedAgents 之後,仍可使用的內建 agent */
9
+ export const builtinAgents = (removed = []) => DEFAULT_CYCLE.filter((n) => !removed.includes(n));
8
10
  //# sourceMappingURL=index.js.map
@@ -9,4 +9,18 @@ export function tryJson(line) {
9
9
  }
10
10
  export const num = (v) => (typeof v === "number" ? v : undefined);
11
11
  export const str = (v) => (typeof v === "string" ? v : undefined);
12
+ /** 從工具參數挑出最能代表「正在做什麼」的欄位;都沒有時以 JSON 顯示全部參數 */
13
+ export function toolDetail(input) {
14
+ if (!input || typeof input !== "object")
15
+ return undefined;
16
+ const o = input;
17
+ for (const key of ["command", "cmd", "file_path", "absolute_path", "path", "pattern", "url", "query"]) {
18
+ const v = o[key];
19
+ if (typeof v === "string" && v.trim())
20
+ return v;
21
+ if (Array.isArray(v) && v.length)
22
+ return v.join(" ");
23
+ }
24
+ return Object.keys(o).length ? JSON.stringify(o) : undefined;
25
+ }
12
26
  //# sourceMappingURL=types.js.map
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { DEFAULT_CYCLE } from "./agents/index.js";
5
+ import { builtinAgents } from "./agents/index.js";
6
6
  import { advance, loadRepoConfig } from "./engine.js";
7
7
  import { API_KEY_VARS, probeAgent, resolveAgent, runCommand } from "./runner.js";
8
8
  import { addWorktree, git, removeWorktree } from "./git.js";
@@ -62,11 +62,11 @@ async function resolveCycle(flag) {
62
62
  return wanted;
63
63
  }
64
64
  const found = [];
65
- for (const name of DEFAULT_CYCLE)
65
+ for (const name of builtinAgents(cfg.removedAgents))
66
66
  if (await probeAgent(resolveAgent(cfg, name)))
67
67
  found.push(name);
68
68
  if (!found.length)
69
- throw new Error("沒有偵測到任何 agent CLI(claude、codex、gemini),可用 agentflowctl doctor 檢查");
69
+ throw new Error(`沒有偵測到任何 agent CLI(${builtinAgents(cfg.removedAgents).join("、") || "內建 agent 都已移除"}),可用 agentflowctl doctor 檢查`);
70
70
  return found;
71
71
  }
72
72
  const program = new Command()
@@ -211,12 +211,12 @@ agent
211
211
  .action(async () => {
212
212
  const cfg = loadRepoConfig();
213
213
  const cycle = await resolveCycle().catch(() => cfg.cycle ?? []);
214
- const names = [...new Set([...DEFAULT_CYCLE, ...Object.keys(cfg.agents)])];
214
+ const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
215
215
  for (const name of names) {
216
216
  const def = resolveAgent(cfg, name);
217
217
  const ok = await probeAgent(def);
218
218
  const pos = cycle.indexOf(name);
219
- const kind = DEFAULT_CYCLE.includes(name) ? (name in cfg.agents ? "內建(已覆寫)" : "內建") : "自訂";
219
+ const kind = builtinAgents(cfg.removedAgents).includes(name) ? (name in cfg.agents ? "內建(已覆寫)" : "內建") : "自訂";
220
220
  const detail = [
221
221
  `adapter=${def.adapter}`,
222
222
  def.model && `model=${def.model}`,
@@ -225,6 +225,8 @@ agent
225
225
  ].filter(Boolean);
226
226
  console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${kind.padEnd(8)} ${pos >= 0 ? `輪替 #${pos + 1}` : "不在輪替"} ${detail.join(" ")}`);
227
227
  }
228
+ if (cfg.removedAgents.length)
229
+ console.log(`\n已移除的內建 agent:${cfg.removedAgents.join("、")}(可用 agent add <name> --adapter <name> 加回)`);
228
230
  console.log(`
229
231
  輪替順序:${cycle.length ? cycle.join(" → ") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
230
232
  });
@@ -248,8 +250,8 @@ agent
248
250
  });
249
251
  agent
250
252
  .command("remove <name>")
251
- .description("刪除自訂 agent(一併從輪替移除),或刪除內建 agent 的覆寫設定")
252
- .action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已刪除 ${name} 的設定`));
253
+ .description("刪除 agent(含內建的 claude、codex、gemini),一併從輪替移除;內建的可用 agent add 加回")
254
+ .action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已移除 ${name}`));
253
255
  agent
254
256
  .command("cycle [names]")
255
257
  .description("顯示輪替順序,或用逗號分隔設定新的順序")
@@ -272,7 +274,7 @@ program
272
274
  .description("檢查可用的 agent CLI 與目前的輪替設定")
273
275
  .action(async () => {
274
276
  const cfg = loadRepoConfig();
275
- const names = [...new Set([...DEFAULT_CYCLE, ...Object.keys(cfg.agents)])];
277
+ const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
276
278
  for (const name of names) {
277
279
  const def = resolveAgent(cfg, name);
278
280
  const ok = await probeAgent(def);
package/dist/runner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { z } from "zod";
4
- import { ADAPTERS, DEFAULT_CYCLE } from "./agents/index.js";
4
+ import { ADAPTERS, builtinAgents } from "./agents/index.js";
5
5
  import { AgentDef } from "./schemas.js";
6
6
  import { projectRoot, runDir } from "./paths.js";
7
7
  import { exec, execShell } from "./proc.js";
@@ -56,14 +56,22 @@ function appendLog(file, text) {
56
56
  mkdirSync(dirname(file), { recursive: true });
57
57
  appendFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
58
58
  }
59
+ /** 一行工具事件的畫面輸出:完整顯示指令或參數,多行時後續行縮排對齊 */
60
+ export function formatToolLine(agent, tool) {
61
+ const head = ` 🔧 [${agent}] ${tool.name}`;
62
+ const detail = tool.detail?.trim();
63
+ return detail ? `${head}: ${detail.split("\n").join("\n ")}` : head;
64
+ }
59
65
  /** 內建的 claude、codex、gemini 定義,可在 flow.config.json 覆寫或新增其他 agent */
60
66
  export function resolveAgent(cfg, name) {
61
67
  const custom = cfg.agents[name];
62
68
  if (custom)
63
69
  return custom;
64
- if (DEFAULT_CYCLE.includes(name)) {
70
+ if (builtinAgents(cfg.removedAgents).includes(name)) {
65
71
  return AgentDef.parse({ adapter: name });
66
72
  }
73
+ if (cfg.removedAgents.includes(name))
74
+ throw new Error(`${name} 已從設定移除(removedAgents),要使用請先 agent add ${name} --adapter ${name}`);
67
75
  throw new Error(`未定義的 agent:${name}(請在 flow.config.json 的 agents 裡設定)`);
68
76
  }
69
77
  /** 確認某個 agent 的 CLI 是否可以執行 */
@@ -108,7 +116,7 @@ export async function runAgent(name, def, t, prompt) {
108
116
  console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
109
117
  }
110
118
  else if (ev.kind === "tool") {
111
- console.log(` 🔧 [${name}] ${ev.name}`);
119
+ console.log(formatToolLine(name, ev));
112
120
  }
113
121
  else if (ev.kind === "usage") {
114
122
  inputTokens += ev.inputTokens ?? 0;
@@ -134,6 +142,7 @@ export async function runAgent(name, def, t, prompt) {
134
142
  */
135
143
  export async function runCommand(t, cmd) {
136
144
  appendLog(t.logFile, `$ ${cmd}`);
145
+ console.log(` $ ${cmd.trim().split("\n").join("\n ")}`);
137
146
  const r = await execShell(cmd, { cwd: t.cwd });
138
147
  const output = `${r.stdout}\n${r.stderr}`.trim();
139
148
  appendLog(t.logFile, output);
package/dist/schemas.js CHANGED
@@ -51,6 +51,8 @@ export const AgentDef = z.object({
51
51
  export const RepoConfig = z.object({
52
52
  /** 自訂或覆寫 agent;claude、codex、gemini 三個名稱內建 */
53
53
  agents: z.record(z.string(), AgentDef).default({}),
54
+ /** 移除的內建 agent:不再自動偵測,也不能放進輪替;用 agent add 加回 */
55
+ removedAgents: z.array(z.string()).default([]),
54
56
  /** 輪替順序;未設定時自動偵測已安裝的 CLI */
55
57
  cycle: z.array(z.string()).min(1).optional(),
56
58
  /** review 後的修正由誰做:ring=輪到下一位;author=最後寫程式的 agent */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",