@pentoshi/clai 3.6.0 → 3.7.3
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/dist/agent/compaction-summary.d.ts +17 -0
- package/dist/agent/compaction-summary.js +64 -0
- package/dist/agent/compaction-summary.js.map +1 -0
- package/dist/agent/confirm-port.d.ts +3 -1
- package/dist/agent/confirm-port.js +5 -1
- package/dist/agent/confirm-port.js.map +1 -1
- package/dist/agent/context-manager.js +45 -22
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +6 -0
- package/dist/agent/evidence-governor.d.ts +37 -0
- package/dist/agent/evidence-governor.js +70 -0
- package/dist/agent/evidence-governor.js.map +1 -0
- package/dist/agent/loop-guard.d.ts +29 -11
- package/dist/agent/loop-guard.js +93 -39
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/must-continue.d.ts +56 -0
- package/dist/agent/must-continue.js +240 -0
- package/dist/agent/must-continue.js.map +1 -0
- package/dist/agent/outcomes.d.ts +101 -0
- package/dist/agent/outcomes.js +359 -0
- package/dist/agent/outcomes.js.map +1 -0
- package/dist/agent/plan-decision.d.ts +33 -0
- package/dist/agent/plan-decision.js +104 -0
- package/dist/agent/plan-decision.js.map +1 -0
- package/dist/agent/plan-tool.d.ts +12 -0
- package/dist/agent/plan-tool.js +312 -104
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/project-root.js +3 -0
- package/dist/agent/project-root.js.map +1 -1
- package/dist/agent/prompt-composer.d.ts +21 -0
- package/dist/agent/prompt-composer.js +46 -0
- package/dist/agent/prompt-composer.js.map +1 -0
- package/dist/agent/runner.d.ts +11 -1
- package/dist/agent/runner.js +1226 -971
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/scope-context.d.ts +3 -0
- package/dist/agent/scope-context.js +24 -0
- package/dist/agent/scope-context.js.map +1 -0
- package/dist/agent/scratch-write.d.ts +4 -0
- package/dist/agent/scratch-write.js +54 -0
- package/dist/agent/scratch-write.js.map +1 -0
- package/dist/agent/session-policy.d.ts +7 -0
- package/dist/agent/session-policy.js +61 -0
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/session-state.d.ts +41 -0
- package/dist/agent/session-state.js +107 -0
- package/dist/agent/session-state.js.map +1 -0
- package/dist/agent/step-budget.d.ts +14 -0
- package/dist/agent/step-budget.js +26 -0
- package/dist/agent/step-budget.js.map +1 -0
- package/dist/agent/task-analyzer.d.ts +12 -3
- package/dist/agent/task-analyzer.js +263 -23
- package/dist/agent/task-analyzer.js.map +1 -1
- package/dist/agent/task-evidence.d.ts +111 -29
- package/dist/agent/task-evidence.js +519 -91
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/task-plan.d.ts +57 -0
- package/dist/agent/task-plan.js +147 -1
- package/dist/agent/task-plan.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +4 -12
- package/dist/agent/tool-call-parser.js +34 -67
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +7 -0
- package/dist/agent/tool-history.js +50 -0
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/agent/tool-output-formatting.d.ts +13 -1
- package/dist/agent/tool-output-formatting.js +75 -22
- package/dist/agent/tool-output-formatting.js.map +1 -1
- package/dist/agent/turn-outcome.d.ts +11 -0
- package/dist/agent/turn-outcome.js +17 -0
- package/dist/agent/turn-outcome.js.map +1 -0
- package/dist/agent/turn-state.d.ts +11 -0
- package/dist/agent/turn-state.js +28 -0
- package/dist/agent/turn-state.js.map +1 -0
- package/dist/agent/workspace-orient.d.ts +11 -0
- package/dist/agent/workspace-orient.js +103 -12
- package/dist/agent/workspace-orient.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.d.ts +2 -13
- package/dist/app/adapters/agent-event-adapter.js +63 -25
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.d.ts +0 -5
- package/dist/app/adapters/current-agent-adapter.js +4 -7
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/adapters/current-jobs-adapter.js +1 -0
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
- package/dist/app/adapters/current-terminal-adapter.d.ts +0 -7
- package/dist/app/adapters/current-terminal-adapter.js +0 -7
- package/dist/app/adapters/current-terminal-adapter.js.map +1 -1
- package/dist/app/adapters/current-updates-adapter.d.ts +0 -5
- package/dist/app/adapters/current-updates-adapter.js +0 -5
- package/dist/app/adapters/current-updates-adapter.js.map +1 -1
- package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +0 -5
- package/dist/app/adapters/in-memory-clipboard-adapter.js +0 -5
- package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -1
- package/dist/app/commands/command.d.ts +0 -5
- package/dist/app/commands/command.js.map +1 -1
- package/dist/app/commands/registry.d.ts +0 -14
- package/dist/app/commands/registry.js +0 -20
- package/dist/app/commands/registry.js.map +1 -1
- package/dist/app/controllers/disposable.d.ts +0 -5
- package/dist/app/controllers/disposable.js +0 -5
- package/dist/app/controllers/disposable.js.map +1 -1
- package/dist/app/controllers/job-controller.d.ts +1 -5
- package/dist/app/controllers/job-controller.js +0 -4
- package/dist/app/controllers/job-controller.js.map +1 -1
- package/dist/app/controllers/plan-controller.d.ts +5 -8
- package/dist/app/controllers/plan-controller.js +30 -12
- package/dist/app/controllers/plan-controller.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +17 -32
- package/dist/app/controllers/session-controller.js +56 -35
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/turn-controller.d.ts +2 -6
- package/dist/app/controllers/turn-controller.js +8 -8
- package/dist/app/controllers/turn-controller.js.map +1 -1
- package/dist/app/events/app-event.d.ts +4 -7
- package/dist/app/events/app-event.js +0 -6
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/app/events/event-buffer.d.ts +6 -17
- package/dist/app/events/event-buffer.js +27 -24
- package/dist/app/events/event-buffer.js.map +1 -1
- package/dist/app/events/sequencer.d.ts +0 -9
- package/dist/app/events/sequencer.js +0 -4
- package/dist/app/events/sequencer.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +13 -3
- package/dist/app/ports/clipboard-port.d.ts +0 -5
- package/dist/app/ports/confirm-port.d.ts +0 -7
- package/dist/app/ports/jobs-port.d.ts +2 -5
- package/dist/app/ports/persistence-port.d.ts +0 -4
- package/dist/app/ports/secret-port.d.ts +0 -5
- package/dist/app/ports/terminal-port.d.ts +0 -6
- package/dist/app/ports/updates-port.d.ts +0 -4
- package/dist/attachments/service.d.ts +29 -0
- package/dist/attachments/service.js +47 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/commands/providers.js +0 -3
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/search-providers.d.ts +0 -33
- package/dist/commands/search-providers.js +0 -33
- package/dist/commands/search-providers.js.map +1 -1
- package/dist/commands/update.js +1 -9
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +22 -42
- package/dist/index.js.map +1 -1
- package/dist/llm/adapters/ollama-tools.d.ts +0 -4
- package/dist/llm/adapters/ollama-tools.js +0 -4
- package/dist/llm/adapters/ollama-tools.js.map +1 -1
- package/dist/llm/capabilities.d.ts +10 -10
- package/dist/llm/capabilities.js +43 -17
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/groq.js +0 -6
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +0 -17
- package/dist/llm/http.js +0 -45
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/router.d.ts +0 -6
- package/dist/llm/router.js +0 -15
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/tool-protocol.d.ts +0 -6
- package/dist/llm/tool-protocol.js +3 -7
- package/dist/llm/tool-protocol.js.map +1 -1
- package/dist/modes/agent.d.ts +2 -1
- package/dist/modes/agent.js +4 -2
- package/dist/modes/agent.js.map +1 -1
- package/dist/modes/ask.d.ts +0 -18
- package/dist/modes/ask.js +0 -49
- package/dist/modes/ask.js.map +1 -1
- package/dist/os/cwd.d.ts +0 -21
- package/dist/os/cwd.js +0 -24
- package/dist/os/cwd.js.map +1 -1
- package/dist/os/pkgmgr.js +0 -6
- package/dist/os/pkgmgr.js.map +1 -1
- package/dist/prompts/embedded.d.ts +1 -0
- package/dist/prompts/embedded.js +9 -0
- package/dist/prompts/embedded.js.map +1 -0
- package/dist/prompts/index.d.ts +6 -18
- package/dist/prompts/index.js +104 -293
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +228 -0
- package/dist/prompts/system.ask.md +56 -0
- package/dist/repl/prompt-line.js +0 -36
- package/dist/repl/prompt-line.js.map +1 -1
- package/dist/repl/slash-commands.d.ts +0 -9
- package/dist/repl/slash-commands.js +2 -23
- package/dist/repl/slash-commands.js.map +1 -1
- package/dist/repl.d.ts +5 -0
- package/dist/repl.js +169 -55
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +17 -9
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/engagement-policy.d.ts +46 -0
- package/dist/safety/engagement-policy.js +198 -0
- package/dist/safety/engagement-policy.js.map +1 -0
- package/dist/store/engagement.d.ts +115 -0
- package/dist/store/engagement.js +278 -0
- package/dist/store/engagement.js.map +1 -0
- package/dist/store/logs.d.ts +5 -0
- package/dist/store/logs.js +52 -2
- package/dist/store/logs.js.map +1 -1
- package/dist/store/plan.d.ts +57 -5
- package/dist/store/plan.js +182 -8
- package/dist/store/plan.js.map +1 -1
- package/dist/store/scope.d.ts +4 -1
- package/dist/store/scope.js +2 -2
- package/dist/store/scope.js.map +1 -1
- package/dist/tools/definitions.js +70 -1
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/elevated-shell.d.ts +60 -0
- package/dist/tools/elevated-shell.js +233 -0
- package/dist/tools/elevated-shell.js.map +1 -0
- package/dist/tools/file-diff.d.ts +125 -0
- package/dist/tools/file-diff.js +495 -0
- package/dist/tools/file-diff.js.map +1 -0
- package/dist/tools/fs.d.ts +11 -13
- package/dist/tools/fs.js +269 -78
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/http.d.ts +15 -0
- package/dist/tools/http.js +202 -61
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/jobs.d.ts +71 -10
- package/dist/tools/jobs.js +403 -124
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/nmap-runner.d.ts +11 -0
- package/dist/tools/nmap-runner.js +115 -74
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/pentest-workflows.d.ts +8 -0
- package/dist/tools/pentest-workflows.js +71 -0
- package/dist/tools/pentest-workflows.js.map +1 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +244 -57
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +3 -0
- package/dist/tools/shell.js +35 -13
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/tool-types.d.ts +11 -0
- package/dist/tools/web/fetch-core.d.ts +7 -0
- package/dist/tools/web/fetch-core.js +40 -1
- package/dist/tools/web/fetch-core.js.map +1 -1
- package/dist/tools/web/fetch.js +8 -1
- package/dist/tools/web/fetch.js.map +1 -1
- package/dist/tui-v2/app/App.js +34 -41
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/command-handlers.js +20 -1
- package/dist/tui-v2/app/command-handlers.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +3 -0
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/session-commands.js +5 -0
- package/dist/tui-v2/app/commands/session-commands.js.map +1 -1
- package/dist/tui-v2/app/plan-lifecycle.d.ts +25 -3
- package/dist/tui-v2/app/plan-lifecycle.js +87 -24
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
- package/dist/tui-v2/bootstrap/patch-opentui-text.d.ts +6 -0
- package/dist/tui-v2/bootstrap/patch-opentui-text.js +53 -0
- package/dist/tui-v2/bootstrap/patch-opentui-text.js.map +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js +7 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +23 -14
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
- package/dist/tui-v2/components/modal/confirm-modal.d.ts +1 -1
- package/dist/tui-v2/components/modal/confirm-modal.js +18 -7
- package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
- package/dist/tui-v2/components/overlay/overlay-host.js +1 -1
- package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
- package/dist/tui-v2/components/pager/pager.d.ts +6 -0
- package/dist/tui-v2/components/pager/pager.js +313 -67
- package/dist/tui-v2/components/pager/pager.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +9 -0
- package/dist/tui-v2/components/status/status-line.js +17 -3
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/tool-card.d.ts +9 -1
- package/dist/tui-v2/components/transcript/tool-card.js +114 -9
- package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.js +2 -2
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +16 -0
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/tui-v2/controllers/overlay-controller.d.ts +16 -2
- package/dist/tui-v2/controllers/overlay-controller.js +92 -12
- package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
- package/dist/tui-v2/rendering/artifact-pager-source.d.ts +19 -0
- package/dist/tui-v2/rendering/artifact-pager-source.js +103 -0
- package/dist/tui-v2/rendering/artifact-pager-source.js.map +1 -0
- package/dist/tui-v2/rendering/file-diff-view.d.ts +63 -0
- package/dist/tui-v2/rendering/file-diff-view.js +263 -0
- package/dist/tui-v2/rendering/file-diff-view.js.map +1 -0
- package/dist/tui-v2/rendering/open-tool-output.d.ts +9 -1
- package/dist/tui-v2/rendering/open-tool-output.js +65 -12
- package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
- package/dist/tui-v2/rendering/pager-chrome.d.ts +12 -0
- package/dist/tui-v2/rendering/pager-chrome.js +56 -0
- package/dist/tui-v2/rendering/pager-chrome.js.map +1 -0
- package/dist/tui-v2/rendering/plan-view.js +1 -1
- package/dist/tui-v2/rendering/plan-view.js.map +1 -1
- package/dist/tui-v2/rendering/syntax-highlight.d.ts +37 -0
- package/dist/tui-v2/rendering/syntax-highlight.js +1303 -0
- package/dist/tui-v2/rendering/syntax-highlight.js.map +1 -0
- package/dist/tui-v2/rendering/theme.d.ts +28 -0
- package/dist/tui-v2/rendering/theme.js +28 -0
- package/dist/tui-v2/rendering/theme.js.map +1 -1
- package/dist/tui-v2/rendering/tool-presenter.d.ts +5 -0
- package/dist/tui-v2/rendering/tool-presenter.js +35 -3
- package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
- package/dist/tui-v2/state/transcript-hydrate.js +5 -0
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +12 -2
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/transcript-store.d.ts +7 -0
- package/dist/tui-v2/state/transcript-store.js +46 -0
- package/dist/tui-v2/state/transcript-store.js.map +1 -1
- package/dist/tui-v2/state/transcript-types.d.ts +12 -0
- package/dist/tui-v2/state/transcript-types.js +9 -0
- package/dist/tui-v2/state/transcript-types.js.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/ui/mentions.d.ts +6 -0
- package/dist/ui/mentions.js +50 -12
- package/dist/ui/mentions.js.map +1 -1
- package/dist/ui/plan-pane.js +1 -1
- package/dist/ui/plan-pane.js.map +1 -1
- package/package.json +18 -12
package/dist/agent/plan-tool.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
|
|
2
|
+
import { createPlan, loadPlan, savePlan, markTask, isBareTaskIdTitle, isPlanTerminal, isPlanSuccessful, readyPlanTasks, validateSessionPlan, } from "../store/plan.js";
|
|
3
3
|
import { renderPlanChecklist } from "../ui/plan-pane.js";
|
|
4
4
|
import { isLumpedSingleTask } from "./tool-call-parser.js";
|
|
5
|
+
import { getActiveProjectRoot } from "./project-root.js";
|
|
6
|
+
import { detectPackageManager } from "./workspace-orient.js";
|
|
5
7
|
/** Titles match for plan merge (exact or mutual long substring). */
|
|
6
8
|
export function titlesMatchForPlan(a, b) {
|
|
7
9
|
const t1 = a.trim().toLowerCase();
|
|
@@ -71,6 +73,67 @@ export function ensureCodingPlanInstallTask(kind, goal, detail, tasks) {
|
|
|
71
73
|
out.splice(insertAt, 0, "Install project dependencies (npm/yarn/pnpm/bun install in project root)");
|
|
72
74
|
return out;
|
|
73
75
|
}
|
|
76
|
+
function looksLikeFeatureImplementTask(title) {
|
|
77
|
+
const t = title.toLowerCase();
|
|
78
|
+
if (looksLikeInstallTask(title) || looksLikeRunServerTask(title))
|
|
79
|
+
return false;
|
|
80
|
+
if (looksLikeScaffoldishTask(title) && !/\b(feature|todo|component|ui)\b/.test(t)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return (/\b(implement|feature|todo|component|ui|crud|auth|localstorage|persist|integrate)\b/.test(t) ||
|
|
84
|
+
(/\b(add|build|write|create)\b/.test(t) &&
|
|
85
|
+
/\b(todo|feature|component|page|ui|list|form)\b/.test(t)));
|
|
86
|
+
}
|
|
87
|
+
function looksLikeFeatureAppPlan(goal, detail, tasks) {
|
|
88
|
+
const blob = `${goal}\n${detail}\n${tasks.join("\n")}`.toLowerCase();
|
|
89
|
+
if (/\b(just|only)\s+(scaffold|init|boilerplate|starter)\b/.test(blob)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return (/\b(todo|to-?do|blog|dashboard|chat|kanban|notes?|crm|shop|auth|login|crud|portfolio)\b/.test(blob) &&
|
|
93
|
+
/\b(app|application|project|site|ui|feature)\b/.test(blob));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Ensure feature-app plans include an implement-feature task (not scaffold-only).
|
|
97
|
+
*/
|
|
98
|
+
export function ensureCodingPlanFeatureTask(kind, goal, detail, tasks) {
|
|
99
|
+
if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
|
|
100
|
+
return tasks;
|
|
101
|
+
if (!looksLikeFeatureAppPlan(goal, detail, tasks))
|
|
102
|
+
return tasks;
|
|
103
|
+
if (tasks.some(looksLikeFeatureImplementTask))
|
|
104
|
+
return tasks;
|
|
105
|
+
const out = [...tasks];
|
|
106
|
+
// Insert after install if present, else after scaffold, else before last (verify)
|
|
107
|
+
const installIdx = out.findIndex(looksLikeInstallTask);
|
|
108
|
+
const scaffoldIdx = out.findIndex(looksLikeScaffoldishTask);
|
|
109
|
+
const runIdx = out.findIndex(looksLikeRunServerTask);
|
|
110
|
+
let insertAt;
|
|
111
|
+
if (installIdx >= 0)
|
|
112
|
+
insertAt = installIdx + 1;
|
|
113
|
+
else if (scaffoldIdx >= 0)
|
|
114
|
+
insertAt = scaffoldIdx + 1;
|
|
115
|
+
else if (runIdx >= 0)
|
|
116
|
+
insertAt = runIdx;
|
|
117
|
+
else
|
|
118
|
+
insertAt = out.length;
|
|
119
|
+
out.splice(insertAt, 0, "Implement the requested product feature (replace starter boilerplate with real UI/state)");
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Ensure local app plans end with shell.start + probe + leave running. */
|
|
123
|
+
export function ensureCodingPlanRunVerifyTask(kind, goal, detail, tasks) {
|
|
124
|
+
if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
|
|
125
|
+
return tasks;
|
|
126
|
+
if (tasks.some(looksLikeRunServerTask))
|
|
127
|
+
return tasks;
|
|
128
|
+
return [
|
|
129
|
+
...tasks,
|
|
130
|
+
"Start dev server with shell.start, probe localhost, leave running, report URL/port/job id",
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/** Preserve the model/user-authored checklist exactly for fresh coding plans. */
|
|
134
|
+
export function normalizeCodingPlanTasks(_kind, _goal, _detail, tasks) {
|
|
135
|
+
return [...tasks];
|
|
136
|
+
}
|
|
74
137
|
/** Goal is only "run/start the existing app" — do not open a new plan. */
|
|
75
138
|
export function looksLikeRunOnlyGoal(goal, detail) {
|
|
76
139
|
const blob = `${goal} ${detail}`.toLowerCase();
|
|
@@ -173,14 +236,20 @@ export function normalizePlanTaskEntries(args) {
|
|
|
173
236
|
.split(/\n|;|(?:,\s*(?=[A-Z0-9\-]))/)
|
|
174
237
|
.map((s) => s.replace(/^\s*[-*\d.)]+\s*/, "").trim())
|
|
175
238
|
.filter(Boolean)
|
|
176
|
-
.map((title) => ({
|
|
239
|
+
.map((title) => ({
|
|
240
|
+
title,
|
|
241
|
+
aliases: [],
|
|
242
|
+
dependencies: [],
|
|
243
|
+
dependenciesSpecified: false,
|
|
244
|
+
resourceLocks: [],
|
|
245
|
+
}));
|
|
177
246
|
}
|
|
178
247
|
if (!Array.isArray(raw))
|
|
179
248
|
return [];
|
|
180
249
|
return raw
|
|
181
250
|
.map((t) => {
|
|
182
251
|
const title = normalizeTaskTitle(t);
|
|
183
|
-
if (!title)
|
|
252
|
+
if (!title || isBareTaskIdTitle(title))
|
|
184
253
|
return null;
|
|
185
254
|
const aliases = [];
|
|
186
255
|
if (t && typeof t === "object") {
|
|
@@ -200,7 +269,18 @@ export function normalizePlanTaskEntries(args) {
|
|
|
200
269
|
const slug = slugifyTaskId(title);
|
|
201
270
|
if (slug && !aliases.includes(slug))
|
|
202
271
|
aliases.push(slug);
|
|
203
|
-
|
|
272
|
+
const object = t && typeof t === "object" ? t : {};
|
|
273
|
+
const stringArray = (value) => Array.isArray(value)
|
|
274
|
+
? [...new Set(value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean))]
|
|
275
|
+
: [];
|
|
276
|
+
return {
|
|
277
|
+
title,
|
|
278
|
+
aliases: [...new Set(aliases)],
|
|
279
|
+
dependencies: stringArray(object.dependencies ?? object.dependsOn),
|
|
280
|
+
dependenciesSpecified: Object.prototype.hasOwnProperty.call(object, "dependencies") ||
|
|
281
|
+
Object.prototype.hasOwnProperty.call(object, "dependsOn"),
|
|
282
|
+
resourceLocks: stringArray(object.resourceLocks ?? object.resources),
|
|
283
|
+
};
|
|
204
284
|
})
|
|
205
285
|
.filter((x) => Boolean(x));
|
|
206
286
|
}
|
|
@@ -217,13 +297,6 @@ export function resolvePlanTaskId(plan, taskId) {
|
|
|
217
297
|
if (t.aliases?.some((a) => a === raw || a.toLowerCase() === lower)) {
|
|
218
298
|
return t.id;
|
|
219
299
|
}
|
|
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
300
|
}
|
|
228
301
|
return undefined;
|
|
229
302
|
}
|
|
@@ -234,7 +307,7 @@ export function renderPlanForTerminal(plan) {
|
|
|
234
307
|
/** Build the system-context block describing the session's active plan. */
|
|
235
308
|
export function planContextMessage(plan, approved) {
|
|
236
309
|
const lines = [];
|
|
237
|
-
lines.push(`ACTIVE PLAN for this session (goal: ${plan.goal}, status: ${plan.status}):`);
|
|
310
|
+
lines.push(`ACTIVE PLAN v${plan.version ?? 1} for this session (goal: ${plan.goal}, status: ${plan.status}):`);
|
|
238
311
|
if (plan.detail.trim())
|
|
239
312
|
lines.push(plan.detail.trim());
|
|
240
313
|
lines.push("Tasks:");
|
|
@@ -242,39 +315,52 @@ export function planContextMessage(plan, approved) {
|
|
|
242
315
|
const aliasHint = t.aliases?.length && t.aliases[0] !== t.id
|
|
243
316
|
? ` [aliases: ${t.aliases.slice(0, 3).join(", ")}]`
|
|
244
317
|
: "";
|
|
245
|
-
|
|
318
|
+
const dependencyHint = t.dependencies?.length
|
|
319
|
+
? ` [depends: ${t.dependencies.join(", ")}]`
|
|
320
|
+
: "";
|
|
321
|
+
const resourceHint = t.resourceLocks?.length
|
|
322
|
+
? ` [locks: ${t.resourceLocks.join(", ")}]`
|
|
323
|
+
: "";
|
|
324
|
+
const evidenceHint = t.evidence?.successWorkCount
|
|
325
|
+
? ` [evidence: ${t.evidence.successWorkCount} successful tool${t.evidence.successWorkCount === 1 ? "" : "s"}${t.evidence.lastOkTool ? `; last ${t.evidence.lastOkTool}` : ""}]`
|
|
326
|
+
: "";
|
|
327
|
+
lines.push(` ${i + 1}. [${t.id}] (${t.state}) ${t.title}${aliasHint}${dependencyHint}${resourceHint}${evidenceHint}`);
|
|
246
328
|
});
|
|
247
329
|
lines.push("task.update taskId MUST be t1, t2, … from this list (or a listed alias) — never invent free-form slugs alone.");
|
|
330
|
+
if (plan.meta?.projectRoot) {
|
|
331
|
+
lines.push(`project_root: ${plan.meta.projectRoot}`);
|
|
332
|
+
}
|
|
333
|
+
if (plan.meta?.packageManager) {
|
|
334
|
+
lines.push(`package_manager: ${plan.meta.packageManager}`);
|
|
335
|
+
}
|
|
248
336
|
if (approved) {
|
|
249
337
|
const inProgress = plan.tasks.find((t) => t.state === "in_progress");
|
|
250
|
-
const firstPending = plan
|
|
251
|
-
lines.push("The user APPROVED this plan. Execute it
|
|
338
|
+
const firstPending = readyPlanTasks(plan)[0];
|
|
339
|
+
lines.push("The user APPROVED this plan. Execute it NOW. Tasks are checkpoints — you still own the whole goal.");
|
|
252
340
|
if (inProgress) {
|
|
253
341
|
lines.push(`RESUME TASK ${inProgress.id} (${inProgress.title}) — it was started but interrupted. ` +
|
|
254
342
|
"Retry what was in progress; do NOT restart completed work from scratch. " +
|
|
255
|
-
"Do NOT re-do tasks already marked done
|
|
343
|
+
"Do NOT re-do tasks already marked done.");
|
|
256
344
|
}
|
|
257
345
|
else if (firstPending) {
|
|
258
346
|
lines.push(`START WITH TASK ${firstPending.id} (${firstPending.title}). ` +
|
|
259
|
-
"Do NOT re-do tasks already marked done
|
|
347
|
+
"Do NOT re-do tasks already marked done.");
|
|
260
348
|
}
|
|
261
|
-
lines.push("
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"If a tool fails
|
|
267
|
-
"
|
|
268
|
-
"For run/verify: shell.start, leave server running, final message includes URL, port, and job id."
|
|
349
|
+
lines.push("Flow: task.update in_progress → real work → WAIT for and READ every tool result for that task → " +
|
|
350
|
+
"only then mark done when the task outcome is satisfied → immediately open the next task. " +
|
|
351
|
+
"Never mark done right after firing a command on the hope it will succeed — analyze the actual output first. " +
|
|
352
|
+
"Durable evidence shown beside a task survives resume; use it to close that task rather than repeating already-confirmed work. " +
|
|
353
|
+
"Independent read-only lookups may parallelize within a task. " +
|
|
354
|
+
"If a tool fails: mark failed or stay in progress, fix, retry until the task is truly done. " +
|
|
355
|
+
"For software builds: run automated checks (typecheck/build/tests when applicable) then live/runtime proof. " +
|
|
356
|
+
"For run/verify: prove runtime (shell.start, ready tail, LISTEN, or localhost GET), leave server running, final message includes URL, port, and job id. " +
|
|
357
|
+
"Do not re-open done tasks. If mid-task evidence shows the plan is wrong, adapt (fix root cause; for pentest, revise plan with completed tasks preserved).");
|
|
269
358
|
}
|
|
270
359
|
else {
|
|
271
|
-
lines.push("This plan is NOT yet approved
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"plan, then STOP and wait. Do NOT call shell.exec, pkg.install, net.scan, tool.check, fs.write, or any " +
|
|
276
|
-
"other execution tool. The user will APPROVE with /implement, or CANCEL with /discard. Only after " +
|
|
277
|
-
"/implement may you begin executing.");
|
|
360
|
+
lines.push("This plan is NOT yet approved — do not execute tasks (no scaffold/write/install/exploit). " +
|
|
361
|
+
"User free-text is PLAN REVISION feedback, not approval — even if it sounds like an instruction. " +
|
|
362
|
+
"Refine with plan.create and/or read-only explore/research, then STOP. " +
|
|
363
|
+
"User may Accept, Discard, View, or Suggest changes (or /implement / /discard).");
|
|
278
364
|
}
|
|
279
365
|
return lines.join("\n");
|
|
280
366
|
}
|
|
@@ -284,7 +370,9 @@ export function planContextMessage(plan, approved) {
|
|
|
284
370
|
* the agent keeps it in context across the whole session.
|
|
285
371
|
*/
|
|
286
372
|
export async function handlePlanTool(call, session, ctx) {
|
|
287
|
-
|
|
373
|
+
const autoApprove = Boolean(ctx.autoApprove);
|
|
374
|
+
void ctx.loopGuard;
|
|
375
|
+
void ctx.step;
|
|
288
376
|
if (call.name === "plan.create") {
|
|
289
377
|
const goal = normalizePlanGoal(call.args);
|
|
290
378
|
const detail = normalizePlanDetail(call.args);
|
|
@@ -310,10 +398,9 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
310
398
|
handled: true,
|
|
311
399
|
ok: false,
|
|
312
400
|
display: chalk.red(" ✗ plan.create: that single task lumps the whole build into one step\n"),
|
|
313
|
-
modelNote: "plan.create rejected: you put everything into ONE task. Break it into
|
|
314
|
-
"ordered tasks — each a distinct action
|
|
315
|
-
"
|
|
316
|
-
"'install deps and run dev server to verify'. Call plan.create again with that tasks array.",
|
|
401
|
+
modelNote: "plan.create rejected: you put everything into ONE task. Break it into separate " +
|
|
402
|
+
"ordered tasks — each a distinct action (scaffold, implement feature, install, run/verify). " +
|
|
403
|
+
"Call plan.create again with a proper tasks array.",
|
|
317
404
|
};
|
|
318
405
|
}
|
|
319
406
|
const existingPlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
@@ -333,40 +420,48 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
333
420
|
"the URL (http://localhost:<port>), port, and job id.",
|
|
334
421
|
};
|
|
335
422
|
}
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
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.
|
|
423
|
+
// Preserve authored task count/order. Build, install, and verification
|
|
424
|
+
// evidence may span tasks; the runtime must not inject regex-derived
|
|
425
|
+
// checkpoints that change IDs or make progress diverge from the plan.
|
|
352
426
|
const normalizedTitles = existingPlan
|
|
353
427
|
? taskTitles
|
|
354
|
-
:
|
|
428
|
+
: normalizeCodingPlanTasks(kind, goal, detail, taskTitles);
|
|
429
|
+
const root = getActiveProjectRoot();
|
|
430
|
+
const pm = root ? detectPackageManager(root) : undefined;
|
|
431
|
+
const meta = root || existingPlan?.meta
|
|
432
|
+
? {
|
|
433
|
+
...(existingPlan?.meta ?? {}),
|
|
434
|
+
...(root ? { projectRoot: root } : {}),
|
|
435
|
+
...(pm ? { packageManager: pm } : {}),
|
|
436
|
+
}
|
|
437
|
+
: undefined;
|
|
355
438
|
const plan = createPlan({
|
|
356
439
|
sessionId: session.sessionId,
|
|
357
440
|
goal,
|
|
358
441
|
detail,
|
|
359
442
|
taskTitles: normalizedTitles,
|
|
360
443
|
kind,
|
|
444
|
+
meta,
|
|
361
445
|
});
|
|
362
446
|
// Attach model-supplied slug aliases so task.update can resolve them (X3).
|
|
363
447
|
for (let i = 0; i < plan.tasks.length; i++) {
|
|
364
448
|
const aliases = taskEntries[i]?.aliases ?? [];
|
|
365
449
|
if (aliases.length)
|
|
366
450
|
plan.tasks[i].aliases = aliases;
|
|
451
|
+
const locks = taskEntries[i]?.resourceLocks ?? [];
|
|
452
|
+
if (locks.length)
|
|
453
|
+
plan.tasks[i].resourceLocks = locks;
|
|
454
|
+
}
|
|
455
|
+
for (let i = 0; i < plan.tasks.length; i++) {
|
|
456
|
+
const rawDependencies = taskEntries[i]?.dependencies ?? [];
|
|
457
|
+
const dependencies = rawDependencies.map((dependency) => resolvePlanTaskId(plan, dependency) ?? dependency);
|
|
458
|
+
if (taskEntries[i]?.dependenciesSpecified) {
|
|
459
|
+
plan.tasks[i].dependencies = [...new Set(dependencies)];
|
|
460
|
+
}
|
|
367
461
|
}
|
|
368
462
|
let additiveOnly = false;
|
|
369
463
|
if (existingPlan) {
|
|
464
|
+
plan.version = (existingPlan.version ?? 1) + 1;
|
|
370
465
|
const usedOldIds = new Set();
|
|
371
466
|
const mappedNewTasks = plan.tasks.map((task) => {
|
|
372
467
|
const match = existingPlan.tasks.find((t) => !usedOldIds.has(t.id) && titlesMatchForPlan(t.title, task.title));
|
|
@@ -377,6 +472,7 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
377
472
|
id: match.id,
|
|
378
473
|
state: match.state,
|
|
379
474
|
note: match.note,
|
|
475
|
+
evidence: match.evidence,
|
|
380
476
|
};
|
|
381
477
|
}
|
|
382
478
|
return task;
|
|
@@ -398,8 +494,32 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
398
494
|
taken.add(task.id);
|
|
399
495
|
}
|
|
400
496
|
}
|
|
401
|
-
const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => !
|
|
402
|
-
|
|
497
|
+
const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => !isBareTaskIdTitle(oldTask.title) &&
|
|
498
|
+
!mappedNewTasks.some((newTask) => titlesMatchForPlan(oldTask.title, newTask.title)));
|
|
499
|
+
plan.tasks = [...oldTasksToKeep, ...mappedNewTasks].filter((t) => !isBareTaskIdTitle(t.title));
|
|
500
|
+
const finalIdByInputReference = new Map();
|
|
501
|
+
for (let index = 0; index < mappedNewTasks.length; index += 1) {
|
|
502
|
+
const task = mappedNewTasks[index];
|
|
503
|
+
const entry = taskEntries[index];
|
|
504
|
+
finalIdByInputReference.set(`t${index + 1}`, task.id);
|
|
505
|
+
finalIdByInputReference.set(task.id, task.id);
|
|
506
|
+
finalIdByInputReference.set(slugifyTaskId(task.title), task.id);
|
|
507
|
+
for (const alias of entry?.aliases ?? []) {
|
|
508
|
+
finalIdByInputReference.set(alias, task.id);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (let index = 0; index < mappedNewTasks.length; index += 1) {
|
|
512
|
+
const task = mappedNewTasks[index];
|
|
513
|
+
const entry = taskEntries[index];
|
|
514
|
+
const references = entry?.dependenciesSpecified
|
|
515
|
+
? entry.dependencies
|
|
516
|
+
: index > 0
|
|
517
|
+
? [`t${index}`]
|
|
518
|
+
: [];
|
|
519
|
+
task.dependencies = [...new Set(references.map((reference) => finalIdByInputReference.get(reference) ??
|
|
520
|
+
resolvePlanTaskId(plan, reference) ??
|
|
521
|
+
reference))];
|
|
522
|
+
}
|
|
403
523
|
// X7: keep approval when only additive pending work remains.
|
|
404
524
|
const priorFinished = existingPlan.tasks.filter((t) => t.state === "done" || t.state === "skipped" || t.state === "failed");
|
|
405
525
|
const finishedStillFinished = priorFinished.length > 0 &&
|
|
@@ -427,13 +547,27 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
427
547
|
plan.createdAt = existingPlan.createdAt;
|
|
428
548
|
}
|
|
429
549
|
}
|
|
430
|
-
|
|
550
|
+
const dag = validateSessionPlan(plan);
|
|
551
|
+
if (!dag.ok) {
|
|
552
|
+
return {
|
|
553
|
+
handled: true,
|
|
554
|
+
ok: false,
|
|
555
|
+
display: chalk.red(` ✗ plan.create: invalid dependency graph — ${dag.reason}\n`),
|
|
556
|
+
modelNote: `plan.create failed: ${dag.reason}. Use only declared task ids/aliases and remove dependency cycles.`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
431
559
|
if (additiveOnly) {
|
|
432
560
|
session.planApproved.value = true;
|
|
561
|
+
plan.status = "in_progress";
|
|
562
|
+
}
|
|
563
|
+
else if (autoApprove) {
|
|
564
|
+
session.planApproved.value = true;
|
|
565
|
+
plan.status = "approved";
|
|
433
566
|
}
|
|
434
567
|
else {
|
|
435
568
|
session.planApproved.value = false;
|
|
436
569
|
}
|
|
570
|
+
await savePlan(plan).catch(() => undefined);
|
|
437
571
|
const checklist = renderPlanForTerminal(plan);
|
|
438
572
|
const display = additiveOnly
|
|
439
573
|
? chalk.cyan(" ● plan updated (additive)\n") +
|
|
@@ -441,12 +575,17 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
441
575
|
"\n" +
|
|
442
576
|
chalk.dim(" ✦ done tasks preserved — continue from the first pending task only.\n" +
|
|
443
577
|
" Do NOT re-run completed scaffold/build work.\n")
|
|
444
|
-
:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"
|
|
449
|
-
|
|
578
|
+
: autoApprove
|
|
579
|
+
? chalk.cyan(" ● plan created (auto-approved)\n") +
|
|
580
|
+
checklist +
|
|
581
|
+
"\n" +
|
|
582
|
+
chalk.dim(" ✦ plan approved in agent mode — continue executing task by task.\n")
|
|
583
|
+
: chalk.cyan(" ● planning\n") +
|
|
584
|
+
checklist +
|
|
585
|
+
"\n" +
|
|
586
|
+
chalk.dim(" ✦ plan created — Accept to run, Discard, View, or Suggest changes\n" +
|
|
587
|
+
" (/implement, /discard, Ctrl+P also work).\n");
|
|
588
|
+
const firstPending = readyPlanTasks(plan)[0];
|
|
450
589
|
return {
|
|
451
590
|
handled: true,
|
|
452
591
|
ok: true,
|
|
@@ -461,15 +600,17 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
461
600
|
"If the only new work is run/verify on an existing app, prefer shell.start + probe + report URL — " +
|
|
462
601
|
"do not reboot the world. When the run/verify task finishes, final message MUST include " +
|
|
463
602
|
"http://localhost:<port>, port, job id, and that the server is still running."
|
|
464
|
-
:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
603
|
+
: autoApprove
|
|
604
|
+
? `Plan saved and approved with ${plan.tasks.length} task(s). Execute now. ` +
|
|
605
|
+
(firstPending
|
|
606
|
+
? `Start with [${firstPending.id}] "${firstPending.title}". `
|
|
607
|
+
: "") +
|
|
608
|
+
"task.update in_progress → work → verify → done. When run/verify completes: report URL, port, job id, server still running."
|
|
609
|
+
: `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
|
|
610
|
+
"Do NOT start executing until the user accepts the plan. " +
|
|
611
|
+
"If the user's next message gives feedback, that is a REVISION: call plan.create again with the updated plan and STOP again. " +
|
|
612
|
+
"The user may discard the plan. After acceptance: task.update in_progress → work → verify → done. " +
|
|
613
|
+
"When run/verify completes: report URL, port, job id, and that the server is still running.",
|
|
473
614
|
};
|
|
474
615
|
}
|
|
475
616
|
// task.update
|
|
@@ -506,43 +647,103 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
506
647
|
}
|
|
507
648
|
// X3: accept t1..tn or model slug aliases / title slugs.
|
|
508
649
|
const taskId = resolvePlanTaskId(plan, taskIdRaw) ?? taskIdRaw;
|
|
509
|
-
// Only one task may be in_progress at a time. This forces genuine
|
|
510
|
-
// task-by-task execution: the model must close (done/failed/skipped) the
|
|
511
|
-
// current task before opening the next one, instead of leaving a task
|
|
512
|
-
// "in_progress" as an umbrella while it quietly works through the rest
|
|
513
|
-
// of the plan underneath it.
|
|
514
650
|
if (stateRaw === "in_progress") {
|
|
515
|
-
const
|
|
516
|
-
|
|
651
|
+
const target = plan.tasks.find((task) => task.id === taskId);
|
|
652
|
+
const ready = readyPlanTasks(plan).some((task) => task.id === taskId);
|
|
653
|
+
const retryingFailedTask = target?.state === "failed";
|
|
654
|
+
if (target?.state === "done" || target?.state === "skipped") {
|
|
655
|
+
const nextPending = readyPlanTasks(plan)[0];
|
|
656
|
+
const nextHint = nextPending
|
|
657
|
+
? ` Continue with task.update {taskId:"${nextPending.id}", state:"in_progress"} ("${nextPending.title}").`
|
|
658
|
+
: " All ready work is finished — write the final summary if needed.";
|
|
517
659
|
return {
|
|
518
660
|
handled: true,
|
|
519
661
|
ok: false,
|
|
520
|
-
display: chalk.red(`
|
|
521
|
-
modelNote: `task.update failed:
|
|
522
|
-
|
|
523
|
-
`for [${otherInProgress.id}] before starting [${taskId}]. ` +
|
|
524
|
-
"Use ONLY ids like t1, t2 from the plan context (not title slugs).",
|
|
662
|
+
display: chalk.red(` ✗ task.update: [${taskId}] is already ${target.state} — do not re-open\n`),
|
|
663
|
+
modelNote: `task.update failed: [${taskId}] is already ${target.state}. ` +
|
|
664
|
+
`Do not re-run completed tasks.${nextHint}`,
|
|
525
665
|
};
|
|
526
666
|
}
|
|
667
|
+
if (target?.state !== "in_progress") {
|
|
668
|
+
const incompleteDependencies = (target?.dependencies ?? []).filter((dependency) => {
|
|
669
|
+
const dependencyTask = plan.tasks.find((task) => task.id === dependency);
|
|
670
|
+
return !dependencyTask || (dependencyTask.state !== "done" && dependencyTask.state !== "skipped");
|
|
671
|
+
});
|
|
672
|
+
const heldLocks = new Set(plan.tasks
|
|
673
|
+
.filter((task) => task.state === "in_progress" && task.id !== taskId)
|
|
674
|
+
.flatMap((task) => task.resourceLocks ?? []));
|
|
675
|
+
const conflictingLocks = (target?.resourceLocks ?? []).filter((lock) => heldLocks.has(lock));
|
|
676
|
+
if ((!ready && !retryingFailedTask) ||
|
|
677
|
+
incompleteDependencies.length > 0 ||
|
|
678
|
+
conflictingLocks.length > 0) {
|
|
679
|
+
const nextReady = readyPlanTasks(plan)[0];
|
|
680
|
+
return {
|
|
681
|
+
handled: true,
|
|
682
|
+
ok: false,
|
|
683
|
+
display: chalk.red(` ✗ task.update: [${taskId}] is not dependency/resource ready\n`),
|
|
684
|
+
modelNote: `task.update failed: [${taskId}] is not ready. ` +
|
|
685
|
+
(incompleteDependencies.length ? `Incomplete dependencies: ${incompleteDependencies.join(", ")}. ` : "") +
|
|
686
|
+
(conflictingLocks.length ? `Resources currently locked: ${conflictingLocks.join(", ")}. ` : "") +
|
|
687
|
+
(nextReady
|
|
688
|
+
? `Open the next ready task first: ${nextReady.id} ("${nextReady.title}").`
|
|
689
|
+
: ""),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// A retry is a new execution attempt. Previous receipts proved only the
|
|
694
|
+
// failed attempt and must not close the task before recovery work succeeds.
|
|
695
|
+
if (retryingFailedTask && target)
|
|
696
|
+
target.evidence = undefined;
|
|
527
697
|
}
|
|
528
|
-
// X8: cannot mark later task done while an earlier task is still open.
|
|
529
698
|
if (stateRaw === "done") {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
699
|
+
const target = plan.tasks.find((task) => task.id === taskId);
|
|
700
|
+
const incompleteDependencies = (target?.dependencies ?? []).filter((dependency) => {
|
|
701
|
+
const dependencyTask = plan.tasks.find((task) => task.id === dependency);
|
|
702
|
+
return !dependencyTask || (dependencyTask.state !== "done" && dependencyTask.state !== "skipped");
|
|
703
|
+
});
|
|
704
|
+
if (incompleteDependencies.length > 0) {
|
|
705
|
+
return {
|
|
706
|
+
handled: true,
|
|
707
|
+
ok: false,
|
|
708
|
+
display: chalk.red(` ✗ task.update: cannot complete [${taskId}] before dependencies\n`),
|
|
709
|
+
modelNote: `task.update failed: earlier task/dependency ${incompleteDependencies.join(", ")} is not complete for [${taskId}].`,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
if (target && target.state !== "in_progress") {
|
|
713
|
+
// Soft-auto: pending + dependency-ready → open then complete in one call.
|
|
714
|
+
// Models often skip the ritual in_progress step after work already landed.
|
|
715
|
+
if (target.state === "pending" && incompleteDependencies.length === 0) {
|
|
716
|
+
const opened = markTask(plan, taskId, "in_progress", note);
|
|
717
|
+
if (!opened) {
|
|
718
|
+
return {
|
|
719
|
+
handled: true,
|
|
720
|
+
ok: false,
|
|
721
|
+
display: chalk.red(` ✗ task.update: could not auto-open [${taskId}] before completion\n`),
|
|
722
|
+
modelNote: `task.update failed: could not auto-open pending [${taskId}].`,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
// Fall through to mark done below (state is now in_progress).
|
|
726
|
+
}
|
|
727
|
+
else if (target.state === "failed") {
|
|
728
|
+
return {
|
|
729
|
+
handled: true,
|
|
730
|
+
ok: false,
|
|
731
|
+
display: chalk.red(` ✗ task.update: [${taskId}] is failed — retry with in_progress first\n`),
|
|
732
|
+
modelNote: `task.update failed: [${taskId}] is failed. ` +
|
|
733
|
+
`Call task.update {taskId:"${taskId}", state:"in_progress"} to retry, then mark done after recovery work.`,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
const nextReady = readyPlanTasks(plan)[0];
|
|
539
738
|
return {
|
|
540
739
|
handled: true,
|
|
541
740
|
ok: false,
|
|
542
|
-
display: chalk.red(` ✗ task.update:
|
|
543
|
-
modelNote: `task.update failed:
|
|
544
|
-
|
|
545
|
-
|
|
741
|
+
display: chalk.red(` ✗ task.update: [${taskId}] must be in_progress before completion\n`),
|
|
742
|
+
modelNote: `task.update failed: [${taskId}] is ${target.state}. ` +
|
|
743
|
+
"Start or retry it, perform fresh work, then mark it done." +
|
|
744
|
+
(nextReady
|
|
745
|
+
? ` Open next ready: ${nextReady.id} ("${nextReady.title}").`
|
|
746
|
+
: ""),
|
|
546
747
|
};
|
|
547
748
|
}
|
|
548
749
|
}
|
|
@@ -564,18 +765,25 @@ export async function handlePlanTool(call, session, ctx) {
|
|
|
564
765
|
if (plan.status === "draft" || plan.status === "approved") {
|
|
565
766
|
plan.status = "in_progress";
|
|
566
767
|
}
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
768
|
+
const terminal = isPlanTerminal(plan);
|
|
769
|
+
const successful = isPlanSuccessful(plan);
|
|
770
|
+
if (terminal)
|
|
771
|
+
plan.status = successful ? "completed" : "abandoned";
|
|
570
772
|
await savePlan(plan).catch(() => undefined);
|
|
571
773
|
const checklist = renderPlanForTerminal(plan);
|
|
572
|
-
const nextPending = plan
|
|
774
|
+
const nextPending = readyPlanTasks(plan)[0];
|
|
573
775
|
let modelNote;
|
|
574
|
-
if (
|
|
776
|
+
if (successful) {
|
|
575
777
|
modelNote =
|
|
576
|
-
"Task updated.
|
|
778
|
+
"Task updated. All required tasks succeeded or were explicitly skipped. Verify the result and give your final summary. " +
|
|
577
779
|
"If a dev server was started: report URL, port, job id, and that it is still running.";
|
|
578
780
|
}
|
|
781
|
+
else if (terminal) {
|
|
782
|
+
const failed = plan.tasks.filter((task) => task.state === "failed");
|
|
783
|
+
modelNote =
|
|
784
|
+
`Task updated. The plan is terminal but NOT successful: ${failed.length} task(s) failed. ` +
|
|
785
|
+
"Do not claim completion. Report a failed/partial outcome with the failed tasks and remaining user outcome.";
|
|
786
|
+
}
|
|
579
787
|
else if (stateRaw === "done" && nextPending) {
|
|
580
788
|
modelNote =
|
|
581
789
|
`Task [${taskId}] marked done after verified work. ` +
|