@heyhuynhgiabuu/pi-task 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +67 -3
  2. package/dist/constants.d.ts +4 -0
  3. package/dist/constants.js +4 -0
  4. package/dist/conversation.d.ts +8 -0
  5. package/dist/conversation.js +96 -1
  6. package/dist/helpers.d.ts +2 -2
  7. package/dist/helpers.js +4 -9
  8. package/dist/index.d.ts +6 -23
  9. package/dist/index.js +91 -589
  10. package/dist/lifecycle/completion.d.ts +3 -0
  11. package/dist/lifecycle/completion.js +50 -0
  12. package/dist/lifecycle/index.d.ts +5 -0
  13. package/dist/lifecycle/index.js +5 -0
  14. package/dist/lifecycle/polling.d.ts +16 -0
  15. package/dist/lifecycle/polling.js +61 -0
  16. package/dist/lifecycle/restore.d.ts +2 -0
  17. package/dist/lifecycle/restore.js +34 -0
  18. package/dist/lifecycle/toolStats.d.ts +2 -0
  19. package/dist/lifecycle/toolStats.js +17 -0
  20. package/dist/lifecycle/widget.d.ts +8 -0
  21. package/dist/lifecycle/widget.js +75 -0
  22. package/dist/session-text.d.ts +9 -0
  23. package/dist/session-text.js +50 -0
  24. package/dist/subagent/runSdk.js +50 -26
  25. package/dist/subagent/tmux.d.ts +12 -9
  26. package/dist/subagent/tmux.js +107 -44
  27. package/dist/subagent/waitCompletion.d.ts +4 -5
  28. package/dist/subagent/waitCompletion.js +27 -43
  29. package/dist/tool/index.d.ts +5 -0
  30. package/dist/tool/index.js +5 -0
  31. package/dist/tool/prompt.d.ts +8 -0
  32. package/dist/tool/prompt.js +17 -0
  33. package/dist/tool/renderCall.d.ts +3 -0
  34. package/dist/tool/renderCall.js +12 -0
  35. package/dist/tool/renderResult.d.ts +8 -0
  36. package/dist/tool/renderResult.js +51 -0
  37. package/dist/tool/schema.d.ts +8 -0
  38. package/dist/tool/schema.js +24 -0
  39. package/dist/tool/taskComplete.d.ts +8 -0
  40. package/dist/tool/taskComplete.js +65 -0
  41. package/dist/types.d.ts +54 -0
  42. package/dist/types.js +1 -0
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,69 @@ All notable changes to `@heyhuynhgiabuu/pi-task` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.2.0] — 2026-06-25
8
+
9
+ ### Changed
10
+
11
+ - **Modular refactor of `src/`.** The single-file `index.ts` is now a thin
12
+ wiring layer; the implementation is split across focused modules:
13
+ - `src/tool/` — `renderCall`, `renderResult`, `taskComplete`, `prompt`,
14
+ `schema`.
15
+ - `src/lifecycle/` — `polling`, `completion`, `toolStats`, `widget`,
16
+ `restore`.
17
+ - `src/subagent/` — `buildArgv`, `runSdk`, `tmux`, `waitCompletion`.
18
+ - `src/conversation.ts` — `findJsonlSessionByName`, registry and
19
+ `task-session-history` helpers.
20
+ - `src/constants.ts` — `BACKGROUND_CHECK_MS`, `COUNT_POLL_MS`,
21
+ `TASK_TIMEOUT_MS`, `MAX_POLL_ERRORS`.
22
+ - `src/types.ts` — `BackgroundTask`, `RegistryEntry`,
23
+ `TaskSessionHistoryEntry`, `TaskDetails`.
24
+ - **Session JSONL is now the single source of truth for task results.**
25
+ `RESULT.md` is no longer read for completion detection or result text —
26
+ the final assistant message in `~/.pi/agent/sessions/.../<id>.jsonl`
27
+ is the authoritative result. This removes mid-write `EACCES` and
28
+ "stale truncated `RESULT.md`" failure modes entirely.
29
+ - **Completion detection is gated on `stopReason`.** `hasAgentFinished()`
30
+ in `src/session-text.ts` only treats an assistant message as final when
31
+ its `stopReason` is `stop`, `endTurn`, `length`, `error`, or `aborted`.
32
+ `toolUse` mid-turn streaming text is correctly ignored.
33
+ - **Background polling is hardened.**
34
+ - `checkInFlight` guard prevents overlapping poll ticks (no more
35
+ double-completion races on the `backgroundTasks` map).
36
+ - `MAX_POLL_ERRORS = 3` per-task counter absorbs transient filesystem
37
+ errors; a single rejected `readFile` no longer orphans a task.
38
+ - Try/catch around `checkTaskCompletion()` keeps the interval alive on
39
+ one-off failures.
40
+ - **Reordered completion check flow.** Session JSONL is consulted before
41
+ pane liveness, so `remain-on-exit` panes no longer block detection.
42
+
43
+ ### Added
44
+
45
+ - `renderCall` / `renderResult` / task-complete renderers with **Ctrl+O
46
+ expand/collapse** (via `keyHint("app.tools.expand")`) on the `task`
47
+ tool. Foreground results show stats + preview; expanded shows the full
48
+ result text. The keybinding hint falls back to `Ctrl+O` if the
49
+ `app.tools.expand` keybinding is not registered.
50
+ - **Foreground real-time tool-call progress.** The foreground `execute`
51
+ path now polls the session file and emits `_onUpdate` callbacks while
52
+ waiting, so the parent pane shows a live `${n} tool calls` count
53
+ alongside the spawned subagent pane.
54
+
55
+ ### Fixed
56
+
57
+ - The "scout - Description" / "scout — Description" duplicate header in
58
+ foreground results: `renderResult` no longer re-renders the header
59
+ that `renderCall` already rendered.
60
+ - The `( to expand)` empty-keybinding hint: now falls back to a plain
61
+ `Ctrl+O to expand` label when `keyText("app.tools.expand")` is empty.
62
+
63
+ ### Verified
64
+
65
+ - `npm run typecheck` passes
66
+ - `npm run build` passes
67
+ - `npm run smoke` passes
68
+ - `npm pack --dry-run` succeeds
69
+
7
70
  ## [0.1.6] — 2026-06-25
8
71
 
9
72
  ### Changed
@@ -164,6 +227,7 @@ See the git history: `git log --oneline -- CHANGELOG.md`.
164
227
  [0.1.1]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.1
165
228
  [0.1.4]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.4
166
229
  [0.1.5]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.5
167
- [0.1.6]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.6
168
- [Keep a Changelog]: https://keepachangelog.com/
169
- [Semantic Versioning]: https://semver.org/
230
+ [0.2.0]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.2.0
231
+ [0.1.6]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.6
232
+ [Keep a Changelog]: https://keepachangelog.com/
233
+ [Semantic Versioning]: https://semver.org/
@@ -0,0 +1,4 @@
1
+ export declare const BACKGROUND_CHECK_MS = 10000;
2
+ export declare const COUNT_POLL_MS = 3000;
3
+ export declare const TASK_TIMEOUT_MS: number;
4
+ export declare const MAX_POLL_ERRORS = 3;
@@ -0,0 +1,4 @@
1
+ export const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
2
+ export const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
3
+ export const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
4
+ export const MAX_POLL_ERRORS = 3; // consecutive poll failures before giving up on a task
@@ -1,3 +1,4 @@
1
+ import type { RegistryEntry, TaskSessionHistoryEntry } from "./types.js";
1
2
  /**
2
3
  * Conversational subagent helpers.
3
4
  *
@@ -31,6 +32,13 @@ export type TaskSessionsRegistry = Record<string, {
31
32
  }>;
32
33
  export declare function getTasksFilePath(piDir: string): string;
33
34
  export declare function getTaskSessionsRegistryPath(piDir: string): string;
35
+ export declare function readRegistry(piDir: string): RegistryEntry[];
36
+ export declare function writeRegistry(piDir: string, entries: RegistryEntry[]): void;
37
+ export declare function readTaskSessionHistory(piDir: string): TaskSessionHistoryEntry[];
38
+ export declare function writeTaskSessionHistory(piDir: string, entries: TaskSessionHistoryEntry[]): void;
39
+ export declare function upsertTaskSessionHistory(piDir: string, entry: TaskSessionHistoryEntry): void;
40
+ export declare function findTaskSessionHistory(piDir: string, idOrSessionName: string): TaskSessionHistoryEntry | undefined;
41
+ export declare function findJsonlSessionByName(piDir: string, sessionName: string, agentType: string): TaskSessionHistoryEntry | undefined;
34
42
  export declare function normalizeConversationId(value: unknown): string | undefined;
35
43
  export declare function readTaskSessionsRegistry(piDir: string): TaskSessionsRegistry;
36
44
  export declare function writeTaskSessionsRegistry(piDir: string, registry: TaskSessionsRegistry): void;
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  /**
4
4
  * Conversational subagent helpers.
@@ -24,6 +24,101 @@ export function getTasksFilePath(piDir) {
24
24
  export function getTaskSessionsRegistryPath(piDir) {
25
25
  return join(piDir, "artifacts", TASK_SESSIONS_REGISTRY_FILE);
26
26
  }
27
+ export function readRegistry(piDir) {
28
+ const path = join(piDir, "task-registry.json");
29
+ try {
30
+ return JSON.parse(readFileSync(path, "utf-8"));
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
36
+ export function writeRegistry(piDir, entries) {
37
+ const path = join(piDir, "task-registry.json");
38
+ writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
39
+ }
40
+ export function readTaskSessionHistory(piDir) {
41
+ const path = join(piDir, "task-session-history.json");
42
+ try {
43
+ return JSON.parse(readFileSync(path, "utf-8"));
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
49
+ export function writeTaskSessionHistory(piDir, entries) {
50
+ const path = join(piDir, "task-session-history.json");
51
+ writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
52
+ }
53
+ export function upsertTaskSessionHistory(piDir, entry) {
54
+ const entries = readTaskSessionHistory(piDir);
55
+ const index = entries.findIndex((existing) => existing.id === entry.id);
56
+ if (index >= 0) {
57
+ entries[index] = { ...entries[index], ...entry };
58
+ }
59
+ else {
60
+ entries.push(entry);
61
+ }
62
+ writeTaskSessionHistory(piDir, entries);
63
+ }
64
+ export function findTaskSessionHistory(piDir, idOrSessionName) {
65
+ return readTaskSessionHistory(piDir).find((entry) => entry.id === idOrSessionName || entry.sessionName === idOrSessionName);
66
+ }
67
+ export function findJsonlSessionByName(piDir, sessionName, agentType) {
68
+ const artifactsDir = join(piDir, "artifacts");
69
+ const sessionDir = join(artifactsDir, "sessions");
70
+ try {
71
+ if (!existsSync(sessionDir))
72
+ return undefined;
73
+ const files = readdirSync(sessionDir)
74
+ .filter((file) => file.endsWith(".jsonl"))
75
+ .sort()
76
+ .reverse();
77
+ for (const file of files) {
78
+ const content = readFileSync(join(sessionDir, file), "utf-8");
79
+ let startedAt = Date.now();
80
+ for (const rawLine of content.split("\n")) {
81
+ const line = rawLine.trim();
82
+ if (!line)
83
+ continue;
84
+ try {
85
+ const entry = JSON.parse(line);
86
+ if (entry.type === "session" && entry.timestamp) {
87
+ const parsed = Date.parse(entry.timestamp);
88
+ if (Number.isFinite(parsed))
89
+ startedAt = parsed;
90
+ }
91
+ if (entry.type === "session_info") {
92
+ const name = entry.name ?? entry.session_info?.name;
93
+ if (name === sessionName) {
94
+ return {
95
+ id: sessionName,
96
+ agentType,
97
+ description: `Resumed session ${sessionName}`,
98
+ sessionName,
99
+ sessionRef: join(sessionDir, file),
100
+ startedAt,
101
+ piDir,
102
+ dir: artifactsDir,
103
+ conversationId: sessionName,
104
+ status: "done",
105
+ background: false,
106
+ };
107
+ }
108
+ break;
109
+ }
110
+ }
111
+ catch {
112
+ // Skip malformed lines.
113
+ }
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ return undefined;
121
+ }
27
122
  export function normalizeConversationId(value) {
28
123
  if (typeof value !== "string")
29
124
  return undefined;
package/dist/helpers.d.ts CHANGED
@@ -40,8 +40,8 @@ export interface ToolCallRecord {
40
40
  id: string;
41
41
  }
42
42
  export declare const TASK_BACKGROUND_DEFAULT = true;
43
- export declare const TASK_RESULT_XML_INSTRUCTIONS = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
44
- export declare const OUTPUT_FORMAT_GUIDE = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
43
+ export declare const TASK_PROMPT_INSTRUCTIONS = "Your final assistant message IS the result the parent agent will read.\n\nWhen you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file \u2014 the parent agent reads your final assistant message from the session JSONL, not from any file.";
44
+ export declare const OUTPUT_FORMAT_GUIDE = "Your final assistant message IS the result the parent agent will read.\n\nWhen you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file \u2014 the parent agent reads your final assistant message from the session JSONL, not from any file.";
45
45
  export declare const TASK_TOOL_DESCRIPTION = "Launch a new agent to handle complex, multistep tasks autonomously.\n\nInclude relevant context from your current work in the prompt parameter \u2014\nthis becomes the subagent's instructions. The subagent knows nothing about what you've been doing except what you put in the prompt.\n\nWhen NOT to use:\n- To read a specific file path, use Read or Grep instead\n- To search for a class definition like 'class Foo', use Grep instead\n- To search code within 2-3 files, use Read instead\n- If no available agent fits the task, use other tools directly\n\nUsage notes:\n1. Provide complete context in the prompt \u2014 the subagent starts with a fresh context\n2. Launch multiple agents concurrently when possible (use a single message with multiple tool calls)\n3. Once you delegate work, do NOT duplicate it. Continue with non-overlapping tasks, or wait for the result\n4. Background is the default. Use background:false only when you need the caller to wait inline for the tmux task result\n5. Do not trust delegated output blindly. Read changed files, review the diff, verify scope, and run the relevant checks before claiming completion\n6. Clearly tell the agent whether to write code or just research, since it doesn't know the user's intent\n7. The result returned by the agent is not visible to the user. Send a concise summary back to the user\n8. Pass task_id to resume a previous subagent session (continues with its prior context)\n\nBackground mode (background: true):\n- Launches the subagent asynchronously and returns immediately\n- You will be notified automatically when it finishes\n- DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background\n- Avoid working with the same files or topics the background task is using\n- Work on non-overlapping tasks, or briefly tell the user what you launched and end your response";
46
46
  /** @deprecated Import from ./agent-tools.js */
47
47
  export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
package/dist/helpers.js CHANGED
@@ -32,15 +32,10 @@ export function parseMarkdownFrontmatter(content) {
32
32
  }
33
33
  // ─── Constants ───────────────────────────────────────────────────────────────
34
34
  export const TASK_BACKGROUND_DEFAULT = true;
35
- export const TASK_RESULT_XML_INSTRUCTIONS = `<status>success|failure|blocked|partial</status>
36
- <summary>One sentence: what was accomplished</summary>
37
- <findings>Key findings with file:line references</findings>
38
- <evidence>Verification evidence, commands run, output snippets</evidence>
39
- <confidence>high|medium|low (optional — how certain the findings are)</confidence>
40
- <files>Comma-separated absolute paths of files read/created (optional)</files>
35
+ export const TASK_PROMPT_INSTRUCTIONS = `Your final assistant message IS the result the parent agent will read.
41
36
 
42
- Prefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.`;
43
- export const OUTPUT_FORMAT_GUIDE = TASK_RESULT_XML_INSTRUCTIONS;
37
+ When you are done, end your final assistant message with a clear, self-contained summary in plain text. Do not wrap it in XML tags. Do not write a RESULT.md file — the parent agent reads your final assistant message from the session JSONL, not from any file.`;
38
+ export const OUTPUT_FORMAT_GUIDE = TASK_PROMPT_INSTRUCTIONS;
44
39
  export const TASK_TOOL_DESCRIPTION = `Launch a new agent to handle complex, multistep tasks autonomously.
45
40
 
46
41
  Include relevant context from your current work in the prompt parameter —
@@ -89,7 +84,7 @@ export function parseResultXml(raw) {
89
84
  !extractTag(raw, EVIDENCE_RE)) {
90
85
  return {
91
86
  status: "unknown",
92
- summary: raw.slice(0, 500),
87
+ summary: raw.trim(),
93
88
  findings: "",
94
89
  evidence: "",
95
90
  confidence: "",
package/dist/index.d.ts CHANGED
@@ -1,35 +1,18 @@
1
1
  /**
2
2
  * Task Tool — Delegate complex work to specialist agents.
3
3
  *
4
- * Spawns pi CLI in a tmux split pane (so you can watch it live) and
5
- * detects completion via RESULT.md polling. On completion, tool call
6
- * count and duration are reported as a notification.
4
+ * Spawns pi CLI in a tmux split pane (foreground) or background.
5
+ * Completion is detected from the subagent's final assistant message
6
+ * in the persistent session JSONL (stopReason gating). The final message
7
+ * is the authoritative result; no RESULT.md is used.
7
8
  *
8
9
  * Three agent sources:
9
10
  * - .pi/agents/*.md project-local agents
10
11
  * - ~/.pi/agent/agents/*.md user-global agents (fallback)
11
12
  *
12
13
  * P0: Persistent task registry (appendEntry + JSON), --session resume,
13
- * sendMessage completion notification.
14
- * P1: Foreground mode (background:false, inline subprocess), pane death
15
- * detection, 30-minute timeout.
14
+ * sendMessage completion notification, Ctrl+O expand/collapse.
15
+ * P1: Foreground mode (background:false), pane death detection, timeout.
16
16
  */
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
- export /** Details attached to tool result for rendering. */ interface TaskDetails {
19
- task_id: string;
20
- agent_type: string;
21
- description: string;
22
- conversation_id?: string;
23
- phase: "done" | "timeout" | "aborted" | "failed";
24
- status?: string;
25
- summary?: string;
26
- findings?: string;
27
- evidence?: string;
28
- confidence?: string;
29
- duration_ms?: number;
30
- turn_count?: number;
31
- tool_uses?: number;
32
- background?: boolean;
33
- tmux_session?: string;
34
- }
35
18
  export default function (pi: ExtensionAPI): void;