@proagentstore/cli 0.4.42 → 0.4.43

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
@@ -604,7 +604,19 @@ export class HeadlessSession {
604
604
  return;
605
605
  this.awaitingResult.set(toolUseId, acts);
606
606
  }
607
- /** The matching `tool_result` arrived — stamp the outcome and publish. */
607
+ /**
608
+ * The matching `tool_result` arrived — stamp the outcome and publish.
609
+ *
610
+ * The result carries more than the outcome: `gh pr create --fill` states its PR number nowhere
611
+ * but its own stdout, so an unnumbered `pr.open`/`pr.merge` takes it from here (#417). It is read
612
+ * from the RAW `block.content`, never from `toolResult()`'s display line — that truncates to 240
613
+ * characters for the transcript and would cut the URL off a verbose result.
614
+ *
615
+ * This path (and `noteAct`) is reachable ONLY from the structured stream-json handling above
616
+ * (`assistant` → `tool_use`, `user` → `tool_result`). A Codex/Grok session is a raw spawn with no
617
+ * such framing, so its PRs stay unattributed by construction; scraping its transcript instead
618
+ * would be the temporal guess `pull-attribution.ts` refuses.
619
+ */
608
620
  settleAct(block) {
609
621
  const id = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
610
622
  const acts = this.awaitingResult.get(id);
@@ -612,7 +624,7 @@ export class HeadlessSession {
612
624
  return;
613
625
  this.awaitingResult.delete(id);
614
626
  const ok = block.is_error !== true;
615
- for (const a of acts)
627
+ for (const a of fillTargetFromResult(acts, block.content))
616
628
  this.publishAct({ ...a, ok });
617
629
  }
618
630
  /** Publish everything still waiting, with an unknown outcome. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.42",
3
+ "version": "0.4.43",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",