edsger 0.48.1 → 0.49.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.
@@ -33,14 +33,8 @@ export const runRunSheetCommand = async (options) => {
33
33
  if (result.cloneError) {
34
34
  logInfo(`Clone warning: ${result.cloneError}`);
35
35
  }
36
- if (result.commitsError) {
37
- logInfo(`Commits warning: could not fetch commit list (${result.commitsError}). Run sheet rendered without {{commits}} / {{#each commits}} content.`);
38
- }
39
- if (result.missingPlaceholders && result.missingPlaceholders.length > 0) {
40
- logInfo(`Unresolved placeholders: ${result.missingPlaceholders.join(', ')}`);
41
- }
42
- if (result.commitsTruncated) {
43
- logInfo('Commit list truncated at GitHub 250-commit cap.');
36
+ if (typeof result.agentTurns === 'number') {
37
+ logInfo(`Agent used ${result.agentTurns} turn(s)${result.model ? ` on model ${result.model}` : ''}.`);
44
38
  }
45
39
  logInfo('View it in the Release detail page of your product dashboard.');
46
40
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Run-sheet generation via the Claude Agent SDK.
3
+ *
4
+ * The agent is handed the product's cloned repo (checked out at the
5
+ * release ref) plus release metadata and a free-form template, and is
6
+ * asked to produce the final run sheet Markdown. Unlike the previous
7
+ * placeholder-based renderer, the template here is a structural guide
8
+ * — it may contain no placeholders at all. The agent reads the diff
9
+ * and source to decide what each section should say.
10
+ */
11
+ export interface AgentReleaseContext {
12
+ tag: string;
13
+ name: string | null;
14
+ body: string | null;
15
+ url: string | null;
16
+ published_at: string | null;
17
+ previous_tag: string | null;
18
+ previous_published_at: string | null;
19
+ }
20
+ export interface AgentProductContext {
21
+ name: string;
22
+ github_repository_full_name: string | null;
23
+ }
24
+ export interface RunSheetAgentInput {
25
+ product: AgentProductContext;
26
+ release: AgentReleaseContext;
27
+ template: string;
28
+ repoDir: string | null;
29
+ isDraft: boolean;
30
+ draftBaseRef: string | null;
31
+ verbose?: boolean;
32
+ }
33
+ export interface RunSheetAgentResult {
34
+ content: string;
35
+ turns: number;
36
+ model: string;
37
+ subtype: string;
38
+ error: string | null;
39
+ }
40
+ /** Exported for unit tests; not part of the public CLI surface. */
41
+ export declare function buildUserPrompt(input: RunSheetAgentInput): string;
42
+ /**
43
+ * Strip a single Markdown code fence wrapping the entire response.
44
+ * Defensive: the prompt forbids it, but models still occasionally do it.
45
+ *
46
+ * Exported for unit tests.
47
+ */
48
+ export declare function stripWrappingFence(text: string): string;
49
+ export declare function generateRunSheetWithAgent(input: RunSheetAgentInput): Promise<RunSheetAgentResult>;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Run-sheet generation via the Claude Agent SDK.
3
+ *
4
+ * The agent is handed the product's cloned repo (checked out at the
5
+ * release ref) plus release metadata and a free-form template, and is
6
+ * asked to produce the final run sheet Markdown. Unlike the previous
7
+ * placeholder-based renderer, the template here is a structural guide
8
+ * — it may contain no placeholders at all. The agent reads the diff
9
+ * and source to decide what each section should say.
10
+ */
11
+ import { query } from '@anthropic-ai/claude-agent-sdk';
12
+ import { DEFAULT_MODEL } from '../../constants.js';
13
+ import { logError, logInfo } from '../../utils/logger.js';
14
+ // Dropped from the agent's tool surface entirely so it cannot mutate the
15
+ // cloned repo or anything else on disk. Bash stays available — we further
16
+ // constrain it to read-only commands via the system prompt.
17
+ const DISALLOWED_TOOLS = ['Edit', 'Write', 'NotebookEdit'];
18
+ const SYSTEM_PROMPT = `You are generating a release run sheet for a software product.
19
+
20
+ A run sheet is a release-readiness document: it summarises what is in the release, what operators need to do to ship it safely, and what to verify after rollout. You will receive product/release metadata and a TEMPLATE that describes the desired structure. The template is authoritative guidance — match its headings, section order, tone, and level of detail. It may be free-form prose with no placeholders or variable references at all.
21
+
22
+ You have READ-ONLY access to the product's git repository, already cloned and checked out at the release ref (or the default branch for drafts), via the Bash, Read, Grep, and Glob tools. Use them to ground your output in real code:
23
+ - \`git --no-pager log --no-decorate --pretty=format:"%h %s (%an)" <previous_ref>..HEAD\` to enumerate commits in the release
24
+ - \`git --no-pager diff --stat <previous_ref>..HEAD\` for the files-changed summary
25
+ - \`git --no-pager diff <previous_ref>..HEAD -- <path>\` to inspect specific changes when writing a section
26
+ - \`git --no-pager show <sha>\` to inspect an individual commit
27
+ - README / CHANGELOG / package manifests for product-level context
28
+
29
+ Bash safety rules (strict):
30
+ - Use \`git --no-pager ...\` for every git invocation so output never paginates.
31
+ - ONLY run read-only commands. Allowed: \`git log\`, \`git diff\`, \`git show\`, \`git status\`, \`git rev-parse\`, \`git ls-files\`, \`git config --get\`, \`cat\`, \`head\`, \`tail\`, \`wc\`, \`ls\`. Forbidden: anything that mutates state — \`git push\`, \`git commit\`, \`git reset\`, \`git checkout\`, \`git fetch\`, \`git pull\`, \`git clean\`, \`git config <set>\`, \`rm\`, \`mv\`, \`cp\`, \`chmod\`, \`curl\`, \`wget\`, package managers, build commands.
32
+ - Prefer Read/Grep/Glob over Bash for plain file inspection.
33
+
34
+ Output rules:
35
+ 1. Output ONLY the final run-sheet Markdown. No preamble ("Here is..."), no trailing commentary, no wrapping code fence around the whole document.
36
+ 2. Follow the structure of the TEMPLATE exactly — same section headings in the same order. If the template is free-form prose, infer sensible sections from it.
37
+ 3. Ground every claim in the repo. Do not invent features, APIs, or risks that are not evidenced by the diff or code.
38
+ 4. If the release is marked DRAFT, include a short "Draft" note near the top explaining the tag is not yet cut.
39
+ 5. Prefer concise bullet points over long paragraphs for actionable items. Aim for under 600 lines of Markdown total — be specific, not exhaustive.
40
+ 6. If the diff is empty or there is no previous ref to compare against, state that plainly rather than fabricating content.`;
41
+ function userMessage(content) {
42
+ return { type: 'user', message: { role: 'user', content } };
43
+ }
44
+ // eslint-disable-next-line @typescript-eslint/require-await -- async generator required by SDK interface
45
+ async function* makePrompt(text) {
46
+ yield userMessage(text);
47
+ }
48
+ /** Exported for unit tests; not part of the public CLI surface. */
49
+ export function buildUserPrompt(input) {
50
+ const { product, release, template, isDraft, draftBaseRef } = input;
51
+ const baseRef = release.previous_tag ?? '(none — no previous release)';
52
+ const headRef = isDraft && draftBaseRef ? draftBaseRef : release.tag;
53
+ const draftLine = isDraft
54
+ ? `DRAFT: true — the tag \`${release.tag}\` has not been cut on GitHub yet; the working tree is the default branch \`${draftBaseRef ?? 'HEAD'}\`.`
55
+ : 'DRAFT: false';
56
+ const metadata = [
57
+ `Product: ${product.name}`,
58
+ `Repository: ${product.github_repository_full_name ?? '(unknown)'}`,
59
+ `Release tag: ${release.tag}`,
60
+ `Release name: ${release.name ?? release.tag}`,
61
+ `Previous tag: ${release.previous_tag ?? '(none)'}`,
62
+ `Published at: ${release.published_at ?? '(unpublished)'}`,
63
+ `Previous published at: ${release.previous_published_at ?? '(n/a)'}`,
64
+ `Release URL: ${release.url ?? '(n/a)'}`,
65
+ draftLine,
66
+ `Compare range: ${baseRef}..${headRef}`,
67
+ ].join('\n');
68
+ const body = release.body?.trim()
69
+ ? `\n\n--- RELEASE NOTES (from GitHub release body) ---\n${release.body.trim()}`
70
+ : '';
71
+ return `Here is the context for the release you are writing a run sheet for.
72
+
73
+ --- METADATA ---
74
+ ${metadata}${body}
75
+
76
+ --- TEMPLATE (follow its structure; it may contain no placeholders) ---
77
+ ${template}
78
+
79
+ --- INSTRUCTIONS ---
80
+ Inspect the repo (it is your current working directory) and produce the final run sheet in Markdown. Output only the Markdown — no preamble, no wrapping fence.`;
81
+ }
82
+ /**
83
+ * Strip a single Markdown code fence wrapping the entire response.
84
+ * Defensive: the prompt forbids it, but models still occasionally do it.
85
+ *
86
+ * Exported for unit tests.
87
+ */
88
+ export function stripWrappingFence(text) {
89
+ const trimmed = text.trim();
90
+ const fence = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/);
91
+ return fence ? fence[1].trim() : trimmed;
92
+ }
93
+ function summariseToolUse(name, input, turn) {
94
+ const i = (input ?? {});
95
+ const descRaw = i.description ?? i.command ?? 'Running...';
96
+ const desc = typeof descRaw === 'string' ? descRaw : 'Running...';
97
+ return `[Turn ${turn}] ${name}: ${desc.slice(0, 160)}`;
98
+ }
99
+ // eslint-disable-next-line complexity -- agent loop with message type handling
100
+ export async function generateRunSheetWithAgent(input) {
101
+ const { repoDir } = input;
102
+ let assistantText = '';
103
+ let resultText = '';
104
+ let turns = 0;
105
+ let subtype = 'unknown';
106
+ let error = null;
107
+ logInfo('Connecting to Claude Agent SDK for run-sheet generation...');
108
+ const userPrompt = buildUserPrompt(input);
109
+ for await (const message of query({
110
+ prompt: makePrompt(userPrompt),
111
+ options: {
112
+ systemPrompt: {
113
+ type: 'preset',
114
+ preset: 'claude_code',
115
+ append: SYSTEM_PROMPT,
116
+ },
117
+ model: DEFAULT_MODEL,
118
+ permissionMode: 'bypassPermissions',
119
+ disallowedTools: DISALLOWED_TOOLS,
120
+ ...(repoDir ? { cwd: repoDir } : {}),
121
+ },
122
+ })) {
123
+ if (message.type === 'assistant' && message.message?.content) {
124
+ turns++;
125
+ for (const content of message.message.content) {
126
+ if (content.type === 'text') {
127
+ assistantText += `${content.text}\n`;
128
+ }
129
+ else if (content.type === 'tool_use') {
130
+ // Always emit tool-use lines (not gated on verbose) so the
131
+ // desktop CliTerminal shows live progress instead of going
132
+ // silent for the duration of the agent run.
133
+ logInfo(summariseToolUse(content.name, content.input, turns));
134
+ }
135
+ }
136
+ }
137
+ if (message.type === 'result') {
138
+ subtype = message.subtype;
139
+ resultText = ('result' in message ? (message.result ?? '') : '') || '';
140
+ if (message.subtype !== 'success') {
141
+ error = `Agent finished with subtype=${message.subtype}`;
142
+ logError(`Run-sheet agent incomplete: ${message.subtype}`);
143
+ }
144
+ }
145
+ }
146
+ const raw = resultText.trim() || assistantText.trim();
147
+ if (!raw) {
148
+ throw new Error(`Run-sheet agent produced no output${error ? ` (${error})` : ''}`);
149
+ }
150
+ return {
151
+ content: stripWrappingFence(raw),
152
+ turns,
153
+ model: DEFAULT_MODEL,
154
+ subtype,
155
+ error,
156
+ };
157
+ }
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Given a release, fetches the product's `run_sheet_template`, clones the
5
5
  * repo at the release tag (reusing the workspace layout used by
6
- * smoke-test), renders placeholders, and upserts the result to the
7
- * `run_sheets` table.
6
+ * smoke-test), and hands the template + repo to the Claude Agent SDK
7
+ * which reads the diff and produces the run sheet Markdown. The result
8
+ * is upserted to the `run_sheets` table.
8
9
  *
9
10
  * Release tags are immutable, so if an existing run sheet has the same
10
11
  * template_snapshot + tag and no clone error, we short-circuit without
@@ -22,24 +23,11 @@ export interface RunSheetResult {
22
23
  releaseTag?: string;
23
24
  summary: string;
24
25
  runSheetId?: string;
25
- missingPlaceholders?: string[];
26
26
  cloneError?: string | null;
27
- commitsError?: string | null;
28
- commitsTruncated?: boolean;
29
27
  isDraft?: boolean;
28
+ agentTurns?: number;
29
+ model?: string;
30
30
  }
31
- /**
32
- * Draft releases have no stored `diff_summary` / `diff_stats` (those are
33
- * only written by smoke-test against a cut tag). Compute a lightweight
34
- * substitute on the fly so draft templates that reference `{{diff_summary}}`
35
- * or `{{diff_stats.*}}` still render something useful.
36
- *
37
- * Exported for unit tests.
38
- */
39
- export declare function fetchDraftDiff(owner: string, repo: string, base: string | null, head: string, token: string): Promise<{
40
- summary: string | null;
41
- stats: Record<string, unknown> | null;
42
- }>;
43
31
  /**
44
32
  * Atomic-create a `.lock` file alongside the workspace dir. Returns
45
33
  * true if we acquired the lock, false if another CLI instance holds a
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * Given a release, fetches the product's `run_sheet_template`, clones the
5
5
  * repo at the release tag (reusing the workspace layout used by
6
- * smoke-test), renders placeholders, and upserts the result to the
7
- * `run_sheets` table.
6
+ * smoke-test), and hands the template + repo to the Claude Agent SDK
7
+ * which reads the diff and produces the run sheet Markdown. The result
8
+ * is upserted to the `run_sheets` table.
8
9
  *
9
10
  * Release tags are immutable, so if an existing run sheet has the same
10
11
  * template_snapshot + tag and no clone error, we short-circuit without
11
12
  * re-cloning. Pass `{ force: true }` to regenerate anyway.
12
13
  */
13
- import { createHash } from 'crypto';
14
14
  import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'fs';
15
15
  import { join } from 'path';
16
16
  import { getGitHubConfigByProduct } from '../../api/github.js';
@@ -18,60 +18,9 @@ import { getProduct } from '../../api/products.js';
18
18
  import { getRelease } from '../../api/releases.js';
19
19
  import { getRunSheetByRelease, upsertRunSheet, } from '../../api/run-sheets.js';
20
20
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
21
- import { cloneFeatureRepo, ensureWorkspaceDir, getFeatureRepoPath, syncRepoToRef, } from '../../workspace/workspace-manager.js';
22
- import { fetchAllCompareCommits, fetchCompare, getDefaultBranchHead, summariseStats, } from '../release-sync/github.js';
23
- import { isSafeGitRef, renderTemplate } from './render.js';
24
- async function fetchCommitsBetween(owner, repo, base, head, token) {
25
- if (!base) {
26
- return { text: '', list: [], truncated: false };
27
- }
28
- try {
29
- const { commits, truncated } = await fetchAllCompareCommits(owner, repo, base, head, token);
30
- const list = commits.map((c) => {
31
- const message = c.commit.message ?? '';
32
- const summary = message.split('\n')[0];
33
- return {
34
- sha: c.sha,
35
- short_sha: c.sha.slice(0, 7),
36
- summary,
37
- message,
38
- author: c.commit.author?.name ?? '',
39
- url: c.html_url ?? '',
40
- };
41
- });
42
- const text = list.map((c) => `- ${c.short_sha} ${c.summary}`).join('\n');
43
- return { text, list, truncated };
44
- }
45
- catch (err) {
46
- const message = err instanceof Error ? err.message : String(err);
47
- logWarning(`Could not fetch commits for run sheet: ${message}`);
48
- return { text: '', list: [], truncated: false, error: message };
49
- }
50
- }
51
- /**
52
- * Draft releases have no stored `diff_summary` / `diff_stats` (those are
53
- * only written by smoke-test against a cut tag). Compute a lightweight
54
- * substitute on the fly so draft templates that reference `{{diff_summary}}`
55
- * or `{{diff_stats.*}}` still render something useful.
56
- *
57
- * Exported for unit tests.
58
- */
59
- export async function fetchDraftDiff(owner, repo, base, head, token) {
60
- if (!base) {
61
- return { summary: null, stats: null };
62
- }
63
- try {
64
- const compare = await fetchCompare(owner, repo, base, head, token);
65
- const stats = summariseStats(compare);
66
- const summary = `Draft diff from ${base} to ${head}: ${stats.total_commits} commit(s), ${stats.files_changed} file(s) changed (+${stats.additions}/-${stats.deletions}).`;
67
- return { summary, stats: stats };
68
- }
69
- catch (err) {
70
- const message = err instanceof Error ? err.message : String(err);
71
- logWarning(`Could not compute draft diff summary: ${message}`);
72
- return { summary: null, stats: null };
73
- }
74
- }
21
+ import { cloneFeatureRepo, ensureWorkspaceDir, getFeatureRepoPath, isSafeGitRef, syncRepoToRef, } from '../../workspace/workspace-manager.js';
22
+ import { getDefaultBranchHead } from '../release-sync/github.js';
23
+ import { generateRunSheetWithAgent } from './agent.js';
75
24
  // Stale locks (e.g. left behind by a crashed or SIGKILLed CLI) are
76
25
  // considered abandoned after this many ms.
77
26
  const LOCK_STALE_MS = 15 * 60 * 1000;
@@ -218,17 +167,6 @@ export function pruneStaleRunSheetWorkspaces(workspaceRoot, now = Date.now(), ma
218
167
  }
219
168
  return { removed };
220
169
  }
221
- function computeInputHash(parts) {
222
- const h = createHash('sha256');
223
- h.update(`template:${parts.template}\n`);
224
- h.update(`tag:${parts.tag}\n`);
225
- h.update(`prev:${parts.previousTag ?? ''}\n`);
226
- for (const f of [...parts.filesRead].sort((a, b) => a.path.localeCompare(b.path))) {
227
- h.update(`file:${f.path}:${f.bytes}\n`);
228
- }
229
- h.update(`commits:${parts.commitShas.join(',')}\n`);
230
- return h.digest('hex');
231
- }
232
170
  /** Exported for unit tests; not part of the public CLI surface. */
233
171
  export function runSheetIsFresh(existing, template, tag) {
234
172
  if (!existing || !existing.content) {
@@ -244,15 +182,17 @@ export function runSheetIsFresh(existing, template, tag) {
244
182
  if (meta.clone_error) {
245
183
  return false;
246
184
  }
185
+ if (meta.agent_error) {
186
+ return false;
187
+ }
247
188
  // Drafts are rendered from the default branch HEAD, which moves. Always
248
- // regenerate so `{{commits}}` / `{{diff_summary}}` reflect the current
249
- // branch tip rather than a stale snapshot.
189
+ // regenerate so the agent sees current code.
250
190
  if (meta.is_draft) {
251
191
  return false;
252
192
  }
253
193
  return true;
254
194
  }
255
- // eslint-disable-next-line complexity -- orchestration with cache, clone, lock, render
195
+ // eslint-disable-next-line complexity -- orchestration with cache, clone, lock, agent
256
196
  export async function runRunSheet(options) {
257
197
  const { releaseId, force, verbose } = options;
258
198
  let release;
@@ -286,7 +226,7 @@ export async function runRunSheet(options) {
286
226
  summary: 'Product has no run_sheet_template configured. Set one in product settings.',
287
227
  };
288
228
  }
289
- // Short-circuit cache: identical template + tag, no prior clone error.
229
+ // Short-circuit cache: identical template + tag, no prior clone / agent error.
290
230
  if (!force) {
291
231
  const existing = await getRunSheetByRelease(releaseId, verbose).catch(() => null);
292
232
  if (existing && runSheetIsFresh(existing, template, release.tag)) {
@@ -297,42 +237,25 @@ export async function runRunSheet(options) {
297
237
  releaseTag: release.tag,
298
238
  runSheetId: existing.id,
299
239
  summary: 'Cached (no change since last render)',
300
- missingPlaceholders: existing.metadata?.missing_placeholders ?? [],
301
240
  cloneError: null,
302
- commitsTruncated: Boolean(existing.metadata?.commits_truncated),
303
241
  };
304
242
  }
305
243
  }
306
244
  const gh = await getGitHubConfigByProduct(release.product_id, verbose);
307
245
  const repoConfigured = gh.configured && gh.token && gh.owner && gh.repo;
308
- // Clone at tag + fetch commits (both best-effort — we still render with
309
- // whatever metadata we have).
310
246
  let repoDir = null;
311
247
  let cloneError = null;
312
- let commitsError = null;
313
- let commits = '';
314
- let commitsList = [];
315
- let commitsTruncated = false;
316
248
  let lockPath = null;
317
249
  // Draft mode: the release's tag doesn't exist on GitHub yet (e.g. a
318
250
  // Maven SNAPSHOT being planned pre-cut). We still want to produce a
319
- // useful run sheet, so we fall back to the default branch HEAD for
320
- // both the checkout and the commits compare.
251
+ // useful run sheet, so we fall back to the default branch HEAD.
321
252
  let isDraft = false;
322
253
  let draftBaseRef = null;
323
- // Draft-only: live-computed diff stats/summary (DB columns are empty
324
- // until smoke-test runs against a real tag).
325
- let draftDiffSummary = null;
326
- let draftDiffStats = null;
327
254
  if (repoConfigured && gh.owner && gh.repo && gh.token) {
328
255
  const { owner, repo, token } = gh;
329
256
  // Serialise concurrent CLI invocations for the *same release* by
330
- // taking a file lock next to the clone dir. Different releases get
331
- // different lock files (per-release workspace).
257
+ // taking a file lock next to the clone dir.
332
258
  const workspaceRoot = ensureWorkspaceDir();
333
- // Sweep old per-release workspaces before cloning. Cheap (one
334
- // readdir + stat per entry); silent unless something is actually
335
- // removed.
336
259
  const pruned = pruneStaleRunSheetWorkspaces(workspaceRoot);
337
260
  if (pruned.removed.length > 0) {
338
261
  logInfo(`Pruned ${pruned.removed.length} stale run-sheet workspace(s).`);
@@ -349,20 +272,12 @@ export async function runRunSheet(options) {
349
272
  };
350
273
  }
351
274
  try {
352
- // Clone into a per-release directory so two releases of the same
353
- // product don't stomp each other's checkout during concurrent
354
- // generation. (smoke-test uses `release-${product_id}` — that races;
355
- // we don't want to inherit that bug here.)
356
275
  const { repoPath } = cloneFeatureRepo(workspaceRoot, workspaceName, owner, repo, token);
357
276
  repoDir = repoPath;
358
- // SNAPSHOT releases (those discovered via `release-sync`'s snapshot
359
- // detection) have no GitHub release object backing them, so
277
+ // SNAPSHOT releases have no GitHub release object backing them, so
360
278
  // `published_at` is null. Use that as a free local signal to skip
361
- // the tag-checkout entirely — no extra GitHub round-trip, no
362
- // misleading "Failed to checkout refs/tags/..." cloneError. For
363
- // real releases (`published_at` set) we still attempt the tag and
364
- // fall back if the checkout itself fails (deleted tag, network).
365
- const expectDraft = release.published_at == null;
279
+ // the tag-checkout entirely.
280
+ const expectDraft = release.published_at === null;
366
281
  if (expectDraft) {
367
282
  logInfo(`Tag ${release.tag} not yet cut (no published_at) — generating draft run sheet from default branch.`);
368
283
  try {
@@ -401,66 +316,46 @@ export async function runRunSheet(options) {
401
316
  cloneError = err instanceof Error ? err.message : String(err);
402
317
  logWarning(`Clone failed: ${cloneError}`);
403
318
  }
404
- const head = isDraft && draftBaseRef ? draftBaseRef : release.tag;
405
- const c = await fetchCommitsBetween(owner, repo, release.previous_tag, head, token);
406
- commits = c.text;
407
- commitsList = c.list;
408
- commitsTruncated = c.truncated;
409
- commitsError = c.error ?? null;
410
- if (isDraft) {
411
- const d = await fetchDraftDiff(owner, repo, release.previous_tag, head, token);
412
- draftDiffSummary = d.summary;
413
- draftDiffStats = d.stats;
414
- }
415
319
  }
416
320
  else {
417
321
  cloneError = 'Product is not linked to a GitHub repository';
418
322
  }
419
- const draftNotice = isDraft
420
- ? `> ⚠️ **Draft run sheet** — tag \`${release.tag}\` does not exist on GitHub yet. Rendered from default branch${draftBaseRef ? ` \`${draftBaseRef}\`` : ''}; regenerate after the tag is cut for final content.`
421
- : '';
422
- const { rendered, missing, filesRead } = await renderTemplate(template, {
423
- name: product?.name ?? 'Unknown product',
424
- github_repository_full_name: product?.github_repository_full_name ?? null,
425
- }, {
426
- tag: release.tag,
427
- name: release.name,
428
- body: release.body,
429
- url: release.url,
430
- published_at: release.published_at,
431
- previous_tag: release.previous_tag,
432
- previous_published_at: release.previous_published_at,
433
- diff_summary: isDraft
434
- ? (draftDiffSummary ?? release.diff_summary)
435
- : release.diff_summary,
436
- diff_stats: isDraft
437
- ? (draftDiffStats ?? release.diff_stats ?? {})
438
- : (release.diff_stats ?? {}),
439
- }, repoDir, commits, draftNotice, commitsList);
440
- const inputHash = computeInputHash({
441
- template,
442
- tag: release.tag,
443
- previousTag: release.previous_tag,
444
- filesRead,
445
- commitShas: commitsList.map((c) => c.sha),
446
- });
447
323
  try {
324
+ const agentResult = await generateRunSheetWithAgent({
325
+ product: {
326
+ name: product?.name ?? 'Unknown product',
327
+ github_repository_full_name: product?.github_repository_full_name ?? null,
328
+ },
329
+ release: {
330
+ tag: release.tag,
331
+ name: release.name,
332
+ body: release.body,
333
+ url: release.url,
334
+ published_at: release.published_at,
335
+ previous_tag: release.previous_tag,
336
+ previous_published_at: release.previous_published_at,
337
+ },
338
+ template,
339
+ repoDir,
340
+ isDraft,
341
+ draftBaseRef,
342
+ verbose,
343
+ });
448
344
  const saved = await upsertRunSheet({
449
345
  release_id: release.id,
450
- content: rendered,
346
+ content: agentResult.content,
451
347
  title: `Run Sheet — ${product?.name ?? ''} ${release.tag}`.trim(),
452
348
  template_snapshot: template,
453
349
  metadata: {
454
- missing_placeholders: missing,
455
350
  clone_error: cloneError,
456
- commits_error: commitsError,
457
351
  repo: product?.github_repository_full_name ?? null,
458
352
  tag: release.tag,
459
- commits_truncated: commitsTruncated,
460
353
  is_draft: isDraft,
461
354
  draft_base_ref: draftBaseRef,
462
- input_hash: inputHash,
463
- commits_count: commitsList.length,
355
+ model: agentResult.model,
356
+ agent_turns: agentResult.turns,
357
+ agent_subtype: agentResult.subtype,
358
+ agent_error: agentResult.error,
464
359
  },
465
360
  generated_at: new Date().toISOString(),
466
361
  }, verbose);
@@ -470,22 +365,21 @@ export async function runRunSheet(options) {
470
365
  releaseId: release.id,
471
366
  releaseTag: release.tag,
472
367
  runSheetId: saved.id,
473
- summary: `Rendered ${rendered.length} characters`,
474
- missingPlaceholders: missing,
368
+ summary: `Rendered ${agentResult.content.length} characters (${agentResult.turns} agent turn(s))`,
475
369
  cloneError,
476
- commitsError,
477
- commitsTruncated,
478
370
  isDraft,
371
+ agentTurns: agentResult.turns,
372
+ model: agentResult.model,
479
373
  };
480
374
  }
481
375
  catch (err) {
482
376
  const message = err instanceof Error ? err.message : String(err);
483
- logError(`Failed to upsert run sheet: ${message}`);
377
+ logError(`Run-sheet generation failed: ${message}`);
484
378
  return {
485
379
  status: 'error',
486
380
  releaseId: release.id,
487
381
  releaseTag: release.tag,
488
- summary: `Failed to upsert run sheet: ${message}`,
382
+ summary: `Run-sheet generation failed: ${message}`,
489
383
  };
490
384
  }
491
385
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.48.1",
3
+ "version": "0.49.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"