@proagentstore/cli 0.4.42 → 0.4.44
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.
|
@@ -94,6 +94,70 @@ function prRef(segment) {
|
|
|
94
94
|
const num = segment.match(/\bgh\s+pr\s+(?:merge|create)\b(?:\s+-{1,2}\S+(?:=\S+)?)*\s+(\d+)\b/);
|
|
95
95
|
return num ? `#${num[1]}` : null;
|
|
96
96
|
}
|
|
97
|
+
/** A pull request URL anywhere in a command's own output. Anchored on a literal `http` prefix. */
|
|
98
|
+
const PR_URL = /https?:\/\/\S*?\/pull\/(\d+)/;
|
|
99
|
+
/**
|
|
100
|
+
* Flatten a `tool_result` block's content to text — the RAW result, not the display line.
|
|
101
|
+
*
|
|
102
|
+
* `headless.ts`'s `toolResult()` collapses whitespace and truncates to 240 characters for the
|
|
103
|
+
* transcript, which is right for a human-readable pane and wrong here: `gh pr create` prints its
|
|
104
|
+
* URL after whatever else the compound command wrote, so the one token that matters is exactly what
|
|
105
|
+
* a 240-character cap would drop.
|
|
106
|
+
*/
|
|
107
|
+
export function resultText(content) {
|
|
108
|
+
if (typeof content === "string")
|
|
109
|
+
return content;
|
|
110
|
+
if (Array.isArray(content)) {
|
|
111
|
+
return content
|
|
112
|
+
.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text ?? "") : ""))
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
/** An act that names a pull request but does not yet say WHICH one. */
|
|
118
|
+
function wantsPrNumber(act) {
|
|
119
|
+
return (act.kind === "pr.open" || act.kind === "pr.merge") && act.target === null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fill in the PR number from the command's OWN output, for the acts that lack one (#417).
|
|
123
|
+
*
|
|
124
|
+
* `gh pr create --fill` is the common form and carries no number on the command line, so
|
|
125
|
+
* {@link prRef} returns null and `pull-attribution.ts` leaves the row unbadged. That file states the
|
|
126
|
+
* rule — **ATTRIBUTION IS EXACT OR ABSENT** — and rejects the tempting repair of pairing an
|
|
127
|
+
* unnumbered `pr.open` with whichever PR appeared around the same time, because a badge that is
|
|
128
|
+
* right most of the time is worse than no badge.
|
|
129
|
+
*
|
|
130
|
+
* This is not that repair. `gh pr create` prints the new PR's URL on stdout, and that output comes
|
|
131
|
+
* back as the `tool_result` for the SAME `tool_use_id` as the command that was classified. It is the
|
|
132
|
+
* command's own answer, in the same class of fact as the `ok` flag already taken from that block —
|
|
133
|
+
* not a temporal guess about what appeared nearby. When the output names no PR the target stays
|
|
134
|
+
* null, so absent stays absent.
|
|
135
|
+
*
|
|
136
|
+
* Deliberately narrow, because the one failure this module exists to avoid is a confident wrong
|
|
137
|
+
* number: only `pr.open`/`pr.merge` are eligible, only when their target is still null, and only
|
|
138
|
+
* against the result correlated to them by `tool_use_id`. A PR URL quoted incidentally in some other
|
|
139
|
+
* command's output reaches no act.
|
|
140
|
+
*
|
|
141
|
+
* **The "already exists" case is decided, not overlooked.** `gh pr create` on a branch that already
|
|
142
|
+
* has a PR fails with "a pull request for branch X already exists:" and that PR's URL, and this
|
|
143
|
+
* attributes the act to it. That is a judgement: it IS the pull request for this branch and the
|
|
144
|
+
* agent did just act on it, and dropping a real signal to avoid a case where the answer is still
|
|
145
|
+
* true costs more than it saves. The act still carries `ok: false`, so the record remains honest
|
|
146
|
+
* that the command failed.
|
|
147
|
+
*
|
|
148
|
+
* Only reachable from the structured stream-json path. A Codex/Grok raw spawn has no
|
|
149
|
+
* `tool_use`/`tool_result` framing at all, so its PRs stay unattributed — regexing its transcript
|
|
150
|
+
* instead would reintroduce exactly the temporal guess this design refuses.
|
|
151
|
+
*/
|
|
152
|
+
export function fillTargetFromResult(acts, content) {
|
|
153
|
+
if (!acts.some(wantsPrNumber))
|
|
154
|
+
return acts;
|
|
155
|
+
const m = resultText(content).match(PR_URL);
|
|
156
|
+
if (!m)
|
|
157
|
+
return acts;
|
|
158
|
+
const target = `#${m[1]}`;
|
|
159
|
+
return acts.map((a) => (wantsPrNumber(a) ? { ...a, target } : a));
|
|
160
|
+
}
|
|
97
161
|
function pushTarget(segment) {
|
|
98
162
|
// `git push [flags] <remote> <refspec>` — take the first two non-flag words after `push`.
|
|
99
163
|
const after = segment.split(/\bgit\s+push\b/)[1] ?? "";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { classifyCommand, commandFromToolInput } from "./engine-acts.js";
|
|
2
|
+
import { classifyCommand, commandFromToolInput, fillTargetFromResult } from "./engine-acts.js";
|
|
3
3
|
import { parseEngineUsage } from "./engine-usage.js";
|
|
4
4
|
/**
|
|
5
5
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
@@ -154,10 +154,26 @@ export class HeadlessSession {
|
|
|
154
154
|
get authResolved() {
|
|
155
155
|
return resolveEngineAuth(this.config.clientType, mergeEnv(process.env, this.config.env));
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Did this engine launch with a conversation to continue (#408)?
|
|
159
|
+
*
|
|
160
|
+
* Reported back to the cloud by `/coding/start` so the sentence the agent says to the user is
|
|
161
|
+
* something this side CONFIRMED rather than something the cloud asked for. The distinction is
|
|
162
|
+
* not academic: a runner published before #408 ignores `resumeFrom` entirely and always starts
|
|
163
|
+
* clean, so a cloud that announced "resumed where we left off" on its own intent would be
|
|
164
|
+
* telling most of the fleet's users the opposite of what happened.
|
|
165
|
+
*
|
|
166
|
+
* False for a raw (non-Claude) engine under every circumstance — `--resume` is a Claude Code
|
|
167
|
+
* flag and {@link buildClaudeArgs} is only reached in stream-json mode.
|
|
168
|
+
*/
|
|
169
|
+
get resumedConversation() {
|
|
170
|
+
return this.mode === "stream-json" && this.claudeSessionId !== null;
|
|
171
|
+
}
|
|
157
172
|
constructor(config) {
|
|
158
173
|
this.config = config;
|
|
159
174
|
this.engineLabel = `${config.clientType}:${config.id}`;
|
|
160
|
-
|
|
175
|
+
// Our own key first, the cloud's nominated predecessor second. See `resumeFrom`.
|
|
176
|
+
this.claudeSessionId = readState(config.statePath, config.id) ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom) : null);
|
|
161
177
|
// Claude is the structured engine; everything else is a raw CLI.
|
|
162
178
|
this.mode = config.clientType === "claude" ? "stream-json" : "raw";
|
|
163
179
|
const { bin, args } = parseCommand(config.command);
|
|
@@ -604,7 +620,19 @@ export class HeadlessSession {
|
|
|
604
620
|
return;
|
|
605
621
|
this.awaitingResult.set(toolUseId, acts);
|
|
606
622
|
}
|
|
607
|
-
/**
|
|
623
|
+
/**
|
|
624
|
+
* The matching `tool_result` arrived — stamp the outcome and publish.
|
|
625
|
+
*
|
|
626
|
+
* The result carries more than the outcome: `gh pr create --fill` states its PR number nowhere
|
|
627
|
+
* but its own stdout, so an unnumbered `pr.open`/`pr.merge` takes it from here (#417). It is read
|
|
628
|
+
* from the RAW `block.content`, never from `toolResult()`'s display line — that truncates to 240
|
|
629
|
+
* characters for the transcript and would cut the URL off a verbose result.
|
|
630
|
+
*
|
|
631
|
+
* This path (and `noteAct`) is reachable ONLY from the structured stream-json handling above
|
|
632
|
+
* (`assistant` → `tool_use`, `user` → `tool_result`). A Codex/Grok session is a raw spawn with no
|
|
633
|
+
* such framing, so its PRs stay unattributed by construction; scraping its transcript instead
|
|
634
|
+
* would be the temporal guess `pull-attribution.ts` refuses.
|
|
635
|
+
*/
|
|
608
636
|
settleAct(block) {
|
|
609
637
|
const id = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
|
|
610
638
|
const acts = this.awaitingResult.get(id);
|
|
@@ -612,7 +640,7 @@ export class HeadlessSession {
|
|
|
612
640
|
return;
|
|
613
641
|
this.awaitingResult.delete(id);
|
|
614
642
|
const ok = block.is_error !== true;
|
|
615
|
-
for (const a of acts)
|
|
643
|
+
for (const a of fillTargetFromResult(acts, block.content))
|
|
616
644
|
this.publishAct({ ...a, ok });
|
|
617
645
|
}
|
|
618
646
|
/** Publish everything still waiting, with an unknown outcome. */
|
|
@@ -89,12 +89,17 @@ export class CodingRuntime {
|
|
|
89
89
|
command: input.command,
|
|
90
90
|
env: input.env,
|
|
91
91
|
statePath: defaultStatePath(this.reposBaseDir),
|
|
92
|
+
resumeFrom: input.resumeFrom,
|
|
92
93
|
bin: input.bin,
|
|
93
94
|
});
|
|
94
95
|
this.sessions.set(input.sessionId, session);
|
|
95
96
|
}
|
|
97
|
+
// Read BEFORE `start()`: a bad `--resume` can kill the process on spawn, and the engine
|
|
98
|
+
// clears its own key on that exit. Reporting after would say "started clean" about a launch
|
|
99
|
+
// that did carry a conversation, and the transcript (which shows the crash) would disagree.
|
|
100
|
+
const resumed = session.resumedConversation;
|
|
96
101
|
session.start();
|
|
97
|
-
return this.snapshot(input.sessionId);
|
|
102
|
+
return { ...this.snapshot(input.sessionId), resumed };
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* The pane the brain reasons over + the inferred run state.
|