@mindfoldhq/trellis 0.6.0-beta.2 → 0.6.0-beta.20
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/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/claude.d.ts +29 -0
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
- package/dist/commands/channel/adapters/claude.js +203 -0
- package/dist/commands/channel/adapters/claude.js.map +1 -0
- package/dist/commands/channel/adapters/codex.d.ts +85 -0
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
- package/dist/commands/channel/adapters/codex.js +505 -0
- package/dist/commands/channel/adapters/codex.js.map +1 -0
- package/dist/commands/channel/adapters/index.d.ts +84 -0
- package/dist/commands/channel/adapters/index.d.ts.map +1 -0
- package/dist/commands/channel/adapters/index.js +115 -0
- package/dist/commands/channel/adapters/index.js.map +1 -0
- package/dist/commands/channel/adapters/types.d.ts +33 -0
- package/dist/commands/channel/adapters/types.d.ts.map +1 -0
- package/dist/commands/channel/adapters/types.js +2 -0
- package/dist/commands/channel/adapters/types.js.map +1 -0
- package/dist/commands/channel/agent-loader.d.ts +32 -0
- package/dist/commands/channel/agent-loader.d.ts.map +1 -0
- package/dist/commands/channel/agent-loader.js +154 -0
- package/dist/commands/channel/agent-loader.js.map +1 -0
- package/dist/commands/channel/context-loader.d.ts +26 -0
- package/dist/commands/channel/context-loader.d.ts.map +1 -0
- package/dist/commands/channel/context-loader.js +290 -0
- package/dist/commands/channel/context-loader.js.map +1 -0
- package/dist/commands/channel/context.d.ts +16 -0
- package/dist/commands/channel/context.d.ts.map +1 -0
- package/dist/commands/channel/context.js +83 -0
- package/dist/commands/channel/context.js.map +1 -0
- package/dist/commands/channel/create.d.ts +27 -0
- package/dist/commands/channel/create.d.ts.map +1 -0
- package/dist/commands/channel/create.js +39 -0
- package/dist/commands/channel/create.js.map +1 -0
- package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
- package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
- package/dist/commands/channel/dev-parse-trace.js +70 -0
- package/dist/commands/channel/dev-parse-trace.js.map +1 -0
- package/dist/commands/channel/guard.d.ts +150 -0
- package/dist/commands/channel/guard.d.ts.map +1 -0
- package/dist/commands/channel/guard.js +474 -0
- package/dist/commands/channel/guard.js.map +1 -0
- package/dist/commands/channel/index.d.ts +3 -0
- package/dist/commands/channel/index.d.ts.map +1 -0
- package/dist/commands/channel/index.js +531 -0
- package/dist/commands/channel/index.js.map +1 -0
- package/dist/commands/channel/interrupt.d.ts +10 -0
- package/dist/commands/channel/interrupt.d.ts.map +1 -0
- package/dist/commands/channel/interrupt.js +22 -0
- package/dist/commands/channel/interrupt.js.map +1 -0
- package/dist/commands/channel/kill.d.ts +7 -0
- package/dist/commands/channel/kill.d.ts.map +1 -0
- package/dist/commands/channel/kill.js +121 -0
- package/dist/commands/channel/kill.js.map +1 -0
- package/dist/commands/channel/list.d.ts +17 -0
- package/dist/commands/channel/list.d.ts.map +1 -0
- package/dist/commands/channel/list.js +233 -0
- package/dist/commands/channel/list.js.map +1 -0
- package/dist/commands/channel/messages.d.ts +15 -0
- package/dist/commands/channel/messages.d.ts.map +1 -0
- package/dist/commands/channel/messages.js +245 -0
- package/dist/commands/channel/messages.js.map +1 -0
- package/dist/commands/channel/rm.d.ts +27 -0
- package/dist/commands/channel/rm.d.ts.map +1 -0
- package/dist/commands/channel/rm.js +216 -0
- package/dist/commands/channel/rm.js.map +1 -0
- package/dist/commands/channel/run.d.ts +30 -0
- package/dist/commands/channel/run.d.ts.map +1 -0
- package/dist/commands/channel/run.js +130 -0
- package/dist/commands/channel/run.js.map +1 -0
- package/dist/commands/channel/send.d.ts +11 -0
- package/dist/commands/channel/send.d.ts.map +1 -0
- package/dist/commands/channel/send.js +24 -0
- package/dist/commands/channel/send.js.map +1 -0
- package/dist/commands/channel/spawn.d.ts +40 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -0
- package/dist/commands/channel/spawn.js +244 -0
- package/dist/commands/channel/spawn.js.map +1 -0
- package/dist/commands/channel/store/events.d.ts +39 -0
- package/dist/commands/channel/store/events.d.ts.map +1 -0
- package/dist/commands/channel/store/events.js +87 -0
- package/dist/commands/channel/store/events.js.map +1 -0
- package/dist/commands/channel/store/filter.d.ts +3 -0
- package/dist/commands/channel/store/filter.d.ts.map +1 -0
- package/dist/commands/channel/store/filter.js +2 -0
- package/dist/commands/channel/store/filter.js.map +1 -0
- package/dist/commands/channel/store/lock.d.ts +23 -0
- package/dist/commands/channel/store/lock.d.ts.map +1 -0
- package/dist/commands/channel/store/lock.js +99 -0
- package/dist/commands/channel/store/lock.js.map +1 -0
- package/dist/commands/channel/store/paths.d.ts +63 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -0
- package/dist/commands/channel/store/paths.js +246 -0
- package/dist/commands/channel/store/paths.js.map +1 -0
- package/dist/commands/channel/store/schema.d.ts +27 -0
- package/dist/commands/channel/store/schema.d.ts.map +1 -0
- package/dist/commands/channel/store/schema.js +34 -0
- package/dist/commands/channel/store/schema.js.map +1 -0
- package/dist/commands/channel/store/thread-state.d.ts +5 -0
- package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
- package/dist/commands/channel/store/thread-state.js +16 -0
- package/dist/commands/channel/store/thread-state.js.map +1 -0
- package/dist/commands/channel/store/watch.d.ts +19 -0
- package/dist/commands/channel/store/watch.d.ts.map +1 -0
- package/dist/commands/channel/store/watch.js +146 -0
- package/dist/commands/channel/store/watch.js.map +1 -0
- package/dist/commands/channel/supervisor/idle.d.ts +46 -0
- package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/idle.js +72 -0
- package/dist/commands/channel/supervisor/idle.js.map +1 -0
- package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
- package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/inbox.js +160 -0
- package/dist/commands/channel/supervisor/inbox.js.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.js +146 -0
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
- package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/stdout.js +121 -0
- package/dist/commands/channel/supervisor/stdout.js.map +1 -0
- package/dist/commands/channel/supervisor/turns.d.ts +31 -0
- package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/turns.js +45 -0
- package/dist/commands/channel/supervisor/turns.js.map +1 -0
- package/dist/commands/channel/supervisor/warning.d.ts +48 -0
- package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/warning.js +77 -0
- package/dist/commands/channel/supervisor/warning.js.map +1 -0
- package/dist/commands/channel/supervisor.d.ts +59 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -0
- package/dist/commands/channel/supervisor.js +344 -0
- package/dist/commands/channel/supervisor.js.map +1 -0
- package/dist/commands/channel/text-body.d.ts +13 -0
- package/dist/commands/channel/text-body.d.ts.map +1 -0
- package/dist/commands/channel/text-body.js +47 -0
- package/dist/commands/channel/text-body.js.map +1 -0
- package/dist/commands/channel/threads.d.ts +39 -0
- package/dist/commands/channel/threads.d.ts.map +1 -0
- package/dist/commands/channel/threads.js +106 -0
- package/dist/commands/channel/threads.js.map +1 -0
- package/dist/commands/channel/title.d.ts +12 -0
- package/dist/commands/channel/title.d.ts.map +1 -0
- package/dist/commands/channel/title.js +24 -0
- package/dist/commands/channel/title.js.map +1 -0
- package/dist/commands/channel/wait.d.ts +17 -0
- package/dist/commands/channel/wait.d.ts.map +1 -0
- package/dist/commands/channel/wait.js +75 -0
- package/dist/commands/channel/wait.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +97 -42
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts +13 -117
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +168 -1074
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +28 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +31 -111
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.d.ts +28 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +84 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/workflow.d.ts +35 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +219 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +1 -0
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +5 -3
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/shared.js +4 -4
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +8 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +3 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.5.10.json +9 -0
- package/dist/migrations/manifests/0.5.11.json +16 -0
- package/dist/migrations/manifests/0.5.12.json +9 -0
- package/dist/migrations/manifests/0.5.13.json +9 -0
- package/dist/migrations/manifests/0.5.14.json +9 -0
- package/dist/migrations/manifests/0.5.15.json +9 -0
- package/dist/migrations/manifests/0.5.16.json +9 -0
- package/dist/migrations/manifests/0.5.17.json +9 -0
- package/dist/migrations/manifests/0.5.18.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
- package/dist/templates/claude/agents/trellis-check.md +13 -7
- package/dist/templates/claude/agents/trellis-implement.md +8 -7
- package/dist/templates/claude/settings.json +4 -4
- package/dist/templates/codebuddy/agents/trellis-check.md +13 -7
- package/dist/templates/codebuddy/agents/trellis-implement.md +8 -7
- package/dist/templates/codebuddy/settings.json +4 -4
- package/dist/templates/codex/agents/trellis-check.toml +4 -4
- package/dist/templates/codex/agents/trellis-implement.toml +4 -4
- package/dist/templates/codex/config.toml +5 -3
- package/dist/templates/codex/hooks/session-start.py +205 -119
- package/dist/templates/codex/hooks.json +2 -2
- package/dist/templates/codex/skills/before-dev/SKILL.md +12 -6
- package/dist/templates/codex/skills/brainstorm/SKILL.md +69 -457
- package/dist/templates/codex/skills/check/SKILL.md +86 -18
- package/dist/templates/codex/skills/start/SKILL.md +33 -323
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +7 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +35 -6
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +5 -4
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/SKILL.md +41 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/mcp-setup.md +90 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/repository-analysis.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-task-planning.md +61 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-writing.md +70 -0
- package/dist/templates/common/commands/continue.md +6 -5
- package/dist/templates/common/commands/start.md +9 -6
- package/dist/templates/common/skills/before-dev.md +12 -6
- package/dist/templates/common/skills/brainstorm.md +68 -504
- package/dist/templates/common/skills/check.md +7 -1
- package/dist/templates/copilot/hooks/session-start.py +219 -101
- package/dist/templates/copilot/hooks.json +2 -2
- package/dist/templates/copilot/prompts/before-dev.prompt.md +12 -6
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +69 -457
- package/dist/templates/copilot/prompts/check.prompt.md +86 -18
- package/dist/templates/copilot/prompts/parallel.prompt.md +16 -8
- package/dist/templates/copilot/prompts/start.prompt.md +33 -367
- package/dist/templates/cursor/agents/trellis-check.md +13 -7
- package/dist/templates/cursor/agents/trellis-implement.md +8 -7
- package/dist/templates/cursor/hooks.json +1 -7
- package/dist/templates/droid/droids/trellis-check.md +13 -7
- package/dist/templates/droid/droids/trellis-implement.md +8 -7
- package/dist/templates/droid/settings.json +4 -4
- package/dist/templates/gemini/agents/trellis-check.md +11 -5
- package/dist/templates/gemini/agents/trellis-implement.md +7 -6
- package/dist/templates/gemini/settings.json +2 -2
- package/dist/templates/kiro/agents/trellis-check.json +1 -1
- package/dist/templates/kiro/agents/trellis-implement.json +1 -1
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +127 -9
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +171 -6
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +333 -43
- package/dist/templates/markdown/spec/guides/index.md.txt +18 -0
- package/dist/templates/opencode/agents/trellis-check.md +13 -7
- package/dist/templates/opencode/agents/trellis-implement.md +9 -8
- package/dist/templates/opencode/lib/session-utils.js +212 -123
- package/dist/templates/opencode/lib/trellis-context.js +73 -11
- package/dist/templates/opencode/plugins/inject-subagent-context.js +131 -29
- package/dist/templates/opencode/plugins/inject-workflow-state.js +9 -5
- package/dist/templates/opencode/plugins/session-start.js +9 -1
- package/dist/templates/pi/agents/trellis-check.md +5 -4
- package/dist/templates/pi/agents/trellis-implement.md +5 -4
- package/dist/templates/pi/extensions/trellis/index.ts.txt +1357 -754
- package/dist/templates/qoder/agents/trellis-check.md +11 -5
- package/dist/templates/qoder/agents/trellis-implement.md +7 -6
- package/dist/templates/qoder/settings.json +4 -4
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +0 -1
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-subagent-context.py +36 -14
- package/dist/templates/shared-hooks/inject-workflow-state.py +40 -42
- package/dist/templates/shared-hooks/session-start.py +222 -171
- package/dist/templates/trellis/config.yaml +38 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +50 -24
- package/dist/templates/trellis/scripts/common/config.py +57 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +285 -0
- package/dist/templates/trellis/scripts/common/session_context.py +384 -137
- package/dist/templates/trellis/scripts/common/task_context.py +3 -3
- package/dist/templates/trellis/scripts/common/task_store.py +161 -15
- package/dist/templates/trellis/scripts/common/workflow_phase.py +7 -10
- package/dist/templates/trellis/scripts/task.py +3 -3
- package/dist/templates/trellis/workflow.md +119 -98
- package/dist/utils/cwd-guard.d.ts +38 -0
- package/dist/utils/cwd-guard.d.ts.map +1 -0
- package/dist/utils/cwd-guard.js +62 -0
- package/dist/utils/cwd-guard.js.map +1 -0
- package/dist/utils/file-writer.d.ts +13 -0
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +59 -1
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +61 -0
- package/dist/utils/manifest-prune.d.ts.map +1 -0
- package/dist/utils/manifest-prune.js +136 -0
- package/dist/utils/manifest-prune.js.map +1 -0
- package/dist/utils/task-json.d.ts +9 -42
- package/dist/utils/task-json.d.ts.map +1 -1
- package/dist/utils/task-json.js +8 -45
- package/dist/utils/task-json.js.map +1 -1
- package/dist/utils/template-hash.d.ts +32 -6
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +53 -31
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +21 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/dist/utils/workflow-resolver.d.ts +86 -0
- package/dist/utils/workflow-resolver.d.ts.map +1 -0
- package/dist/utils/workflow-resolver.js +265 -0
- package/dist/utils/workflow-resolver.js.map +1 -0
- package/package.json +9 -8
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { delimiter, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
6
7
|
type JsonObject = Record<string, unknown>;
|
|
7
8
|
type TextContent = { type: "text"; text: string };
|
|
8
|
-
|
|
9
9
|
interface PiToolResult {
|
|
10
10
|
content: TextContent[];
|
|
11
|
-
details?:
|
|
11
|
+
details?: unknown;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
13
|
interface PiExtensionContext {
|
|
15
14
|
hasUI?: boolean;
|
|
16
15
|
sessionManager?: {
|
|
@@ -18,892 +17,1389 @@ interface PiExtensionContext {
|
|
|
18
17
|
getSessionFile?: () => string | undefined;
|
|
19
18
|
};
|
|
20
19
|
ui?: {
|
|
21
|
-
notify?: (
|
|
20
|
+
notify?: (msg: string, type?: "info" | "warning" | "error") => void;
|
|
22
21
|
};
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
interface PiBeforeAgentStartEvent {
|
|
26
|
-
systemPrompt?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface PiContextEvent {
|
|
30
|
-
messages?: unknown[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface PiToolCallEvent {
|
|
34
|
-
toolName?: string;
|
|
35
|
-
input?: JsonObject;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
23
|
interface SubagentInput {
|
|
39
24
|
agent?: string;
|
|
40
25
|
prompt?: string;
|
|
41
26
|
mode?: "single" | "parallel" | "chain";
|
|
42
27
|
prompts?: string[];
|
|
43
28
|
model?: string;
|
|
44
|
-
thinking?:
|
|
29
|
+
thinking?: string;
|
|
45
30
|
}
|
|
46
|
-
|
|
47
|
-
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
48
|
-
|
|
49
31
|
interface AgentConfig {
|
|
50
32
|
model?: string;
|
|
51
|
-
thinking?:
|
|
52
|
-
// Parsed for pi-subagents-compatible agent files; Pi CLI has no documented fallback-model flag to pass through here.
|
|
33
|
+
thinking?: string;
|
|
53
34
|
fallbackModels: string[];
|
|
54
35
|
}
|
|
55
|
-
|
|
56
|
-
interface AgentDefinition {
|
|
57
|
-
content: string;
|
|
58
|
-
config: AgentConfig;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
interface PiRunConfig {
|
|
62
37
|
model?: string;
|
|
63
|
-
thinking?:
|
|
38
|
+
thinking?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Lazy-load pi-tui (avoid failing top-level imports) ─────────────────
|
|
42
|
+
let _piTui: {
|
|
43
|
+
visibleWidth?: (s: string) => number;
|
|
44
|
+
truncateToWidth?: (s: string, w: number, ellipsis?: string) => string;
|
|
45
|
+
} | null = null;
|
|
46
|
+
function piTui() {
|
|
47
|
+
if (!_piTui) {
|
|
48
|
+
try {
|
|
49
|
+
_piTui = require("@earendil-works/pi-tui");
|
|
50
|
+
} catch {
|
|
51
|
+
_piTui = {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return _piTui;
|
|
55
|
+
}
|
|
56
|
+
function trunc(s: string, w: number) {
|
|
57
|
+
const t = piTui();
|
|
58
|
+
return t.truncateToWidth
|
|
59
|
+
? t.truncateToWidth(s, w, "…")
|
|
60
|
+
: s.length <= w
|
|
61
|
+
? s
|
|
62
|
+
: w > 1
|
|
63
|
+
? s.slice(0, w - 1) + "…"
|
|
64
|
+
: s.slice(0, w);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// ── Constants ─────────────────────────────────────────────────────────
|
|
66
68
|
const TRELLIS_AGENT_JSONL: Record<string, string> = {
|
|
67
69
|
"trellis-implement": "implement.jsonl",
|
|
68
70
|
implement: "implement.jsonl",
|
|
69
71
|
"trellis-check": "check.jsonl",
|
|
70
72
|
check: "check.jsonl",
|
|
71
73
|
};
|
|
74
|
+
const MAX_STDOUT = 8 * 1024 * 1024;
|
|
75
|
+
const MAX_STDERR = 1024 * 1024;
|
|
76
|
+
const MAX_TAIL = 256 * 1024;
|
|
77
|
+
const MAX_LINE_BUFFER = 1024 * 1024;
|
|
78
|
+
const MAX_TOOL_ARG_CHARS = 2048;
|
|
79
|
+
const MAX_TOOLS = 256;
|
|
80
|
+
const MAX_PARALLEL_PROMPTS = 6;
|
|
81
|
+
const ABORT_KILL_GRACE_MS = 1500;
|
|
82
|
+
const SESSION_OVERVIEW_TIMEOUT_MS = 1500;
|
|
83
|
+
const THROTTLE_MS = 500;
|
|
84
|
+
|
|
85
|
+
// ── State types ───────────────────────────────────────────────────────
|
|
86
|
+
type RunStatus = "pending" | "running" | "succeeded" | "failed" | "cancelled";
|
|
87
|
+
type ToolStatus = "running" | "succeeded" | "failed";
|
|
88
|
+
|
|
89
|
+
interface Usage {
|
|
90
|
+
input: number;
|
|
91
|
+
output: number;
|
|
92
|
+
cacheRead: number;
|
|
93
|
+
cacheWrite: number;
|
|
94
|
+
cost: number;
|
|
95
|
+
ctxTokens: number;
|
|
96
|
+
turns: number;
|
|
97
|
+
}
|
|
98
|
+
interface ToolTrace {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
args: string;
|
|
102
|
+
status: ToolStatus;
|
|
103
|
+
startedAt: number;
|
|
104
|
+
finishedAt?: number;
|
|
105
|
+
}
|
|
106
|
+
interface RunState {
|
|
107
|
+
id: string;
|
|
108
|
+
agent: string;
|
|
109
|
+
prompt: string;
|
|
110
|
+
step?: number;
|
|
111
|
+
status: RunStatus;
|
|
112
|
+
startedAt?: number;
|
|
113
|
+
finishedAt?: number;
|
|
114
|
+
finalText: string;
|
|
115
|
+
textTail: string;
|
|
116
|
+
thinkingTail: string;
|
|
117
|
+
stderrTail: string;
|
|
118
|
+
tools: ToolTrace[];
|
|
119
|
+
usage: Usage;
|
|
120
|
+
model?: string;
|
|
121
|
+
thinking?: string;
|
|
122
|
+
errorMessage?: string;
|
|
123
|
+
}
|
|
124
|
+
interface ProgressDetails {
|
|
125
|
+
kind: "trellis-subagent-progress";
|
|
126
|
+
agent: string;
|
|
127
|
+
mode: "single" | "parallel" | "chain";
|
|
128
|
+
startedAt: number;
|
|
129
|
+
updatedAt: number;
|
|
130
|
+
final: boolean;
|
|
131
|
+
runs: RunState[];
|
|
132
|
+
}
|
|
72
133
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
// ── Native partial-update card state ──────────────────────────────────
|
|
135
|
+
interface NativeCardHandle {
|
|
136
|
+
state: JsonObject;
|
|
137
|
+
invalidate: () => void;
|
|
138
|
+
updatedAt: number;
|
|
139
|
+
}
|
|
140
|
+
const MAX_NATIVE_CARDS = 20;
|
|
141
|
+
const nativeCards = new Map<string, NativeCardHandle>();
|
|
142
|
+
let activeSubagentToolCallId: string | null = null;
|
|
143
|
+
function rememberNativeCard(id: string, card: NativeCardHandle) {
|
|
144
|
+
nativeCards.set(id, card);
|
|
145
|
+
const active = activeSubagentToolCallId
|
|
146
|
+
? nativeCards.get(activeSubagentToolCallId)
|
|
147
|
+
: undefined;
|
|
148
|
+
if (!active || card.updatedAt >= active.updatedAt)
|
|
149
|
+
activeSubagentToolCallId = id;
|
|
150
|
+
for (const key of nativeCards.keys()) {
|
|
151
|
+
if (nativeCards.size <= MAX_NATIVE_CARDS) break;
|
|
152
|
+
if (key !== activeSubagentToolCallId) nativeCards.delete(key);
|
|
85
153
|
}
|
|
86
154
|
}
|
|
87
|
-
|
|
88
|
-
|
|
155
|
+
function totalUsage(d: ProgressDetails): Usage {
|
|
156
|
+
const u: Usage = {
|
|
157
|
+
input: 0,
|
|
158
|
+
output: 0,
|
|
159
|
+
cacheRead: 0,
|
|
160
|
+
cacheWrite: 0,
|
|
161
|
+
cost: 0,
|
|
162
|
+
ctxTokens: 0,
|
|
163
|
+
turns: 0,
|
|
164
|
+
};
|
|
165
|
+
for (const r of d.runs) {
|
|
166
|
+
u.input += r.usage.input;
|
|
167
|
+
u.output += r.usage.output;
|
|
168
|
+
u.cacheRead += r.usage.cacheRead;
|
|
169
|
+
u.cacheWrite += r.usage.cacheWrite;
|
|
170
|
+
u.cost += r.usage.cost;
|
|
171
|
+
u.ctxTokens = Math.max(u.ctxTokens, r.usage.ctxTokens);
|
|
172
|
+
u.turns += r.usage.turns;
|
|
173
|
+
}
|
|
174
|
+
return u;
|
|
175
|
+
}
|
|
176
|
+
function activeRun(d: ProgressDetails) {
|
|
177
|
+
return d.runs.find((r) => r.status === "running") ?? d.runs.at(-1);
|
|
178
|
+
}
|
|
179
|
+
function toolArgs(t: ToolTrace) {
|
|
89
180
|
try {
|
|
90
|
-
return
|
|
181
|
+
return JSON.parse(t.args) as Record<string, unknown>;
|
|
91
182
|
} catch {
|
|
92
|
-
return
|
|
183
|
+
return {};
|
|
93
184
|
}
|
|
94
185
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
body: string;
|
|
99
|
-
} {
|
|
100
|
-
const normalized = content.replace(/^\uFEFF/, "");
|
|
101
|
-
const match = normalized.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
102
|
-
return match
|
|
103
|
-
? { frontmatter: match[1] ?? "", body: normalized.slice(match[0].length) }
|
|
104
|
-
: { frontmatter: "", body: normalized };
|
|
186
|
+
function bashCommand(t: ToolTrace) {
|
|
187
|
+
const a = toolArgs(t);
|
|
188
|
+
return String(a.command || "").toLowerCase();
|
|
105
189
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return splitMarkdownFrontmatter(content).body.trimStart();
|
|
190
|
+
function isSearchTool(t: ToolTrace) {
|
|
191
|
+
return t.name === "read" || t.name === "grep" || t.name === "find";
|
|
109
192
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
193
|
+
function isMutationTool(t: ToolTrace) {
|
|
194
|
+
return t.name === "edit" || t.name === "write";
|
|
113
195
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
196
|
+
function isValidationCommand(t: ToolTrace) {
|
|
197
|
+
const c = bashCommand(t);
|
|
198
|
+
return /\b(test|typecheck|lint|build|gofmt|go test|npm run|pnpm|vitest|tsc)\b/.test(
|
|
199
|
+
c,
|
|
200
|
+
);
|
|
117
201
|
}
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
"minimal",
|
|
122
|
-
"low",
|
|
123
|
-
"medium",
|
|
124
|
-
"high",
|
|
125
|
-
"xhigh",
|
|
126
|
-
] as const satisfies readonly ThinkingLevel[];
|
|
127
|
-
const THINKING_SUFFIX_RE = /:(?:off|minimal|low|medium|high|xhigh)$/i;
|
|
128
|
-
|
|
129
|
-
function normalizeThinking(value: unknown): ThinkingLevel | undefined {
|
|
130
|
-
const raw = stringValue(value)?.toLowerCase();
|
|
131
|
-
if (!raw) return undefined;
|
|
132
|
-
return THINKING_LEVELS.includes(raw as ThinkingLevel)
|
|
133
|
-
? (raw as ThinkingLevel)
|
|
134
|
-
: undefined;
|
|
202
|
+
function isInspectionCommand(t: ToolTrace) {
|
|
203
|
+
const c = bashCommand(t);
|
|
204
|
+
return /\b(rg|grep|find|git diff|git status|ls|tree)\b/.test(c);
|
|
135
205
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
return
|
|
206
|
+
function thinkingIntent(text: string) {
|
|
207
|
+
const s = text.toLowerCase();
|
|
208
|
+
if (/error|failed|failure|panic|exception|报错|失败|错误|异常/.test(s))
|
|
209
|
+
return "Analyzing failure cause";
|
|
210
|
+
if (/test|verify|check|typecheck|lint|验证|测试|检查/.test(s))
|
|
211
|
+
return "Planning verification steps";
|
|
212
|
+
if (/plan|approach|design|strategy|方案|计划|思路|设计/.test(s))
|
|
213
|
+
return "Structuring the implementation approach";
|
|
214
|
+
if (/implement|change|edit|modify|refactor|实现|修改|重构/.test(s))
|
|
215
|
+
return "Reasoning through code changes";
|
|
216
|
+
if (/inspect|search|locate|read|context|定位|搜索|阅读|上下文/.test(s))
|
|
217
|
+
return "Locating relevant context";
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
function behaviorSummary(r: RunState) {
|
|
221
|
+
if (r.status === "succeeded") return "Task completed and result returned";
|
|
222
|
+
if (r.status === "failed")
|
|
223
|
+
return "Task failed and error details were retained";
|
|
224
|
+
|
|
225
|
+
const runningTool = r.tools.findLast((t) => t.status === "running");
|
|
226
|
+
if (runningTool) {
|
|
227
|
+
if (isMutationTool(runningTool)) return "Applying the plan to code";
|
|
228
|
+
if (runningTool.name === "bash" && isValidationCommand(runningTool))
|
|
229
|
+
return "Verifying whether the implementation passes";
|
|
230
|
+
if (runningTool.name === "bash" && isInspectionCommand(runningTool))
|
|
231
|
+
return "Inspecting current code state";
|
|
232
|
+
if (isSearchTool(runningTool)) return "Locating relevant code and context";
|
|
233
|
+
if (runningTool.name === "bash")
|
|
234
|
+
return "Validating assumptions with commands";
|
|
235
|
+
return "Using tools to advance the task";
|
|
148
236
|
}
|
|
237
|
+
|
|
238
|
+
const recent = r.tools.slice(-5);
|
|
239
|
+
if (recent.some((t) => t.status === "failed"))
|
|
240
|
+
return "Investigating tool or command failure";
|
|
241
|
+
if (recent.some(isMutationTool)) return "Reviewing recent changes";
|
|
242
|
+
if (recent.some((t) => t.name === "bash" && isValidationCommand(t)))
|
|
243
|
+
return "Analyzing verification results";
|
|
149
244
|
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
245
|
+
recent.length >= 2 &&
|
|
246
|
+
recent.every(
|
|
247
|
+
(t) => isSearchTool(t) || (t.name === "bash" && isInspectionCommand(t)),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return "Mapping code structure and impact";
|
|
251
|
+
|
|
252
|
+
const intent = thinkingIntent(`${r.thinkingTail}\n${r.textTail}`);
|
|
253
|
+
if (intent) return intent;
|
|
254
|
+
if (!r.tools.length) return "Understanding the task and planning execution";
|
|
255
|
+
return "Advancing the task and preparing next steps";
|
|
156
256
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
?
|
|
164
|
-
:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.map((item) => parseFrontmatterScalar(item))
|
|
168
|
-
.filter((item): item is string => !!item);
|
|
257
|
+
function progressState(d: ProgressDetails) {
|
|
258
|
+
const running = d.runs.filter((r) => r.status === "running").length;
|
|
259
|
+
const failed = d.runs.some((r) => r.status === "failed");
|
|
260
|
+
return failed
|
|
261
|
+
? "failed"
|
|
262
|
+
: d.final
|
|
263
|
+
? "completed"
|
|
264
|
+
: running
|
|
265
|
+
? `${running} running`
|
|
266
|
+
: "pending";
|
|
169
267
|
}
|
|
170
|
-
|
|
171
|
-
|
|
268
|
+
function progressDone(d: ProgressDetails) {
|
|
269
|
+
return d.runs.filter((r) => r.status !== "pending" && r.status !== "running")
|
|
270
|
+
.length;
|
|
271
|
+
}
|
|
272
|
+
function summaryText(text: string) {
|
|
273
|
+
return `${text.trim().replace(/[。.!?…]+$/u, "")}...`;
|
|
274
|
+
}
|
|
275
|
+
function splitModelThinking(model?: string, fallbackThinking?: string) {
|
|
276
|
+
const m = model?.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
|
|
277
|
+
return {
|
|
278
|
+
model: m ? m[1] : model,
|
|
279
|
+
thinking: (m?.[2] ?? fallbackThinking)?.toLowerCase(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function modelLabel(r: RunState) {
|
|
283
|
+
const { model, thinking } = splitModelThinking(r.model, r.thinking);
|
|
284
|
+
if (!model) return undefined;
|
|
285
|
+
return thinking && thinking !== "off" ? `${model}(${thinking})` : model;
|
|
286
|
+
}
|
|
287
|
+
function applyRunConfig(r: RunState, cfg: PiRunConfig) {
|
|
288
|
+
const parsed = splitModelThinking(cfg.model, cfg.thinking);
|
|
289
|
+
r.model = parsed.model;
|
|
290
|
+
r.thinking = parsed.thinking;
|
|
291
|
+
}
|
|
292
|
+
function runElapsed(d: ProgressDetails, r: RunState) {
|
|
293
|
+
const start = r.startedAt ?? d.startedAt;
|
|
294
|
+
const end =
|
|
295
|
+
r.finishedAt ?? (r.status === "running" ? Date.now() : d.updatedAt);
|
|
296
|
+
return fmtDur(Math.max(0, end - start));
|
|
297
|
+
}
|
|
298
|
+
function runHeader(d: ProgressDetails, r: RunState) {
|
|
299
|
+
const usage = fmtUsage(r.usage, modelLabel(r)) || fmtUsage(totalUsage(d));
|
|
300
|
+
return `${r.agent} · ${progressDone(d)}/${d.runs.length} done · ${progressState(d)} · ${runElapsed(d, r)}${usage ? ` · ${usage}` : ""}`;
|
|
301
|
+
}
|
|
302
|
+
function renderRunBlock(
|
|
172
303
|
lines: string[],
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
304
|
+
d: ProgressDetails,
|
|
305
|
+
run: RunState,
|
|
306
|
+
expanded: boolean,
|
|
307
|
+
) {
|
|
308
|
+
const step = run.step ? `step ${run.step} · ` : "";
|
|
309
|
+
lines.push(` - ${step}${runHeader(d, run)}`);
|
|
310
|
+
const summary = behaviorSummary(run);
|
|
311
|
+
if (summary) lines.push(` › ${summaryText(summary)}`);
|
|
312
|
+
const visibleTools = expanded ? run.tools.slice(-8) : run.tools.slice(-1);
|
|
313
|
+
for (const t of visibleTools)
|
|
314
|
+
lines.push(` ${toolIcon(t.status)} ${toolBrief(t)}`);
|
|
315
|
+
if (expanded && run.errorMessage) {
|
|
316
|
+
lines.push(` ✗ ${oneLine(run.errorMessage, 120)}`);
|
|
186
317
|
}
|
|
187
|
-
return { values, nextIndex: index - 1 };
|
|
188
318
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
index = result.nextIndex;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
319
|
+
function renderProgressCard(
|
|
320
|
+
d: ProgressDetails,
|
|
321
|
+
expanded: boolean,
|
|
322
|
+
w: number,
|
|
323
|
+
): string[] {
|
|
324
|
+
const r = activeRun(d);
|
|
325
|
+
if (!r) return [];
|
|
326
|
+
const spinner = ["◐", "◓", "◑", "◒"][Math.floor(Date.now() / 250) % 4]!;
|
|
327
|
+
const icon = d.final
|
|
328
|
+
? d.runs.some((x) => x.status === "failed")
|
|
329
|
+
? "✗"
|
|
330
|
+
: "✓"
|
|
331
|
+
: spinner;
|
|
332
|
+
const totalElapsed = fmtDur(
|
|
333
|
+
(d.final ? d.updatedAt : Date.now()) - d.startedAt,
|
|
334
|
+
);
|
|
335
|
+
const lines: string[] = [
|
|
336
|
+
`${icon} subagent ${d.mode} · total ${totalElapsed}`,
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
if (!expanded) {
|
|
340
|
+
renderRunBlock(lines, d, r, false);
|
|
341
|
+
lines.push(" Alt+O expand latest subagent card");
|
|
342
|
+
return lines.map((l) => trunc(l, w));
|
|
216
343
|
}
|
|
217
344
|
|
|
218
|
-
|
|
345
|
+
for (const run of d.runs) renderRunBlock(lines, d, run, true);
|
|
346
|
+
lines.push(" Alt+O collapse latest subagent card");
|
|
347
|
+
const max = 48;
|
|
348
|
+
const shown =
|
|
349
|
+
lines.length > max
|
|
350
|
+
? [
|
|
351
|
+
...lines.slice(0, max - 1),
|
|
352
|
+
` … ${lines.length - max + 1} lines hidden`,
|
|
353
|
+
]
|
|
354
|
+
: lines;
|
|
355
|
+
return shown.map((l) => trunc(l, w));
|
|
219
356
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
357
|
+
function progressKey(d: ProgressDetails) {
|
|
358
|
+
return d.runs
|
|
359
|
+
.map((r) => {
|
|
360
|
+
const t = r.tools.at(-1);
|
|
361
|
+
return [
|
|
362
|
+
r.id,
|
|
363
|
+
r.status,
|
|
364
|
+
r.tools.length,
|
|
365
|
+
t?.id ?? "",
|
|
366
|
+
t?.status ?? "",
|
|
367
|
+
r.usage.turns,
|
|
368
|
+
r.usage.input,
|
|
369
|
+
r.usage.output,
|
|
370
|
+
r.usage.cacheRead,
|
|
371
|
+
r.usage.cacheWrite,
|
|
372
|
+
r.usage.ctxTokens,
|
|
373
|
+
r.model ?? "",
|
|
374
|
+
r.thinking ?? "",
|
|
375
|
+
r.errorMessage ?? "",
|
|
376
|
+
].join("~");
|
|
377
|
+
})
|
|
378
|
+
.join("|");
|
|
223
379
|
}
|
|
224
380
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (model) {
|
|
229
|
-
return [
|
|
230
|
-
"--model",
|
|
231
|
-
thinking && !modelHasThinkingSuffix(model)
|
|
232
|
-
? `${model}:${thinking}`
|
|
233
|
-
: model,
|
|
234
|
-
];
|
|
235
|
-
}
|
|
236
|
-
return thinking ? ["--thinking", thinking] : [];
|
|
381
|
+
// ── Utilities ─────────────────────────────────────────────────────────
|
|
382
|
+
function isObj(v: unknown): v is JsonObject {
|
|
383
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
237
384
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
input: SubagentInput,
|
|
241
|
-
agentConfig: AgentConfig,
|
|
242
|
-
): PiRunConfig {
|
|
243
|
-
return {
|
|
244
|
-
model: stringValue(input.model) ?? agentConfig.model,
|
|
245
|
-
thinking: normalizeThinking(input.thinking) ?? agentConfig.thinking,
|
|
246
|
-
};
|
|
385
|
+
function str(v: unknown): string | null {
|
|
386
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
247
387
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return raw
|
|
251
|
-
.trim()
|
|
252
|
-
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
253
|
-
.replace(/^[._-]+|[._-]+$/g, "")
|
|
254
|
-
.slice(0, 160);
|
|
388
|
+
function num(v: unknown): number {
|
|
389
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
255
390
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
391
|
+
function hash(s: string) {
|
|
392
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 24);
|
|
259
393
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const PI_CLI_JS_SEGMENTS = [
|
|
267
|
-
"node_modules",
|
|
268
|
-
"@mariozechner",
|
|
269
|
-
"pi-coding-agent",
|
|
270
|
-
"dist",
|
|
271
|
-
"cli.js",
|
|
272
|
-
];
|
|
273
|
-
const MAX_SUBAGENT_STDOUT_BYTES = 8 * 1024 * 1024;
|
|
274
|
-
const MAX_SUBAGENT_STDERR_BYTES = 1024 * 1024;
|
|
275
|
-
|
|
276
|
-
// Nested agents can emit unbounded output; keep the tail so diagnostics survive without growing memory indefinitely.
|
|
277
|
-
class BoundedBufferCollector {
|
|
278
|
-
private chunks: Buffer[] = [];
|
|
279
|
-
private length = 0;
|
|
280
|
-
private truncatedBytes = 0;
|
|
281
|
-
|
|
282
|
-
constructor(private readonly maxBytes: number) {}
|
|
283
|
-
|
|
284
|
-
append(chunk: Buffer): void {
|
|
285
|
-
const data = chunk;
|
|
286
|
-
if (data.length >= this.maxBytes) {
|
|
287
|
-
this.truncatedBytes += this.length + data.length - this.maxBytes;
|
|
288
|
-
this.chunks = [data.subarray(data.length - this.maxBytes)];
|
|
289
|
-
this.length = this.maxBytes;
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
this.chunks.push(data);
|
|
294
|
-
this.length += data.length;
|
|
295
|
-
|
|
296
|
-
while (this.length > this.maxBytes) {
|
|
297
|
-
const first = this.chunks[0];
|
|
298
|
-
if (!first) break;
|
|
299
|
-
const overflow = this.length - this.maxBytes;
|
|
300
|
-
if (first.length <= overflow) {
|
|
301
|
-
this.chunks.shift();
|
|
302
|
-
this.length -= first.length;
|
|
303
|
-
this.truncatedBytes += first.length;
|
|
304
|
-
} else {
|
|
305
|
-
this.chunks[0] = first.subarray(overflow);
|
|
306
|
-
this.length -= overflow;
|
|
307
|
-
this.truncatedBytes += overflow;
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
toString(): string {
|
|
314
|
-
const body = Buffer.concat(this.chunks, this.length).toString("utf-8");
|
|
315
|
-
return this.truncatedBytes
|
|
316
|
-
? `[${this.truncatedBytes} bytes truncated]\n${body}`
|
|
317
|
-
: body;
|
|
394
|
+
function readText(p: string) {
|
|
395
|
+
try {
|
|
396
|
+
return readFileSync(p, "utf-8");
|
|
397
|
+
} catch {
|
|
398
|
+
return "";
|
|
318
399
|
}
|
|
319
400
|
}
|
|
320
|
-
|
|
321
|
-
function isExistingFile(path: string): boolean {
|
|
401
|
+
function exists(p: string) {
|
|
322
402
|
try {
|
|
323
|
-
return statSync(
|
|
403
|
+
return statSync(p).isFile();
|
|
324
404
|
} catch {
|
|
325
405
|
return false;
|
|
326
406
|
}
|
|
327
407
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const seen = new Set<string>();
|
|
331
|
-
const unique: string[] = [];
|
|
332
|
-
for (const value of values) {
|
|
333
|
-
if (!value || seen.has(value)) continue;
|
|
334
|
-
seen.add(value);
|
|
335
|
-
unique.push(value);
|
|
336
|
-
}
|
|
337
|
-
return unique;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function candidatePiCliJsPaths(): string[] {
|
|
341
|
-
const candidates: string[] = [];
|
|
342
|
-
|
|
343
|
-
for (const arg of process.argv) {
|
|
344
|
-
if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg)) {
|
|
345
|
-
candidates.push(resolve(arg));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const npmPrefix =
|
|
350
|
-
stringValue(process.env.npm_config_prefix) ??
|
|
351
|
-
stringValue(process.env.NPM_CONFIG_PREFIX);
|
|
352
|
-
if (npmPrefix) {
|
|
353
|
-
candidates.push(join(npmPrefix, ...PI_CLI_JS_SEGMENTS));
|
|
354
|
-
candidates.push(join(npmPrefix, "lib", ...PI_CLI_JS_SEGMENTS));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const appData = stringValue(process.env.APPDATA);
|
|
358
|
-
if (appData) {
|
|
359
|
-
candidates.push(join(appData, "npm", ...PI_CLI_JS_SEGMENTS));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const pathValue = process.env.PATH ?? process.env.Path ?? "";
|
|
363
|
-
for (const pathEntry of pathValue.split(delimiter)) {
|
|
364
|
-
const entry = pathEntry.trim();
|
|
365
|
-
if (!entry) continue;
|
|
366
|
-
candidates.push(join(entry, ...PI_CLI_JS_SEGMENTS));
|
|
367
|
-
candidates.push(join(dirname(entry), ...PI_CLI_JS_SEGMENTS));
|
|
368
|
-
candidates.push(join(dirname(entry), "lib", ...PI_CLI_JS_SEGMENTS));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return uniqueStrings(candidates);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function resolvePiInvocation(): PiInvocation {
|
|
375
|
-
const envCli = stringValue(process.env.TRELLIS_PI_CLI_JS);
|
|
376
|
-
if (envCli) {
|
|
377
|
-
const cliJs = resolve(envCli);
|
|
378
|
-
if (!isExistingFile(cliJs)) {
|
|
379
|
-
throw new Error(`TRELLIS_PI_CLI_JS points to a missing file: ${cliJs}`);
|
|
380
|
-
}
|
|
381
|
-
return { command: process.execPath, argsPrefix: [cliJs] };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const cliJs of candidatePiCliJsPaths()) {
|
|
385
|
-
if (isExistingFile(cliJs)) {
|
|
386
|
-
return { command: process.execPath, argsPrefix: [cliJs] };
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return { command: "pi", argsPrefix: [] };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function createProcessContextKey(projectRoot: string): string {
|
|
394
|
-
return `pi_process_${hashValue(
|
|
395
|
-
[projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(
|
|
396
|
-
":",
|
|
397
|
-
),
|
|
398
|
-
)}`;
|
|
408
|
+
function shellQuote(v: string) {
|
|
409
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
399
410
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
callback: (() => string | undefined) | undefined,
|
|
403
|
-
): string | null {
|
|
404
|
-
if (!callback) return null;
|
|
411
|
+
function callStr(cb: (() => string | undefined) | undefined): string | null {
|
|
412
|
+
if (!cb) return null;
|
|
405
413
|
try {
|
|
406
|
-
return
|
|
414
|
+
return str(cb());
|
|
407
415
|
} catch {
|
|
408
416
|
return null;
|
|
409
417
|
}
|
|
410
418
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (value) return value;
|
|
419
|
+
function lookupStr(data: unknown, keys: string[]): string | null {
|
|
420
|
+
if (!isObj(data)) return null;
|
|
421
|
+
for (const k of keys) {
|
|
422
|
+
const v = str(data[k]);
|
|
423
|
+
if (v) return v;
|
|
417
424
|
}
|
|
418
|
-
for (const
|
|
425
|
+
for (const nk of [
|
|
419
426
|
"input",
|
|
420
427
|
"properties",
|
|
421
428
|
"event",
|
|
422
429
|
"hook_input",
|
|
423
430
|
"hookInput",
|
|
424
431
|
]) {
|
|
425
|
-
const nested = data[
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
432
|
+
const nested = data[nk];
|
|
433
|
+
const v = lookupStr(nested, keys);
|
|
434
|
+
if (v) return v;
|
|
428
435
|
}
|
|
429
436
|
return null;
|
|
430
437
|
}
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
function cmdHasTrellisCtx(cmd: string) {
|
|
439
|
+
const t = cmd.trimStart();
|
|
440
|
+
return (
|
|
441
|
+
/^export\s+TRELLIS_CONTEXT_ID=/.test(t) ||
|
|
442
|
+
/^TRELLIS_CONTEXT_ID=/.test(t) ||
|
|
443
|
+
/^env\s+.*TRELLIS_CONTEXT_ID=/.test(t)
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function fmtDur(ms: number) {
|
|
447
|
+
if (ms < 1000) return `${ms}ms`;
|
|
448
|
+
const s = Math.floor(ms / 1000);
|
|
449
|
+
if (s < 60) return `${s}s`;
|
|
450
|
+
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
451
|
+
}
|
|
452
|
+
function fmtNum(n: number) {
|
|
453
|
+
if (!n) return "0";
|
|
454
|
+
if (Math.abs(n) < 1000) return `${n}`;
|
|
455
|
+
if (Math.abs(n) < 1000000) return `${(n / 1000).toFixed(1)}k`;
|
|
456
|
+
return `${(n / 1000000).toFixed(1)}m`;
|
|
457
|
+
}
|
|
458
|
+
function fmtUsage(u: Usage, m?: string) {
|
|
459
|
+
const p: string[] = [];
|
|
460
|
+
if (u.turns) p.push(`${u.turns}t`);
|
|
461
|
+
if (u.input) p.push(`↑${fmtNum(u.input)}`);
|
|
462
|
+
if (u.output) p.push(`↓${fmtNum(u.output)}`);
|
|
463
|
+
if (u.cost) p.push(`$${u.cost.toFixed(3)}`);
|
|
464
|
+
if (u.ctxTokens) p.push(`ctx:${fmtNum(u.ctxTokens)}`);
|
|
465
|
+
if (m) p.push(m);
|
|
466
|
+
return p.join(" ");
|
|
467
|
+
}
|
|
468
|
+
function statusIcon(s: RunStatus) {
|
|
469
|
+
return s === "pending"
|
|
470
|
+
? "○"
|
|
471
|
+
: s === "running"
|
|
472
|
+
? "●"
|
|
473
|
+
: s === "succeeded"
|
|
474
|
+
? "✓"
|
|
475
|
+
: s === "failed"
|
|
476
|
+
? "✗"
|
|
477
|
+
: "⊘";
|
|
478
|
+
}
|
|
479
|
+
function toolIcon(s: ToolStatus) {
|
|
480
|
+
return s === "running" ? "•" : s === "succeeded" ? "✓" : "✗";
|
|
481
|
+
}
|
|
482
|
+
function latest(text: string, n: number) {
|
|
483
|
+
return text
|
|
484
|
+
.split(/\r?\n/)
|
|
485
|
+
.map((l) => l.trimEnd())
|
|
486
|
+
.filter((l) => l.trim())
|
|
487
|
+
.slice(-n);
|
|
488
|
+
}
|
|
489
|
+
function appendTail(cur: string, next: string, max: number) {
|
|
490
|
+
if (!next) return cur;
|
|
491
|
+
const c = cur + next;
|
|
492
|
+
return c.length <= max ? c : c.slice(-max);
|
|
493
|
+
}
|
|
494
|
+
function extractText(content: unknown): string {
|
|
433
495
|
if (typeof content === "string") return content;
|
|
434
496
|
if (!Array.isArray(content)) return "";
|
|
435
|
-
|
|
436
497
|
return content
|
|
437
|
-
.map((
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
? block.text
|
|
441
|
-
: "";
|
|
442
|
-
})
|
|
498
|
+
.map((b) =>
|
|
499
|
+
isObj(b) && b.type === "text" && typeof b.text === "string" ? b.text : "",
|
|
500
|
+
)
|
|
443
501
|
.join("");
|
|
444
502
|
}
|
|
503
|
+
function extractThinking(content: unknown): string {
|
|
504
|
+
if (!Array.isArray(content)) return "";
|
|
505
|
+
return content
|
|
506
|
+
.map((b) =>
|
|
507
|
+
isObj(b) && b.type === "thinking" && typeof b.thinking === "string"
|
|
508
|
+
? b.thinking
|
|
509
|
+
: "",
|
|
510
|
+
)
|
|
511
|
+
.join("\n");
|
|
512
|
+
}
|
|
513
|
+
function newUsage(): Usage {
|
|
514
|
+
return {
|
|
515
|
+
input: 0,
|
|
516
|
+
output: 0,
|
|
517
|
+
cacheRead: 0,
|
|
518
|
+
cacheWrite: 0,
|
|
519
|
+
cost: 0,
|
|
520
|
+
ctxTokens: 0,
|
|
521
|
+
turns: 0,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function newRun(
|
|
525
|
+
id: string,
|
|
526
|
+
agent: string,
|
|
527
|
+
prompt: string,
|
|
528
|
+
step?: number,
|
|
529
|
+
): RunState {
|
|
530
|
+
return {
|
|
531
|
+
id,
|
|
532
|
+
agent,
|
|
533
|
+
prompt: trunc(prompt.replace(/\s+/g, " ").trim(), 120) || "(empty)",
|
|
534
|
+
step,
|
|
535
|
+
status: "pending",
|
|
536
|
+
finalText: "",
|
|
537
|
+
textTail: "",
|
|
538
|
+
thinkingTail: "",
|
|
539
|
+
stderrTail: "",
|
|
540
|
+
tools: [],
|
|
541
|
+
usage: newUsage(),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function cloneProgress(d: ProgressDetails): ProgressDetails {
|
|
545
|
+
return {
|
|
546
|
+
...d,
|
|
547
|
+
runs: d.runs.map((r) => ({
|
|
548
|
+
...r,
|
|
549
|
+
tools: r.tools.map((t) => ({ ...t })),
|
|
550
|
+
usage: { ...r.usage },
|
|
551
|
+
})),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
445
554
|
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
555
|
+
function oneLine(v: unknown, max = 80) {
|
|
556
|
+
return String(v || "...")
|
|
557
|
+
.replace(/\s+/g, " ")
|
|
558
|
+
.trim()
|
|
559
|
+
.slice(0, max);
|
|
560
|
+
}
|
|
561
|
+
function summarizeToolArgs(name: string, args: unknown): string {
|
|
562
|
+
const a = isObj(args) ? args : {};
|
|
563
|
+
const summary: JsonObject = {};
|
|
564
|
+
if ("path" in a) summary.path = oneLine(a.path, 240);
|
|
565
|
+
if ("file_path" in a) summary.file_path = oneLine(a.file_path, 240);
|
|
566
|
+
if ("command" in a) summary.command = oneLine(a.command, 240);
|
|
567
|
+
if ("pattern" in a) summary.pattern = oneLine(a.pattern, 120);
|
|
568
|
+
if ("limit" in a) summary.limit = a.limit;
|
|
569
|
+
if ("offset" in a) summary.offset = a.offset;
|
|
570
|
+
if (name === "edit" && Array.isArray(a.edits))
|
|
571
|
+
summary.edits = `${a.edits.length} edit(s)`;
|
|
572
|
+
if (name === "write" && "content" in a)
|
|
573
|
+
summary.content = `<${String(a.content ?? "").length} chars>`;
|
|
574
|
+
const json = JSON.stringify(
|
|
575
|
+
Object.keys(summary).length ? summary : { tool: name },
|
|
576
|
+
);
|
|
577
|
+
return json.length <= MAX_TOOL_ARG_CHARS
|
|
578
|
+
? json
|
|
579
|
+
: json.slice(0, MAX_TOOL_ARG_CHARS);
|
|
580
|
+
}
|
|
581
|
+
function toolBrief(t: ToolTrace): string {
|
|
582
|
+
const a = toolArgs(t);
|
|
583
|
+
if (t.name === "read") return `read: ${oneLine(a.path || a.file_path, 80)}`;
|
|
584
|
+
if (t.name === "bash") return `bash: ${oneLine(a.command, 60)}`;
|
|
585
|
+
if (t.name === "write") return `write: ${oneLine(a.path || a.file_path, 80)}`;
|
|
586
|
+
if (t.name === "edit") return `edit: ${oneLine(a.path || a.file_path, 80)}`;
|
|
587
|
+
if (t.name === "grep") return `grep: ${oneLine(a.pattern, 50)}`;
|
|
588
|
+
if (t.name === "find") return `find: ${oneLine(a.pattern || "*", 50)}`;
|
|
589
|
+
return oneLine(t.name, 50);
|
|
590
|
+
}
|
|
452
591
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
592
|
+
// ── Pi CLI path resolution ────────────────────────────────────────────
|
|
593
|
+
const PI_CLI_SEGMENTS = [
|
|
594
|
+
["node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"],
|
|
595
|
+
["node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"],
|
|
596
|
+
];
|
|
457
597
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
598
|
+
function resolvePiCli(): { command: string; args: string[] } {
|
|
599
|
+
const envCli = str(process.env.TRELLIS_PI_CLI_JS);
|
|
600
|
+
if (envCli) {
|
|
601
|
+
const p = resolve(envCli);
|
|
602
|
+
if (!exists(p)) throw new Error(`TRELLIS_PI_CLI_JS missing: ${p}`);
|
|
603
|
+
return { command: process.execPath, args: [p] };
|
|
604
|
+
}
|
|
605
|
+
const candidates: string[] = [];
|
|
606
|
+
for (const arg of process.argv)
|
|
607
|
+
if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg))
|
|
608
|
+
candidates.push(resolve(arg));
|
|
609
|
+
const prefix =
|
|
610
|
+
str(process.env.npm_config_prefix) ?? str(process.env.NPM_CONFIG_PREFIX);
|
|
611
|
+
const appData = str(process.env.APPDATA);
|
|
612
|
+
const pathVal = process.env.PATH ?? process.env.Path ?? "";
|
|
613
|
+
const addBase = (base: string) => {
|
|
614
|
+
for (const seg of PI_CLI_SEGMENTS) candidates.push(join(base, ...seg));
|
|
615
|
+
};
|
|
616
|
+
if (prefix) {
|
|
617
|
+
addBase(prefix);
|
|
618
|
+
addBase(join(prefix, "lib"));
|
|
619
|
+
}
|
|
620
|
+
if (appData) addBase(join(appData, "npm"));
|
|
621
|
+
for (const entry of pathVal.split(delimiter)) {
|
|
622
|
+
const e = entry.trim();
|
|
623
|
+
if (!e) continue;
|
|
624
|
+
addBase(e);
|
|
625
|
+
addBase(dirname(e));
|
|
626
|
+
addBase(join(dirname(e), "lib"));
|
|
463
627
|
}
|
|
628
|
+
for (const c of [...new Set(candidates)])
|
|
629
|
+
if (exists(c)) return { command: process.execPath, args: [c] };
|
|
630
|
+
return { command: "pi", args: [] };
|
|
631
|
+
}
|
|
464
632
|
|
|
465
|
-
|
|
633
|
+
function resolveRunCfg(
|
|
634
|
+
input: SubagentInput,
|
|
635
|
+
agentCfg: AgentConfig,
|
|
636
|
+
inheritedThinking?: string,
|
|
637
|
+
): PiRunConfig {
|
|
638
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
639
|
+
const normalize = (v: unknown): string | undefined => {
|
|
640
|
+
const s = typeof v === "string" && v.trim() ? v.trim().toLowerCase() : "";
|
|
641
|
+
return THINKING_LEVELS.includes(s) ? s : undefined;
|
|
642
|
+
};
|
|
643
|
+
const suffixRe = /:(off|minimal|low|medium|high|xhigh)$/i;
|
|
644
|
+
const inputModel = str(input.model);
|
|
645
|
+
const agentModel = agentCfg.model;
|
|
646
|
+
const rawModel = inputModel ?? agentModel;
|
|
647
|
+
const inputSuffixThinking = normalize(inputModel?.match(suffixRe)?.[1]);
|
|
648
|
+
const agentSuffixThinking = normalize(agentModel?.match(suffixRe)?.[1]);
|
|
649
|
+
const baseModel = rawModel?.replace(suffixRe, "");
|
|
650
|
+
const thinking =
|
|
651
|
+
normalize(input.thinking) ??
|
|
652
|
+
inputSuffixThinking ??
|
|
653
|
+
normalize(agentCfg.thinking) ??
|
|
654
|
+
agentSuffixThinking ??
|
|
655
|
+
normalize(inheritedThinking);
|
|
656
|
+
if (baseModel && thinking && thinking !== "off")
|
|
657
|
+
return { model: `${baseModel}:${thinking}`, thinking };
|
|
658
|
+
return { model: baseModel || rawModel, thinking };
|
|
466
659
|
}
|
|
467
660
|
|
|
468
|
-
function
|
|
469
|
-
|
|
661
|
+
function buildPiArgs(cfg: PiRunConfig): string[] {
|
|
662
|
+
const args = ["--mode", "json", "-p", "--no-session"];
|
|
663
|
+
if (cfg.model)
|
|
664
|
+
args.push(
|
|
665
|
+
"--model",
|
|
666
|
+
cfg.thinking && cfg.thinking !== "off" && !cfg.model.includes(":")
|
|
667
|
+
? `${cfg.model}:${cfg.thinking}`
|
|
668
|
+
: cfg.model,
|
|
669
|
+
);
|
|
670
|
+
else if (cfg.thinking && cfg.thinking !== "off")
|
|
671
|
+
args.push("--thinking", cfg.thinking);
|
|
672
|
+
return args;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ── BoundedBufferCollector ─────────────────────────────────────────────
|
|
676
|
+
class BBC {
|
|
677
|
+
private c: Buffer[] = [];
|
|
678
|
+
private len = 0;
|
|
679
|
+
private trunc = 0;
|
|
680
|
+
constructor(private max: number) {}
|
|
681
|
+
append(b: Buffer) {
|
|
682
|
+
if (b.length >= this.max) {
|
|
683
|
+
this.trunc += this.len + b.length - this.max;
|
|
684
|
+
this.c = [b.subarray(b.length - this.max)];
|
|
685
|
+
this.len = this.max;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.c.push(b);
|
|
689
|
+
this.len += b.length;
|
|
690
|
+
while (this.len > this.max) {
|
|
691
|
+
const f = this.c[0]!;
|
|
692
|
+
if (f.length <= this.len - this.max) {
|
|
693
|
+
this.c.shift();
|
|
694
|
+
this.len -= f.length;
|
|
695
|
+
this.trunc += f.length;
|
|
696
|
+
} else {
|
|
697
|
+
const ov = this.len - this.max;
|
|
698
|
+
this.c[0] = f.subarray(ov);
|
|
699
|
+
this.len -= ov;
|
|
700
|
+
this.trunc += ov;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
toString() {
|
|
706
|
+
const body = Buffer.concat(this.c, this.len).toString("utf-8");
|
|
707
|
+
return this.trunc ? `[${this.trunc} bytes truncated]\n${body}` : body;
|
|
708
|
+
}
|
|
470
709
|
}
|
|
471
710
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
while (
|
|
476
|
-
|
|
477
|
-
|
|
711
|
+
// ── Trellis Context ────────────────────────────────────────────────────
|
|
712
|
+
function findRoot(start: string): string {
|
|
713
|
+
let c = resolve(start);
|
|
714
|
+
while (true) {
|
|
715
|
+
if (existsSync(join(c, ".trellis")) || existsSync(join(c, ".pi"))) return c;
|
|
716
|
+
const p = dirname(c);
|
|
717
|
+
if (p === c) return resolve(start);
|
|
718
|
+
c = p;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function splitFM(c: string) {
|
|
722
|
+
const m = c.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
723
|
+
return m
|
|
724
|
+
? { fm: m[1] ?? "", body: c.slice(m[0].length) }
|
|
725
|
+
: { fm: "", body: c };
|
|
726
|
+
}
|
|
727
|
+
function stripFM(c: string) {
|
|
728
|
+
return splitFM(c).body.trimStart();
|
|
729
|
+
}
|
|
730
|
+
function parseAgentFM(c: string): AgentConfig {
|
|
731
|
+
const cfg: AgentConfig = { fallbackModels: [] };
|
|
732
|
+
const { fm } = splitFM(c);
|
|
733
|
+
const lines = fm.split(/\r?\n/);
|
|
734
|
+
for (let i = 0; i < lines.length; i++) {
|
|
735
|
+
const m = (lines[i] ?? "").match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
736
|
+
if (!m) continue;
|
|
737
|
+
const k = m[1] ?? "",
|
|
738
|
+
v = m[2] ?? "";
|
|
739
|
+
if (k === "model")
|
|
740
|
+
cfg.model = v.trim().replace(/^["']|["']$/g, "") || undefined;
|
|
741
|
+
else if (k === "thinking")
|
|
742
|
+
cfg.thinking = (v.trim().replace(/^["']|["']$/g, "") || undefined) as
|
|
743
|
+
| string
|
|
744
|
+
| undefined;
|
|
745
|
+
else if (k === "fallbackModels" || k === "fallback_models") {
|
|
746
|
+
if (v.trim()) {
|
|
747
|
+
cfg.fallbackModels = v
|
|
748
|
+
.trim()
|
|
749
|
+
.replace(/^\[|\]$/g, "")
|
|
750
|
+
.split(",")
|
|
751
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
752
|
+
.filter(Boolean);
|
|
753
|
+
} else {
|
|
754
|
+
i++;
|
|
755
|
+
while (i < lines.length && /^\s+-\s/.test(lines[i] ?? "")) {
|
|
756
|
+
const item = (lines[i] ?? "")
|
|
757
|
+
.trim()
|
|
758
|
+
.replace(/^-\s+/, "")
|
|
759
|
+
.replace(/^["']|["']$/g, "");
|
|
760
|
+
if (item) cfg.fallbackModels.push(item);
|
|
761
|
+
i++;
|
|
762
|
+
}
|
|
763
|
+
i--;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return cfg;
|
|
478
768
|
}
|
|
479
769
|
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
770
|
+
function contextKey(input?: unknown, ctx?: PiExtensionContext): string | null {
|
|
771
|
+
const ov = str(process.env.TRELLIS_CONTEXT_ID);
|
|
772
|
+
if (ov) return ov.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 160) || hash(ov);
|
|
773
|
+
const sessionId =
|
|
774
|
+
callStr(ctx?.sessionManager?.getSessionId) ??
|
|
775
|
+
str(process.env.PI_SESSION_ID) ??
|
|
776
|
+
str(process.env.PI_SESSIONID) ??
|
|
777
|
+
lookupStr(input, ["session_id", "sessionId", "sessionID"]);
|
|
778
|
+
if (sessionId)
|
|
779
|
+
return `pi_${sessionId.replace(/[^A-Za-z0-9._-]+/g, "_") || hash(sessionId)}`;
|
|
780
|
+
const transcriptPath =
|
|
781
|
+
callStr(ctx?.sessionManager?.getSessionFile) ??
|
|
782
|
+
lookupStr(input, ["transcript_path", "transcriptPath", "transcript"]);
|
|
783
|
+
if (transcriptPath) return `pi_transcript_${hash(transcriptPath)}`;
|
|
784
|
+
return null;
|
|
484
785
|
}
|
|
485
786
|
|
|
486
|
-
function
|
|
787
|
+
function readTaskDir(root: string, key: string | null): string | null {
|
|
788
|
+
if (!key) return null;
|
|
487
789
|
try {
|
|
488
|
-
const
|
|
489
|
-
|
|
790
|
+
const ctx = JSON.parse(
|
|
791
|
+
readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
|
|
792
|
+
) as JsonObject;
|
|
793
|
+
let ref = str(ctx.current_task);
|
|
794
|
+
if (!ref) return null;
|
|
795
|
+
ref = ref;
|
|
796
|
+
ref = ref.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
797
|
+
if (ref.startsWith("tasks/")) ref = `.trellis/${ref}`;
|
|
798
|
+
return ref.startsWith(".trellis/")
|
|
799
|
+
? join(root, ref)
|
|
800
|
+
: isAbsolute(ref)
|
|
801
|
+
? ref
|
|
802
|
+
: join(root, ".trellis", "tasks", ref);
|
|
490
803
|
} catch {
|
|
491
|
-
return
|
|
804
|
+
return null;
|
|
492
805
|
}
|
|
493
806
|
}
|
|
494
|
-
|
|
495
|
-
function activeRuntimeContextKeys(projectRoot: string): string[] {
|
|
496
|
-
const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions");
|
|
807
|
+
function sessionHasTask(root: string, key: string): boolean {
|
|
497
808
|
try {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
sessionFileHasCurrentTask(join(sessionsDir, `${key}.json`)),
|
|
503
|
-
);
|
|
809
|
+
const ctx = JSON.parse(
|
|
810
|
+
readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
|
|
811
|
+
) as JsonObject;
|
|
812
|
+
return !!str(ctx.current_task);
|
|
504
813
|
} catch {
|
|
505
|
-
return
|
|
814
|
+
return false;
|
|
506
815
|
}
|
|
507
816
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
817
|
+
function adoptKey(root: string, key: string): string {
|
|
818
|
+
if (sessionHasTask(root, key)) return key;
|
|
819
|
+
try {
|
|
820
|
+
const dir = join(root, ".trellis", ".runtime", "sessions");
|
|
821
|
+
const keys = readdirSync(dir)
|
|
822
|
+
.filter(
|
|
823
|
+
(f) => f.endsWith(".json") && sessionHasTask(root, f.slice(0, -5)),
|
|
824
|
+
)
|
|
825
|
+
.map((f) => f.slice(0, -5));
|
|
826
|
+
const proc = keys.filter((k) => k.startsWith("pi_process_"));
|
|
827
|
+
const cands = proc.length ? proc : keys;
|
|
828
|
+
return cands.length === 1 ? cands[0]! : key;
|
|
829
|
+
} catch {
|
|
830
|
+
return key;
|
|
516
831
|
}
|
|
517
|
-
|
|
518
|
-
const keys = activeRuntimeContextKeys(projectRoot);
|
|
519
|
-
const processKeys = keys.filter((key) => key.startsWith("pi_process_"));
|
|
520
|
-
const candidates = processKeys.length ? processKeys : keys;
|
|
521
|
-
return candidates.length === 1 ? candidates[0] : contextKey;
|
|
522
832
|
}
|
|
523
833
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
stringValue(process.env.PI_SESSIONID) ??
|
|
536
|
-
lookupString(input, ["session_id", "sessionId", "sessionID"]);
|
|
537
|
-
if (sessionId) return `pi_${sanitizeKey(sessionId) || hashValue(sessionId)}`;
|
|
538
|
-
|
|
539
|
-
const transcriptPath =
|
|
540
|
-
callString(ctx?.sessionManager?.getSessionFile) ??
|
|
541
|
-
lookupString(input, ["transcript_path", "transcriptPath", "transcript"]);
|
|
542
|
-
if (transcriptPath) return `pi_transcript_${hashValue(transcriptPath)}`;
|
|
543
|
-
|
|
544
|
-
return fallback ?? null;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function readCurrentTask(
|
|
548
|
-
projectRoot: string,
|
|
549
|
-
platformInput?: unknown,
|
|
550
|
-
ctx?: PiExtensionContext,
|
|
551
|
-
contextKeyOverride?: string | null,
|
|
552
|
-
): string | null {
|
|
553
|
-
const contextKey =
|
|
554
|
-
contextKeyOverride ?? resolveContextKey(platformInput, ctx);
|
|
555
|
-
if (contextKey) {
|
|
556
|
-
try {
|
|
557
|
-
const rawContext = readText(
|
|
558
|
-
join(
|
|
559
|
-
projectRoot,
|
|
560
|
-
".trellis",
|
|
561
|
-
".runtime",
|
|
562
|
-
"sessions",
|
|
563
|
-
`${contextKey}.json`,
|
|
564
|
-
),
|
|
565
|
-
);
|
|
566
|
-
const context = JSON.parse(rawContext) as JsonObject;
|
|
567
|
-
const taskRef = normalizeTaskRef(stringValue(context.current_task) ?? "");
|
|
568
|
-
if (taskRef) return taskRefToDir(projectRoot, taskRef);
|
|
569
|
-
} catch {
|
|
570
|
-
// Missing or malformed session context means no active task.
|
|
571
|
-
}
|
|
834
|
+
// ── Workflow State Breadcrumb ─────────────────────────────────────────
|
|
835
|
+
const WF_RE =
|
|
836
|
+
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
837
|
+
function workflowBreadcrumb(root: string, key: string | null): string {
|
|
838
|
+
const wf = readText(join(root, ".trellis", "workflow.md"));
|
|
839
|
+
if (!wf) return "";
|
|
840
|
+
const templates: Record<string, string> = {};
|
|
841
|
+
for (const m of wf.matchAll(WF_RE)) {
|
|
842
|
+
const s = m[1] ?? "",
|
|
843
|
+
b = (m[2] ?? "").trim();
|
|
844
|
+
if (s && b) templates[s] = b;
|
|
572
845
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
function readJsonlFiles(
|
|
578
|
-
projectRoot: string,
|
|
579
|
-
taskDir: string,
|
|
580
|
-
jsonlName: string,
|
|
581
|
-
): string {
|
|
582
|
-
const jsonlPath = join(taskDir, jsonlName);
|
|
583
|
-
const lines = readText(jsonlPath).split(/\r?\n/);
|
|
584
|
-
const chunks: string[] = [];
|
|
585
|
-
|
|
586
|
-
for (const line of lines) {
|
|
587
|
-
const trimmed = line.trim();
|
|
588
|
-
if (!trimmed) continue;
|
|
846
|
+
const dir = readTaskDir(root, key);
|
|
847
|
+
let header = "Status: no_task",
|
|
848
|
+
lookup = "no_task";
|
|
849
|
+
if (dir) {
|
|
589
850
|
try {
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
851
|
+
const d = JSON.parse(readText(join(dir, "task.json"))) as JsonObject;
|
|
852
|
+
const status = str(d.status) ?? "";
|
|
853
|
+
const id = str(d.id) ?? dir.split(/[\\/]/).pop() ?? "";
|
|
854
|
+
if (status) {
|
|
855
|
+
header = `Task: ${id} (${status})`;
|
|
856
|
+
lookup = status;
|
|
596
857
|
}
|
|
597
|
-
} catch {
|
|
598
|
-
// Seed rows and malformed lines must not block sub-agent startup.
|
|
599
|
-
}
|
|
858
|
+
} catch {}
|
|
600
859
|
}
|
|
601
|
-
|
|
602
|
-
return
|
|
860
|
+
const body = templates[lookup] ?? "Refer to workflow.md for current step.";
|
|
861
|
+
return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
|
|
603
862
|
}
|
|
604
863
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
864
|
+
// ── Session Overview ───────────────────────────────────────────────────
|
|
865
|
+
function sessionOverview(root: string, key: string | null): string {
|
|
866
|
+
const script = join(root, ".trellis", "scripts", "get_context.py");
|
|
867
|
+
if (!exists(script)) return "";
|
|
868
|
+
try {
|
|
869
|
+
const py = process.platform === "win32" ? "python" : "python3";
|
|
870
|
+
const result = spawnSync(py, [script], {
|
|
871
|
+
cwd: root,
|
|
872
|
+
env: key ? { ...process.env, TRELLIS_CONTEXT_ID: key } : process.env,
|
|
873
|
+
encoding: "utf-8",
|
|
874
|
+
timeout: SESSION_OVERVIEW_TIMEOUT_MS,
|
|
875
|
+
windowsHide: true,
|
|
876
|
+
});
|
|
877
|
+
if (result.status !== 0) return "";
|
|
878
|
+
const stdout = (result.stdout ?? "").trim();
|
|
879
|
+
return stdout ? `<session-overview>\n${stdout}\n</session-overview>` : "";
|
|
880
|
+
} catch {
|
|
881
|
+
return "";
|
|
615
882
|
}
|
|
883
|
+
}
|
|
616
884
|
|
|
617
|
-
|
|
618
|
-
const
|
|
885
|
+
function buildContext(root: string, agent: string, key: string | null): string {
|
|
886
|
+
const dir = readTaskDir(root, key);
|
|
887
|
+
if (!dir)
|
|
888
|
+
return "No active Trellis task found. Read .trellis/ before proceeding.";
|
|
889
|
+
const prd = readText(join(dir, "prd.md"));
|
|
890
|
+
const design = readText(join(dir, "design.md"));
|
|
891
|
+
const impl = readText(join(dir, "implement.md"));
|
|
619
892
|
const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
:
|
|
623
|
-
|
|
893
|
+
let spec = "";
|
|
894
|
+
if (jsonlName) {
|
|
895
|
+
const chunks: string[] = [];
|
|
896
|
+
for (const line of readText(join(dir, jsonlName)).split(/\r?\n/)) {
|
|
897
|
+
const t = line.trim();
|
|
898
|
+
if (!t) continue;
|
|
899
|
+
try {
|
|
900
|
+
const r = JSON.parse(t) as JsonObject;
|
|
901
|
+
const f = typeof r.file === "string" ? r.file : "";
|
|
902
|
+
if (f) {
|
|
903
|
+
const c = readText(join(root, f));
|
|
904
|
+
if (c) chunks.push(`## ${f}\n\n${c}`);
|
|
905
|
+
}
|
|
906
|
+
} catch {}
|
|
907
|
+
}
|
|
908
|
+
spec = chunks.join("\n\n---\n\n");
|
|
909
|
+
}
|
|
624
910
|
return [
|
|
625
|
-
|
|
626
|
-
`Task directory: ${
|
|
911
|
+
`## Trellis Task Context`,
|
|
912
|
+
`Task directory: ${dir}`,
|
|
627
913
|
"",
|
|
628
914
|
"### prd.md",
|
|
629
915
|
prd || "(missing)",
|
|
630
|
-
|
|
631
|
-
|
|
916
|
+
design ? "\n### design.md\n" + design : "",
|
|
917
|
+
impl ? "\n### implement.md\n" + impl : "",
|
|
918
|
+
spec ? "\n### Curated Spec / Research Context\n" + spec : "",
|
|
632
919
|
].join("\n");
|
|
633
920
|
}
|
|
634
921
|
|
|
635
|
-
function
|
|
636
|
-
|
|
922
|
+
function normalizeAgent(agent: string | undefined): string {
|
|
923
|
+
const name = agent ?? "trellis-implement";
|
|
924
|
+
return name.startsWith("trellis-") ? name : `trellis-${name}`;
|
|
637
925
|
}
|
|
638
926
|
|
|
639
|
-
function
|
|
640
|
-
|
|
641
|
-
agent: string,
|
|
642
|
-
): AgentDefinition {
|
|
643
|
-
const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
|
|
644
|
-
const raw = readText(join(projectRoot, ".pi", "agents", `${normalized}.md`));
|
|
645
|
-
return {
|
|
646
|
-
content: stripMarkdownFrontmatter(raw),
|
|
647
|
-
config: parseAgentConfig(raw),
|
|
648
|
-
};
|
|
927
|
+
function isTrellisAgent(root: string, agent: string): boolean {
|
|
928
|
+
return existsSync(join(root, ".pi", "agents", `${agent}.md`));
|
|
649
929
|
}
|
|
650
930
|
|
|
651
|
-
function
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
);
|
|
931
|
+
function buildPrompt(
|
|
932
|
+
root: string,
|
|
933
|
+
input: SubagentInput,
|
|
934
|
+
key: string | null,
|
|
935
|
+
): string {
|
|
936
|
+
const agent = normalizeAgent(input.agent);
|
|
937
|
+
const raw = readText(join(root, ".pi", "agents", `${agent}.md`));
|
|
938
|
+
const def = stripFM(raw);
|
|
939
|
+
const ctx = buildContext(root, agent, key);
|
|
940
|
+
return [
|
|
941
|
+
"## Trellis Agent Definition",
|
|
942
|
+
def || "(missing)",
|
|
943
|
+
"",
|
|
944
|
+
ctx,
|
|
945
|
+
"",
|
|
946
|
+
"## Delegated Task",
|
|
947
|
+
input.prompt ?? "",
|
|
948
|
+
].join("\n");
|
|
658
949
|
}
|
|
659
950
|
|
|
660
|
-
|
|
661
|
-
|
|
951
|
+
// ── Event parsing ─────────────────────────────────────────────────────
|
|
952
|
+
function parseJsonEvent(line: string): JsonObject | null {
|
|
953
|
+
const t = line.trim();
|
|
954
|
+
if (!t) return null;
|
|
955
|
+
const i = t.indexOf("{");
|
|
956
|
+
if (i < 0) return null;
|
|
957
|
+
try {
|
|
958
|
+
const p = JSON.parse(t.slice(i));
|
|
959
|
+
return isObj(p) ? p : null;
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
662
963
|
}
|
|
663
964
|
|
|
664
|
-
function
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return
|
|
965
|
+
function applyEvent(r: RunState, evt: JsonObject): boolean {
|
|
966
|
+
const type = typeof evt.type === "string" ? evt.type : "";
|
|
967
|
+
if (!type) return false;
|
|
968
|
+
if (type === "agent_start" || type === "turn_start") {
|
|
969
|
+
r.status = "running";
|
|
970
|
+
r.startedAt ??= Date.now();
|
|
971
|
+
return true;
|
|
671
972
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
973
|
+
if (type === "message_update") {
|
|
974
|
+
const ae = isObj(evt.assistantMessageEvent)
|
|
975
|
+
? evt.assistantMessageEvent
|
|
976
|
+
: null;
|
|
977
|
+
if (!ae || typeof ae.delta !== "string") return false;
|
|
978
|
+
if (ae.type === "thinking_delta") {
|
|
979
|
+
r.thinkingTail = appendTail(r.thinkingTail, ae.delta, MAX_TAIL);
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
if (ae.type === "text_delta") {
|
|
983
|
+
r.textTail = appendTail(r.textTail, ae.delta, MAX_TAIL);
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
675
986
|
return false;
|
|
676
987
|
}
|
|
677
|
-
if (
|
|
678
|
-
|
|
988
|
+
if (type === "message_end" && isObj(evt.message)) {
|
|
989
|
+
const msg = evt.message;
|
|
990
|
+
if (msg.role !== "assistant") return false;
|
|
991
|
+
r.usage.turns += 1;
|
|
992
|
+
const u = isObj(msg.usage) ? msg.usage : null;
|
|
993
|
+
const cost = isObj(u?.cost) ? u.cost : null;
|
|
994
|
+
r.usage.input += num(u?.input);
|
|
995
|
+
r.usage.output += num(u?.output);
|
|
996
|
+
r.usage.cacheRead += num(u?.cacheRead);
|
|
997
|
+
r.usage.cacheWrite += num(u?.cacheWrite);
|
|
998
|
+
r.usage.cost += num(cost?.total);
|
|
999
|
+
r.usage.ctxTokens = num(u?.totalTokens);
|
|
1000
|
+
const thinking = extractThinking(msg.content);
|
|
1001
|
+
if (thinking) r.thinkingTail = appendTail("", thinking, MAX_TAIL);
|
|
1002
|
+
const text = extractText(msg.content);
|
|
1003
|
+
if (text) {
|
|
1004
|
+
r.finalText = text;
|
|
1005
|
+
r.textTail = appendTail("", text, MAX_TAIL);
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof msg.model === "string") {
|
|
1008
|
+
const parsed = splitModelThinking(msg.model, r.thinking);
|
|
1009
|
+
r.model = parsed.model;
|
|
1010
|
+
r.thinking = parsed.thinking;
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof msg.errorMessage === "string") r.errorMessage = msg.errorMessage;
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
if (type === "tool_execution_start") {
|
|
1016
|
+
const id =
|
|
1017
|
+
typeof evt.toolCallId === "string"
|
|
1018
|
+
? evt.toolCallId
|
|
1019
|
+
: hash(`${Date.now()}`);
|
|
1020
|
+
const name = typeof evt.toolName === "string" ? evt.toolName : "tool";
|
|
1021
|
+
const args = summarizeToolArgs(name, evt.args);
|
|
1022
|
+
const existing = r.tools.findIndex((t) => t.id === id);
|
|
1023
|
+
if (existing >= 0)
|
|
1024
|
+
r.tools[existing] = { ...r.tools[existing]!, args, status: "running" };
|
|
1025
|
+
else
|
|
1026
|
+
r.tools.push({
|
|
1027
|
+
id,
|
|
1028
|
+
name,
|
|
1029
|
+
args,
|
|
1030
|
+
status: "running",
|
|
1031
|
+
startedAt: Date.now(),
|
|
1032
|
+
});
|
|
1033
|
+
if (r.tools.length > MAX_TOOLS)
|
|
1034
|
+
r.tools.splice(0, r.tools.length - MAX_TOOLS);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
if (type === "tool_execution_end") {
|
|
1038
|
+
const id = typeof evt.toolCallId === "string" ? evt.toolCallId : "";
|
|
1039
|
+
const idx = r.tools.findIndex((t) => t.id === id);
|
|
1040
|
+
if (idx >= 0)
|
|
1041
|
+
r.tools[idx] = {
|
|
1042
|
+
...r.tools[idx]!,
|
|
1043
|
+
status: evt.isError ? "failed" : "succeeded",
|
|
1044
|
+
finishedAt: Date.now(),
|
|
1045
|
+
};
|
|
1046
|
+
return true;
|
|
679
1047
|
}
|
|
1048
|
+
if (type === "agent_end") {
|
|
1049
|
+
r.finishedAt = Date.now();
|
|
1050
|
+
if (r.status === "running" || r.status === "pending")
|
|
1051
|
+
r.status = "succeeded";
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
680
1056
|
|
|
681
|
-
|
|
682
|
-
return
|
|
1057
|
+
function finalize(r: RunState, fallback: string): string {
|
|
1058
|
+
return r.finalText || fallback.trim() || r.stderrTail.trim();
|
|
1059
|
+
}
|
|
1060
|
+
function formatPiOutput(stdout: string, stderr: string): string {
|
|
1061
|
+
let ft = "";
|
|
1062
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1063
|
+
const t = line.trim();
|
|
1064
|
+
if (!t) continue;
|
|
1065
|
+
try {
|
|
1066
|
+
const evt = JSON.parse(t) as JsonObject;
|
|
1067
|
+
const msg = isObj(evt.message) ? evt.message : null;
|
|
1068
|
+
if (msg?.role === "assistant") {
|
|
1069
|
+
const txt = extractText(msg.content);
|
|
1070
|
+
if (txt) ft = txt;
|
|
1071
|
+
}
|
|
1072
|
+
} catch {}
|
|
1073
|
+
}
|
|
1074
|
+
return ft || stdout || stderr;
|
|
683
1075
|
}
|
|
684
1076
|
|
|
1077
|
+
// ── runPi: subprocess execution + event processing ───────────────────
|
|
685
1078
|
function runPi(
|
|
686
|
-
|
|
1079
|
+
root: string,
|
|
687
1080
|
prompt: string,
|
|
688
|
-
|
|
689
|
-
|
|
1081
|
+
cfg: PiRunConfig,
|
|
1082
|
+
state: RunState,
|
|
1083
|
+
emit: () => void,
|
|
1084
|
+
key?: string | null,
|
|
690
1085
|
signal?: AbortSignal,
|
|
691
|
-
): Promise<string> {
|
|
692
|
-
return new Promise((
|
|
1086
|
+
): Promise<{ output: string; failed: boolean }> {
|
|
1087
|
+
return new Promise((resolve) => {
|
|
693
1088
|
if (signal?.aborted) {
|
|
694
|
-
|
|
1089
|
+
state.status = "cancelled";
|
|
1090
|
+
state.errorMessage = "cancelled";
|
|
1091
|
+
state.finishedAt = Date.now();
|
|
1092
|
+
emit();
|
|
1093
|
+
resolve({ output: "cancelled", failed: true });
|
|
695
1094
|
return;
|
|
696
1095
|
}
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
env: contextKey
|
|
713
|
-
? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
|
|
714
|
-
: process.env,
|
|
715
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
716
|
-
windowsHide: true,
|
|
717
|
-
},
|
|
718
|
-
);
|
|
719
|
-
|
|
720
|
-
const stdout = new BoundedBufferCollector(MAX_SUBAGENT_STDOUT_BYTES);
|
|
721
|
-
const stderr = new BoundedBufferCollector(MAX_SUBAGENT_STDERR_BYTES);
|
|
1096
|
+
const inv = resolvePiCli();
|
|
1097
|
+
const childEnv = {
|
|
1098
|
+
...process.env,
|
|
1099
|
+
TRELLIS_SUBAGENT_CHILD: "1",
|
|
1100
|
+
...(key ? { TRELLIS_CONTEXT_ID: key } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
const cli = spawn(inv.command, [...inv.args, ...buildPiArgs(cfg)], {
|
|
1103
|
+
cwd: root,
|
|
1104
|
+
env: childEnv,
|
|
1105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1106
|
+
windowsHide: true,
|
|
1107
|
+
});
|
|
1108
|
+
const stdout = new BBC(MAX_STDOUT);
|
|
1109
|
+
const stderr = new BBC(MAX_STDERR);
|
|
1110
|
+
let buf = "";
|
|
722
1111
|
let settled = false;
|
|
723
1112
|
let aborted = false;
|
|
724
|
-
|
|
725
|
-
const
|
|
1113
|
+
let killTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1114
|
+
const abort = () => {
|
|
726
1115
|
aborted = true;
|
|
727
|
-
|
|
1116
|
+
cli.kill();
|
|
1117
|
+
killTimer = setTimeout(() => {
|
|
1118
|
+
if (!settled && cli.exitCode === null) cli.kill("SIGKILL");
|
|
1119
|
+
}, ABORT_KILL_GRACE_MS);
|
|
1120
|
+
killTimer?.unref?.();
|
|
728
1121
|
};
|
|
729
|
-
|
|
730
|
-
const cleanup = (): void => {
|
|
731
|
-
signal?.removeEventListener("abort", abortChild);
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
const fail = (error: Error): void => {
|
|
1122
|
+
const done = (v: { output: string; failed: boolean }) => {
|
|
735
1123
|
if (settled) return;
|
|
736
1124
|
settled = true;
|
|
737
|
-
|
|
738
|
-
|
|
1125
|
+
if (killTimer) clearTimeout(killTimer);
|
|
1126
|
+
signal?.removeEventListener("abort", abort);
|
|
1127
|
+
emit();
|
|
1128
|
+
resolve(v);
|
|
739
1129
|
};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1130
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1131
|
+
state.status = "running";
|
|
1132
|
+
state.startedAt = Date.now();
|
|
1133
|
+
emit();
|
|
1134
|
+
const processLine = (line: string) => {
|
|
1135
|
+
const evt = parseJsonEvent(line);
|
|
1136
|
+
if (evt && applyEvent(state, evt)) emit();
|
|
746
1137
|
};
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1138
|
+
cli.stdout?.on("data", (d: Buffer) => {
|
|
1139
|
+
stdout.append(d);
|
|
1140
|
+
buf += d.toString("utf-8");
|
|
1141
|
+
if (buf.length > MAX_LINE_BUFFER) buf = buf.slice(-MAX_LINE_BUFFER);
|
|
1142
|
+
const lines = buf.split(/\r?\n/);
|
|
1143
|
+
buf = lines.pop() ?? "";
|
|
1144
|
+
for (const l of lines) processLine(l);
|
|
754
1145
|
});
|
|
755
|
-
|
|
756
|
-
|
|
1146
|
+
cli.stderr?.on("data", (d: Buffer) => {
|
|
1147
|
+
stderr.append(d);
|
|
1148
|
+
state.stderrTail = appendTail(
|
|
1149
|
+
state.stderrTail,
|
|
1150
|
+
d.toString("utf-8"),
|
|
1151
|
+
MAX_TAIL,
|
|
1152
|
+
);
|
|
1153
|
+
});
|
|
1154
|
+
cli.stdin?.on("error", (e: Error & { code?: string }) => {
|
|
1155
|
+
if (!aborted && e.code !== "EPIPE")
|
|
1156
|
+
done({ output: e.message, failed: true });
|
|
1157
|
+
});
|
|
1158
|
+
cli.on("error", (e) => {
|
|
1159
|
+
state.status = aborted ? "cancelled" : "failed";
|
|
1160
|
+
state.errorMessage = e instanceof Error ? e.message : String(e);
|
|
1161
|
+
state.finishedAt = Date.now();
|
|
1162
|
+
done({ output: finalize(state, state.errorMessage), failed: true });
|
|
1163
|
+
});
|
|
1164
|
+
cli.on("close", (code) => {
|
|
1165
|
+
if (buf.trim()) processLine(buf);
|
|
757
1166
|
const out = stdout.toString();
|
|
758
1167
|
const err = stderr.toString();
|
|
1168
|
+
state.stderrTail = appendTail("", err, MAX_TAIL);
|
|
1169
|
+
state.finishedAt = Date.now();
|
|
759
1170
|
if (aborted) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
)
|
|
1171
|
+
state.status = "cancelled";
|
|
1172
|
+
state.errorMessage = "cancelled";
|
|
1173
|
+
done({ output: finalize(state, "cancelled"), failed: true });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (code === 0) {
|
|
1177
|
+
if (state.status === "pending" || state.status === "running")
|
|
1178
|
+
state.status = "succeeded";
|
|
1179
|
+
done({
|
|
1180
|
+
output: finalize(state, formatPiOutput(out, err)),
|
|
1181
|
+
failed: false,
|
|
1182
|
+
});
|
|
1183
|
+
return;
|
|
767
1184
|
}
|
|
1185
|
+
state.status = "failed";
|
|
1186
|
+
state.errorMessage = err || out || `exit ${code ?? "?"}`;
|
|
1187
|
+
done({ output: finalize(state, state.errorMessage), failed: true });
|
|
768
1188
|
});
|
|
769
|
-
|
|
770
|
-
child.stdin?.end(prompt);
|
|
1189
|
+
cli.stdin?.end(prompt);
|
|
771
1190
|
});
|
|
772
1191
|
}
|
|
773
1192
|
|
|
774
|
-
|
|
775
|
-
projectRoot: string,
|
|
776
|
-
input: SubagentInput,
|
|
777
|
-
contextKey?: string | null,
|
|
778
|
-
agentName?: string,
|
|
779
|
-
agentDefinition?: AgentDefinition,
|
|
780
|
-
): string {
|
|
781
|
-
const normalized =
|
|
782
|
-
agentName ?? normalizeAgentName(input.agent ?? "trellis-implement");
|
|
783
|
-
const definition =
|
|
784
|
-
agentDefinition ?? readAgentDefinition(projectRoot, normalized);
|
|
785
|
-
const context = buildTrellisContext(
|
|
786
|
-
projectRoot,
|
|
787
|
-
normalized,
|
|
788
|
-
input,
|
|
789
|
-
undefined,
|
|
790
|
-
contextKey,
|
|
791
|
-
);
|
|
792
|
-
const prompt = input.prompt ?? "";
|
|
793
|
-
|
|
794
|
-
return [
|
|
795
|
-
"## Trellis Agent Definition",
|
|
796
|
-
definition.content || "(missing agent definition)",
|
|
797
|
-
"",
|
|
798
|
-
context,
|
|
799
|
-
"",
|
|
800
|
-
"## Delegated Task",
|
|
801
|
-
prompt,
|
|
802
|
-
].join("\n");
|
|
803
|
-
}
|
|
804
|
-
|
|
1193
|
+
// ── runSubagent: orchestrate single/parallel/chain via native partial updates ──
|
|
805
1194
|
async function runSubagent(
|
|
806
|
-
|
|
1195
|
+
root: string,
|
|
807
1196
|
input: SubagentInput,
|
|
808
|
-
|
|
1197
|
+
key: string | null,
|
|
809
1198
|
signal?: AbortSignal,
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
1199
|
+
onUpdate?: (r: PiToolResult) => void,
|
|
1200
|
+
inheritedThinking?: string,
|
|
1201
|
+
): Promise<{ output: string; details: ProgressDetails; failed: boolean }> {
|
|
1202
|
+
const agentName = normalizeAgent(input.agent);
|
|
1203
|
+
const agentRaw = readText(join(root, ".pi", "agents", `${agentName}.md`));
|
|
1204
|
+
const agentCfg = parseAgentFM(agentRaw);
|
|
1205
|
+
const runCfg = resolveRunCfg(input, agentCfg, inheritedThinking);
|
|
814
1206
|
const mode = input.mode ?? "single";
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1207
|
+
const startedAt = Date.now();
|
|
1208
|
+
const details: ProgressDetails = {
|
|
1209
|
+
kind: "trellis-subagent-progress",
|
|
1210
|
+
agent: agentName,
|
|
1211
|
+
mode,
|
|
1212
|
+
startedAt,
|
|
1213
|
+
updatedAt: startedAt,
|
|
1214
|
+
final: false,
|
|
1215
|
+
runs: [],
|
|
1216
|
+
};
|
|
1217
|
+
let lastEmit = 0;
|
|
1218
|
+
let lastPartialKey = "";
|
|
1219
|
+
let closed = false;
|
|
1220
|
+
const pushPartial = (force = false) => {
|
|
1221
|
+
if (closed || !onUpdate) return;
|
|
1222
|
+
const key = progressKey(details);
|
|
1223
|
+
if (!force && key === lastPartialKey) return;
|
|
1224
|
+
lastPartialKey = key;
|
|
1225
|
+
onUpdate({
|
|
1226
|
+
// Keep native partial content stable; renderResult owns the visible progress UI.
|
|
1227
|
+
content: [{ type: "text", text: "subagent running" }],
|
|
1228
|
+
details: cloneProgress(details),
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
const emit = (force = false) => {
|
|
1232
|
+
const now = Date.now();
|
|
1233
|
+
if (!force && now - lastEmit < THROTTLE_MS) return;
|
|
1234
|
+
lastEmit = now;
|
|
1235
|
+
details.updatedAt = now;
|
|
1236
|
+
pushPartial(force);
|
|
1237
|
+
};
|
|
1238
|
+
const finish = (output: string, failed: boolean) => {
|
|
1239
|
+
closed = true;
|
|
1240
|
+
details.final = true;
|
|
1241
|
+
details.updatedAt = Date.now();
|
|
1242
|
+
return { output, details: cloneProgress(details), failed };
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
try {
|
|
1246
|
+
if (mode === "parallel") {
|
|
1247
|
+
const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
|
|
1248
|
+
details.runs = prompts.map((p, i) => {
|
|
1249
|
+
const r = newRun(`${agentName}-${i + 1}`, agentName, p);
|
|
1250
|
+
applyRunConfig(r, runCfg);
|
|
1251
|
+
return r;
|
|
1252
|
+
});
|
|
1253
|
+
emit(true);
|
|
1254
|
+
const results = await Promise.all(
|
|
1255
|
+
prompts.map((p, i) =>
|
|
1256
|
+
runPi(
|
|
1257
|
+
root,
|
|
1258
|
+
buildPrompt(root, { ...input, prompt: p }, key),
|
|
1259
|
+
runCfg,
|
|
1260
|
+
details.runs[i]!,
|
|
1261
|
+
emit,
|
|
1262
|
+
key,
|
|
1263
|
+
signal,
|
|
827
1264
|
),
|
|
828
|
-
runConfig,
|
|
829
|
-
contextKey,
|
|
830
|
-
signal,
|
|
831
|
-
),
|
|
832
|
-
),
|
|
833
|
-
);
|
|
834
|
-
return outputs.join("\n\n---\n\n");
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (mode === "chain") {
|
|
838
|
-
let previous = "";
|
|
839
|
-
const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
|
|
840
|
-
for (const prompt of prompts) {
|
|
841
|
-
previous = await runPi(
|
|
842
|
-
projectRoot,
|
|
843
|
-
buildSubagentPrompt(
|
|
844
|
-
projectRoot,
|
|
845
|
-
{
|
|
846
|
-
...input,
|
|
847
|
-
prompt: previous
|
|
848
|
-
? `${prompt}\n\nPrevious output:\n${previous}`
|
|
849
|
-
: prompt,
|
|
850
|
-
},
|
|
851
|
-
contextKey,
|
|
852
|
-
agentName,
|
|
853
|
-
agentDefinition,
|
|
854
1265
|
),
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1266
|
+
);
|
|
1267
|
+
return finish(
|
|
1268
|
+
results.map((r) => r.output).join("\n\n---\n\n"),
|
|
1269
|
+
results.some((r) => r.failed),
|
|
858
1270
|
);
|
|
859
1271
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1272
|
+
if (mode === "chain") {
|
|
1273
|
+
let prev = "";
|
|
1274
|
+
let failed = false;
|
|
1275
|
+
for (let i = 0; i < (input.prompts?.length ?? 1); i++) {
|
|
1276
|
+
const p = input.prompts?.[i] ?? input.prompt ?? "";
|
|
1277
|
+
const rs = newRun(`${agentName}-${i + 1}`, agentName, p, i + 1);
|
|
1278
|
+
applyRunConfig(rs, runCfg);
|
|
1279
|
+
details.runs.push(rs);
|
|
1280
|
+
emit(true);
|
|
1281
|
+
const result = await runPi(
|
|
1282
|
+
root,
|
|
1283
|
+
buildPrompt(
|
|
1284
|
+
root,
|
|
1285
|
+
{
|
|
1286
|
+
...input,
|
|
1287
|
+
prompt: prev ? `${p}\n\nPrevious output:\n${prev}` : p,
|
|
1288
|
+
},
|
|
1289
|
+
key,
|
|
1290
|
+
),
|
|
1291
|
+
runCfg,
|
|
1292
|
+
rs,
|
|
1293
|
+
emit,
|
|
1294
|
+
key,
|
|
1295
|
+
signal,
|
|
1296
|
+
);
|
|
1297
|
+
prev = result.output;
|
|
1298
|
+
failed = failed || result.failed;
|
|
1299
|
+
if (result.failed) break;
|
|
1300
|
+
}
|
|
1301
|
+
return finish(prev, failed);
|
|
1302
|
+
}
|
|
1303
|
+
const rs = newRun(`${agentName}-1`, agentName, input.prompt ?? "");
|
|
1304
|
+
applyRunConfig(rs, runCfg);
|
|
1305
|
+
details.runs = [rs];
|
|
1306
|
+
emit(true);
|
|
1307
|
+
const result = await runPi(
|
|
1308
|
+
root,
|
|
1309
|
+
buildPrompt(root, input, key),
|
|
1310
|
+
runCfg,
|
|
1311
|
+
rs,
|
|
1312
|
+
emit,
|
|
1313
|
+
key,
|
|
1314
|
+
signal,
|
|
1315
|
+
);
|
|
1316
|
+
return finish(result.output, result.failed);
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1319
|
+
const r = activeRun(details);
|
|
1320
|
+
if (r) {
|
|
1321
|
+
r.status = "failed";
|
|
1322
|
+
r.errorMessage = message;
|
|
1323
|
+
r.finishedAt = Date.now();
|
|
1324
|
+
}
|
|
1325
|
+
return finish(message, true);
|
|
1326
|
+
}
|
|
876
1327
|
}
|
|
877
1328
|
|
|
1329
|
+
// ── Extension ──────────────────────────────────────────────────────────
|
|
878
1330
|
export default function trellisExtension(pi: {
|
|
879
1331
|
registerTool?: (tool: JsonObject) => void;
|
|
1332
|
+
registerShortcut?: (
|
|
1333
|
+
key: string,
|
|
1334
|
+
opts: {
|
|
1335
|
+
description?: string;
|
|
1336
|
+
handler: (ctx: PiExtensionContext) => unknown;
|
|
1337
|
+
},
|
|
1338
|
+
) => void;
|
|
880
1339
|
on?: (
|
|
881
1340
|
event: string,
|
|
882
1341
|
handler: (event: unknown, ctx?: PiExtensionContext) => unknown,
|
|
883
1342
|
) => void;
|
|
884
|
-
|
|
1343
|
+
getThinkingLevel?: () => string;
|
|
885
1344
|
}): void {
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
);
|
|
896
|
-
currentContextKey = adoptExistingContextKey(
|
|
897
|
-
projectRoot,
|
|
898
|
-
resolvedContextKey ?? processContextKey,
|
|
899
|
-
);
|
|
900
|
-
return currentContextKey;
|
|
1345
|
+
if (process.env.TRELLIS_SUBAGENT_CHILD === "1") return;
|
|
1346
|
+
const root = findRoot(process.cwd());
|
|
1347
|
+
const procKey = `pi_process_${hash([root, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"))}`;
|
|
1348
|
+
let curKey: string | null = null;
|
|
1349
|
+
|
|
1350
|
+
const getKey = (input?: unknown, ctx?: PiExtensionContext) => {
|
|
1351
|
+
const k = adoptKey(root, contextKey(input, ctx) ?? curKey ?? procKey);
|
|
1352
|
+
curKey = k;
|
|
1353
|
+
return k;
|
|
901
1354
|
};
|
|
902
1355
|
|
|
1356
|
+
// Per-turn cache to avoid double-spawning python
|
|
1357
|
+
let turnCache: {
|
|
1358
|
+
key: string | null;
|
|
1359
|
+
ts: number;
|
|
1360
|
+
wf: string;
|
|
1361
|
+
ov: string;
|
|
1362
|
+
} | null = null;
|
|
1363
|
+
const getTurnCtx = (k: string | null) => {
|
|
1364
|
+
const now = Date.now();
|
|
1365
|
+
if (turnCache && turnCache.key === k && now - turnCache.ts < 1500)
|
|
1366
|
+
return turnCache;
|
|
1367
|
+
turnCache = {
|
|
1368
|
+
key: k,
|
|
1369
|
+
ts: now,
|
|
1370
|
+
wf: workflowBreadcrumb(root, k),
|
|
1371
|
+
ov: sessionOverview(root, k),
|
|
1372
|
+
};
|
|
1373
|
+
return turnCache;
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// Toggle only the latest subagent native card; do not use Pi global tool expansion.
|
|
1377
|
+
const toggleDetail = (ctx: PiExtensionContext) => {
|
|
1378
|
+
const id = activeSubagentToolCallId;
|
|
1379
|
+
const card = id ? nativeCards.get(id) : undefined;
|
|
1380
|
+
if (!card) {
|
|
1381
|
+
ctx.ui?.notify?.("No subagent card to toggle yet.", "warning");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
card.state.localExpanded = card.state.localExpanded !== true;
|
|
1385
|
+
card.invalidate();
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
pi.registerShortcut?.("alt+o", {
|
|
1389
|
+
description: "Toggle latest subagent card details",
|
|
1390
|
+
handler: async (ctx: PiExtensionContext) => toggleDetail(ctx),
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Tool registration
|
|
903
1394
|
pi.registerTool?.({
|
|
904
|
-
name: "
|
|
905
|
-
label: "Subagent",
|
|
1395
|
+
name: "trellis_subagent",
|
|
1396
|
+
label: "Trellis Subagent",
|
|
906
1397
|
description: "Run a Trellis project sub-agent with active task context.",
|
|
1398
|
+
promptSnippet:
|
|
1399
|
+
'Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from `task.py current`>" before any other instructions.',
|
|
1400
|
+
promptGuidelines: [
|
|
1401
|
+
'Use subagent for task delegation. Your dispatch prompt MUST start with "Active task: <task path from `task.py current`>".',
|
|
1402
|
+
],
|
|
907
1403
|
parameters: {
|
|
908
1404
|
type: "object",
|
|
909
1405
|
properties: {
|
|
@@ -916,15 +1412,11 @@ export default function trellisExtension(pi: {
|
|
|
916
1412
|
type: "string",
|
|
917
1413
|
description: "Task prompt for the sub-agent.",
|
|
918
1414
|
},
|
|
919
|
-
mode: {
|
|
920
|
-
type: "string",
|
|
921
|
-
enum: ["single", "parallel", "chain"],
|
|
922
|
-
description: "Delegation mode.",
|
|
923
|
-
},
|
|
1415
|
+
mode: { type: "string", enum: ["single", "parallel", "chain"] },
|
|
924
1416
|
prompts: {
|
|
925
1417
|
type: "array",
|
|
926
1418
|
items: { type: "string" },
|
|
927
|
-
|
|
1419
|
+
maxItems: MAX_PARALLEL_PROMPTS,
|
|
928
1420
|
},
|
|
929
1421
|
model: {
|
|
930
1422
|
type: "string",
|
|
@@ -933,65 +1425,176 @@ export default function trellisExtension(pi: {
|
|
|
933
1425
|
},
|
|
934
1426
|
thinking: {
|
|
935
1427
|
type: "string",
|
|
936
|
-
enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
937
1428
|
description:
|
|
938
1429
|
"Optional Pi thinking level override for the child sub-agent process.",
|
|
1430
|
+
enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
939
1431
|
},
|
|
940
1432
|
},
|
|
941
|
-
required: ["prompt"],
|
|
942
1433
|
},
|
|
943
1434
|
execute: async (
|
|
944
|
-
|
|
1435
|
+
id: string,
|
|
945
1436
|
input: SubagentInput,
|
|
946
|
-
|
|
947
|
-
|
|
1437
|
+
signal?: AbortSignal,
|
|
1438
|
+
onUpdate?: (r: PiToolResult) => void,
|
|
948
1439
|
ctx?: PiExtensionContext,
|
|
949
|
-
)
|
|
950
|
-
|
|
951
|
-
const
|
|
1440
|
+
) => {
|
|
1441
|
+
activeSubagentToolCallId = id;
|
|
1442
|
+
const agentName = normalizeAgent(input.agent);
|
|
1443
|
+
if (!isTrellisAgent(root, agentName)) {
|
|
1444
|
+
return {
|
|
1445
|
+
content: [
|
|
1446
|
+
{
|
|
1447
|
+
type: "text",
|
|
1448
|
+
text:
|
|
1449
|
+
"`trellis_subagent` is only for Trellis workflow agents with a definition file in .pi/agents/.\n\n" +
|
|
1450
|
+
`No definition found for: ${agentName}\n\n` +
|
|
1451
|
+
"For general-purpose sub-agents, use one of these community tools:\n" +
|
|
1452
|
+
"- `subagent` tool from npm:pi-subagents (nicobailon/pi-subagents)\n" +
|
|
1453
|
+
"- `Agent` tool from npm:@tintinweb/pi-subagents\n\n" +
|
|
1454
|
+
"If neither is installed, ask the user to either:\n" +
|
|
1455
|
+
`- Create .pi/agents/${agentName}.md for your custom Trellis agent\n` +
|
|
1456
|
+
"- Install a community subagent package: pi install -l npm:@tintinweb/pi-subagents",
|
|
1457
|
+
},
|
|
1458
|
+
],
|
|
1459
|
+
details: { agent: agentName, error: "not a trellis workflow agent" },
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const mode = input.mode ?? "single";
|
|
1463
|
+
const prompt = input.prompt?.trim();
|
|
1464
|
+
const prompts = input.prompts?.map((p) => p.trim()).filter(Boolean);
|
|
1465
|
+
if (mode === "single" && !prompt)
|
|
1466
|
+
throw new Error("subagent prompt is required for single mode");
|
|
1467
|
+
if (
|
|
1468
|
+
(mode === "parallel" || mode === "chain") &&
|
|
1469
|
+
!prompt &&
|
|
1470
|
+
!prompts?.length
|
|
1471
|
+
)
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
"subagent prompt or prompts are required for parallel/chain mode",
|
|
1474
|
+
);
|
|
1475
|
+
if (
|
|
1476
|
+
mode === "parallel" &&
|
|
1477
|
+
prompts &&
|
|
1478
|
+
prompts.length > MAX_PARALLEL_PROMPTS
|
|
1479
|
+
)
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`subagent parallel mode supports at most ${MAX_PARALLEL_PROMPTS} prompts`,
|
|
1482
|
+
);
|
|
1483
|
+
const cleanInput: SubagentInput = {
|
|
1484
|
+
...input,
|
|
1485
|
+
prompt,
|
|
1486
|
+
prompts: prompts?.length ? prompts : undefined,
|
|
1487
|
+
};
|
|
1488
|
+
const key = getKey(cleanInput, ctx);
|
|
1489
|
+
const inheritedThinking = pi.getThinkingLevel?.();
|
|
1490
|
+
const result = await runSubagent(
|
|
1491
|
+
root,
|
|
1492
|
+
cleanInput,
|
|
1493
|
+
key,
|
|
1494
|
+
signal,
|
|
1495
|
+
onUpdate,
|
|
1496
|
+
inheritedThinking,
|
|
1497
|
+
);
|
|
952
1498
|
return {
|
|
953
|
-
content: [{ type: "text", text: output }],
|
|
954
|
-
details:
|
|
955
|
-
|
|
956
|
-
|
|
1499
|
+
content: [{ type: "text", text: result.output }],
|
|
1500
|
+
details: result.details,
|
|
1501
|
+
};
|
|
1502
|
+
},
|
|
1503
|
+
// Hide the call renderer so the native card only shows result/progress once.
|
|
1504
|
+
renderCall: () => ({
|
|
1505
|
+
render() {
|
|
1506
|
+
return [];
|
|
1507
|
+
},
|
|
1508
|
+
invalidate() {},
|
|
1509
|
+
}),
|
|
1510
|
+
renderResult: (
|
|
1511
|
+
result: PiToolResult,
|
|
1512
|
+
_opts?: { expanded?: boolean; isPartial?: boolean },
|
|
1513
|
+
_theme?: unknown,
|
|
1514
|
+
context?: unknown,
|
|
1515
|
+
) => {
|
|
1516
|
+
const ctxObj = isObj(context) ? context : null;
|
|
1517
|
+
const toolCallId = str(ctxObj?.toolCallId);
|
|
1518
|
+
const state = isObj(ctxObj?.state) ? (ctxObj.state as JsonObject) : null;
|
|
1519
|
+
const invalidate =
|
|
1520
|
+
typeof ctxObj?.invalidate === "function"
|
|
1521
|
+
? (ctxObj.invalidate as () => void)
|
|
1522
|
+
: null;
|
|
1523
|
+
const isProgress =
|
|
1524
|
+
isObj(result.details) &&
|
|
1525
|
+
result.details.kind === "trellis-subagent-progress";
|
|
1526
|
+
if (toolCallId && state && invalidate) {
|
|
1527
|
+
const updatedAt = isProgress
|
|
1528
|
+
? (result.details as ProgressDetails).updatedAt
|
|
1529
|
+
: Date.now();
|
|
1530
|
+
rememberNativeCard(toolCallId, { state, invalidate, updatedAt });
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
render(w: number) {
|
|
1534
|
+
if (isProgress) {
|
|
1535
|
+
const expanded = state?.localExpanded === true;
|
|
1536
|
+
return renderProgressCard(
|
|
1537
|
+
result.details as ProgressDetails,
|
|
1538
|
+
expanded,
|
|
1539
|
+
w,
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return [trunc(result.content?.[0]?.text ?? "(no output)", w)];
|
|
957
1543
|
},
|
|
1544
|
+
invalidate() {},
|
|
958
1545
|
};
|
|
959
1546
|
},
|
|
960
1547
|
});
|
|
961
1548
|
|
|
1549
|
+
// Events
|
|
962
1550
|
pi.on?.("session_start", (event, ctx) => {
|
|
963
|
-
|
|
1551
|
+
getKey(event, ctx);
|
|
964
1552
|
ctx?.ui?.notify?.(
|
|
965
1553
|
"Trellis project context is available. Use /trellis-continue to resume the current task.",
|
|
966
1554
|
"info",
|
|
967
1555
|
);
|
|
968
1556
|
});
|
|
1557
|
+
pi.on?.("session_shutdown", () => {
|
|
1558
|
+
nativeCards.clear();
|
|
1559
|
+
activeSubagentToolCallId = null;
|
|
1560
|
+
});
|
|
1561
|
+
pi.on?.("tool_call", (event, ctx) => {
|
|
1562
|
+
const k = getKey(event, ctx);
|
|
1563
|
+
const ev = event as { toolName?: string; input?: JsonObject };
|
|
1564
|
+
if (
|
|
1565
|
+
ev.toolName === "bash" &&
|
|
1566
|
+
isObj(ev.input) &&
|
|
1567
|
+
typeof ev.input.command === "string" &&
|
|
1568
|
+
!cmdHasTrellisCtx(ev.input.command)
|
|
1569
|
+
)
|
|
1570
|
+
ev.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(k)}; ${ev.input.command}`;
|
|
1571
|
+
});
|
|
1572
|
+
// Preserve progress details from execute(); mark failed subagent results through
|
|
1573
|
+
// the official tool_result patch hook instead of throwing away renderer details.
|
|
1574
|
+
pi.on?.("tool_result", (event) => {
|
|
1575
|
+
const ev = event as { toolName?: string; details?: unknown };
|
|
1576
|
+
if (
|
|
1577
|
+
ev.toolName === "trellis_subagent" &&
|
|
1578
|
+
isObj(ev.details) &&
|
|
1579
|
+
ev.details.kind === "trellis-subagent-progress" &&
|
|
1580
|
+
Array.isArray(ev.details.runs) &&
|
|
1581
|
+
ev.details.runs.some(
|
|
1582
|
+
(r) => isObj(r) && (r.status === "failed" || r.status === "cancelled"),
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
return { isError: true };
|
|
1586
|
+
return undefined;
|
|
1587
|
+
});
|
|
969
1588
|
pi.on?.("before_agent_start", (event, ctx) => {
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
"trellis-implement",
|
|
975
|
-
event,
|
|
976
|
-
ctx,
|
|
977
|
-
contextKey,
|
|
978
|
-
);
|
|
1589
|
+
const k = getKey(event, ctx);
|
|
1590
|
+
const cur = (event as { systemPrompt?: string }).systemPrompt ?? "";
|
|
1591
|
+
const ctxText = buildContext(root, "trellis-implement", k);
|
|
1592
|
+
const { wf, ov } = getTurnCtx(k);
|
|
979
1593
|
return {
|
|
980
|
-
systemPrompt: [
|
|
1594
|
+
systemPrompt: [cur, ctxText, wf, ov].filter(Boolean).join("\n\n"),
|
|
981
1595
|
};
|
|
982
1596
|
});
|
|
983
1597
|
pi.on?.("context", (event, ctx) => {
|
|
984
|
-
|
|
985
|
-
const messages = (event as PiContextEvent).messages;
|
|
986
|
-
return Array.isArray(messages) ? { messages } : undefined;
|
|
987
|
-
});
|
|
988
|
-
pi.on?.("input", (event, ctx) => {
|
|
989
|
-
getContextKey(event, ctx);
|
|
990
|
-
return { action: "continue" };
|
|
991
|
-
});
|
|
992
|
-
pi.on?.("tool_call", (event, ctx) => {
|
|
993
|
-
const contextKey = getContextKey(event, ctx);
|
|
994
|
-
injectTrellisContextIntoBash(event, contextKey);
|
|
995
|
-
return undefined;
|
|
1598
|
+
getKey(event, ctx);
|
|
996
1599
|
});
|
|
997
1600
|
}
|