Files
msd-core/tests/effort-sync-installed-runtime.test.cjs
Tom Boucher 382bf7c423 fix(#3706): deliver the resolved reasoning effort to OpenCode subagents (#3867)
* test(#3706): failing-first coverage for OpenCode variant emission and frontmatter escaping

* fix(#3706): emit the resolved reasoning effort as OpenCode's variant key

`query resolve-execution` resolved an effort level for every agent, but the
OpenCode bake wrote only `model:` — the effort never reached the generated
agent, so subagents ran at whatever the runtime defaulted the model to. This
is the effort-side twin of the model-side defect fixed in #3705.

The key is written only when an `effort` block is actually configured.
`resolveInstallTimeEffort` always returns a level (the catalog default is
`high`), so gating on its return value would stamp `variant: high` into every
existing OpenCode install — and OpenCode resolves a variant name against a
`variants` map in the user's `opencode.jsonc`, so a value nobody declared is
not a safe default. Gating on `readGsdEffectiveEffortConfig` keeps installs
that never asked for effort routing byte-identical.

Kilo does not receive the key: `EFFORT_ARGV` declares surfaces for claude,
opencode and codex and has no kilo entry. This is deliberately asymmetric with
the model side, where #2794 J8 requires the two runtimes to resolve alike.

Both frontmatter sinks now route through `frontmatterScalar`, which quotes and
escapes any value that is not a plain scalar. The raw interpolation predates
this change, but it was already shown by execution during the #3705 security
review to let a config value containing a newline inject additional top-level
keys (`tools:`, `permission:`) into a generated agent file. This change adds a
second write to that sink, so it is closed here rather than doubled.

* fix(#3706): quote frontmatter values YAML would not read back verbatim

Self-review of the predicate added in the previous commit. Treating
/^[A-Za-z0-9._:/@+-]+$/ as 'safe to emit bare' answers the wrong question:
a value can match it and still not round-trip.

  - A leading '@' is a YAML *reserved* indicator and may not open a plain
    scalar at all, so a scoped ID like '@org/model' emitted bare is a parse
    error, not an ambiguity — the whole agent file becomes unreadable.
  - 'no' / 'y' / 'off' / 'null' resolve to booleans and null, so a variant
    with one of those names would match no entry in the user's variants map.
  - '12:30' resolves to 750 under YAML 1.1 sexagesimal, and ':' is legal
    mid-identifier here, so the form is reachable rather than contrived.

Real model IDs pass every clause and stay bare, so already-generated files
remain byte-identical.

* fix(#3706): route variant through the declared effort seam and cover the live path

Addresses six findings from the isolated review, all confirmed by execution.

The tests were the serious one: they required `../bin/install.js` while the fix
landed in src/, which compiles to gsd-core/bin/lib/. They exercised a different
copy of the converter than the one the bake actually uses, so the whole suite
was green-by-construction against unchanged code and the remote run failed all
13. Every case now runs against BOTH copies from one table, which doubles as the
parity assertion the generative-fix note in runtime-artifact-conversion.cts asks
for, and bin/install.js carries the mirrored change.

Emission no longer hand-rolls the value. It goes through `renderEffortArgv`,
the declared OpenCode effort seam (EFFORT_ARGV.opencode: its own supported set
and clamp). That is what rejects a level that is not a wire value — above all
`inherit`, which per #3533 (10d) means "omit the key and follow the host
default" and was previously written literally, naming a variant that cannot
resolve. Reachable two ways, both now pinned: an agent_overrides entry and a
routing_tier_defaults entry. A bare effort.default does NOT reach a tiered
agent (the #3531 tier ladder answers first), so a test written against
`default` alone asserts nothing — that is pinned too.

The plain-scalar decision moved into frontmatter.cts beside
`scalarNeedsDoubleQuoting` rather than sitting next to it as a second, weaker
predicate. `agentScalarNeedsDoubleQuoting` is a documented superset: it adds a
trailing `:` (read as a nested mapping key, which fails the whole frontmatter),
boolean/null words, and numeric-looking values including YAML 1.1 sexagesimal.

Docs now state the cascade plainly: the gate is on effort being configured at
all, not on the individual agent being named, so every generated OpenCode agent
gets a variant line once any effort block exists.

* test(#3706): assert the two frontmatterScalar copies cannot diverge

A hand-picked adversarial corpus plus a fast-check property over
YAML-significant strings, both run against bin/install.js and the live
src copy. Verified the property can actually fail: mutating one copy's
quoting rule is killed well inside the run budget.

* fix(#3706): close the review findings — predicate, seam, and dead mirror

Third review round; every item below was confirmed by execution.

The scalar predicate was wrong in two families, both found by a round-trip
property test rather than by reading. Basing it on scalarNeedsDoubleQuoting
dropped the "first character must be alphanumeric" clause, so `~`, `.inf`,
`.nan`, `+1`, `-0` and `.5` went out bare and came back as null/floats/ints;
and that base predicate only inspects the FIRST character, so an embedded `: `
(a nested mapping, i.e. a parse error) or ` #` (a comment, i.e. silent
truncation) also passed. Dates round out the set: `2026-08-25` opens
alphanumeric, survives every other clause, and YAML resolves it to a Date.
The property now asserts the contract directly over generated values instead
of trusting an enumerated character list.

The bin/install.js mirror is gone. Its premise was false — install.js already
requires bin/lib at :65 — and it was unreachable besides: install.js's
convertClaudeToOpencodeFrontmatter has no `isAgent: true` call site, because
its agents path resolves converters from the compiled module. It was a third
copy of the YAML rules serving a test rather than a caller, so the file is
back to origin/next and the tests target the live copy only.

Effort clamping moved to `clampEffortForHost`, which renderEffortArgv now
delegates to. The layout was calling renderEffortArgv with a hardcoded 'argv'
to borrow its clamp, which read as if the frontmatter key were gated on the
invocation-time axis. It is not: claude declares effortSurface "argv" and
independently bakes an effort: key. One capability table, one clamp, two
channels that no longer pretend to be each other.

Also corrects an earlier claim of mine: adding EFFORT_RENDERING.opencode would
NOT have made `effort sync` write the wrong key, because it guards on the
runtime name before it ever renders. The seam choice stands on other grounds.
`effort sync` still skips OpenCode, but its stated reason claimed OpenCode
"does not use effort: frontmatter", which this change makes false — so the
message now says what is actually true.

* docs(#3706): restate the changeset around the round-trip contract

* fix(#3706): restore the changeset fragment belonging to #3809

An earlier commit in this branch picked the first file in .changeset/ by
glob order instead of the fragment created for this issue, and overwrote
agile-geese-squeak.md (PR 3815 / #3809) with this change's body. Restored
verbatim from origin/next; this change's text now lives in its own
patient-cranes-parade.md, where it was created.

* feat(#3706): maintain the OpenCode variant key from effort sync

Install bakes the resolved effort into OpenCode agent frontmatter as
`variant:`, so `effort sync` has to maintain it or a config change only takes
effect on reinstall — and its skip message claimed OpenCode does not use
frontmatter effort at all, which this issue made false.

cmdEffortSyncOpencode mirrors the codex branch: resolve per agent, clamp
through the declared OpenCode capability, then write, strip, or skip. A null
target means the key must not exist, which covers both "no effort configured"
and "resolved to inherit or to an unsupported level" — the same states under
which install writes nothing, so sync and install agree by construction.

The frontmatter line-editors are key-parameterised rather than copied:
setEffortFrontmatter / removeEffortFrontmatter are now thin wrappers over the
same internals the variant path uses, and a test pins that the claude `effort:`
behavior did not move. The child-process test harness fixes both HOME and
USERPROFILE, so the hermetic-config assertions cannot pass vacuously on Windows.

* fix(#3706): scope the frontmatter line editors to the matched block

Found by the security review of the sync path, reported as correctness rather
than vulnerability, and reproduced against pre-fix code before being fixed.

Both editors matched the frontmatter with a regex that can match a block after
a preamble, then derived the EOL and the opening-fence length from the START OF
THE FILE. On a CRLF document with a preamble those disagree, the offsets shift
by one byte, and the reassembled document comes back with a mangled fence
(`---\rname: x`). Both now take the EOL from the matched block.

`setFrontmatterKeyLine` additionally did a whole-file `/m` replace when the key
already existed, gated only on the key being present in the frontmatter body —
so a preamble line starting with the same key was rewritten instead of the
frontmatter one. It now replaces inside the frontmatter span only, which is the
hazard `removeFrontmatterKeyLine` already documented and guarded against.

Neither is reachable from an install-written `gsd-*.md` (those begin at byte 0
with `---`), and both predate this change — but the editors are in this diff
because #3706 key-parameterised them, so they are fixed here rather than left
for the next caller to trip over. Three regression tests, each confirmed to
fail against the pre-fix build.

* fix(#3706): treat a present-but-empty key as present, and pin the real seam

Fourth review round.

The MAJOR one: both sync branches read the current value with `(.+?)`, which
needs at least one character, so a key present with an EMPTY value read as
"key absent". When the target was also null the code concluded "already
correct" and skipped — leaving the key in the file, where it reads back as
YAML `null`: exactly the unresolvable-variant state this change exists to
prevent. Whitespace decided whether it fired, since `variant:   ` matched and
`variant:` did not. Presence and value are now separate questions at both the
opencode and the claude branch.

The OpenCode writer now follows the codex branch rather than the claude one:
tmp file plus retryRenameSync with orphan cleanup, and a write failure skips
that agent and is reported instead of aborting the sweep. Same granularity,
same transient-Windows-lock exposure, so the hardened sibling was the right
precedent.

Also: the generic line-editors escape their interpolated key, the JSDoc
stranded by the clampEffortForHost extraction is back on renderEffortArgv, and
a cast that declared a nullable function as non-nullable is corrected.

Tests close the gaps the review listed — empty value (both spellings), CRLF
round-trip through write and strip, the symlink guard, a body line starting
`variant:`, a file with no frontmatter, and the YAML classes that actually
broke the predicate. The new layout-seam test drives the real stage() path and
was verified to FAIL when `variant` is removed from the converter call; a seam
test that survives cutting the seam is worse than none.

* fix(#3706): clear the round-five review findings

No blockers or majors this round; the repo's review gate is zero-tolerance, so
the minors are cleared too.

A duplicated key was only half-stripped: the strip regex had no `g` flag, so a
frontmatter carrying the key twice lost one occurrence, reported success, and
left the "a null target means the key must not exist" invariant false on disk —
converging only on a second run. Such a document is already invalid YAML, so
this is robustness rather than a live corruption path, but a successful sync
has to leave the invariant true.

A run in which every write failed still summarised as `ok`, so a caller could
not tell "nothing to do" from "everything failed". The OpenCode branch now
reports `failed` when any write failed. The write-failure path was also the
newest code in the change with no coverage at all; it now has a test that
injects the failure by monkeypatching the write, per CLAUDE.md §4, rather than
by chmod — mode bits do not bite under root in CI.

`CodexEffortSyncWriteFailure` is renamed `EffortSyncWriteFailure` now that two
branches share it. Removed a guard on the claude concrete path that was
provably unreachable — no member of EFFORT_SET renders null there, so it read
as protection that did not exist. The claude inherit path's presence check is
load-bearing and untouched.

Three stale statements corrected: the OpenCode result shape matches codex's,
not claude's, now that it emits write_failures; the `thread()` test helper now
calls `clampEffortForHost` so it genuinely mirrors the layout instead of
merely claiming to; and a test helper restored `USERPROFILE` by assignment,
writing the literal string "undefined" into the environment on POSIX — it
deletes now.

* fix(#3706): converge the set path, degrade on unreadable files, preserve mode

Rounds five and six of review. No blockers or majors; the review gate is
zero-tolerance, so the minors are cleared too.

`setFrontmatterKeyLine` was the mirror of a defect already fixed in its
sibling: `remove` was made global, `set` was not, so on a frontmatter carrying
the key twice it rewrote the first and left a stale second. Last-wins YAML
readers honour the stale value while the sync's own first-occurrence read
reports "in sync" — permanently non-converging. It now collapses to exactly one
occurrence, in the position of the first, so ordinary single-occurrence
documents stay byte-identical (verified across seven shapes before and after).

An unreadable agent file used to throw and abort the entire sweep, while a
failed WRITE in the same loop degraded into a report. The OpenCode branch now
reports read failures alongside write failures; the claude branch degrades to a
skip without a new result field, because its shape is long-standing and widely
consumed and one bad file aborting the sweep is the actual defect.

The tmp+rename publish dropped the original file's mode — a plain writeFileSync
preserves it, a rename does not — so a 0600 agent came back 0644. Both the
OpenCode and the codex branch now carry the original's permission bits across
the publish, masked with 0o7777: the raw stat mode includes the file-type bits,
and POSIX leaves those unspecified for chmod. Linux is the only OS the remote
matrix runs, so relying on Darwin's tolerance would have been untestable here.

Also documents the `from` contract on EffortSyncChange (null means the key was
absent, '' means present with an empty value — a distinction earlier rounds
introduced and then collapsed in the output), adds OpenCode to the docs
paragraph enumerating where the key is omitted under inherit, and records in a
comment that the 'failed' summary reaches only raw mode and does not change the
exit code, which is a CLI-contract change affecting all three branches and is
deliberately not made here.

* fix(#3706): guard the codex read, close the tmp permission window, rename the failure type

Round seven, plus one thing I found myself.

`cmdEffortSyncCodex` still had an unguarded `fs.readFileSync` — a read fault on
one agent exited 1 and aborted the whole sweep. The claude and opencode
branches were both guarded earlier this round and codex was missed, with the
unguarded read sitting ten lines above the chmod block the previous commit did
edit. It now reports read failures the way the OpenCode branch does, and a read
failure flips its summary to `failed` — which write failures did not do there
either, so both are corrected for consistency.

The tmp file was created at the default mode and only tightened afterwards, so
a 0600 agent's contents sat in a 0644 file for the length of the publish. I
measured the window rather than assuming it, then closed it by passing the
mode at creation. The chmod after the write is deliberately RETAINED and
commented: the `mode` option only applies when the file is actually created, so
a leftover tmp from an earlier crashed run would be truncated and reused at its
old mode, and the chmod is what corrects that.

`EffortSyncWriteFailure` is renamed `EffortSyncFileFailure` — it was typing a
`read_failures` array, the same naming-lie the `Codex…` prefix had last round.

Also pins the codex mode preservation with a test. It only writes on a path
that genuinely rewrites the file, so the fixture is an Anthropic-flavoured
model pin the sync strips, and the test asserts the content changed before
checking the mode — otherwise it would pass on a sync that did nothing.

* fix(#3706): guard the claude writes and share one escaping rule

The security sign-off caught a comment of mine that was factually wrong: the
new claude read guard said the failure is folded in "like the write path in
this same loop does", and there was no write guard in that loop. Rather than
correct the sentence, both claude write sites are now guarded the way the read
is — a failed file is skipped, the sweep continues, and the raw summary token
flips to `failed`. The JSON shape stays frozen deliberately, because it is
long-standing and widely consumed; the token is the channel that can carry the
signal without a compatibility risk, which is the reviewer's own suggestion.

That makes all three branches consistent: reads and writes guarded everywhere,
per-file failures degrade instead of aborting, and every branch reports
`failed` rather than `ok` when something did not sync.

`setFrontmatterKeyLine` interpolated its value raw while the install-side
writer quoted through the shared helpers — two writers of the same frontmatter
key disagreeing on escaping, the divergence class this repo requires closed.
They now share one rule. Verified no churn: all six effort levels are plain
scalars and emit byte-identically, with claude's documented minimal-to-low
clamp the only difference in the table, exactly as before.

* fix(#3706): publish claude agent writes atomically too

Both reviewers found this independently, and it is data loss rather than a
reporting gap. The claude branch wrote in place, so `fs.writeFileSync`'s
O_TRUNC meant a post-open fault left the agent file truncated or half-written:
an injected ENOSPC produced an empty file, and under `ulimit -f` a 60000-byte
agent came back as 512 bytes of wrong content. The guard added earlier this
round then counted that destroyed file as `skipped`, which in JSON mode is
indistinguishable from "already in sync" — so a caller would have read the
sweep as clean while an agent on disk was corrupt.

It now publishes the way the codex and opencode branches already do: write to
a tmp file created at the original's masked mode, chmod, then retryRenameSync,
with the tmp unlinked and the agent skipped on any failure. The corrupting case
is gone rather than merely reported, which matters because this branch
deliberately takes no new result key.

I had claimed all three branches were consistent after the previous commit.
That was true for degradation and reporting and not for atomicity; the reviewer
caught the overclaim. It is true now.

Also sorts the claude file list, which the other two branches already did —
readdir order is platform-dependent, so leaving it unsorted made the reported
`changes` ordering differ across machines for identical inputs.

* chore(#3706): backfill the changeset PR number

pr:0 placeholder replaced with the real PR now that gh api returned it.

* test(#3706): kill the frontmatter mutants this change introduced

CI's Stryker frontmatter shard scored 60.58 against a break floor of 62.
The cause is documented in the lane's own config, from #1882: this PR added a
multi-clause predicate to frontmatter.cts and exported the escaper, but the
tests constraining them live in tests/runtime-converters.test.cjs, which that
shard does not run — so every mutant in the new code was uncovered there even
though the behaviour is tested elsewhere.

The fix is assertions that kill real mutants, per the repo's own instruction,
not a lowered floor and not a Stryker disable: scripts/mutation-matrix.cjs is
untouched. Each clause of agentScalarNeedsDoubleQuoting now has a true case AND
a near-miss that must answer the opposite way, so flipping the clause fails a
specific named test — alnum-first against `a-b`, trailing `:` against `foo:bar`,
embedded `: ` against `a:b`, embedded ` #` against `a#b`, the word list against
`yes1`/`nullish`, the numeric forms against `1a`/`0xzz`, the timestamp against
`2026-08-25x`, plus the case-insensitive spellings that pin the `i` flag.
escapeDoubleQuoted is pinned on exact output, including a case constructed so
that escaping in the wrong ORDER yields a different string.

Two of my expectations were wrong and are asserted as the code actually
behaves: `12:99` is NOT quoted, because the sexagesimal alternative never
range-checks minutes and so does not match — which is right, since YAML would
not read it as sexagesimal either; and `20260825` is quoted by the numeric
clause rather than the timestamp one, being a bare integer.

* chore(#3706): ratchet the frontmatter mutation floor to 65

The lane measured 66.67 on PR 3867 after the mutant-killing unit tests landed —
above its pre-change 63.35 baseline, not merely recovered. Step 3 of this
file's own HOW TO UPDATE procedure says to set minScore = floor(measured) - 1
in the same diff, so 62 becomes 65 and the improvement is locked in rather than
left free to slide back.

The ledger of measured scores now records the new measurement, why the shard
broke in the first place (logic added to frontmatter.cts whose only tests lived
in a file this lane does not run — the same trap the #1882 note describes), and
one discrepancy: step 3 also says to update "the matching RATCHET_BASELINE
entry", but no such declaration exists in this file. The name appears only in
that comment, so minScore and the ledger are all there is to update.

* fix(#3706): update RATCHET_BASELINE alongside the raised floor

The ratchet test caught the previous commit: it raised COVERED['frontmatter']
.minScore to 65 without updating the baseline that mirrors it, which is exactly
the mismatch that guard exists to make visible in review.

I had claimed RATCHET_BASELINE did not exist. It does — in
tests/mutation-matrix-ratchet.test.cjs, not in scripts/mutation-matrix.cjs,
which is the only file I searched before concluding it was a stale reference.
The ledger comment is corrected to say where it lives and to record that the
guard caught the error rather than leaving my wrong claim on the record.

* docs(#3706): put the mutation ledger entries back under their own dates

The 2026-08-25 measurement was spliced into the middle of the 2026-06-14 list,
so adr-parser, config-schema, active-workstream-store and core-utils ended up
sitting under the wrong heading and misattributing their measurement dates.
That ledger is what a future change reads to calibrate a floor, so a wrong date
there is not cosmetic. Each measurement is now under the date it was taken.

Also drops the first-person account of my own mistake from the entry — the
factual half (where RATCHET_BASELINE lives, and that it is updated in the same
diff) is what a reader needs; the confession is not.

---------

Co-authored-by: sim <sim@local>
2026-08-25 19:54:30 -04:00

1254 lines
60 KiB
JavaScript

'use strict';
/**
* #2071 — `gsd-tools effort sync` crashed in an INSTALLED runtime because
* commands.cjs did `require('../../../bin/install.js')`, but the installer only
* copies the `gsd-core/` subtree into a runtime home — the package-root
* `bin/install.js` is never present there, so the require threw MODULE_NOT_FOUND.
*
* This does a real minimal install into a temp home (the same helper the
* golden-parity suite uses) and runs the exact repro from the issue against the
* installed shim: `node <configDir>/gsd-core/bin/gsd-tools.cjs effort sync`. Pre-fix
* this throws `Cannot find module '../../../bin/install.js'`; post-fix the
* install-time resolvers live in the shipped sibling
* `gsd-core/bin/lib/install-effort-resolver.cjs` and the require resolves.
*
* `--config-dir <temp>` keeps it hermetic (targets the temp install, never the
* developer's real ~/.claude); effort sync defaults to dry-run so nothing is written.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { runNode } = require('./helpers/process-seam.cjs');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
// Absolute path to the built module, spawned in a child process below so the
// JSON `cmdEffortSync` writes straight to fd 1 (via io.cjs's writeAllSync) can
// actually be captured — overriding process.stdout.write in-process does not
// intercept that write.
const COMMANDS_CJS = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'commands.cjs');
describe('#2071: effort sync runs in an installed runtime (no package-root bin/install.js)', () => {
test('effort sync does not crash reaching for the un-shipped bin/install.js', () => {
if (process.platform === 'win32') return; // install layout is POSIX-path-shaped
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
try {
// Installed layout invariant: the package-root installer is never copied in.
assert.ok(!fs.existsSync(path.join(root, 'bin', 'install.js')), 'installed home must not contain bin/install.js');
assert.ok(!fs.existsSync(path.join(configDir, 'bin', 'install.js')), 'no bin/install.js beside gsd-core');
// A project effort config gives the sync something to resolve.
fs.mkdirSync(path.join(root, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(root, '.planning', 'config.json'),
JSON.stringify({ effort: { default: 'high' } }),
);
const gsdTools = path.join(configDir, 'gsd-core', 'bin', 'gsd-tools.cjs');
const result = runNode(
[gsdTools, 'effort', 'sync', '--config-dir', configDir],
{ cwd: root, env: { ...process.env, HOME: root }, timeoutMs: 15000 },
);
const combined = `${result.stdout || ''}${result.stderr || ''}`;
assert.doesNotMatch(
combined,
/Cannot find module[^\n]*install\.js|'\.\.\/\.\.\/\.\.\/bin\/install\.js'/,
`effort sync must not reach for the un-shipped bin/install.js:\n${combined}`,
);
assert.doesNotMatch(
combined,
/MODULE_NOT_FOUND/,
`effort sync must not crash on module resolution in an installed runtime:\n${combined}`,
);
} finally {
cleanup(root);
}
});
});
// #3706 — OpenCode's `effort sync` path (cmdEffortSyncOpencode) maintains the
// `variant:` frontmatter key install bakes into `~/.config/opencode/agents/
// gsd-*.md`, mirroring the pre-existing claude/`effort:` and codex branches.
// Each case is run against a fresh sandbox: a project `cwd` (holding
// `.planning/config.json`), a `configDir` (holding `agents/`), and an
// isolated `home` (HOME env, so `~/.gsd/defaults.json` can never leak in from
// the real developer machine) — cmdEffortSync merges home defaults with the
// project config, so a hermetic test must control both sources.
describe('#3706: effort sync maintains OpenCode variant: frontmatter', () => {
function makeSandbox() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-effort-sync-opencode-'));
const cwd = path.join(root, 'project');
const configDir = path.join(root, 'runtime-home');
const agentsDir = path.join(configDir, 'agents');
const home = path.join(root, 'home');
fs.mkdirSync(cwd, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.mkdirSync(home, { recursive: true });
return { root, cwd, configDir, agentsDir, home };
}
function writeProjectEffortConfig(cwd, effort) {
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(cwd, '.planning', 'config.json'),
JSON.stringify(effort === undefined ? {} : { effort }),
);
}
function writeAgent(agentsDir, name, lines) {
const filePath = path.join(agentsDir, name);
fs.writeFileSync(filePath, lines.join('\n'));
return filePath;
}
/**
* Invokes `cmdEffortSync(cwd, false, opts)` in a CHILD process (required
* per module — see COMMANDS_CJS comment above) and parses the JSON result
* `output()` writes to fd 1. Callers that only need the on-disk effect of
* the sync (most cases below) still go through this so the harness stays
* single-shaped, but only cases 2 and 6 actually assert on the returned
* `result`.
*/
function runEffortSync({ cwd, home, configDir, runtime, dryRun }) {
const opts = { dryRun, configDir, runtime };
const script = [
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const jsonStart = spawned.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${spawned.stdout}`);
return JSON.parse(spawned.stdout.slice(jsonStart));
}
test('writes the resolved variant into an agent that has none', () => {
// Protects: cmdEffortSyncOpencode's happy-path write — a fresh agent
// with no `variant:` key gets the resolved effort injected verbatim.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.match(content, /^variant: xhigh$/m);
} finally {
cleanup(root);
}
});
test('a synced agent keeps its original file mode', () => {
// The tmp-file + rename publish does not preserve mode the way an
// in-place writeFileSync would — cmdEffortSyncOpencode stat+chmods the
// tmp file before the rename specifically to compensate. Mode bits are
// not meaningful on Windows (no POSIX permission model), so skip there.
if (process.platform === 'win32') return;
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
fs.chmodSync(filePath, 0o600);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.match(content, /^variant: xhigh$/m, 'the sync must have actually rewritten the file');
assert.strictEqual(
fs.statSync(filePath).mode & 0o777, 0o600,
'the published file must keep the original file mode',
);
} finally {
cleanup(root);
}
});
test('the temp file is never more permissive than the agent it replaces', () => {
// A plain `writeFileSync(tmpPath, data)` creates the tmp file at the
// default `0666 & ~umask`, then only tightens it afterward via chmod —
// for a 0600 agent that briefly leaves its contents world-readable
// inside agents/. Assert the mode is correct at CREATION, not just
// after the later chmod, by monkeypatching fs.writeFileSync in the
// child process to record the on-disk mode immediately after the real
// write runs. Mode bits are not meaningful on Windows, so skip there.
if (process.platform === 'win32') return;
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
fs.chmodSync(filePath, 0o600);
const opts = { dryRun: false, configDir, runtime: 'opencode' };
const script = [
"const fs = require('node:fs');",
'const captures = [];',
'const originalWriteFileSync = fs.writeFileSync;',
'fs.writeFileSync = function (targetPath, data, options) {',
' const result = originalWriteFileSync.call(fs, targetPath, data, options);',
' if (typeof targetPath === "string" && /\\.tmp\\.\\d+$/.test(targetPath)) {',
' captures.push({ options: options || null, modeAfterWrite: fs.statSync(targetPath).mode & 0o777 });',
' }',
' return result;',
'};',
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
'process.stdout.write("###CAPTURE_START###" + JSON.stringify(captures) + "###CAPTURE_END###");',
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`capture child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const match = /###CAPTURE_START###([\s\S]*?)###CAPTURE_END###/.exec(spawned.stdout);
assert.ok(match, `expected capture marker on stdout, got:\n${spawned.stdout}`);
const captures = JSON.parse(match[1]);
assert.strictEqual(captures.length, 1, `expected exactly one tmp-file write, got:\n${JSON.stringify(captures)}`);
// The tmp file's mode immediately after creation must already match
// the target agent's 0600, not the default 0644 a bare writeFileSync
// would produce.
assert.strictEqual(
captures[0].modeAfterWrite, 0o600,
'the tmp file must never be created more permissive than the agent it replaces',
);
const content = fs.readFileSync(filePath, 'utf8');
assert.match(content, /^variant: xhigh$/m, 'the sync must have actually rewritten the file');
assert.strictEqual(fs.statSync(filePath).mode & 0o777, 0o600, 'the published file must keep the original file mode');
} finally {
cleanup(root);
}
});
test('reports the change without writing under dry run', () => {
// Protects: dry-run reports the pending change but leaves the file
// byte-identical — no write happens until dryRun is explicitly false.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const before = fs.readFileSync(filePath);
const result = runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: true });
const after = fs.readFileSync(filePath);
assert.ok(before.equals(after), 'dry run must not touch the file on disk');
assert.strictEqual(result.synced, 1);
assert.deepStrictEqual(result.changes[0], { agent: 'gsd-executor', from: null, to: 'xhigh' });
} finally {
cleanup(root);
}
});
test('an already-correct agent is skipped, not rewritten', () => {
// Protects: an agent already carrying the resolved variant is reported
// skipped and its bytes are left completely untouched (no rewrite churn).
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'high' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant: high', '---', '', 'Body.',
]);
const before = fs.readFileSync(filePath);
const result = runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const after = fs.readFileSync(filePath);
assert.ok(before.equals(after), 'an in-sync agent must not be rewritten');
assert.strictEqual(result.skipped, 1);
assert.strictEqual(result.synced, 0);
} finally {
cleanup(root);
}
});
test('inherit strips the key rather than writing it literally', () => {
// #3533 (10d) — inherit means the key must not exist; writing
// `variant: inherit` would name a variant that cannot resolve.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'inherit' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant: high', '---', '', 'Body.',
]);
const result = runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(content, /^variant:/m);
assert.match(content, /^name: gsd-executor$/m);
assert.match(content, /^description: x$/m);
assert.match(content, /^mode: subagent$/m);
assert.deepStrictEqual(result.changes[0], { agent: 'gsd-executor', from: 'high', to: null });
} finally {
cleanup(root);
}
});
test('no effort config at all strips a stale key', () => {
// Matches what install bakes with no config: an agent that carries a
// stale `variant:` key from a prior sync must lose it, not keep it.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, undefined);
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant: high', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(content, /^variant:/m);
} finally {
cleanup(root);
}
});
test('a missing agents directory is reported, not thrown', () => {
// Protects: an install with no agents/ subdir under configDir must
// report the condition structurally, never throw out of cmdEffortSync.
const { root, cwd, configDir, home } = makeSandbox();
try {
cleanup(path.join(configDir, 'agents'));
const result = runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
assert.strictEqual(result.reason, 'agents directory not found');
} finally {
cleanup(root);
}
});
test('files that are not gsd-*.md are ignored', () => {
// Protects: the gsd-*.md filter — a non-gsd file sitting in agents/ must
// never be synced or skipped, i.e. never even enter the loop.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
writeAgent(agentsDir, 'not-gsd.md', [
'---', 'name: not-gsd', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const result = runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
assert.strictEqual(result.synced, 0);
assert.strictEqual(result.skipped, 0);
} finally {
cleanup(root);
}
});
test('the document body is untouched', () => {
// Protects: the frontmatter line-editors only ever touch the frontmatter
// span — a body containing a colon, a `#`, and a `---` rule must survive
// byte-identical past the closing frontmatter delimiter.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'max' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', '---', '',
'Body with: colons #hash and a --- rule.', '', 'more.',
]);
const before = fs.readFileSync(filePath, 'utf8');
const beforeBody = before.slice(before.indexOf('---', 3) + 3);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const after = fs.readFileSync(filePath, 'utf8');
const afterBody = after.slice(after.indexOf('---', 3) + 3);
assert.strictEqual(afterBody, beforeBody);
assert.match(after, /^variant: max$/m);
} finally {
cleanup(root);
}
});
test('a key present with an empty value is removed, not mistaken for absent', () => {
// The value regex's `(.+?)` requires at least one character, so a bare
// `variant:` line (zero-width value) used to read as "key absent" rather
// than "key present, value empty" — presence and value are now distinct
// questions, and a stale empty-valued key must still be stripped when no
// effort config resolves one.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, undefined);
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant:', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(content, /^variant:/m);
} finally {
cleanup(root);
}
});
test('a key present with a whitespace-only value is removed too (the spelling that always worked)', () => {
// Trailing whitespace after the colon always matched the old regex (it is
// not zero-width), so this spelling never exhibited the absent-vs-empty
// bug above — pinned as the control that makes the bug look like a
// whitespace lottery rather than a real presence/value distinction.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, undefined);
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant: ', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(content, /^variant:/m);
} finally {
cleanup(root);
}
});
test('CRLF agents keep their line endings through a write and a strip', () => {
// Key-parameterising the frontmatter editors (claude's effort: vs
// opencode's variant:) is exactly the kind of change that would regress
// CRLF handling if the line ending were baked in per-key rather than
// detected from the matched frontmatter block.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = path.join(agentsDir, 'gsd-executor.md');
const beforeWrite = [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
].join('\r\n');
fs.writeFileSync(filePath, beforeWrite);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const afterWrite = fs.readFileSync(filePath, 'utf8');
assert.match(afterWrite, /^variant: xhigh\r$/m);
assert.doesNotMatch(afterWrite, /(?<!\r)\n/, 'no lone LF must appear in a CRLF document');
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'inherit' } });
const beforeStrip = [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'variant: high', '---', '', 'Body.',
].join('\r\n');
fs.writeFileSync(filePath, beforeStrip);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const afterStrip = fs.readFileSync(filePath, 'utf8');
assert.doesNotMatch(afterStrip, /^variant:/m);
assert.doesNotMatch(afterStrip, /(?<!\r)\n/, 'no lone LF must appear in a CRLF document');
} finally {
cleanup(root);
}
});
test('a symlinked agent file is skipped, never followed', () => {
// A symlinked gsd-*.md must never have its TARGET file rewritten — the
// sync must skip it structurally (readdir's Dirent reports a symlink
// entry as not-a-file), not follow it and edit whatever it points to.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
const targetPath = path.join(root, 'outside-target.md');
const targetContent = [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
].join('\n');
fs.writeFileSync(targetPath, targetContent);
const linkPath = path.join(agentsDir, 'gsd-executor.md');
try {
fs.symlinkSync(targetPath, linkPath);
} catch {
return; // platform cannot create symlinks (e.g. unprivileged Windows) — skip
}
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const after = fs.readFileSync(targetPath, 'utf8');
assert.strictEqual(after, targetContent, 'the symlink target must be left untouched');
} finally {
cleanup(root);
}
});
test('a body line starting with variant: is never the line edited', () => {
// Scoping-by-content, not a whole-file /m replace: a body line that
// happens to start with `variant:` must survive untouched while the
// frontmatter gains its own key.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '',
'variant: in-the-body', '', 'more.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.ok(content.includes('variant: in-the-body'), 'the body line must survive untouched');
const fmEnd = content.indexOf('\n---', 4);
assert.match(content.slice(0, fmEnd), /^variant: xhigh$/m, 'the frontmatter must gain its own variant: line');
} finally {
cleanup(root);
}
});
test('a file with no frontmatter at all is skipped, not corrupted', () => {
// No `---` fences at all: the sync must leave the file byte-identical
// rather than guessing where a frontmatter block would go.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'Just a body, no frontmatter fences at all.',
]);
const before = fs.readFileSync(filePath);
runEffortSync({ cwd, home, configDir, runtime: 'opencode', dryRun: false });
const after = fs.readFileSync(filePath);
assert.ok(before.equals(after), 'a file with no frontmatter must be byte-identical afterward');
} finally {
cleanup(root);
}
});
test('the claude path still writes effort:, not variant:', () => {
// The frontmatter line-editors were key-parameterised for #3706; this is
// the control that the pre-existing claude behavior did not move.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'claude', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.match(content, /^effort: xhigh$/m);
assert.doesNotMatch(content, /^variant:/m);
} finally {
cleanup(root);
}
});
test('a claude read failure reports failed without changing the result shape', () => {
// Protects: the claude branch of cmdEffortSync deliberately does NOT gain
// a `read_failures` key on a read failure — that result shape
// (`{synced, skipped, changes, dry_run, agents_dir}`) is long-standing
// and widely consumed. The failure is surfaced only through output()'s
// third argument (the raw-mode summary token), which is never merged
// into the JSON object. Asserts both halves: the raw token is 'failed',
// and the JSON result's key set is exactly the historic five, with no
// read_failures/write_failures key added.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-unreadable': 'xhigh' } });
writeAgent(agentsDir, 'gsd-unreadable.md', [
'---', 'name: gsd-unreadable', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const opts = { dryRun: false, configDir, runtime: 'claude' };
const makeScript = raw => [
`const fs = require('node:fs');`,
`const originalReadFileSync = fs.readFileSync;`,
`fs.readFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-unreadable.md')) {`,
` throw new Error('injected read failure');`,
` }`,
` return originalReadFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, ${raw}, ${JSON.stringify(opts)});`,
].join('\n');
const jsonRun = runNode(['-e', makeScript(false)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
jsonRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${jsonRun.outcome}):\nstdout: ${jsonRun.stdout}\nstderr: ${jsonRun.stderr}`,
);
const jsonStart = jsonRun.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${jsonRun.stdout}`);
const result = JSON.parse(jsonRun.stdout.slice(jsonStart));
assert.strictEqual(result.skipped, 1);
assert.deepStrictEqual(
Object.keys(result).sort(),
['agents_dir', 'changes', 'dry_run', 'skipped', 'synced'],
'the claude result shape must gain no read_failures/write_failures key',
);
const rawRun = runNode(['-e', makeScript(true)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
rawRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${rawRun.outcome}):\nstdout: ${rawRun.stdout}\nstderr: ${rawRun.stderr}`,
);
assert.strictEqual(rawRun.stdout.trim(), 'failed', `expected the raw summary token 'failed', got:\n${rawRun.stdout}`);
} finally {
cleanup(root);
}
});
test('a claude write failure is reported and does not abort the sweep', () => {
// Protects: the claude branch's fs.writeFileSync (both the inherit/strip
// call site and the concrete/set call site) used to sit outside any try,
// so a single unwritable agent file threw and aborted the entire sweep.
// Injected deterministically by monkeypatching fs.writeFileSync inside
// the child script for exactly ONE agent's path — per this repo's
// CLAUDE.md §4, chmod-based injection is forbidden (root bypasses mode
// bits under Docker/CI and it leaks resources).
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-broken': 'xhigh', 'gsd-executor': 'xhigh' } });
const brokenPath = writeAgent(agentsDir, 'gsd-broken.md', [
'---', 'name: gsd-broken', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const okPath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const brokenBefore = fs.readFileSync(brokenPath);
const opts = { dryRun: false, configDir, runtime: 'claude' };
const makeScript = raw => [
`const fs = require('node:fs');`,
`const originalWriteFileSync = fs.writeFileSync;`,
`fs.writeFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-broken.md')) {`,
` throw new Error('injected write failure');`,
` }`,
` return originalWriteFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, ${raw}, ${JSON.stringify(opts)});`,
].join('\n');
const jsonRun = runNode(['-e', makeScript(false)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
jsonRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${jsonRun.outcome}):\nstdout: ${jsonRun.stdout}\nstderr: ${jsonRun.stderr}`,
);
const jsonStart = jsonRun.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${jsonRun.stdout}`);
const result = JSON.parse(jsonRun.stdout.slice(jsonStart));
assert.strictEqual(result.skipped, 1);
assert.deepStrictEqual(
Object.keys(result).sort(),
['agents_dir', 'changes', 'dry_run', 'skipped', 'synced'],
'the claude result shape must gain no read_failures/write_failures key',
);
const brokenAfter = fs.readFileSync(brokenPath);
assert.ok(brokenBefore.equals(brokenAfter), 'the failing agent file must be byte-unchanged');
const okContent = fs.readFileSync(okPath, 'utf8');
assert.match(okContent, /^effort: xhigh$/m, 'the sweep must continue past the failing agent');
const rawRun = runNode(['-e', makeScript(true)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
rawRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${rawRun.outcome}):\nstdout: ${rawRun.stdout}\nstderr: ${rawRun.stderr}`,
);
assert.strictEqual(rawRun.stdout.trim(), 'failed', `expected the raw summary token 'failed', got:\n${rawRun.stdout}`);
} finally {
cleanup(root);
}
});
test('a claude agent survives a fault mid-publish', () => {
// Protects: the claude branch now publishes atomically (tmp-write +
// chmod + retryRenameSync), same discipline as cmdEffortSyncOpencode. An
// ENOSPC-mid-write injected on the TMP path must never truncate or empty
// the real agent file — pre-fix, an in-place `writeFileSync(filePath,
// ...)` truncates via O_TRUNC before the fault, leaving the file empty.
// Injected deterministically by monkeypatching fs.writeFileSync inside
// the child script for exactly the `.tmp.` path — per this repo's
// CLAUDE.md §4, chmod-based injection is forbidden (root bypasses mode
// bits under Docker/CI and it leaks resources).
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const before = fs.readFileSync(filePath);
const opts = { dryRun: false, configDir, runtime: 'claude' };
const makeScript = raw => [
`const fs = require('node:fs');`,
`const originalWriteFileSync = fs.writeFileSync;`,
// cmdEffortSync's claude branch writes to `<filePath>.tmp.<pid>`
// before renaming over the real path — throw only for that tmp
// write, simulating an ENOSPC fault after the original file has
// already been truncated by a naive in-place write (which this
// atomic-publish path no longer performs).
`fs.writeFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-executor.md.tmp.')) {`,
` throw new Error('injected ENOSPC mid-write');`,
` }`,
` return originalWriteFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, ${raw}, ${JSON.stringify(opts)});`,
].join('\n');
const jsonRun = runNode(['-e', makeScript(false)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
jsonRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${jsonRun.outcome}):\nstdout: ${jsonRun.stdout}\nstderr: ${jsonRun.stderr}`,
);
const after = fs.readFileSync(filePath);
assert.ok(before.equals(after), 'the original agent file must be byte-identical after a fault mid-publish');
const leftoverTmp = fs.readdirSync(agentsDir).filter(f => f.includes('.tmp.'));
assert.deepStrictEqual(leftoverTmp, [], `no orphan tmp file must remain in agents_dir, found: ${leftoverTmp.join(', ')}`);
const rawRun = runNode(['-e', makeScript(true)], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
rawRun.exitCode, 0,
`cmdEffortSync child process failed (outcome=${rawRun.outcome}):\nstdout: ${rawRun.stdout}\nstderr: ${rawRun.stderr}`,
);
assert.strictEqual(rawRun.stdout.trim(), 'failed', `expected the raw summary token 'failed', got:\n${rawRun.stdout}`);
} finally {
cleanup(root);
}
});
test('a claude sync preserves the agent file mode', () => {
// The tmp-file + rename publish does not preserve mode the way an
// in-place writeFileSync would — cmdEffortSync's claude branch
// stat+chmods the tmp file before the rename specifically to compensate,
// mirroring cmdEffortSyncOpencode. Mode bits are not meaningful on
// Windows (no POSIX permission model), so skip there.
if (process.platform === 'win32') return;
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': 'xhigh' } });
const filePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
fs.chmodSync(filePath, 0o600);
runEffortSync({ cwd, home, configDir, runtime: 'claude', dryRun: false });
const content = fs.readFileSync(filePath, 'utf8');
assert.match(content, /^effort: xhigh$/m, 'the sync must have actually rewritten the file');
assert.strictEqual(
fs.statSync(filePath).mode & 0o777, 0o600,
'the published file must keep the original file mode',
);
} finally {
cleanup(root);
}
});
test('effort values are written unquoted', () => {
// Control for the shared-escaping fix: setFrontmatterKeyLine now routes
// through the same agentScalarNeedsDoubleQuoting/escapeDoubleQuoted
// helpers the install-side frontmatterScalar writer uses, but every
// effort level the sync actually writes today is a plain scalar — the
// output must stay byte-identical (unquoted) across runtimes/levels.
for (const level of ['low', 'xhigh']) {
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-executor': level } });
const claudePath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
runEffortSync({ cwd, home, configDir, runtime: 'claude', dryRun: false });
const claudeContent = fs.readFileSync(claudePath, 'utf8');
assert.match(claudeContent, new RegExp(`^effort: ${level}$`, 'm'));
assert.doesNotMatch(claudeContent, /^effort: "/m);
} finally {
cleanup(root);
}
const { root: root2, cwd: cwd2, configDir: configDir2, agentsDir: agentsDir2, home: home2 } = makeSandbox();
try {
writeProjectEffortConfig(cwd2, { agent_overrides: { 'gsd-executor': level } });
const opencodePath = writeAgent(agentsDir2, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
runEffortSync({ cwd: cwd2, home: home2, configDir: configDir2, runtime: 'opencode', dryRun: false });
const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
assert.match(opencodeContent, new RegExp(`^variant: ${level}$`, 'm'));
assert.doesNotMatch(opencodeContent, /^variant: "/m);
} finally {
cleanup(root2);
}
}
});
test('a write failure on one agent is reported and does not stop the sweep', () => {
// Protects: cmdEffortSyncOpencode's atomic tmp-write + rename can fail
// mid-sync (fs fault). The failure must be reported per-agent in
// write_failures, the remaining agents must still be processed, the
// failing agent's file must be byte-unchanged, and cmdEffortSync must
// never throw. Injected deterministically by monkeypatching
// fs.writeFileSync inside the child script — per this repo's CLAUDE.md
// §4, chmod-based injection is forbidden (root bypasses mode bits under
// Docker/CI and it leaks resources).
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-broken': 'xhigh', 'gsd-executor': 'xhigh' } });
const brokenPath = writeAgent(agentsDir, 'gsd-broken.md', [
'---', 'name: gsd-broken', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const okPath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const brokenBefore = fs.readFileSync(brokenPath);
const opts = { dryRun: false, configDir, runtime: 'opencode' };
const script = [
`const fs = require('node:fs');`,
`const originalWriteFileSync = fs.writeFileSync;`,
// The tmp-write + rename publish (cmdEffortSyncOpencode) writes to
// `<filePath>.tmp.<pid>` before renaming over the real path — throw
// only for that one agent's tmp file, so every other write (and any
// fs machinery the require chain itself performs) passes through
// unmodified.
`fs.writeFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-broken.md.tmp.')) {`,
` throw new Error('injected write failure');`,
` }`,
` return originalWriteFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const jsonStart = spawned.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${spawned.stdout}`);
const result = JSON.parse(spawned.stdout.slice(jsonStart));
assert.strictEqual(result.write_failures.length, 1);
assert.strictEqual(result.write_failures[0].agent, 'gsd-broken');
const brokenAfter = fs.readFileSync(brokenPath);
assert.ok(brokenBefore.equals(brokenAfter), 'the failing agent file must be byte-unchanged');
const okContent = fs.readFileSync(okPath, 'utf8');
assert.match(okContent, /^variant: xhigh$/m, 'the sweep must continue past the failing agent');
} finally {
cleanup(root);
}
});
test('an unreadable agent file is reported and does not abort the sweep', () => {
// Protects: cmdEffortSyncOpencode's fs.readFileSync used to sit outside
// any try, so a single unreadable agent file threw and aborted the
// entire sweep, unlike the write path in the same loop which degrades
// into write_failures. Injected deterministically by monkeypatching
// fs.readFileSync inside the child script — per this repo's CLAUDE.md
// §4, chmod-based injection is forbidden (root bypasses mode bits under
// Docker/CI and it leaks resources).
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-unreadable': 'xhigh', 'gsd-executor': 'xhigh' } });
writeAgent(agentsDir, 'gsd-unreadable.md', [
'---', 'name: gsd-unreadable', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const okPath = writeAgent(agentsDir, 'gsd-executor.md', [
'---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const opts = { dryRun: false, configDir, runtime: 'opencode' };
const script = [
`const fs = require('node:fs');`,
`const originalReadFileSync = fs.readFileSync;`,
`fs.readFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-unreadable.md')) {`,
` throw new Error('injected read failure');`,
` }`,
` return originalReadFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const jsonStart = spawned.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${spawned.stdout}`);
const result = JSON.parse(spawned.stdout.slice(jsonStart));
assert.strictEqual(result.read_failures.length, 1);
assert.strictEqual(result.read_failures[0].agent, 'gsd-unreadable');
const okContent = fs.readFileSync(okPath, 'utf8');
assert.match(okContent, /^variant: xhigh$/m, 'the sweep must continue past the unreadable agent');
} finally {
cleanup(root);
}
});
test('the summary reports failed when an opencode write could not complete', () => {
// Protects: the raw-mode summary TOKEN cmdEffortSync passes as output()'s
// third argument (never merged into the JSON object — io.cts `output()`
// only reads it when `raw === true`, entirely replacing the JSON
// payload) must flip to 'failed' when a write_failures entry exists, even
// though the JSON result itself carries no such flag. Captured by
// running the child with `raw: true` and asserting on the literal stdout
// text `output()` writes for that mode, not on parsed JSON.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, { agent_overrides: { 'gsd-broken': 'xhigh' } });
writeAgent(agentsDir, 'gsd-broken.md', [
'---', 'name: gsd-broken', 'description: x', 'mode: subagent', '---', '', 'Body.',
]);
const opts = { dryRun: false, configDir, runtime: 'opencode' };
const script = [
`const fs = require('node:fs');`,
`const originalWriteFileSync = fs.writeFileSync;`,
`fs.writeFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-broken.md.tmp.')) {`,
` throw new Error('injected write failure');`,
` }`,
` return originalWriteFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, true, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
assert.strictEqual(spawned.stdout.trim(), 'failed', `expected the raw summary token 'failed', got:\n${spawned.stdout}`);
} finally {
cleanup(root);
}
});
});
// ADR-2313 D7 (#3243) — the Codex branch (cmdEffortSyncCodex) strips a stale
// Anthropic-flavored `model` pin (and its coupled `model_reasoning_effort`)
// from every installed `~/.codex/agents/<agent>.toml`, publishing via the same
// tmp-file + chmod + retryRenameSync discipline as the OpenCode branch above.
describe('#3243: effort sync strips Anthropic-flavored model pins from Codex agents', () => {
function makeSandbox() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-effort-sync-codex-'));
const cwd = path.join(root, 'project');
const configDir = path.join(root, 'runtime-home');
const agentsDir = path.join(configDir, 'agents');
const home = path.join(root, 'home');
fs.mkdirSync(cwd, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.mkdirSync(home, { recursive: true });
return { root, cwd, configDir, agentsDir, home };
}
function writeAgent(agentsDir, name, lines) {
const filePath = path.join(agentsDir, name);
fs.writeFileSync(filePath, lines.join('\n'));
return filePath;
}
test('a synced codex agent keeps its original file mode', () => {
// The tmp-file + rename publish does not preserve mode the way an
// in-place writeFileSync would — cmdEffortSyncCodex stat+chmods the tmp
// file before the rename specifically to compensate, same discipline as
// the OpenCode branch. Mode bits are not meaningful on Windows, so skip
// there. The reachable rewrite path is the Anthropic-model-strip: a
// `.toml` agent carrying an Anthropic-flavored `model` value.
if (process.platform === 'win32') return;
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
const before = ['model = "claude-sonnet-4"', 'model_reasoning_effort = "high"', ''].join('\n');
const filePath = writeAgent(agentsDir, 'gsd-executor.toml', [before]);
fs.chmodSync(filePath, 0o600);
const opts = { dryRun: false, configDir, runtime: 'codex' };
const script = [
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const after = fs.readFileSync(filePath, 'utf8');
assert.notStrictEqual(after, before, 'the sync must have actually rewritten the file');
assert.doesNotMatch(after, /claude/i, 'the Anthropic-flavored model pin must have been stripped');
assert.strictEqual(
fs.statSync(filePath).mode & 0o777, 0o600,
'the published file must keep the original file mode',
);
} finally {
cleanup(root);
}
});
test('an unreadable codex agent is reported and does not abort the sweep', () => {
// Protects: cmdEffortSyncCodex's fs.readFileSync used to sit outside any
// try, so a single unreadable agent file would throw and abort the
// entire sweep instead of degrading into read_failures like the write
// path in the same loop. Injected deterministically by monkeypatching
// fs.readFileSync inside the child script — per this repo's CLAUDE.md
// §4, chmod-based injection is forbidden (root bypasses mode bits under
// Docker/CI and it leaks resources). The Anthropic-flavored `model` pin
// is the reachable codex rewrite path (verified by execution above), so
// the sibling agent uses that fixture to prove it still gets rewritten.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeAgent(agentsDir, 'gsd-unreadable.toml', [
'model = "claude-sonnet-4"', 'model_reasoning_effort = "high"', '',
]);
const okPath = writeAgent(agentsDir, 'gsd-ok.toml', [
'model = "claude-sonnet-4"', 'model_reasoning_effort = "high"', '',
]);
const okBefore = fs.readFileSync(okPath, 'utf8');
const opts = { dryRun: false, configDir, runtime: 'codex' };
const script = [
`const fs = require('node:fs');`,
`const originalReadFileSync = fs.readFileSync;`,
`fs.readFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-unreadable.toml')) {`,
` throw new Error('injected read failure');`,
` }`,
` return originalReadFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
const jsonStart = spawned.stdout.indexOf('{');
assert.notStrictEqual(jsonStart, -1, `expected JSON output on stdout, got:\n${spawned.stdout}`);
const result = JSON.parse(spawned.stdout.slice(jsonStart));
assert.strictEqual(result.read_failures.length, 1);
assert.strictEqual(result.read_failures[0].agent, 'gsd-unreadable');
const okAfter = fs.readFileSync(okPath, 'utf8');
assert.notStrictEqual(okAfter, okBefore, 'the sweep must continue past the unreadable agent and still rewrite the sibling');
assert.doesNotMatch(okAfter, /claude/i, 'the sibling agent must have had its Anthropic-flavored model pin stripped');
} finally {
cleanup(root);
}
});
test('the summary reports failed when a codex sync could not complete', () => {
// Protects: same raw-mode summary TOKEN contract as the opencode case
// above — output()'s third argument is never merged into the JSON
// object, so a raw-mode caller can only see the failure via this token.
// Captured the same way: run with `raw: true` and assert on the literal
// stdout text, not on parsed JSON.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeAgent(agentsDir, 'gsd-unreadable.toml', [
'model = "claude-sonnet-4"', 'model_reasoning_effort = "high"', '',
]);
const opts = { dryRun: false, configDir, runtime: 'codex' };
const script = [
`const fs = require('node:fs');`,
`const originalReadFileSync = fs.readFileSync;`,
`fs.readFileSync = function (targetPath, ...rest) {`,
` if (typeof targetPath === 'string' && targetPath.includes('gsd-unreadable.toml')) {`,
` throw new Error('injected read failure');`,
` }`,
` return originalReadFileSync.call(fs, targetPath, ...rest);`,
`};`,
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, true, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
assert.strictEqual(spawned.stdout.trim(), 'failed', `expected the raw summary token 'failed', got:\n${spawned.stdout}`);
} finally {
cleanup(root);
}
});
});
// #3706 — setFrontmatterKeyLine / removeFrontmatterKeyLine are not exported
// directly; they are reached through the exported cmdEffortSync (claude
// runtime, `effort:` key), the only public entry point that exercises them.
describe('#3706: frontmatter line editors are scoped to the matched block', () => {
function makeSandbox() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-effort-sync-fm-scope-'));
const cwd = path.join(root, 'project');
const configDir = path.join(root, 'runtime-home');
const agentsDir = path.join(configDir, 'agents');
const home = path.join(root, 'home');
fs.mkdirSync(cwd, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.mkdirSync(home, { recursive: true });
return { root, cwd, configDir, agentsDir, home };
}
function writeProjectEffortConfig(cwd, override) {
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(cwd, '.planning', 'config.json'),
JSON.stringify({ effort: { agent_overrides: { 'gsd-executor': override } } }),
);
}
function runEffortSync({ cwd, home, configDir }) {
const opts = { dryRun: false, configDir, runtime: 'claude' };
const script = [
`const { cmdEffortSync } = require(${JSON.stringify(COMMANDS_CJS)});`,
`cmdEffortSync(${JSON.stringify(cwd)}, false, ${JSON.stringify(opts)});`,
].join('\n');
const spawned = runNode(['-e', script], {
cwd,
env: { ...process.env, HOME: home, USERPROFILE: home },
});
assert.strictEqual(
spawned.exitCode, 0,
`cmdEffortSync child process failed (outcome=${spawned.outcome}):\nstdout: ${spawned.stdout}\nstderr: ${spawned.stderr}`,
);
}
test('a CRLF document with a preamble keeps its opening fence intact', () => {
// Pre-fix: openLen was derived from `/^---\r\n/.test(content)` (start of
// file, which here is "Preamble line", not CRLF) rather than from the
// matched frontmatter block, so the CRLF fence misaligned by one byte.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, 'inherit');
const filePath = path.join(agentsDir, 'gsd-executor.md');
const before = 'Preamble line\r\n\r\n---\r\nname: x\r\neffort: high\r\n---\r\n\r\nBody.\r\n';
fs.writeFileSync(filePath, before);
runEffortSync({ cwd, home, configDir });
const after = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(after, 'Preamble line\r\n\r\n---\r\nname: x\r\n---\r\n\r\nBody.\r\n');
assert.ok(after.includes('---\r\nname: x\r\n'), 'the CRLF opening fence must survive intact');
assert.ok(after.startsWith('Preamble line\r\n\r\n'), 'preamble must survive untouched');
assert.ok(after.endsWith('\r\n\r\nBody.\r\n'), 'body must survive untouched');
assert.doesNotMatch(after, /^-{1,2}\r?\n/m, 'no truncated/stray fence');
} finally {
cleanup(root);
}
});
test('a preamble line starting with the key is not the line rewritten', () => {
// Pre-fix: `content.replace(keyLineRe, ...)` was a whole-file /m replace
// gated only on the key being present in fmBody, so the FIRST matching
// line in the whole file (the preamble) was rewritten instead of the
// frontmatter line.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, 'xhigh');
const filePath = path.join(agentsDir, 'gsd-executor.md');
const before = 'effort: not-the-frontmatter\n\n---\nname: x\neffort: high\n---\n\nBody.\n';
fs.writeFileSync(filePath, before);
runEffortSync({ cwd, home, configDir });
const after = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(after, 'effort: not-the-frontmatter\n\n---\nname: x\neffort: xhigh\n---\n\nBody.\n');
// Bytes-based scoping (not a whole-file regex): the preamble line must
// precede the first `---` fence, and only the line AFTER that fence may
// read `effort: xhigh`.
const firstFence = after.indexOf('---');
assert.ok(after.slice(0, firstFence).includes('effort: not-the-frontmatter'), 'preamble line must be untouched');
assert.ok(after.slice(firstFence).includes('effort: xhigh'), 'frontmatter line must carry the new value');
} finally {
cleanup(root);
}
});
test('a document whose frontmatter starts at byte 0 is byte-identical to before', () => {
// No-regression control: a normal install-written agent (frontmatter at
// byte 0, LF) must see only the one targeted line change, nothing else.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, 'xhigh');
const filePath = path.join(agentsDir, 'gsd-executor.md');
const before = ['---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.'].join('\n');
fs.writeFileSync(filePath, before);
runEffortSync({ cwd, home, configDir });
const afterSet = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(
afterSet,
['---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'effort: xhigh', '---', '', 'Body.'].join('\n'),
);
writeProjectEffortConfig(cwd, 'inherit');
const beforeRemove = ['---', 'name: gsd-executor', 'description: x', 'mode: subagent', 'effort: high', '---', '', 'Body.'].join('\n');
fs.writeFileSync(filePath, beforeRemove);
runEffortSync({ cwd, home, configDir });
const afterRemove = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(
afterRemove,
['---', 'name: gsd-executor', 'description: x', 'mode: subagent', '---', '', 'Body.'].join('\n'),
);
} finally {
cleanup(root);
}
});
test('a duplicated key converges to one occurrence on a single set', () => {
// Protects: setFrontmatterKeyLine used to rewrite only the FIRST
// occurrence of a duplicated key, leaving a stale second occurrence in
// place. A last-wins YAML reader would then honour the stale value while
// this function's own first-occurrence read reports "in sync" — a
// permanently non-converging state. One run must collapse a duplicated
// key to exactly one occurrence, carrying the new value, in the position
// of the FIRST occurrence.
const { root, cwd, configDir, agentsDir, home } = makeSandbox();
try {
writeProjectEffortConfig(cwd, 'xhigh');
const filePath = path.join(agentsDir, 'gsd-executor.md');
const before = [
'---', 'name: gsd-executor', 'effort: low', 'description: x', 'effort: high', '---', '', 'Body.',
].join('\n');
fs.writeFileSync(filePath, before);
runEffortSync({ cwd, home, configDir });
const after = fs.readFileSync(filePath, 'utf8');
assert.strictEqual(
after,
['---', 'name: gsd-executor', 'effort: xhigh', 'description: x', '---', '', 'Body.'].join('\n'),
'exactly one effort: line must remain, at the position of the first occurrence, carrying the resolved value',
);
const occurrences = after.match(/^effort:/gm) || [];
assert.strictEqual(occurrences.length, 1, 'exactly one effort: line must remain');
} finally {
cleanup(root);
}
});
});