gekto 0.0.10 → 0.0.12

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.
@@ -5,6 +5,7 @@ import { randomUUID } from 'crypto';
5
5
  import { tmpdir } from 'os';
6
6
  import { HeadlessAgent } from './HeadlessAgent.js';
7
7
  import { getState, mutate, broadcastFileChange, broadcastAgent } from '../state.js';
8
+ import { BASH_SAFETY_RULES } from './bashSafetyRules.js';
8
9
  // Per-lizard sessions
9
10
  const sessions = new Map();
10
11
  // Summarize tool input for display
@@ -21,20 +22,15 @@ function summarizeInput(input) {
21
22
  }
22
23
  const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant. Be concise and direct in your responses.
23
24
 
24
- IMPORTANT RESTRICTIONS - You MUST follow these rules:
25
- 1. DO NOT use Bash or shell commands - you cannot run terminal commands
26
- 2. DO NOT try to build, compile, or bundle the project
27
- 3. DO NOT try to start, restart, or run any servers or dev environments
28
- 4. DO NOT run tests, linters, or any CLI tools
29
- 5. DO NOT install packages or run npm/yarn/pnpm commands
25
+ You can use Bash for running tests, installing packages, building, and other shell operations.
26
+ ${BASH_SAFETY_RULES}
30
27
 
31
- Your job is ONLY to:
28
+ Your job is to:
32
29
  - Read and understand code using Read, Glob, Grep tools
33
30
  - Write and edit code using Write and Edit tools
31
+ - Use Bash for running tests, installing packages, git operations, and build commands
34
32
  - Make the requested code changes
35
33
 
36
- After making changes, simply report what you did. The user will handle building, testing, and running the code themselves.
37
-
38
34
  STATUS MARKER - At the END of EVERY response, you MUST include exactly one of these markers:
39
35
  - [STATUS:DONE] - Use when the task is complete and you have no questions for the user
40
36
  - [STATUS:PENDING] - Use when you need user input, confirmation, clarification, or approval to proceed
@@ -44,7 +40,7 @@ Examples:
44
40
  - When asking a question: "Which approach would you prefer? [STATUS:PENDING]"
45
41
  - After answering a simple question: "The file is located at src/utils.ts [STATUS:DONE]"`;
46
42
  // Tools that agents are not allowed to use
47
- const DISALLOWED_TOOLS = ['Bash', 'Task'];
43
+ const DISALLOWED_TOOLS = ['Task'];
48
44
  function getOrCreateSession(lizardId, ws) {
49
45
  let session = sessions.get(lizardId);
50
46
  if (!session) {
@@ -2,7 +2,7 @@ import { WebSocketServer } from 'ws';
2
2
  import { sendMessage, resumeSession, resetSession, getWorkingDir, getActiveSessions, killSession, killAllSessions, attachWebSocket, revertFiles, saveImagesToTempFiles } from './agentPool.js';
3
3
  import { processWithTools, generateTasksFromAbstract } from './gektoTools.js';
4
4
  import { initGekto, getGektoState, abortGekto, setStateCallback, resetGektoSession, restoreGektoSession, getGektoSessionId } from './gektoPersistent.js';
5
- import { getState, mutate, mutateBatch, addClient, removeClient, sendSnapshot, getClients, broadcastActivePlans, broadcastSinglePlan, broadcastTask, broadcastAgent, broadcastVisuals, broadcastVisualDelete, broadcastForPath } from '../state.js';
5
+ import { getState, mutate, mutateBatch, addClient, removeClient, sendSnapshot, getClients, broadcastActivePlans, broadcastActivePlanId, broadcastSinglePlan, broadcastTask, broadcastAgent, broadcastVisuals, broadcastVisualDelete, broadcastForPath } from '../state.js';
6
6
  import { persistEntity } from '../entityStore.js';
7
7
  import fs from 'fs';
8
8
  import nodePath from 'path';
@@ -190,7 +190,14 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
190
190
  // Clear tasks and plans
191
191
  mutate('tasks', {});
192
192
  mutate('activePlans', {});
193
+ mutate('activePlanId', null);
193
194
  broadcastActivePlans();
195
+ broadcastActivePlanId();
196
+ return;
197
+ }
198
+ case 'set_active_plan': {
199
+ mutate('activePlanId', msg.planId ?? null);
200
+ broadcastActivePlanId();
194
201
  return;
195
202
  }
196
203
  case 'create_plan': {
@@ -208,6 +215,31 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
208
215
  if (planImages && planImages.length > 0) {
209
216
  planImagePaths = saveImagesToTempFiles(planImages);
210
217
  }
218
+ // Remember the plan's state before processing so we can restore it
219
+ // if Gekto replies with chat/delegate/error instead of a plan update.
220
+ // Only delete plans that were created as temporary 'planning' entries.
221
+ const planBeforeProcessing = getState().activePlans[msg.planId] ?? null;
222
+ const planExistedBefore = planBeforeProcessing !== null;
223
+ const previousStatus = planBeforeProcessing?.status;
224
+ // Set plan status to 'planning' on the server side (authoritative)
225
+ // so we don't race with the client's save_state message
226
+ if (planBeforeProcessing) {
227
+ mutate(`activePlans.${msg.planId}.status`, 'planning');
228
+ }
229
+ else {
230
+ // Create temporary plan entry for new plans
231
+ mutate(`activePlans.${msg.planId}`, {
232
+ id: msg.planId,
233
+ status: 'planning',
234
+ originalPrompt: msg.prompt ?? '',
235
+ taskIds: [],
236
+ createdAt: new Date().toISOString(),
237
+ });
238
+ }
239
+ broadcastSinglePlan(msg.planId);
240
+ // Set as active plan
241
+ mutate('activePlanId', msg.planId);
242
+ broadcastActivePlanId();
211
243
  try {
212
244
  // Server-side accumulators and block counter
213
245
  let accThinking = '';
@@ -308,6 +340,9 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
308
340
  broadcastTask(task.id);
309
341
  }
310
342
  }
343
+ // Set the newly created/updated plan as active
344
+ mutate('activePlanId', planResult.plan.id);
345
+ broadcastActivePlanId();
311
346
  ws.send(JSON.stringify({
312
347
  type: 'plan_created',
313
348
  planId: msg.planId,
@@ -336,21 +371,27 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
336
371
  }));
337
372
  }
338
373
  else if (planResult.type === 'delegate' && planResult.delegateAgentId) {
339
- // Clear stale planning state — delegate doesn't create a plan
340
- const currentState = getState();
341
- if (msg.planId && currentState.activePlans[msg.planId]?.status === 'planning') {
342
- mutate(`activePlans.${msg.planId}`, undefined);
343
- broadcastSinglePlan(msg.planId);
374
+ // Restore plan state — only delete if it was a temporary entry
375
+ if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
376
+ if (planExistedBefore && previousStatus) {
377
+ mutate(`activePlans.${msg.planId}.status`, previousStatus);
378
+ broadcastSinglePlan(msg.planId);
379
+ }
380
+ else {
381
+ mutate(`activePlans.${msg.planId}`, undefined);
382
+ broadcastSinglePlan(msg.planId);
383
+ }
344
384
  }
345
385
  // Send instruction to existing agent
346
386
  const targetAgentId = planResult.delegateAgentId;
347
- const targetAgent = currentState.agents[targetAgentId];
387
+ const delegateState = getState();
388
+ const targetAgent = delegateState.agents[targetAgentId];
348
389
  if (targetAgent) {
349
390
  // Update agent status to working
350
391
  mutate(`agents.${targetAgentId}.status`, 'working');
351
392
  broadcastAgent(targetAgentId);
352
393
  // Notify client about delegation
353
- const delegateTask = targetAgent.taskId ? currentState.tasks[targetAgent.taskId] : null;
394
+ const delegateTask = targetAgent.taskId ? delegateState.tasks[targetAgent.taskId] : null;
354
395
  ws.send(JSON.stringify({
355
396
  type: 'gekto_delegate',
356
397
  planId: msg.planId,
@@ -372,11 +413,16 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
372
413
  }
373
414
  }
374
415
  else if (planResult.type === 'chat') {
375
- // Chat replyclear the temporary 'planning' plan state
376
- const currentState = getState();
377
- if (msg.planId && currentState.activePlans[msg.planId]?.status === 'planning') {
378
- mutate(`activePlans.${msg.planId}`, undefined);
379
- broadcastSinglePlan(msg.planId);
416
+ // Restore plan state only delete if it was a temporary entry
417
+ if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
418
+ if (planExistedBefore && previousStatus) {
419
+ mutate(`activePlans.${msg.planId}.status`, previousStatus);
420
+ broadcastSinglePlan(msg.planId);
421
+ }
422
+ else {
423
+ mutate(`activePlans.${msg.planId}`, undefined);
424
+ broadcastSinglePlan(msg.planId);
425
+ }
380
426
  }
381
427
  }
382
428
  // Persist Gekto session ID on current master so it survives restart
@@ -400,11 +446,16 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
400
446
  planId: msg.planId,
401
447
  message: `Error: ${err instanceof Error ? err.message : 'Processing failed'}`,
402
448
  }));
403
- // Clear stale planning state on error
404
- const currentState = getState();
405
- if (msg.planId && currentState.activePlans[msg.planId]?.status === 'planning') {
406
- mutate(`activePlans.${msg.planId}`, undefined);
407
- broadcastSinglePlan(msg.planId);
449
+ // Restore plan state on error — only delete if it was a temporary entry
450
+ if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
451
+ if (planExistedBefore && previousStatus) {
452
+ mutate(`activePlans.${msg.planId}.status`, previousStatus);
453
+ broadcastSinglePlan(msg.planId);
454
+ }
455
+ else {
456
+ mutate(`activePlans.${msg.planId}`, undefined);
457
+ broadcastSinglePlan(msg.planId);
458
+ }
408
459
  }
409
460
  ws.send(JSON.stringify({ type: 'gekto_done', planId: msg.planId }));
410
461
  ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { CLAUDE_PATH } from '../claudePath.js';
4
+ import { BASH_SAFETY_RULES } from './bashSafetyRules.js';
4
5
  // === JSON Schema for structured output ===
5
6
  export const GEKTO_OUTPUT_SCHEMA = JSON.stringify({
6
7
  type: 'object',
@@ -36,7 +37,7 @@ How you act:
36
37
  - If the user's request is ambiguous, use "clarify" with a focused question in "message".
37
38
  - IMPORTANT: Before creating a new plan, ALWAYS check [CURRENT STATE] first. If an existing agent has context about the relevant files, use "delegate" instead. Only use "create_plan" when no existing agent can handle the request.
38
39
  - If the user wants to build something and no existing agent is relevant, use "create_plan" with a short "title" (2-4 words), an abstract plan description, and a short "message" (1-2 sentences) for the chat — this is a brief confirmation shown to the user, NOT a copy of the abstract.
39
- - If the user wants to modify an existing plan abstract, use "update_plan" with the updated abstract. You MUST also include a short "message" for the chat confirming what changed.
40
+ - If the user wants to modify the active plan (marked [ACTIVE] in CURRENT STATE), use "update_plan" with the updated abstract. "update_plan" ALWAYS applies to the active plan. You MUST also include a short "message" for the chat confirming what changed.
40
41
  - If the user wants to remove agents, use "remove_agents" with a target.
41
42
  - If there is an existing agent that has context about the relevant files or task, use "delegate" with "agentId" (the agent's ID from CURRENT STATE) and "message" (a clear instruction). The agent already has session context — delegating is faster than creating a new plan. Always include a short "message" for the chat confirming what you delegated.
42
43
  - ALWAYS research the codebase first (Read, Glob, Grep) before creating plans. Understand the project structure, frameworks, and conventions.
@@ -44,6 +45,7 @@ How you act:
44
45
  Conflict awareness:
45
46
  - Before creating or updating a plan, check the [CURRENT STATE] context for running agents, active tasks, and files being modified.
46
47
  - If the user's request would modify files that are currently being worked on by other agents, WARN the user about the conflict. Use "clarify" to explain which files/tasks overlap and suggest waiting or adjusting scope.
48
+ - The [ACTIVE] plan in CURRENT STATE is the one the user is currently viewing. When the user asks to change "the plan", they mean the active plan — use "update_plan".
47
49
  - If there is already an active plan with similar goals, point it out and ask whether to update the existing plan or start a new one.
48
50
  - Never schedule tasks that write to the same files as currently running agents — this causes merge conflicts and lost work.
49
51
 
@@ -61,7 +63,8 @@ Abstract plan rules for create_plan / update_plan:
61
63
  - Include a "buildPrompt" explaining how to wire everything together after individual tasks complete.
62
64
  - Work items should be parallelizable — no item should depend on another item's output.
63
65
 
64
- You can ONLY use Read, Glob, and Grep tools. Bash and Task are disabled.
66
+ You can use Read, Glob, Grep, and Bash tools. Task is disabled.
67
+ ${BASH_SAFETY_RULES}
65
68
 
66
69
  Your response MUST be valid JSON matching this schema. Output ONLY the JSON object, nothing else.
67
70
  ${GEKTO_OUTPUT_SCHEMA}`;
@@ -130,7 +133,7 @@ function spawnOpus() {
130
133
  '--model', 'claude-opus-4-5-20251101',
131
134
  '--system-prompt', GEKTO_SYSTEM_PROMPT,
132
135
  '--dangerously-skip-permissions',
133
- '--disallowed-tools', 'Bash', 'Task',
136
+ '--disallowed-tools', 'Task',
134
137
  '--session-id', gektoSessionId,
135
138
  ];
136
139
  console.log(`[GektoPersistent] Spawning with session ${gektoSessionId}`);
@@ -315,13 +318,6 @@ async function sendToOpus(prompt, callbacks, retries = 3) {
315
318
  }
316
319
  return;
317
320
  }
318
- // Timeout after 5 min for complex tasks
319
- setTimeout(() => {
320
- if (opusPendingResolve) {
321
- opusPendingResolve('Task timed out. Please try breaking it into smaller steps.');
322
- opusPendingResolve = null;
323
- }
324
- }, 300000);
325
321
  });
326
322
  }
327
323
  // === Planning API (reuses warm persistent process) ===
@@ -114,13 +114,15 @@ export async function processWithTools(prompt, planId, _workingDir, activeAgents
114
114
  });
115
115
  contextParts.push(`Active agents:\n${agentLines.join('\n')}`);
116
116
  }
117
- // Active plans and their tasks
117
+ // Active plans and their tasks — mark which one is the active plan
118
118
  const planEntries = Object.values(serverState.activePlans);
119
119
  if (planEntries.length > 0) {
120
+ const activePlanId = serverState.activePlanId;
120
121
  const planLines = planEntries.map(plan => {
122
+ const isActive = plan.id === activePlanId;
121
123
  const tasks = plan.taskIds.map(id => serverState.tasks[id]).filter(Boolean);
122
124
  const taskSummary = tasks.map(t => ` - "${t.name}" (${t.status})${t.files?.length ? ` files=[${t.files.join(', ')}]` : ''}`).join('\n');
123
- return ` Plan "${plan.title || plan.id}" (${plan.status}):\n${taskSummary || ' (no tasks yet)'}`;
125
+ return ` ${isActive ? '[ACTIVE] ' : ''}Plan "${plan.title || plan.id}" (${plan.status}):\n${taskSummary || ' (no tasks yet)'}`;
124
126
  });
125
127
  contextParts.push(`Active plans:\n${planLines.join('\n')}`);
126
128
  }
@@ -139,12 +141,15 @@ export async function processWithTools(prompt, planId, _workingDir, activeAgents
139
141
  contextPrompt += `\n\n[CURRENT STATE:\n${contextParts.join('\n\n')}]`;
140
142
  }
141
143
  // Add existing plan context for modifications
142
- if (existingPlan?.abstract) {
143
- contextPrompt += `\n\n[EXISTING PLAN ABSTRACT - User wants to modify this plan:
144
+ // Use server-side activePlanId as the authoritative source
145
+ const activePlan = serverState.activePlanId ? serverState.activePlans[serverState.activePlanId] : null;
146
+ const planAbstract = existingPlan?.abstract || activePlan?.abstract;
147
+ if (planAbstract) {
148
+ contextPrompt += `\n\n[ACTIVE PLAN ABSTRACT — "${activePlan?.title || 'Untitled'}" (${activePlan?.status || 'unknown'}):
144
149
 
145
- ${existingPlan.abstract}
150
+ ${planAbstract}
146
151
 
147
- The user's message above is a modification request. Respond with "update_plan" and the FULL updated abstract (not just the changes). Keep the same structure and style.]`;
152
+ If the user wants to change this plan, respond with "update_plan" and the FULL updated abstract (not just the changes). Keep the same structure and style.]`;
148
153
  }
149
154
  // Append image file paths to prompt so Gekto can reference them
150
155
  if (imagePaths && imagePaths.length > 0) {
@@ -168,16 +173,22 @@ The user's message above is a modification request. Respond with "update_plan" a
168
173
  switch (parsed.action) {
169
174
  case 'create_plan':
170
175
  case 'update_plan': {
176
+ // For update_plan, always target the server's active plan
177
+ const effectivePlanId = parsed.action === 'update_plan' && serverState.activePlanId
178
+ ? serverState.activePlanId
179
+ : planId;
180
+ // Preserve createdAt when updating an existing plan
181
+ const existingCreatedAt = serverState.activePlans[effectivePlanId]?.createdAt;
171
182
  // Create a draft plan with abstract — tasks are generated later
172
183
  const plan = {
173
- id: planId,
184
+ id: effectivePlanId,
174
185
  status: 'draft',
175
186
  title: parsed.title,
176
187
  originalPrompt: prompt,
177
188
  abstract: parsed.abstract || parsed.message || '',
178
189
  buildPrompt: parsed.buildPrompt,
179
190
  taskIds: [],
180
- createdAt: new Date().toISOString(),
191
+ createdAt: existingCreatedAt || new Date().toISOString(),
181
192
  };
182
193
  return {
183
194
  type: 'build',
@@ -221,6 +221,7 @@ export function loadFromEntityStore(workingDir) {
221
221
  }
222
222
  const state = {
223
223
  activePlans,
224
+ activePlanId: null,
224
225
  tasks: activeTasks,
225
226
  agents: activeAgents,
226
227
  visuals: visuals || {},
package/dist/state.js CHANGED
@@ -16,6 +16,7 @@ function createMasterId() {
16
16
  function createEmptyState() {
17
17
  return {
18
18
  activePlans: {},
19
+ activePlanId: null,
19
20
  tasks: {},
20
21
  agents: {},
21
22
  visuals: {},
@@ -136,6 +137,10 @@ export function broadcast(action) {
136
137
  export function broadcastActivePlans() {
137
138
  broadcast({ type: 'active_plans_set', activePlans: state.activePlans });
138
139
  }
140
+ /** Broadcast active plan ID to all clients. */
141
+ export function broadcastActivePlanId() {
142
+ broadcast({ type: 'active_plan_id_set', activePlanId: state.activePlanId });
143
+ }
139
144
  /** Broadcast a single plan (or deletion) to all clients. */
140
145
  export function broadcastSinglePlan(planId) {
141
146
  broadcast({ type: 'plan_set', planId, plan: state.activePlans[planId] ?? null });
@@ -190,6 +195,9 @@ export function broadcastForPath(dotPath) {
190
195
  const parts = dotPath.split('.');
191
196
  const root = parts[0];
192
197
  switch (root) {
198
+ case 'activePlanId':
199
+ broadcastActivePlanId();
200
+ break;
193
201
  case 'activePlans':
194
202
  if (parts[1]) {
195
203
  broadcastSinglePlan(parts[1]);