@bill10/agent-007 0.14.2 → 0.15.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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.0.2
1
+ 0.15.0.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.0",
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/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 });
@@ -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