Files
msd-core/tests/agent-size-budget.test.cjs
Tom Boucher 79002a00cb chore(#518): rename npm package + bin to @opengsd/gsd-core (#519)
* chore: rename npm package + bin to @opengsd/gsd-core (functional)

- package.json: name @opengsd/get-shit-done-redux → @opengsd/gsd-core,
  bin key get-shit-done-redux → gsd-core, repository/homepage/bugs URLs
- package-lock.json: regenerated (npm install --package-lock-only)
- tests/**, scripts/**, bin/**, .github/**, agents/**, commands/**,
  get-shit-done/bin/**, get-shit-done/workflows/**:
  applied the 4-rule replacement (scoped npm ref, GitHub repo path,
  bin/clone invocations) per #505 single-source refactor

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: sweep live references to @opengsd/gsd-core

Update all live documentation (README.md + translations, docs/**,
CONTRIBUTING.md, VERSIONING.md, SECURITY.md, CONTEXT.md,
docs/CANARY.md) to reflect the renamed package and repository.

Rules applied:
- @opengsd/get-shit-done-redux → @opengsd/gsd-core (scoped npm name)
- open-gsd/get-shit-done-redux → open-gsd/gsd-core (GitHub repo)
- GSD-redux/get-shit-done-redux → open-gsd/gsd-core (stale badge org)
- bare bin/clone refs → gsd-core

CHANGELOG.md, docs/adr/**, docs/RELEASE-*.md, docs/research/**,
and .changeset/** are preserved byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: add negative lookbehind to slash-command regex in bug-2954 test

The extractSlashReferences regex matched /gsd-core inside npm package
URLs (@opengsd/gsd-core), producing a false /gsd:core command reference.
Adding a negative lookbehind (?<![a-z]) excludes matches preceded by a
letter, so only standalone /gsd-<cmd> and /gsd:<cmd> tokens are found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#518): add changeset for package rename

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#518): update package-identity expectations to the renamed coordinates

The rebase regenerated the seam to @opengsd/gsd-core (bin gsd-core, repo
open-gsd/gsd-core). The #498 seam tests assert deriveIdentity against the REAL
package.json, so their expected literals must follow the rename. The drift-lint
unit test is left as-is — its SEAM is a self-consistent fixture and its
stale-literal detection cases would shift if altered; the live-repo scan in it
already passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 17:25:02 -04:00

118 lines
3.7 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// Workflow .md / agent .md / command .md / reference .md files — their text
// IS what the runtime loads. Testing text content tests the deployed contract.
// Per CONTRIBUTING.md exception matrix.
/**
* Agent size budget.
*
* Agent definitions in `agents/gsd-*.md` are loaded verbatim into Claude's
* context on every subagent dispatch. Unbounded growth is paid on every call
* across every workflow.
*
* Budgets are tiered to reflect the intent of each agent class:
* - XL : top-level orchestrators that own end-to-end rubrics
* - LARGE : multi-phase operators with branching workflows
* - DEFAULT : focused single-purpose agents
*
* Raising a budget is a deliberate choice — adjust the constant, write a
* rationale in the PR, and make sure the bloat is not duplicated content
* that belongs in `get-shit-done/references/`.
*
* See: https://github.com/open-gsd/gsd-core/issues/2361
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const XL_BUDGET = 1600;
const LARGE_BUDGET = 1000;
const DEFAULT_BUDGET = 500;
const XL_AGENTS = new Set([
'gsd-debugger',
'gsd-planner',
]);
const LARGE_AGENTS = new Set([
'gsd-phase-researcher',
'gsd-verifier',
'gsd-doc-writer',
'gsd-plan-checker',
'gsd-executor',
'gsd-code-fixer',
'gsd-codebase-mapper',
'gsd-project-researcher',
'gsd-roadmapper',
]);
const ALL_AGENTS = fs.readdirSync(AGENTS_DIR)
.filter(f => f.startsWith('gsd-') && f.endsWith('.md'))
.map(f => f.replace('.md', ''));
function budgetFor(agent) {
if (XL_AGENTS.has(agent)) return { tier: 'XL', limit: XL_BUDGET };
if (LARGE_AGENTS.has(agent)) return { tier: 'LARGE', limit: LARGE_BUDGET };
return { tier: 'DEFAULT', limit: DEFAULT_BUDGET };
}
function lineCount(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
if (content.length === 0) return 0;
const trailingNewline = content.endsWith('\n') ? 1 : 0;
return content.split('\n').length - trailingNewline;
}
describe('SIZE: agent line-count budget', () => {
for (const agent of ALL_AGENTS) {
const { tier, limit } = budgetFor(agent);
test(`${agent} (${tier}) stays under ${limit} lines`, () => {
const filePath = path.join(AGENTS_DIR, agent + '.md');
const lines = lineCount(filePath);
assert.ok(
lines <= limit,
`${agent}.md has ${lines} lines — exceeds ${tier} budget of ${limit}. ` +
`Extract shared boilerplate to get-shit-done/references/ or raise the budget ` +
`in tests/agent-size-budget.test.cjs with a rationale.`
);
});
}
});
describe('SIZE: every agent is classified', () => {
test('every agent falls in exactly one tier', () => {
for (const agent of ALL_AGENTS) {
const inXL = XL_AGENTS.has(agent);
const inLarge = LARGE_AGENTS.has(agent);
assert.ok(
!(inXL && inLarge),
`${agent} is in both XL_AGENTS and LARGE_AGENTS — pick one`
);
}
});
test('every named XL agent exists', () => {
for (const agent of XL_AGENTS) {
const filePath = path.join(AGENTS_DIR, agent + '.md');
assert.ok(
fs.existsSync(filePath),
`XL_AGENTS references ${agent}.md which does not exist — clean up the set`
);
}
});
test('every named LARGE agent exists', () => {
for (const agent of LARGE_AGENTS) {
const filePath = path.join(AGENTS_DIR, agent + '.md');
assert.ok(
fs.existsSync(filePath),
`LARGE_AGENTS references ${agent}.md which does not exist — clean up the set`
);
}
});
});