@pentoshi/clai 3.0.0 → 3.6.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 +1 -1
- package/dist/agent/classic-renderer.js +9 -1
- package/dist/agent/classic-renderer.js.map +1 -1
- package/dist/agent/context-manager.js +8 -2
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +5 -0
- package/dist/agent/loop-guard.d.ts +2 -0
- package/dist/agent/loop-guard.js +39 -2
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/plan-tool.d.ts +22 -0
- package/dist/agent/plan-tool.js +434 -49
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/project-root.d.ts +33 -0
- package/dist/agent/project-root.js +158 -0
- package/dist/agent/project-root.js.map +1 -0
- package/dist/agent/runner.js +1204 -253
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/task-evidence.d.ts +103 -0
- package/dist/agent/task-evidence.js +413 -0
- package/dist/agent/task-evidence.js.map +1 -0
- package/dist/agent/tool-call-parser.d.ts +21 -0
- package/dist/agent/tool-call-parser.js +101 -28
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +27 -0
- package/dist/agent/tool-history.js +150 -0
- package/dist/agent/tool-history.js.map +1 -0
- package/dist/agent/workspace-orient.d.ts +64 -0
- package/dist/agent/workspace-orient.js +418 -0
- package/dist/agent/workspace-orient.js.map +1 -0
- package/dist/app/adapters/agent-event-adapter.js +5 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/controllers/session-controller.js +4 -0
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/events/app-event.d.ts +4 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/index.js +40 -8
- package/dist/index.js.map +1 -1
- package/dist/llm/adapters/anthropic-tools.d.ts +99 -0
- package/dist/llm/adapters/anthropic-tools.js +225 -0
- package/dist/llm/adapters/anthropic-tools.js.map +1 -0
- package/dist/llm/adapters/gemini-tools.d.ts +54 -0
- package/dist/llm/adapters/gemini-tools.js +139 -0
- package/dist/llm/adapters/gemini-tools.js.map +1 -0
- package/dist/llm/adapters/ollama-tools.d.ts +22 -0
- package/dist/llm/adapters/ollama-tools.js +62 -0
- package/dist/llm/adapters/ollama-tools.js.map +1 -0
- package/dist/llm/adapters/openai-tools.d.ts +39 -0
- package/dist/llm/adapters/openai-tools.js +71 -0
- package/dist/llm/adapters/openai-tools.js.map +1 -0
- package/dist/llm/agentrouter.js +23 -4
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.js +93 -92
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +93 -56
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +23 -4
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +7 -0
- package/dist/llm/capabilities.js +74 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.js +63 -48
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.js +23 -4
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +22 -17
- package/dist/llm/http.js +99 -26
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +23 -4
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/nvidia.js +23 -4
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +49 -31
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +23 -4
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +23 -4
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/qwen-cloud.js +23 -4
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/router.js +48 -6
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/tool-protocol.d.ts +73 -0
- package/dist/llm/tool-protocol.js +282 -0
- package/dist/llm/tool-protocol.js.map +1 -0
- package/dist/modes/ask.js +74 -20
- package/dist/modes/ask.js.map +1 -1
- package/dist/prompts/index.d.ts +13 -5
- package/dist/prompts/index.js +122 -23
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.js +7 -0
- package/dist/repl.js.map +1 -1
- package/dist/store/config.d.ts +7 -0
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/plan.d.ts +2 -0
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/capabilities.d.ts +2 -0
- package/dist/tools/capabilities.js +101 -6
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/command-intent.d.ts +1 -0
- package/dist/tools/command-intent.js +31 -0
- package/dist/tools/command-intent.js.map +1 -1
- package/dist/tools/definitions.d.ts +20 -0
- package/dist/tools/definitions.js +509 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/fs.d.ts +6 -0
- package/dist/tools/fs.js +68 -9
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/net-ping-sweep.js +69 -16
- package/dist/tools/net-ping-sweep.js.map +1 -1
- package/dist/tools/nmap-runner.d.ts +8 -7
- package/dist/tools/nmap-runner.js +126 -65
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/registry.js +122 -33
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +53 -0
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/web/search.js +48 -4
- package/dist/tools/web/search.js.map +1 -1
- package/dist/tui/runtime.d.ts +19 -0
- package/dist/tui/runtime.js +92 -0
- package/dist/tui/runtime.js.map +1 -0
- package/dist/tui/state.js +2 -0
- package/dist/tui/state.js.map +1 -1
- package/dist/tui-v2/app/App.d.ts +3 -3
- package/dist/tui-v2/app/App.js +22 -10
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/plan-lifecycle.d.ts +6 -0
- package/dist/tui-v2/app/plan-lifecycle.js +43 -7
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
- package/dist/tui-v2/components/modal/confirm-modal.d.ts +5 -6
- package/dist/tui-v2/components/modal/confirm-modal.js +60 -21
- package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
- package/dist/tui-v2/components/modal/secret-modal.d.ts +3 -9
- package/dist/tui-v2/components/modal/secret-modal.js +17 -17
- package/dist/tui-v2/components/modal/secret-modal.js.map +1 -1
- package/dist/tui-v2/components/overlay/overlay-host.d.ts +12 -3
- package/dist/tui-v2/components/overlay/overlay-host.js +19 -4
- package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
- package/dist/tui-v2/components/plan/plan-view.d.ts +1 -1
- package/dist/tui-v2/components/plan/plan-view.js +8 -4
- package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.js +10 -1
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +6 -0
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.d.ts +5 -5
- package/dist/tui-v2/components/transcript/compacted-row.js +15 -40
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/tool-card.js +43 -13
- package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.js +8 -1
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
- package/dist/tui-v2/rendering/batch-sections.d.ts +12 -0
- package/dist/tui-v2/rendering/batch-sections.js +68 -0
- package/dist/tui-v2/rendering/batch-sections.js.map +1 -1
- package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
- package/dist/tui-v2/rendering/tool-presenter.d.ts +6 -1
- package/dist/tui-v2/rendering/tool-presenter.js +68 -2
- package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
- package/dist/tui-v2/state/transcript-hydrate.js +6 -2
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +74 -16
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/transcript-types.d.ts +1 -1
- package/dist/types.d.ts +58 -0
- package/dist/ui/ansi-box.d.ts +2 -0
- package/dist/ui/ansi-box.js +8 -1
- package/dist/ui/ansi-box.js.map +1 -1
- package/package.json +13 -4
package/dist/agent/plan-tool.js
CHANGED
|
@@ -2,6 +2,231 @@ import chalk from "chalk";
|
|
|
2
2
|
import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
|
|
3
3
|
import { renderPlanChecklist } from "../ui/plan-pane.js";
|
|
4
4
|
import { isLumpedSingleTask } from "./tool-call-parser.js";
|
|
5
|
+
/** Titles match for plan merge (exact or mutual long substring). */
|
|
6
|
+
export function titlesMatchForPlan(a, b) {
|
|
7
|
+
const t1 = a.trim().toLowerCase();
|
|
8
|
+
const t2 = b.trim().toLowerCase();
|
|
9
|
+
return (t1 === t2 ||
|
|
10
|
+
(t1.length > 8 && t2.length > 8 && (t1.includes(t2) || t2.includes(t1))));
|
|
11
|
+
}
|
|
12
|
+
/** Final coding-plan task: start server / probe localhost / leave running. */
|
|
13
|
+
export function looksLikeRunServerTask(title) {
|
|
14
|
+
const t = title.trim().toLowerCase();
|
|
15
|
+
if (/\b(dev\s*server|npm\s+run\s+dev|pnpm\s+(run\s+)?dev|yarn\s+dev|bun\s+run\s+dev|shell\.start|localhost|leave\s+(it\s+)?running|verify\s+in\s+browser|open\s+in\s+browser|probe\s+(localhost|http)|http:\/\/localhost)\b/.test(t)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (/\b(start|run)\b.+\b(server|dev|vite|next|preview)\b/.test(t))
|
|
19
|
+
return true;
|
|
20
|
+
if (/\b(server|dev|vite|next|preview)\b.+\b(start|run|verify|tail|probe)\b/.test(t)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// "run and verify" / "start app" style finals
|
|
24
|
+
if (/\b(run|start)\b.+\b(app|verify)\b/.test(t) && /\b(server|dev|browser|localhost|url|port|job)\b/.test(t)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
export function looksLikeLocalAppScaffold(kind, goal, detail, tasks) {
|
|
30
|
+
if (kind === "pentest")
|
|
31
|
+
return false;
|
|
32
|
+
const blob = `${kind} ${goal} ${detail} ${tasks.join(" ")}`.toLowerCase();
|
|
33
|
+
// Pure libraries / CLIs with no server surface
|
|
34
|
+
if (/\b(library|cli\b|command[- ]line|npm package|rust crate|go module)\b/.test(blob) &&
|
|
35
|
+
!/\b(react|vite|next|vue|svelte|web\s*app|frontend|dev\s*server|localhost|todo\s*app|dashboard|spa)\b/.test(blob)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (kind === "coding") {
|
|
39
|
+
// coding + no library-only signal → require run/verify when app-like
|
|
40
|
+
return /\b(app|react|vite|next|vue|svelte|nuxt|angular|frontend|web|ui|todo|blog|dashboard|spa|server|localhost|scaffold)\b/.test(blob);
|
|
41
|
+
}
|
|
42
|
+
return /\b(react|vite|next\.?js?|vue|svelte|nuxt|angular|web\s*app|frontend|spa|todo\s*app|blog\s*app|dashboard)\b/.test(blob);
|
|
43
|
+
}
|
|
44
|
+
export function codingPlanNeedsRunVerifyTask(kind, goal, detail, tasks) {
|
|
45
|
+
if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
|
|
46
|
+
return false;
|
|
47
|
+
return !tasks.some(looksLikeRunServerTask);
|
|
48
|
+
}
|
|
49
|
+
function looksLikeInstallTask(title) {
|
|
50
|
+
const t = title.toLowerCase();
|
|
51
|
+
return (/\b(install|dependencies|deps|packages)\b/.test(t) &&
|
|
52
|
+
!/\b(dev\s*server|localhost|probe|run\s+dev)\b/.test(t));
|
|
53
|
+
}
|
|
54
|
+
function looksLikeScaffoldishTask(title) {
|
|
55
|
+
return /\b(scaffold|create|init|vite|next|bootstrap|cargo\s+new)\b/i.test(title);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Ensure coding app plans have an explicit install step after scaffold.
|
|
59
|
+
* Avoids "implement blocked from npm install" + install attributed to wrong task.
|
|
60
|
+
*/
|
|
61
|
+
export function ensureCodingPlanInstallTask(kind, goal, detail, tasks) {
|
|
62
|
+
if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
|
|
63
|
+
return tasks;
|
|
64
|
+
if (tasks.some(looksLikeInstallTask))
|
|
65
|
+
return tasks;
|
|
66
|
+
if (!tasks.some(looksLikeScaffoldishTask))
|
|
67
|
+
return tasks;
|
|
68
|
+
const out = [...tasks];
|
|
69
|
+
const scaffoldIdx = out.findIndex(looksLikeScaffoldishTask);
|
|
70
|
+
const insertAt = scaffoldIdx >= 0 ? scaffoldIdx + 1 : 0;
|
|
71
|
+
out.splice(insertAt, 0, "Install project dependencies (npm/yarn/pnpm/bun install in project root)");
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Goal is only "run/start the existing app" — do not open a new plan. */
|
|
75
|
+
export function looksLikeRunOnlyGoal(goal, detail) {
|
|
76
|
+
const blob = `${goal} ${detail}`.toLowerCase();
|
|
77
|
+
if (!/\b(run|start|launch|serve|dev\s*server|npm\s+run\s+dev)\b/.test(blob)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// Exclude fresh scaffold goals
|
|
81
|
+
if (/\b(scaffold|create|build|implement|add feature|from scratch|new app)\b/.test(blob)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return /\b(existing|already|the app|dev server|server|verify|test it|open it)\b/.test(blob)
|
|
85
|
+
|| /^(run|start)\b/.test(blob.trim());
|
|
86
|
+
}
|
|
87
|
+
function nextTaskId(existingIds) {
|
|
88
|
+
let max = 0;
|
|
89
|
+
for (const id of existingIds) {
|
|
90
|
+
const m = /^t(\d+)$/i.exec(id);
|
|
91
|
+
if (m)
|
|
92
|
+
max = Math.max(max, Number(m[1]));
|
|
93
|
+
}
|
|
94
|
+
return `t${max + 1}`;
|
|
95
|
+
}
|
|
96
|
+
/** Coerce model-sloppy plan.create fields into clean titles. */
|
|
97
|
+
function normalizePlanGoal(args) {
|
|
98
|
+
const raw = args.goal ?? args.objective ?? args.title ?? args.name;
|
|
99
|
+
if (typeof raw === "string")
|
|
100
|
+
return raw.trim();
|
|
101
|
+
if (raw && typeof raw === "object") {
|
|
102
|
+
const o = raw;
|
|
103
|
+
for (const k of ["title", "text", "goal", "name", "summary"]) {
|
|
104
|
+
if (typeof o[k] === "string" && o[k].toString().trim()) {
|
|
105
|
+
return String(o[k]).trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
function normalizePlanDetail(args) {
|
|
112
|
+
const raw = args.detail ?? args.description ?? args.approach ?? args.notes;
|
|
113
|
+
if (typeof raw === "string")
|
|
114
|
+
return raw.trim();
|
|
115
|
+
if (Array.isArray(raw)) {
|
|
116
|
+
return raw
|
|
117
|
+
.map((x) => (typeof x === "string" ? x : JSON.stringify(x)))
|
|
118
|
+
.join("\n")
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
function normalizePlanKind(args) {
|
|
124
|
+
const raw = args.kind ?? args.type ?? args.category;
|
|
125
|
+
if (typeof raw === "string" && raw.trim())
|
|
126
|
+
return raw.trim().toLowerCase();
|
|
127
|
+
return "general";
|
|
128
|
+
}
|
|
129
|
+
function normalizeTaskTitle(t) {
|
|
130
|
+
if (typeof t === "string")
|
|
131
|
+
return t.trim();
|
|
132
|
+
if (typeof t === "number" && Number.isFinite(t))
|
|
133
|
+
return String(t);
|
|
134
|
+
if (t && typeof t === "object") {
|
|
135
|
+
const o = t;
|
|
136
|
+
for (const k of [
|
|
137
|
+
"title",
|
|
138
|
+
"task",
|
|
139
|
+
"name",
|
|
140
|
+
"text",
|
|
141
|
+
"description",
|
|
142
|
+
"label",
|
|
143
|
+
"step",
|
|
144
|
+
"summary",
|
|
145
|
+
]) {
|
|
146
|
+
if (typeof o[k] === "string" && o[k].toString().trim()) {
|
|
147
|
+
return String(o[k]).trim();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
export function slugifyTaskId(text) {
|
|
154
|
+
return text
|
|
155
|
+
.trim()
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
158
|
+
.replace(/^_+|_+$/g, "")
|
|
159
|
+
.slice(0, 64);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Accept tasks as string[], object[], or a single newline/comma-separated string.
|
|
163
|
+
* Models frequently emit `[{id,title}]` instead of plain strings.
|
|
164
|
+
*/
|
|
165
|
+
function normalizePlanTasks(args) {
|
|
166
|
+
return normalizePlanTaskEntries(args).map((t) => t.title);
|
|
167
|
+
}
|
|
168
|
+
/** Full entries including model-supplied id/name aliases (X3). */
|
|
169
|
+
export function normalizePlanTaskEntries(args) {
|
|
170
|
+
const raw = args.tasks ?? args.steps ?? args.checklist ?? args.items ?? args.todos;
|
|
171
|
+
if (typeof raw === "string") {
|
|
172
|
+
return raw
|
|
173
|
+
.split(/\n|;|(?:,\s*(?=[A-Z0-9\-]))/)
|
|
174
|
+
.map((s) => s.replace(/^\s*[-*\d.)]+\s*/, "").trim())
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.map((title) => ({ title, aliases: [] }));
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(raw))
|
|
179
|
+
return [];
|
|
180
|
+
return raw
|
|
181
|
+
.map((t) => {
|
|
182
|
+
const title = normalizeTaskTitle(t);
|
|
183
|
+
if (!title)
|
|
184
|
+
return null;
|
|
185
|
+
const aliases = [];
|
|
186
|
+
if (t && typeof t === "object") {
|
|
187
|
+
const o = t;
|
|
188
|
+
for (const k of ["id", "taskId", "key", "slug"]) {
|
|
189
|
+
if (typeof o[k] === "string" && o[k].toString().trim()) {
|
|
190
|
+
aliases.push(String(o[k]).trim());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// `name` is often the title; only alias if it differs.
|
|
194
|
+
if (typeof o.name === "string" &&
|
|
195
|
+
o.name.trim() &&
|
|
196
|
+
o.name.trim().toLowerCase() !== title.toLowerCase()) {
|
|
197
|
+
aliases.push(o.name.trim());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const slug = slugifyTaskId(title);
|
|
201
|
+
if (slug && !aliases.includes(slug))
|
|
202
|
+
aliases.push(slug);
|
|
203
|
+
return { title, aliases: [...new Set(aliases)] };
|
|
204
|
+
})
|
|
205
|
+
.filter((x) => Boolean(x));
|
|
206
|
+
}
|
|
207
|
+
/** Resolve model taskId (t1 or slug) to the canonical plan task id. */
|
|
208
|
+
export function resolvePlanTaskId(plan, taskId) {
|
|
209
|
+
const raw = taskId.trim();
|
|
210
|
+
if (!raw)
|
|
211
|
+
return undefined;
|
|
212
|
+
if (plan.tasks.some((t) => t.id === raw))
|
|
213
|
+
return raw;
|
|
214
|
+
const lower = raw.toLowerCase();
|
|
215
|
+
const slug = slugifyTaskId(raw);
|
|
216
|
+
for (const t of plan.tasks) {
|
|
217
|
+
if (t.aliases?.some((a) => a === raw || a.toLowerCase() === lower)) {
|
|
218
|
+
return t.id;
|
|
219
|
+
}
|
|
220
|
+
if (slugifyTaskId(t.title) === slug)
|
|
221
|
+
return t.id;
|
|
222
|
+
if (slug.length > 4 &&
|
|
223
|
+
(slugifyTaskId(t.title).includes(slug) ||
|
|
224
|
+
slug.includes(slugifyTaskId(t.title)))) {
|
|
225
|
+
return t.id;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
5
230
|
/** Render the portable inline form; the Ink TUI owns its responsive sidebar. */
|
|
6
231
|
export function renderPlanForTerminal(plan) {
|
|
7
232
|
return renderPlanChecklist(plan);
|
|
@@ -14,8 +239,12 @@ export function planContextMessage(plan, approved) {
|
|
|
14
239
|
lines.push(plan.detail.trim());
|
|
15
240
|
lines.push("Tasks:");
|
|
16
241
|
plan.tasks.forEach((t, i) => {
|
|
17
|
-
|
|
242
|
+
const aliasHint = t.aliases?.length && t.aliases[0] !== t.id
|
|
243
|
+
? ` [aliases: ${t.aliases.slice(0, 3).join(", ")}]`
|
|
244
|
+
: "";
|
|
245
|
+
lines.push(` ${i + 1}. [${t.id}] (${t.state}) ${t.title}${aliasHint}`);
|
|
18
246
|
});
|
|
247
|
+
lines.push("task.update taskId MUST be t1, t2, … from this list (or a listed alias) — never invent free-form slugs alone.");
|
|
19
248
|
if (approved) {
|
|
20
249
|
const inProgress = plan.tasks.find((t) => t.state === "in_progress");
|
|
21
250
|
const firstPending = plan.tasks.find((t) => t.state === "pending");
|
|
@@ -30,10 +259,13 @@ export function planContextMessage(plan, approved) {
|
|
|
30
259
|
"Do NOT re-do tasks already marked done, and do NOT skip ahead to later tasks.");
|
|
31
260
|
}
|
|
32
261
|
lines.push("STRICT ORDER: call task.update {taskId, state:'in_progress'} → do the real work → " +
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
262
|
+
"WAIT for and READ the tool result → only if you are satisfied it succeeded, " +
|
|
263
|
+
"call task.update {taskId, state:'done'} → then open the NEXT task. " +
|
|
264
|
+
"Never mark done before a successful tool result. Never start the next task's work " +
|
|
265
|
+
"(or mark a later task in_progress) until the current one is done and verified. " +
|
|
266
|
+
"If a tool fails, mark the task 'failed' with a note, fix, retry. " +
|
|
267
|
+
"Never re-run tasks already marked done. " +
|
|
268
|
+
"For run/verify: shell.start, leave server running, final message includes URL, port, and job id.");
|
|
37
269
|
}
|
|
38
270
|
else {
|
|
39
271
|
lines.push("This plan is NOT yet approved, so you MUST NOT execute any of its tasks yet. " +
|
|
@@ -54,19 +286,19 @@ export function planContextMessage(plan, approved) {
|
|
|
54
286
|
export async function handlePlanTool(call, session, ctx) {
|
|
55
287
|
void ctx;
|
|
56
288
|
if (call.name === "plan.create") {
|
|
57
|
-
const goal =
|
|
58
|
-
const detail =
|
|
59
|
-
const kind =
|
|
60
|
-
const
|
|
61
|
-
const taskTitles =
|
|
62
|
-
.map((t) => (typeof t === "string" ? t : ""))
|
|
63
|
-
.filter(Boolean);
|
|
289
|
+
const goal = normalizePlanGoal(call.args);
|
|
290
|
+
const detail = normalizePlanDetail(call.args);
|
|
291
|
+
const kind = normalizePlanKind(call.args);
|
|
292
|
+
const taskEntries = normalizePlanTaskEntries(call.args);
|
|
293
|
+
const taskTitles = taskEntries.map((t) => t.title);
|
|
64
294
|
if (!goal || taskTitles.length === 0) {
|
|
65
295
|
return {
|
|
66
296
|
handled: true,
|
|
67
297
|
ok: false,
|
|
68
298
|
display: chalk.red(" ✗ plan.create needs a non-empty goal and at least one task title\n"),
|
|
69
|
-
modelNote: "plan.create failed: provide a string goal and a non-empty tasks array
|
|
299
|
+
modelNote: "plan.create failed: provide a string goal and a non-empty tasks array. " +
|
|
300
|
+
'Accepted task shapes: ["step 1","step 2"] or [{"title":"step 1"},{"id":"t1","title":"step 2"}]. ' +
|
|
301
|
+
'Example: {"name":"plan.create","args":{"goal":"Assess example.com","detail":"…","tasks":["DNS enum","Port scan","HTTP fingerprint"],"kind":"pentest"}}',
|
|
70
302
|
};
|
|
71
303
|
}
|
|
72
304
|
// Reject a low-quality "everything in one step" plan. A single task that
|
|
@@ -85,60 +317,159 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
85
317
|
};
|
|
86
318
|
}
|
|
87
319
|
const existingPlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
320
|
+
// X12: do not open a second full plan just to run an existing app.
|
|
321
|
+
if (existingPlan &&
|
|
322
|
+
(existingPlan.status === "completed" ||
|
|
323
|
+
existingPlan.tasks.every((t) => t.state === "done" || t.state === "skipped" || t.state === "failed")) &&
|
|
324
|
+
looksLikeRunOnlyGoal(goal, detail)) {
|
|
325
|
+
return {
|
|
326
|
+
handled: true,
|
|
327
|
+
ok: false,
|
|
328
|
+
display: chalk.yellow(" ⚠ plan.create skipped — use tools to run the existing app\n"),
|
|
329
|
+
modelNote: "plan.create rejected: the previous plan is already finished and this goal is only run/verify. " +
|
|
330
|
+
"Do NOT create a new plan or re-list old scaffold tasks. " +
|
|
331
|
+
"Just shell.start the dev server, shell.tail until ready, probe localhost " +
|
|
332
|
+
"(curl or http.fetch with iOwnThis:true), LEAVE the server running, and tell the user " +
|
|
333
|
+
"the URL (http://localhost:<port>), port, and job id.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// X12: coding/local-app plans must include a final run/verify server task.
|
|
337
|
+
if (codingPlanNeedsRunVerifyTask(kind, goal, detail, taskTitles)) {
|
|
338
|
+
return {
|
|
339
|
+
handled: true,
|
|
340
|
+
ok: false,
|
|
341
|
+
display: chalk.red(" ✗ plan.create: coding app plan needs a final run/verify server task\n"),
|
|
342
|
+
modelNote: "plan.create rejected: for local web/app scaffolds (React/Vite/Next/etc.), tasks MUST end with a " +
|
|
343
|
+
"final run/verify step — e.g. 'Start dev server (shell.start), tail until ready, probe localhost, " +
|
|
344
|
+
"leave server running, report URL/port/job id'. Prefer shell.start (background); do not shell.stop " +
|
|
345
|
+
"unless the user asks. Build alone is not enough. Add that final task and call plan.create again. " +
|
|
346
|
+
"Pure libraries/CLIs with no server do not need this.",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// Inject install step on fresh plans when model forgot it (avoids install
|
|
350
|
+
// blocked under implement + wrong auto-start attribution). Skip rewrite on
|
|
351
|
+
// merge/revise so completed task order stays stable.
|
|
352
|
+
const normalizedTitles = existingPlan
|
|
353
|
+
? taskTitles
|
|
354
|
+
: ensureCodingPlanInstallTask(kind, goal, detail, taskTitles);
|
|
88
355
|
const plan = createPlan({
|
|
89
356
|
sessionId: session.sessionId,
|
|
90
357
|
goal,
|
|
91
358
|
detail,
|
|
92
|
-
taskTitles,
|
|
359
|
+
taskTitles: normalizedTitles,
|
|
93
360
|
kind,
|
|
94
361
|
});
|
|
362
|
+
// Attach model-supplied slug aliases so task.update can resolve them (X3).
|
|
363
|
+
for (let i = 0; i < plan.tasks.length; i++) {
|
|
364
|
+
const aliases = taskEntries[i]?.aliases ?? [];
|
|
365
|
+
if (aliases.length)
|
|
366
|
+
plan.tasks[i].aliases = aliases;
|
|
367
|
+
}
|
|
368
|
+
let additiveOnly = false;
|
|
95
369
|
if (existingPlan) {
|
|
370
|
+
const usedOldIds = new Set();
|
|
96
371
|
const mappedNewTasks = plan.tasks.map((task) => {
|
|
97
|
-
const match = existingPlan.tasks.find((t) =>
|
|
98
|
-
const t1 = t.title.trim().toLowerCase();
|
|
99
|
-
const t2 = task.title.trim().toLowerCase();
|
|
100
|
-
return (t1 === t2 ||
|
|
101
|
-
(t1.length > 8 && t2.length > 8 && (t1.includes(t2) || t2.includes(t1))));
|
|
102
|
-
});
|
|
372
|
+
const match = existingPlan.tasks.find((t) => !usedOldIds.has(t.id) && titlesMatchForPlan(t.title, task.title));
|
|
103
373
|
if (match) {
|
|
374
|
+
usedOldIds.add(match.id);
|
|
104
375
|
return {
|
|
105
376
|
...task,
|
|
377
|
+
id: match.id,
|
|
106
378
|
state: match.state,
|
|
107
379
|
note: match.note,
|
|
108
380
|
};
|
|
109
381
|
}
|
|
110
382
|
return task;
|
|
111
383
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
384
|
+
// Free ids for brand-new tasks (avoid clobbering preserved ids).
|
|
385
|
+
const taken = new Set(mappedNewTasks.map((t) => t.id));
|
|
386
|
+
for (const task of mappedNewTasks) {
|
|
387
|
+
if (usedOldIds.has(task.id))
|
|
388
|
+
continue;
|
|
389
|
+
if (existingPlan.tasks.some((t) => t.id === task.id) ||
|
|
390
|
+
mappedNewTasks.filter((t) => t.id === task.id).length > 1) {
|
|
391
|
+
let id = nextTaskId([...taken]);
|
|
392
|
+
while (taken.has(id))
|
|
393
|
+
id = nextTaskId([...taken, id]);
|
|
394
|
+
task.id = id;
|
|
395
|
+
taken.add(id);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
taken.add(task.id);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => !mappedNewTasks.some((newTask) => titlesMatchForPlan(oldTask.title, newTask.title)));
|
|
120
402
|
plan.tasks = [...oldTasksToKeep, ...mappedNewTasks];
|
|
403
|
+
// X7: keep approval when only additive pending work remains.
|
|
404
|
+
const priorFinished = existingPlan.tasks.filter((t) => t.state === "done" || t.state === "skipped" || t.state === "failed");
|
|
405
|
+
const finishedStillFinished = priorFinished.length > 0 &&
|
|
406
|
+
priorFinished.every((old) => plan.tasks.some((n) => titlesMatchForPlan(old.title, n.title) &&
|
|
407
|
+
(n.state === "done" ||
|
|
408
|
+
n.state === "skipped" ||
|
|
409
|
+
n.state === "failed")));
|
|
410
|
+
const hasNewPending = plan.tasks.some((t) => t.state === "pending");
|
|
411
|
+
const noDoneReopened = !plan.tasks.some((n) => {
|
|
412
|
+
const old = existingPlan.tasks.find((t) => titlesMatchForPlan(t.title, n.title));
|
|
413
|
+
return (old &&
|
|
414
|
+
(old.state === "done" || old.state === "skipped") &&
|
|
415
|
+
n.state === "pending");
|
|
416
|
+
});
|
|
417
|
+
additiveOnly =
|
|
418
|
+
finishedStillFinished &&
|
|
419
|
+
hasNewPending &&
|
|
420
|
+
noDoneReopened &&
|
|
421
|
+
(session.planApproved.value ||
|
|
422
|
+
existingPlan.status === "approved" ||
|
|
423
|
+
existingPlan.status === "in_progress" ||
|
|
424
|
+
existingPlan.status === "completed");
|
|
425
|
+
if (additiveOnly) {
|
|
426
|
+
plan.status = "in_progress";
|
|
427
|
+
plan.createdAt = existingPlan.createdAt;
|
|
428
|
+
}
|
|
121
429
|
}
|
|
122
430
|
await savePlan(plan).catch(() => undefined);
|
|
123
|
-
|
|
124
|
-
|
|
431
|
+
if (additiveOnly) {
|
|
432
|
+
session.planApproved.value = true;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
session.planApproved.value = false;
|
|
436
|
+
}
|
|
125
437
|
const checklist = renderPlanForTerminal(plan);
|
|
126
|
-
const display =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"
|
|
438
|
+
const display = additiveOnly
|
|
439
|
+
? chalk.cyan(" ● plan updated (additive)\n") +
|
|
440
|
+
checklist +
|
|
441
|
+
"\n" +
|
|
442
|
+
chalk.dim(" ✦ done tasks preserved — continue from the first pending task only.\n" +
|
|
443
|
+
" Do NOT re-run completed scaffold/build work.\n")
|
|
444
|
+
: chalk.cyan(" ● planning\n") +
|
|
445
|
+
checklist +
|
|
446
|
+
"\n" +
|
|
447
|
+
chalk.dim(" ✦ plan created — press Ctrl+P to view it, /implement to approve and run it,\n" +
|
|
448
|
+
" or /discard to cancel it. Any other message refines this plan.\n");
|
|
449
|
+
const firstPending = plan.tasks.find((t) => t.state === "pending");
|
|
131
450
|
return {
|
|
132
451
|
handled: true,
|
|
133
452
|
ok: true,
|
|
134
453
|
plan,
|
|
135
454
|
display,
|
|
136
|
-
modelNote:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
455
|
+
modelNote: additiveOnly
|
|
456
|
+
? `Plan updated with additive task(s); ${plan.tasks.filter((t) => t.state === "done" || t.state === "skipped").length} prior task(s) stay done. ` +
|
|
457
|
+
"Do NOT re-execute completed tasks (no re-scaffold). " +
|
|
458
|
+
(firstPending
|
|
459
|
+
? `Continue from [${firstPending.id}] "${firstPending.title}" only. `
|
|
460
|
+
: "") +
|
|
461
|
+
"If the only new work is run/verify on an existing app, prefer shell.start + probe + report URL — " +
|
|
462
|
+
"do not reboot the world. When the run/verify task finishes, final message MUST include " +
|
|
463
|
+
"http://localhost:<port>, port, job id, and that the server is still running."
|
|
464
|
+
: `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
|
|
465
|
+
"Do NOT start executing tasks until the user approves with /implement. " +
|
|
466
|
+
"If the user's next message gives feedback instead of /implement, that is a REVISION: call plan.create " +
|
|
467
|
+
"again with the updated plan and STOP again. The user may cancel the whole plan with /discard. " +
|
|
468
|
+
"Only after /implement do you begin, working task by task, calling task.update to mark each " +
|
|
469
|
+
"in_progress before and done after you finish it. " +
|
|
470
|
+
"Do NOT call plan.create again only to add a run-dev-server step — put that in this plan's final task, " +
|
|
471
|
+
"or after completion use shell.start + probe directly. " +
|
|
472
|
+
"When run/verify completes: report URL, port, job id, and that the server is still running.",
|
|
142
473
|
};
|
|
143
474
|
}
|
|
144
475
|
// task.update
|
|
@@ -151,7 +482,11 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
151
482
|
modelNote: "task.update failed: there is no active plan. Call plan.create first.",
|
|
152
483
|
};
|
|
153
484
|
}
|
|
154
|
-
const
|
|
485
|
+
const taskIdRaw = typeof call.args.taskId === "string"
|
|
486
|
+
? call.args.taskId
|
|
487
|
+
: typeof call.args.id === "string"
|
|
488
|
+
? call.args.id
|
|
489
|
+
: "";
|
|
155
490
|
const stateRaw = typeof call.args.state === "string" ? call.args.state : "";
|
|
156
491
|
const note = typeof call.args.note === "string" ? call.args.note : undefined;
|
|
157
492
|
const validStates = [
|
|
@@ -169,6 +504,8 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
169
504
|
modelNote: `task.update failed: state must be one of ${validStates.join(", ")}.`,
|
|
170
505
|
};
|
|
171
506
|
}
|
|
507
|
+
// X3: accept t1..tn or model slug aliases / title slugs.
|
|
508
|
+
const taskId = resolvePlanTaskId(plan, taskIdRaw) ?? taskIdRaw;
|
|
172
509
|
// Only one task may be in_progress at a time. This forces genuine
|
|
173
510
|
// task-by-task execution: the model must close (done/failed/skipped) the
|
|
174
511
|
// current task before opening the next one, instead of leaving a task
|
|
@@ -183,18 +520,45 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
183
520
|
display: chalk.red(` \u2717 task.update: task [${otherInProgress.id}] "${otherInProgress.title}" is still in_progress\n`),
|
|
184
521
|
modelNote: `task.update failed: task [${otherInProgress.id}] "${otherInProgress.title}" is still in_progress. ` +
|
|
185
522
|
"Finish it first \u2014 call task.update with state 'done' (or 'failed'/'skipped' with a note) " +
|
|
186
|
-
`for [${otherInProgress.id}] before starting [${taskId}]
|
|
523
|
+
`for [${otherInProgress.id}] before starting [${taskId}]. ` +
|
|
524
|
+
"Use ONLY ids like t1, t2 from the plan context (not title slugs).",
|
|
187
525
|
};
|
|
188
526
|
}
|
|
189
527
|
}
|
|
528
|
+
// X8: cannot mark later task done while an earlier task is still open.
|
|
529
|
+
if (stateRaw === "done") {
|
|
530
|
+
const targetIdx = plan.tasks.findIndex((t) => t.id === taskId);
|
|
531
|
+
if (targetIdx > 0) {
|
|
532
|
+
const blocking = plan.tasks
|
|
533
|
+
.slice(0, targetIdx)
|
|
534
|
+
.filter((t) => t.state === "pending" ||
|
|
535
|
+
t.state === "in_progress" ||
|
|
536
|
+
t.state === "failed");
|
|
537
|
+
if (blocking.length > 0) {
|
|
538
|
+
const b = blocking[0];
|
|
539
|
+
return {
|
|
540
|
+
handled: true,
|
|
541
|
+
ok: false,
|
|
542
|
+
display: chalk.red(` ✗ task.update: cannot mark [${taskId}] done while [${b.id}] is ${b.state}\n`),
|
|
543
|
+
modelNote: `task.update failed: cannot mark [${taskId}] done while earlier task ` +
|
|
544
|
+
`[${b.id}] "${b.title}" is still ${b.state}. ` +
|
|
545
|
+
`Finish or skip [${b.id}] first (state done/skipped), then mark [${taskId}] done.`,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
190
550
|
const ok = markTask(plan, taskId, stateRaw, note);
|
|
191
551
|
if (!ok) {
|
|
192
|
-
const
|
|
552
|
+
const idMap = plan.tasks
|
|
553
|
+
.map((t) => `${t.id}="${t.title}"` +
|
|
554
|
+
(t.aliases?.length ? ` (also: ${t.aliases.join(", ")})` : ""))
|
|
555
|
+
.join("; ");
|
|
193
556
|
return {
|
|
194
557
|
handled: true,
|
|
195
558
|
ok: false,
|
|
196
|
-
display: chalk.red(` ✗ task.update: unknown taskId "${
|
|
197
|
-
modelNote: `task.update failed: unknown taskId
|
|
559
|
+
display: chalk.red(` ✗ task.update: unknown taskId "${taskIdRaw}" (have: ${plan.tasks.map((t) => t.id).join(", ")})\n`),
|
|
560
|
+
modelNote: `task.update failed: unknown taskId "${taskIdRaw}". ` +
|
|
561
|
+
`Valid ids (use these, not title text): ${idMap}.`,
|
|
198
562
|
};
|
|
199
563
|
}
|
|
200
564
|
if (plan.status === "draft" || plan.status === "approved") {
|
|
@@ -205,14 +569,35 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
205
569
|
plan.status = "completed";
|
|
206
570
|
await savePlan(plan).catch(() => undefined);
|
|
207
571
|
const checklist = renderPlanForTerminal(plan);
|
|
572
|
+
const nextPending = plan.tasks.find((t) => t.state === "pending");
|
|
573
|
+
let modelNote;
|
|
574
|
+
if (allDone) {
|
|
575
|
+
modelNote =
|
|
576
|
+
"Task updated. ALL tasks are now finished. Verify the result and give your final summary. " +
|
|
577
|
+
"If a dev server was started: report URL, port, job id, and that it is still running.";
|
|
578
|
+
}
|
|
579
|
+
else if (stateRaw === "done" && nextPending) {
|
|
580
|
+
modelNote =
|
|
581
|
+
`Task [${taskId}] marked done after verified work. ` +
|
|
582
|
+
`NEXT: open only the next task with task.update {taskId:"${nextPending.id}", state:"in_progress"}, ` +
|
|
583
|
+
`then do work for "${nextPending.title}" only. Wait for tool results before marking that one done. ` +
|
|
584
|
+
"Do not batch later tasks. Do not start a later task's tools early.";
|
|
585
|
+
}
|
|
586
|
+
else if (stateRaw === "in_progress") {
|
|
587
|
+
modelNote =
|
|
588
|
+
`Task [${taskId}] is now in_progress. Do ONLY this task's work, wait for tool results, ` +
|
|
589
|
+
`and mark done only when you are satisfied the results prove success. ` +
|
|
590
|
+
`Then task.update {taskId:"${taskId}", state:"done"}. Do not open or work on later tasks yet.`;
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
modelNote = "Task updated. Continue with the next pending task.";
|
|
594
|
+
}
|
|
208
595
|
return {
|
|
209
596
|
handled: true,
|
|
210
597
|
ok: true,
|
|
211
598
|
plan,
|
|
212
599
|
display: checklist + "\n",
|
|
213
|
-
modelNote
|
|
214
|
-
? "Task updated. ALL tasks are now finished. Verify the result and give your final summary."
|
|
215
|
-
: "Task updated. Continue with the next pending task.",
|
|
600
|
+
modelNote,
|
|
216
601
|
};
|
|
217
602
|
}
|
|
218
603
|
//# sourceMappingURL=plan-tool.js.map
|