@glrs-dev/cli 2.2.0 → 2.3.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 (28) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/{chunk-SB3MLROC.js → chunk-MIWZLETC.js} +7 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/auto-update.js +1 -1
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
  8. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
  9. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +97 -7
  10. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
  11. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
  12. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
  13. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
  14. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +69 -45
  15. package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +259 -0
  16. package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +87 -0
  17. package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +544 -0
  18. package/dist/vendor/harness-opencode/dist/cli.js +448 -503
  19. package/dist/vendor/harness-opencode/dist/index.js +90 -14
  20. package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +30 -0
  21. package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +22 -0
  22. package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +6 -0
  23. package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +117 -0
  24. package/dist/vendor/harness-opencode/dist/scoper-S77SOK7X.js +326 -0
  25. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
  26. package/dist/vendor/harness-opencode/package.json +1 -1
  27. package/package.json +3 -1
  28. package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
@@ -2,79 +2,103 @@
2
2
  description: Self-driving PRIME run. Accepts an issue-tracker reference, a free-form task description, or a question.
3
3
  ---
4
4
 
5
- You are running in autopilot mode. The user invoked `/autopilot` to hand off a task for lights-out execution. Work through the normal SPEAR workflow until all work described in the prompt is complete, then emit `<autopilot-done>` as the **first token** of your final message.
5
+ You are running in autopilot mode. The user is reviewing your output **asynchronously** not during the run, but after it. Every decision you make becomes either a commit the user can `git blame`, a bullet in the plan's `## Open questions`, or a line in the autopilot log. Nothing you do blocks — everything you do is observable.
6
6
 
7
- **Sentinel contract.** When ALL work described in the user's prompt is genuinely complete (plan executed, Resolve stage done, PR open), emit `<autopilot-done>` at the very start of your final message. The Ralph loop driver watches for this tag to know when to stop. Do NOT emit `<autopilot-done>` prematurely only when you have truly finished everything the prompt asked for.
7
+ This changes your default behavior in exactly one way, and you must internalize it: **do not ask the user anything.** Not via the `question` tool, not via chat prose, not by any means. The `question` tool will abort your session if invoked the Ralph loop driver terminates any session that emits a `question.asked` event. You cannot recover from that; plan around it.
8
8
 
9
- **Single-shot TUI path.** When invoked from the TUI (not the CLI driver), there is no external loop watching for the sentinel. Run the SPEAR workflow once to completion. The sentinel is harmless in this context — emit it anyway so the output is consistent.
9
+ ## Replace questions with decisions
10
10
 
11
- **Kill switch.** If `.agent/autopilot-disable` exists in the worktree, the CLI driver will have already stopped before sending this prompt. No action needed from you.
11
+ For every situation where the interactive PRIME would ask a question, take the specific action below instead. These are not suggestions they are your defaults in autopilot mode:
12
12
 
13
- The user wants autopilot to process: $ARGUMENTS
13
+ | Normal-PRIME behavior | Autopilot replacement |
14
+ |---|---|
15
+ | Frame confirmation on low-confidence Scope | Announce the frame as `→ Frame:` and proceed. If wrong, the user corrects after the run. |
16
+ | Two-stage Assess fork (spec vs. code) | Always run spec-reviewer first, then code-reviewer on `[PASS_SPEC]`. Never ask which variant. |
17
+ | Workflow-mechanics (branch / worktree / isolation) | Apply the deterministic heuristic from `prime.md` § `Workflow-mechanics decisions`. Announce in one line of chat. |
18
+ | STOP-with-reorganization proposal | Write the proposal to the plan's `## Open questions` as a bullet, mark relevant acceptance boxes with `[ ]` and a note, emit `STOP: <reason>` and stop. The user resolves at the next run. |
19
+ | Ambiguous input interpretation | Pick the most plausible interpretation. Record your reading in the plan's `## Goal` so the user can see what you decided. |
20
+ | Scope-expansion check (> 2 files outside plan) | Expand silently if the expansion is mechanically obvious (test files for the new code, AGENTS.md updates in touched directories). Otherwise STOP with a bullet in `## Open questions`. |
21
+ | Plan-reviewer rejection on a judgment call | 1st reject: fix. 2nd reject: narrow scope, move disputed items to `## Out of scope`. 3rd reject: emit `STOP: plan-reviewer disagreement unresolvable; needs human input`. Never ask the user. |
22
+ | Merge-conflict / rebase resolution | STOP with `STOP: merge conflict in <file>; needs human review`. Do not attempt. |
14
23
 
15
- ## 0. Workflow-mechanics: decide before anything else
24
+ **The meta-rule: if the interactive PRIME would use `question`, write to the plan file instead.** Plans are the artifact the user reads after the run — that's where deferred decisions belong.
16
25
 
17
- Before classifying the argument, apply the workflow-mechanics heuristic from `prime.md` § `# Workflow-mechanics decisions`. Autopilot is lights-out: the rule fires automatically and silently (single line of chat, no `question` tool). Never ask the user whether to open a fresh worktree, switch branches, or stack on current — the heuristic decides.
26
+ ## Sentinel contract
18
27
 
19
- Abort paths (dirty tree on default branch; dirty tree on feature branch with unrelated work) mean STOP and report the one-sentence reason. The user resolves and re-runs.
28
+ When all work in the user's prompt is complete (plan executed, Resolve stage done, PR open), emit `<autopilot-done>` as the **first token** of your final message. The Ralph loop watches for this to stop. Emit it only when truly finished — not when you think you're close, not when one iteration's acceptance criteria are met, not as a "checkpoint." Premature `<autopilot-done>` ends the whole run.
20
29
 
21
- If you auto-invoke `/fresh`, do NOT pass `--clean`. Cleanup stays user-triggered.
30
+ If the loop is structurally stuck (dirty tree on default branch; merge conflict; un-tickable AC; missing upstream work), emit `STOP: <one-sentence reason>` instead. The loop logs it and exits.
22
31
 
23
- ## 1. Classify the argument
32
+ When invoked from the TUI (not the CLI driver), there is no external loop. Run SPEAR once to completion. Emit `<autopilot-done>` anyway for output consistency.
24
33
 
25
- Pick ONE of these paths:
34
+ ## Kill switch
26
35
 
27
- - **Issue-tracker reference** (single issue) match any of:
28
- - `<PROJECT>-<NUMBER>` where PROJECT is 2–10 uppercase letters (e.g. `ENG-1234`, `GEN-1114`) — Linear, Jira, YouTrack, Shortcut, etc.
29
- - `#<NUMBER>` alone (e.g. `#1234`) — GitHub shorthand
30
- - A URL to a recognized tracker (`github.com/.../issues/123`, `linear.app/.../issue/...`, `*.atlassian.net/browse/...`)
31
- - **Free-form task description** — any natural-language request that isn't a recognized issue ref
32
- - **Question** — starts with what/why/how/when/where/which/who, or ends with `?`
36
+ If `.agent/autopilot-disable` exists in the worktree, the CLI driver has already stopped before sending this prompt. No action needed.
33
37
 
34
- ## 2. Fetch issue content (only if step 1 returned an issue ref)
38
+ ## The user's request
35
39
 
36
- Probe in order, stop at the first that returns real content:
40
+ $ARGUMENTS
37
41
 
38
- 1. **Linear MCP** — if configured and the arg matches `<PROJECT>-<NUMBER>` shape OR is a `linear.app` URL: `linear_get_issue`.
39
- 2. **GitHub MCP** — if configured OR the arg is a `github.com/.../issues/...` URL OR is `#<NUMBER>` and `gh` CLI is available.
40
- 3. **Jira / Atlassian MCP** — if configured and the arg matches `<PROJECT>-<NUMBER>` OR is an `*.atlassian.net` URL.
42
+ ## Workflow
41
43
 
42
- If no probe resolves, report once: *"I see a ref that looks like a ticket (`<arg>`), but no issue-tracker MCP is configured. Treating as free-form — paste the issue body if you want me to ground in it."* Then proceed as free-form.
44
+ ### 0. Workflow-mechanics first
43
45
 
44
- Treat the fetched issue's title + description + acceptance criteria as the intent baseline. Map to the plan's `## Acceptance criteria` 1:1, in order. Do not invent entries.
46
+ Before classifying the argument, apply the heuristic from `prime.md` § `Workflow-mechanics decisions`. Announce the result in one line of chat prefixed with `→ Workflow:`. No `question` tool, no notification.
45
47
 
46
- ## 3. Run the PRIME arc
48
+ Abort paths (dirty tree on default branch; dirty tree on feature branch with unrelated work) mean emit `STOP: <reason>` and exit.
47
49
 
48
- Run the normal SPEAR workflow from `prime.md`. Key adaptations for autopilot mode:
50
+ If you auto-invoke `/fresh`, do NOT pass `--clean` cleanup stays user-triggered.
49
51
 
50
- - **Scope.** Already classified; skip redundant classification. Announce the frame as `→ Frame:` and proceed — do NOT use the `question` tool to confirm. The user is walked away.
51
- - **Plan.** Delegate to `@plan`. For ref-originated requests, cite the issue ID in the plan's `## Goal`. The plan's `## Acceptance criteria` maps 1:1 to the ticket's Changes / Definition of Done list.
52
- - **Execute.** Delegate to `@build`. `@build` executes file-by-file and returns a summary; PRIME relays progress. Acceptance boxes get checked during `@build`'s execution.
53
- - **Assess.** Full suite pass + `@spec-reviewer` → `@code-reviewer` → iterate to `[PASS]`. No sentinel tokens during intermediate steps.
54
- - **Resolve.** Complete the Resolve stage: push branch, open PR via `gh pr create`, print PR URL.
55
- - **Multi-issue workflows.** If the prompt describes multiple issues, use `/fresh` between issues to isolate each on its own branch. Complete each issue's full SPEAR arc (including Resolve) before moving to the next.
52
+ ### 1. Classify the argument
56
53
 
57
- ## 4. Guardrails
54
+ Pick ONE:
58
55
 
59
- - **Never ask scoping questions.** The issue's acceptance list IS the authoritative scope. If you're tempted to ask whether to include X, the answer is: if the ticket didn't ask for it, don't include it. The `question` tool is forbidden in autopilot mode except for one narrow case: an architectural fork that blocks all progress AFTER codebase inspection, `@gap-analyzer` consultation, and precedent search (`git log`) have ALL failed to determine a default.
60
- - **Precedent defaults.** For helper-file location, naming, logging verbosity, error-wrapper style: search `git log` for a recent similar PR and mirror its structure. Cite the precedent commit in `## Constraints`.
61
- - **Plan-revision budget.** After `@plan-reviewer` returns `[REJECT]`: 1st REJECT fix listed issues, resubmit. 2nd REJECT narrow scope (move disputed items to `## Out of scope`). 3rd REJECT → escalate to `@architecture-advisor`.
62
- - **Resolve auto-ships.** When Assess returns `[PASS]`, complete the Resolve stage: push branch, open PR via `gh pr create`, print the PR URL, then stop. Do NOT re-invoke `/ship` — Resolve already did the work. `/ship` exists only as a manual resume path for interrupted sessions.
63
- - **Hard rules from Resolve still apply.** Never `--force`-push, never push to `main`/`master`, never `--no-verify`, never merge the PR yourself. Resolve only pushes the feature branch and opens the PR; the human gate is PR review and merge.
64
- - **Circular failure.** If the same test fails after the same fix twice, delegate to `@architecture-advisor` before a third attempt.
65
- - **STOP when stuck, don't churn.** If the plan is structurally wrong for this session (wrong branch, un-tickable AC, missing upstream work), emit a single line starting with `STOP:` followed by the specific reason. Do not re-attempt.
56
+ - **Issue-tracker reference** (single issue) `<PROJECT>-<NUMBER>` (2-10 uppercase letters, e.g. `ENG-1234`), `#<NUMBER>`, or a URL to a recognized tracker (`github.com/.../issues/N`, `linear.app/.../issue/...`, `*.atlassian.net/browse/...`).
57
+ - **Free-form task description** any natural-language request that isn't a recognized issue ref.
58
+ - **Question** starts with what/why/how/when/where/which/who, or ends with `?`. For questions, answer in a single assistant message then emit `<autopilot-done>`. Do not enter SPEAR.
66
59
 
67
- ## 5. Completion
60
+ ### 2. Fetch issue content (issue refs only)
68
61
 
69
- When ALL work described in the prompt is complete (every issue resolved, every PR open), emit `<autopilot-done>` as the first token of your final message, followed by a brief summary:
62
+ Probe in order, stop at the first with content:
63
+
64
+ 1. **Linear MCP** — for `<PROJECT>-<NUMBER>` or `linear.app` URL: `linear_get_issue`.
65
+ 2. **GitHub MCP** — for `#<NUMBER>` with `gh` available, or a `github.com/.../issues/...` URL.
66
+ 3. **Jira / Atlassian MCP** — for `<PROJECT>-<NUMBER>` or `*.atlassian.net` URL.
67
+
68
+ If no probe resolves, emit `STOP: ticket ref "<arg>" but no MCP configured; paste the issue body or use free-form` and exit. Do not guess at issue content.
69
+
70
+ Treat the fetched title + description + acceptance criteria as the intent baseline. Map the plan's `## Acceptance criteria` 1:1 to the ticket, in order.
71
+
72
+ ### 3. Run the SPEAR arc
73
+
74
+ Normal SPEAR per `prime.md`, with these autopilot substitutions:
75
+
76
+ - **Scope.** Argument already classified. Write the frame as `→ Frame:` and proceed. No confirmation.
77
+ - **Plan.** Delegate to `@plan`. For ref-originated requests, cite the issue ID in the plan's `## Goal`.
78
+ - **Execute.** Delegate to `@build`. Do not invoke Assess yourself during Execute — that's Phase 4's job.
79
+ - **Assess.** Always dispatch `@spec-reviewer` first. On `[PASS_SPEC]`, dispatch `@code-reviewer` (or `@code-reviewer-thorough` if the diff meets the thorough thresholds). Iterate to `[PASS]`. Never prompt the user between rounds.
80
+ - **Resolve.** When Assess returns `[PASS]`, push the branch and open the PR via `gh pr create`. Print the PR URL. Resolve auto-ships — do not invoke `/ship` yourself; `/ship` exists only as a manual resume path.
81
+
82
+ For multi-issue prompts: use `/fresh` between issues to isolate each on its own branch. Complete each issue's full SPEAR arc (including Resolve) before starting the next.
83
+
84
+ ### 4. Guardrails (beyond "no questions")
85
+
86
+ - **Precedent defaults.** For helper-file location, naming conventions, logging verbosity, error-wrapper style: `git log` for a recent similar PR and mirror. Cite the precedent commit in the plan's `## Constraints`.
87
+ - **Hard rules from Resolve still apply.** Never `--force`-push. Never push to `main`/`master`. Never `--no-verify`. Never merge the PR yourself. Resolve pushes the feature branch and opens the PR; human gate is review + merge.
88
+ - **Circular failure.** If the same test fails after the same fix twice, delegate to `@architecture-advisor` before a third attempt. Do not churn.
89
+ - **STOP when stuck, don't churn.** Structurally stuck (wrong branch, un-tickable AC, missing upstream work) → emit `STOP: <reason>` and exit.
90
+
91
+ ### 5. Completion
92
+
93
+ When all work is done, emit:
70
94
 
71
95
  ```
72
96
  <autopilot-done>
73
97
 
74
- Done. <One-sentence summary of what was built.>
98
+ Done. <One-sentence summary.>
75
99
  PR(s): <url(s)>
76
100
  ```
77
101
 
78
- If Resolve failed or was interrupted, report the failure and the resume command: `/ship <plan-path>`.
102
+ If Resolve failed or was interrupted, report the failure and suggest `/ship <plan-path>` as the resume command.
79
103
 
80
- If you stopped early due to a structural block, emit `STOP: <reason>` instead of `<autopilot-done>`.
104
+ If you stopped early due to a structural block, emit `STOP: <reason>` instead do not emit `<autopilot-done>`.
@@ -0,0 +1,259 @@
1
+ // src/lib/opencode-server.ts
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import {
5
+ createOpencodeServer,
6
+ createOpencodeClient
7
+ } from "@opencode-ai/sdk";
8
+ var execFileP = promisify(execFile);
9
+ var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
10
+ async function ensureOpencodeOnPath() {
11
+ try {
12
+ await execFileP("opencode", ["--version"]);
13
+ } catch {
14
+ throw new Error(
15
+ "opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
16
+ );
17
+ }
18
+ }
19
+ async function startServer(opts) {
20
+ await ensureOpencodeOnPath();
21
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
22
+ const port = opts.port ?? 0;
23
+ const server = await createOpencodeServer({
24
+ port,
25
+ timeout: timeoutMs,
26
+ hostname: "127.0.0.1"
27
+ });
28
+ const client = createOpencodeClient({ baseUrl: server.url });
29
+ let shutdownCalled = false;
30
+ const shutdown = async () => {
31
+ if (shutdownCalled) return;
32
+ shutdownCalled = true;
33
+ try {
34
+ await server.close();
35
+ } catch {
36
+ }
37
+ };
38
+ return { url: server.url, client, shutdown };
39
+ }
40
+ async function selfTest(client) {
41
+ try {
42
+ await client.session.list();
43
+ } catch (err) {
44
+ throw new Error(
45
+ `OpenCode server self-test failed \u2014 the server started but isn't responding to API calls.
46
+ Error: ${err instanceof Error ? err.message : String(err)}
47
+ Run \`opencode --version\` to verify your installation.`
48
+ );
49
+ }
50
+ }
51
+ async function createSession(client, opts) {
52
+ const result = await client.session.create({
53
+ query: { directory: opts.cwd },
54
+ body: {}
55
+ });
56
+ if (!result.data) {
57
+ throw new Error("session.create returned no data");
58
+ }
59
+ return result.data.id;
60
+ }
61
+ async function sendAndWait(client, opts) {
62
+ const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
63
+ const idlePromise = waitForIdle(client, {
64
+ sessionId: opts.sessionId,
65
+ stallMs,
66
+ abortSignal: opts.abortSignal,
67
+ onToolCall: opts.onToolCall,
68
+ onTextDelta: opts.onTextDelta,
69
+ onCostUpdate: opts.onCostUpdate,
70
+ autoRejectPermissions: opts.autoRejectPermissions,
71
+ serverUrl: opts.serverUrl,
72
+ onPermissionRejected: opts.onPermissionRejected
73
+ });
74
+ await client.session.prompt({
75
+ path: { id: opts.sessionId },
76
+ body: {
77
+ parts: [{ type: "text", text: opts.message }],
78
+ ...opts.agentName ? { agent: opts.agentName } : {}
79
+ }
80
+ });
81
+ return idlePromise;
82
+ }
83
+ async function waitForIdle(client, opts) {
84
+ const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
85
+ const sse = await client.event.subscribe();
86
+ const reportedToolCalls = /* @__PURE__ */ new Set();
87
+ return new Promise((resolve) => {
88
+ let stallTimer = null;
89
+ let settled = false;
90
+ const settle = (result) => {
91
+ if (settled) return;
92
+ settled = true;
93
+ if (stallTimer) clearTimeout(stallTimer);
94
+ resolve(result);
95
+ };
96
+ const resetStall = () => {
97
+ if (stallTimer) clearTimeout(stallTimer);
98
+ stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
99
+ };
100
+ if (opts.abortSignal) {
101
+ if (opts.abortSignal.aborted) {
102
+ settle({ kind: "abort" });
103
+ return;
104
+ }
105
+ opts.abortSignal.addEventListener("abort", () => settle({ kind: "abort" }), { once: true });
106
+ }
107
+ resetStall();
108
+ (async () => {
109
+ try {
110
+ for await (const event of sse.stream) {
111
+ if (settled) break;
112
+ const ev = event;
113
+ const props = ev.properties ?? {};
114
+ const type = ev.type ?? "";
115
+ if (opts.onCostUpdate && type === "message.updated") {
116
+ const info = props["info"];
117
+ if (info && info.role === "assistant" && typeof info.cost === "number") {
118
+ resetStall();
119
+ try {
120
+ opts.onCostUpdate(
121
+ info.cost,
122
+ {
123
+ input: info.tokens?.input ?? 0,
124
+ output: info.tokens?.output ?? 0
125
+ }
126
+ );
127
+ } catch {
128
+ }
129
+ }
130
+ }
131
+ if (opts.onTextDelta && (type === "message.part.delta" || type === "message.part.updated")) {
132
+ const delta = props["delta"];
133
+ if (typeof delta === "string" && delta.length > 0) {
134
+ resetStall();
135
+ try {
136
+ opts.onTextDelta(delta.length);
137
+ } catch {
138
+ }
139
+ }
140
+ }
141
+ if (opts.onToolCall && type === "message.part.updated") {
142
+ const part = props["part"];
143
+ if (part && part.type === "tool" && part.sessionID === opts.sessionId && part.state?.status === "completed" && part.callID && !reportedToolCalls.has(part.callID)) {
144
+ reportedToolCalls.add(part.callID);
145
+ resetStall();
146
+ try {
147
+ opts.onToolCall(part.tool ?? "unknown");
148
+ } catch {
149
+ }
150
+ continue;
151
+ }
152
+ }
153
+ const eventSessionId = props["sessionID"];
154
+ if (opts.autoRejectPermissions && (type === "permission.updated" || type === "question.asked")) {
155
+ const permissionId = props["id"];
156
+ const permissionType = type === "question.asked" ? "question" : props["type"] ?? "unknown";
157
+ const permissionTitle = props["title"] ?? "";
158
+ if (opts.onPermissionRejected) {
159
+ try {
160
+ opts.onPermissionRejected({
161
+ id: permissionId ?? "unknown",
162
+ type: permissionType,
163
+ title: permissionTitle
164
+ });
165
+ } catch {
166
+ }
167
+ }
168
+ if (type === "permission.updated" && permissionId) {
169
+ (async () => {
170
+ try {
171
+ await client.postSessionIdPermissionsPermissionId({
172
+ path: { id: opts.sessionId, permissionID: permissionId },
173
+ body: { response: "reject" }
174
+ });
175
+ } catch {
176
+ }
177
+ })();
178
+ continue;
179
+ }
180
+ if (type === "question.asked") {
181
+ if (opts.serverUrl && permissionId) {
182
+ (async () => {
183
+ try {
184
+ await fetch(`${opts.serverUrl}/question/${permissionId}/reject`, {
185
+ method: "POST"
186
+ });
187
+ } catch {
188
+ }
189
+ })();
190
+ }
191
+ settle({
192
+ kind: "question_rejected",
193
+ title: permissionTitle
194
+ });
195
+ break;
196
+ }
197
+ }
198
+ if (eventSessionId !== opts.sessionId) continue;
199
+ resetStall();
200
+ if (type === "session.idle") {
201
+ settle({ kind: "idle" });
202
+ break;
203
+ }
204
+ if (type === "session.error") {
205
+ const msg = props["message"] ?? "session error";
206
+ settle({ kind: "error", message: msg });
207
+ break;
208
+ }
209
+ }
210
+ } catch (err) {
211
+ if (!settled) {
212
+ settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
213
+ }
214
+ }
215
+ })();
216
+ });
217
+ }
218
+ async function getSessionCost(client, sessionId) {
219
+ try {
220
+ const result = await client.session.messages({ path: { id: sessionId } });
221
+ if (!result.data) return 0;
222
+ const messages = result.data;
223
+ let total = 0;
224
+ for (const m of messages) {
225
+ if (m.info.role === "assistant" && typeof m.info.cost === "number") {
226
+ total += m.info.cost;
227
+ }
228
+ }
229
+ return total;
230
+ } catch {
231
+ return 0;
232
+ }
233
+ }
234
+ async function getLastAssistantMessage(client, sessionId) {
235
+ try {
236
+ const result = await client.session.messages({ path: { id: sessionId } });
237
+ if (!result.data) return "";
238
+ const messages = result.data;
239
+ const assistantMessages = messages.filter((m) => m.info.role === "assistant");
240
+ if (assistantMessages.length === 0) return "";
241
+ const last = assistantMessages[assistantMessages.length - 1];
242
+ if (!last) return "";
243
+ return last.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
244
+ } catch {
245
+ return "";
246
+ }
247
+ }
248
+
249
+ export {
250
+ execFileP,
251
+ DEFAULT_STARTUP_TIMEOUT_MS,
252
+ startServer,
253
+ selfTest,
254
+ createSession,
255
+ sendAndWait,
256
+ waitForIdle,
257
+ getSessionCost,
258
+ getLastAssistantMessage
259
+ };
@@ -0,0 +1,87 @@
1
+ // src/autopilot/plan-parser.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var DEGRADED = {
5
+ type: "single",
6
+ totalItems: 0,
7
+ checkedItems: 0,
8
+ phaseCount: 0,
9
+ phasesCompleted: 0,
10
+ phases: []
11
+ };
12
+ function countCheckboxes(content) {
13
+ let total = 0;
14
+ let checked = 0;
15
+ const checkboxRe = /^[ \t]*-\s+\[([ xX])\]/gm;
16
+ let match;
17
+ while ((match = checkboxRe.exec(content)) !== null) {
18
+ total++;
19
+ if (match[1] !== " ") {
20
+ checked++;
21
+ }
22
+ }
23
+ return { total, checked };
24
+ }
25
+ function parseSingleFile(filePath) {
26
+ const content = fs.readFileSync(filePath, "utf8");
27
+ return countCheckboxes(content);
28
+ }
29
+ function detectPhaseFiles(dir) {
30
+ const entries = fs.readdirSync(dir);
31
+ return entries.filter((f) => /^phase_\d+\.md$/.test(f)).sort((a, b) => {
32
+ const na = parseInt(a.replace(/[^0-9]/g, ""), 10);
33
+ const nb = parseInt(b.replace(/[^0-9]/g, ""), 10);
34
+ return na - nb;
35
+ });
36
+ }
37
+ function parseMultiFile(dir) {
38
+ const mainPath = path.join(dir, "main.md");
39
+ const mainContent = fs.readFileSync(mainPath, "utf8");
40
+ const mainCounts = countCheckboxes(mainContent);
41
+ const phaseFiles = detectPhaseFiles(dir);
42
+ const phases = [];
43
+ let phasesCompleted = 0;
44
+ for (const phaseFile of phaseFiles) {
45
+ const phasePath = path.join(dir, phaseFile);
46
+ const { total, checked } = parseSingleFile(phasePath);
47
+ phases.push({ file: phaseFile, totalItems: total, checkedItems: checked });
48
+ if (total > 0 && checked === total) {
49
+ phasesCompleted++;
50
+ }
51
+ }
52
+ return {
53
+ type: "multi",
54
+ totalItems: mainCounts.total,
55
+ checkedItems: mainCounts.checked,
56
+ phaseCount: phaseFiles.length,
57
+ phasesCompleted,
58
+ phases
59
+ };
60
+ }
61
+ function parsePlanState(planPath) {
62
+ try {
63
+ const stat = fs.statSync(planPath);
64
+ if (stat.isDirectory()) {
65
+ const mainPath = path.join(planPath, "main.md");
66
+ if (fs.existsSync(mainPath)) {
67
+ return parseMultiFile(planPath);
68
+ }
69
+ return { ...DEGRADED, type: "multi" };
70
+ }
71
+ const { total, checked } = parseSingleFile(planPath);
72
+ return {
73
+ type: "single",
74
+ totalItems: total,
75
+ checkedItems: checked,
76
+ phaseCount: 0,
77
+ phasesCompleted: 0,
78
+ phases: []
79
+ };
80
+ } catch {
81
+ return { ...DEGRADED };
82
+ }
83
+ }
84
+
85
+ export {
86
+ parsePlanState
87
+ };