@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
- this.claudeSessionId = readState(config.statePath, config.id);
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
- /** The matching `tool_result` arrived — stamp the outcome and publish. */
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",