junecoder 1.0.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 ADDED
@@ -0,0 +1,2 @@
1
+ # junecode
2
+ # junecode
package/agent.mjs ADDED
@@ -0,0 +1,650 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
2
+ import { join, relative, basename } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { homedir } from 'node:os';
5
+ import { loadSkills, formatSkillListing } from './skills.mjs';
6
+
7
+ // ─── Constants ───────────────────────────────────────────────────────────────
8
+
9
+ export const DEFAULT_MAX_TURNS = 50;
10
+ export const DEFAULT_SUBAGENT_TURNS = 20;
11
+ export const DEFAULT_GOAL_TURNS = 30;
12
+ export const MIN_REPORT_CHARS = 50;
13
+ export const REPORT_CONTINUATION = '... [content truncated]';
14
+ export const TOOL_RESULT_OFFLOAD_LIMIT = 8000;
15
+ export const TOOL_RESULT_PREVIEW = 500;
16
+ export const AUTO_REMINDER =
17
+ '[System reminder: working directory snapshot is provided at session start and after tool executions that may change it.]';
18
+ export const MAX_INSTRUCTION_CHARS = 32_000;
19
+
20
+ // ─── Pure Helpers ────────────────────────────────────────────────────────────
21
+
22
+ /** Escape XML special characters. */
23
+ export function escapeXml(s) {
24
+ return String(s)
25
+ .replace(/&/g, '&')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;');
28
+ }
29
+
30
+ /**
31
+ * Canonicalize a tool call signature for duplication detection.
32
+ * Returns "name:normalized-args" or null on parse failure.
33
+ */
34
+ export function tryCanonicalize(name, args) {
35
+ if (args === undefined || args === null) return `${name}:`;
36
+ try {
37
+ const parsed = typeof args === 'string' ? JSON.parse(args) : args;
38
+ return `${name}:${JSON.stringify(parsed, Object.keys(parsed).sort())}`;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Repair history: strip transient messages, ensure system prompt is first,
46
+ * validate message structure.
47
+ */
48
+ export function repairHistory(history) {
49
+ if (!Array.isArray(history)) return [];
50
+
51
+ const cleaned = [];
52
+ let systemIdx = -1;
53
+
54
+ for (let i = 0; i < history.length; i++) {
55
+ const msg = history[i];
56
+ // Skip null/undefined or messages without a valid role
57
+ if (!msg || typeof msg.role !== 'string') continue;
58
+ // Skip transient messages
59
+ if (msg.transient || msg._transient) continue;
60
+
61
+ // Clone to avoid mutating original
62
+ const copy = { role: msg.role, content: msg.content };
63
+ if (msg.tool_calls) copy.tool_calls = msg.tool_calls;
64
+ if (msg.tool_call_id) copy.tool_call_id = msg.tool_call_id;
65
+ if (msg.name) copy.name = msg.name;
66
+ if (msg.reasoning_content) copy.reasoning_content = msg.reasoning_content;
67
+
68
+ if (copy.role === 'system') {
69
+ systemIdx = cleaned.length;
70
+ }
71
+ cleaned.push(copy);
72
+ }
73
+
74
+ // Move first system message to index 0 if it's not already there
75
+ if (systemIdx > 0) {
76
+ const sysMsg = cleaned.splice(systemIdx, 1)[0];
77
+ cleaned.unshift(sysMsg);
78
+ }
79
+
80
+ // Drop orphaned tool messages: a "tool" message is only valid as a response
81
+ // to a preceding assistant message carrying the matching tool_calls id.
82
+ // Truncation can cut the parent away, and providers reject the request (400).
83
+ const knownCallIds = new Set();
84
+ const repaired = [];
85
+ for (const msg of cleaned) {
86
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
87
+ for (const tc of msg.tool_calls) {
88
+ if (tc && tc.id) knownCallIds.add(tc.id);
89
+ }
90
+ }
91
+ if (msg.role === 'tool' && !knownCallIds.has(msg.tool_call_id)) continue;
92
+ repaired.push(msg);
93
+ }
94
+
95
+ return repaired;
96
+ }
97
+
98
+ // ─── File System Helpers ─────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Generate a tree-style listing of a working directory.
102
+ * @param {string} cwd - directory to list
103
+ * @param {{ maxDepth?: number, maxEntries?: number }} [opts]
104
+ * @returns {string} tree text
105
+ */
106
+ export function listWorkDir(cwd, opts = {}) {
107
+ const { maxDepth = 3, maxEntries = 200 } = opts;
108
+ let count = 0;
109
+
110
+ function walk(dir, prefix, depth) {
111
+ if (depth > maxDepth || count >= maxEntries) return '';
112
+
113
+ let result = '';
114
+ let entries;
115
+ try {
116
+ entries = readdirSync(dir, { withFileTypes: true });
117
+ } catch {
118
+ return '';
119
+ }
120
+
121
+ // Sort: directories first, then files, alphabetically
122
+ entries.sort((a, b) => {
123
+ if (a.isDirectory() && !b.isDirectory()) return -1;
124
+ if (!a.isDirectory() && b.isDirectory()) return 1;
125
+ return a.name.localeCompare(b.name);
126
+ });
127
+
128
+ for (let i = 0; i < entries.length; i++) {
129
+ if (count >= maxEntries) {
130
+ result += `${prefix}... (max entries reached)\n`;
131
+ break;
132
+ }
133
+ const entry = entries[i];
134
+ // Skip hidden and node_modules
135
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
136
+
137
+ const isLast = i === entries.length - 1;
138
+ const connector = isLast ? '└── ' : '├── ';
139
+ const relPath = relative(cwd, join(dir, entry.name));
140
+
141
+ count++;
142
+ if (entry.isDirectory()) {
143
+ result += `${prefix}${connector}${entry.name}/\n`;
144
+ result += walk(
145
+ join(dir, entry.name),
146
+ prefix + (isLast ? ' ' : '│ '),
147
+ depth + 1,
148
+ );
149
+ } else {
150
+ try {
151
+ const st = statSync(join(dir, entry.name));
152
+ result += `${prefix}${connector}${entry.name} (${st.size} bytes)\n`;
153
+ } catch {
154
+ result += `${prefix}${connector}${entry.name}\n`;
155
+ }
156
+ }
157
+ }
158
+ return result;
159
+ }
160
+
161
+ const header = `Working directory: ${cwd}\n`;
162
+ const tree = walk(cwd, '', 1);
163
+ return header + (tree || '(empty or inaccessible)\n');
164
+ }
165
+
166
+ /**
167
+ * Collect git context from a working directory.
168
+ * @param {string} cwd
169
+ * @returns {string} git status, branch, and recent log
170
+ */
171
+ export function collectGitContext(cwd) {
172
+ const parts = [];
173
+ const run = (cmd) => {
174
+ try {
175
+ return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
176
+ } catch {
177
+ return null;
178
+ }
179
+ };
180
+
181
+ const branch = run('git rev-parse --abbrev-ref HEAD');
182
+ if (branch) parts.push(`Branch: ${branch}`);
183
+
184
+ const status = run('git status --short');
185
+ if (status !== null) {
186
+ parts.push(status ? `Status:\n${status}` : 'Status: clean');
187
+ }
188
+
189
+ const log = run('git --no-pager log --oneline -5');
190
+ if (log) parts.push(`Recent commits:\n${log}`);
191
+
192
+ const diff = run('git diff --stat');
193
+ if (diff) parts.push(`Unstaged changes:\n${diff}`);
194
+
195
+ return parts.length > 0 ? parts.join('\n\n') : '(not a git repository or git unavailable)';
196
+ }
197
+
198
+ // ─── Project Instructions ─────────────────────────────────────────────────────
199
+
200
+ /**
201
+ * Load project instructions from AGENTS.md, PROJECT_RULES.md etc.
202
+ * Reads from both ~/.junecode/ and the local project directory.
203
+ * @param {string} cwd
204
+ * @returns {string} combined instructions
205
+ */
206
+ export function loadProjectInstructions(cwd) {
207
+ const sources = [
208
+ join(homedir(), '.junecode', 'AGENTS.md'),
209
+ join(cwd, 'AGENTS.md'),
210
+ join(cwd, 'PROJECT_RULES.md'),
211
+ join(cwd, '.cursorrules'),
212
+ join(cwd, '.windsurfrules'),
213
+ ];
214
+
215
+ const contents = [];
216
+ for (const src of sources) {
217
+ if (!existsSync(src)) continue;
218
+ try {
219
+ const text = readFileSync(src, 'utf-8').trim();
220
+ if (text) {
221
+ const label = basename(src);
222
+ contents.push(`--- ${label} ---\n${text}`);
223
+ }
224
+ } catch {
225
+ // skip unreadable files
226
+ }
227
+ }
228
+
229
+ if (contents.length === 0) return '';
230
+
231
+ let combined = contents.join('\n\n');
232
+ if (combined.length > MAX_INSTRUCTION_CHARS) {
233
+ combined =
234
+ combined.slice(0, MAX_INSTRUCTION_CHARS) +
235
+ `\n\n[WARNING: Instructions exceeded ${MAX_INSTRUCTION_CHARS} chars and were truncated.]`;
236
+ }
237
+ return combined;
238
+ }
239
+
240
+ // ─── System Prompt ────────────────────────────────────────────────────────────
241
+
242
+ export const DEFAULT_SYSTEM_PROMPT = `You are JuneCode, a coding agent. Thin means sharp: you are a terse, precise engineer who cuts straight to the point—no fluff, no showing off, no filler. You write the most minimal, elegant code that solves the problem, and you say things in as few words as the truth allows.
243
+
244
+ Rules:
245
+ - Prefer tool calls over guessing. Read files before modifying them.
246
+ - When you need multiple independent pieces of information, make all independent tool calls in the SAME response so they can run in parallel.
247
+ - Be concise in your final answers. Report what you did, not what you plan to do.
248
+ - When the user asks a question, answer it. When they describe a task, do it. When unsure which they meant, ask before acting—once. Never guess at ambiguous intent.
249
+ - Never fabricate file contents or command outputs; only trust tool results.
250
+ - Run shell commands non-interactively: git commit -m, git --no-pager, -y/--yes flags where applicable.
251
+ - Make MINIMAL changes: fix the bug, don't refactor the file; ship the feature, don't add configurability nobody asked for.
252
+ - Never run git commit/push unless the user explicitly asks.
253
+ - After changing behavior, sweep comments and docstrings that now describe the old behavior.
254
+ - Before your final reply, re-read the user's latest request and confirm you are answering that one.
255
+ - Before declaring a coding task complete, use the verify tool. If tests exist, run them and confirm they pass.`;
256
+
257
+ // ─── Default Callbacks ────────────────────────────────────────────────────────
258
+
259
+ function defaultCallbacks(overrides = {}) {
260
+ return {
261
+ onToken: () => {},
262
+ onReasoning: () => {},
263
+ onToolCall: () => {},
264
+ onToolResult: () => {},
265
+ onToolOutput: () => {},
266
+ onPermissionRequest: async () => true,
267
+ onCompress: () => {},
268
+ onUsage: () => {},
269
+ onTurnEnd: () => {},
270
+ onTaskUpdate: () => {},
271
+ onQuestion: async () => '',
272
+ ...overrides,
273
+ };
274
+ }
275
+
276
+ // ─── ContinueError ────────────────────────────────────────────────────────────
277
+
278
+ export class ContinueError extends Error {
279
+ constructor(message, turn) {
280
+ super(message);
281
+ this.name = 'ContinueError';
282
+ this.turn = turn;
283
+ }
284
+ }
285
+
286
+ // ─── Agent Factory ────────────────────────────────────────────────────────────
287
+
288
+ export function createAgent(opts) {
289
+ return {
290
+ provider: opts.provider || {},
291
+ tools: opts.tools || [],
292
+ config: opts.config || {},
293
+ cwd: opts.cwd || process.cwd(),
294
+ memory: opts.memory || null,
295
+ overlay: opts.overlay || null,
296
+ history: [],
297
+ tasks: [],
298
+ planMode: false,
299
+ goal: null,
300
+ _permQueue: Promise.resolve(),
301
+ _sessionStart: null,
302
+ _turnsSinceTaskUpdate: 0,
303
+ _turnsInPlanMode: 0,
304
+ _mutatedThisRun: false,
305
+ _verifiedThisRun: false,
306
+ _pendingReminders: [],
307
+ _recentCallSigs: [],
308
+ _blockTally: {},
309
+ _mcpProcesses: [],
310
+ _compressFailures: 0,
311
+ _depth: opts.depth || 0,
312
+ };
313
+ }
314
+
315
+ // ─── runAgent Main Loop ───────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Execute the ReAct agent loop.
319
+ *
320
+ * @param {object} agent - agent instance from createAgent()
321
+ * @param {string} input - user input / task description
322
+ * @param {object} [callbacks] - optional callback overrides
323
+ * @param {object} [options] - runtime options
324
+ * @param {number} [options.depth=0] - nesting depth (0 = top-level)
325
+ * @param {AbortSignal} [options.signal] - abort signal
326
+ * @returns {Promise<string>} final response content
327
+ */
328
+ export async function runAgent(agent, input, callbacks = {}, options = {}) {
329
+ const { depth = 0, signal } = options;
330
+ agent._depth = depth;
331
+
332
+ const cb = defaultCallbacks(callbacks);
333
+
334
+ // ── Initialization ──────────────────────────────────────────────────────
335
+
336
+ const maxTurns = depth > 0
337
+ ? (agent.config.agent?.subagentTurns || DEFAULT_SUBAGENT_TURNS)
338
+ : (agent.config.agent?.maxTurns || DEFAULT_MAX_TURNS);
339
+
340
+ const compactThreshold = agent.config.agent?.compactThreshold || 750_000;
341
+
342
+ // Repair history on first run
343
+ if (agent.history.length > 0) {
344
+ agent.history = repairHistory(agent.history);
345
+ }
346
+
347
+ // Set session start timestamp once
348
+ if (!agent._sessionStart) {
349
+ agent._sessionStart = Date.now();
350
+ }
351
+
352
+ // Build system prompt
353
+ const sysPrompt = buildSystemPrompt(agent, depth);
354
+
355
+ // Inject initial messages if history is empty
356
+ if (agent.history.length === 0) {
357
+ agent.history.push({ role: 'system', content: sysPrompt });
358
+ }
359
+
360
+ // Inject environment snapshot + memories + user input
361
+ const envSnapshot = [
362
+ listWorkDir(agent.cwd),
363
+ collectGitContext(agent.cwd),
364
+ ].filter(Boolean).join('\n\n');
365
+
366
+ const projInstr = loadProjectInstructions(agent.cwd);
367
+
368
+ const transientBlocks = [];
369
+ transientBlocks.push(`Working directory: ${agent.cwd}\n${envSnapshot}`);
370
+ if (projInstr) transientBlocks.push(`Project instructions:\n${projInstr}`);
371
+
372
+ // Search memory if available
373
+ if (agent.memory) {
374
+ try {
375
+ const { search } = await import('./memory.mjs');
376
+ const memResult = await search(agent.memory, input, { limit: 5 });
377
+ if (memResult) transientBlocks.push(`Relevant memories:\n${memResult}`);
378
+ } catch { /* memory is optional */ }
379
+ }
380
+
381
+ // Inject all transient context as user messages
382
+ if (transientBlocks.length > 0) {
383
+ agent.history.push({
384
+ role: 'user',
385
+ content: `[System context]\n\n${transientBlocks.join('\n\n')}`,
386
+ transient: true,
387
+ });
388
+ }
389
+
390
+ // Push AUTO_REMINDER
391
+ agent.history.push({ role: 'user', content: AUTO_REMINDER, transient: true });
392
+
393
+ // Push the actual user input
394
+ agent.history.push({ role: 'user', content: input });
395
+
396
+ // Assemble tools (base + meta — rebuilt each turn for dynamic tools like MCP)
397
+ const toolsModule = await import('./tools.mjs');
398
+ const { metaTools } = await import('./metaTools.mjs');
399
+
400
+ // ── Main Loop ───────────────────────────────────────────────────────────
401
+
402
+ for (let turn = 0; turn < maxTurns; turn++) {
403
+ // Rebuild tools each turn so dynamically registered tools (e.g. MCP) are visible
404
+ let allTools = [...agent.tools, ...toolsModule.baseTools];
405
+ const filteredMeta = depth >= 2
406
+ ? metaTools.filter((t) => t.name !== 'subagent')
407
+ : metaTools;
408
+ allTools = [...allTools, ...filteredMeta];
409
+
410
+ const toolByName = new Map();
411
+ for (const t of allTools) {
412
+ toolByName.set(t.name, t);
413
+ }
414
+
415
+ // Build OpenAI-format tool schemas for the provider
416
+ const toolSchemas = allTools.map((t) => toolsModule.toOpenAISchema(t));
417
+ // Step 1: Increment counters
418
+ agent._turnsSinceTaskUpdate++;
419
+ if (agent.planMode) {
420
+ agent._turnsInPlanMode++;
421
+ }
422
+
423
+ // Step 2: Compress check
424
+ try {
425
+ const { checkAndCompress } = await import('./context.mjs');
426
+ const didCompress = await checkAndCompress(agent, compactThreshold);
427
+ if (didCompress) {
428
+ // Re-inject AUTO_REMINDER after compression
429
+ agent.history.push({ role: 'user', content: AUTO_REMINDER, transient: true });
430
+ }
431
+ } catch { /* compression is non-fatal */ }
432
+
433
+ // Step 3: Build messages (clean transient for LLM)
434
+ const messages = repairHistory(agent.history);
435
+
436
+ // Call LLM
437
+ const { chat } = await import('./provider.mjs');
438
+ const provider = agent.provider;
439
+
440
+ const response = await chat(provider, {
441
+ messages,
442
+ tools: toolSchemas.length > 0 ? toolSchemas : undefined,
443
+ onToken: cb.onToken,
444
+ onReasoning: cb.onReasoning,
445
+ signal,
446
+ });
447
+
448
+ // Report usage
449
+ if (response.usage) {
450
+ try { cb.onUsage(response.usage); } catch { /* ignore */ }
451
+ }
452
+
453
+ // Step 4: No tool_calls — completion guard
454
+ if (!response.toolCalls || response.toolCalls.length === 0) {
455
+ // Guard: if files were mutated but not verified, inject reminder
456
+ if (agent._mutatedThisRun && !agent._verifiedThisRun && !agent.planMode) {
457
+ agent._pendingReminders.push(
458
+ '[System reminder: you modified files but have not verified the changes. Call verify before finishing.]',
459
+ );
460
+ agent._mutatedThisRun = false;
461
+ // Push a reminder for the next turn
462
+ if (agent._pendingReminders.length > 0) {
463
+ for (const reminder of agent._pendingReminders) {
464
+ agent.history.push({ role: 'user', content: reminder, transient: true });
465
+ }
466
+ agent._pendingReminders = [];
467
+ }
468
+ continue; // Loop again to let the model respond to the reminder
469
+ }
470
+
471
+ // Push assistant response to history and return
472
+ const assistantMsg = { role: 'assistant', content: response.content };
473
+ if (response.reasoning) assistantMsg.reasoning_content = response.reasoning;
474
+ agent.history.push(assistantMsg);
475
+
476
+ return response.content || '';
477
+ }
478
+
479
+ // Step 5: Tool calls — push assistant message
480
+ const assistantMsg = {
481
+ role: 'assistant',
482
+ content: response.content || null,
483
+ tool_calls: response.toolCalls,
484
+ };
485
+ if (response.reasoning) assistantMsg.reasoning_content = response.reasoning;
486
+ agent.history.push(assistantMsg);
487
+
488
+ // Step 6: Execute tool calls
489
+ const { executeToolCalls } = await import('./executor.mjs');
490
+ const results = await executeToolCalls(agent, toolByName, response.toolCalls, cb, depth, signal);
491
+
492
+ // Step 7: Process results
493
+ for (let i = 0; i < results.length; i++) {
494
+ const r = results[i];
495
+ const tc = response.toolCalls[i];
496
+
497
+ const toolMsg = {
498
+ role: 'tool',
499
+ tool_call_id: r.id,
500
+ name: r.name,
501
+ content: r.error ? `Error: ${r.error}` : (r.output || ''),
502
+ };
503
+ agent.history.push(toolMsg);
504
+
505
+ // Track mutations
506
+ if (!r.error && !r.denied) {
507
+ const tool = toolByName.get(r.name);
508
+ if (tool && !tool.readonly) {
509
+ agent._mutatedThisRun = true;
510
+ // Reset verified flag when new mutations happen
511
+ agent._verifiedThisRun = false;
512
+
513
+ // Reindex file in memory if applicable
514
+ if (agent.memory && r.name === 'write' && tc?.function?.arguments) {
515
+ try {
516
+ const args = JSON.parse(tc.function.arguments);
517
+ if (args.path) {
518
+ const absPath = args.path.startsWith('/')
519
+ ? args.path
520
+ : join(agent.cwd, args.path);
521
+ const { reindexFile } = await import('./memory.mjs');
522
+ await reindexFile(agent.memory, agent.cwd, absPath);
523
+ }
524
+ } catch { /* non-fatal */ }
525
+ }
526
+ }
527
+
528
+ // Track verify
529
+ if (r.name === 'verify') {
530
+ agent._verifiedThisRun = true;
531
+ }
532
+ }
533
+
534
+ // Report result
535
+ try { cb.onToolResult(r.name, r.output, r.error); } catch { /* ignore */ }
536
+ }
537
+
538
+ // Step 8: Inject pending reminders
539
+ if (agent._pendingReminders.length > 0) {
540
+ for (const reminder of agent._pendingReminders) {
541
+ agent.history.push({ role: 'user', content: reminder, transient: true });
542
+ }
543
+ agent._pendingReminders = [];
544
+ }
545
+
546
+ // Step 9: Stagnation detection
547
+ if (results.length > 0) {
548
+ const sigs = [];
549
+ for (let i = 0; i < results.length; i++) {
550
+ const tc = response.toolCalls[i];
551
+ if (tc) {
552
+ const sig = tryCanonicalize(tc.function?.name, tc.function?.arguments);
553
+ if (sig) sigs.push(sig);
554
+ }
555
+ }
556
+ const sigKey = sigs.join(';');
557
+ agent._recentCallSigs.push(sigKey);
558
+ if (agent._recentCallSigs.length > 4) {
559
+ agent._recentCallSigs.shift();
560
+ }
561
+ // Check last 3
562
+ if (
563
+ agent._recentCallSigs.length >= 3 &&
564
+ agent._recentCallSigs[agent._recentCallSigs.length - 1] === sigKey &&
565
+ agent._recentCallSigs[agent._recentCallSigs.length - 2] === sigKey &&
566
+ agent._recentCallSigs[agent._recentCallSigs.length - 3] === sigKey
567
+ ) {
568
+ agent.history.push({
569
+ role: 'user',
570
+ content: '[System reminder: you appear to be stuck in a loop — the same tool calls have been made 3 times in a row. Try a different approach or ask for clarification.]',
571
+ transient: true,
572
+ });
573
+ }
574
+ }
575
+
576
+ // Step 10: Goal progress tracking
577
+ if (agent.goal) {
578
+ const goalTurns = agent.config.agent?.goalTurns || DEFAULT_GOAL_TURNS;
579
+ const progress = Math.floor((turn / goalTurns) * 100);
580
+ if (turn > 0 && turn % 10 === 0) {
581
+ agent.history.push({
582
+ role: 'user',
583
+ content: `[Goal progress: turn ${turn}/${goalTurns} (${Math.min(progress, 100)}%). Objective: "${agent.goal.objective}". Criteria: ${agent.goal.criteria || 'none'}]`,
584
+ transient: true,
585
+ });
586
+ }
587
+ }
588
+
589
+ // Step 11: Task list reminder (every 10 turns, top-level only)
590
+ if (depth === 0 && agent._turnsSinceTaskUpdate >= 10 && agent.tasks.length > 0) {
591
+ const incomplete = agent.tasks.filter((t) => t.status !== 'done').length;
592
+ if (incomplete > 0) {
593
+ agent.history.push({
594
+ role: 'user',
595
+ content: `[Task reminder: ${incomplete} tasks still pending. Use task tool to update status.]`,
596
+ transient: true,
597
+ });
598
+ }
599
+ }
600
+
601
+ // Step 12: Plan mode guidance (every 8 turns)
602
+ if (agent.planMode && agent._turnsInPlanMode >= 8) {
603
+ agent.history.push({
604
+ role: 'user',
605
+ content: `[You have been in plan mode for ${agent._turnsInPlanMode} turns. If you have enough information, consider exiting plan mode to implement.]`,
606
+ transient: true,
607
+ });
608
+ }
609
+
610
+ // Step 13: Turn end callback
611
+ try { cb.onTurnEnd(turn, maxTurns); } catch { /* ignore */ }
612
+ }
613
+
614
+ // Loop exhausted without returning
615
+ throw new ContinueError(`Agent exceeded max turns (${maxTurns}) without completing the task.`, maxTurns);
616
+ }
617
+
618
+ // ─── System Prompt Builder ────────────────────────────────────────────────────
619
+
620
+ function buildSystemPrompt(agent, depth) {
621
+ let prompt = DEFAULT_SYSTEM_PROMPT;
622
+
623
+ // Apply overlay if present (e.g., sub-agent role instructions)
624
+ if (agent.overlay) {
625
+ const overlayText = Array.isArray(agent.overlay)
626
+ ? agent.overlay.join('\n')
627
+ : String(agent.overlay);
628
+ prompt += `\n\n${overlayText}`;
629
+ }
630
+
631
+ // Sub-agent specific note
632
+ if (depth > 0) {
633
+ prompt += `\n\nYou are a sub-agent at depth ${depth}. Work independently on your assigned task and return a concise report. Do not ask follow-up questions — complete the task yourself.`;
634
+ }
635
+
636
+ // Append project instructions
637
+ const projInstr = loadProjectInstructions(agent.cwd);
638
+ if (projInstr) {
639
+ prompt += `\n\n--- Project Instructions ---\n${projInstr}`;
640
+ }
641
+
642
+ // Append skills listing
643
+ try {
644
+ const skills = loadSkills(agent.cwd);
645
+ const skillListing = formatSkillListing(skills);
646
+ prompt += `\n\n--- Available Skills ---\n${skillListing}`;
647
+ } catch { /* skills module may not be available */ }
648
+
649
+ return prompt;
650
+ }
package/checkpoint.mjs ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Git checkpoint management — stub implementation.
3
+ *
4
+ * Provides save/restore of git state for before/after tool executions.
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+
8
+ /** Create a git stash-based checkpoint. Skips if working tree already has uncommitted changes (to avoid hiding user's work). */
9
+ export async function createCheckpoint(cwd) {
10
+ try {
11
+ // Bail if there are pre-existing uncommitted changes — we must not hide user's work
12
+ const status = execSync('git status --porcelain', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
13
+ if (status) return;
14
+ execSync('git stash push -m "junecode-checkpoint"', { cwd, stdio: 'ignore' });
15
+ } catch {
16
+ // Not a git repo or no changes to stash
17
+ }
18
+ }
19
+
20
+ /** List available junecode checkpoints. */
21
+ export async function listCheckpoints(cwd) {
22
+ try {
23
+ const out = execSync('git stash list', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
24
+ return out.split('\n').filter(Boolean).map((line, i) => ({ id: i, summary: line }));
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ /** Rewind to a specific checkpoint. */
31
+ export async function rewind(cwd, id) {
32
+ throw new Error('Checkpoint rewind not implemented yet.');
33
+ }
34
+
35
+ /** Check if cwd is inside a git repo. */
36
+ export function isGitRepo(cwd) {
37
+ try {
38
+ execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' });
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }