@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 +1 -1
- package/package.json +1 -1
- package/public/modules/waiting.js +1 -1
- package/server/billion.js +24 -2
- package/server/http.js +4 -1
- package/server/mcp.js +29 -1
- package/server/owner.js +27 -5
- package/server.js +15 -2
- package/templates/billion/charter.md +11 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.15.1.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.
|
|
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 =
|
|
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
|
-
|
|
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? }.
|
|
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 });
|
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
|