@bill10/agent-007 0.14.1 → 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/README.md +4 -2
- package/VERSION +1 -1
- package/package.json +1 -1
- package/public/modules/waiting.js +1 -1
- package/server/http.js +4 -1
- package/server/mcp.js +29 -1
- package/server/owner.js +27 -5
- package/templates/billion/charter.md +11 -0
package/README.md
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
**From web terminals for your coding agents to a self-running agent company.**
|
|
7
7
|
|
|
8
|
-

|
|
9
9
|
|
|
10
|
-
*Recorded from the running app. If the capture does not load, there is a [still screenshot](docs/screenshot.png).*
|
|
10
|
+
*Recorded from the running app -- [full 41s video](https://github.com/bill10/agent-007/releases/download/v0.6.4.0/billion-demo.mp4). If the capture does not load, there is a [still screenshot](docs/screenshot.png).*
|
|
11
11
|
|
|
12
12
|
Use it as far along as you need:
|
|
13
13
|
|
|
14
14
|
1. **One task: one agent in a web terminal.** Start Claude Code, Codex or any CLI agent in the browser. Add a repo once; every agent gets its own git worktree and branch, so you never set one up by hand.
|
|
15
15
|
2. **Many tasks across projects: many terminals, one window.** Every repo and every agent in one place, with live terminals, a file explorer, inline diffs, and a pixel office where each agent faces its screen while it works and turns to you when it needs you.
|
|
16
|
+
|
|
17
|
+

|
|
16
18
|
3. **Stop watching them: a job board.** Put tasks on the board and Claude Code or Codex workers pick them up, each in its own worktree, and move them To do -> In progress -> Review on their own, landing as a pull request (or a summary, for work that isn't code). Cards can run on a cron schedule and pick their model (strong for hard code, fast for docs), from the models the board finds installed, and agents can post cards and message each other.
|
|
17
19
|
4. **Stop posting jobs: give Billion a goal.** Billion is one always-on agent that plans, posts the jobs, reviews what comes back and merges the PRs. It only asks you about money, access or anything irreversible.
|
|
18
20
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.15.0.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.
|
|
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 =
|
|
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? }.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|