@phuetz/code-buddy 1.3.1 → 1.4.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 +23 -4
- package/dist/agent/autonomous/agentic-coding-runner.d.ts +2 -2
- package/dist/agent/autonomous/agentic-coding-runner.js +24 -24
- package/dist/agent/autonomous/checkpoint-manager.d.ts +2 -2
- package/dist/agent/base-agent.d.ts +1 -1
- package/dist/agent/base-agent.js +1 -1
- package/dist/agent/execution/tool-selection-strategy.js +4 -1
- package/dist/agent/hermes-tool-parity-local.js +2 -2
- package/dist/agent/tool-handler.js +2 -2
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +4 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +34 -13
- package/dist/codebuddy/providers/provider-openai-compat.js +8 -0
- package/dist/codebuddy/tool-definitions/code-explorer-tools.d.ts +8 -0
- package/dist/codebuddy/tool-definitions/code-explorer-tools.js +24 -0
- package/dist/codebuddy/tool-definitions/graph-tools.d.ts +1 -1
- package/dist/codebuddy/tool-definitions/graph-tools.js +1 -1
- package/dist/codebuddy/tool-definitions/index.d.ts +1 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -2
- package/dist/codebuddy/tools.d.ts +2 -2
- package/dist/codebuddy/tools.js +28 -13
- package/dist/collaboration/ai-colab-manager.js +9 -4
- package/dist/commands/cli/code-explorer-commands.d.ts +7 -0
- package/dist/commands/cli/code-explorer-commands.js +40 -0
- package/dist/commands/cli/native-engine-commands.js +64 -0
- package/dist/commands/handlers/channel-handlers.js +39 -22
- package/dist/commands/handlers/graph-handlers.d.ts +1 -1
- package/dist/commands/handlers/graph-handlers.js +1 -1
- package/dist/config/model-tools.js +1 -1
- package/dist/daemon/autonomous-loop.d.ts +0 -7
- package/dist/daemon/autonomous-loop.js +24 -14
- package/dist/fleet/capability-registry.js +9 -1
- package/dist/fleet/colab-store.d.ts +109 -3
- package/dist/fleet/colab-store.js +181 -6
- package/dist/index.js +28 -11
- package/dist/input/text-to-speech.js +16 -1
- package/dist/kanban/colab-kanban-adapter.d.ts +31 -0
- package/dist/kanban/colab-kanban-adapter.js +232 -0
- package/dist/knowledge/community-detector.d.ts +1 -1
- package/dist/knowledge/community-detector.js +1 -1
- package/dist/knowledge/process-detector.d.ts +1 -1
- package/dist/knowledge/process-detector.js +1 -1
- package/dist/plugins/{gitnexus/GitNexusMCPClient.d.ts → code-explorer/CodeExplorerMCPClient.d.ts} +9 -9
- package/dist/plugins/{gitnexus/GitNexusMCPClient.js → code-explorer/CodeExplorerMCPClient.js} +17 -17
- package/dist/plugins/{gitnexus/GitNexusManager.d.ts → code-explorer/CodeExplorerManager.d.ts} +16 -16
- package/dist/plugins/{gitnexus/GitNexusManager.js → code-explorer/CodeExplorerManager.js} +35 -35
- package/dist/plugins/code-explorer/index.d.ts +9 -0
- package/dist/plugins/code-explorer/index.js +8 -0
- package/dist/providers/provider-catalog.js +23 -1
- package/dist/services/prompt-builder.js +9 -9
- package/dist/skills/parser.js +1 -1
- package/dist/tools/{gitnexus-tool.d.ts → code-explorer-tool.d.ts} +9 -9
- package/dist/tools/{gitnexus-tool.js → code-explorer-tool.js} +16 -16
- package/dist/tools/metadata.js +3 -3
- package/dist/tools/registry/code-explorer-tools.d.ts +18 -0
- package/dist/tools/registry/{gitnexus-tools.js → code-explorer-tools.js} +14 -14
- package/dist/tools/registry/graph-tools.d.ts +1 -1
- package/dist/tools/registry/graph-tools.js +1 -1
- package/dist/tools/registry/index.d.ts +1 -1
- package/dist/tools/registry/index.js +6 -6
- package/dist/tools/registry/kanban-tools.d.ts +1 -1
- package/dist/tools/registry/kanban-tools.js +2 -2
- package/dist/voice/local-whisper.d.ts +9 -0
- package/dist/voice/local-whisper.js +93 -0
- package/package.json +1 -1
- package/dist/codebuddy/tool-definitions/gitnexus-tools.d.ts +0 -8
- package/dist/codebuddy/tool-definitions/gitnexus-tools.js +0 -24
- package/dist/commands/cli/gitnexus-commands.d.ts +0 -7
- package/dist/commands/cli/gitnexus-commands.js +0 -42
- package/dist/plugins/gitnexus/index.d.ts +0 -9
- package/dist/plugins/gitnexus/index.js +0 -8
- package/dist/tools/registry/gitnexus-tools.d.ts +0 -18
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeExplorer CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Exposes commands to consult CodeExplorer and push session summaries.
|
|
5
|
+
*/
|
|
6
|
+
import { CodeExplorerTool } from '../../tools/code-explorer-tool.js';
|
|
7
|
+
export function registerCodeExplorerCommands(program) {
|
|
8
|
+
const codeExplorerCmd = program
|
|
9
|
+
.command('code-explorer')
|
|
10
|
+
.description('Interact with CodeExplorer for code understanding and session syncing');
|
|
11
|
+
codeExplorerCmd.command('ask')
|
|
12
|
+
.description('Consult CodeExplorer for a query or code understanding request')
|
|
13
|
+
.argument('<query>', 'The query or task description to ask CodeExplorer about')
|
|
14
|
+
.action(async (query) => {
|
|
15
|
+
try {
|
|
16
|
+
const codeExplorer = new CodeExplorerTool();
|
|
17
|
+
const result = await codeExplorer.ask(query);
|
|
18
|
+
console.log(JSON.stringify(result, null, 2));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Error querying CodeExplorer:', error);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
codeExplorerCmd.command('push-session')
|
|
26
|
+
.description('Push the session summary to CodeExplorer as technical memory')
|
|
27
|
+
.argument('<summary>', 'The session summary to push')
|
|
28
|
+
.action(async (summary) => {
|
|
29
|
+
try {
|
|
30
|
+
const codeExplorer = new CodeExplorerTool();
|
|
31
|
+
const result = await codeExplorer.pushSession(summary);
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Error pushing session to CodeExplorer:', error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=code-explorer-commands.js.map
|
|
@@ -939,6 +939,70 @@ export function registerFleetAutonomyCommands(program) {
|
|
|
939
939
|
const goalNote = task.goalMode ? ` goal-mode(${task.goalMaxTurns ?? 5} turns)` : '';
|
|
940
940
|
console.log(`Added task ${task.id} [${task.priority}]${goalNote}${task.dependsOn ? ` depends on ${task.dependsOn.join(', ')}` : ''}`);
|
|
941
941
|
});
|
|
942
|
+
tasks
|
|
943
|
+
.command('board')
|
|
944
|
+
.description('Render the unified fleet board as Hermes-style columns (To Do / In Progress / Review / Done)')
|
|
945
|
+
.option('--dir <path>', 'colab dir')
|
|
946
|
+
.option('--json', 'output JSON')
|
|
947
|
+
.action(async (opts) => {
|
|
948
|
+
const { FleetColabStore } = await import('../../fleet/colab-store.js');
|
|
949
|
+
const store = new FleetColabStore({ ...(opts.dir ? { dir: opts.dir } : {}) });
|
|
950
|
+
const all = store.listTasks();
|
|
951
|
+
const columns = [
|
|
952
|
+
{ key: 'open', label: 'To Do' },
|
|
953
|
+
{ key: 'in_progress', label: 'In Progress' },
|
|
954
|
+
{ key: 'blocked', label: 'Review' },
|
|
955
|
+
{ key: 'completed', label: 'Done' },
|
|
956
|
+
];
|
|
957
|
+
if (opts.json) {
|
|
958
|
+
const grouped = Object.fromEntries(columns.map((c) => [c.label, all.filter((t) => t.status === c.key)]));
|
|
959
|
+
console.log(JSON.stringify(grouped, null, 2));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
for (const col of columns) {
|
|
963
|
+
const items = all.filter((t) => t.status === col.key);
|
|
964
|
+
console.log(`\n${col.label} (${items.length})`);
|
|
965
|
+
if (items.length === 0) {
|
|
966
|
+
console.log(' —');
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
for (const t of items) {
|
|
970
|
+
const annotations = [];
|
|
971
|
+
if (t.claimedBy)
|
|
972
|
+
annotations.push(`@${t.claimedBy}`);
|
|
973
|
+
if (t.attempts)
|
|
974
|
+
annotations.push(`attempts:${t.attempts}`);
|
|
975
|
+
if (t.dependsOn?.length)
|
|
976
|
+
annotations.push(`deps:${t.dependsOn.length}`);
|
|
977
|
+
if (t.goalMode)
|
|
978
|
+
annotations.push('goal');
|
|
979
|
+
if (col.key === 'blocked' && t.blockedReason)
|
|
980
|
+
annotations.push(`(${t.blockedReason})`);
|
|
981
|
+
const suffix = annotations.length ? ` — ${annotations.join(' ')}` : '';
|
|
982
|
+
console.log(` ${t.id} [${t.priority}] ${t.title}${suffix}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
tasks
|
|
987
|
+
.command('import-kanban')
|
|
988
|
+
.description('Migrate cards from a legacy kanban-board.json into the unified fleet board')
|
|
989
|
+
.option('--board <path>', 'source kanban-board.json (default <cwd>/.codebuddy/kanban-board.json)')
|
|
990
|
+
.option('--dir <path>', 'target colab dir')
|
|
991
|
+
.option('--json', 'output JSON')
|
|
992
|
+
.action(async (opts) => {
|
|
993
|
+
const { KanbanStore } = await import('../../kanban/kanban-store.js');
|
|
994
|
+
const { FleetColabStore } = await import('../../fleet/colab-store.js');
|
|
995
|
+
const { importKanbanCards } = await import('../../kanban/colab-kanban-adapter.js');
|
|
996
|
+
const source = new KanbanStore(opts.board ? { boardPath: opts.board } : {});
|
|
997
|
+
const cards = await source.listCards({ includeArchived: true, includeDone: true });
|
|
998
|
+
const store = new FleetColabStore({ ...(opts.dir ? { dir: opts.dir } : {}) });
|
|
999
|
+
const { imported, skipped } = importKanbanCards(cards, store);
|
|
1000
|
+
if (opts.json) {
|
|
1001
|
+
console.log(JSON.stringify({ imported, skipped }, null, 2));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
console.log(`Imported ${imported.length} card(s) into the unified board, skipped ${skipped.length} (archived or already present).`);
|
|
1005
|
+
});
|
|
942
1006
|
fleet
|
|
943
1007
|
.command('swarm <goal>')
|
|
944
1008
|
.description('Create a workers → verifier → synthesizer task graph')
|
|
@@ -327,13 +327,8 @@ export async function registerAIMessageHandler(manager) {
|
|
|
327
327
|
aiHandlerRegistered = true;
|
|
328
328
|
manager.onMessage(async (message, channel) => {
|
|
329
329
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
logger.warn('No API key for channel AI responses');
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const { checkDMPairing, getDMPairing, getRouteAgentConfig } = await import('../../channels/core.js');
|
|
336
|
-
// 1. Check DM pairing first
|
|
330
|
+
// 1. DM pairing gate — unapproved senders get a code, then we stop.
|
|
331
|
+
const { checkDMPairing, getDMPairing } = await import('../../channels/core.js');
|
|
337
332
|
const pairingStatus = await checkDMPairing(message);
|
|
338
333
|
if (!pairingStatus.approved) {
|
|
339
334
|
if (pairingStatus.code) {
|
|
@@ -347,14 +342,35 @@ export async function registerAIMessageHandler(manager) {
|
|
|
347
342
|
}
|
|
348
343
|
return;
|
|
349
344
|
}
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
345
|
+
// Nothing to answer (e.g. a non-text message with no transcription).
|
|
346
|
+
if (!message.content || !message.content.trim()) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// 2. Context-adaptive agent reply (« comme Claude »): the agent's own
|
|
350
|
+
// query-classifier + buildForQuery scale the system prompt to the
|
|
351
|
+
// request (a greeting → minimal ~800B prompt, NOT the 73KB legacy),
|
|
352
|
+
// and tools load on demand — RAG selects only the relevant ~15 and the
|
|
353
|
+
// `tool_search` meta-tool pulls more when actually needed. Bounded
|
|
354
|
+
// rounds keep a simple chat fast while a real task can still act.
|
|
355
|
+
const { resolveProviderFromEnv } = await import('../../fleet/peer-chat-client-factory.js');
|
|
356
|
+
const knownProviders = ['ollama', 'chatgpt', 'gemini', 'grok', 'anthropic'];
|
|
357
|
+
const preferredProvider = process.env.CODEBUDDY_PROVIDER && knownProviders.includes(process.env.CODEBUDDY_PROVIDER)
|
|
358
|
+
? process.env.CODEBUDDY_PROVIDER
|
|
359
|
+
: 'auto';
|
|
360
|
+
const resolved = resolveProviderFromEnv(preferredProvider);
|
|
361
|
+
if (!resolved) {
|
|
362
|
+
logger.warn('No LLM provider for channel chat — set CODEBUDDY_PROVIDER + a provider key/env');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const { getRouteAgentConfig } = await import('../../channels/core.js');
|
|
353
366
|
const { CodeBuddyAgent } = await import('../../agent/codebuddy-agent.js');
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const agent = new CodeBuddyAgent(apiKey,
|
|
357
|
-
//
|
|
367
|
+
const agentConfig = getRouteAgentConfig(message);
|
|
368
|
+
const model = agentConfig.model || resolved.model;
|
|
369
|
+
const agent = new CodeBuddyAgent(resolved.apiKey || 'local', resolved.baseUrl, model, agentConfig.maxToolRounds ?? 6, // bounded (vs the 50-round default)
|
|
370
|
+
true, // useRAGToolSelection — relevant tools on demand, not all ~194
|
|
371
|
+
process.env.CODEBUDDY_CHANNEL_PROMPT_ID || 'auto', // minimal/adaptive prompt, not the 73KB legacy
|
|
372
|
+
process.cwd());
|
|
373
|
+
// Multi-turn: restore prior session history into the agent.
|
|
358
374
|
const sessionKey = message.sessionKey || 'default-global';
|
|
359
375
|
const sessionStore = agent.getSessionStore();
|
|
360
376
|
let session = await sessionStore.loadSession(sessionKey);
|
|
@@ -371,18 +387,16 @@ export async function registerAIMessageHandler(manager) {
|
|
|
371
387
|
await sessionStore.saveSession(session);
|
|
372
388
|
}
|
|
373
389
|
await sessionStore.resumeSession(sessionKey);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const messages = activeSession.messages.map(m => ({
|
|
390
|
+
if (session.messages && session.messages.length > 0) {
|
|
391
|
+
const chatHistory = sessionStore.convertMessagesToChatEntries(session.messages);
|
|
392
|
+
const priorMessages = session.messages.map((m) => ({
|
|
378
393
|
role: m.type === 'user' ? 'user' : 'assistant',
|
|
379
|
-
content: m.content
|
|
394
|
+
content: m.content,
|
|
380
395
|
}));
|
|
381
396
|
const historyRestorer = agent;
|
|
382
397
|
historyRestorer.historyManager.setChatHistory(chatHistory);
|
|
383
|
-
historyRestorer.historyManager.setMessages(
|
|
398
|
+
historyRestorer.historyManager.setMessages(priorMessages);
|
|
384
399
|
}
|
|
385
|
-
// 5. Run agent turn
|
|
386
400
|
const entries = await agent.processUserMessage(message.content);
|
|
387
401
|
const lastEntry = entries[entries.length - 1];
|
|
388
402
|
const response = lastEntry ? String(lastEntry.content) : '';
|
|
@@ -412,7 +426,10 @@ export async function instantiateChannel(config) {
|
|
|
412
426
|
switch (config.type) {
|
|
413
427
|
case 'telegram': {
|
|
414
428
|
const { TelegramChannel } = await import('../../channels/telegram/index.js');
|
|
415
|
-
|
|
429
|
+
// TelegramChannel reads `config.token` (client.ts) — pass `token`, not
|
|
430
|
+
// `botToken`, or it throws "Telegram bot token is required" and the
|
|
431
|
+
// channel never starts from channels.json / server intake.
|
|
432
|
+
return new TelegramChannel({ token: config.token || '', ...opts });
|
|
416
433
|
}
|
|
417
434
|
case 'discord': {
|
|
418
435
|
const { DiscordChannel } = await import('../../channels/discord/index.js');
|
|
@@ -147,7 +147,7 @@ const DEFAULT_MODEL_CONFIGS = [
|
|
|
147
147
|
},
|
|
148
148
|
// ChatGPT Codex backend (Phase d.23) — exposed via OAuth subscription
|
|
149
149
|
// auth at chatgpt.com/backend-api/codex/responses. `gpt-5.5` matches the
|
|
150
|
-
//
|
|
150
|
+
// CodeExplorer helper default; `gpt-5.2` remains the known-good fallback.
|
|
151
151
|
{
|
|
152
152
|
model: 'gpt-5.5*',
|
|
153
153
|
supportsReasoning: true,
|
|
@@ -82,13 +82,6 @@ export declare class FleetAutonomousLoop {
|
|
|
82
82
|
private readonly executor;
|
|
83
83
|
private readonly policy;
|
|
84
84
|
private readonly enabled;
|
|
85
|
-
/**
|
|
86
|
-
* Per-task consecutive-failure counts (in-memory, this run). Fed to
|
|
87
|
-
* {@link chooseAutonomousModel} so a task that keeps failing on the cheap tier
|
|
88
|
-
* escalates to a stronger model (policy `escalateAfterFailures`). Cleared on
|
|
89
|
-
* success. Resets across process restarts — escalation is a within-run feature.
|
|
90
|
-
*/
|
|
91
|
-
private readonly failures;
|
|
92
85
|
private readonly goalJudge;
|
|
93
86
|
private readonly selfImprove;
|
|
94
87
|
private readonly selfImproveCooldownMs;
|
|
@@ -76,13 +76,10 @@ export class FleetAutonomousLoop {
|
|
|
76
76
|
executor;
|
|
77
77
|
policy;
|
|
78
78
|
enabled;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* success. Resets across process restarts — escalation is a within-run feature.
|
|
84
|
-
*/
|
|
85
|
-
failures = new Map();
|
|
79
|
+
// Per-task failure counts now live on the task itself (`ColabTask.attempts`,
|
|
80
|
+
// persisted by FleetColabStore) so they survive daemon restarts and are visible
|
|
81
|
+
// cross-machine. They feed {@link chooseAutonomousModel} for model-ladder
|
|
82
|
+
// escalation AND the retry budget that dead-letters a hopeless task.
|
|
86
83
|
goalJudge;
|
|
87
84
|
selfImprove;
|
|
88
85
|
selfImproveCooldownMs;
|
|
@@ -146,6 +143,10 @@ export class FleetAutonomousLoop {
|
|
|
146
143
|
return { outcome: 'saturated', detail: 'at capacity — leaving the queue to idle peers' };
|
|
147
144
|
}
|
|
148
145
|
this.store.updatePresence({ status: 'active' });
|
|
146
|
+
// Zombie sweep (Hermes-kanban parity): reclaim crashed peers' expired claims
|
|
147
|
+
// before picking work. Each reclaim counts against the task's retry budget,
|
|
148
|
+
// dead-lettering a task that has been claimed-and-abandoned too many times.
|
|
149
|
+
this.store.reclaimExpired();
|
|
149
150
|
const next = this.store.nextClaimable();
|
|
150
151
|
if (!next) {
|
|
151
152
|
// No real work — use the idle moment for one bounded self-improvement
|
|
@@ -163,7 +164,7 @@ export class FleetAutonomousLoop {
|
|
|
163
164
|
return { outcome: 'idle', detail: err instanceof Error ? err.message : String(err) };
|
|
164
165
|
}
|
|
165
166
|
this.store.updatePresence({ status: 'active', currentTask: task.title });
|
|
166
|
-
const failures =
|
|
167
|
+
const failures = task.attempts ?? 0;
|
|
167
168
|
const model = chooseAutonomousModel(this.tierConfig, { priority: task.priority, ...(failures > 0 ? { failures } : {}) }, this.policy);
|
|
168
169
|
let result;
|
|
169
170
|
const doneLoad = beginFleetWork('autonomy.task');
|
|
@@ -185,7 +186,7 @@ export class FleetAutonomousLoop {
|
|
|
185
186
|
return goalOutcome;
|
|
186
187
|
// null → judge said done (or skipped): fall through to completion.
|
|
187
188
|
}
|
|
188
|
-
this.
|
|
189
|
+
this.store.resetAttempts(task.id);
|
|
189
190
|
this.store.completeTask(task.id, {
|
|
190
191
|
summary: result.summary,
|
|
191
192
|
filesModified: result.filesModified ?? [],
|
|
@@ -194,18 +195,27 @@ export class FleetAutonomousLoop {
|
|
|
194
195
|
this.store.updatePresence({ status: 'idle', currentTask: null });
|
|
195
196
|
return { outcome: 'completed', taskId: task.id, taskTitle: task.title, model };
|
|
196
197
|
}
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
//
|
|
198
|
+
// Persist the failure (retry-budget counter — survives restarts, visible
|
|
199
|
+
// cross-machine). The next attempt can escalate up the model ladder; once the
|
|
200
|
+
// budget is exhausted the task is dead-lettered to `blocked` (the Review
|
|
201
|
+
// column) instead of being released to spin forever.
|
|
202
|
+
const { attempts, exhausted } = this.store.recordFailure(task.id);
|
|
200
203
|
this.store.appendWorklog({
|
|
201
204
|
agent: this.store.agentId,
|
|
202
205
|
taskId: task.id,
|
|
203
206
|
summary: `Autonomous attempt failed: ${result.summary}`,
|
|
204
207
|
filesModified: [],
|
|
205
208
|
issues: [result.error ?? 'unknown error'],
|
|
206
|
-
nextSteps:
|
|
209
|
+
nextSteps: exhausted
|
|
210
|
+
? [`retry budget (${attempts} attempts) exhausted — dead-lettered for human review`]
|
|
211
|
+
: ['retry on a later tick or escalate to the strong model'],
|
|
207
212
|
});
|
|
208
|
-
|
|
213
|
+
if (exhausted) {
|
|
214
|
+
this.store.blockTask(task.id, `Failed ${attempts}× (retry budget exhausted) — needs review`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.store.releaseTask(task.id);
|
|
218
|
+
}
|
|
209
219
|
this.store.updatePresence({ status: 'idle', currentTask: null });
|
|
210
220
|
return {
|
|
211
221
|
outcome: 'failed',
|
|
@@ -351,7 +351,15 @@ function buildGrokCatalog() {
|
|
|
351
351
|
}));
|
|
352
352
|
}
|
|
353
353
|
function buildMistralCatalog() {
|
|
354
|
-
const ids = [
|
|
354
|
+
const ids = [
|
|
355
|
+
'mistral-large-latest',
|
|
356
|
+
'mistral-medium-latest',
|
|
357
|
+
'mistral-small-latest',
|
|
358
|
+
'codestral-latest',
|
|
359
|
+
'devstral-latest',
|
|
360
|
+
'magistral-medium-latest',
|
|
361
|
+
'ministral-8b-latest',
|
|
362
|
+
];
|
|
355
363
|
return ids.map((id) => ({
|
|
356
364
|
id,
|
|
357
365
|
contextWindow: 128_000,
|
|
@@ -36,6 +36,21 @@ export interface ColabTask {
|
|
|
36
36
|
assignedAgent?: string | null;
|
|
37
37
|
claimedBy?: string | null;
|
|
38
38
|
claimedAt?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Last lease renewal (heartbeat). A live worker calls {@link FleetColabStore.heartbeat}
|
|
41
|
+
* to re-stamp `claimedAt`, so {@link FleetColabStore.isClaimExpired} (which measures
|
|
42
|
+
* `claimedAt` age) doubles as zombie detection: a silent claim ages out, a
|
|
43
|
+
* heartbeated one does not. Informational mirror of the last bump.
|
|
44
|
+
*/
|
|
45
|
+
lastHeartbeatAt?: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Persisted failure/zombie-reclaim count (Hermes-kanban retry-budget parity).
|
|
48
|
+
* Survives daemon restarts and is visible cross-machine, unlike an in-memory
|
|
49
|
+
* counter. Reset on success; incremented on each failed attempt or zombie reclaim.
|
|
50
|
+
*/
|
|
51
|
+
attempts?: number;
|
|
52
|
+
/** Per-task override of the dead-letter threshold (default {@link DEFAULT_RETRY_BUDGET}). */
|
|
53
|
+
retryBudget?: number;
|
|
39
54
|
completedAt?: string | null;
|
|
40
55
|
blockedReason?: string;
|
|
41
56
|
filesToModify?: string[];
|
|
@@ -64,6 +79,34 @@ export interface ColabTask {
|
|
|
64
79
|
goalLastReason?: string;
|
|
65
80
|
createdBy?: string;
|
|
66
81
|
createdAt?: string;
|
|
82
|
+
/** Free-text labels for filtering (`kanban_list --tag`). */
|
|
83
|
+
tags?: string[];
|
|
84
|
+
/** Human/agent the card is assigned to (distinct from `claimedBy`, which is the live worker). */
|
|
85
|
+
assignee?: string;
|
|
86
|
+
/** Discussion thread (block/unblock/complete also append here). */
|
|
87
|
+
comments?: ColabComment[];
|
|
88
|
+
/** Progress pings (each also renews the lease via {@link FleetColabStore.heartbeat}). */
|
|
89
|
+
heartbeats?: ColabHeartbeat[];
|
|
90
|
+
/** Free-form references (PRs, commits, files) — NOT DAG edges; dependency edges live in `dependsOn`. */
|
|
91
|
+
links?: ColabLink[];
|
|
92
|
+
}
|
|
93
|
+
export interface ColabComment {
|
|
94
|
+
id: string;
|
|
95
|
+
author?: string;
|
|
96
|
+
text: string;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ColabHeartbeat {
|
|
100
|
+
id: string;
|
|
101
|
+
author?: string;
|
|
102
|
+
message?: string;
|
|
103
|
+
createdAt: string;
|
|
104
|
+
}
|
|
105
|
+
export interface ColabLink {
|
|
106
|
+
id: string;
|
|
107
|
+
target: string;
|
|
108
|
+
label?: string;
|
|
109
|
+
createdAt: string;
|
|
67
110
|
}
|
|
68
111
|
export interface ColabWorklogFileChange {
|
|
69
112
|
file: string;
|
|
@@ -97,6 +140,12 @@ export interface FleetColabStoreConfig {
|
|
|
97
140
|
* not stay stuck. Lazy-on-read (no timer); 0 disables. Default 15 min.
|
|
98
141
|
*/
|
|
99
142
|
claimTtlMs?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Default retry budget for tasks that don't set their own (default
|
|
145
|
+
* {@link DEFAULT_RETRY_BUDGET}). After this many failures/zombie-reclaims a task
|
|
146
|
+
* is dead-lettered to `blocked` for review instead of being retried forever.
|
|
147
|
+
*/
|
|
148
|
+
retryBudget?: number;
|
|
100
149
|
/** Injectable clock (epoch ms) for deterministic tests. */
|
|
101
150
|
now?: () => number;
|
|
102
151
|
/** Injectable id generator for deterministic tests. */
|
|
@@ -120,6 +169,11 @@ export interface AddTaskInput {
|
|
|
120
169
|
dependsOn?: string[];
|
|
121
170
|
goalMode?: boolean;
|
|
122
171
|
goalMaxTurns?: number;
|
|
172
|
+
/** Per-task dead-letter threshold override (default {@link DEFAULT_RETRY_BUDGET}). */
|
|
173
|
+
retryBudget?: number;
|
|
174
|
+
tags?: string[];
|
|
175
|
+
assignee?: string;
|
|
176
|
+
status?: ColabTaskStatus;
|
|
123
177
|
createdBy?: string;
|
|
124
178
|
id?: string;
|
|
125
179
|
}
|
|
@@ -131,6 +185,8 @@ export declare class FleetColabStore {
|
|
|
131
185
|
private readonly now;
|
|
132
186
|
private readonly generateId;
|
|
133
187
|
private readonly claimTtlMs;
|
|
188
|
+
private readonly retryBudget;
|
|
189
|
+
private writeSeq;
|
|
134
190
|
private readonly tasksPath;
|
|
135
191
|
private readonly worklogPath;
|
|
136
192
|
private readonly presencePath;
|
|
@@ -150,12 +206,39 @@ export declare class FleetColabStore {
|
|
|
150
206
|
* reclaim of a crashed agent's work). Lazy: evaluated on read, no timer.
|
|
151
207
|
*/
|
|
152
208
|
isClaimExpired(task: Pick<ColabTask, 'status' | 'claimedAt'>, nowMs?: number): boolean;
|
|
209
|
+
/** The dead-letter threshold for a task (its own `retryBudget`, else the store default). */
|
|
210
|
+
resolveRetryBudget(task: Pick<ColabTask, 'retryBudget'>): number;
|
|
153
211
|
/**
|
|
154
|
-
* Sweep expired claims
|
|
155
|
-
*
|
|
156
|
-
*
|
|
212
|
+
* Sweep expired claims (zombie detection) — a crashed agent's claim ages out
|
|
213
|
+
* because it stopped heartbeating. Each reclaim counts against the task's
|
|
214
|
+
* retry budget: under budget it returns to `open` for retry; at/over budget it
|
|
215
|
+
* is dead-lettered to `blocked` (the "Review" column) instead of spinning
|
|
216
|
+
* forever. Returns every reclaimed task id (re-opened or dead-lettered).
|
|
217
|
+
* Lazy callers can rely on {@link nextClaimable}, which treats an expired claim
|
|
218
|
+
* as available — but only the sweep enforces the retry budget, so a daemon
|
|
219
|
+
* should call this each tick.
|
|
157
220
|
*/
|
|
158
221
|
reclaimExpired(): string[];
|
|
222
|
+
/**
|
|
223
|
+
* Renew a claim's lease (Hermes-kanban heartbeat). Re-stamps `claimedAt` and
|
|
224
|
+
* `lastHeartbeatAt` so a long-running but live worker is not reclaimed as a
|
|
225
|
+
* zombie. Only valid while the task is `in_progress`; when `agentId` is given it
|
|
226
|
+
* must match the current claimant.
|
|
227
|
+
*/
|
|
228
|
+
heartbeat(taskId: string, agentId?: string): ColabTask;
|
|
229
|
+
/**
|
|
230
|
+
* Record a failed attempt (persisted retry-budget counter). Increments
|
|
231
|
+
* `attempts` and reports whether the budget is now exhausted, so the caller can
|
|
232
|
+
* dead-letter (`blockTask`) vs retry (`releaseTask`). Does not change status —
|
|
233
|
+
* the daemon owns the worklog/release flow.
|
|
234
|
+
*/
|
|
235
|
+
recordFailure(taskId: string): {
|
|
236
|
+
task: ColabTask;
|
|
237
|
+
attempts: number;
|
|
238
|
+
exhausted: boolean;
|
|
239
|
+
};
|
|
240
|
+
/** Reset the retry-budget counter (call on a successful attempt). */
|
|
241
|
+
resetAttempts(taskId: string): ColabTask;
|
|
159
242
|
/** Dependency ids that are not yet `completed` (unknown/missing ids count as unmet). */
|
|
160
243
|
unmetDependencies(task: Pick<ColabTask, 'dependsOn'>, tasks: ColabTask[]): string[];
|
|
161
244
|
/** True when every dependency of a task is `completed` (DAG readiness). */
|
|
@@ -192,6 +275,21 @@ export declare class FleetColabStore {
|
|
|
192
275
|
link(childId: string, parentId: string): ColabTask;
|
|
193
276
|
/** Remove a `child dependsOn parent` edge. Returns false if the edge was absent. */
|
|
194
277
|
unlink(childId: string, parentId: string): boolean;
|
|
278
|
+
/** Append a comment to a task's discussion thread. */
|
|
279
|
+
addComment(taskId: string, text: string, author?: string): ColabTask;
|
|
280
|
+
/** Attach a free-form reference (PR/commit/file/url) — not a DAG edge (see {@link link}). */
|
|
281
|
+
addLink(taskId: string, target: string, label?: string): ColabTask;
|
|
282
|
+
/**
|
|
283
|
+
* Tool-facing heartbeat: records a progress ping AND renews the lease. Lenient
|
|
284
|
+
* (unlike {@link heartbeat}, which is the strict programmatic lease-renewal):
|
|
285
|
+
* an `open` task transitions to `in_progress` and is claimed by `agentId`, so
|
|
286
|
+
* an agent pinging a fresh card takes ownership the way Hermes's kanban does.
|
|
287
|
+
*/
|
|
288
|
+
recordHeartbeat(taskId: string, message?: string, author?: string, agentId?: string): ColabTask;
|
|
289
|
+
/** Resume a blocked task (back to in_progress) and record why. Mirrors `kanban_unblock`. */
|
|
290
|
+
unblockTask(taskId: string, comment?: string, author?: string): ColabTask;
|
|
291
|
+
/** Shared read-modify-write helper for single-task surface mutations. */
|
|
292
|
+
private mutateTask;
|
|
195
293
|
appendWorklog(entry: Omit<ColabWorklogEntry, 'id' | 'date'> & {
|
|
196
294
|
id?: string;
|
|
197
295
|
date?: string;
|
|
@@ -214,5 +312,13 @@ export declare class FleetColabStore {
|
|
|
214
312
|
private readPresence;
|
|
215
313
|
private writePresence;
|
|
216
314
|
private readJson;
|
|
315
|
+
/**
|
|
316
|
+
* Atomic write (temp file + rename). `fs.renameSync` is atomic on POSIX, so a
|
|
317
|
+
* concurrent same-host reader/writer never sees a half-written file — important
|
|
318
|
+
* now that the daemon, the `kanban_*` tools, and `/colab` can all drive the
|
|
319
|
+
* same `colab-tasks.json`. (Cross-machine arbitration stays git-push-order by
|
|
320
|
+
* design; this guards only the local race.) Mirrors the idiom in
|
|
321
|
+
* `src/kanban/kanban-store.ts`.
|
|
322
|
+
*/
|
|
217
323
|
private writeJson;
|
|
218
324
|
}
|