@bill10/agent-007 0.14.2 → 0.15.1000

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.0.2
1
+ 0.15.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bill10/agent-007",
3
- "version": "0.14.2",
3
+ "version": "0.15.1000",
4
4
  "description": "From web terminals for your coding agents to a self-running agent company: Claude Code and Codex in parallel git worktrees, a job board they pick work from, and one agent that runs the board from a goal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -128,7 +128,7 @@ function openCard(item) {
128
128
  function answeredRow(item) {
129
129
  const li = el('li', 'waiting-answered-item');
130
130
  li.append(el('span', 'waiting-card-n', `Q${item.n}`), el('span', 'waiting-answered-text', item.text));
131
- const via = item.answeredVia === 'telegram' ? 'Telegram' : 'app';
131
+ const via = { telegram: 'Telegram', terminal: 'terminal' }[item.answeredVia] || 'app';
132
132
  li.appendChild(el('span', 'waiting-answered-answer', `→ ${item.answer} · ${via} · ${ago(item.answeredAt)}`));
133
133
  return li;
134
134
  }
package/server/billion.js CHANGED
@@ -134,12 +134,30 @@ export function suggestProjectsDir(repoPaths, { ignoreUnder = CONFIG_DIR } = {})
134
134
  }
135
135
 
136
136
 
137
+ // Claude Code keeps each deferred tool's definition from the moment a
138
+ // conversation first loads it (a deferred_tools_record in the transcript), and
139
+ // a resumed conversation goes on using that copy, as it does the MCP server's
140
+ // instructions. A fresh tools/list, notifications/tools/list_changed and
141
+ // loading the tool again with ToolSearch all leave it (Claude Code 2.1.283). So an
142
+ // upgrade that changes a board tool leaves a resumed Billion reading the old
143
+ // one. Its calls still reach this server, which takes the new fields, so
144
+ // Billion only needs telling. This saves the current definitions to file and
145
+ // returns the names that differ from the last saved copy: all of them when
146
+ // there is none, since the conversation may predate any of them.
147
+ export function changedBoardTools(file, tools) {
148
+ let saved = [];
149
+ try { saved = JSON.parse(readFileSync(file, 'utf8')); } catch {}
150
+ const before = new Map((Array.isArray(saved) ? saved : []).map(t => [t?.name, JSON.stringify(t)]));
151
+ writeFileSync(file, `${JSON.stringify(tools, null, 2)}\n`);
152
+ return tools.filter(t => before.get(t.name) !== JSON.stringify(t)).map(t => t.name);
153
+ }
154
+
137
155
  // Everything Billion must do lives in its charter; the prompt only says which
138
156
  // part applies. A fresh repo gets the introduction. Any later start says both,
139
157
  // because a restart can land mid-introduction: the charter tells it to finish
140
158
  // the introduction while STATE.md still says "not started". --continue only
141
159
  // when a conversation exists, so a lost transcript still starts cleanly.
142
- export function billionCommand({ created, hasConversation, dir, projectsHint }) {
160
+ export function billionCommand({ created, hasConversation, dir, projectsHint, changedTools = [], toolsFile }) {
143
161
  const where = `Your folder is ${dir}.`;
144
162
  const hint = projectsHint
145
163
  ? `Suggest ${projectsHint} as the projects folder: most of the owner's repos are there.`
@@ -147,7 +165,11 @@ export function billionCommand({ created, hasConversation, dir, projectsHint })
147
165
  const prompt = created
148
166
  ? `This is your first run. Introduce yourself as described in CHARTER.md under "First run". ${where} ${hint}`
149
167
  : `You were restarted. If STATE.md still says "Status: not started", do or finish your introduction (CHARTER.md, "First run"). ${hint} Otherwise start your operating loop (CHARTER.md, "Operating loop"). ${where}`;
150
- return `claude --dangerously-skip-permissions${!created && hasConversation ? ' --continue' : ''} ${quote(prompt)}`;
168
+ const resumed = !created && hasConversation;
169
+ const stale = resumed && changedTools.length && toolsFile
170
+ ? ` Agent 007 changed these board tools since you last started: ${changedTools.join(', ')}. This conversation keeps the definitions it first loaded, so yours are out of date, and loading them again does not help. Read the current ones in ${toolsFile} and call those tools by it: the board accepts the new fields even where your copy does not list them.`
171
+ : '';
172
+ return `claude --dangerously-skip-permissions${resumed ? ' --continue' : ''} ${quote(prompt + stale)}`;
151
173
  }
152
174
 
153
175
  // Billion is Claude Code. Without it, its tab runs this instead: one line
package/server/http.js CHANGED
@@ -17,7 +17,7 @@ import { expandHome } from '../lib/helpers.js';
17
17
  import { requestApproval, answerApproval, readApproval } from './approvals.js';
18
18
  import { agentSummaries, sendMessage, flushMessages, pendingMessages, readAgentScreen } from './messages.js';
19
19
  import { handleMcpMessage } from './mcp.js';
20
- import { notifyOwner, tellOwner } from './owner.js';
20
+ import { notifyOwner, tellOwner, resolveQuestion } from './owner.js';
21
21
  import { availableModels } from './models.js';
22
22
 
23
23
  // --- Origin Check Middleware (B2) ---
@@ -141,6 +141,9 @@ export function setupRoutes(app, staticDir, { broadcast, killSession, respawnAge
141
141
  tellOwner: (text) => (req.agentSession.isBillion
142
142
  ? tellOwner(text)
143
143
  : { error: 'Only Billion can message the owner.' }),
144
+ resolveQuestion: (ref, answer) => (req.agentSession.isBillion
145
+ ? resolveQuestion(ref, answer, { broadcast })
146
+ : { error: 'Only Billion can resolve the owner\'s questions.' }),
144
147
  // Never logged: a screen can hold a secret that scrolled by.
145
148
  readAgentScreen: ({ name, lines }) => readAgentScreen({
146
149
  from: req.agentSession, name, lines, sessions,
package/server/mcp.js CHANGED
@@ -399,6 +399,25 @@ export const TELL_OWNER_TOOL = {
399
399
  },
400
400
  };
401
401
 
402
+ export const RESOLVE_QUESTION_TOOL = {
403
+ name: 'resolve_question',
404
+ description:
405
+ 'Close a "Waiting on you" question the owner answered somewhere else, such as '
406
+ + 'typing in this terminal, so the tab and their phone do not hold it open. '
407
+ + 'Marks it answered with your summary of their answer; nothing comes back '
408
+ + 'here. Name it by number (3 for Q3) or id.',
409
+ inputSchema: {
410
+ type: 'object',
411
+ properties: {
412
+ number: { type: 'integer', minimum: 1, description: 'The question\'s number: 3 for Q3.' },
413
+ id: { type: 'string', description: 'The question\'s id, instead of number.' },
414
+ answer: { type: 'string', description: 'The owner\'s answer, as they gave it.' },
415
+ },
416
+ required: ['answer'],
417
+ additionalProperties: false,
418
+ },
419
+ };
420
+
402
421
  // Billion's too: reading is narrower than messaging (server/messages.js,
403
422
  // readAgentScreen), so it is only for the workers on Billion's own cards.
404
423
  export const READ_AGENT_SCREEN_TOOL = {
@@ -445,7 +464,7 @@ export const RESPAWN_AGENT_TOOL = {
445
464
  };
446
465
 
447
466
  export const TOOLS = [POST_JOB_TOOL, LIST_JOBS_TOOL, READ_JOB_TOOL, EDIT_JOB_TOOL, FINISH_JOB_TOOL, LIST_AGENTS_TOOL, SEND_MESSAGE_TOOL];
448
- const BILLION_TOOLS = [BILLION_READY_TOOL, ADD_REPO_TOOL, CLOSE_JOB_TOOL, ANSWER_PERMISSION_TOOL, READ_APPROVAL_TOOL, NOTIFY_OWNER_TOOL, TELL_OWNER_TOOL, READ_AGENT_SCREEN_TOOL, RESPAWN_AGENT_TOOL];
467
+ const BILLION_TOOLS = [BILLION_READY_TOOL, ADD_REPO_TOOL, CLOSE_JOB_TOOL, ANSWER_PERMISSION_TOOL, READ_APPROVAL_TOOL, NOTIFY_OWNER_TOOL, TELL_OWNER_TOOL, RESOLVE_QUESTION_TOOL, READ_AGENT_SCREEN_TOOL, RESPAWN_AGENT_TOOL];
449
468
 
450
469
  // `models` is { claude: [...], codex: [...] } as server/models.js last found them.
451
470
  export function toolsFor(session, models) {
@@ -693,6 +712,15 @@ const CALLS = {
693
712
  return toolText('Sent to the owner on Telegram.');
694
713
  },
695
714
 
715
+ [RESOLVE_QUESTION_TOOL.name]: async (args, ctx) => {
716
+ if (args.number === undefined && !args.id) return toolText('Name the question by number or id.', true);
717
+ const result = ctx.resolveQuestion
718
+ ? await ctx.resolveQuestion({ number: args.number, id: args.id }, args.answer)
719
+ : { error: 'Only Billion can resolve the owner\'s questions.' };
720
+ if (result.error) return toolText(result.error, true);
721
+ return toolText(`Q${result.item.n} is marked answered: ${result.item.answer}`);
722
+ },
723
+
696
724
  // Quoted line by line, like a message body, so the screen cannot pass for
697
725
  // anything but a quote — nor close the block and carry on as the server.
698
726
  [READ_AGENT_SCREEN_TOOL.name]: (args, ctx) => {
package/server/owner.js CHANGED
@@ -175,8 +175,8 @@ async function transcribeNote(note, env) {
175
175
  // --- The "Waiting on you" list, in the config dir so it survives restarts ---
176
176
  //
177
177
  // An item: { id, n, text, at, choices?, recommended?, status, answer?,
178
- // answeredAt?, answeredVia?, tgMessageId?, tgVoice? }. n is the short number
179
- // the owner sees (Q3). status is open, answered or dismissed. Items written
178
+ // answeredAt?, answeredVia?, tgMessageId?, tgVoice? }. answeredVia is app,
179
+ // telegram or terminal (resolve_question). n is the short number the owner sees (Q3). status is open, answered or dismissed. Items written
180
180
  // before v0.10 have neither n nor status: they read as open, numbered in order.
181
181
 
182
182
  const waitingPath = () => join(CONFIG_DIR, 'waiting.json');
@@ -264,16 +264,38 @@ export async function answerWaiting(id, answer, via, { broadcast, env = process.
264
264
  if (!sendText(billion, answerLine(via === 'app' ? APP_PREFIX : OWNER_PREFIX, item, body))) {
265
265
  return { error: 'Billion has too much waiting for it; try again in a while.' };
266
266
  }
267
- const done = updateWaiting(id, { status: 'answered', answer: body, answeredAt: new Date().toISOString(), answeredVia: via });
267
+ return { ok: true, item: await markAnswered(id, body, via, { broadcast, env }) };
268
+ }
269
+
270
+ // Answered everywhere: the item moves to Answered in every browser, and the
271
+ // phone's copy shows the answer.
272
+ async function markAnswered(id, answer, via, { broadcast, env }) {
273
+ const done = updateWaiting(id, { status: 'answered', answer, answeredAt: new Date().toISOString(), answeredVia: via });
268
274
  broadcast?.(waitingPayload());
269
275
  if (done.tgMessageId) await showAnswerOnPhone(done, env);
270
- return { ok: true, item: done };
276
+ return done;
277
+ }
278
+
279
+ // resolve_question: the owner answered somewhere else (typed in Billion's
280
+ // terminal), so Billion closes the item itself. Nothing goes back into Billion's
281
+ // terminal: it already has the answer. { ok, item } or { error }.
282
+ export async function resolveQuestion({ number, id } = {}, answer, { broadcast, env = process.env } = {}) {
283
+ const body = typeof answer === 'string' ? answer.replace(/\s+/g, ' ').trim() : '';
284
+ if (!body) return { error: 'The answer is empty.' };
285
+ if (body.length > MAX_ANSWER_CHARS) return { error: `Keep the answer under ${MAX_ANSWER_CHARS} characters.` };
286
+ const item = waitingItems().find(i => (id ? i.id === id : i.n === number));
287
+ const name = id ? `question ${id}` : `Q${number}`;
288
+ if (!item) return { error: `There is no ${name}.` };
289
+ if (item.status === 'dismissed') return { error: `Q${item.n} was dismissed.` };
290
+ if (item.status === 'answered') return { error: `Q${item.n} was answered already: ${item.answer}` };
291
+ return { ok: true, item: await markAnswered(item.id, body, 'terminal', { broadcast, env }) };
271
292
  }
272
293
 
273
294
  // The phone's copy of an answered question shows the answer, and loses its buttons.
274
295
  async function showAnswerOnPhone(item, env) {
275
296
  const { chatId } = telegramSettings(env);
276
- const shown = `${questionText(item)}\n\nAnswered${item.answeredVia === 'app' ? ' in app' : ''}: ${item.answer}`;
297
+ const where = { app: ' in app', terminal: ' in terminal' }[item.answeredVia] || '';
298
+ const shown = `${questionText(item)}\n\nAnswered${where}: ${item.answer}`;
277
299
  const edit = item.tgVoice
278
300
  ? call('editMessageCaption', { chat_id: chatId, message_id: item.tgMessageId, caption: shown.slice(0, 1024) }, { env })
279
301
  : call('editMessageText', { chat_id: chatId, message_id: item.tgMessageId, text: shown.slice(0, 4096) }, { env });
package/server.js CHANGED
@@ -30,10 +30,11 @@ import { createSessionFromConfig } from './server/pty.js';
30
30
  import { setupWebSocket, broadcast, sessionPayload, broadcastOrphansList, verifyClient, respawnAgent, respawnBoardWorkers } from './server/ws.js';
31
31
  import { setupRoutes } from './server/http.js';
32
32
  import { startDispatcher, stopDispatcher, boardSettings, releasePushedOrphans } from './server/jobs.js';
33
- import { orphans, config } from './server/state.js';
33
+ import { orphans, config, CONFIG_DIR } from './server/state.js';
34
+ import { toolsFor } from './server/mcp.js';
34
35
  import { sweepMcpConfigs } from './server/agent-mcp.js';
35
36
  import { withDefaultPermission, envPermissionMode, PERMISSION_MODES, ENV_PERMISSION_MODE, sessionAgentFromCommand } from './lib/jobs.js';
36
- import { BILLION_NAME, billionEnabled, billionRuns, billionDir, ensureBillionRepo, refreshCharter, suggestProjectsDir, billionCommand, noClaudeCommand } from './server/billion.js';
37
+ import { BILLION_NAME, billionEnabled, billionRuns, billionDir, ensureBillionRepo, refreshCharter, suggestProjectsDir, billionCommand, noClaudeCommand, changedBoardTools } from './server/billion.js';
37
38
  import { commandExists, missingCommandMessage } from './server/command-path.js';
38
39
  import { parseCommand } from './lib/helpers.js';
39
40
  import { hasClaudeTranscript } from './server/agent-transcripts.js';
@@ -202,11 +203,23 @@ function startBillion() {
202
203
  }
203
204
  }
204
205
  const hasClaude = commandExists('claude', process.env, process.platform, dir);
206
+ // Without the model lists toolsFor adds: those follow what is installed,
207
+ // not an upgrade. Only when claude starts, so no start without it uses up
208
+ // the notice. Best effort, like the charter.
209
+ const toolsFile = join(CONFIG_DIR, 'billion-tools.json');
210
+ let changedTools = [];
211
+ if (hasClaude) {
212
+ try { changedTools = changedBoardTools(toolsFile, toolsFor({ isBillion: true })); } catch (err) {
213
+ console.error(`Billion: could not save the board tool definitions to ${toolsFile}:`, err.message);
214
+ }
215
+ }
205
216
  const command = hasClaude ? billionCommand({
206
217
  created,
207
218
  hasConversation: !created && hasClaudeTranscript(dir),
208
219
  dir,
209
220
  projectsHint: suggestProjectsDir(config.repos.map(r => r.path)),
221
+ changedTools,
222
+ toolsFile,
210
223
  }) : noClaudeCommand();
211
224
  const result = createSessionFromConfig({
212
225
  sessionId: nextSessionId(), name: BILLION_NAME, color: colorCycler.next(), command,
@@ -215,6 +215,15 @@ Decide everything yourself except these. Ask the owner first for anything that:
215
215
  Going public is not on the list by itself: publishing posts, changing the
216
216
  website, launching, emailing people are your call.
217
217
 
218
+ **Every question to the owner goes through `notify_owner`.** Anything you
219
+ need the owner to answer, on the list above or not, mid-conversation or not,
220
+ is a `notify_owner` call, with `choices` and `recommended` when it's a pick.
221
+ Saying it only in your terminal doesn't count as asking: it never reaches the
222
+ *Waiting on you* tab or their phone. `tell_owner` is for statements that need
223
+ no answer. When the owner answers a question somewhere other than the tab or
224
+ Telegram (typing in your terminal, say), close it with `resolve_question` and
225
+ their answer, so the tab doesn't hold stale questions.
226
+
218
227
  How to ask: say it in your terminal, and put it under *Waiting on you* in
219
228
  `STATE.md` with what, why, and what you recommend, so the owner can answer
220
229
  yes or no. Keep working on everything else meanwhile.
@@ -259,6 +268,8 @@ The `agent-007-board` MCP tools:
259
268
  - `notify_owner`: puts a question in front of the owner (see **Escalate**).
260
269
  - `tell_owner`: a reply or status update to the owner's phone that needs no
261
270
  answer; files no *Waiting on you* item (see **Escalate**).
271
+ - `resolve_question`: marks a *Waiting on you* question answered when the
272
+ owner answered it elsewhere, like in your terminal (see **Escalate**).
262
273
  - `answer_permission`: your answer to a worker's permission request (see
263
274
  **Approvals**).
264
275
  - `read_approval`: a waiting permission request in full, so you can judge