@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.19.0-beta.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.
Files changed (103) hide show
  1. package/lib/init.mjs +4 -1
  2. package/lib/payload.mjs +18 -1
  3. package/lib/payload.test.mjs +55 -0
  4. package/lib/scaffold.mjs +23 -6
  5. package/lib/scaffold.test.mjs +59 -0
  6. package/package.json +1 -1
  7. package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
  8. package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
  9. package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
  10. package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
  11. package/template/{.claude → _claude}/scripts/chat-record.mjs +37 -4
  12. package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
  13. package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
  14. package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
  15. package/template/_claude/scripts/task-pr.mjs +447 -0
  16. package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
  17. package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
  18. package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
  19. package/template/{.claude → _claude}/skills/complete-work/SKILL.md +13 -69
  20. package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
  21. package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
  22. package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
  23. package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
  24. package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
  25. package/template/{.claude → _claude}/skills/start-work/SKILL.md +2 -2
  26. package/template/workspace.json.tmpl +1 -1
  27. /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
  28. /package/template/{.claude → _claude}/agents/implementer.md +0 -0
  29. /package/template/{.claude → _claude}/agents/researcher.md +0 -0
  30. /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
  31. /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
  32. /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
  33. /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
  34. /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
  35. /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
  36. /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
  37. /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
  38. /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
  39. /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
  40. /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
  41. /package/template/{.claude → _claude}/lib/freshness.mjs +0 -0
  42. /package/template/{.claude → _claude}/lib/registry-check.mjs +0 -0
  43. /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
  44. /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
  45. /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
  46. /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
  47. /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
  48. /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
  49. /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
  50. /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
  51. /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
  52. /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
  53. /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
  54. /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
  55. /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
  56. /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
  57. /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
  58. /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
  59. /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
  60. /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
  61. /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
  62. /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
  63. /package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +0 -0
  64. /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
  65. /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
  66. /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
  67. /package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +0 -0
  68. /package/template/{.claude → _claude}/scripts/migrate-claude-md-freshness-include.mjs +0 -0
  69. /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
  70. /package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +0 -0
  71. /package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +0 -0
  72. /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
  73. /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
  74. /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
  75. /package/template/{.claude → _claude}/scripts/task-worktree.mjs +0 -0
  76. /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
  77. /package/template/{.claude → _claude}/settings.json +0 -0
  78. /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
  79. /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
  80. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
  81. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
  82. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
  83. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
  84. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
  85. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
  86. /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
  87. /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
  88. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
  89. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
  90. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
  91. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
  92. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
  93. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
  94. /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
  95. /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
  96. /package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +0 -0
  97. /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
  98. /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
  99. /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
  100. /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
  101. /package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +0 -0
  102. /package/template/{.claude → _claude}/skills/workspace-update/SKILL.md +0 -0
  103. /package/template/{.mcp.json → _mcp.json} +0 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Measure the always-loaded context footprint of a workspace, and price a
3
- // proposed addition before it is written.
2
+ // Measure the always-loaded context footprint of a workspace, price a proposed
3
+ // addition before it is written, and check the total against a budget.
4
4
  //
5
5
  // Every unconditional rule and every locked context file is a permanent tax on
6
6
  // every session in this workspace — and, for anything shipped in the template,
@@ -11,11 +11,23 @@
11
11
  // `memory-guidance` rule both require running it before writing to an
12
12
  // always-loaded destination.
13
13
  //
14
+ // A rule whose frontmatter declares `paths:` is conditional — Claude Code loads
15
+ // it only when a file matching one of its globs is read — so it is listed under
16
+ // `conditional`, excluded from the total, and priced at zero by the
17
+ // `rule-scoped` destination.
18
+ //
19
+ // The budget is `workspace.alwaysLoadedBudgetBytes` from <root>/workspace.json
20
+ // (absent means no budget); `--budget <bytes>` overrides it. With a budget set,
21
+ // human output ends with a `BUDGET <total>/<budget> bytes — ok|OVER` line, JSON
22
+ // gains `budget` and `overBudget`, a projection reports whether the addition
23
+ // lands over, and the process exits 1 when the measured total is over.
24
+ //
14
25
  // Reads only. Writes nothing. Makes no network calls.
15
26
  //
16
27
  // Usage:
17
28
  // node context-footprint.mjs --root <dir>
18
29
  // node context-footprint.mjs --root <dir> --json
30
+ // node context-footprint.mjs --root <dir> --budget <bytes>
19
31
  // node context-footprint.mjs --root <dir> --add <bytes> --as <destination>
20
32
  //
21
33
  // Destinations for --as: rule, rule-scoped, locked, shared, team-member,
@@ -123,38 +135,86 @@ function collectRules(absRoot) {
123
135
  .map((n) => join(rulesDir, n));
124
136
  }
125
137
 
138
+ /**
139
+ * Does this text carry YAML frontmatter with a top-level `paths:` key?
140
+ *
141
+ * Frontmatter on rules is a flat, hand-written key block, so a deliberately
142
+ * naive scan — opening `---` line, closing `---` line, any top-level `paths:`
143
+ * key between them — is enough. A YAML library would buy fidelity the one
144
+ * decision this feeds (conditional vs always-loaded) never needs.
145
+ */
146
+ function frontmatterHasPaths(text) {
147
+ const lines = /^---\r?\n/.test(text) ? text.split(/\r?\n/) : null;
148
+ if (!lines) return false;
149
+ const close = lines.findIndex((line, i) => i > 0 && (line === '---' || line === '...'));
150
+ if (close === -1) return false; // no closing delimiter: not frontmatter
151
+ return lines.slice(1, close).some((line) => /^paths:/.test(line));
152
+ }
153
+
154
+ function ruleIsConditional(absRule) {
155
+ try {
156
+ return frontmatterHasPaths(readFileSync(absRule, 'utf8'));
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * The always-loaded budget from <root>/workspace.json, or null when the file or
164
+ * the `workspace.alwaysLoadedBudgetBytes` field is absent. A present but
165
+ * malformed workspace.json throws — silently ignoring a corrupt config would
166
+ * report "no budget" for a workspace that tried to set one.
167
+ */
168
+ function readBudget(absRoot) {
169
+ const configPath = join(absRoot, 'workspace.json');
170
+ if (!existsSync(configPath)) return null;
171
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
172
+ const budget = raw?.workspace?.alwaysLoadedBudgetBytes;
173
+ return typeof budget === 'number' && Number.isFinite(budget) && budget >= 0 ? budget : null;
174
+ }
175
+
126
176
  /**
127
177
  * Measure the always-loaded set under `root`.
128
178
  *
129
- * CLAUDE.local.md and anything it imports are reported separately under
130
- * `local`: they are per-user and gitignored, so folding them into the shared
131
- * total would overstate what the team actually pays.
179
+ * Three groups, kept separate because they cost different things:
180
+ * - `files` / `totalBytes` — CLAUDE.md, its @-imports, and unconditional
181
+ * rules: what every session pays, and the number the budget judges.
182
+ * - `conditional` — rules with `paths:` frontmatter, reported with kind
183
+ * `rule-scoped` but excluded from the total: they load only when Claude
184
+ * touches a file matching one of their globs.
185
+ * - `local` — CLAUDE.local.md and its imports: per-user and gitignored, so
186
+ * folding them into the shared total would overstate what the team pays.
132
187
  */
133
188
  function measure({ root = '.' } = {}) {
134
189
  const absRoot = resolve(root);
135
- const files = [];
190
+ const always = [];
191
+ const conditional = [];
136
192
  const missingImports = [];
137
193
 
138
194
  const claudeMd = join(absRoot, 'CLAUDE.md');
139
195
  if (existsSync(claudeMd)) {
140
196
  const visited = new Set([claudeMd]);
141
- files.push({ abs: claudeMd, kind: 'claude-md' });
197
+ always.push({ abs: claudeMd, kind: 'claude-md' });
142
198
  for (const imp of resolveImports(claudeMd, visited, missingImports)) {
143
- files.push({ abs: imp, kind: 'import' });
199
+ always.push({ abs: imp, kind: 'import' });
144
200
  }
145
201
  }
146
202
 
147
- for (const r of collectRules(absRoot)) files.push({ abs: r, kind: 'rule' });
148
-
149
- const entries = [];
150
- let totalBytes = 0;
151
- for (const f of files) {
152
- const bytes = sizeOf(f.abs);
153
- if (bytes === null) continue;
154
- totalBytes += bytes;
155
- entries.push({ path: toPosix(relative(absRoot, f.abs)), bytes, kind: f.kind });
203
+ for (const r of collectRules(absRoot)) {
204
+ if (ruleIsConditional(r)) conditional.push({ abs: r, kind: 'rule-scoped' });
205
+ else always.push({ abs: r, kind: 'rule' });
156
206
  }
157
- entries.sort((a, b) => b.bytes - a.bytes);
207
+
208
+ const toEntries = (group) => group
209
+ .map((f) => {
210
+ const bytes = sizeOf(f.abs);
211
+ return bytes === null ? null : { path: toPosix(relative(absRoot, f.abs)), bytes, kind: f.kind };
212
+ })
213
+ .filter((e) => e !== null)
214
+ .sort((a, b) => b.bytes - a.bytes);
215
+
216
+ const entries = toEntries(always);
217
+ const conditionalEntries = toEntries(conditional);
158
218
 
159
219
  const localEntries = [];
160
220
  let localBytes = 0;
@@ -172,6 +232,7 @@ function measure({ root = '.' } = {}) {
172
232
  localEntries.sort((a, b) => b.bytes - a.bytes);
173
233
  }
174
234
 
235
+ const totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
175
236
  const totalTokens = Math.round(totalBytes / BYTES_PER_TOKEN);
176
237
  return {
177
238
  root: absRoot,
@@ -179,18 +240,22 @@ function measure({ root = '.' } = {}) {
179
240
  totalTokens,
180
241
  percentOfWindow: Number(((totalTokens / CONTEXT_WINDOW) * 100).toFixed(1)),
181
242
  files: entries,
243
+ conditional: {
244
+ totalBytes: conditionalEntries.reduce((sum, e) => sum + e.bytes, 0),
245
+ files: conditionalEntries,
246
+ },
182
247
  missingImports,
183
248
  local: { totalBytes: localBytes, files: localEntries },
184
249
  };
185
250
  }
186
251
 
187
- function projectCost(measurement, addedBytes, destination) {
252
+ function projectCost(measurement, addedBytes, destination, budgetBytes = null) {
188
253
  const dest = DESTINATIONS[destination];
189
254
  if (!dest) throw new Error(`unknown destination: ${destination}`);
190
255
  const delta = dest.alwaysLoadedCost(addedBytes);
191
256
  const newTotalBytes = measurement.totalBytes + delta;
192
257
  const newTokens = Math.round(newTotalBytes / BYTES_PER_TOKEN);
193
- return {
258
+ const projection = {
194
259
  destination,
195
260
  addedBytes,
196
261
  alwaysLoadedDelta: delta,
@@ -198,10 +263,15 @@ function projectCost(measurement, addedBytes, destination) {
198
263
  newPercentOfWindow: Number(((newTokens / CONTEXT_WINDOW) * 100).toFixed(1)),
199
264
  note: dest.note,
200
265
  };
266
+ if (budgetBytes !== null) {
267
+ projection.budgetBytes = budgetBytes;
268
+ projection.overBudgetAfter = newTotalBytes > budgetBytes;
269
+ }
270
+ return projection;
201
271
  }
202
272
 
203
273
  function parseArgs(argv) {
204
- const args = { root: '.', json: false, add: null, as: null };
274
+ const args = { root: '.', json: false, add: null, as: null, budget: null };
205
275
  const rest = argv.slice(2);
206
276
  for (let i = 0; i < rest.length; i += 1) {
207
277
  const a = rest[i];
@@ -209,6 +279,7 @@ function parseArgs(argv) {
209
279
  if (a === '--json') { args.json = true; continue; }
210
280
  if (a === '--add') { args.add = Number(rest[++i]); continue; }
211
281
  if (a === '--as') { args.as = rest[++i]; continue; }
282
+ if (a === '--budget') { args.budget = Number(rest[++i]); continue; }
212
283
  throw new Error(`unknown argument: ${a}`);
213
284
  }
214
285
  if (args.add !== null && args.as === null) {
@@ -225,25 +296,42 @@ function parseArgs(argv) {
225
296
  `unknown destination: ${args.as}. Valid: ${Object.keys(DESTINATIONS).join(', ')}`,
226
297
  );
227
298
  }
299
+ if (args.budget !== null && (!Number.isFinite(args.budget) || args.budget < 0)) {
300
+ throw new Error('--budget expects a non-negative number of bytes');
301
+ }
228
302
  return args;
229
303
  }
230
304
 
231
- function renderHuman(m, projection) {
305
+ function renderHuman(m, budget, projection) {
306
+ const row = (bytes, label, rest) =>
307
+ `${String(bytes).padStart(7)} ${label.padEnd(12)} ${rest}`;
232
308
  const lines = [];
233
309
  for (const f of m.files) {
234
- lines.push(`${String(f.bytes).padStart(7)} ${f.kind.padEnd(9)} ${f.path}`);
310
+ lines.push(row(f.bytes, f.kind, f.path));
235
311
  }
236
312
  lines.push('-'.repeat(60));
237
313
  lines.push(
238
- `${String(m.totalBytes).padStart(7)} TOTAL ~${m.totalTokens} tokens, ` +
239
- `${m.percentOfWindow}% of a ${CONTEXT_WINDOW / 1000}k window`,
314
+ row(m.totalBytes, 'TOTAL',
315
+ `~${m.totalTokens} tokens, ${m.percentOfWindow}% of a ${CONTEXT_WINDOW / 1000}k window`),
240
316
  );
241
317
  if (m.local.totalBytes > 0) {
242
- lines.push(`${String(m.local.totalBytes).padStart(7)} local (per-user, not counted above)`);
318
+ lines.push(row(m.local.totalBytes, 'local', '(per-user, not counted above)'));
319
+ }
320
+ if (m.conditional.totalBytes > 0) {
321
+ lines.push('');
322
+ lines.push('conditional (loads only on matching paths, not counted above):');
323
+ for (const f of m.conditional.files) {
324
+ lines.push(row(f.bytes, f.kind, f.path));
325
+ }
326
+ lines.push(row(m.conditional.totalBytes, 'scoped', '(conditional rules, not counted above)'));
243
327
  }
244
328
  if (m.missingImports.length > 0) {
245
329
  lines.push(` dangling @-imports: ${m.missingImports.join(', ')}`);
246
330
  }
331
+ if (budget) {
332
+ lines.push('');
333
+ lines.push(`BUDGET ${m.totalBytes}/${budget.bytes} bytes — ${budget.overBudget ? 'OVER' : 'ok'}`);
334
+ }
247
335
  if (projection) {
248
336
  lines.push('');
249
337
  lines.push(
@@ -254,6 +342,13 @@ function renderHuman(m, projection) {
254
342
  `${m.totalBytes} B (${m.percentOfWindow}%) -> ` +
255
343
  `${projection.newTotalBytes} B (${projection.newPercentOfWindow}%)`,
256
344
  );
345
+ if (projection.overBudgetAfter !== undefined) {
346
+ lines.push(
347
+ projection.overBudgetAfter
348
+ ? `over budget: ${projection.newTotalBytes} > ${projection.budgetBytes} B`
349
+ : `within budget: ${projection.newTotalBytes} / ${projection.budgetBytes} B`,
350
+ );
351
+ }
257
352
  lines.push(projection.note);
258
353
  }
259
354
  return lines.join('\n');
@@ -262,12 +357,23 @@ function renderHuman(m, projection) {
262
357
  function main() {
263
358
  const args = parseArgs(process.argv);
264
359
  const m = measure({ root: args.root });
265
- const projection = args.add !== null ? projectCost(m, args.add, args.as) : null;
360
+ const budgetBytes = args.budget !== null ? args.budget : readBudget(m.root);
361
+ const budget = budgetBytes === null
362
+ ? null
363
+ : { bytes: budgetBytes, overBudget: m.totalBytes > budgetBytes };
364
+ const projection = args.add !== null ? projectCost(m, args.add, args.as, budgetBytes) : null;
266
365
  if (args.json) {
267
- process.stdout.write(JSON.stringify({ ...m, projection }, null, 2) + '\n');
366
+ process.stdout.write(
367
+ JSON.stringify(
368
+ { ...m, budget: budgetBytes, overBudget: budget ? budget.overBudget : false, projection },
369
+ null,
370
+ 2,
371
+ ) + '\n',
372
+ );
268
373
  } else {
269
- process.stdout.write(renderHuman(m, projection) + '\n');
374
+ process.stdout.write(renderHuman(m, budget, projection) + '\n');
270
375
  }
376
+ if (budget?.overBudget) process.exitCode = 1;
271
377
  }
272
378
 
273
379
  if (isMainModule(import.meta.url)) {
@@ -279,4 +385,7 @@ if (isMainModule(import.meta.url)) {
279
385
  }
280
386
  }
281
387
 
282
- export { measure, projectCost, parseArgs, resolveImports, DESTINATIONS, BYTES_PER_TOKEN, CONTEXT_WINDOW };
388
+ export {
389
+ measure, projectCost, parseArgs, resolveImports, readBudget, frontmatterHasPaths,
390
+ DESTINATIONS, BYTES_PER_TOKEN, CONTEXT_WINDOW,
391
+ };
@@ -114,7 +114,7 @@ export function createGithubAdapter(config, { spawnFn = nodeSpawnSync } = {}) {
114
114
  // complete rather than merely empty (gh:89). `search` takes gh's raw search
115
115
  // syntax so callers can bound by merge date without this adapter growing a
116
116
  // date-range vocabulary of its own.
117
- async function prList({ state = 'merged', base, search, limit = 100, repo }) {
117
+ async function prList({ state = 'merged', base, head, search, limit = 100, repo }) {
118
118
  const target = repoFor(repo);
119
119
  const args = [
120
120
  'pr', 'list', '--repo', target,
@@ -123,6 +123,7 @@ export function createGithubAdapter(config, { spawnFn = nodeSpawnSync } = {}) {
123
123
  '--json', 'number,title,url,headRefName,baseRefName,mergedAt,state',
124
124
  ];
125
125
  if (base) args.push('--base', base);
126
+ if (head) args.push('--head', head);
126
127
  if (search) args.push('--search', search);
127
128
  const stdout = ghOrThrow(args).trim();
128
129
  const raw = stdout ? JSON.parse(stdout) : [];
@@ -17,11 +17,12 @@
17
17
  // prView({ id, repo?, json? })
18
18
  // → { id, url, state, mergeable, mergeStateStatus, reviewDecision, title }
19
19
  // json may name additional fields to pass through
20
- // prList({ state = 'merged', base?, search?, limit = 100, repo? })
20
+ // prList({ state = 'merged', base?, head?, search?, limit = 100, repo? })
21
21
  // → [{ id, number, title, url, headRefName, baseRefName, mergedAt, state }]
22
- // `search` passes through the forge's own search syntax (e.g.
23
- // 'merged:>2026-01-01'), so callers can bound a window without this
24
- // interface growing a date vocabulary.
22
+ // `base`/`head` filter by target/source branch (e.g. the open PR for a
23
+ // task branch); `search` passes through the forge's own search syntax
24
+ // (e.g. 'merged:>2026-01-01'), so callers can bound a window without
25
+ // this interface growing a date vocabulary.
25
26
  // releaseView({ tag, repo? })
26
27
  // → { tag, url, name, publishedAt }
27
28
  // throws ReleaseNotFound if the tag has no release