@causyne/agent 1.10.13 → 1.10.18

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.
Files changed (76) hide show
  1. package/docs/ARCHITECTURE.md +118 -17
  2. package/docs/SECURITY_REVIEW.md +13 -1
  3. package/docs/THIRD_PARTY_NOTICES.md +46 -10
  4. package/package.json +10 -4
  5. package/src/agentLoop.js +123 -21
  6. package/src/agentProfiles.js +135 -0
  7. package/src/agentProfiles.ts +143 -0
  8. package/src/approvalGrants.js +17 -35
  9. package/src/approvalGrants.ts +54 -0
  10. package/src/auditLog.d.ts +3 -0
  11. package/src/browser/browserRuntime.js +88 -0
  12. package/src/browser/browserRuntime.ts +107 -0
  13. package/src/browser/causyneBrowser.js +495 -0
  14. package/src/browser/causyneBrowser.ts +594 -0
  15. package/src/browser/searchProviders.js +156 -0
  16. package/src/browser/searchProviders.ts +156 -0
  17. package/src/browser/snapshotWindow.js +28 -0
  18. package/src/browser/snapshotWindow.ts +40 -0
  19. package/src/browser/urlPolicy.js +89 -0
  20. package/src/browser/urlPolicy.ts +89 -0
  21. package/src/checkpoints.js +19 -6
  22. package/src/cliMode.js +13 -1
  23. package/src/commands.js +37 -8
  24. package/src/config.d.ts +2 -0
  25. package/src/decisionPolicy.js +68 -0
  26. package/src/decisionPolicy.ts +106 -0
  27. package/src/hooks.js +121 -107
  28. package/src/hooks.ts +193 -0
  29. package/src/implementationPlan.js +50 -0
  30. package/src/implementationPlan.ts +59 -0
  31. package/src/mcp/mcpClient.js +520 -132
  32. package/src/mcp/mcpClient.ts +584 -0
  33. package/src/mcp/mcpRegistry.js +399 -185
  34. package/src/mcp/mcpRegistry.ts +451 -0
  35. package/src/planCache.js +473 -450
  36. package/src/planCache.ts +840 -0
  37. package/src/plugins/pluginManager.js +40 -5
  38. package/src/plugins/pluginSkills.js +84 -0
  39. package/src/plugins/pluginSkills.ts +106 -0
  40. package/src/provisioning/capabilityProvisioner.d.ts +10 -0
  41. package/src/provisioning/capabilityProvisioner.js +5 -0
  42. package/src/provisioning/capabilityTools.js +2 -1
  43. package/src/sandbox.js +32 -2
  44. package/src/sessionJournal.js +471 -0
  45. package/src/sessionJournal.ts +496 -0
  46. package/src/sessionStore.js +3 -279
  47. package/src/skillsManager.js +18 -0
  48. package/src/subagentJournal.js +215 -0
  49. package/src/subagentJournal.ts +254 -0
  50. package/src/taskShape.js +179 -198
  51. package/src/taskShape.ts +259 -0
  52. package/src/toolPacks.js +7 -5
  53. package/src/tools/browserAutomator.js +45 -241
  54. package/src/tools/browserAutomator.ts +57 -0
  55. package/src/tools/manageLocalAiDependencies.js +1 -1
  56. package/src/tools/registry.js +4 -3
  57. package/src/tools/requestDecision.js +87 -0
  58. package/src/tools/requestDecision.ts +107 -0
  59. package/src/tools/subagentOrchestrator.js +73 -12
  60. package/src/tools/updatePlan.js +37 -84
  61. package/src/tools/updatePlan.ts +48 -0
  62. package/src/tools/webFetch.js +183 -68
  63. package/src/tools/webFetch.ts +185 -0
  64. package/src/tools/webSearch.js +50 -75
  65. package/src/tools/webSearch.ts +56 -0
  66. package/src/tui/ask.js +41 -1
  67. package/src/tui/chatRenderer.js +28 -0
  68. package/src/tui/dialogs.js +91 -0
  69. package/src/tui/prompt.js +32 -3
  70. package/src/tui/toolDisplay.js +0 -1
  71. package/src/types/contracts.js +8 -0
  72. package/src/types/contracts.ts +263 -0
  73. package/src/utils/paths.d.ts +3 -0
  74. package/src/version.d.ts +1 -0
  75. package/src/mcp/jsonRpc.js +0 -165
  76. package/src/tools/todoWrite.js +0 -75
@@ -12,7 +12,6 @@ import { processList } from './processList.js';
12
12
  import { procTerm } from './procTerm.js';
13
13
  import { webFetch } from './webFetch.js';
14
14
  import { desktopOpen } from './desktopOpen.js';
15
- import { todoWrite } from './todoWrite.js';
16
15
  import { multiEdit } from './multiEdit.js';
17
16
  import { codebaseSearch } from './codebaseSearch.js';
18
17
  import { browserAct } from './browserAutomator.js';
@@ -22,6 +21,7 @@ import { palmierAutomator } from './palmierAutomator.js';
22
21
  import { viewImage } from './viewImage.js';
23
22
  import { webSearch } from './webSearch.js';
24
23
  import { updatePlan } from './updatePlan.js';
24
+ import { requestDecision } from './requestDecision.js';
25
25
  import { clipboard } from './clipboard.js';
26
26
  import { screenshot } from './screenshot.js';
27
27
  import { axSnapshot } from './axSnapshot.js';
@@ -106,9 +106,9 @@ const TOOL_MODULES = [
106
106
  sysRun, sysRunBg, fsRead, fsWrite,
107
107
  fsEdit, fsFind, fsSearch, fsList, fsMove,
108
108
  fsDelete, gitExec, processList, procTerm,
109
- webFetch, desktopOpen, todoWrite, multiEdit,
109
+ webFetch, desktopOpen, multiEdit,
110
110
  codebaseSearch, browserAct, viewImage, webSearch,
111
- updatePlan, clipboard, screenshot, axSnapshot, desktopAct, archive,
111
+ updatePlan, requestDecision, clipboard, screenshot, axSnapshot, desktopAct, archive,
112
112
  checkSystemSpecs, executeProceduralMedia, manageLocalAiDependencies,
113
113
  generateMediaViaApi, memoryWrite, printerPrint, printerList, printerStatus, toolForgeTool, pluginManagerTool,
114
114
  astryxOracle, pdfRead, codeVerifySyntax, codeListSymbols, blenderAct, resolveAutomator, palmierAutomator,
@@ -145,6 +145,7 @@ for (const module of TOOL_MODULES) {
145
145
  // a tool whose canonical schema name is different. Resolved only at execution
146
146
  // time — never advertised, so they cost no context.
147
147
  const TOOL_ALIASES = {
148
+ TodoWrite: 'update_plan',
148
149
  manage_local_ai_dependencies: 'manage_local_dependencies',
149
150
  procTerm: 'proc_term',
150
151
  gitExec: 'gitOp',
@@ -0,0 +1,87 @@
1
+ import crypto from 'node:crypto';
2
+ const KINDS = new Set(['architecture', 'product', 'data', 'security', 'workflow', 'other']);
3
+ function text(value, name, limit) {
4
+ if (typeof value !== 'string')
5
+ throw new Error(`${name} must be a string`);
6
+ const cleaned = value.replace(/\s+/g, ' ').trim();
7
+ if (!cleaned)
8
+ throw new Error(`${name} must not be empty`);
9
+ if (cleaned.length > limit)
10
+ throw new Error(`${name} exceeds ${limit} characters`);
11
+ return cleaned;
12
+ }
13
+ function normalizeRequest(args) {
14
+ const kind = String(args.kind ?? 'other');
15
+ if (!KINDS.has(kind))
16
+ throw new Error('kind is invalid');
17
+ if (!Array.isArray(args.options) || args.options.length < 2 || args.options.length > 4) {
18
+ throw new Error('options must contain 2-4 choices');
19
+ }
20
+ const ids = new Set();
21
+ const options = args.options.map((raw, index) => {
22
+ const option = (raw && typeof raw === 'object' ? raw : {});
23
+ const id = text(option.id ?? `option-${index + 1}`, `options[${index}].id`, 48);
24
+ if (ids.has(id))
25
+ throw new Error(`Duplicate option id: ${id}`);
26
+ ids.add(id);
27
+ return {
28
+ id,
29
+ label: text(option.label, `options[${index}].label`, 80),
30
+ description: text(option.description, `options[${index}].description`, 240)
31
+ };
32
+ });
33
+ return {
34
+ id: crypto.randomUUID(),
35
+ kind,
36
+ header: text(args.header, 'header', 48),
37
+ question: text(args.question, 'question', 500),
38
+ reason: text(args.reason, 'reason', 500),
39
+ options,
40
+ allowCustom: true
41
+ };
42
+ }
43
+ export const requestDecision = {
44
+ schema: {
45
+ name: 'request_decision',
46
+ description: 'Pause for a durable user decision when several materially different implementation directions are valid. Provide 2-4 concrete options; a custom answer is always available. This is a design decision, not a tool permission request.',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: {
50
+ kind: { type: 'string', enum: ['architecture', 'product', 'data', 'security', 'workflow', 'other'] },
51
+ header: { type: 'string', description: 'Short decision label, at most 48 characters.' },
52
+ question: { type: 'string', description: 'One specific decision the user must make.' },
53
+ reason: { type: 'string', description: 'Why this choice has durable consequences.' },
54
+ options: {
55
+ type: 'array', minItems: 2, maxItems: 4,
56
+ items: {
57
+ type: 'object',
58
+ properties: {
59
+ id: { type: 'string' },
60
+ label: { type: 'string' },
61
+ description: { type: 'string' }
62
+ },
63
+ required: ['id', 'label', 'description'],
64
+ additionalProperties: false
65
+ }
66
+ }
67
+ },
68
+ required: ['kind', 'header', 'question', 'reason', 'options'],
69
+ additionalProperties: false
70
+ }
71
+ },
72
+ async execute(args, context = {}) {
73
+ try {
74
+ if (!context.parentLoop || typeof context.parentLoop.requestDecision !== 'function') {
75
+ return { success: false, error: 'Structured decisions require an active Causyne session' };
76
+ }
77
+ const request = normalizeRequest(args);
78
+ const answer = await context.parentLoop.requestDecision(request);
79
+ if (!answer)
80
+ return { success: false, error: 'The user dismissed the decision without choosing an option' };
81
+ return { success: true, output: JSON.stringify(answer), decision: answer };
82
+ }
83
+ catch (error) {
84
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
85
+ }
86
+ }
87
+ };
@@ -0,0 +1,107 @@
1
+ import crypto from 'node:crypto';
2
+ import type { DecisionKind, DecisionRequest } from '../decisionPolicy.js';
3
+
4
+ interface ToolContext {
5
+ parentLoop?: {
6
+ requestDecision(request: DecisionRequest): Promise<unknown>;
7
+ };
8
+ }
9
+
10
+ interface RawOption {
11
+ id?: unknown;
12
+ label?: unknown;
13
+ description?: unknown;
14
+ }
15
+
16
+ interface RawInput {
17
+ kind?: unknown;
18
+ header?: unknown;
19
+ question?: unknown;
20
+ reason?: unknown;
21
+ options?: unknown;
22
+ }
23
+
24
+ const KINDS = new Set<DecisionKind>(['architecture', 'product', 'data', 'security', 'workflow', 'other']);
25
+
26
+ function text(value: unknown, name: string, limit: number): string {
27
+ if (typeof value !== 'string') throw new Error(`${name} must be a string`);
28
+ const cleaned = value.replace(/\s+/g, ' ').trim();
29
+ if (!cleaned) throw new Error(`${name} must not be empty`);
30
+ if (cleaned.length > limit) throw new Error(`${name} exceeds ${limit} characters`);
31
+ return cleaned;
32
+ }
33
+
34
+ function normalizeRequest(args: RawInput): DecisionRequest {
35
+ const kind = String(args.kind ?? 'other') as DecisionKind;
36
+ if (!KINDS.has(kind)) throw new Error('kind is invalid');
37
+ if (!Array.isArray(args.options) || args.options.length < 2 || args.options.length > 4) {
38
+ throw new Error('options must contain 2-4 choices');
39
+ }
40
+ const ids = new Set<string>();
41
+ const options = args.options.map((raw, index) => {
42
+ const option = (raw && typeof raw === 'object' ? raw : {}) as RawOption;
43
+ const id = text(option.id ?? `option-${index + 1}`, `options[${index}].id`, 48);
44
+ if (ids.has(id)) throw new Error(`Duplicate option id: ${id}`);
45
+ ids.add(id);
46
+ return {
47
+ id,
48
+ label: text(option.label, `options[${index}].label`, 80),
49
+ description: text(option.description, `options[${index}].description`, 240)
50
+ };
51
+ });
52
+ return {
53
+ id: crypto.randomUUID(),
54
+ kind,
55
+ header: text(args.header, 'header', 48),
56
+ question: text(args.question, 'question', 500),
57
+ reason: text(args.reason, 'reason', 500),
58
+ options,
59
+ allowCustom: true
60
+ };
61
+ }
62
+
63
+ export const requestDecision = {
64
+ schema: {
65
+ name: 'request_decision',
66
+ description: 'Pause for a durable user decision when several materially different implementation directions are valid. Provide 2-4 concrete options; a custom answer is always available. This is a design decision, not a tool permission request.',
67
+ parameters: {
68
+ type: 'object',
69
+ properties: {
70
+ kind: { type: 'string', enum: ['architecture', 'product', 'data', 'security', 'workflow', 'other'] },
71
+ header: { type: 'string', description: 'Short decision label, at most 48 characters.' },
72
+ question: { type: 'string', description: 'One specific decision the user must make.' },
73
+ reason: { type: 'string', description: 'Why this choice has durable consequences.' },
74
+ options: {
75
+ type: 'array', minItems: 2, maxItems: 4,
76
+ items: {
77
+ type: 'object',
78
+ properties: {
79
+ id: { type: 'string' },
80
+ label: { type: 'string' },
81
+ description: { type: 'string' }
82
+ },
83
+ required: ['id', 'label', 'description'],
84
+ additionalProperties: false
85
+ }
86
+ }
87
+ },
88
+ required: ['kind', 'header', 'question', 'reason', 'options'],
89
+ additionalProperties: false
90
+ }
91
+ },
92
+
93
+ async execute(args: RawInput, context: ToolContext = {}) {
94
+ try {
95
+ if (!context.parentLoop || typeof context.parentLoop.requestDecision !== 'function') {
96
+ return { success: false, error: 'Structured decisions require an active Causyne session' };
97
+ }
98
+ const request = normalizeRequest(args);
99
+ const answer = await context.parentLoop.requestDecision(request);
100
+ if (!answer) return { success: false, error: 'The user dismissed the decision without choosing an option' };
101
+ return { success: true, output: JSON.stringify(answer), decision: answer };
102
+ } catch (error) {
103
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
104
+ }
105
+ }
106
+ };
107
+
@@ -1,4 +1,10 @@
1
1
  import { TokenTracker } from '../tokenTracker.js';
2
+ import { listAgentProfiles, loadAgentProfile } from '../agentProfiles.js';
3
+ import {
4
+ completeSubagentJournal,
5
+ failSubagentJournal,
6
+ startSubagentJournal
7
+ } from '../subagentJournal.js';
2
8
 
3
9
  // Subagent delegation.
4
10
  //
@@ -67,6 +73,8 @@ async function executeSubagent(record, parentLoop) {
67
73
  const child = new AgentLoop(transport, record.subagentId, record.prompt);
68
74
 
69
75
  // A delegated task is a bounded errand, not a second full session.
76
+ child.maxStepsCeiling = record.maxSteps;
77
+ child.taskTokenLimit = record.maxTokens;
70
78
  child.maxSteps = Math.min(child.maxSteps || 25, record.maxSteps);
71
79
  child.maxCircuitBreaker = child.maxSteps;
72
80
  child.messages = [];
@@ -78,8 +86,16 @@ async function executeSubagent(record, parentLoop) {
78
86
 
79
87
  record.loop = child;
80
88
  record.startedAtMs = Date.now();
89
+ record.journalStarted = startSubagentJournal(record);
81
90
 
82
- await child.run();
91
+ try {
92
+ await child.run();
93
+ } catch (error) {
94
+ if (record.journalStarted) {
95
+ failSubagentJournal(record.subagentId, error instanceof Error ? error.message : String(error));
96
+ }
97
+ throw error;
98
+ }
83
99
 
84
100
  const finalAssistant = [...child.messages].reverse()
85
101
  .find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
@@ -88,7 +104,7 @@ async function executeSubagent(record, parentLoop) {
88
104
  try { return JSON.parse(m.content).success === false; } catch (e) { return false; }
89
105
  });
90
106
 
91
- return {
107
+ const result = {
92
108
  subagentId: record.subagentId,
93
109
  name: record.name,
94
110
  stepsCompleted: child.stepsCompleted,
@@ -100,6 +116,8 @@ async function executeSubagent(record, parentLoop) {
100
116
  summary: finalAssistant ? finalAssistant.content : null,
101
117
  stoppedEarly: child.stepsCompleted >= child.maxSteps
102
118
  };
119
+ if (record.journalStarted) completeSubagentJournal(record.subagentId, child.messages, result);
120
+ return result;
103
121
  }
104
122
 
105
123
  export const subagentOrchestrator = {
@@ -111,15 +129,16 @@ export const subagentOrchestrator = {
111
129
  properties: {
112
130
  action: {
113
131
  type: 'string',
114
- enum: ['spawn', 'list', 'kill', 'delegate_and_wait'],
115
- description: 'spawn (start in the background and return immediately), list (show active subagents), kill (stop one), delegate_and_wait (run it and return the result).'
132
+ enum: ['spawn', 'list', 'list_profiles', 'kill', 'delegate_and_wait'],
133
+ description: 'spawn (start in the background), list (show active subagents), list_profiles (show static user-defined agent profiles), kill (stop one), or delegate_and_wait (run and return the result).'
116
134
  },
117
135
  name: { type: 'string', description: 'A short name for the subagent, e.g. Audit_Worker.' },
118
136
  prompt: { type: 'string', description: 'The complete instruction for the subagent. It has no memory of this conversation, so it must be self-contained.' },
137
+ profile: { type: 'string', description: 'Optional profile name from ~/.causyne/agents/<name>.json. Its tool and budget limits cannot be widened by the call.' },
119
138
  subagentId: { type: 'string', description: 'Target subagent for the kill action.' },
120
139
  toolScope: { type: 'array', items: { type: 'string' }, description: 'Catalogue tool names the subagent may use. Defaults to read-only inspection tools. It can never receive the delegation tool itself.' },
121
- maxSteps: { type: 'integer', description: 'Step ceiling for the subagent. Defaults to 25.' },
122
- maxTokens: { type: 'integer', description: 'Advisory token budget for this subagent. Defaults to 50000.' },
140
+ maxSteps: { type: 'integer', description: 'Hard step ceiling for the subagent. Defaults to 25.' },
141
+ maxTokens: { type: 'integer', description: 'Hard per-task token ceiling for the subagent. Defaults to 50000.' },
123
142
  depth: { type: 'integer', description: 'Hierarchy depth. 0 = master, 1 = worker. Subagents cannot spawn subagents.' }
124
143
  },
125
144
  required: ['action']
@@ -153,6 +172,23 @@ export const subagentOrchestrator = {
153
172
  };
154
173
  }
155
174
 
175
+ if (action === 'list_profiles') {
176
+ const { profiles, problems } = listAgentProfiles();
177
+ return {
178
+ success: true,
179
+ output: JSON.stringify({
180
+ profiles: profiles.map(profile => ({
181
+ name: profile.name,
182
+ description: profile.description,
183
+ toolScope: profile.toolScope,
184
+ maxSteps: profile.maxSteps,
185
+ maxTokens: profile.maxTokens
186
+ })),
187
+ problems
188
+ }, null, 2)
189
+ };
190
+ }
191
+
156
192
  if (action === 'kill') {
157
193
  const id = args.subagentId || args.name;
158
194
  if (!id) return { success: false, error: 'subagentId or name is required to stop a subagent.' };
@@ -173,7 +209,17 @@ export const subagentOrchestrator = {
173
209
  return { success: false, error: `Unknown action: ${action}` };
174
210
  }
175
211
 
176
- const prompt = args.prompt;
212
+ let profile = null;
213
+ if (args.profile !== undefined) {
214
+ const loaded = loadAgentProfile(args.profile);
215
+ if (!loaded.ok) return { success: false, error: `Agent profile ${JSON.stringify(args.profile)}: ${loaded.error}` };
216
+ profile = loaded.profile;
217
+ }
218
+
219
+ const requestedPrompt = args.prompt;
220
+ const prompt = profile
221
+ ? `${profile.instructions}\n\nDelegated task:\n${String(requestedPrompt || '')}`
222
+ : requestedPrompt;
177
223
  if (!prompt) return { success: false, error: 'A prompt is required to delegate a task.' };
178
224
 
179
225
  const depth = args.depth !== undefined ? args.depth : 1;
@@ -195,11 +241,19 @@ export const subagentOrchestrator = {
195
241
  };
196
242
  }
197
243
 
198
- const name = args.name || `Worker_${Math.random().toString(36).slice(2, 7)}`;
244
+ const name = args.name || profile?.name || `Worker_${Math.random().toString(36).slice(2, 7)}`;
199
245
  const subagentId = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
200
- const requestedScope = Array.isArray(args.toolScope) && args.toolScope.length > 0
246
+ let requestedScope = Array.isArray(args.toolScope) && args.toolScope.length > 0
201
247
  ? args.toolScope
202
- : DEFAULT_TOOL_SCOPE;
248
+ : profile?.toolScope || DEFAULT_TOOL_SCOPE;
249
+ if (profile) {
250
+ const ceiling = new Set(profile.toolScope);
251
+ requestedScope = requestedScope.filter(tool => ceiling.has(String(tool).replace(/\./g, '_')));
252
+ if (requestedScope.length === 0) {
253
+ return { success: false, error: `Agent profile ${profile.name} permits none of the requested tools.` };
254
+ }
255
+ }
256
+ requestedScope = [...new Set(requestedScope.map(tool => String(tool).replace(/\./g, '_')))];
203
257
 
204
258
  // Reject an unknown tool name instead of silently accepting it. The old
205
259
  // implementation accepted anything, which is easy to do when the scope is
@@ -211,14 +265,21 @@ export const subagentOrchestrator = {
211
265
  return { success: false, error: `Unknown tools in toolScope: ${unknown.join(', ')}.` };
212
266
  }
213
267
 
268
+ const requestedMaxSteps = Number.isInteger(args.maxSteps)
269
+ ? Math.max(1, Math.min(args.maxSteps, 100))
270
+ : profile?.maxSteps || 25;
271
+ const requestedMaxTokens = Number.isInteger(args.maxTokens)
272
+ ? Math.max(1_000, Math.min(args.maxTokens, 200_000))
273
+ : profile?.maxTokens || 50_000;
214
274
  const record = {
215
275
  subagentId,
216
276
  name,
217
277
  prompt,
218
278
  toolScope: requestedScope,
219
279
  depth,
220
- maxSteps: args.maxSteps || 25,
221
- maxTokens: args.maxTokens || 50000,
280
+ maxSteps: Math.min(requestedMaxSteps, profile?.maxSteps || 100),
281
+ maxTokens: Math.min(requestedMaxTokens, profile?.maxTokens || 200_000),
282
+ profile: profile?.name || null,
222
283
  status: 'running',
223
284
  startTime: new Date().toISOString(),
224
285
  summary: null,
@@ -1,88 +1,41 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
-
1
+ import { normalizePlanUpdate, summarizePlan } from '../implementationPlan.js';
4
2
  export const updatePlan = {
5
- schema: {
6
- name: 'update_plan',
7
- description: 'Self-Correcting Trajectory Engine: Update specific tasks in your plan using delta operations without rewriting everything.',
8
- parameters: {
9
- type: 'object',
10
- properties: {
11
- action: {
12
- type: 'string',
13
- enum: ['read', 'ADD_TASK', 'REMOVE_TASK', 'MARK_COMPLETED', 'MARK_FAILED']
14
- },
15
- taskId: { type: 'string', description: 'ID of the task to modify' },
16
- taskContent: { type: 'string', description: 'Content of the new task (if ADD_TASK)' },
17
- failureReason: { type: 'string', description: 'REQUIRED if action is MARK_FAILED' }
18
- },
19
- required: ['action']
20
- }
21
- },
22
-
23
- execute: async (args) => {
24
- try {
25
- const todoPath = path.resolve(process.cwd(), '.causyne_todos.json');
26
- const mdPath = path.resolve(process.cwd(), 'AGI_PLAN.md');
27
-
28
- let todos = [];
29
- if (fs.existsSync(todoPath)) {
30
- todos = JSON.parse(fs.readFileSync(todoPath, 'utf8'));
31
- }
32
-
33
- if (args.action === 'read') {
34
- if (!fs.existsSync(mdPath)) return "No plan exists.";
35
- return fs.readFileSync(mdPath, 'utf8');
36
- }
37
-
38
- if (args.action === 'MARK_FAILED' && !args.failureReason) {
39
- return "Error: You MUST provide a failureReason when marking a task as failed.";
40
- }
41
-
42
- if (args.action === 'ADD_TASK') {
43
- if (!args.taskId || !args.taskContent) return "Missing taskId or taskContent";
44
- todos.push({ id: args.taskId, content: args.taskContent, status: 'pending' });
45
- }
46
- else if (args.action === 'REMOVE_TASK') {
47
- todos = todos.filter(t => t.id !== args.taskId);
48
- }
49
- else if (args.action === 'MARK_COMPLETED') {
50
- const t = todos.find(t => t.id === args.taskId);
51
- if (t) t.status = 'completed';
52
- }
53
- else if (args.action === 'MARK_FAILED') {
54
- const t = todos.find(t => t.id === args.taskId);
55
- if (t) {
56
- t.status = 'pending'; // Reset it
57
- // Phase 2 AGI Upgrade: Auto-Pivot
58
- todos.push({
59
- id: `investigate_${args.taskId}`,
60
- content: `Investigate failure of ${args.taskId}: ${args.failureReason}`,
61
- status: 'in_progress'
62
- });
3
+ schema: {
4
+ name: 'update_plan',
5
+ description: 'Create or replace the visible implementation checklist. Use it before complex implementation and update statuses as work advances. Keep exactly one step in_progress.',
6
+ parameters: {
7
+ type: 'object',
8
+ properties: {
9
+ explanation: { type: 'string', description: 'Short reason for this plan update.' },
10
+ plan: {
11
+ type: 'array', minItems: 1, maxItems: 24,
12
+ items: {
13
+ type: 'object',
14
+ properties: {
15
+ id: { type: 'string' },
16
+ step: { type: 'string', description: 'Atomic, verifiable implementation step.' },
17
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] }
18
+ },
19
+ required: ['id', 'step', 'status'],
20
+ additionalProperties: false
21
+ }
22
+ }
23
+ },
24
+ required: ['plan'],
25
+ additionalProperties: false
26
+ }
27
+ },
28
+ async execute(args, context = {}) {
29
+ try {
30
+ if (!context.parentLoop || typeof context.parentLoop.updateImplementationPlan !== 'function') {
31
+ return { success: false, error: 'Implementation plans require an active Causyne session' };
32
+ }
33
+ const update = normalizePlanUpdate(args);
34
+ context.parentLoop.updateImplementationPlan(update);
35
+ return { success: true, output: summarizePlan(update.plan), plan: update.plan };
36
+ }
37
+ catch (error) {
38
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
63
39
  }
64
- }
65
-
66
- // Sync JSON
67
- fs.writeFileSync(todoPath, JSON.stringify(todos, null, 2), 'utf8');
68
-
69
- // Sync MD
70
- let mdContent = '# AGI Execution Plan\n\n';
71
- for (const t of todos) {
72
- let box = '[ ]';
73
- if (t.status === 'in_progress') box = '[/]';
74
- if (t.status === 'completed') box = '[x]';
75
- mdContent += `- ${box} ${t.id}: ${t.content}\n`;
76
- }
77
- fs.writeFileSync(mdPath, mdContent, 'utf8');
78
-
79
- if (args.action === 'MARK_FAILED') {
80
- return `Task failed. Auto-pivot engaged. Added investigation task.`;
81
- }
82
-
83
- return "Plan delta updated successfully.";
84
- } catch (error) {
85
- return `Error updating plan: ${error.message}`;
86
40
  }
87
- }
88
41
  };
@@ -0,0 +1,48 @@
1
+ import { normalizePlanUpdate, summarizePlan, type PlanUpdate } from '../implementationPlan.js';
2
+
3
+ interface ToolContext {
4
+ parentLoop?: {
5
+ updateImplementationPlan(update: PlanUpdate): void;
6
+ };
7
+ }
8
+
9
+ export const updatePlan = {
10
+ schema: {
11
+ name: 'update_plan',
12
+ description: 'Create or replace the visible implementation checklist. Use it before complex implementation and update statuses as work advances. Keep exactly one step in_progress.',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ explanation: { type: 'string', description: 'Short reason for this plan update.' },
17
+ plan: {
18
+ type: 'array', minItems: 1, maxItems: 24,
19
+ items: {
20
+ type: 'object',
21
+ properties: {
22
+ id: { type: 'string' },
23
+ step: { type: 'string', description: 'Atomic, verifiable implementation step.' },
24
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] }
25
+ },
26
+ required: ['id', 'step', 'status'],
27
+ additionalProperties: false
28
+ }
29
+ }
30
+ },
31
+ required: ['plan'],
32
+ additionalProperties: false
33
+ }
34
+ },
35
+
36
+ async execute(args: unknown, context: ToolContext = {}) {
37
+ try {
38
+ if (!context.parentLoop || typeof context.parentLoop.updateImplementationPlan !== 'function') {
39
+ return { success: false, error: 'Implementation plans require an active Causyne session' };
40
+ }
41
+ const update = normalizePlanUpdate(args);
42
+ context.parentLoop.updateImplementationPlan(update);
43
+ return { success: true, output: summarizePlan(update.plan), plan: update.plan };
44
+ } catch (error) {
45
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
46
+ }
47
+ }
48
+ };