@agentproto/runtime 2.7.0 → 2.8.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.
@@ -122,6 +122,15 @@ declare function buildSessionPrFooter(session: FooterSession, options?: SessionF
122
122
  * a second footer.
123
123
  */
124
124
  declare function appendFooterOnce(body: string, footer: string): string;
125
+ /**
126
+ * Whether a PR/review body already carries a RENDERED provenance footer — the
127
+ * `<sub>…@agentproto-bot…</sub>` line {@link buildFooter} emits — as opposed
128
+ * to merely MENTIONING the marker in prose. The distinction matters: a PR
129
+ * whose description discusses the provenance machinery itself (they exist —
130
+ * #999 explains a footer bug and quotes the marker) would otherwise read as
131
+ * "already stamped" forever and never receive its real footer.
132
+ */
133
+ declare function hasProvenanceFooter(body: string): boolean;
125
134
  /**
126
135
  * Recognise a successful `gh pr create` from its argv + stdout and pull the
127
136
  * created PR's URL + number out. Returns null for anything that isn't a
@@ -135,6 +144,25 @@ declare function parseGhPrCreate(command: string, args: readonly string[], stdou
135
144
  url: string;
136
145
  number: number;
137
146
  } | null;
147
+ /**
148
+ * String-form counterpart of {@link parseGhPrCreate} for the in-agent tool
149
+ * lane: an ACP harness's Bash-style tool reports ONE shell string (possibly
150
+ * compound — `git push && gh pr create … | tail -1`), not an argv, so the
151
+ * argv parser above can't see it. Detects "this call created a PR" from the
152
+ * command string plus the call's recorded result text, and returns the
153
+ * created PR (LAST `…/pull/<n>` match in the result, same shadowing rule as
154
+ * {@link parseGhPrCreate} — `gh pr create` prints its URL after any notices).
155
+ *
156
+ * Quoted spans are stripped before matching so a command that merely
157
+ * MENTIONS the phrase — `grep "gh pr create" src/` over a result that quotes
158
+ * a PR url — can never read as a create. Best-effort by design: the caller
159
+ * additionally gates on the call's own `isError` (a failed create whose
160
+ * stderr cites an existing PR must not attribute that PR here).
161
+ */
162
+ declare function detectShellPrCreate(command: string | undefined, resultText: string | undefined): {
163
+ url: string;
164
+ number: number;
165
+ } | null;
138
166
  /**
139
167
  * Best-effort attribution of a `command_execute` run to the executor agent
140
168
  * session that most likely issued it: the newest agent-cli session whose cwd
@@ -145,4 +173,4 @@ declare function parseGhPrCreate(command: string, args: readonly string[], stdou
145
173
  */
146
174
  declare function pickExecutorSession<T extends FooterSession>(sessions: readonly T[], cwd: string): T | undefined;
147
175
 
148
- export { type BuildFooterInput, type FooterProvenance, type FooterSession, MARKER, type SessionFooterOptions, appendFooterOnce, buildFooter, buildSessionPrFooter, fmtTokens, parseGhPrCreate, pickExecutorSession, sessionFooterProvenance };
176
+ export { type BuildFooterInput, type FooterProvenance, type FooterSession, MARKER, type SessionFooterOptions, appendFooterOnce, buildFooter, buildSessionPrFooter, detectShellPrCreate, fmtTokens, hasProvenanceFooter, parseGhPrCreate, pickExecutorSession, sessionFooterProvenance };
@@ -75,9 +75,12 @@ function buildSessionPrFooter(session, options = {}) {
75
75
  return buildFooter({ prov, authMode, sha: options.sha, kind: "PR" });
76
76
  }
77
77
  function appendFooterOnce(body, footer) {
78
- if (body.includes(MARKER)) return body;
78
+ if (hasProvenanceFooter(body)) return body;
79
79
  return `${body}${footer}`;
80
80
  }
81
+ function hasProvenanceFooter(body) {
82
+ return new RegExp(`<sub>[^\\n]*${MARKER}`).test(body);
83
+ }
81
84
  function parseGhPrCreate(command, args, stdout) {
82
85
  if (basename(command) !== "gh") return null;
83
86
  const positionals = args.filter((a) => !a.startsWith("-"));
@@ -90,6 +93,18 @@ function parseGhPrCreate(command, args, stdout) {
90
93
  }
91
94
  return last;
92
95
  }
96
+ function detectShellPrCreate(command, resultText) {
97
+ if (!command || !resultText) return null;
98
+ const unquoted = command.replace(/'[^']*'/g, " ").replace(/"(?:[^"\\]|\\.)*"/g, " ");
99
+ if (!/(^|[\s;&|({])gh\s+pr\s+create(\s|$)/.test(unquoted)) return null;
100
+ const re = /https?:\/\/\S+?\/pull\/(\d+)/g;
101
+ let match;
102
+ let last = null;
103
+ while ((match = re.exec(resultText)) !== null) {
104
+ last = { url: match[0], number: Number(match[1]) };
105
+ }
106
+ return last;
107
+ }
93
108
  function cwdRelated(sessionCwd, cwd) {
94
109
  if (sessionCwd === cwd) return true;
95
110
  const sep = "/";
@@ -107,6 +122,6 @@ function pickExecutorSession(sessions, cwd) {
107
122
  return [...candidates].sort(byRecency)[0];
108
123
  }
109
124
 
110
- export { MARKER, appendFooterOnce, buildFooter, buildSessionPrFooter, fmtTokens, parseGhPrCreate, pickExecutorSession, sessionFooterProvenance };
125
+ export { MARKER, appendFooterOnce, buildFooter, buildSessionPrFooter, detectShellPrCreate, fmtTokens, hasProvenanceFooter, parseGhPrCreate, pickExecutorSession, sessionFooterProvenance };
111
126
  //# sourceMappingURL=pr-provenance.mjs.map
112
127
  //# sourceMappingURL=pr-provenance.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pr-provenance.ts"],"names":[],"mappings":"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","file":"pr-provenance.mjs","sourcesContent":["/**\n * PR provenance footer — daemon/MCP lane.\n *\n * This is the SAME footer the runner-side agentflow scripts stamp on the\n * CI-PR, review, and local-`gh_open_pr` lanes; `scripts/lib/provenance-\n * footer.mjs` re-exports {@link buildFooter}/{@link MARKER}/{@link fmtTokens}\n * from HERE so there is exactly one format, one marker, one renderer. The\n * only reason this canonical copy lives in `@agentproto/runtime` rather than\n * in `scripts/` is direction of dependency: the daemon is a package and\n * cannot import a loose script, but a script can import a package's built\n * `dist/` (it already does for `computeProvenance`).\n *\n * What the DAEMON lane adds over the runner lanes: the footer is built from\n * a live {@link SessionDescriptor} (the executor agent-cli session that ran\n * `gh pr create`) plus its supervisor/parent session, rather than from a\n * `computeProvenance` join over the worktree. So this file also owns the\n * mapping from a session descriptor to the footer's provenance shape\n * ({@link sessionFooterProvenance}) and the small pure helpers the\n * command_execute stamp path needs ({@link parseGhPrCreate},\n * {@link pickExecutorSession}, {@link appendFooterOnce}).\n *\n * Pure by construction — only `node:path`, no daemon imports, no I/O — so it\n * bundles to a standalone `dist/pr-provenance.mjs` the scripts can import\n * without pulling the rest of the runtime in. Keep it that way. The\n * side-effecting orchestration (spawning `gh`, editing the PR body, recording\n * the opened PR) lives in `pr-provenance-stamp.ts`.\n */\n\nimport { basename } from \"node:path\"\n\n/** Deterministic marker the runner owns — a reliable native-vs-legacy\n * discriminator across every lane (CI-PR, review, local, daemon). */\nexport const MARKER = \"@agentproto-bot\"\n\nexport const fmtTokens = (n: unknown): string | null => {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return null\n return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)\n}\n\n/** The footer's provenance shape — field names are the renderer's, not the\n * session registry's (see {@link sessionFooterProvenance} for the mapping). */\nexport interface FooterProvenance {\n sessionId?: string\n label?: string\n /** Adapter / harness slug that ran (\"claude-code\", \"codex\", …). */\n adapter?: string\n model?: string\n /** Bound auth-profile IDENTITY — a label or ref, NEVER the credential. */\n authProfile?: string\n sandboxId?: string\n /** Supervisor / parent session that spawned the executor. */\n parentSessionId?: string\n tokensIn?: number\n tokensOut?: number\n costUsd?: number\n /** \"adapter\" | \"local\" | \"daemon\" — drives host/cwd vs run-link rendering. */\n source?: string\n host?: string\n cwd?: string\n /** The session's registered workspace slug (`SessionDescriptor.workspaceSlug`,\n * e.g. \"ts\", \"default\"). Rendered alongside `cwd`'s leaf directory name so\n * the footer identifies WHICH workspace ran the command, not just an\n * arbitrary trailing path segment — a bare `basename(cwd)` reads as\n * meaningless (or actively misleading) when the session's cwd is a\n * workspace root rather than a per-branch worktree. */\n workspaceSlug?: string\n}\n\n/**\n * Render the footer's `cwd` value. A bare `basename(cwd)` collapses to a\n * meaningless (or actively misleading) fragment whenever the session's cwd IS\n * the workspace root rather than a per-branch worktree dir — e.g. a checkout\n * literally named `ts` renders as the opaque `cwd \\`ts\\`` with no indication\n * that's a workspace name, not a worktree/branch leaf. Prefixing the\n * registered workspace slug disambiguates it, and is dropped when it's\n * identical to the leaf (the common per-worktree case) so the common case\n * stays exactly as compact as before.\n */\nfunction cwdLabel(cwd: string, workspaceSlug?: string): string {\n const leaf = basename(cwd)\n if (!workspaceSlug || workspaceSlug === leaf) return workspaceSlug ?? leaf\n return `${workspaceSlug}/${leaf}`\n}\n\nexport interface BuildFooterInput {\n prov?: FooterProvenance\n authMode?: string\n runId?: string\n runUrl?: string\n sha?: string\n kind?: string\n}\n\nexport const buildFooter = ({\n prov,\n authMode,\n runId,\n runUrl,\n sha,\n kind = \"review\",\n}: BuildFooterInput): string => {\n const parts = [`🤖 **${MARKER}** — ${kind}`]\n if (prov?.sessionId) {\n parts.push(`session \\`${prov.sessionId}\\`${prov.label ? ` (\\`${prov.label}\\`)` : \"\"}`)\n }\n // Engine label: \"adapter / authMode\" when an adapter ran; \"legacy fallback\n // (authMode)\" when no agent session exists at all (the API-key fallback path);\n // bare authMode only if a session ran without a resolved adapter slug.\n if (prov?.adapter) parts.push([prov.adapter, authMode].filter(Boolean).join(\" / \"))\n else if (!prov?.sessionId) parts.push(`legacy fallback${authMode ? ` (${authMode})` : \"\"}`)\n else if (authMode) parts.push(authMode)\n // Bound auth profile identity (the \"wallet\") — daemon lane; NEVER the secret.\n if (prov?.authProfile) parts.push(`auth-profile \\`${prov.authProfile}\\``)\n if (prov?.model) parts.push(`model \\`${prov.model}\\``)\n if (prov?.sandboxId) parts.push(`e2b \\`${prov.sandboxId}\\``)\n if (prov?.parentSessionId) parts.push(`supervisor \\`${prov.parentSessionId}\\``)\n const tin = fmtTokens(prov?.tokensIn)\n const tout = fmtTokens(prov?.tokensOut)\n if (tin || tout) parts.push(`${tin ?? \"?\"} in / ${tout ?? \"?\"} out`)\n if (typeof prov?.costUsd === \"number\") {\n parts.push(`$${prov.costUsd.toFixed(4)}${prov.source && prov.source !== \"adapter\" ? ` (${prov.source})` : \"\"}`)\n }\n const showLocalHostCwd = prov?.source === \"local\" || prov?.source === \"daemon\" || !runId\n if (runId && !showLocalHostCwd) parts.push(`run [${runId}](${runUrl})`)\n if (showLocalHostCwd) {\n if (prov?.host) parts.push(`host \\`${prov.host}\\``)\n if (prov?.cwd) parts.push(`cwd \\`${cwdLabel(prov.cwd, prov.workspaceSlug)}\\``)\n }\n if (sha) parts.push(`sha \\`${sha.slice(0, 7)}\\``)\n return `\\n\\n---\\n<sub>${parts.join(\" · \")}</sub>`\n}\n\n/** Structural subset of `SessionDescriptor` the footer needs. The runtime's\n * full descriptor satisfies this; keeping it structural is what lets this\n * module stay free of a `./sessions.js` import (and thus standalone). */\nexport interface FooterSession {\n id: string\n kind?: string\n status?: string\n startedAt?: string\n label?: string\n cwd?: string\n workspaceSlug?: string\n adapterSlug?: string\n harness?: string\n model?: string\n parentSessionId?: string\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n auth?: { mode?: string }\n accessProfile?: { profileRef: string; label?: string }\n}\n\nexport interface SessionFooterOptions {\n /** The supervisor/parent session descriptor, when resolvable. */\n supervisor?: { id: string } | null\n host?: string\n /** Provenance source tag; defaults to \"daemon\". */\n source?: string\n}\n\n/**\n * Map a live executor session (+ its supervisor) onto the footer's provenance\n * shape and the `authMode` the renderer takes separately. The adapter is the\n * canonical harness slug, falling back to `adapterSlug`. The bound auth\n * profile is the profile's human label (or its ref), never the credential.\n */\nexport function sessionFooterProvenance(\n session: FooterSession,\n options: SessionFooterOptions = {},\n): { prov: FooterProvenance; authMode: string | undefined } {\n const prov: FooterProvenance = {\n sessionId: session.id,\n label: session.label,\n adapter: session.harness ?? session.adapterSlug,\n model: session.model,\n authProfile: session.accessProfile?.label ?? session.accessProfile?.profileRef,\n parentSessionId: options.supervisor?.id,\n costUsd: session.costUsd,\n tokensIn: session.tokensIn,\n tokensOut: session.tokensOut,\n source: options.source ?? \"daemon\",\n host: options.host,\n cwd: session.cwd,\n workspaceSlug: session.workspaceSlug,\n }\n return { prov, authMode: session.auth?.mode }\n}\n\n/** Build the daemon-lane PR footer for one executor session directly. */\nexport function buildSessionPrFooter(\n session: FooterSession,\n options: SessionFooterOptions & { sha?: string } = {},\n): string {\n const { prov, authMode } = sessionFooterProvenance(session, options)\n return buildFooter({ prov, authMode, sha: options.sha, kind: \"PR\" })\n}\n\n/**\n * Append the footer to a PR body exactly once. Idempotent by the marker: a\n * body that already carries a `@agentproto-bot` footer is returned unchanged,\n * so a retried stamp (network reply lost after the edit landed) never stacks\n * a second footer.\n */\nexport function appendFooterOnce(body: string, footer: string): string {\n if (body.includes(MARKER)) return body\n return `${body}${footer}`\n}\n\n/**\n * Recognise a successful `gh pr create` from its argv + stdout and pull the\n * created PR's URL + number out. Returns null for anything that isn't a\n * PR-creating `gh` invocation, or a create whose stdout carried no PR URL.\n *\n * `gh pr create` prints the PR URL on its own line (often the last line,\n * after any advisory notices) — we take the LAST `…/pull/<n>` match so a\n * \"Warning: …/pull/…\" style notice can't shadow the real one.\n */\nexport function parseGhPrCreate(\n command: string,\n args: readonly string[],\n stdout: string,\n): { url: string; number: number } | null {\n if (basename(command) !== \"gh\") return null\n // Skip global flags; the first two non-flag tokens must be `pr` then `create`.\n const positionals = args.filter(a => !a.startsWith(\"-\"))\n if (positionals[0] !== \"pr\" || positionals[1] !== \"create\") return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(stdout)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/** `session.cwd == cwd || one contains the other` — the executor's cwd is\n * usually the worktree root while the command may run in a subdir (or vice\n * versa), so containment is checked in both directions. */\nfunction cwdRelated(sessionCwd: string, cwd: string): boolean {\n if (sessionCwd === cwd) return true\n const sep = \"/\"\n return cwd.startsWith(sessionCwd + sep) || sessionCwd.startsWith(cwd + sep)\n}\n\n/**\n * Best-effort attribution of a `command_execute` run to the executor agent\n * session that most likely issued it: the newest agent-cli session whose cwd\n * is related to the command's cwd, preferring a still-alive one. Returns\n * undefined when no agent-cli session matches (e.g. a bare shell opened the\n * PR) — the caller then skips stamping rather than emit a misleading \"legacy\n * fallback\" footer.\n */\nexport function pickExecutorSession<T extends FooterSession>(\n sessions: readonly T[],\n cwd: string,\n): T | undefined {\n const candidates = sessions.filter(\n s => s.kind === \"agent-cli\" && typeof s.cwd === \"string\" && cwdRelated(s.cwd, cwd),\n )\n if (candidates.length === 0) return undefined\n const alive = (s: T) => s.status === \"running\" || s.status === \"starting\"\n const byRecency = (a: T, b: T) => (b.startedAt ?? \"\").localeCompare(a.startedAt ?? \"\")\n const live = candidates.filter(alive).sort(byRecency)\n if (live.length > 0) return live[0]\n return [...candidates].sort(byRecency)[0]\n}\n"]}
1
+ {"version":3,"sources":["../src/pr-provenance.ts"],"names":[],"mappings":"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","file":"pr-provenance.mjs","sourcesContent":["/**\n * PR provenance footer — daemon/MCP lane.\n *\n * This is the SAME footer the runner-side agentflow scripts stamp on the\n * CI-PR, review, and local-`gh_open_pr` lanes; `scripts/lib/provenance-\n * footer.mjs` re-exports {@link buildFooter}/{@link MARKER}/{@link fmtTokens}\n * from HERE so there is exactly one format, one marker, one renderer. The\n * only reason this canonical copy lives in `@agentproto/runtime` rather than\n * in `scripts/` is direction of dependency: the daemon is a package and\n * cannot import a loose script, but a script can import a package's built\n * `dist/` (it already does for `computeProvenance`).\n *\n * What the DAEMON lane adds over the runner lanes: the footer is built from\n * a live {@link SessionDescriptor} (the executor agent-cli session that ran\n * `gh pr create`) plus its supervisor/parent session, rather than from a\n * `computeProvenance` join over the worktree. So this file also owns the\n * mapping from a session descriptor to the footer's provenance shape\n * ({@link sessionFooterProvenance}) and the small pure helpers the\n * command_execute stamp path needs ({@link parseGhPrCreate},\n * {@link pickExecutorSession}, {@link appendFooterOnce}).\n *\n * Pure by construction — only `node:path`, no daemon imports, no I/O — so it\n * bundles to a standalone `dist/pr-provenance.mjs` the scripts can import\n * without pulling the rest of the runtime in. Keep it that way. The\n * side-effecting orchestration (spawning `gh`, editing the PR body, recording\n * the opened PR) lives in `pr-provenance-stamp.ts`.\n */\n\nimport { basename } from \"node:path\"\n\n/** Deterministic marker the runner owns — a reliable native-vs-legacy\n * discriminator across every lane (CI-PR, review, local, daemon). */\nexport const MARKER = \"@agentproto-bot\"\n\nexport const fmtTokens = (n: unknown): string | null => {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return null\n return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)\n}\n\n/** The footer's provenance shape — field names are the renderer's, not the\n * session registry's (see {@link sessionFooterProvenance} for the mapping). */\nexport interface FooterProvenance {\n sessionId?: string\n label?: string\n /** Adapter / harness slug that ran (\"claude-code\", \"codex\", …). */\n adapter?: string\n model?: string\n /** Bound auth-profile IDENTITY — a label or ref, NEVER the credential. */\n authProfile?: string\n sandboxId?: string\n /** Supervisor / parent session that spawned the executor. */\n parentSessionId?: string\n tokensIn?: number\n tokensOut?: number\n costUsd?: number\n /** \"adapter\" | \"local\" | \"daemon\" — drives host/cwd vs run-link rendering. */\n source?: string\n host?: string\n cwd?: string\n /** The session's registered workspace slug (`SessionDescriptor.workspaceSlug`,\n * e.g. \"ts\", \"default\"). Rendered alongside `cwd`'s leaf directory name so\n * the footer identifies WHICH workspace ran the command, not just an\n * arbitrary trailing path segment — a bare `basename(cwd)` reads as\n * meaningless (or actively misleading) when the session's cwd is a\n * workspace root rather than a per-branch worktree. */\n workspaceSlug?: string\n}\n\n/**\n * Render the footer's `cwd` value. A bare `basename(cwd)` collapses to a\n * meaningless (or actively misleading) fragment whenever the session's cwd IS\n * the workspace root rather than a per-branch worktree dir — e.g. a checkout\n * literally named `ts` renders as the opaque `cwd \\`ts\\`` with no indication\n * that's a workspace name, not a worktree/branch leaf. Prefixing the\n * registered workspace slug disambiguates it, and is dropped when it's\n * identical to the leaf (the common per-worktree case) so the common case\n * stays exactly as compact as before.\n */\nfunction cwdLabel(cwd: string, workspaceSlug?: string): string {\n const leaf = basename(cwd)\n if (!workspaceSlug || workspaceSlug === leaf) return workspaceSlug ?? leaf\n return `${workspaceSlug}/${leaf}`\n}\n\nexport interface BuildFooterInput {\n prov?: FooterProvenance\n authMode?: string\n runId?: string\n runUrl?: string\n sha?: string\n kind?: string\n}\n\nexport const buildFooter = ({\n prov,\n authMode,\n runId,\n runUrl,\n sha,\n kind = \"review\",\n}: BuildFooterInput): string => {\n const parts = [`🤖 **${MARKER}** — ${kind}`]\n if (prov?.sessionId) {\n parts.push(`session \\`${prov.sessionId}\\`${prov.label ? ` (\\`${prov.label}\\`)` : \"\"}`)\n }\n // Engine label: \"adapter / authMode\" when an adapter ran; \"legacy fallback\n // (authMode)\" when no agent session exists at all (the API-key fallback path);\n // bare authMode only if a session ran without a resolved adapter slug.\n if (prov?.adapter) parts.push([prov.adapter, authMode].filter(Boolean).join(\" / \"))\n else if (!prov?.sessionId) parts.push(`legacy fallback${authMode ? ` (${authMode})` : \"\"}`)\n else if (authMode) parts.push(authMode)\n // Bound auth profile identity (the \"wallet\") — daemon lane; NEVER the secret.\n if (prov?.authProfile) parts.push(`auth-profile \\`${prov.authProfile}\\``)\n if (prov?.model) parts.push(`model \\`${prov.model}\\``)\n if (prov?.sandboxId) parts.push(`e2b \\`${prov.sandboxId}\\``)\n if (prov?.parentSessionId) parts.push(`supervisor \\`${prov.parentSessionId}\\``)\n const tin = fmtTokens(prov?.tokensIn)\n const tout = fmtTokens(prov?.tokensOut)\n if (tin || tout) parts.push(`${tin ?? \"?\"} in / ${tout ?? \"?\"} out`)\n if (typeof prov?.costUsd === \"number\") {\n parts.push(`$${prov.costUsd.toFixed(4)}${prov.source && prov.source !== \"adapter\" ? ` (${prov.source})` : \"\"}`)\n }\n const showLocalHostCwd = prov?.source === \"local\" || prov?.source === \"daemon\" || !runId\n if (runId && !showLocalHostCwd) parts.push(`run [${runId}](${runUrl})`)\n if (showLocalHostCwd) {\n if (prov?.host) parts.push(`host \\`${prov.host}\\``)\n if (prov?.cwd) parts.push(`cwd \\`${cwdLabel(prov.cwd, prov.workspaceSlug)}\\``)\n }\n if (sha) parts.push(`sha \\`${sha.slice(0, 7)}\\``)\n return `\\n\\n---\\n<sub>${parts.join(\" · \")}</sub>`\n}\n\n/** Structural subset of `SessionDescriptor` the footer needs. The runtime's\n * full descriptor satisfies this; keeping it structural is what lets this\n * module stay free of a `./sessions.js` import (and thus standalone). */\nexport interface FooterSession {\n id: string\n kind?: string\n status?: string\n startedAt?: string\n label?: string\n cwd?: string\n workspaceSlug?: string\n adapterSlug?: string\n harness?: string\n model?: string\n parentSessionId?: string\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n auth?: { mode?: string }\n accessProfile?: { profileRef: string; label?: string }\n}\n\nexport interface SessionFooterOptions {\n /** The supervisor/parent session descriptor, when resolvable. */\n supervisor?: { id: string } | null\n host?: string\n /** Provenance source tag; defaults to \"daemon\". */\n source?: string\n}\n\n/**\n * Map a live executor session (+ its supervisor) onto the footer's provenance\n * shape and the `authMode` the renderer takes separately. The adapter is the\n * canonical harness slug, falling back to `adapterSlug`. The bound auth\n * profile is the profile's human label (or its ref), never the credential.\n */\nexport function sessionFooterProvenance(\n session: FooterSession,\n options: SessionFooterOptions = {},\n): { prov: FooterProvenance; authMode: string | undefined } {\n const prov: FooterProvenance = {\n sessionId: session.id,\n label: session.label,\n adapter: session.harness ?? session.adapterSlug,\n model: session.model,\n authProfile: session.accessProfile?.label ?? session.accessProfile?.profileRef,\n parentSessionId: options.supervisor?.id,\n costUsd: session.costUsd,\n tokensIn: session.tokensIn,\n tokensOut: session.tokensOut,\n source: options.source ?? \"daemon\",\n host: options.host,\n cwd: session.cwd,\n workspaceSlug: session.workspaceSlug,\n }\n return { prov, authMode: session.auth?.mode }\n}\n\n/** Build the daemon-lane PR footer for one executor session directly. */\nexport function buildSessionPrFooter(\n session: FooterSession,\n options: SessionFooterOptions & { sha?: string } = {},\n): string {\n const { prov, authMode } = sessionFooterProvenance(session, options)\n return buildFooter({ prov, authMode, sha: options.sha, kind: \"PR\" })\n}\n\n/**\n * Append the footer to a PR body exactly once. Idempotent by the marker: a\n * body that already carries a `@agentproto-bot` footer is returned unchanged,\n * so a retried stamp (network reply lost after the edit landed) never stacks\n * a second footer.\n */\nexport function appendFooterOnce(body: string, footer: string): string {\n if (hasProvenanceFooter(body)) return body\n return `${body}${footer}`\n}\n\n/**\n * Whether a PR/review body already carries a RENDERED provenance footer — the\n * `<sub>…@agentproto-bot…</sub>` line {@link buildFooter} emits — as opposed\n * to merely MENTIONING the marker in prose. The distinction matters: a PR\n * whose description discusses the provenance machinery itself (they exist —\n * #999 explains a footer bug and quotes the marker) would otherwise read as\n * \"already stamped\" forever and never receive its real footer.\n */\nexport function hasProvenanceFooter(body: string): boolean {\n return new RegExp(`<sub>[^\\\\n]*${MARKER}`).test(body)\n}\n\n/**\n * Recognise a successful `gh pr create` from its argv + stdout and pull the\n * created PR's URL + number out. Returns null for anything that isn't a\n * PR-creating `gh` invocation, or a create whose stdout carried no PR URL.\n *\n * `gh pr create` prints the PR URL on its own line (often the last line,\n * after any advisory notices) — we take the LAST `…/pull/<n>` match so a\n * \"Warning: …/pull/…\" style notice can't shadow the real one.\n */\nexport function parseGhPrCreate(\n command: string,\n args: readonly string[],\n stdout: string,\n): { url: string; number: number } | null {\n if (basename(command) !== \"gh\") return null\n // Skip global flags; the first two non-flag tokens must be `pr` then `create`.\n const positionals = args.filter(a => !a.startsWith(\"-\"))\n if (positionals[0] !== \"pr\" || positionals[1] !== \"create\") return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(stdout)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/**\n * String-form counterpart of {@link parseGhPrCreate} for the in-agent tool\n * lane: an ACP harness's Bash-style tool reports ONE shell string (possibly\n * compound — `git push && gh pr create … | tail -1`), not an argv, so the\n * argv parser above can't see it. Detects \"this call created a PR\" from the\n * command string plus the call's recorded result text, and returns the\n * created PR (LAST `…/pull/<n>` match in the result, same shadowing rule as\n * {@link parseGhPrCreate} — `gh pr create` prints its URL after any notices).\n *\n * Quoted spans are stripped before matching so a command that merely\n * MENTIONS the phrase — `grep \"gh pr create\" src/` over a result that quotes\n * a PR url — can never read as a create. Best-effort by design: the caller\n * additionally gates on the call's own `isError` (a failed create whose\n * stderr cites an existing PR must not attribute that PR here).\n */\nexport function detectShellPrCreate(\n command: string | undefined,\n resultText: string | undefined,\n): { url: string; number: number } | null {\n if (!command || !resultText) return null\n const unquoted = command.replace(/'[^']*'/g, \" \").replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, \" \")\n if (!/(^|[\\s;&|({])gh\\s+pr\\s+create(\\s|$)/.test(unquoted)) return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(resultText)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/** `session.cwd == cwd || one contains the other` — the executor's cwd is\n * usually the worktree root while the command may run in a subdir (or vice\n * versa), so containment is checked in both directions. */\nfunction cwdRelated(sessionCwd: string, cwd: string): boolean {\n if (sessionCwd === cwd) return true\n const sep = \"/\"\n return cwd.startsWith(sessionCwd + sep) || sessionCwd.startsWith(cwd + sep)\n}\n\n/**\n * Best-effort attribution of a `command_execute` run to the executor agent\n * session that most likely issued it: the newest agent-cli session whose cwd\n * is related to the command's cwd, preferring a still-alive one. Returns\n * undefined when no agent-cli session matches (e.g. a bare shell opened the\n * PR) — the caller then skips stamping rather than emit a misleading \"legacy\n * fallback\" footer.\n */\nexport function pickExecutorSession<T extends FooterSession>(\n sessions: readonly T[],\n cwd: string,\n): T | undefined {\n const candidates = sessions.filter(\n s => s.kind === \"agent-cli\" && typeof s.cwd === \"string\" && cwdRelated(s.cwd, cwd),\n )\n if (candidates.length === 0) return undefined\n const alive = (s: T) => s.status === \"running\" || s.status === \"starting\"\n const byRecency = (a: T, b: T) => (b.startedAt ?? \"\").localeCompare(a.startedAt ?? \"\")\n const live = candidates.filter(alive).sort(byRecency)\n if (live.length > 0) return live[0]\n return [...candidates].sort(byRecency)[0]\n}\n"]}
@@ -60,8 +60,14 @@ interface ResumeStrategy {
60
60
  * most-recently-modified, filtered to files at-or-after `prevStartedAt`
61
61
  * (avoids resuming an unrelated prior conversation).
62
62
  *
63
+ * `configDir` is the session's isolated provider config dir
64
+ * (`SessionDescriptor.adapterConfigDir`): a daemon-spawned claude-code
65
+ * session (#824) persists its transcripts under that dir, not the
66
+ * provider's global store — the probe must look there. Omit for a
67
+ * native PTY / pre-#824 session (global store applies).
68
+ *
63
69
  * Skip when the adapter doesn't persist sessions externally. */
64
- fsProbe?(cwd: string, prevStartedAt: string, expectedId?: string): Promise<string | null>;
70
+ fsProbe?(cwd: string, prevStartedAt: string, expectedId?: string, configDir?: string): Promise<string | null>;
65
71
  /** Return the argv to spawn a PTY that resumes into the given id.
66
72
  * When omitted, the daemon falls back to ACP-level resume via
67
73
  * the agent-cli protocol instead of the provider's native CLI. */
@@ -98,6 +104,12 @@ interface RestartCandidate {
98
104
  interface FsProbeCandidate extends RestartCandidate {
99
105
  cwd?: string;
100
106
  startedAt: string;
107
+ /** The session's isolated provider config dir (`SessionDescriptor.
108
+ * adapterConfigDir`) — where a daemon-spawned claude-code session's
109
+ * transcripts actually live since #824. A full `SessionDescriptor`
110
+ * satisfies this structurally; absent on pre-#824/PTY rows, where the
111
+ * provider's global store is the right place to probe. */
112
+ adapterConfigDir?: string;
101
113
  }
102
114
  /**
103
115
  * Where a restart should land. Computed once so callers never diverge:
@@ -1,6 +1,6 @@
1
+ import { join, resolve } from 'path';
1
2
  import { createReadStream, promises } from 'fs';
2
3
  import { homedir } from 'os';
3
- import { join, resolve } from 'path';
4
4
  import { createInterface } from 'readline';
5
5
 
6
6
  /**
@@ -99,6 +99,10 @@ var init_tool_call_record = __esm({
99
99
  "src/tool-call-record.ts"() {
100
100
  }
101
101
  });
102
+ var init_pr_provenance = __esm({
103
+ "src/pr-provenance.ts"() {
104
+ }
105
+ });
102
106
  function sessionTranscriptDir(sessionId, baseDir) {
103
107
  return join(join(homedir(), ".agentproto", "sessions"), sessionId);
104
108
  }
@@ -108,6 +112,7 @@ function sessionEventsPath(sessionId, baseDir) {
108
112
  var init_transcript_writer = __esm({
109
113
  "src/transcript-writer.ts"() {
110
114
  init_tool_call_record();
115
+ init_pr_provenance();
111
116
  }
112
117
  });
113
118
 
@@ -219,14 +224,13 @@ function renderMarkdown(session, opts = {}) {
219
224
  function renderJson(session) {
220
225
  return JSON.stringify(session, null, 2);
221
226
  }
222
- async function exportClaudeCodeSession(adapterSessionId, cwd) {
227
+ async function exportClaudeCodeSession(adapterSessionId, cwd, configDir) {
223
228
  if (!cwd) {
224
229
  throw new Error(
225
230
  "claude-code exporter: cwd is required to locate the JSONL file.\nPass cwd explicitly or use a session id that is in the registry."
226
231
  );
227
232
  }
228
- const encoded = claudeProjectSlug(cwd);
229
- const filePath = join(homedir(), ".claude", "projects", encoded, `${adapterSessionId}.jsonl`);
233
+ const filePath = join(claudeCodeProjectDir(cwd, configDir), `${adapterSessionId}.jsonl`);
230
234
  let stream;
231
235
  try {
232
236
  stream = createReadStream(filePath, { encoding: "utf8" });
@@ -1320,6 +1324,7 @@ async function exportAgentSession(input) {
1320
1324
  let adapterSlug = input.adapter;
1321
1325
  let cwd = input.cwd;
1322
1326
  let adapterSessionId = sessionId;
1327
+ let configDir;
1323
1328
  const err = (msg) => ({
1324
1329
  sessionId,
1325
1330
  adapter: adapterSlug ?? "unknown",
@@ -1332,6 +1337,7 @@ async function exportAgentSession(input) {
1332
1337
  if (desc) {
1333
1338
  adapterSlug = adapterSlug ?? desc.adapterSlug;
1334
1339
  cwd = cwd ?? desc.cwd;
1340
+ configDir = desc.adapterConfigDir;
1335
1341
  if (desc.adapterSessionId) adapterSessionId = desc.adapterSessionId;
1336
1342
  }
1337
1343
  const tryNative = async () => {
@@ -1349,7 +1355,7 @@ Pass adapter explicitly or use a known session id (sess_xxx or name).`
1349
1355
  Only sessions spawned via claude-code or hermes can be exported.`
1350
1356
  );
1351
1357
  }
1352
- return exporter.exportSession(adapterSessionId, cwd);
1358
+ return exporter.exportSession(adapterSessionId, cwd, configDir);
1353
1359
  };
1354
1360
  const tryDaemon = () => exportDaemonEventsSession(daemonSessionId, desc);
1355
1361
  let session;
@@ -1429,8 +1435,9 @@ var init_transcript_export = __esm({
1429
1435
  function claudeProjectSlug(cwd) {
1430
1436
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
1431
1437
  }
1432
- function claudeCodeProjectDir(cwd) {
1433
- return resolve(homedir(), ".claude", "projects", claudeProjectSlug(cwd));
1438
+ function claudeCodeProjectDir(cwd, configDir) {
1439
+ const base = configDir ?? resolve(homedir(), ".claude");
1440
+ return resolve(base, "projects", claudeProjectSlug(cwd));
1434
1441
  }
1435
1442
  function extractFirstText(content) {
1436
1443
  if (typeof content === "string") {
@@ -1491,8 +1498,8 @@ function claudeEntrypointFor(mode) {
1491
1498
  return mode === "native" ? "cli" : "sdk-ts";
1492
1499
  }
1493
1500
  async function discoverClaudeCode(input) {
1494
- const { cwd, since, until, attachmentMode, expectedId } = input;
1495
- const dir = claudeCodeProjectDir(cwd);
1501
+ const { cwd, since, until, attachmentMode, configDir, expectedId } = input;
1502
+ const dir = claudeCodeProjectDir(cwd, configDir);
1496
1503
  if (expectedId) {
1497
1504
  const filePath = join(dir, `${expectedId}.jsonl`);
1498
1505
  try {
@@ -1537,9 +1544,9 @@ async function discoverClaudeCode(input) {
1537
1544
  scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
1538
1545
  return scored.map((s) => s.candidate);
1539
1546
  }
1540
- async function readClaudeCode(conversationId, cwd) {
1547
+ async function readClaudeCode(conversationId, cwd, configDir) {
1541
1548
  const { exportClaudeCodeSession: exportClaudeCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
1542
- return exportClaudeCodeSession2(conversationId, cwd);
1549
+ return exportClaudeCodeSession2(conversationId, cwd, configDir);
1543
1550
  }
1544
1551
  async function discoverHermes(input) {
1545
1552
  const { discoverHermesSessions: discoverHermesSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
@@ -1641,11 +1648,12 @@ var RESUME_STRATEGIES = Object.fromEntries(
1641
1648
  {
1642
1649
  outputHint: store.outputHint,
1643
1650
  storeAs: store.storeAs,
1644
- fsProbe: async (cwd, prevStartedAt, expectedId) => {
1651
+ fsProbe: async (cwd, prevStartedAt, expectedId, configDir) => {
1645
1652
  const candidates = await s.discover({
1646
1653
  cwd,
1647
1654
  since: prevStartedAt,
1648
- expectedId
1655
+ expectedId,
1656
+ configDir
1649
1657
  });
1650
1658
  return candidates[0]?.conversationId ?? null;
1651
1659
  },
@@ -1694,7 +1702,10 @@ async function augmentWithFsResume(prev) {
1694
1702
  const id = await strategy.fsProbe(
1695
1703
  prev.cwd,
1696
1704
  prev.startedAt,
1697
- prev.adapterSessionId
1705
+ prev.adapterSessionId,
1706
+ // A config-dir-isolated session (#824) persisted its transcript under
1707
+ // its own CLAUDE_CONFIG_DIR — probe there, not the global ~/.claude.
1708
+ prev.adapterConfigDir
1698
1709
  );
1699
1710
  if (!id) return prev;
1700
1711
  return {