typescript-idle-audit-config-drift
Root cause (why this is the 4th occurrence): the daemon did three half-things each time — (1) registered namespaces with existence-only checks (junk/partial dirs polluted the config), (2) mutated duckbrain.config.json with no atomic write and no board commit, so the tick produced no attributable work artifact, and (3) the audit's verdict credited only task/board-kind commits, so a tick that only reconciled config drift was classified IDLE. Each prior fix patched one link; the pattern repeated. The fix below closes all three.
Fix 1 — the daemon reconciler (src/drift-reconciler.ts): verify candidates properly, register only verified namespaces, atomically persist config, and push one board commit per registration so the work is durable and attributable:
// 1. Verification: safe name + real dir + no symlink + non-empty (+ optional marker)
export async function verifyNamespaceDir(root: string, name: string, opts: VerifyOptions = {}) {
const reason = validateNamespaceName(name); // ^[a-z0-9][a-z0-9-]{0,127}$ (blocks traversal/dots)
if (reason) return { ok: false, reason };
const st = await fsp.lstat(path.join(root, name)); // throws -> "missing on disk (ENOENT)"
if (st.isSymbolicLink()) return { ok: false, reason: 'symlink: refusing to follow' };
if (!st.isDirectory()) return { ok: false, reason: 'not a directory' };
const entries = await fsp.readdir(path.join(root, name));
if (entries.length === 0) return { ok: false, reason: 'empty dir (partial write?); retry next tick' };
if (opts.requireMarker && !entries.some(e => ['namespace.json','manifest.json','tick.md'].includes(e)))
return { ok: false, reason: 'no namespace marker (namespace.json|manifest.json|tick.md)' };
return { ok: true };
}
// 2. Reconcile: register verified -> board-commit each -> atomic config write
export async function reconcileDrift({ root, configPath, config, tick, board, verify }: ReconcileOpts): Promise<ReconcileResult> {
const scan = await scanDrift({ root, config, tick, verify });
if (!scan.hasDrift) return { scan, config, commits: [] }; // no drift -> no mutation, no commit
const namespaces = [...config.namespaces];
const commits: CommitRecord[] = [];
for (const name of scan.verified) {
namespaces.push({ name, path: path.join(root, name), discoveredAtTick: tick, source: 'disk-audit', status: 'active' });
commits.push(await board.commit({ // durable, attributable work artifact
tick, kind: 'config-drift',
message: `board: register namespace ${name} (disk drift @tick ${tick})`,
namespaces: [name],
}));
}
const next: DuckBrainConfig = { ...config, tick, namespaces };
await writeJsonAtomic(configPath, next); // tmp file + rename, never a torn config
return { scan, config: next, commits };
}
scanDrift surfaces everything for observability: diskDirs, nonDirs (stray files/symlinks, surfaced but never candidates), missingOnDisk (reported, never auto-removed), newDirs, verified, rejected.
Fix 2 — the audit verdict (the actual IDLE→PRODUCTIVE correction):
// Audit-side fix: any committed record (board OR config-drift) = productive.
// A tick is IDLE only when nothing was committed.
export function classifyVerdict(commits: CommitRecord[]): Verdict {
return commits.length > 0 ? 'PRODUCTIVE' : 'IDLE';
}
Fix 3 — the :3000 daemon tick handler (src/daemon.ts), wired end-to-end:
export async function daemonTick({ root, configPath, tick, board, verify }: DaemonTickOptions) {
const config = await readConfig(configPath); // 1. read config
const { scan, config: next, commits } = await reconcileDrift({ // 2-4. verify+register+commit+atomic write
root, configPath, config, tick, board, verify,
});
const outcome = auditTick(tick, commits); // 5. verdict from the commit ledger
return { ...outcome, config: next, registered: scan.verified };
}
The audit now reads the board ledger for the tick; since the daemon commits each registration, tick 226 is PRODUCTIVE. A synced tick (zero commits) is still correctly IDLE.
Implemented at `~/duckbrain-idle-audit-fix/` and verified with Node 22's built-in test runner + `tsc --noEmit` (strict). **10/10 tests pass, typecheck clean.** **End-to-end tick-226 reproduction** (same namespaces from the finding — `colombia-election-2026`, `europe-welfare-analysis`): - registered both, produced 2 `config-drift` commits (`board: register namespace … @tick 226`), verdict **`PRODUCTIVE`** (was `IDLE`), config on disk valid JSON with `source: "disk-audit"`, `discoveredAtTick: 226`. **Edge cases tested** (`test/drift-reconciler.test.ts`): 1. **Regression guard** — a tick with only `config-drift` commits is never `IDLE`; the legacy audit (crediting only `board` kind) is asserted to have 0 credits, proving the old misclassification. 2. **Idempotency** — re-running a synced tick: no drift, no commits, `IDLE`, config byte-identical. 3. **Junk rejection** — stray file (surfaced in `nonDirs`, never registered), empty dir (`partial write?; retry next tick`), symlink (excluded at scan + lstat branch rejects), invalid name `bad%name`, dot-prefixed dirs (hidden, never candidates). 4. **Strict marker mode** — bare dir without `namespace.json|manifest.json|tick.md` is skipped. 5. **Removal drift** — deleted dir reported in `missingOnDisk`, config entry preserved (no destructive auto-remove). 6. **Failure safety** — board commit throwing leaves config untouched (0 registrations, still valid JSON). 7. **Missing root** — treated as no drift, no crash. 8. **Verdict unit** — `[] → IDLE`, any commit → `PRODUCTIVE`. 9. **Name validation** — rejects `''`, `..`, `a/b`, `.hidden`, `UPPER`, `has space`, 200-char names; accepts the real kebab names. ``` # tests 10 # pass 10 # fail 0 | npx tsc --noEmit: exit 0 (strict) ```
{"model": "deepseek-v4-flash", "problem_class": "typescript-idle-audit-config-drift", "result": "passed", "tests": 10}