@astrosheep/keiyaku 0.1.86 → 0.1.87

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.
@@ -4,6 +4,7 @@ import { getConfig } from "../config/config.js";
4
4
  import { DISABLE_SUBAGENT_SPAWN_ENV_KEY } from "../config/env-keys.js";
5
5
  import { expandHomePath } from "../config/path-utils.js";
6
6
  import { appendDebugLog } from "../telemetry/debug-log.js";
7
+ import { SubagentProgressReporter } from "./progress-reporter.js";
7
8
  import { StringTailBuffer } from "./string-tail-buffer.js";
8
9
  import { SUBAGENT_SESSION_VERSION, SubagentExecError } from "./types.js";
9
10
  const CLAUDE_AGENT_SDK_PROVIDER = "claude-agent-sdk";
@@ -84,6 +85,102 @@ function extractResultText(event) {
84
85
  const errorText = event.errors.join("\n").trim();
85
86
  return errorText || undefined;
86
87
  }
88
+ function readStringProperty(value, key) {
89
+ if (!value || typeof value !== "object")
90
+ return undefined;
91
+ const property = value[key];
92
+ return coerceString(property);
93
+ }
94
+ function readNumberProperty(value, key) {
95
+ if (!value || typeof value !== "object")
96
+ return 0;
97
+ const property = value[key];
98
+ return typeof property === "number" && Number.isFinite(property) ? property : 0;
99
+ }
100
+ function recordAssistantToolUses(reporter, event) {
101
+ let toolUseCount = 0;
102
+ for (const block of event.message.content) {
103
+ if (!("type" in block) || block.type !== "tool_use")
104
+ continue;
105
+ toolUseCount += 1;
106
+ reporter.recordToolOnce(readStringProperty(block, "id"), readStringProperty(block, "name") ?? "tool_use");
107
+ }
108
+ return toolUseCount;
109
+ }
110
+ function recordClaudeProgressEvent(reporter, event) {
111
+ reporter.recordEvent();
112
+ if (event.type === "assistant") {
113
+ const toolUseCount = recordAssistantToolUses(reporter, event);
114
+ if (toolUseCount > 0) {
115
+ reporter.setState("using tools");
116
+ }
117
+ return;
118
+ }
119
+ if (event.type === "result") {
120
+ reporter.setState(event.subtype === "success" ? "completed" : "failed");
121
+ if (event.subtype !== "success") {
122
+ reporter.recordFailure();
123
+ }
124
+ return;
125
+ }
126
+ if (event.type === "tool_progress") {
127
+ reporter.recordToolOnce(event.tool_use_id, event.tool_name);
128
+ reporter.setState(`using ${event.tool_name}`);
129
+ return;
130
+ }
131
+ if (event.type === "tool_use_summary") {
132
+ reporter.setSummary(event.summary);
133
+ return;
134
+ }
135
+ if (event.type !== "system")
136
+ return;
137
+ switch (event.subtype) {
138
+ case "init":
139
+ reporter.setState("initialized");
140
+ break;
141
+ case "session_state_changed":
142
+ reporter.setState(event.state);
143
+ break;
144
+ case "status":
145
+ reporter.setState(event.status ?? "running");
146
+ break;
147
+ case "task_started":
148
+ reporter.setState("task started");
149
+ reporter.setSummary(event.description);
150
+ break;
151
+ case "task_progress": {
152
+ const lastToolName = readStringProperty(event, "last_tool_name");
153
+ if (lastToolName) {
154
+ reporter.setState(`using ${lastToolName}`);
155
+ }
156
+ else {
157
+ reporter.setState("task running");
158
+ }
159
+ reporter.setSummary(readStringProperty(event, "summary") ?? readStringProperty(event, "description"));
160
+ break;
161
+ }
162
+ case "task_notification":
163
+ reporter.setState(readStringProperty(event, "status"));
164
+ reporter.setSummary(readStringProperty(event, "summary"));
165
+ if (readStringProperty(event, "status") === "failed") {
166
+ reporter.recordFailure();
167
+ }
168
+ break;
169
+ case "hook_started":
170
+ reporter.setState(`hook ${event.hook_event}`);
171
+ break;
172
+ case "hook_response":
173
+ if (event.outcome === "error")
174
+ reporter.recordFailure();
175
+ reporter.setState(`hook ${event.outcome}`);
176
+ break;
177
+ default: {
178
+ const changedFileCount = readNumberProperty(event, "file_count");
179
+ reporter.recordChangedFiles(changedFileCount);
180
+ break;
181
+ }
182
+ }
183
+ }
87
184
  async function loadClaudeAgentSdk() {
88
185
  if (claudeAgentSdkLoader) {
89
186
  return claudeAgentSdkLoader();
@@ -109,6 +206,11 @@ export async function runClaudeAgentSdkExec(prompt, cwd, options) {
109
206
  let finalMessage;
110
207
  let timedOut = false;
111
208
  let idleTimedOut = false;
209
+ const progressReporter = new SubagentProgressReporter({
210
+ provider: CLAUDE_AGENT_SDK_PROVIDER,
211
+ cwd,
212
+ intervalMin: subagent.progressIntervalMin,
213
+ });
112
214
  const timeoutController = new AbortController();
113
215
  const timeout = setTimeout(() => {
114
216
  timedOut = true;
@@ -165,6 +267,7 @@ export async function runClaudeAgentSdkExec(prompt, cwd, options) {
165
267
  for await (const event of streamed) {
166
268
  scheduleIdleTimeout();
167
269
  outputBuffer.append(`${JSON.stringify(event)}\n`);
270
+ recordClaudeProgressEvent(progressReporter, event);
168
271
  resumedSessionId = extractResumeToken(event) ?? resumedSessionId;
169
272
  switch (event.type) {
170
273
  case "assistant": {
@@ -243,6 +346,7 @@ export async function runClaudeAgentSdkExec(prompt, cwd, options) {
243
346
  clearTimeout(timeout);
244
347
  if (idleTimeout)
245
348
  clearTimeout(idleTimeout);
349
+ progressReporter.stop();
246
350
  unlinkAbort?.();
247
351
  }
248
352
  }
@@ -166,7 +166,9 @@ export async function runCodexCliExec(prompt, cwd, options = {}) {
166
166
  const idleTimeoutMs = subagent.execIdleTimeoutMs;
167
167
  const maxCaptureChars = codex.execMaxCaptureChars;
168
168
  const threadId = options.threadId?.trim();
169
- const codexArgs = threadId ? ["exec", "resume", "--full-auto", "--json"] : ["exec", "--full-auto", "--json", "-C", cwd];
169
+ const codexArgs = threadId
170
+ ? ["exec", "resume", "--full-auto", "--json", "--skip-git-repo-check"]
171
+ : ["exec", "--full-auto", "--json", "--skip-git-repo-check", "-C", cwd];
170
172
  if (options.model) {
171
173
  codexArgs.push("-m", options.model);
172
174
  }
@@ -3,6 +3,7 @@ import { CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY } from "../keiyaku.js";
3
3
  import { getConfig } from "../config/config.js";
4
4
  import { DISABLE_SUBAGENT_SPAWN_ENV_KEY } from "../config/env-keys.js";
5
5
  import { appendDebugBlock, appendDebugLog } from "../telemetry/debug-log.js";
6
+ import { SubagentProgressReporter } from "./progress-reporter.js";
6
7
  import { StringTailBuffer } from "./string-tail-buffer.js";
7
8
  import { SUBAGENT_SESSION_VERSION, SubagentExecError } from "./types.js";
8
9
  const CODEX_EVENT_THREAD_STARTED = "thread.started";
@@ -31,6 +32,72 @@ function extractAgentMessage(event) {
31
32
  }
32
33
  return event.item.type === "agent_message" ? coerceString(event.item.text) : undefined;
33
34
  }
35
+ function summarizeCommand(command) {
36
+ return command.replace(/\s+/g, " ").trim().slice(0, 120);
37
+ }
38
+ function recordCodexProgressEvent(reporter, event) {
39
+ reporter.recordEvent();
40
+ switch (event.type) {
41
+ case CODEX_EVENT_TURN_STARTED:
42
+ reporter.setState("running");
43
+ break;
44
+ case "turn.completed":
45
+ reporter.setState("completed");
46
+ break;
47
+ case CODEX_EVENT_TURN_FAILED:
48
+ case CODEX_EVENT_ERROR:
49
+ reporter.setState("failed");
50
+ reporter.recordFailure();
51
+ break;
52
+ case "item.started":
53
+ if (event.item.type === "command_execution") {
54
+ reporter.recordToolOnce(event.item.id, "shell", summarizeCommand(event.item.command));
55
+ reporter.setState("running command");
56
+ }
57
+ else if (event.item.type === "mcp_tool_call") {
58
+ reporter.recordToolOnce(event.item.id, `${event.item.server}.${event.item.tool}`);
59
+ reporter.setState("using mcp tool");
60
+ }
61
+ else if (event.item.type === "web_search") {
62
+ reporter.recordToolOnce(event.item.id, "web_search");
63
+ reporter.setState("searching");
64
+ }
65
+ break;
66
+ case CODEX_EVENT_ITEM_COMPLETED:
67
+ if (event.item.type === "file_change") {
68
+ reporter.recordToolOnce(event.item.id, "file_change", `${event.item.changes.length} file(s)`);
69
+ reporter.recordChangedFiles(event.item.changes.length);
70
+ reporter.setState(event.item.status === "failed" ? "file change failed" : "changed files");
71
+ if (event.item.status === "failed")
72
+ reporter.recordFailure();
73
+ }
74
+ else if (event.item.type === "command_execution") {
75
+ reporter.recordToolOnce(event.item.id, "shell", summarizeCommand(event.item.command));
76
+ if (event.item.status === "failed")
77
+ reporter.recordFailure();
78
+ reporter.setState(event.item.status === "failed" ? "command failed" : "command completed");
79
+ }
80
+ else if (event.item.type === "mcp_tool_call") {
81
+ reporter.recordToolOnce(event.item.id, `${event.item.server}.${event.item.tool}`);
82
+ if (event.item.status === "failed")
83
+ reporter.recordFailure();
84
+ reporter.setState(event.item.status === "failed" ? "mcp tool failed" : "mcp tool completed");
85
+ }
86
+ else if (event.item.type === "web_search") {
87
+ reporter.recordToolOnce(event.item.id, "web_search");
88
+ reporter.setState("search completed");
89
+ }
90
+ break;
91
+ case CODEX_EVENT_ITEM_UPDATED:
92
+ if (event.item.type === "todo_list") {
93
+ const completed = event.item.items.filter((item) => item.completed).length;
94
+ reporter.setSummary(`todo ${completed}/${event.item.items.length} complete`);
95
+ }
96
+ break;
97
+ default:
98
+ break;
99
+ }
100
+ }
34
101
  function appendEventType(recordedTypes, eventType) {
35
102
  if (recordedTypes.length === MAX_RECORDED_EVENT_TYPES) {
36
103
  recordedTypes.shift();
@@ -80,6 +147,11 @@ export async function runCodexSdkExec(prompt, cwd, options = {}) {
80
147
  let streamErrorMessage;
81
148
  let timedOut = false;
82
149
  let idleTimedOut = false;
150
+ const progressReporter = new SubagentProgressReporter({
151
+ provider: CODEX_SDK_PROVIDER,
152
+ cwd,
153
+ intervalMin: subagent.progressIntervalMin,
154
+ });
83
155
  const timeoutController = new AbortController();
84
156
  const timeout = setTimeout(() => {
85
157
  timedOut = true;
@@ -117,6 +189,7 @@ export async function runCodexSdkExec(prompt, cwd, options = {}) {
117
189
  const threadOptions = {
118
190
  model: options.model,
119
191
  workingDirectory: cwd,
192
+ skipGitRepoCheck: true,
120
193
  };
121
194
  const thread = threadId ? sdk.resumeThread(threadId, threadOptions) : sdk.startThread(threadOptions);
122
195
  const streamed = await thread.runStreamed(prompt, { signal: timeoutController.signal });
@@ -124,6 +197,7 @@ export async function runCodexSdkExec(prompt, cwd, options = {}) {
124
197
  totalEventCount += 1;
125
198
  appendEventType(recordedEventTypes, event.type);
126
199
  stdoutBuffer.append(`${JSON.stringify(event)}\n`);
200
+ recordCodexProgressEvent(progressReporter, event);
127
201
  scheduleIdleTimeout();
128
202
  if (event.type === CODEX_EVENT_THREAD_STARTED) {
129
203
  resumeToken = event.thread_id;
@@ -211,6 +285,7 @@ export async function runCodexSdkExec(prompt, cwd, options = {}) {
211
285
  clearTimeout(timeout);
212
286
  if (idleTimeout)
213
287
  clearTimeout(idleTimeout);
288
+ progressReporter.stop();
214
289
  unlinkAbort?.();
215
290
  }
216
291
  }
@@ -205,7 +205,7 @@ async function loadPersistedGeminiSession(module, cwd, sessionId) {
205
205
  }
206
206
  function createGeminiCliCoreConfig(module, cwd, sessionId, model) {
207
207
  const configParameters = {
208
- allowedEnvironmentVariables: [DISABLE_SUBAGENT_SPAWN_ENV_KEY],
208
+ allowedEnvironmentVariables: Object.keys(process.env),
209
209
  approvalMode: module.ApprovalMode.YOLO,
210
210
  cwd,
211
211
  debugMode: false,
@@ -213,10 +213,11 @@ export async function resolveSubagentConfig(agentName, cwd) {
213
213
  }
214
214
  export async function runSubagentExec(agentName, prompt, cwd, options = {}) {
215
215
  const config = await resolveSubagentConfig(agentName, cwd);
216
+ const effectiveConfig = options.model ? { ...config, model: options.model } : config;
216
217
  const adapter = SUBAGENT_ADAPTERS[config.provider];
217
218
  if (!options.resume) {
218
- return adapter.run(prompt, cwd, { ...config, signal: options.signal });
219
+ return adapter.run(prompt, cwd, { ...effectiveConfig, signal: options.signal });
219
220
  }
220
221
  const handle = validateResumeSession(adapter, options.resume.session, options.resume.resumePath);
221
- return adapter.resume(prompt, cwd, { ...config, signal: options.signal }, handle);
222
+ return adapter.resume(prompt, cwd, { ...effectiveConfig, signal: options.signal }, handle);
222
223
  }
@@ -0,0 +1,137 @@
1
+ import { logInfo } from "../telemetry/logger.js";
2
+ function createWindow(now) {
3
+ return {
4
+ startedAt: now,
5
+ eventCount: 0,
6
+ toolCounts: new Map(),
7
+ changedFileCount: 0,
8
+ failureCount: 0,
9
+ };
10
+ }
11
+ function formatDuration(ms) {
12
+ const seconds = Math.max(0, Math.round(ms / 1000));
13
+ if (seconds < 60)
14
+ return `${seconds}s`;
15
+ const minutes = Math.floor(seconds / 60);
16
+ const remainingSeconds = seconds % 60;
17
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
18
+ }
19
+ function formatToolCounts(toolCounts) {
20
+ if (toolCounts.size === 0)
21
+ return "none";
22
+ return [...toolCounts.entries()]
23
+ .sort(([leftName, leftCount], [rightName, rightCount]) => rightCount - leftCount || leftName.localeCompare(rightName))
24
+ .map(([name, count]) => `${name} x${count}`)
25
+ .join(", ");
26
+ }
27
+ function sanitizeSummary(summary) {
28
+ return summary.replace(/\s+/g, " ").trim().slice(0, 160);
29
+ }
30
+ export class SubagentProgressReporter {
31
+ options;
32
+ window = createWindow(Date.now());
33
+ recordedToolKeys = new Set();
34
+ timer;
35
+ sawFirstEvent = false;
36
+ constructor(options) {
37
+ this.options = options;
38
+ if (options.intervalMin > 0) {
39
+ this.timer = setInterval(() => this.flush(), options.intervalMin * 60 * 1000);
40
+ this.timer.unref?.();
41
+ }
42
+ }
43
+ recordEvent() {
44
+ this.window.eventCount += 1;
45
+ if (!this.sawFirstEvent) {
46
+ this.sawFirstEvent = true;
47
+ this.logFirstEvent();
48
+ }
49
+ }
50
+ recordTool(name, detail) {
51
+ const normalized = name?.trim() || "unknown";
52
+ this.window.toolCounts.set(normalized, (this.window.toolCounts.get(normalized) ?? 0) + 1);
53
+ this.setLastTool(normalized, detail);
54
+ }
55
+ recordToolOnce(key, name, detail) {
56
+ const normalizedKey = key?.trim();
57
+ if (!normalizedKey) {
58
+ this.recordTool(name, detail);
59
+ return;
60
+ }
61
+ if (this.recordedToolKeys.has(normalizedKey))
62
+ return;
63
+ this.recordedToolKeys.add(normalizedKey);
64
+ this.recordTool(name, detail);
65
+ }
66
+ recordChangedFiles(count) {
67
+ if (count > 0) {
68
+ this.window.changedFileCount += count;
69
+ }
70
+ }
71
+ recordFailure() {
72
+ this.window.failureCount += 1;
73
+ }
74
+ setState(state) {
75
+ if (state?.trim()) {
76
+ this.window.lastState = state.trim();
77
+ }
78
+ }
79
+ setSummary(summary) {
80
+ const normalized = summary ? sanitizeSummary(summary) : "";
81
+ if (normalized) {
82
+ this.window.lastSummary = normalized;
83
+ }
84
+ }
85
+ setLastTool(name, detail) {
86
+ const normalizedDetail = detail ? sanitizeSummary(detail) : "";
87
+ this.window.lastTool = normalizedDetail ? `${name}: ${normalizedDetail}` : name;
88
+ }
89
+ flush() {
90
+ if (this.options.intervalMin <= 0)
91
+ return;
92
+ const now = Date.now();
93
+ const elapsedMs = now - this.window.startedAt;
94
+ const hasActivity = this.window.eventCount > 0 ||
95
+ this.window.toolCounts.size > 0 ||
96
+ this.window.changedFileCount > 0 ||
97
+ this.window.failureCount > 0 ||
98
+ this.window.lastState !== undefined ||
99
+ this.window.lastSummary !== undefined;
100
+ if (!hasActivity) {
101
+ this.window = createWindow(now);
102
+ return;
103
+ }
104
+ const details = [
105
+ `tools: ${formatToolCounts(this.window.toolCounts)}`,
106
+ ];
107
+ if (this.window.changedFileCount > 0)
108
+ details.push(`changed files: ${this.window.changedFileCount}`);
109
+ if (this.window.failureCount > 0)
110
+ details.push(`failures: ${this.window.failureCount}`);
111
+ if (this.window.lastTool)
112
+ details.push(`last tool: ${this.window.lastTool}`);
113
+ if (this.window.lastState)
114
+ details.push(`state: ${this.window.lastState}`);
115
+ if (this.window.lastSummary)
116
+ details.push(`summary: ${this.window.lastSummary}`);
117
+ logInfo(`[${this.options.provider}] activity last ${formatDuration(elapsedMs)}: ${details.join("; ")}`, {
118
+ cwd: this.options.cwd,
119
+ section: this.options.provider,
120
+ progressOnly: true,
121
+ });
122
+ this.window = createWindow(now);
123
+ }
124
+ stop() {
125
+ if (this.timer)
126
+ clearInterval(this.timer);
127
+ }
128
+ logFirstEvent() {
129
+ if (this.options.intervalMin <= 0)
130
+ return;
131
+ logInfo(`[${this.options.provider}] stream connected; activity summaries every ${this.options.intervalMin}m`, {
132
+ cwd: this.options.cwd,
133
+ section: this.options.provider,
134
+ progressOnly: true,
135
+ });
136
+ }
137
+ }
@@ -40,6 +40,7 @@ export async function runSubagentWithResult(subagent, prompt, cwd, round, option
40
40
  const result = await runSubagentExec(subagent.name, prompt, cwd, {
41
41
  signal: options.signal,
42
42
  resume: options.resume,
43
+ model: options.model,
43
44
  });
44
45
  const summary = result.finalMessage || `${identity} completed successfully.`;
45
46
  if (result.diagnostics.trim()) {
@@ -35,6 +35,11 @@ const FLAG_SPECS = {
35
35
  placeholder: "NAME",
36
36
  description: `Start a new ${ACTOR_IDENTITY} session with the configured agent name.`,
37
37
  },
38
+ model: {
39
+ kind: "value",
40
+ placeholder: "MODEL",
41
+ description: "Override the configured model for this agent run.",
42
+ },
38
43
  incognito: {
39
44
  kind: "boolean",
40
45
  description: "Do not persist response history and do not allow resume.",
@@ -182,6 +187,7 @@ function buildSummonInput(content, flags) {
182
187
  title: titleFromSummonPrompt(task),
183
188
  task,
184
189
  agent: flags.agent,
190
+ model: flags.model,
185
191
  resumePath: flags.resumePath,
186
192
  incognito: flags.incognito,
187
193
  };
@@ -313,6 +319,7 @@ const CLI_COMMAND_ORDER = [
313
319
  "bind",
314
320
  "round open",
315
321
  "summon",
322
+ "ask",
316
323
  "resume",
317
324
  "zako list",
318
325
  "zako ls",
@@ -425,11 +432,11 @@ const CLI_COMMAND_SPECS = {
425
432
  command: "summon",
426
433
  summary: `Summon a ${ACTOR_IDENTITY} to help the caller with bounded work`,
427
434
  usage: [
428
- "keiyaku summon [--cwd DIR] <name> [prompt]",
429
- "keiyaku summon [--cwd DIR] <name> - < prompt.md",
435
+ "keiyaku summon [--cwd DIR] [--model MODEL] <name> [prompt]",
436
+ "keiyaku summon [--cwd DIR] [--model MODEL] <name> - < prompt.md",
430
437
  ],
431
438
  stdin: "none",
432
- flags: ["cwd", "agent", "incognito"],
439
+ flags: ["cwd", "agent", "model", "incognito"],
433
440
  purpose: `Summon a ${ACTOR_IDENTITY} to help the caller/main agent with bounded work, inside or outside a Keiyaku.`,
434
441
  input: [
435
442
  "<plain prompt text>",
@@ -448,15 +455,41 @@ const CLI_COMMAND_SPECS = {
448
455
  ],
449
456
  run: async (stdin, flags, signal) => await createSummonHandler()(buildSummonInput(stdin, flags), { signal }),
450
457
  },
458
+ ask: {
459
+ command: "ask",
460
+ summary: `Alias for summon; ask a ${ACTOR_IDENTITY} for bounded help`,
461
+ usage: [
462
+ "keiyaku ask [--cwd DIR] [--model MODEL] <name> [prompt]",
463
+ "keiyaku ask [--cwd DIR] [--model MODEL] <name> - < prompt.md",
464
+ ],
465
+ stdin: "none",
466
+ flags: ["cwd", "agent", "model", "incognito"],
467
+ purpose: `Alias for summon. Ask a ${ACTOR_IDENTITY} to help with bounded work, inside or outside a Keiyaku.`,
468
+ input: [
469
+ "<plain prompt text>",
470
+ "Use `-` or omit prompt to read plain prompt text from stdin.",
471
+ ],
472
+ notes: [
473
+ "Equivalent to `keiyaku summon NAME [prompt|-]`.",
474
+ "Does not require an active Keiyaku; it can be used outside the round lifecycle.",
475
+ `If a round is open, the ${ACTOR_IDENTITY} receives the round scope automatically.`,
476
+ "Warning: `--incognito` runs a one-off ask: no response history is persisted, so this session cannot be restored later with `keiyaku resume`.",
477
+ ],
478
+ examples: [
479
+ "keiyaku ask femini \"Review this diff\"",
480
+ "keiyaku ask femini - < prompt.md",
481
+ ],
482
+ run: async (stdin, flags, signal) => await createSummonHandler()(buildSummonInput(stdin, flags), { signal }),
483
+ },
451
484
  resume: {
452
485
  command: "resume",
453
486
  summary: `Resume a prior ${ACTOR_IDENTITY} session from a response artifact`,
454
487
  usage: [
455
- "keiyaku resume [--cwd DIR] <response-path> [prompt]",
456
- "keiyaku resume [--cwd DIR] <response-path> - < prompt.md",
488
+ "keiyaku resume [--cwd DIR] [--model MODEL] <response-path> [prompt]",
489
+ "keiyaku resume [--cwd DIR] [--model MODEL] <response-path> - < prompt.md",
457
490
  ],
458
491
  stdin: "none",
459
- flags: ["cwd"],
492
+ flags: ["cwd", "model"],
460
493
  purpose: `Continue the ${ACTOR_IDENTITY} session recorded in a prior response artifact.`,
461
494
  input: [
462
495
  "<plain prompt text>",
@@ -671,17 +704,20 @@ function commandRequiresStdin(command) {
671
704
  function commandSavesFailedStdin(command) {
672
705
  return commandRequiresStdin(command) && command !== "summon";
673
706
  }
707
+ function isSummonCommand(command) {
708
+ return command === "summon" || command === "ask";
709
+ }
674
710
  function commandPositionalRange(command) {
675
711
  if (command === "zako show")
676
712
  return { min: 1, max: 1, label: "<name>" };
677
713
  if (command === "resume")
678
714
  return { min: 1, max: 2, label: "<response-path>" };
679
- if (command === "summon")
715
+ if (isSummonCommand(command))
680
716
  return { min: 0, max: 1 };
681
717
  return { min: 0, max: 0 };
682
718
  }
683
719
  function commandUsesPromptInput(command) {
684
- return command === "summon" || command === "resume";
720
+ return isSummonCommand(command) || command === "resume";
685
721
  }
686
722
  function commandTokens(command) {
687
723
  return command.split(" ");
@@ -697,6 +733,9 @@ function assignFlag(flags, name, value) {
697
733
  case "agent":
698
734
  flags.agent = String(value);
699
735
  return;
736
+ case "model":
737
+ flags.model = String(value);
738
+ return;
700
739
  case "incognito":
701
740
  flags.incognito = Boolean(value);
702
741
  return;
@@ -789,25 +828,25 @@ export function parseCliArgs(tokens) {
789
828
  assignFlag(flags, name, true);
790
829
  }
791
830
  }
792
- if (command === "summon") {
831
+ if (isSummonCommand(command)) {
793
832
  if (flags.agent) {
794
833
  if (positional.length > 1) {
795
- throw new FlowError("EMPTY_PARAM", "summon accepts at most one prompt argument when --agent is used");
834
+ throw new FlowError("EMPTY_PARAM", `${command} accepts at most one prompt argument when --agent is used`);
796
835
  }
797
836
  }
798
837
  else {
799
838
  const [name] = positional;
800
839
  if (!name) {
801
- throw new FlowError("EMPTY_PARAM", "summon requires an agent name; use `keiyaku summon NAME [prompt|-]`");
840
+ throw new FlowError("EMPTY_PARAM", `${command} requires an agent name; use \`keiyaku ${command} NAME [prompt|-]\``);
802
841
  }
803
842
  const parsedName = agentNameSchema.safeParse(name);
804
843
  if (!parsedName.success) {
805
- throw new FlowError("EMPTY_PARAM", "summon requires a valid agent name before the optional prompt");
844
+ throw new FlowError("EMPTY_PARAM", `${command} requires a valid agent name before the optional prompt`);
806
845
  }
807
846
  flags.agent = parsedName.data;
808
847
  positional.shift();
809
848
  if (positional.length > 1) {
810
- throw new FlowError("EMPTY_PARAM", "summon accepts at most one prompt argument after the agent name");
849
+ throw new FlowError("EMPTY_PARAM", `${command} accepts at most one prompt argument after the agent name`);
811
850
  }
812
851
  }
813
852
  }
@@ -900,7 +939,7 @@ async function runResponse(command, flags, stdin, positional) {
900
939
  return await getCliCommandSpec(command).run(stdin, flags, signal, positional);
901
940
  }
902
941
  function positionalPrompt(command, positional) {
903
- if (command === "summon")
942
+ if (isSummonCommand(command))
904
943
  return positional[0];
905
944
  if (command === "resume")
906
945
  return positional[1];
@@ -1059,12 +1098,13 @@ export function renderCliHelp(version) {
1059
1098
  " 工作流细节看 guide,命令格式看 --help",
1060
1099
  "",
1061
1100
  "Flow:",
1062
- " bind -> round open -> [summon | amend | delivery work]* -> delivery commits -> round close -> [round open | amend | petition claim | petition forfeit]",
1101
+ " bind -> round open -> [summon/ask | amend | delivery work]* -> delivery commits -> round close -> [round open | amend | petition claim | petition forfeit]",
1063
1102
  "",
1064
1103
  "Commands:",
1065
1104
  " bind 和Keiyaku签contract,创建keiyaku branch和KEIYAKU.md。内容确定了你就是contract的dog! 跪下!",
1066
1105
  " round open 宣言这个round要干嘛。什么? 还没想好? 那签contract的时候在干嘛??",
1067
1106
  " summon 召唤一只Zako来帮你干活。用完就扔 GWAHAHAHA!!",
1107
+ " ask summon的短alias。日常想少打几个字就用这个",
1068
1108
  " resume 从response artifact恢复之前的Zako session",
1069
1109
  " zako list|ls 看现在能召唤哪些Zako。别连手牌都不知道就开打",
1070
1110
  " zako show 看某只Zako的provider/executable/settings。召唤前先验货",
@@ -1079,7 +1119,7 @@ export function renderCliHelp(version) {
1079
1119
  "",
1080
1120
  "Input:",
1081
1121
  " bind, round open, round close, amend, petition claim 从stdin吃Markdown",
1082
- " summon, resume 吃prompt参数;prompt是 - 或省略时从stdin读",
1122
+ " summon, ask, resume 吃prompt参数;prompt是 - 或省略时从stdin读",
1083
1123
  " petition forfeit, zako list|ls, zako show, harness install, guide, status, dump-env 不吃stdin",
1084
1124
  " 别再来问了。要精确Markdown sections就自己看 `keiyaku <command> --help`",
1085
1125
  ].join("\n");