@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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/classic-renderer.js +9 -1
  3. package/dist/agent/classic-renderer.js.map +1 -1
  4. package/dist/agent/context-manager.js +8 -2
  5. package/dist/agent/context-manager.js.map +1 -1
  6. package/dist/agent/events.d.ts +5 -0
  7. package/dist/agent/loop-guard.d.ts +2 -0
  8. package/dist/agent/loop-guard.js +39 -2
  9. package/dist/agent/loop-guard.js.map +1 -1
  10. package/dist/agent/plan-tool.d.ts +22 -0
  11. package/dist/agent/plan-tool.js +434 -49
  12. package/dist/agent/plan-tool.js.map +1 -1
  13. package/dist/agent/project-root.d.ts +33 -0
  14. package/dist/agent/project-root.js +158 -0
  15. package/dist/agent/project-root.js.map +1 -0
  16. package/dist/agent/runner.js +1204 -253
  17. package/dist/agent/runner.js.map +1 -1
  18. package/dist/agent/task-evidence.d.ts +103 -0
  19. package/dist/agent/task-evidence.js +413 -0
  20. package/dist/agent/task-evidence.js.map +1 -0
  21. package/dist/agent/tool-call-parser.d.ts +21 -0
  22. package/dist/agent/tool-call-parser.js +101 -28
  23. package/dist/agent/tool-call-parser.js.map +1 -1
  24. package/dist/agent/tool-history.d.ts +27 -0
  25. package/dist/agent/tool-history.js +150 -0
  26. package/dist/agent/tool-history.js.map +1 -0
  27. package/dist/agent/workspace-orient.d.ts +64 -0
  28. package/dist/agent/workspace-orient.js +418 -0
  29. package/dist/agent/workspace-orient.js.map +1 -0
  30. package/dist/app/adapters/agent-event-adapter.js +5 -0
  31. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  32. package/dist/app/controllers/session-controller.js +4 -0
  33. package/dist/app/controllers/session-controller.js.map +1 -1
  34. package/dist/app/events/app-event.d.ts +4 -0
  35. package/dist/app/events/app-event.js.map +1 -1
  36. package/dist/commands/doctor.js +14 -1
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/update.js +1 -1
  39. package/dist/index.js +40 -8
  40. package/dist/index.js.map +1 -1
  41. package/dist/llm/adapters/anthropic-tools.d.ts +99 -0
  42. package/dist/llm/adapters/anthropic-tools.js +225 -0
  43. package/dist/llm/adapters/anthropic-tools.js.map +1 -0
  44. package/dist/llm/adapters/gemini-tools.d.ts +54 -0
  45. package/dist/llm/adapters/gemini-tools.js +139 -0
  46. package/dist/llm/adapters/gemini-tools.js.map +1 -0
  47. package/dist/llm/adapters/ollama-tools.d.ts +22 -0
  48. package/dist/llm/adapters/ollama-tools.js +62 -0
  49. package/dist/llm/adapters/ollama-tools.js.map +1 -0
  50. package/dist/llm/adapters/openai-tools.d.ts +39 -0
  51. package/dist/llm/adapters/openai-tools.js +71 -0
  52. package/dist/llm/adapters/openai-tools.js.map +1 -0
  53. package/dist/llm/agentrouter.js +23 -4
  54. package/dist/llm/agentrouter.js.map +1 -1
  55. package/dist/llm/anthropic.js +93 -92
  56. package/dist/llm/anthropic.js.map +1 -1
  57. package/dist/llm/aws-mantle.js +93 -56
  58. package/dist/llm/aws-mantle.js.map +1 -1
  59. package/dist/llm/bynara.js +23 -4
  60. package/dist/llm/bynara.js.map +1 -1
  61. package/dist/llm/capabilities.d.ts +7 -0
  62. package/dist/llm/capabilities.js +74 -0
  63. package/dist/llm/capabilities.js.map +1 -1
  64. package/dist/llm/gemini.js +63 -48
  65. package/dist/llm/gemini.js.map +1 -1
  66. package/dist/llm/groq.js +23 -4
  67. package/dist/llm/groq.js.map +1 -1
  68. package/dist/llm/http.d.ts +22 -17
  69. package/dist/llm/http.js +99 -26
  70. package/dist/llm/http.js.map +1 -1
  71. package/dist/llm/kimchi.js +23 -4
  72. package/dist/llm/kimchi.js.map +1 -1
  73. package/dist/llm/nvidia.js +23 -4
  74. package/dist/llm/nvidia.js.map +1 -1
  75. package/dist/llm/ollama.js +49 -31
  76. package/dist/llm/ollama.js.map +1 -1
  77. package/dist/llm/openai.js +23 -4
  78. package/dist/llm/openai.js.map +1 -1
  79. package/dist/llm/openrouter.js +23 -4
  80. package/dist/llm/openrouter.js.map +1 -1
  81. package/dist/llm/qwen-cloud.js +23 -4
  82. package/dist/llm/qwen-cloud.js.map +1 -1
  83. package/dist/llm/router.js +48 -6
  84. package/dist/llm/router.js.map +1 -1
  85. package/dist/llm/tool-protocol.d.ts +73 -0
  86. package/dist/llm/tool-protocol.js +282 -0
  87. package/dist/llm/tool-protocol.js.map +1 -0
  88. package/dist/modes/ask.js +74 -20
  89. package/dist/modes/ask.js.map +1 -1
  90. package/dist/prompts/index.d.ts +13 -5
  91. package/dist/prompts/index.js +122 -23
  92. package/dist/prompts/index.js.map +1 -1
  93. package/dist/repl.js +7 -0
  94. package/dist/repl.js.map +1 -1
  95. package/dist/store/config.d.ts +7 -0
  96. package/dist/store/config.js +1 -0
  97. package/dist/store/config.js.map +1 -1
  98. package/dist/store/plan.d.ts +2 -0
  99. package/dist/store/plan.js.map +1 -1
  100. package/dist/tools/capabilities.d.ts +2 -0
  101. package/dist/tools/capabilities.js +101 -6
  102. package/dist/tools/capabilities.js.map +1 -1
  103. package/dist/tools/command-intent.d.ts +1 -0
  104. package/dist/tools/command-intent.js +31 -0
  105. package/dist/tools/command-intent.js.map +1 -1
  106. package/dist/tools/definitions.d.ts +20 -0
  107. package/dist/tools/definitions.js +509 -0
  108. package/dist/tools/definitions.js.map +1 -0
  109. package/dist/tools/fs.d.ts +6 -0
  110. package/dist/tools/fs.js +68 -9
  111. package/dist/tools/fs.js.map +1 -1
  112. package/dist/tools/net-ping-sweep.js +69 -16
  113. package/dist/tools/net-ping-sweep.js.map +1 -1
  114. package/dist/tools/nmap-runner.d.ts +8 -7
  115. package/dist/tools/nmap-runner.js +126 -65
  116. package/dist/tools/nmap-runner.js.map +1 -1
  117. package/dist/tools/registry.js +122 -33
  118. package/dist/tools/registry.js.map +1 -1
  119. package/dist/tools/validate.d.ts +11 -0
  120. package/dist/tools/validate.js +53 -0
  121. package/dist/tools/validate.js.map +1 -1
  122. package/dist/tools/web/search.js +48 -4
  123. package/dist/tools/web/search.js.map +1 -1
  124. package/dist/tui/runtime.d.ts +19 -0
  125. package/dist/tui/runtime.js +92 -0
  126. package/dist/tui/runtime.js.map +1 -0
  127. package/dist/tui/state.js +2 -0
  128. package/dist/tui/state.js.map +1 -1
  129. package/dist/tui-v2/app/App.d.ts +3 -3
  130. package/dist/tui-v2/app/App.js +22 -10
  131. package/dist/tui-v2/app/App.js.map +1 -1
  132. package/dist/tui-v2/app/plan-lifecycle.d.ts +6 -0
  133. package/dist/tui-v2/app/plan-lifecycle.js +43 -7
  134. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
  135. package/dist/tui-v2/components/modal/confirm-modal.d.ts +5 -6
  136. package/dist/tui-v2/components/modal/confirm-modal.js +60 -21
  137. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
  138. package/dist/tui-v2/components/modal/secret-modal.d.ts +3 -9
  139. package/dist/tui-v2/components/modal/secret-modal.js +17 -17
  140. package/dist/tui-v2/components/modal/secret-modal.js.map +1 -1
  141. package/dist/tui-v2/components/overlay/overlay-host.d.ts +12 -3
  142. package/dist/tui-v2/components/overlay/overlay-host.js +19 -4
  143. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  144. package/dist/tui-v2/components/plan/plan-view.d.ts +1 -1
  145. package/dist/tui-v2/components/plan/plan-view.js +8 -4
  146. package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
  147. package/dist/tui-v2/components/status/status-line.js +10 -1
  148. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  149. package/dist/tui-v2/components/transcript/assistant-message.js +6 -0
  150. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  151. package/dist/tui-v2/components/transcript/compacted-row.d.ts +5 -5
  152. package/dist/tui-v2/components/transcript/compacted-row.js +15 -40
  153. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  154. package/dist/tui-v2/components/transcript/tool-card.js +43 -13
  155. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  156. package/dist/tui-v2/components/transcript/transcript-row.js +8 -1
  157. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  158. package/dist/tui-v2/rendering/batch-sections.d.ts +12 -0
  159. package/dist/tui-v2/rendering/batch-sections.js +68 -0
  160. package/dist/tui-v2/rendering/batch-sections.js.map +1 -1
  161. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
  162. package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
  163. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
  164. package/dist/tui-v2/rendering/tool-presenter.d.ts +6 -1
  165. package/dist/tui-v2/rendering/tool-presenter.js +68 -2
  166. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
  167. package/dist/tui-v2/state/transcript-hydrate.js +6 -2
  168. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  169. package/dist/tui-v2/state/transcript-reducer.js +74 -16
  170. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  171. package/dist/tui-v2/state/transcript-types.d.ts +1 -1
  172. package/dist/types.d.ts +58 -0
  173. package/dist/ui/ansi-box.d.ts +2 -0
  174. package/dist/ui/ansi-box.js +8 -1
  175. package/dist/ui/ansi-box.js.map +1 -1
  176. package/package.json +13 -4
@@ -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
- lines.push(` ${i + 1}. [${t.id}] (${t.state}) ${t.title}`);
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
- "call task.update {taskId, state:'done'} ONLY after the tool calls actually succeed. " +
34
- "If a tool fails, mark the task 'failed' with a note, fix the problem, then retry. " +
35
- "Do NOT mark a task done when its commands error out. " +
36
- "Never claim something ran without a successful tool call.");
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 = typeof call.args.goal === "string" ? call.args.goal : "";
58
- const detail = typeof call.args.detail === "string" ? call.args.detail : "";
59
- const kind = typeof call.args.kind === "string" ? call.args.kind : "general";
60
- const rawTasks = Array.isArray(call.args.tasks) ? call.args.tasks : [];
61
- const taskTitles = rawTasks
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 of step titles.",
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
- const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => {
113
- return !mappedNewTasks.some((newTask) => {
114
- const t1 = oldTask.title.trim().toLowerCase();
115
- const t2 = newTask.title.trim().toLowerCase();
116
- return (t1 === t2 ||
117
- (t1.length > 8 && t2.length > 8 && (t1.includes(t2) || t2.includes(t1))));
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
- // A freshly (re)created plan resets approval — the user must /implement.
124
- session.planApproved.value = false;
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 = chalk.cyan(" ● planning\n") +
127
- checklist +
128
- "\n" +
129
- chalk.dim(" ✦ plan created — press Ctrl+P to view it, /implement to approve and run it,\n" +
130
- " or /discard to cancel it. Any other message refines this plan.\n");
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: `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
137
- "Do NOT start executing tasks until the user approves with /implement. " +
138
- "If the user's next message gives feedback instead of /implement, that is a REVISION: call plan.create " +
139
- "again with the updated plan and STOP again. The user may cancel the whole plan with /discard. " +
140
- "Only after /implement do you begin, working task by task, calling task.update to mark each " +
141
- "in_progress before and done after you finish it.",
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 taskId = typeof call.args.taskId === "string" ? call.args.taskId : "";
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 ids = plan.tasks.map((t) => t.id).join(", ");
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 "${taskId}" (have: ${ids})\n`),
197
- modelNote: `task.update failed: unknown taskId. Valid ids: ${ids}.`,
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: allDone
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