@phuetz/code-buddy 1.3.0 → 1.3.2
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 +64 -22
- 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/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 +13 -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/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/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/package.json +2 -2
- 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
package/dist/codebuddy/tools.js
CHANGED
|
@@ -15,7 +15,7 @@ import { applyToolFilter } from "../utils/tool-filter.js";
|
|
|
15
15
|
import { TOOL_METADATA } from "../tools/metadata.js";
|
|
16
16
|
import { getPluginMarketplace } from "../plugins/marketplace.js";
|
|
17
17
|
// Import modular tool definitions
|
|
18
|
-
import { CORE_TOOLS, MORPH_EDIT_TOOL, isMorphEnabled, SEARCH_TOOLS, TODO_TOOLS, KANBAN_TOOLS, MESSAGING_TOOLS, YUANBAO_TOOLS, HOMEASSISTANT_TOOLS, MOA_TOOLS, SPOTIFY_TOOLS, X_SEARCH_TOOLS, FEISHU_TOOLS, CRON_TOOLS, WEB_TOOLS, ADVANCED_TOOLS, MULTIMODAL_TOOLS, COMPUTER_CONTROL_TOOLS, BROWSER_TOOLS, CANVAS_TOOLS, AGENT_TOOLS, FIRECRAWL_TOOLS, LSP_TOOLS, SECRETS_TOOLS, ADVISOR_TOOLS, ASK_USER_QUESTION_TOOLS, EXIT_PLAN_MODE_TOOLS, CODEBASE_REPLACE_TOOLS, SESSION_TOOLS,
|
|
18
|
+
import { CORE_TOOLS, MORPH_EDIT_TOOL, isMorphEnabled, SEARCH_TOOLS, TODO_TOOLS, KANBAN_TOOLS, MESSAGING_TOOLS, YUANBAO_TOOLS, HOMEASSISTANT_TOOLS, MOA_TOOLS, SPOTIFY_TOOLS, X_SEARCH_TOOLS, FEISHU_TOOLS, CRON_TOOLS, WEB_TOOLS, ADVANCED_TOOLS, MULTIMODAL_TOOLS, COMPUTER_CONTROL_TOOLS, BROWSER_TOOLS, CANVAS_TOOLS, AGENT_TOOLS, FIRECRAWL_TOOLS, LSP_TOOLS, SECRETS_TOOLS, ADVISOR_TOOLS, ASK_USER_QUESTION_TOOLS, EXIT_PLAN_MODE_TOOLS, CODEBASE_REPLACE_TOOLS, SESSION_TOOLS, CODE_EXPLORER_TOOLS, WINDOWS_TOOLS, } from "./tool-definitions/index.js";
|
|
19
19
|
import { FLEET_TOOLS } from "./fleet-tool-defs.js";
|
|
20
20
|
// Explicit re-exports from tool-definitions (no blanket export *)
|
|
21
21
|
export { CORE_TOOLS, MORPH_EDIT_TOOL, isMorphEnabled, SEARCH_TOOLS, TODO_TOOLS, KANBAN_TOOLS, MESSAGING_TOOLS, YUANBAO_TOOLS, HOMEASSISTANT_TOOLS, MOA_TOOLS, SPOTIFY_TOOLS, X_SEARCH_TOOLS, FEISHU_TOOLS, CRON_TOOLS, WEB_TOOLS, ADVANCED_TOOLS, MULTIMODAL_TOOLS, COMPUTER_CONTROL_TOOLS, BROWSER_TOOLS, CANVAS_TOOLS, REASON_TOOL, EXECUTE_CODE_TOOL, WINDOWS_TOOLS, } from "./tool-definitions/index.js";
|
|
@@ -50,7 +50,7 @@ export function getBuiltinToolNames() {
|
|
|
50
50
|
CODEBASE_REPLACE_TOOLS,
|
|
51
51
|
SESSION_TOOLS,
|
|
52
52
|
FLEET_TOOLS,
|
|
53
|
-
|
|
53
|
+
CODE_EXPLORER_TOOLS,
|
|
54
54
|
WINDOWS_TOOLS,
|
|
55
55
|
];
|
|
56
56
|
return Array.from(new Set(groups.flatMap((tools) => tools.map((tool) => tool.function.name))));
|
|
@@ -145,8 +145,8 @@ export function initializeToolRegistry() {
|
|
|
145
145
|
// Fleet tools — peer_delegate, list_peers (Phase (d).17). Always available;
|
|
146
146
|
// peer_delegate returns a clear error when no peers are connected.
|
|
147
147
|
registerGroup(FLEET_TOOLS);
|
|
148
|
-
//
|
|
149
|
-
registerGroup(
|
|
148
|
+
// CodeExplorer tools
|
|
149
|
+
registerGroup(CODE_EXPLORER_TOOLS);
|
|
150
150
|
isRegistryInitialized = true;
|
|
151
151
|
logger.debug('Tool registry initialized with built-in tools');
|
|
152
152
|
}
|
|
@@ -162,14 +162,14 @@ export function getMCPManager() {
|
|
|
162
162
|
return mcpManager;
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
|
-
* True when the Code Explorer (
|
|
166
|
-
* one `
|
|
165
|
+
* True when the Code Explorer (code-explorer) MCP tools are connected — i.e. at least
|
|
166
|
+
* one `mcp__code-explorer__*` tool is registered. Used to conditionally steer the
|
|
167
167
|
* agent toward Code Explorer for relationship/impact questions. Returns false
|
|
168
168
|
* (no behavior change) whenever Code Explorer is not installed.
|
|
169
169
|
*/
|
|
170
170
|
export function isCodeExplorerAvailable() {
|
|
171
171
|
try {
|
|
172
|
-
return getMCPManager().getTools().some((t) => t.name.startsWith('
|
|
172
|
+
return getMCPManager().getTools().some((t) => t.name.startsWith('mcp__code-explorer__'));
|
|
173
173
|
}
|
|
174
174
|
catch {
|
|
175
175
|
return false;
|
|
@@ -387,14 +387,14 @@ export async function getAllCodeBuddyTools() {
|
|
|
387
387
|
}
|
|
388
388
|
// Apply CLI tool filter (--enabled-tools, --disabled-tools, --allowed-tools)
|
|
389
389
|
allTools = applyToolFilter(allTools);
|
|
390
|
-
// When Code Explorer (
|
|
390
|
+
// When Code Explorer (code-explorer) is connected, make the built-in graph tools
|
|
391
391
|
// defer to it at the decision point — its graph is broader / more complete.
|
|
392
392
|
// Conditional & non-mutating: returns fresh objects only for code_graph /
|
|
393
|
-
// codebase_map, and only when a
|
|
394
|
-
if (allTools.some((t) => t.function.name.startsWith('
|
|
395
|
-
const DEFER = ' NOTE: Code Explorer (
|
|
396
|
-
'dead-code and cycle questions PREFER its MCP tools (`
|
|
397
|
-
'`query` / `find_cycles`); use this built-in only as a fallback if a
|
|
393
|
+
// codebase_map, and only when a code-explorer tool is present (no change otherwise).
|
|
394
|
+
if (allTools.some((t) => t.function.name.startsWith('mcp__code-explorer__'))) {
|
|
395
|
+
const DEFER = ' NOTE: Code Explorer (code-explorer) is available — for code-relationship, blast-radius/impact, ' +
|
|
396
|
+
'dead-code and cycle questions PREFER its MCP tools (`mcp__code-explorer__impact` / `context` / ' +
|
|
397
|
+
'`query` / `find_cycles`); use this built-in only as a fallback if a code-explorer tool errors.';
|
|
398
398
|
allTools = allTools.map((t) => t.function.name === 'code_graph' || t.function.name === 'codebase_map'
|
|
399
399
|
? { ...t, function: { ...t.function, description: (t.function.description ?? '') + DEFER } }
|
|
400
400
|
: t);
|
|
@@ -16,8 +16,13 @@ export class AIColabManager {
|
|
|
16
16
|
workLog = [];
|
|
17
17
|
constructor(workingDirectory = process.cwd()) {
|
|
18
18
|
this.colabFilePath = path.join(workingDirectory, 'COLAB.md');
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// The shared fleet board `.codebuddy/colab-tasks.json` is now owned solely by
|
|
20
|
+
// FleetColabStore (the kanban_* tools + the autonomous daemon). This legacy
|
|
21
|
+
// `/colab` manager uses an incompatible schema (`not_started` status, no
|
|
22
|
+
// version/comment preservation, non-atomic full-file rewrite), so it writes
|
|
23
|
+
// its OWN files to avoid clobbering the unified board. See ColabKanbanAdapter.
|
|
24
|
+
this.tasksFilePath = path.join(workingDirectory, '.codebuddy', 'ai-colab-tasks.json');
|
|
25
|
+
this.workLogFilePath = path.join(workingDirectory, '.codebuddy', 'ai-colab-worklog.json');
|
|
21
26
|
this.config = {
|
|
22
27
|
project: 'Code Buddy',
|
|
23
28
|
version: '1.0.0',
|
|
@@ -317,8 +322,8 @@ Couverture de tests actuelle: ${this.config.currentCoverage}% (objectif: ${this.
|
|
|
317
322
|
|
|
318
323
|
- \`COLAB.md\` - Document de coordination principal
|
|
319
324
|
- \`CLAUDE.md\` - Instructions de build et développement
|
|
320
|
-
- \`.codebuddy/colab-tasks.json\` - Tâches en cours
|
|
321
|
-
- \`.codebuddy/colab-worklog.json\` - Journal de travail
|
|
325
|
+
- \`.codebuddy/ai-colab-tasks.json\` - Tâches en cours
|
|
326
|
+
- \`.codebuddy/ai-colab-worklog.json\` - Journal de travail
|
|
322
327
|
|
|
323
328
|
### Règles de Collaboration
|
|
324
329
|
|
|
@@ -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')
|
|
@@ -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
|
}
|