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:
@@ -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));
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user