@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.
- package/CHANGELOG.md +2 -0
- package/dist/{chunk-SB3MLROC.js → chunk-MIWZLETC.js} +7 -2
- package/dist/cli.js +1 -1
- package/dist/lib/auto-update.js +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +97 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +69 -45
- package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +259 -0
- package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +87 -0
- package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +544 -0
- package/dist/vendor/harness-opencode/dist/cli.js +448 -503
- package/dist/vendor/harness-opencode/dist/index.js +90 -14
- package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +30 -0
- package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +22 -0
- package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +6 -0
- package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +117 -0
- package/dist/vendor/harness-opencode/dist/scoper-S77SOK7X.js +326 -0
- package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +3 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
## Replace questions with decisions
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
## Sentinel contract
|
|
18
27
|
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
## Kill switch
|
|
26
35
|
|
|
27
|
-
|
|
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
|
-
##
|
|
38
|
+
## The user's request
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
$ARGUMENTS
|
|
37
41
|
|
|
38
|
-
|
|
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
|
-
|
|
44
|
+
### 0. Workflow-mechanics first
|
|
43
45
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
If you auto-invoke `/fresh`, do NOT pass `--clean` — cleanup stays user-triggered.
|
|
49
51
|
|
|
50
|
-
|
|
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
|
-
|
|
54
|
+
Pick ONE:
|
|
58
55
|
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
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
|
-
|
|
60
|
+
### 2. Fetch issue content (issue refs only)
|
|
68
61
|
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
};
|