@causyne/agent 1.10.17 → 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.
- package/docs/ARCHITECTURE.md +25 -8
- package/docs/SECURITY_REVIEW.md +4 -4
- package/docs/THIRD_PARTY_NOTICES.md +3 -0
- package/package.json +1 -1
- package/src/agentLoop.js +99 -19
- package/src/browser/{camofoxRuntime.js → browserRuntime.js} +10 -15
- package/src/browser/{camofoxRuntime.ts → browserRuntime.ts} +11 -16
- package/src/browser/causyneBrowser.js +6 -6
- package/src/browser/causyneBrowser.ts +9 -9
- package/src/cliMode.js +13 -1
- package/src/decisionPolicy.js +68 -0
- package/src/decisionPolicy.ts +106 -0
- package/src/implementationPlan.js +50 -0
- package/src/implementationPlan.ts +59 -0
- package/src/provisioning/capabilityProvisioner.js +1 -1
- package/src/taskShape.js +1 -1
- package/src/taskShape.ts +1 -1
- package/src/toolPacks.js +7 -5
- package/src/tools/browserAutomator.js +1 -1
- package/src/tools/browserAutomator.ts +1 -1
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/registry.js +4 -3
- package/src/tools/requestDecision.js +87 -0
- package/src/tools/requestDecision.ts +107 -0
- package/src/tools/updatePlan.js +37 -84
- package/src/tools/updatePlan.ts +48 -0
- package/src/tools/webFetch.js +1 -1
- package/src/tools/webFetch.ts +1 -1
- package/src/tools/webSearch.js +1 -1
- package/src/tools/webSearch.ts +1 -1
- package/src/tui/ask.js +41 -1
- package/src/tui/chatRenderer.js +28 -0
- package/src/tui/dialogs.js +91 -0
- package/src/tui/prompt.js +32 -3
- package/src/tui/toolDisplay.js +0 -1
- package/src/tools/todoWrite.js +0 -75
package/src/tools/updatePlan.js
CHANGED
|
@@ -1,88 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
1
|
+
import { normalizePlanUpdate, summarizePlan } from '../implementationPlan.js';
|
|
4
2
|
export const updatePlan = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
};
|
package/src/tools/webFetch.js
CHANGED
|
@@ -144,7 +144,7 @@ export const webFetch = {
|
|
|
144
144
|
if (!response.ok && !args.outputFile && contentType.includes('text/html') && [401, 403, 429, 503].includes(response.status)) {
|
|
145
145
|
const fallback = await browserFallback(finalUrl.href);
|
|
146
146
|
if (fallback.success)
|
|
147
|
-
return { ...fallback, status: response.status, finalUrl: finalUrl.href, via: '
|
|
147
|
+
return { ...fallback, status: response.status, finalUrl: finalUrl.href, via: 'causyne-browser' };
|
|
148
148
|
return { success: false, error: `HTTP ${response.status}; browser fallback failed: ${fallback.error}`, requiresConsent: fallback.requiresConsent };
|
|
149
149
|
}
|
|
150
150
|
if (!response.ok)
|
package/src/tools/webFetch.ts
CHANGED
|
@@ -142,7 +142,7 @@ export const webFetch = {
|
|
|
142
142
|
|
|
143
143
|
if (!response.ok && !args.outputFile && contentType.includes('text/html') && [401, 403, 429, 503].includes(response.status)) {
|
|
144
144
|
const fallback = await browserFallback(finalUrl.href);
|
|
145
|
-
if (fallback.success) return { ...fallback, status: response.status, finalUrl: finalUrl.href, via: '
|
|
145
|
+
if (fallback.success) return { ...fallback, status: response.status, finalUrl: finalUrl.href, via: 'causyne-browser' };
|
|
146
146
|
return { success: false, error: `HTTP ${response.status}; browser fallback failed: ${fallback.error}`, requiresConsent: fallback.requiresConsent };
|
|
147
147
|
}
|
|
148
148
|
if (!response.ok) return { success: false, error: `HTTP ${response.status} ${response.statusText}`.trim(), status: response.status, finalUrl: finalUrl.href };
|
package/src/tools/webSearch.js
CHANGED
|
@@ -14,7 +14,7 @@ function renderResults(query, results) {
|
|
|
14
14
|
export const webSearch = {
|
|
15
15
|
schema: {
|
|
16
16
|
name: 'web_search',
|
|
17
|
-
description: 'Search the public web through lightweight providers, with the optional Causyne
|
|
17
|
+
description: 'Search the public web through lightweight providers, with the optional Causyne Browser as a rendered-page fallback. No browser binary is downloaded automatically.',
|
|
18
18
|
parameters: {
|
|
19
19
|
type: 'object',
|
|
20
20
|
properties: {
|
package/src/tools/webSearch.ts
CHANGED
|
@@ -15,7 +15,7 @@ function renderResults(query: string, results: SearchResult[]): string {
|
|
|
15
15
|
export const webSearch = {
|
|
16
16
|
schema: {
|
|
17
17
|
name: 'web_search',
|
|
18
|
-
description: 'Search the public web through lightweight providers, with the optional Causyne
|
|
18
|
+
description: 'Search the public web through lightweight providers, with the optional Causyne Browser as a rendered-page fallback. No browser binary is downloaded automatically.',
|
|
19
19
|
parameters: {
|
|
20
20
|
type: 'object',
|
|
21
21
|
properties: {
|
package/src/tui/ask.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import readline from 'readline';
|
|
27
27
|
import { ChatPrompt, isInteractive } from './prompt.js';
|
|
28
|
-
import { SelectLayer, InputLayer, ConfirmLayer } from './dialogs.js';
|
|
28
|
+
import { SelectLayer, InputLayer, ConfirmLayer, DecisionLayer } from './dialogs.js';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Run one layer on a temporary prompt host.
|
|
@@ -162,3 +162,43 @@ export async function askConfirm(message) {
|
|
|
162
162
|
const raw = await fallbackQuestion(`${message} [y/N]`);
|
|
163
163
|
return /^y(es)?$/i.test(String(raw).trim());
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Ask for a durable product or architecture choice.
|
|
168
|
+
*
|
|
169
|
+
* This remains separate from permissions: choosing an implementation direction
|
|
170
|
+
* never grants a tool the right to mutate files or execute a command.
|
|
171
|
+
*/
|
|
172
|
+
export async function askDecision(request = {}) {
|
|
173
|
+
const options = Array.isArray(request.options) ? request.options : [];
|
|
174
|
+
|
|
175
|
+
if (canAskInPanel()) {
|
|
176
|
+
const answer = await hostOneLayer((resolve) => new DecisionLayer({
|
|
177
|
+
request,
|
|
178
|
+
onAnswer: resolve
|
|
179
|
+
}));
|
|
180
|
+
if (answer !== undefined) return answer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const custom = { id: '__custom__', label: 'Type your own answer', description: '' };
|
|
184
|
+
const selected = await askSelect({
|
|
185
|
+
title: request.header || 'Decision required',
|
|
186
|
+
items: [...options, custom]
|
|
187
|
+
});
|
|
188
|
+
if (!selected) return null;
|
|
189
|
+
if (selected.id === custom.id) {
|
|
190
|
+
const label = await askText({
|
|
191
|
+
title: request.question || 'Describe the direction you want',
|
|
192
|
+
lines: request.reason ? [request.reason] : [],
|
|
193
|
+
validate: (value) => String(value).trim() ? null : 'An answer is required.'
|
|
194
|
+
});
|
|
195
|
+
if (!label) return null;
|
|
196
|
+
return { requestId: request.id, optionId: null, label: String(label).trim(), custom: true };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
requestId: request.id,
|
|
200
|
+
optionId: selected.id,
|
|
201
|
+
label: selected.label,
|
|
202
|
+
custom: false
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/tui/chatRenderer.js
CHANGED
|
@@ -211,6 +211,7 @@ export class ChatRenderer {
|
|
|
211
211
|
this.spinnerIndex = 0;
|
|
212
212
|
this.currentStatus = '';
|
|
213
213
|
this.pendingApprovals = new Map();
|
|
214
|
+
this.latestPlan = [];
|
|
214
215
|
this.status = new StatusLine();
|
|
215
216
|
this.turnStartedAt = null;
|
|
216
217
|
|
|
@@ -548,9 +549,34 @@ export class ChatRenderer {
|
|
|
548
549
|
onAgentPlan(msg) {
|
|
549
550
|
if (!Array.isArray(msg.plan) || msg.plan.length === 0) return;
|
|
550
551
|
const plan = msg.plan.slice();
|
|
552
|
+
this.latestPlan = plan;
|
|
553
|
+
if (this.onPlanChange) this.onPlanChange(plan);
|
|
551
554
|
this.frame('plan', () => this.renderPlan(plan), { items: plan.length });
|
|
552
555
|
}
|
|
553
556
|
|
|
557
|
+
onDecisionRequest(msg) {
|
|
558
|
+
if (this.dialogWillAsk) return;
|
|
559
|
+
const request = msg.request || {};
|
|
560
|
+
this.frame('approval', () => {
|
|
561
|
+
this.write();
|
|
562
|
+
this.write(`${paint('accent', GLYPHS.arrow)} ${bold(safeDisplay(request.header || 'decision required'))}`);
|
|
563
|
+
for (const row of wrapBlock(safeDisplay(request.question || ''), Math.max(20, this.width() - 4))) this.write(` ${row}`);
|
|
564
|
+
this.write();
|
|
565
|
+
}, { kind: request.kind || 'other' });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
onDecisionResult(msg) {
|
|
569
|
+
const request = msg.request || {};
|
|
570
|
+
const answer = msg.answer;
|
|
571
|
+
this.frame('approval', () => {
|
|
572
|
+
if (!answer) {
|
|
573
|
+
this.write(` ${paint('muted', 'decision dismissed')} ${safeDisplay(request.header || '')}`);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
this.write(` ${paint('success', GLYPHS.check)} ${bold(safeDisplay(answer.label || ''))}${answer.custom ? dim(' custom choice') : ''}`);
|
|
577
|
+
}, { kind: request.kind || 'other', answered: Boolean(answer) });
|
|
578
|
+
}
|
|
579
|
+
|
|
554
580
|
renderPlan(plan) {
|
|
555
581
|
const width = this.width();
|
|
556
582
|
this.write();
|
|
@@ -940,6 +966,8 @@ export class ChatRenderer {
|
|
|
940
966
|
case 'step_error': this.onStepError(msg); return true;
|
|
941
967
|
case 'checkpoint': this.onCheckpoint(msg); return true;
|
|
942
968
|
case 'approval_request': this.onApprovalRequest(msg); return true;
|
|
969
|
+
case 'decision_request': this.onDecisionRequest(msg); return true;
|
|
970
|
+
case 'decision_result': this.onDecisionResult(msg); return true;
|
|
943
971
|
case 'error': this.onError(msg); return true;
|
|
944
972
|
case 'budget_warning': this.onBudgetWarning(msg); return true;
|
|
945
973
|
case 'notice': this.onNotice(msg); return true;
|
package/src/tui/dialogs.js
CHANGED
|
@@ -489,6 +489,97 @@ export class ApprovalLayer extends Layer {
|
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
/** A structured product or architecture choice, separate from permissions. */
|
|
493
|
+
export class DecisionLayer extends Layer {
|
|
494
|
+
constructor(options = {}) {
|
|
495
|
+
super({ width: options.width || 82, height: options.height || 24, ...options });
|
|
496
|
+
this.request = options.request || {};
|
|
497
|
+
this.onAnswer = options.onAnswer || null;
|
|
498
|
+
this.options = Array.isArray(this.request.options) ? this.request.options.slice(0, 4) : [];
|
|
499
|
+
this.selected = 0;
|
|
500
|
+
this.customIndex = this.options.length;
|
|
501
|
+
this.customMode = false;
|
|
502
|
+
this.editor = new LineEditor();
|
|
503
|
+
this.answered = false;
|
|
504
|
+
this.title = safeDisplay(this.request.header || 'decision required');
|
|
505
|
+
this.footer = `↑↓ choose ${GLYPHS.dot} enter select ${GLYPHS.dot} esc cancel`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
preferredHeight() {
|
|
509
|
+
const questionRows = wrap(safeDisplay(this.request.question || ''), Math.max(20, this.width - 6)).length;
|
|
510
|
+
const reasonRows = wrap(safeDisplay(this.request.reason || ''), Math.max(20, this.width - 6)).length;
|
|
511
|
+
return Math.min(this.height, questionRows + reasonRows + (this.options.length * 2) + 7);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
body(width, height) {
|
|
515
|
+
const rows = [];
|
|
516
|
+
rows.push(...wrap(bold(safeDisplay(this.request.question || 'Choose a direction')), width).slice(0, 4));
|
|
517
|
+
if (this.request.reason) rows.push(...wrap(dim(safeDisplay(this.request.reason)), width).slice(0, 3));
|
|
518
|
+
rows.push('');
|
|
519
|
+
|
|
520
|
+
for (const [index, option] of this.options.entries()) {
|
|
521
|
+
const selected = index === this.selected;
|
|
522
|
+
const marker = selected ? paint('accent', GLYPHS.arrow) : paint('muted', GLYPHS.dot);
|
|
523
|
+
rows.push(`${marker} ${selected ? bold(safeDisplay(option.label || '')) : safeDisplay(option.label || '')}`);
|
|
524
|
+
if (option.description) rows.push(` ${dim(truncate(safeDisplay(option.description), Math.max(1, width - 2), GLYPHS.ellipsis))}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const customSelected = this.selected === this.customIndex;
|
|
528
|
+
rows.push(`${customSelected ? paint('accent', GLYPHS.arrow) : paint('muted', GLYPHS.dot)} ${customSelected ? bold('Type your own answer') : 'Type your own answer'}`);
|
|
529
|
+
if (this.customMode) {
|
|
530
|
+
const value = this.editor.getText();
|
|
531
|
+
rows.push(`${paint('accent', `${GLYPHS.arrow} `)}${value || dim('Describe the direction you want')}`);
|
|
532
|
+
this.cursor = { x: 2 + visibleWidth(value.slice(0, this.editor.col)), y: rows.length - 1 };
|
|
533
|
+
this.footer = `enter submit ${GLYPHS.dot} esc choices`;
|
|
534
|
+
} else {
|
|
535
|
+
this.cursor = null;
|
|
536
|
+
this.footer = `↑↓ choose ${GLYPHS.dot} enter select ${GLYPHS.dot} esc cancel`;
|
|
537
|
+
}
|
|
538
|
+
return rows.slice(0, height);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
key(event) {
|
|
542
|
+
if (!event) return null;
|
|
543
|
+
if (this.customMode) {
|
|
544
|
+
if (event.name === 'escape') { this.customMode = false; return 'handled'; }
|
|
545
|
+
if (event.name === 'enter' || event.name === 'return') {
|
|
546
|
+
const label = this.editor.getText().replace(/\s+/g, ' ').trim();
|
|
547
|
+
if (!label) return 'handled';
|
|
548
|
+
this.answer({ requestId: this.request.id, optionId: null, label, custom: true });
|
|
549
|
+
return 'close';
|
|
550
|
+
}
|
|
551
|
+
if (event.name === 'backspace') { this.editor.backspace(); return 'handled'; }
|
|
552
|
+
if (event.name === 'delete') { this.editor.deleteForward(); return 'handled'; }
|
|
553
|
+
if (event.name === 'left') { this.editor.moveLeft(); return 'handled'; }
|
|
554
|
+
if (event.name === 'right') { this.editor.moveRight(); return 'handled'; }
|
|
555
|
+
if (event.name === 'home') { this.editor.moveHome(); return 'handled'; }
|
|
556
|
+
if (event.name === 'end') { this.editor.moveEnd(); return 'handled'; }
|
|
557
|
+
if (event.name === 'paste') { this.editor.insert(event.text); return 'handled'; }
|
|
558
|
+
if (event.name === 'char') { this.editor.insert(event.text); return 'handled'; }
|
|
559
|
+
return 'handled';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (event.name === 'escape') { this.answer(null); return 'close'; }
|
|
563
|
+
const count = this.options.length + 1;
|
|
564
|
+
if (event.name === 'up') { this.selected = (this.selected - 1 + count) % count; return 'handled'; }
|
|
565
|
+
if (event.name === 'down') { this.selected = (this.selected + 1) % count; return 'handled'; }
|
|
566
|
+
if (event.name === 'enter' || event.name === 'return') {
|
|
567
|
+
if (this.selected === this.customIndex) { this.customMode = true; return 'handled'; }
|
|
568
|
+
const option = this.options[this.selected];
|
|
569
|
+
if (!option) return 'handled';
|
|
570
|
+
this.answer({ requestId: this.request.id, optionId: option.id, label: option.label, custom: false });
|
|
571
|
+
return 'close';
|
|
572
|
+
}
|
|
573
|
+
return 'handled';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
answer(value = null) {
|
|
577
|
+
if (this.answered) return;
|
|
578
|
+
this.answered = true;
|
|
579
|
+
if (this.onAnswer) this.onAnswer(value);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
492
583
|
/** A yes/no confirm, for destructive commands that are not tool approvals. */
|
|
493
584
|
export class ConfirmLayer extends Layer {
|
|
494
585
|
constructor(options = {}) {
|
package/src/tui/prompt.js
CHANGED
|
@@ -26,7 +26,7 @@ import { barBlock, GUTTER } from './block.js';
|
|
|
26
26
|
import { visibleWidth, truncate, wrapBlock, shortenHome, safeDisplay } from './text.js';
|
|
27
27
|
import { pickExample, fitHints } from './banner.js';
|
|
28
28
|
import { panelRows } from './overlay.js';
|
|
29
|
-
import { commandPalette, skillPicker, ApprovalLayer, ConfirmLayer, HomeLayer, TextLayer } from './dialogs.js';
|
|
29
|
+
import { commandPalette, skillPicker, ApprovalLayer, DecisionLayer, ConfirmLayer, HomeLayer, TextLayer } from './dialogs.js';
|
|
30
30
|
import { describeTool } from './toolDisplay.js';
|
|
31
31
|
|
|
32
32
|
const CTRL_C_WINDOW_MS = 2000;
|
|
@@ -264,6 +264,7 @@ export class ChatPrompt {
|
|
|
264
264
|
this.statusMessage = message;
|
|
265
265
|
this.scheduleDraw();
|
|
266
266
|
};
|
|
267
|
+
renderer.onPlanChange = () => this.scheduleDraw();
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/**
|
|
@@ -321,6 +322,26 @@ export class ChatPrompt {
|
|
|
321
322
|
return visibleWidth(full) > width ? truncate(full, width, GLYPHS.ellipsis) : full;
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
/** Persistent checklist shown outside the chat transcript. */
|
|
326
|
+
planRows(width) {
|
|
327
|
+
const plan = this.renderer && Array.isArray(this.renderer.latestPlan) ? this.renderer.latestPlan : [];
|
|
328
|
+
if (plan.length === 0) return [];
|
|
329
|
+
const completed = plan.filter(item => item.status === 'completed').length;
|
|
330
|
+
const rows = [`${bold(paint('accent', 'implementation plan'))} ${dim(`${completed}/${plan.length}`)}`];
|
|
331
|
+
const limit = Math.min(plan.length, 5);
|
|
332
|
+
for (const item of plan.slice(0, limit)) {
|
|
333
|
+
const mark = item.status === 'completed'
|
|
334
|
+
? paint('success', GLYPHS.check)
|
|
335
|
+
: item.status === 'in_progress'
|
|
336
|
+
? paint('accent', GLYPHS.arrow)
|
|
337
|
+
: paint('muted', GLYPHS.dot);
|
|
338
|
+
rows.push(` ${mark} ${truncate(safeDisplay(item.text || item.step || ''), Math.max(8, width - 5), GLYPHS.ellipsis)}`);
|
|
339
|
+
}
|
|
340
|
+
if (plan.length > limit) rows.push(` ${dim(`${GLYPHS.ellipsis} ${plan.length - limit} more`)}`);
|
|
341
|
+
rows.push('');
|
|
342
|
+
return rows;
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
startTick() {
|
|
325
346
|
if (this.tickTimer) return;
|
|
326
347
|
this.tickTimer = setInterval(() => {
|
|
@@ -456,6 +477,13 @@ export class ChatPrompt {
|
|
|
456
477
|
});
|
|
457
478
|
}
|
|
458
479
|
|
|
480
|
+
/** Ask for a durable implementation choice, with a free-form option. */
|
|
481
|
+
decide(request = {}) {
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
this.openLayer(new DecisionLayer({ request, onAnswer: resolve }));
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
459
487
|
/** A plain yes/no, for the questions that are not tool approvals. */
|
|
460
488
|
confirm(message, options = {}) {
|
|
461
489
|
return new Promise((resolve) => {
|
|
@@ -1136,12 +1164,13 @@ export class ChatPrompt {
|
|
|
1136
1164
|
const status = this.statusRow(width);
|
|
1137
1165
|
const gap = status.length > 0 ? new Array(FOOTER_GAP).fill('') : [];
|
|
1138
1166
|
|
|
1167
|
+
const planRows = this.planRows(width);
|
|
1139
1168
|
const all = this.clampRows(
|
|
1140
|
-
|
|
1169
|
+
planRows.concat(boxRows, pickerRows, noticeRows, gap, status), width
|
|
1141
1170
|
);
|
|
1142
1171
|
|
|
1143
1172
|
// Park the cursor where the user is typing, not at the end of the picker.
|
|
1144
|
-
this.present(all, caretRow + padTop, layout.cursorCol + inset);
|
|
1173
|
+
this.present(all, planRows.length + caretRow + padTop, layout.cursorCol + inset);
|
|
1145
1174
|
}
|
|
1146
1175
|
|
|
1147
1176
|
/**
|
package/src/tui/toolDisplay.js
CHANGED
|
@@ -82,7 +82,6 @@ const TOOL_DISPLAY = new Map([
|
|
|
82
82
|
['code_verify_syntax', (a) => ({ verb: 'verify', detail: pathDetail(firstString(a, ['path', 'file_path'])) })],
|
|
83
83
|
|
|
84
84
|
['memory_write', (a) => ({ verb: 'remember', detail: firstString(a, ['fact', 'key', 'project']) })],
|
|
85
|
-
['TodoWrite', (a) => ({ verb: 'plan', detail: countDetail(a.todos, 'item') })],
|
|
86
85
|
['update_plan', (a) => ({ verb: 'plan', detail: countDetail(a.plan, 'step') })],
|
|
87
86
|
['tool_search', (a) => ({ verb: 'discover', detail: firstString(a, ['query', 'description']) })],
|
|
88
87
|
['fs_read_skill', (a) => ({ verb: 'skill', detail: firstString(a, ['id', 'skillId', 'domain']) })],
|
package/src/tools/todoWrite.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
export const todoWrite = {
|
|
5
|
-
schema: {
|
|
6
|
-
name: 'TodoWrite',
|
|
7
|
-
description: 'Dynamic Context Injector: Create and manage structured tasks. Forces atomic, granular steps instead of vague goals.',
|
|
8
|
-
parameters: {
|
|
9
|
-
type: 'object',
|
|
10
|
-
properties: {
|
|
11
|
-
todos: {
|
|
12
|
-
type: 'array',
|
|
13
|
-
description: 'The updated todo list (JSON state)',
|
|
14
|
-
items: {
|
|
15
|
-
type: 'object',
|
|
16
|
-
properties: {
|
|
17
|
-
id: { type: 'string' },
|
|
18
|
-
content: { type: 'string', minLength: 5, description: 'Must be an atomic, actionable step.' },
|
|
19
|
-
status: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
enum: ['pending', 'in_progress', 'completed']
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
required: ['id', 'content', 'status'],
|
|
25
|
-
additionalProperties: false
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
required: ['todos'],
|
|
30
|
-
additionalProperties: false
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
async execute(args) {
|
|
34
|
-
try {
|
|
35
|
-
const todoPath = path.join(process.cwd(), '.causyne_todos.json');
|
|
36
|
-
const mdPath = path.join(process.cwd(), 'AGI_PLAN.md');
|
|
37
|
-
|
|
38
|
-
// Phase 2 AGI Upgrade: Atomic Validation
|
|
39
|
-
for (const task of args.todos) {
|
|
40
|
-
const words = task.content.split(' ').length;
|
|
41
|
-
if (words < 2) {
|
|
42
|
-
return { success: false, error: `Task [${task.id}] is too vague ("${task.content}"). Rewrite it into an atomic, actionable step (e.g., "Scaffold React Navbar").` };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Save JSON state
|
|
47
|
-
await fs.writeFile(todoPath, JSON.stringify(args.todos, null, 2), 'utf8');
|
|
48
|
-
|
|
49
|
-
// Render to MD for user visibility
|
|
50
|
-
let mdContent = '# AGI Execution Plan\n\n';
|
|
51
|
-
for (const t of args.todos) {
|
|
52
|
-
let box = '[ ]';
|
|
53
|
-
if (t.status === 'in_progress') box = '[/]';
|
|
54
|
-
if (t.status === 'completed') box = '[x]';
|
|
55
|
-
mdContent += `- ${box} ${t.id}: ${t.content}\n`;
|
|
56
|
-
}
|
|
57
|
-
await fs.writeFile(mdPath, mdContent, 'utf8');
|
|
58
|
-
|
|
59
|
-
// Phase 2 AGI Upgrade: Memory Hook (Signaling back to AgentLoop context window)
|
|
60
|
-
const inProgress = args.todos.filter(t => t.status === 'in_progress');
|
|
61
|
-
const completed = args.todos.filter(t => t.status === 'completed');
|
|
62
|
-
|
|
63
|
-
if (inProgress.length > 1) {
|
|
64
|
-
return { success: true, output: `[Memory Hook] Saved. WARNING: You have ${inProgress.length} tasks in_progress. Only ONE task may be in progress at a time.` };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
success: true,
|
|
69
|
-
output: `[Memory Hook] Tasks synchronized. ${completed.length} completed, ${inProgress.length} active. Focus locked on: ${inProgress[0]?.content || 'None'}`
|
|
70
|
-
};
|
|
71
|
-
} catch (e) {
|
|
72
|
-
return { success: false, error: e.message };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|