@pentoshi/clai 3.5.1 → 3.7.2
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/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 +53 -24
- 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 -9
- package/dist/agent/loop-guard.js +117 -26
- 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 +34 -0
- package/dist/agent/plan-tool.js +594 -76
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/project-root.d.ts +33 -0
- package/dist/agent/project-root.js +161 -0
- package/dist/agent/project-root.js.map +1 -0
- 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 +1915 -886
- 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 +173 -0
- package/dist/agent/task-evidence.js +802 -0
- package/dist/agent/task-evidence.js.map +1 -0
- 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 +19 -11
- package/dist/agent/tool-call-parser.js +96 -71
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +34 -0
- package/dist/agent/tool-history.js +200 -0
- package/dist/agent/tool-history.js.map +1 -0
- 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 +75 -0
- package/dist/agent/workspace-orient.js +509 -0
- package/dist/agent/workspace-orient.js.map +1 -0
- 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 +60 -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/doctor.js +3 -0
- package/dist/commands/doctor.js.map +1 -1
- 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/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 +18 -0
- package/dist/llm/adapters/ollama-tools.js +58 -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 +13 -6
- package/dist/llm/capabilities.js +113 -13
- 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 -10
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +22 -34
- package/dist/llm/http.js +97 -69
- 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.d.ts +0 -6
- package/dist/llm/router.js +48 -21
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/tool-protocol.d.ts +67 -0
- package/dist/llm/tool-protocol.js +278 -0
- package/dist/llm/tool-protocol.js.map +1 -0
- 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 +71 -66
- 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/index.d.ts +17 -21
- package/dist/prompts/index.js +131 -259
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +242 -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 +176 -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/config.d.ts +7 -0
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- 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 +59 -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/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 +578 -0
- package/dist/tools/definitions.js.map +1 -0
- 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 +280 -80
- 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/net-ping-sweep.js +69 -16
- package/dist/tools/net-ping-sweep.js.map +1 -1
- package/dist/tools/nmap-runner.d.ts +19 -7
- package/dist/tools/nmap-runner.js +182 -80
- 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 +272 -63
- 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/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-v2/app/App.js +25 -39
- 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 +115 -10
- 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 +11 -1
- package/dist/tui-v2/rendering/tool-presenter.js +101 -5
- 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 +75 -1
- 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/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 +16 -11
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getActiveProjectRoot } from "./project-root.js";
|
|
4
|
+
/** Rehydrate the live task ledger from durable plan evidence after a resume. */
|
|
5
|
+
export function ledgerFromTaskEvidence(taskId, evidence) {
|
|
6
|
+
return {
|
|
7
|
+
taskId,
|
|
8
|
+
successWorkCount: evidence?.successWorkCount ?? 0,
|
|
9
|
+
...(evidence ?? {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Store only serializable evidence on the plan; the task id is already its key. */
|
|
13
|
+
export function taskEvidenceFromLedger(ledger) {
|
|
14
|
+
const { taskId: _taskId, ...evidence } = ledger;
|
|
15
|
+
return evidence;
|
|
16
|
+
}
|
|
17
|
+
export function isMetaPlanTool(name) {
|
|
18
|
+
return name === "plan.create" || name === "task.update" || name === "agent.handoff";
|
|
19
|
+
}
|
|
20
|
+
/** Tools that count as real work/verify evidence for the open task. */
|
|
21
|
+
export function isEvidenceWorkTool(name) {
|
|
22
|
+
return !isMetaPlanTool(name);
|
|
23
|
+
}
|
|
24
|
+
export function isPentestPlanKind(kind) {
|
|
25
|
+
return (kind ?? "").toLowerCase() === "pentest";
|
|
26
|
+
}
|
|
27
|
+
export function classifyTaskTitle(title, opts) {
|
|
28
|
+
const t = title.toLowerCase();
|
|
29
|
+
const pentest = isPentestPlanKind(opts?.planKind);
|
|
30
|
+
// Pentest plans: never map security work onto coding verify/implement gates.
|
|
31
|
+
if (pentest) {
|
|
32
|
+
if (/\b(report|write.?up|finding|summar|document|residual)\b/.test(t)) {
|
|
33
|
+
return "report";
|
|
34
|
+
}
|
|
35
|
+
if (/\b(exploit|poc|payload|privesc|lateral|inject|bruteforce|sqlmap|listener|reverse\s+shell|c2)\b/.test(t)) {
|
|
36
|
+
return "exploit";
|
|
37
|
+
}
|
|
38
|
+
if (/\b(recon|enumerat|fingerprint|osint|whois|dns|nmap|discover|probe|scan|fuzz|content\s+discover|subdomain|port)\b/.test(t)) {
|
|
39
|
+
return "recon";
|
|
40
|
+
}
|
|
41
|
+
if (/\b(explore|inspect|list|read|survey|map)\b/.test(t)) {
|
|
42
|
+
return "explore";
|
|
43
|
+
}
|
|
44
|
+
return "generic";
|
|
45
|
+
}
|
|
46
|
+
if (/\b(dev\s*server|run\s+dev|start\s+.*server|localhost|shell\.start|leave\s+.*running|probe|verify\s+in\s+browser)\b/.test(t) ||
|
|
47
|
+
(/\b(start|run)\b/.test(t) && /\b(server|dev|app|verify)\b/.test(t))) {
|
|
48
|
+
return "verify";
|
|
49
|
+
}
|
|
50
|
+
if (looksLikeInstallTaskTitle(title))
|
|
51
|
+
return "install";
|
|
52
|
+
if (looksLikeScaffoldTaskTitle(title))
|
|
53
|
+
return "scaffold";
|
|
54
|
+
if (/\b(implement|integrate|feature|rewrite|component|todo|persist|localstorage|styling|styles?|ui|page)\b/.test(t) ||
|
|
55
|
+
// endpoint/route only count as implement for product apps, not bare "API routes" recon phrasing
|
|
56
|
+
(/\b(endpoint|route)\b/.test(t) &&
|
|
57
|
+
/\b(implement|build|add|create|feature|component|ui|page)\b/.test(t))) {
|
|
58
|
+
return "implement";
|
|
59
|
+
}
|
|
60
|
+
if (/\b(recon|enumerat|fingerprint|osint|whois|dns|nmap|discover)\b/.test(t)) {
|
|
61
|
+
return "recon";
|
|
62
|
+
}
|
|
63
|
+
if (/\b(exploit|poc|payload|privesc|lateral|inject|bruteforce|sqlmap)\b/.test(t)) {
|
|
64
|
+
return "exploit";
|
|
65
|
+
}
|
|
66
|
+
if (/\b(report|write.?up|finding|summar|document)\b/.test(t)) {
|
|
67
|
+
return "report";
|
|
68
|
+
}
|
|
69
|
+
if (/\b(explore|inspect|list|read|survey|map)\b/.test(t) ||
|
|
70
|
+
/\bcheck\b.+\b(exists?|empty|directory|folder|path)\b/.test(t) ||
|
|
71
|
+
/\b(exists?|empty)\b.+\b(directory|folder|path|project)\b/.test(t) ||
|
|
72
|
+
// "Check Node.js and npm availability", "verify tools present", etc.
|
|
73
|
+
/\bcheck\b.+\b(node|npm|pnpm|yarn|bun|python|availability|available|installed|present|toolchain|tools?)\b/.test(t) ||
|
|
74
|
+
/\b(availability|available|installed|present)\b.+\b(node|npm|toolchain|tools?)\b/.test(t) ||
|
|
75
|
+
/\bverify\b.+\b(tools?|node|npm|setup|environment|prereq)\b/.test(t)) {
|
|
76
|
+
return "explore";
|
|
77
|
+
}
|
|
78
|
+
return "generic";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a successful tool can satisfy a task of the given class.
|
|
82
|
+
* Used to retroactively credit preflight work (tool.check before in_progress).
|
|
83
|
+
*/
|
|
84
|
+
export function toolFitsTaskClass(toolName, taskTitle, opts) {
|
|
85
|
+
if (!isEvidenceWorkTool(toolName))
|
|
86
|
+
return false;
|
|
87
|
+
const cls = classifyTaskTitle(taskTitle, { planKind: opts?.planKind });
|
|
88
|
+
const s = opts?.signals;
|
|
89
|
+
switch (cls) {
|
|
90
|
+
case "explore":
|
|
91
|
+
return (toolName === "tool.check" ||
|
|
92
|
+
toolName === "sysinfo" ||
|
|
93
|
+
toolName === "fs.list" ||
|
|
94
|
+
toolName === "fs.read" ||
|
|
95
|
+
toolName === "fs.search" ||
|
|
96
|
+
toolName === "net.context" ||
|
|
97
|
+
toolName === "shell.exec" // version probes, which/where
|
|
98
|
+
);
|
|
99
|
+
case "scaffold":
|
|
100
|
+
return (Boolean(s?.scaffoldOk) ||
|
|
101
|
+
toolName === "fs.write" ||
|
|
102
|
+
toolName === "fs.writeMany" ||
|
|
103
|
+
toolName === "shell.exec" ||
|
|
104
|
+
toolName === "shell.start");
|
|
105
|
+
case "install":
|
|
106
|
+
return Boolean(s?.installOk) || toolName === "pkg.install";
|
|
107
|
+
case "implement":
|
|
108
|
+
return (Boolean(s?.sourceWrite || s?.featureWrite) ||
|
|
109
|
+
toolName === "fs.write" ||
|
|
110
|
+
toolName === "fs.writeMany" ||
|
|
111
|
+
toolName === "fs.edit" ||
|
|
112
|
+
toolName === "fs.replaceLines" ||
|
|
113
|
+
toolName === "fs.append");
|
|
114
|
+
case "verify":
|
|
115
|
+
return (Boolean(s?.devServerStart ||
|
|
116
|
+
s?.localHttpProbeOk ||
|
|
117
|
+
s?.serverReady ||
|
|
118
|
+
s?.portListening) ||
|
|
119
|
+
toolName === "shell.start" ||
|
|
120
|
+
toolName === "shell.tail" ||
|
|
121
|
+
toolName === "http.fetch" ||
|
|
122
|
+
toolName === "web.fetch");
|
|
123
|
+
case "recon":
|
|
124
|
+
return (Boolean(s?.remoteReconOk) ||
|
|
125
|
+
/^(dns\.lookup|whois\.lookup|http\.fetch|web\.fetch|net\.scan|pentest\.recon|net\.pingSweep|net\.context|tool\.batch)$/.test(toolName));
|
|
126
|
+
case "exploit":
|
|
127
|
+
return Boolean(s?.remoteActiveTestOk) || isEvidenceWorkTool(toolName);
|
|
128
|
+
default:
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Fold turn-level loose successes into a task ledger so preflight work
|
|
134
|
+
* (tool.check / fs.list before in_progress) can satisfy explore/check tasks.
|
|
135
|
+
*/
|
|
136
|
+
export function absorbLooseWorkIntoLedger(ledger, taskId, taskTitle, loose, opts) {
|
|
137
|
+
if (!taskId || loose.length === 0)
|
|
138
|
+
return ledger;
|
|
139
|
+
let led = ledger;
|
|
140
|
+
for (const item of loose) {
|
|
141
|
+
if (!toolFitsTaskClass(item.toolName, taskTitle, {
|
|
142
|
+
planKind: opts?.planKind,
|
|
143
|
+
signals: item.signals,
|
|
144
|
+
})) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
led = recordTaskWorkSuccess(led, taskId, item.toolName, item.signals);
|
|
148
|
+
}
|
|
149
|
+
return led;
|
|
150
|
+
}
|
|
151
|
+
export function recordTaskWorkSuccess(ledger, taskId, toolName, signals) {
|
|
152
|
+
if (!taskId || !isEvidenceWorkTool(toolName))
|
|
153
|
+
return ledger;
|
|
154
|
+
const base = !ledger || ledger.taskId !== taskId
|
|
155
|
+
? { taskId, successWorkCount: 1, lastOkTool: toolName }
|
|
156
|
+
: {
|
|
157
|
+
...ledger,
|
|
158
|
+
successWorkCount: ledger.successWorkCount + 1,
|
|
159
|
+
lastOkTool: toolName,
|
|
160
|
+
};
|
|
161
|
+
if (signals?.sourceWrite)
|
|
162
|
+
base.sawSourceWrite = true;
|
|
163
|
+
if (signals?.featureWrite)
|
|
164
|
+
base.sawFeatureWrite = true;
|
|
165
|
+
if (signals?.installOk)
|
|
166
|
+
base.sawInstallOk = true;
|
|
167
|
+
if (signals?.scaffoldOk)
|
|
168
|
+
base.sawScaffoldOk = true;
|
|
169
|
+
if (signals?.devServerStart)
|
|
170
|
+
base.sawDevServerStart = true;
|
|
171
|
+
if (signals?.localHttpProbeOk)
|
|
172
|
+
base.sawLocalHttpProbeOk = true;
|
|
173
|
+
if (signals?.serverReady)
|
|
174
|
+
base.sawServerReady = true;
|
|
175
|
+
if (signals?.portListening)
|
|
176
|
+
base.sawPortListening = true;
|
|
177
|
+
if (signals?.remoteReconOk)
|
|
178
|
+
base.sawRemoteReconOk = true;
|
|
179
|
+
if (signals?.remoteActiveTestOk)
|
|
180
|
+
base.sawRemoteActiveTestOk = true;
|
|
181
|
+
return base;
|
|
182
|
+
}
|
|
183
|
+
export function openTaskLedger(taskId) {
|
|
184
|
+
return { taskId, successWorkCount: 0 };
|
|
185
|
+
}
|
|
186
|
+
/** Local app runtime is proven by any strong independent signal. */
|
|
187
|
+
export function hasLocalRuntimeProof(evidence) {
|
|
188
|
+
return Boolean(evidence?.sawDevServerStart ||
|
|
189
|
+
evidence?.sawLocalHttpProbeOk ||
|
|
190
|
+
evidence?.sawServerReady ||
|
|
191
|
+
evidence?.sawPortListening);
|
|
192
|
+
}
|
|
193
|
+
export function hasRemoteWorkProof(evidence) {
|
|
194
|
+
return Boolean(evidence?.sawRemoteReconOk || evidence?.sawRemoteActiveTestOk);
|
|
195
|
+
}
|
|
196
|
+
/** Dev-server job log shows ready + local URL. */
|
|
197
|
+
export function isServerReadyOutput(output) {
|
|
198
|
+
if (!output)
|
|
199
|
+
return false;
|
|
200
|
+
const hasUrl = /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\b/i.test(output) ||
|
|
201
|
+
/➜\s*Local:\s*https?:\/\//i.test(output);
|
|
202
|
+
const hasReady = /\b(?:ready in|VITE\s+v?\d|compiled successfully|started server|listening on|Local:\s*http)\b/i.test(output);
|
|
203
|
+
return hasUrl || hasReady;
|
|
204
|
+
}
|
|
205
|
+
/** lsof/ss (or similar) shows a process LISTENing on a local port. */
|
|
206
|
+
export function isPortListeningOutput(command, output) {
|
|
207
|
+
if (!output || !/\bLISTEN(?:ING)?\b/i.test(output))
|
|
208
|
+
return false;
|
|
209
|
+
const cmd = command.toLowerCase();
|
|
210
|
+
if (/\b(lsof|ss\b|netstat)\b/.test(cmd))
|
|
211
|
+
return true;
|
|
212
|
+
// Output-only: node/… LISTEN with a localhost/port line
|
|
213
|
+
return (/\b(?:localhost|127\.0\.0\.1|\[::1\]|\*:)\S*\d+/i.test(output) ||
|
|
214
|
+
/\bTCP\b.+\bLISTEN/i.test(output));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Leave/keep-running observational tasks may inherit already-proven runtime.
|
|
218
|
+
* "for user to test" must NOT block inheritance (prior brittle exclusion of "test").
|
|
219
|
+
*/
|
|
220
|
+
export function isRuntimeObservationTask(title) {
|
|
221
|
+
const t = title.toLowerCase();
|
|
222
|
+
const leaveKeep = /\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bserver\s+(?:is\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/.test(t);
|
|
223
|
+
if (!leaveKeep)
|
|
224
|
+
return false;
|
|
225
|
+
// Primary action is still start/launch/probe → not pure observation
|
|
226
|
+
if (/\b(?:start|launch|run)\s+(?:the\s+)?(?:dev\s*)?server\b/.test(t) ||
|
|
227
|
+
/\b(?:probe|curl)\s+(?:localhost|http)\b/.test(t)) {
|
|
228
|
+
// Combined "start … leave running" is verify work, not pure observation
|
|
229
|
+
if (/\b(start|run|launch)\b/.test(t) && /\b(dev\s*server|npm\s+run\s+dev|shell\.start)\b/.test(t)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Titles that are ONLY leave/keep running (optionally "for user to test")
|
|
234
|
+
if (/\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/.test(t)) {
|
|
235
|
+
// If title is primarily leave-running without requiring a fresh start as the main verb first
|
|
236
|
+
if (!/^(?:start|run|launch)\b/.test(t.trim()))
|
|
237
|
+
return true;
|
|
238
|
+
// "Leave server running for user to test" — leave is the head intent
|
|
239
|
+
if (/^leave\b|^keep\b/.test(t.trim()))
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return (/\bserver\s+(?:is\s+)?running\b/.test(t) &&
|
|
243
|
+
!/\b(?:start|run|launch)\b/.test(t));
|
|
244
|
+
}
|
|
245
|
+
/** Report / residual documentation that can inherit prior remote evidence. */
|
|
246
|
+
export function isRemoteObservationTask(title) {
|
|
247
|
+
const t = title.toLowerCase();
|
|
248
|
+
if (/\b(report|write.?up|summar|document|residual|findings?\s+summary)\b/.test(t)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (/\bleave\s+(?:the\s+)?(?:listener|job|tunnel)\s+running\b|\bkeep\s+(?:the\s+)?listener\b/.test(t)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Typed evidence gate: base rule is ≥1 successful work tool; implement/install/verify
|
|
258
|
+
* (and pentest recon) require stronger signals when classified. Existing project and
|
|
259
|
+
* already-proven runtime/remote facts satisfy corresponding observational tasks.
|
|
260
|
+
*/
|
|
261
|
+
export function canMarkTaskDone(ledger, taskId, opts) {
|
|
262
|
+
const title = opts?.taskTitle ?? "";
|
|
263
|
+
const planKind = opts?.planKind;
|
|
264
|
+
const pentest = isPentestPlanKind(planKind);
|
|
265
|
+
const cls = title ? classifyTaskTitle(title, { planKind }) : "generic";
|
|
266
|
+
const leaveRunningIntent = isRuntimeObservationTask(title) ||
|
|
267
|
+
/\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/i.test(title);
|
|
268
|
+
const inheritedCompletion = (cls === "scaffold" && Boolean(opts?.existingProject)) ||
|
|
269
|
+
(!pentest && leaveRunningIntent && Boolean(opts?.runtimeVerified)) ||
|
|
270
|
+
(pentest &&
|
|
271
|
+
isRemoteObservationTask(title) &&
|
|
272
|
+
Boolean(opts?.remoteWorkVerified));
|
|
273
|
+
if ((!ledger || ledger.taskId !== taskId) && !inheritedCompletion) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
reason: `Cannot mark [${taskId}] done: no work ledger for this task. ` +
|
|
277
|
+
`Call task.update {taskId:"${taskId}", state:"in_progress"}, do the real work, ` +
|
|
278
|
+
`inspect the tool result, and only then mark done if you are satisfied.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if ((ledger?.successWorkCount ?? 0) < 1 && !inheritedCompletion) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
reason: `Cannot mark [${taskId}] done: no successful tool result observed since it went in_progress. ` +
|
|
285
|
+
`Do the work, wait for the tool output, verify it succeeded, and only mark done when satisfied. ` +
|
|
286
|
+
(ledger?.lastOkTool
|
|
287
|
+
? ""
|
|
288
|
+
: "Example: fs.write / shell.exec / shell.start must return ok first."),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const evidence = ledger && ledger.taskId === taskId ? ledger : openTaskLedger(taskId);
|
|
292
|
+
// Feature implement gate is coding-only.
|
|
293
|
+
if (!pentest && cls === "implement" && opts?.featureAppRequired) {
|
|
294
|
+
const featureOk = Boolean(evidence.sawFeatureWrite) || Boolean(opts.sessionFeatureSeen);
|
|
295
|
+
if (!featureOk) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
reason: `Cannot mark [${taskId}] done: this is a feature/implement task for a product app, ` +
|
|
299
|
+
`but no real feature code was written yet (starter boilerplate does not count). ` +
|
|
300
|
+
`Replace the default starter with the requested feature (e.g. todo add/list/toggle), then mark done.`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!pentest && cls === "install" && !evidence.sawInstallOk) {
|
|
305
|
+
if (evidence.successWorkCount < 1) {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
reason: `Cannot mark [${taskId}] done: install task needs a successful package install ` +
|
|
309
|
+
`(npm/pnpm/yarn/bun install, etc.) under this task.`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Local app runtime: multi-signal proof. Never apply to pentest plans.
|
|
314
|
+
if (!pentest && (cls === "verify" || leaveRunningIntent)) {
|
|
315
|
+
const wantsServer = leaveRunningIntent ||
|
|
316
|
+
/\b(dev\s*server|localhost|shell\.start|run\s+dev|leave\s+.*running)\b/i.test(title);
|
|
317
|
+
if (wantsServer) {
|
|
318
|
+
const proof = hasLocalRuntimeProof(evidence) || Boolean(opts?.runtimeVerified);
|
|
319
|
+
if (!proof) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
reason: `Cannot mark [${taskId}] done: local app runtime not proven yet. ` +
|
|
323
|
+
`Accept any of: shell.start (dev server), shell.tail ready+URL, port LISTEN (lsof/ss), ` +
|
|
324
|
+
`or successful localhost GET probe. If an earlier plan task already proved the server, ` +
|
|
325
|
+
`confirm it is still alive once then mark done — do not thrash ports with restarts.`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Pentest recon: need real remote recon, not local fs noise alone.
|
|
331
|
+
if (pentest && cls === "recon") {
|
|
332
|
+
const remoteOk = hasRemoteWorkProof(evidence) || Boolean(opts?.remoteWorkVerified);
|
|
333
|
+
if (!remoteOk) {
|
|
334
|
+
// Allow if last tool was a recon tool name even without typed flag (soft)
|
|
335
|
+
const last = evidence.lastOkTool ?? "";
|
|
336
|
+
const lastLooksRecon = /^(dns\.lookup|whois\.lookup|http\.fetch|web\.fetch|net\.scan|pentest\.recon|net\.pingSweep|tool\.batch)$/.test(last);
|
|
337
|
+
if (!lastLooksRecon) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
reason: `Cannot mark [${taskId}] done: recon needs successful remote evidence ` +
|
|
341
|
+
`(dns/http/net.scan/pentest.recon/whois against the target), not local-only tools.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { ok: true };
|
|
347
|
+
}
|
|
348
|
+
/** True when a tool call is remote recon (not local app tooling). */
|
|
349
|
+
export function isRemoteReconToolCall(call) {
|
|
350
|
+
if (call.name === "dns.lookup" ||
|
|
351
|
+
call.name === "whois.lookup" ||
|
|
352
|
+
call.name === "net.scan" ||
|
|
353
|
+
call.name === "pentest.recon" ||
|
|
354
|
+
call.name === "net.pingSweep" ||
|
|
355
|
+
call.name === "net.context") {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
if (call.name === "http.fetch" || call.name === "web.fetch") {
|
|
359
|
+
const url = typeof call.args.url === "string" ? call.args.url : "";
|
|
360
|
+
// Localhost is coding probe, not remote recon
|
|
361
|
+
if (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(url)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return Boolean(url);
|
|
365
|
+
}
|
|
366
|
+
if (call.name === "shell.exec" || call.name === "shell.start") {
|
|
367
|
+
const cmd = commandOf(call);
|
|
368
|
+
return /\b(?:nmap|masscan|ffuf|gobuster|feroxbuster|nikto|nuclei|httpx|subfinder|amass|dig|whois|whatweb)\b/i.test(cmd);
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
/** Active/offensive test (not passive recon). */
|
|
373
|
+
export function isRemoteActiveTestCall(call) {
|
|
374
|
+
if (call.name === "http.fetch") {
|
|
375
|
+
const method = String(call.args.method ?? "GET").toUpperCase();
|
|
376
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
const url = typeof call.args.url === "string" ? call.args.url : "";
|
|
380
|
+
return /\b(?:union\s+select|<\s*script|javascript:|file:\/\/|169\.254\.169\.254)\b/i.test(url);
|
|
381
|
+
}
|
|
382
|
+
if (call.name === "shell.exec" || call.name === "shell.start") {
|
|
383
|
+
const cmd = commandOf(call);
|
|
384
|
+
return /\b(?:sqlmap|hydra|nikto|nuclei|msfconsole|exploit|payload|reverse\s+shell)\b/i.test(cmd);
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
function commandOf(call) {
|
|
389
|
+
return typeof call.args.command === "string" ? call.args.command : "";
|
|
390
|
+
}
|
|
391
|
+
/** One-shot scaffolders — must NEVER be treated as "start the dev server". Stack-agnostic. */
|
|
392
|
+
export function isScaffoldCreateCommand(cmd) {
|
|
393
|
+
return /\b(?:npm\s+create|npm\s+init|yarn\s+create|pnpm\s+create|bun\s+create|npx\s+(?:--yes\s+)?create-[\w-]+|create-vite|create-next-app|create-react-app|cargo\s+new|cargo\s+init|go\s+mod\s+init|poetry\s+new|django-admin\s+startproject|rails\s+new|composer\s+create-project|mix\s+new|flutter\s+create|dotnet\s+new)\b/i.test(cmd);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* True only for actually running a long-lived dev server — not create-vite /
|
|
397
|
+
* npm create which merely contain the word "vite".
|
|
398
|
+
*/
|
|
399
|
+
export function isDevServerCall(call) {
|
|
400
|
+
const cmd = commandOf(call);
|
|
401
|
+
if (isScaffoldCreateCommand(cmd))
|
|
402
|
+
return false;
|
|
403
|
+
if (call.name === "shell.start") {
|
|
404
|
+
// Explicit background job is usually a server; still exclude create-*.
|
|
405
|
+
if (!cmd)
|
|
406
|
+
return true;
|
|
407
|
+
return (/\bnpm\s+run\s+dev\b|\byarn\s+dev\b|\bpnpm\s+(run\s+)?dev\b|\bbun\s+(run\s+)?dev\b|\bnext\s+dev\b|\bnuxt\s+dev\b|\bcargo\s+watch\b|\bflask\s+run\b|\buvicorn\b|\bgunicorn\b|\brails\s+s(?:erver)?\b|\bdjango(-admin)?\s+runserver\b|\bdotnet\s+run\b|\bgo\s+run\b/i.test(cmd) ||
|
|
408
|
+
// bare `vite` / `vite --host` as the process — not `create vite`
|
|
409
|
+
/(?:^|[;&|]\s*)vite(?:\s|$)/i.test(cmd) ||
|
|
410
|
+
/\b(python3?\s+-m\s+http\.server|php\s+-S)\b/i.test(cmd));
|
|
411
|
+
}
|
|
412
|
+
return (/\bnpm\s+run\s+dev\b|\byarn\s+dev\b|\bpnpm\s+(run\s+)?dev\b|\bbun\s+(run\s+)?dev\b|\bnext\s+dev\b|\bnuxt\s+dev\b|\brails\s+s(?:erver)?\b|\bdjango(-admin)?\s+runserver\b/i.test(cmd) || /(?:^|[;&|]\s*)vite(?:\s|$)/i.test(cmd));
|
|
413
|
+
}
|
|
414
|
+
/** Inspect tools allowed without opening a plan task. */
|
|
415
|
+
export function isPlanPreflightTool(name) {
|
|
416
|
+
return (name === "tool.check" ||
|
|
417
|
+
name === "sysinfo" ||
|
|
418
|
+
name === "fs.list" ||
|
|
419
|
+
name === "fs.read" ||
|
|
420
|
+
name === "fs.search" ||
|
|
421
|
+
name === "net.context");
|
|
422
|
+
}
|
|
423
|
+
/** Read-only recon tools that may run without an in_progress task on pentest plans. */
|
|
424
|
+
export function isReadOnlyReconTool(name) {
|
|
425
|
+
return (isPlanPreflightTool(name) ||
|
|
426
|
+
name === "tool.batch" ||
|
|
427
|
+
name === "dns.lookup" ||
|
|
428
|
+
name === "whois.lookup" ||
|
|
429
|
+
name === "http.fetch" ||
|
|
430
|
+
name === "web.fetch" ||
|
|
431
|
+
name === "web.search" ||
|
|
432
|
+
name === "net.scan" ||
|
|
433
|
+
name === "pentest.recon" ||
|
|
434
|
+
name === "wordlist.find");
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Tools whose failure must NOT cancel later siblings in the same model turn.
|
|
438
|
+
* Plan bookkeeping failures (task.update done-gate, etc.) and independent
|
|
439
|
+
* recon lookups should never wipe a whole batch of http.fetch / dns calls.
|
|
440
|
+
*/
|
|
441
|
+
export function isBatchSoftFailTool(name) {
|
|
442
|
+
if (name === "plan.create" || name === "task.update")
|
|
443
|
+
return true;
|
|
444
|
+
if (name === "tool.batch" || name === "tool.check")
|
|
445
|
+
return true;
|
|
446
|
+
if (isReadOnlyReconTool(name))
|
|
447
|
+
return true;
|
|
448
|
+
if (name === "net.pingSweep" ||
|
|
449
|
+
name === "shell.jobs" ||
|
|
450
|
+
name === "shell.tail" ||
|
|
451
|
+
name === "sysinfo") {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
/** Tools allowed on a coding build turn before plan.create exists. */
|
|
457
|
+
export function isBuildPrePlanAllowedTool(name) {
|
|
458
|
+
return (name === "plan.create" ||
|
|
459
|
+
isPlanPreflightTool(name) ||
|
|
460
|
+
name === "tool.batch" ||
|
|
461
|
+
name === "fs.search" ||
|
|
462
|
+
name === "web.search" ||
|
|
463
|
+
name === "web.fetch");
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Commands that legitimately sit quiet for minutes (npm install, create-next-app).
|
|
467
|
+
* Must not be killed by the short "no output" stall watchdog.
|
|
468
|
+
*/
|
|
469
|
+
export function isLongQuietInstallOrScaffoldCommand(cmd) {
|
|
470
|
+
if (!cmd.trim())
|
|
471
|
+
return false;
|
|
472
|
+
if (isScaffoldCreateCommand(cmd))
|
|
473
|
+
return true;
|
|
474
|
+
return (/\b(?:npm|pnpm|yarn|bun)\s+i(?:nstall)?\b/i.test(cmd) ||
|
|
475
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:ci|update)\b/i.test(cmd) ||
|
|
476
|
+
/\bpip(?:3)?\s+install\b/i.test(cmd) ||
|
|
477
|
+
/\bpoetry\s+install\b/i.test(cmd) ||
|
|
478
|
+
/\bcomposer\s+install\b/i.test(cmd) ||
|
|
479
|
+
/\bbundle\s+install\b/i.test(cmd) ||
|
|
480
|
+
/\bcargo\s+(?:build|fetch|install)\b/i.test(cmd) ||
|
|
481
|
+
/\bgo\s+mod\s+(?:download|tidy)\b/i.test(cmd) ||
|
|
482
|
+
/\bdotnet\s+restore\b/i.test(cmd));
|
|
483
|
+
}
|
|
484
|
+
/** Stall watchdog budget: scaffold/install can be silent for a long time. */
|
|
485
|
+
export function toolStallBudgetMs(call) {
|
|
486
|
+
const cmd = typeof call.args.command === "string" ? call.args.command : "";
|
|
487
|
+
if (call.name === "pkg.install")
|
|
488
|
+
return 10 * 60_000;
|
|
489
|
+
if ((call.name === "shell.exec" || call.name === "shell.start") &&
|
|
490
|
+
isLongQuietInstallOrScaffoldCommand(cmd)) {
|
|
491
|
+
return 15 * 60_000; // create-next-app + npm install often 2–10+ min with quiet stretches
|
|
492
|
+
}
|
|
493
|
+
return 60_000;
|
|
494
|
+
}
|
|
495
|
+
/** Harmless version/which probes models use instead of tool.check before planning. */
|
|
496
|
+
export function isReadOnlyVersionProbeCommand(cmd) {
|
|
497
|
+
const c = cmd.trim();
|
|
498
|
+
if (!c || /[;&|`]/.test(c))
|
|
499
|
+
return false; // no chains
|
|
500
|
+
return (/^(?:node|nodejs|npm|npx|pnpm|yarn|bun|deno|python3?|pip3?|go|rustc|cargo|ruby|php|java|dotnet|flutter|swift)\s+(?:-v|--version|version)\s*$/i.test(c) || /^(?:which|command\s+-v|type)\s+[A-Za-z0-9._-]+\s*$/i.test(c));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Multi-step coding builds must plan before mutate (scaffold/write/install).
|
|
504
|
+
* Trivial one-liners and non-build prompts skip this.
|
|
505
|
+
*/
|
|
506
|
+
export function codingBuildRequiresPlan(prompt, opts) {
|
|
507
|
+
if (opts?.informational || opts?.idle || opts?.pentest)
|
|
508
|
+
return false;
|
|
509
|
+
const p = prompt.trim();
|
|
510
|
+
if (!p)
|
|
511
|
+
return false;
|
|
512
|
+
// Explicit single-file micro edits can skip the plan
|
|
513
|
+
if (p.length < 100 &&
|
|
514
|
+
/\b(fix|typo|rename|bump|tweak)\b/i.test(p) &&
|
|
515
|
+
!/\b(app|project|scaffold|create|build|todo|blog)\b/i.test(p)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true; // caller already checked looksLikeBuildTask
|
|
519
|
+
}
|
|
520
|
+
function isLocalHttpProbe(call) {
|
|
521
|
+
const blob = `${call.name} ${commandOf(call)} ${JSON.stringify(call.args)}`;
|
|
522
|
+
return (/\b(localhost|127\.0\.0\.1)\b/i.test(blob) &&
|
|
523
|
+
(call.name === "shell.exec" ||
|
|
524
|
+
call.name === "http.fetch" ||
|
|
525
|
+
call.name === "web.fetch" ||
|
|
526
|
+
/\bcurl\b/i.test(blob)));
|
|
527
|
+
}
|
|
528
|
+
export function looksLikeInstallTaskTitle(title) {
|
|
529
|
+
const t = title.toLowerCase();
|
|
530
|
+
return (/\b(install|dependencies|deps|packages)\b/.test(t) &&
|
|
531
|
+
!/\b(dev\s*server|localhost|probe|run\s+dev)\b/.test(t));
|
|
532
|
+
}
|
|
533
|
+
export function looksLikeScaffoldTaskTitle(title) {
|
|
534
|
+
const t = title.toLowerCase();
|
|
535
|
+
return (/\b(scaffold|create-vite|create vite|create-next|init project|bootstrap|cargo\s+new|rails\s+new|poetry\s+new)\b/.test(t) ||
|
|
536
|
+
(/\b(create|init|generate)\b/.test(t) &&
|
|
537
|
+
/\b(project|app|package|crate|module|vite)\b/.test(t) &&
|
|
538
|
+
!/\b(feature|component|endpoint|todo|style|persist)\b/.test(t)));
|
|
539
|
+
}
|
|
540
|
+
export function isPackageInstallCommand(cmd) {
|
|
541
|
+
return /\bnpm\s+i(nstall)?\b|\byarn\s+install\b|\bpnpm\s+i(nstall)?\b|\bbun\s+install\b|\bpip\s+install\b|\bpoetry\s+install\b|\bcargo\s+build\b|\bcomposer\s+install\b|\bbundle\s+install\b|\bgo\s+mod\s+tidy\b/.test(cmd);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Choose which pending plan task should own this tool call (auto-start).
|
|
545
|
+
* Prevents npm install from being attributed to "localStorage" / style tasks.
|
|
546
|
+
*/
|
|
547
|
+
export function pickPendingTaskForToolCall(pending, call, _allTitles) {
|
|
548
|
+
if (pending.length === 0)
|
|
549
|
+
return undefined;
|
|
550
|
+
const cmd = commandOf(call);
|
|
551
|
+
const score = (title) => {
|
|
552
|
+
// Heuristics rank likely ownership only. They must never make a pending
|
|
553
|
+
// task ineligible or authorize/block the underlying tool call.
|
|
554
|
+
let s = 1;
|
|
555
|
+
if (isPackageInstallCommand(cmd)) {
|
|
556
|
+
if (looksLikeInstallTaskTitle(title))
|
|
557
|
+
s += 20;
|
|
558
|
+
else if (looksLikeScaffoldTaskTitle(title))
|
|
559
|
+
s += 10;
|
|
560
|
+
else
|
|
561
|
+
s -= 5; // prefer not to attach install to feature tasks
|
|
562
|
+
}
|
|
563
|
+
if (isDevServerCall(call) || isLocalHttpProbe(call)) {
|
|
564
|
+
if (/\b(dev\s*server|run\s+dev|localhost|shell\.start|probe|verify)\b/i.test(title)) {
|
|
565
|
+
s += 20;
|
|
566
|
+
}
|
|
567
|
+
else
|
|
568
|
+
s -= 10;
|
|
569
|
+
}
|
|
570
|
+
if (call.name === "fs.write" ||
|
|
571
|
+
call.name === "fs.writeMany" ||
|
|
572
|
+
call.name === "fs.edit") {
|
|
573
|
+
if (looksLikeInstallTaskTitle(title))
|
|
574
|
+
s -= 15;
|
|
575
|
+
if (/\b(component|feature|todo|implement|style|persist|localstorage|ui|page)\b/i.test(title)) {
|
|
576
|
+
s += 12;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// Keyword overlap (SSRF task ↔ /api/og-image fetch, fuzz ↔ /api/*, …)
|
|
580
|
+
const blob = `${call.name} ${cmd} ${JSON.stringify(call.args ?? {})}`.toLowerCase();
|
|
581
|
+
const words = title
|
|
582
|
+
.toLowerCase()
|
|
583
|
+
.split(/[^a-z0-9]+/)
|
|
584
|
+
.filter((w) => w.length > 3);
|
|
585
|
+
let hits = 0;
|
|
586
|
+
for (const w of words) {
|
|
587
|
+
if (blob.includes(w))
|
|
588
|
+
hits += 1;
|
|
589
|
+
}
|
|
590
|
+
if (hits > 0)
|
|
591
|
+
s += Math.min(12, hits * 3);
|
|
592
|
+
return s;
|
|
593
|
+
};
|
|
594
|
+
let best;
|
|
595
|
+
let bestScore = -Infinity;
|
|
596
|
+
for (const p of pending) {
|
|
597
|
+
const sc = score(p.title);
|
|
598
|
+
if (sc > bestScore) {
|
|
599
|
+
best = p;
|
|
600
|
+
bestScore = sc;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return best ?? pending[0];
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* User asked for a product feature app (todo, blog, …), not "just scaffold framework X".
|
|
607
|
+
* Stack-agnostic: detects intent, not a particular framework.
|
|
608
|
+
*/
|
|
609
|
+
export function userAskedForFeatureApp(prompt) {
|
|
610
|
+
const p = prompt.trim().toLowerCase();
|
|
611
|
+
if (!p)
|
|
612
|
+
return false;
|
|
613
|
+
if (/\b(just|only)\s+(scaffold|init|initialize|boilerplate|starter|template)\b/.test(p)) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
// Named product feature + app/project shape
|
|
617
|
+
if (/\b(todo|to-?do|blog|dashboard|chat|kanban|notes?|crm|shop|store|cart|auth|login|signup|crud|calendar|inbox|quiz|portfolio|gallery|tracker|inventory|booking|counter|timer|weather)\b/.test(p) &&
|
|
618
|
+
/\b(app|application|project|site|page|ui|list|board|service|api)\b/.test(p)) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
// "build a full … with X feature"
|
|
622
|
+
if (/\b(?:with|including|that\s+has|that\s+supports)\b[\s\S]{0,40}\b(todo|auth|crud|login|dashboard|blog)\b/.test(p)) {
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
/** Paths that look like application source (not lockfiles / build output). */
|
|
628
|
+
export function isAppSourcePath(path) {
|
|
629
|
+
const p = path.replace(/\\/g, "/");
|
|
630
|
+
if (/(?:^|\/)(?:node_modules|\.next|dist|build|coverage|\.git|vendor|target)(?:\/|$)/i.test(p)) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
if (/(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?)$/i.test(p)) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
return /\.(jsx?|tsx?|vue|svelte|py|go|rs|java|kt|rb|php|swift|cs|css|scss|sass|less|html|mdx)$/i.test(p);
|
|
637
|
+
}
|
|
638
|
+
export function extractWritePathsFromCall(call) {
|
|
639
|
+
const paths = [];
|
|
640
|
+
if (call.name === "fs.write" ||
|
|
641
|
+
call.name === "fs.edit" ||
|
|
642
|
+
call.name === "fs.replaceLines" ||
|
|
643
|
+
call.name === "fs.append") {
|
|
644
|
+
if (typeof call.args.path === "string")
|
|
645
|
+
paths.push(call.args.path);
|
|
646
|
+
}
|
|
647
|
+
if (call.name === "fs.writeMany" && Array.isArray(call.args.files)) {
|
|
648
|
+
for (const f of call.args.files) {
|
|
649
|
+
if (f && typeof f === "object" && typeof f.path === "string") {
|
|
650
|
+
paths.push(f.path);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return paths;
|
|
655
|
+
}
|
|
656
|
+
/** Default framework starter / marketing copy — not the product feature. */
|
|
657
|
+
export function looksLikeStarterBoilerplate(content) {
|
|
658
|
+
const c = content.slice(0, 8_000);
|
|
659
|
+
return (/\bWelcome to (?:Vite|React|Next\.js|Create React App|Nuxt|SvelteKit|Angular)\b/i.test(c) ||
|
|
660
|
+
/\bGet started by editing\b/i.test(c) ||
|
|
661
|
+
/\bEdit\s+`?(?:src\/)?App\.(?:tsx?|jsx?)`?\s+and save to (?:test|reload)/i.test(c) ||
|
|
662
|
+
/\blogos?\/(?:react|vite|next)\.svg\b/i.test(c) ||
|
|
663
|
+
/\bnext\.js logo\b/i.test(c) ||
|
|
664
|
+
/https?:\/\/(?:react\.dev|vitejs\.dev|nextjs\.org)\/(?:learn|docs)?/i.test(c) && /\b(?:Deploy|Learn|Templates|create-next-app)\b/i.test(c));
|
|
665
|
+
}
|
|
666
|
+
/** Content that looks like real product UI/logic for common feature apps. */
|
|
667
|
+
export function looksLikeFeatureProductCode(content) {
|
|
668
|
+
const c = content.slice(0, 12_000);
|
|
669
|
+
if (looksLikeStarterBoilerplate(c))
|
|
670
|
+
return false;
|
|
671
|
+
// Interactive product signals (todo/crud/auth/etc.)
|
|
672
|
+
if (/\b(?:useState|useReducer|createContext|signal\(|createSignal|ref\()\b/.test(c) &&
|
|
673
|
+
/\b(?:todo|todos|onClick|onSubmit|addTodo|toggle|complete|delete|remove|login|signup|password|fetch\(|axios|localStorage)\b/i.test(c)) {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
if (/\b(?:add|create|toggle|delete|remove|update)\b/i.test(c) &&
|
|
677
|
+
/\b(?:todo|item|task|post|note|user|cart)\b/i.test(c) &&
|
|
678
|
+
/\b(?:map\s*\(|filter\s*\(|set[A-Z]\w+|useState)\b/.test(c)) {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
// API/backend feature
|
|
682
|
+
if (/\b(?:app\.(?:get|post|put|patch|delete)|router\.(?:get|post)|@(?:Get|Post|Put|Delete)|def\s+\w+\(.*request)/i.test(c) &&
|
|
683
|
+
!looksLikeStarterBoilerplate(c)) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
function extractWriteContentsFromCall(call) {
|
|
689
|
+
const out = [];
|
|
690
|
+
if ((call.name === "fs.write" ||
|
|
691
|
+
call.name === "fs.append" ||
|
|
692
|
+
call.name === "fs.replaceLines") &&
|
|
693
|
+
typeof call.args.content === "string") {
|
|
694
|
+
out.push(call.args.content);
|
|
695
|
+
}
|
|
696
|
+
if (call.name === "fs.edit" && typeof call.args.newText === "string") {
|
|
697
|
+
out.push(call.args.newText);
|
|
698
|
+
}
|
|
699
|
+
if (call.name === "fs.writeMany" && Array.isArray(call.args.files)) {
|
|
700
|
+
for (const f of call.args.files) {
|
|
701
|
+
if (f &&
|
|
702
|
+
typeof f === "object" &&
|
|
703
|
+
typeof f.content === "string") {
|
|
704
|
+
out.push(f.content);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* True when a successful tool call implements product feature code (not only scaffold).
|
|
712
|
+
* Starter boilerplate alone does not count.
|
|
713
|
+
*/
|
|
714
|
+
export function isFeatureImplementationCall(call) {
|
|
715
|
+
const paths = extractWritePathsFromCall(call);
|
|
716
|
+
const contents = extractWriteContentsFromCall(call);
|
|
717
|
+
if (contents.some(looksLikeFeatureProductCode))
|
|
718
|
+
return true;
|
|
719
|
+
// Content present but only starter → not feature
|
|
720
|
+
if (contents.length > 0 &&
|
|
721
|
+
contents.every((c) => looksLikeStarterBoilerplate(c) || c.trim().length < 40)) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
// Path-only signal: source write without inspectable content still counts
|
|
725
|
+
// (e.g. truncated args) but not lockfiles/config alone.
|
|
726
|
+
if (paths.some(isAppSourcePath) && contents.length === 0)
|
|
727
|
+
return true;
|
|
728
|
+
if (paths.some(isAppSourcePath) &&
|
|
729
|
+
contents.some((c) => !looksLikeStarterBoilerplate(c) && c.trim().length >= 80)) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
// Hand-written multi-file setup with non-starter content
|
|
733
|
+
if (call.name === "fs.writeMany" &&
|
|
734
|
+
paths.length >= 2 &&
|
|
735
|
+
contents.some((c) => !looksLikeStarterBoilerplate(c))) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* User said work belongs on the Desktop (or similar). Returns absolute path.
|
|
742
|
+
*/
|
|
743
|
+
export function resolveUserDestinationHint(prompt, home = homedir()) {
|
|
744
|
+
const p = prompt.trim();
|
|
745
|
+
if (!p)
|
|
746
|
+
return undefined;
|
|
747
|
+
// Explicit absolute /Users/.../Desktop/... already in the prompt — prefer that root
|
|
748
|
+
const absDesktop = p.match(/((?:\/Users\/[^/\s]+|~)\/Desktop(?:\/[\w.-]+)?)/i);
|
|
749
|
+
if (absDesktop?.[1]) {
|
|
750
|
+
const raw = absDesktop[1].replace(/^~(?=\/|$)/, home);
|
|
751
|
+
// If they named a subfolder in the path, use parent Desktop for cwd default
|
|
752
|
+
if (/\/Desktop$/i.test(raw) || /\/Desktop\//i.test(raw)) {
|
|
753
|
+
const desk = raw.match(/^(.*?\/Desktop)/i);
|
|
754
|
+
return desk?.[1] ?? raw;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (/\b(?:in|on|into|under|to)\s+(?:the\s+)?desktop(?:\s+directory|\s+folder)?\b/i.test(p) ||
|
|
758
|
+
/\bdesktop\s+(?:directory|folder)\b/i.test(p) ||
|
|
759
|
+
/\bcreate\b.+\b(?:on|in)\s+desktop\b/i.test(p)) {
|
|
760
|
+
return join(home, "Desktop");
|
|
761
|
+
}
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Default shell cwd when the model omitted it but a project root / Desktop hint exists.
|
|
766
|
+
*/
|
|
767
|
+
export function applyDestinationCwd(call, destinationHint) {
|
|
768
|
+
if (call.name !== "shell.exec" && call.name !== "shell.start")
|
|
769
|
+
return call;
|
|
770
|
+
if (typeof call.args.cwd === "string" && call.args.cwd.trim())
|
|
771
|
+
return call;
|
|
772
|
+
const cmd = commandOf(call);
|
|
773
|
+
// Prefer sticky project root (todo-app) over bare Desktop for install/dev.
|
|
774
|
+
const cwd = getActiveProjectRoot() ?? destinationHint;
|
|
775
|
+
if (!cwd)
|
|
776
|
+
return call;
|
|
777
|
+
const looksCreate = isScaffoldCreateCommand(cmd);
|
|
778
|
+
// create into named subfolder: cwd should be parent of project if sticky root is the project
|
|
779
|
+
if (looksCreate) {
|
|
780
|
+
const nameMatch = cmd.match(/(?:create-vite(?:@\S+)?|create-next-app(?:@\S+)?|cargo\s+new|rails\s+new|poetry\s+new|flutter\s+create|mix\s+new|django-admin\s+startproject)\s+([A-Za-z0-9._-]+)/i);
|
|
781
|
+
if (nameMatch?.[1] && !nameMatch[1].startsWith("-")) {
|
|
782
|
+
const parent = cwd.replace(/[/\\][^/\\]+$/, "");
|
|
783
|
+
const useParent = cwd.endsWith(nameMatch[1]) ||
|
|
784
|
+
cwd.endsWith("/" + nameMatch[1]) ||
|
|
785
|
+
cwd.endsWith("\\" + nameMatch[1]);
|
|
786
|
+
return {
|
|
787
|
+
...call,
|
|
788
|
+
args: { ...call.args, cwd: useParent && parent ? parent : cwd },
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
// Prefer destination parent (Desktop) for create when sticky root is already a project
|
|
792
|
+
return { ...call, args: { ...call.args, cwd: destinationHint ?? cwd } };
|
|
793
|
+
}
|
|
794
|
+
const looksProjectLocal = /\b(npm\s+i(nstall)?|npm\s+run|yarn\s|pnpm\s|bun\s|pip\s+install|poetry\s+|cargo\s+|go\s+run|go\s+build|composer\s+|bundle\s+|dotnet\s+|flutter\s+|mix\s+|rails\s+|django)/i.test(cmd) || call.name === "shell.start";
|
|
795
|
+
if (!looksProjectLocal)
|
|
796
|
+
return call;
|
|
797
|
+
return {
|
|
798
|
+
...call,
|
|
799
|
+
args: { ...call.args, cwd },
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
//# sourceMappingURL=task-evidence.js.map
|