@agentproto/runtime 2.6.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.
- package/README.md +16 -0
- package/dist/catalog-models.d.ts +3 -3
- package/dist/config.d.ts +27 -3
- package/dist/config.mjs.map +1 -1
- package/dist/{context-continuity-B9n0t0v-.d.ts → context-continuity-ib9_bVYM.d.ts} +12 -0
- package/dist/index.d.ts +334 -23
- package/dist/index.mjs +1111 -309
- package/dist/index.mjs.map +1 -1
- package/dist/pr-provenance.d.ts +29 -1
- package/dist/pr-provenance.mjs +17 -2
- package/dist/pr-provenance.mjs.map +1 -1
- package/dist/resume-strategies.d.ts +13 -1
- package/dist/resume-strategies.mjs +25 -14
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/{session-config-DIf6wYYP.d.ts → session-config-DbWP9RRj.d.ts} +1 -1
- package/dist/{spawn-defaults-CYJoeHeO.d.ts → spawn-defaults-DVgmfxWo.d.ts} +1 -1
- package/dist/user-presets.d.ts +2 -2
- package/package.json +11 -11
package/dist/pr-provenance.d.ts
CHANGED
|
@@ -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 };
|
package/dist/pr-provenance.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 {
|