@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.
- package/lib/init.mjs +4 -1
- package/lib/payload.mjs +18 -1
- package/lib/payload.test.mjs +55 -0
- package/lib/scaffold.mjs +23 -6
- package/lib/scaffold.test.mjs +59 -0
- package/package.json +1 -1
- package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
- package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
- package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
- package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
- package/template/{.claude → _claude}/scripts/chat-record.mjs +37 -4
- package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
- package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
- package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
- package/template/_claude/scripts/task-pr.mjs +447 -0
- package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
- package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
- package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/complete-work/SKILL.md +13 -69
- package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
- package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
- package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
- package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
- package/template/{.claude → _claude}/skills/start-work/SKILL.md +2 -2
- package/template/workspace.json.tmpl +1 -1
- /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/implementer.md +0 -0
- /package/template/{.claude → _claude}/agents/researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
- /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
- /package/template/{.claude → _claude}/lib/freshness.mjs +0 -0
- /package/template/{.claude → _claude}/lib/registry-check.mjs +0 -0
- /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
- /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
- /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
- /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
- /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
- /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
- /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
- /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
- /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
- /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-claude-md-freshness-include.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/task-worktree.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
- /package/template/{.claude → _claude}/settings.json +0 -0
- /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/workspace-update/SKILL.md +0 -0
- /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,
|
|
3
|
-
//
|
|
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
|
-
*
|
|
130
|
-
* `
|
|
131
|
-
*
|
|
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
|
|
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
|
-
|
|
197
|
+
always.push({ abs: claudeMd, kind: 'claude-md' });
|
|
142
198
|
for (const imp of resolveImports(claudeMd, visited, missingImports)) {
|
|
143
|
-
|
|
199
|
+
always.push({ abs: imp, kind: 'import' });
|
|
144
200
|
}
|
|
145
201
|
}
|
|
146
202
|
|
|
147
|
-
for (const r of collectRules(absRoot))
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
310
|
+
lines.push(row(f.bytes, f.kind, f.path));
|
|
235
311
|
}
|
|
236
312
|
lines.push('-'.repeat(60));
|
|
237
313
|
lines.push(
|
|
238
|
-
|
|
239
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
-
// `
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|