fix(3347): surface auto-build state via graphifyStatus; trim planner edits to free size budget

agents/gsd-planner.md was 49,316 chars after the initial PR; the
planner-decomposition <48K test was passing on main at 49,150 chars (just under
the 49152 limit). My addition pushed it over.

Restructure: instead of teaching the planner agent to read .last-build-status.json
directly, fold the auto-build state into graphifyStatus()'s existing `stale: true`
signal. The planner's existing rule ("if stale: true, treat as approximate") fires
correctly for failed and in-flight auto-builds — no new planner-side prompt content
needed. The full state is exposed under `last_build_auto_update` for callers that
want exit_code / duration_ms / commit-sha context.

- get-shit-done/bin/lib/graphify.cjs: graphifyStatus() reads
  .planning/graphs/.last-build-status.json; OR-folds status in {failed, running}
  into the existing stale signal; exposes last_build_auto_update field
- agents/gsd-planner.md: revert the auto-update awareness paragraph (49,524 → 49,150)
- agents/gsd-phase-researcher.md: revert the parallel paragraph for consistency
- get-shit-done/references/planner-graphify-auto-update.md: rewrite to document
  the graphifyStatus seam instead of planner-side prompt instructions
- tests/feat-3347-graphify-auto-update-config.test.cjs: 4 new graphifyStatus
  tests pinning the failed/running/ok/missing matrix
- tests/feat-3347-graphify-auto-update-hook.test.cjs: bump per-spawn timeout
  5s → 30s and wait-deadline 5s → 15s to absorb cold-start latency under
  parallel-test-file load (full suite runs many *.test.cjs concurrently)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-15 10:46:20 -04:00
parent b0fd732579
commit dd4f75376a
6 changed files with 140 additions and 31 deletions

View File

@@ -30,6 +30,40 @@ const {
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
} = require('../get-shit-done/bin/lib/configuration.generated.cjs');
const fsx = require('node:fs');
const pathx = require('node:path');
const cpx = require('node:child_process');
const osx = require('node:os');
const { graphifyStatus } = require('../get-shit-done/bin/lib/graphify.cjs');
function makeStatusProject(autoUpdate) {
const tmpDir = fsx.mkdtempSync(pathx.join(osx.tmpdir(), 'gsd-3347-status-'));
cpx.execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: tmpDir });
cpx.execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpDir });
cpx.execFileSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir });
fsx.writeFileSync(pathx.join(tmpDir, 'README.md'), '# t\n');
cpx.execFileSync('git', ['add', '.'], { cwd: tmpDir });
cpx.execFileSync('git', ['commit', '-qm', 'init'], { cwd: tmpDir });
fsx.mkdirSync(pathx.join(tmpDir, '.planning/graphs'), { recursive: true });
fsx.writeFileSync(
pathx.join(tmpDir, '.planning/config.json'),
JSON.stringify({ graphify: { enabled: true } }),
);
// Write a fresh (current-mtime) graph so age-based stale is false; only the
// auto-update status field can set stale: true.
fsx.writeFileSync(
pathx.join(tmpDir, '.planning/graphs/graph.json'),
JSON.stringify({ nodes: [], edges: [] }),
);
if (autoUpdate !== null) {
fsx.writeFileSync(
pathx.join(tmpDir, '.planning/graphs/.last-build-status.json'),
JSON.stringify(autoUpdate),
);
}
return tmpDir;
}
describe('#3347 — graphify.auto_update is a registered config key', () => {
test('VALID_CONFIG_KEYS contains graphify.auto_update', () => {
assert.ok(
@@ -126,6 +160,61 @@ describe('#3347 — config-set graphify.auto_update round-trips', () => {
);
});
test('graphifyStatus folds auto-update status=failed into stale=true', (t) => {
const tmpDir = makeStatusProject({
ts: '2026-05-15T12:00:00Z',
status: 'failed',
exit_code: 1,
duration_ms: 1234,
head_at_build: 'abcdef0',
graphify_version: null,
});
t.after(() => fsx.rmSync(tmpDir, { recursive: true, force: true }));
const s = graphifyStatus(tmpDir);
assert.strictEqual(s.stale, true, 'auto-build failure must set stale=true');
assert.ok(s.last_build_auto_update, 'last_build_auto_update must be exposed');
assert.strictEqual(s.last_build_auto_update.status, 'failed');
assert.strictEqual(s.last_build_auto_update.exit_code, 1);
});
test('graphifyStatus folds auto-update status=running into stale=true', (t) => {
const tmpDir = makeStatusProject({
ts: '2026-05-15T12:00:00Z',
status: 'running',
exit_code: null,
duration_ms: null,
head_at_build: 'abcdef0',
graphify_version: null,
});
t.after(() => fsx.rmSync(tmpDir, { recursive: true, force: true }));
const s = graphifyStatus(tmpDir);
assert.strictEqual(s.stale, true, 'auto-build in-flight must set stale=true');
assert.strictEqual(s.last_build_auto_update.status, 'running');
});
test('graphifyStatus leaves stale alone when auto-update status=ok and graph is fresh', (t) => {
const tmpDir = makeStatusProject({
ts: '2026-05-15T12:00:00Z',
status: 'ok',
exit_code: 0,
duration_ms: 1234,
head_at_build: 'abcdef0',
graphify_version: null,
});
t.after(() => fsx.rmSync(tmpDir, { recursive: true, force: true }));
const s = graphifyStatus(tmpDir);
assert.strictEqual(s.stale, false, 'fresh graph + ok auto-build => not stale');
assert.strictEqual(s.last_build_auto_update.status, 'ok');
});
test('graphifyStatus exposes last_build_auto_update: null when status file absent', (t) => {
const tmpDir = makeStatusProject(null);
t.after(() => fsx.rmSync(tmpDir, { recursive: true, force: true }));
const s = graphifyStatus(tmpDir);
assert.strictEqual(s.last_build_auto_update, null);
assert.strictEqual(s.stale, false, 'no status file => stale follows mtime only');
});
test('config-set graphify.auto_update does not perturb sibling graphify.enabled', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));

View File

@@ -94,7 +94,7 @@ function runHook(tmpDir, toolPayload, { env = {}, pathPrepend = '' } = {}) {
...env,
},
encoding: 'utf8',
timeout: 5000,
timeout: 30000,
});
}
@@ -256,7 +256,7 @@ describe('#3347 hook — dispatch path (all gates pass)', () => {
// Wait up to 5s for the detached process to finish updating the status
const statusPath = path.join(tmpDir, '.planning/graphs/.last-build-status.json');
const deadline = Date.now() + 5000;
const deadline = Date.now() + 15000;
let status;
while (Date.now() < deadline) {
if (fs.existsSync(statusPath)) {
@@ -285,7 +285,7 @@ describe('#3347 hook — dispatch path (all gates pass)', () => {
);
const statusPath = path.join(tmpDir, '.planning/graphs/.last-build-status.json');
const deadline = Date.now() + 5000;
const deadline = Date.now() + 15000;
let status;
while (Date.now() < deadline) {
if (fs.existsSync(statusPath)) {