From 2fd259a0d460de7d44edc250a566fa0552598e31 Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Mon, 28 Sep 2026 23:07:58 -0700 Subject: [PATCH 1/4] docs(plan): map V4 follow ups v2 branch --- docs/plans/2026-09-28-follow-ups-v2.md | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/plans/2026-09-28-follow-ups-v2.md diff --git a/docs/plans/2026-09-28-follow-ups-v2.md b/docs/plans/2026-09-28-follow-ups-v2.md new file mode 100644 index 00000000..57d3bbfe --- /dev/null +++ b/docs/plans/2026-09-28-follow-ups-v2.md @@ -0,0 +1,33 @@ +# Follow ups v2: V4 branch plan + +**Status:** Active. **Branch:** `fix/follow-ups-v2`. **Exact base:** `e2f9dcae0554ff63921df618a819fd5e6afe80d2` (develop bootstrap #272). + +Source contract: [remediation program V4](2026-09-28-remediation-program-v2.md#v4-fixfollow-ups-v2-every-small-product-cli-and-upstream-item) and the archived Branch 9 plan. Each row is a separate test-first unit commit. This dispatch implements B1 only; later rows require controller review and assignment. File and test mappings below are intended scope, subject to source inspection when a row starts. + +| Row | Likely files | Proof and prerequisite | +| --- | --- | --- | +| A1 | `bin/agentic-kit.mjs`, `src/commands/{usage,models,host,audit,heal,telemetry,x}*` | CLI `--json` error tests; 6b parked item 2 | +| A2 | `src/commands/host*` | host dry-run JSON refusal/off/reset tests; m-4 | +| A3 | self-drift module, `docs/adr/ADR-0063*` | offline TTL edge tests; reconcile ADR path | +| A4 | refresh service/collector module | injected `refreshStages` and `service` test proves no collector | +| B1 | `src/lib/paths.mjs`, XDG readers listed in B1 brief, `tests/kit/xdg-relative.test.mjs` | relative XDG path, child process and source guard RED/GREEN; exact-head CI gate | +| B2 | `src/commands/x/{daemon-gc,host}.mjs`, `src/commands/setup.mjs` | repaired evidence re-record tests; Branch 9 Task 8 | +| B3 | Ruflo memory path module, `src/lib/paths.mjs` | dual-reason and equality boundary tests | +| B4 | N4 target per D-4 | D-4 decision first; corresponding focused test | +| B5 | AQE stray scan and Codex MCP hint modules | dot-folder, home-store and hint tests; upstream #757 | +| B6 | Ruflo component status module | applied row test; hooks row only after #3419 answer | +| B7 | daemon settings sync/config modules | dry-run/live parity and hidden YAML tests; F6/F7 | +| B8 | maintenance pause module | restart while paused test | +| B9 | process tree termination module | abort tree test; Windows CI required | +| B10 | persisted file-ID comparison modules | large ID serialization tests; identify all sites first | +| B11 | deja-vu module and temp-folder checks | skipped verdict and cleanup tests | +| B12 | setup probe module | disposable real Ruflo reproduction first; fix only if confirmed | +| B13 | sync AQE pin module | §2 step 2 evidence first; fix only if sync missed pin | +| C1 | temporary CI workflow, upstream registry | disposable Linux/Windows busy-rule and memory-routing live tests; remove temporary job | +| C2 | `docs/HOST-SUPPORT.md` | verify AQE 3.14.4 and live upstream #2356/#420 evidence | +| C3 | nightly workflow, `trace-ort.mjs` | artifact test; user approves exact log text before external post | +| C4 | upstream watch scripts, registry | N-5 M7/M8/minors tests; inspect deferred-minors report; M10 declined | +| C5 | upstream issue draft | D-6 A decision; user approves exact text before post; B0-16 only if D-16 B | +| C6 | support evidence/report | newest supported Ruflo, Codex, AQE releases at pre-PR check | + +For B1: preserve OpenCode's nullable fallback; use `xdgBase` for all source readers except the documented statusline template and manifest name list. Keep scratch homes disposable, unset `FORCE_COLOR`, use guarded focused tests, regression tests, typecheck and changed-file lint. Record RED/GREEN and self-review in the ignored task report. No shared manifests, ADR index or decision log changes. From fb54f02b2c2cb981e776ffbc13bd159dd21b242e Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Mon, 28 Sep 2026 23:14:04 -0700 Subject: [PATCH 2/4] fix(paths): ignore a relative XDG_* value, as the XDG Base Directory spec requires --- src/commands/uninstall.mjs | 4 +- src/lib/footprint/consumers.mjs | 6 +- src/lib/footprint/index.mjs | 6 +- src/lib/footprint/install.mjs | 4 +- .../footprint/storage-reclaim-detectors.mjs | 6 +- src/lib/footprint/storage.mjs | 4 +- src/lib/hook-audit/providers/opencode.mjs | 3 +- src/lib/host-readiness-local.mjs | 7 +- src/lib/live/process-sessions.mjs | 4 +- src/lib/paths.mjs | 22 ++++-- src/lib/usage-opencode.mjs | 3 +- tests/kit/xdg-relative.test.mjs | 72 +++++++++++++++++++ 12 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 tests/kit/xdg-relative.test.mjs diff --git a/src/commands/uninstall.mjs b/src/commands/uninstall.mjs index c27a1c2a..1a9fe198 100644 --- a/src/commands/uninstall.mjs +++ b/src/commands/uninstall.mjs @@ -101,9 +101,9 @@ function hasDejaVuOwnership(cfg) { function protectedDejaVuRoots(homeDir, env) { const absolute = (value) => typeof value === 'string' && path.isAbsolute(value); - const configBases = [path.join(homeDir, '.config'), env.XDG_CONFIG_HOME, env.APPDATA] + const configBases = [path.join(homeDir, '.config'), paths.xdgBase('XDG_CONFIG_HOME', null, { env }), env.APPDATA] .filter(absolute); - const dataBases = [path.join(homeDir, '.local', 'share'), env.XDG_DATA_HOME] + const dataBases = [path.join(homeDir, '.local', 'share'), paths.xdgBase('XDG_DATA_HOME', null, { env })] .filter(absolute); return { sourceRoots: [ diff --git a/src/lib/footprint/consumers.mjs b/src/lib/footprint/consumers.mjs index 41345a2a..8b5c0081 100644 --- a/src/lib/footprint/consumers.mjs +++ b/src/lib/footprint/consumers.mjs @@ -55,7 +55,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { - claudeDir, codexDir, configDir, globalRoot, home, isWindows, npxCacheDir, + claudeDir, codexDir, configDir, globalRoot, home, isWindows, npxCacheDir, xdgBase, } from '../paths.mjs'; import { hasValue, measured, rootMeasurements, sumMeasurements, unknown, walkTree, @@ -167,8 +167,8 @@ export const CONSUMER_WALK_LIMITS = Object.freeze({ // to audit for the sake of a read-only ranking. Kit and host paths still come // from paths.mjs — nothing home-relative that the kit itself owns is spelled out // below. -const xdgCache = (env) => env.XDG_CACHE_HOME || path.join(home, '.cache'); -const xdgData = (env) => env.XDG_DATA_HOME || path.join(home, '.local', 'share'); +const xdgCache = (env) => xdgBase('XDG_CACHE_HOME', path.join(home, '.cache'), { env }); +const xdgData = (env) => xdgBase('XDG_DATA_HOME', path.join(home, '.local', 'share'), { env }); const macCache = () => path.join(home, 'Library', 'Caches'); const winLocalAppData = (env) => env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); diff --git a/src/lib/footprint/index.mjs b/src/lib/footprint/index.mjs index 863a2a1d..35daee7e 100644 --- a/src/lib/footprint/index.mjs +++ b/src/lib/footprint/index.mjs @@ -28,7 +28,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { - claudeDir, claudeSettingsPath, claudeUserMcpPath, codexConfigPath, codexDir, configDir, home, + claudeDir, claudeSettingsPath, claudeUserMcpPath, codexConfigPath, codexDir, configDir, stateBase, } from '../paths.mjs'; import { loadKitConfig } from '../config.mjs'; import { defaultOpencodeDbPath } from '../usage-opencode.mjs'; @@ -65,8 +65,8 @@ export const INCLUDE_PROJECT_TREES_DEFAULT = false; * point: these are the files that grow fastest between deep scans (ledgers, * tee files, index caches), and a user watching one grow should not have to * run a deep scan to see it move. */ -function knownFileSpecs() { - const stateRoot = process.env.XDG_STATE_HOME || path.join(home, '.local', 'state'); +export function knownFileSpecs() { + const stateRoot = stateBase(); const kit = (name) => path.join(configDir(), name); // [id, host, category, label, path] — the categories are STORAGE_CATEGORIES' // vocabulary so a known file and its deep-tier node land in the same bucket. diff --git a/src/lib/footprint/install.mjs b/src/lib/footprint/install.mjs index a83414c1..f4c96e03 100644 --- a/src/lib/footprint/install.mjs +++ b/src/lib/footprint/install.mjs @@ -26,7 +26,7 @@ import path from 'node:path'; import { MANAGED_COMPANION_REGISTRY } from '../adapters/companion-registry.mjs'; import { HOST_REGISTRY } from '../adapters/registries.mjs'; import { - home, isWindows, globalRoot, npxCacheDir, claudeDir, codexPluginCacheDir, + home, isWindows, globalRoot, npxCacheDir, claudeDir, codexPluginCacheDir, xdgBase, } from '../paths.mjs'; import { installedVersion, KIT_PKG } from '../versions.mjs'; import { kbDir, present as brainPresent, installedVersion as brainVersion } from '../ruvnet-brain.mjs'; @@ -580,7 +580,7 @@ function vibiumCachePath({ env, platform }) { if (platform === 'win32') { return path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'vibium'); } - return path.join(env.XDG_CACHE_HOME || path.join(home, '.cache'), 'vibium'); + return path.join(xdgBase('XDG_CACHE_HOME', path.join(home, '.cache'), { env }), 'vibium'); } const presentFile = (file, fsImpl) => { diff --git a/src/lib/footprint/storage-reclaim-detectors.mjs b/src/lib/footprint/storage-reclaim-detectors.mjs index e162425a..97da3b89 100644 --- a/src/lib/footprint/storage-reclaim-detectors.mjs +++ b/src/lib/footprint/storage-reclaim-detectors.mjs @@ -16,15 +16,15 @@ // process.platform, so the wrong-platform root simply reads absent and a machine // carrying both (a tool that moved its cache) reports both. import path from 'node:path'; -import { home, isWindows } from '../paths.mjs'; +import { home, isWindows, xdgBase } from '../paths.mjs'; import { decodeClaudeProjectDir } from './project-sources.mjs'; import { rootMeasurements, measured, unknown, statNode, sumMeasurements, hasValue, } from './walk.mjs'; import { candidate } from './storage-reclaim.mjs'; -const xdgCache = (env) => env.XDG_CACHE_HOME || path.join(home, '.cache'); -const xdgData = (env) => env.XDG_DATA_HOME || path.join(home, '.local', 'share'); +const xdgCache = (env) => xdgBase('XDG_CACHE_HOME', path.join(home, '.cache'), { env }); +const xdgData = (env) => xdgBase('XDG_DATA_HOME', path.join(home, '.local', 'share'), { env }); const macCache = () => path.join(home, 'Library', 'Caches'); const winLocalAppData = (env) => env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); diff --git a/src/lib/footprint/storage.mjs b/src/lib/footprint/storage.mjs index 3b62e2c3..0d76870a 100644 --- a/src/lib/footprint/storage.mjs +++ b/src/lib/footprint/storage.mjs @@ -52,7 +52,7 @@ // `detectWorktrees` is false. import fs from 'node:fs'; import path from 'node:path'; -import { home, claudeDir, codexDir, configDir } from '../paths.mjs'; +import { home, claudeDir, codexDir, configDir, xdgBase } from '../paths.mjs'; import { defaultOpencodeDbPath } from '../usage-opencode.mjs'; import { decodeClaudeProjectDir, transcriptMetadata } from './project-sources.mjs'; import { classifyWorkingContext } from './working-context.mjs'; @@ -119,7 +119,7 @@ const flatDir = () => true; * @returns {StorageRoot[]} */ export function defaultStorageRoots({ env = process.env, projects = null } = {}) { - const stateRoot = env.XDG_STATE_HOME || path.join(home, '.local', 'state'); + const stateRoot = xdgBase('XDG_STATE_HOME', path.join(home, '.local', 'state'), { env }); const opencodeData = path.dirname(defaultOpencodeDbPath()); const claude = (name) => path.join(claudeDir(), name); const codex = (name) => path.join(codexDir(), name); diff --git a/src/lib/hook-audit/providers/opencode.mjs b/src/lib/hook-audit/providers/opencode.mjs index 25cbc560..a7738968 100644 --- a/src/lib/hook-audit/providers/opencode.mjs +++ b/src/lib/hook-audit/providers/opencode.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { xdgBase } from '../../paths.mjs'; import { normalizedOccurrence, publicSource, readBoundedFile, readJsonSource, @@ -90,7 +91,7 @@ function moduleRecords(source) { } export function auditOpenCodeHooks({ - opencodeRoot = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode'), + opencodeRoot = path.join(xdgBase('XDG_CONFIG_HOME', path.join(os.homedir(), '.config')), 'opencode'), projectRoots = [process.cwd()], opencodeVersion = 'unknown', ownership = null, diff --git a/src/lib/host-readiness-local.mjs b/src/lib/host-readiness-local.mjs index ebd19eaa..2ea1bb7d 100644 --- a/src/lib/host-readiness-local.mjs +++ b/src/lib/host-readiness-local.mjs @@ -9,6 +9,7 @@ import path from 'node:path'; import { createHash } from 'node:crypto'; import { readContextConfig } from './codex-context-config.mjs'; import { withDb } from './sqlite.mjs'; +import { xdgBase } from './paths.mjs'; const LIMIT = 1024 * 1024; const plain = x => x !== null && typeof x === 'object' && !Array.isArray(x); @@ -199,8 +200,8 @@ function selectedOpenCodeAgent(config, agentDirs) { } function loadOpenCode({ cwd, home, env }, evidence) { - const global = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode'); - const data = path.join(env.XDG_DATA_HOME || path.join(home, '.local/share'), 'opencode'); + const global = path.join(xdgBase('XDG_CONFIG_HOME', path.join(home, '.config'), { env }), 'opencode'); + const data = path.join(xdgBase('XDG_DATA_HOME', path.join(home, '.local/share'), { env }), 'opencode'); const auth = document(path.join(data, 'auth.json'), evidence, env); if (Object.values(auth).some(value => value?.type === 'wellknown') || openCodeRemote(data, evidence)) throw new Error('unsupported'); if (fs.existsSync(path.join(global, 'config'))) throw new Error('unsupported'); // legacy TOML migration is native-owned @@ -289,7 +290,7 @@ function defaultOpenCode({ config, auth, home, env, allowed, credentialed }, evi // Native default tries recent available selections, then a configured // provider. Do not borrow credentials from an unrelated provider. - const recent = document(path.join(env.XDG_STATE_HOME || path.join(home, '.local/state'), 'opencode/model.json'), evidence, env).recent; + const recent = document(path.join(xdgBase('XDG_STATE_HOME', path.join(home, '.local/state'), { env }), 'opencode/model.json'), evidence, env).recent; if (Array.isArray(recent) && recent.length) throw new Error('unsupported'); // availability requires native provider catalog const configured = Object.keys(config.provider ?? {}).filter(allowed); if (configured.length === 1) provider = configured[0]; diff --git a/src/lib/live/process-sessions.mjs b/src/lib/live/process-sessions.mjs index acf2eb80..ce105b15 100644 --- a/src/lib/live/process-sessions.mjs +++ b/src/lib/live/process-sessions.mjs @@ -1,10 +1,10 @@ import { execFile } from 'node:child_process'; import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import { inspectGitWorkspace } from './git-workspace.mjs'; +import { stateBase } from '../paths.mjs'; const execFileAsync = promisify(execFile); @@ -31,7 +31,7 @@ const WIN32_SURVEY_SCRIPT = fileURLToPath( function runtimeDebug(stage, fields = {}) { if (!process?.env || process.env.AK_RUNTIME_DEBUG !== '1') return; try { - const root = process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'); + const root = stateBase(); const file = process.env.AK_RUNTIME_DEBUG_FILE || path.join(root, 'agentic-kit', 'runtime-debug.log'); const safeStage = String(stage || 'unknown').replace(/[^a-z0-9._-]/gi, '_').slice(0, 64); const kv = Object.entries(fields) diff --git a/src/lib/paths.mjs b/src/lib/paths.mjs index 393fb420..ce15f16e 100644 --- a/src/lib/paths.mjs +++ b/src/lib/paths.mjs @@ -10,14 +10,20 @@ import { writePrivateFileAtomic } from './file-write.mjs'; const home = os.homedir(); const isWindows = process.platform === 'win32'; +/** The XDG Base Directory spec ignores relative environment overrides. */ +export function xdgBase(name, fallback, { env = process.env, p = path } = {}) { + const value = env[name]; + return value && p.isAbsolute(value) ? value : fallback; +} + /** Kit config dir: XDG on POSIX, %APPDATA% on Windows. */ function configBase() { if (isWindows) return process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); - return process.env.XDG_CONFIG_HOME || path.join(home, '.config'); + return xdgBase('XDG_CONFIG_HOME', path.join(home, '.config')); } -function stateBase() { +export function stateBase() { if (isWindows) return process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - return process.env.XDG_STATE_HOME || path.join(home, '.local', 'state'); + return xdgBase('XDG_STATE_HOME', path.join(home, '.local', 'state')); } export const configDir = () => path.join(configBase(), 'agentic-kit'); export const telemetryDir = () => path.join(configDir(), 'telemetry'); @@ -98,8 +104,10 @@ export function toolInternalDirs({ home: h = home, env = process.env, platform = p.join(h, '.claude'), env.CLAUDE_CONFIG_DIR, p.join(h, '.codex'), env.CODEX_HOME, p.join(h, '.claude-flow'), p.join(h, '.ruflo'), - p.join(h, '.config'), env.XDG_CONFIG_HOME, p.join(h, '.local'), env.XDG_DATA_HOME, - env.XDG_STATE_HOME, p.join(h, '.cache'), env.XDG_CACHE_HOME, + p.join(h, '.config'), xdgBase('XDG_CONFIG_HOME', null, { env, p }), + p.join(h, '.local'), xdgBase('XDG_DATA_HOME', null, { env, p }), + xdgBase('XDG_STATE_HOME', null, { env, p }), p.join(h, '.cache'), + xdgBase('XDG_CACHE_HOME', null, { env, p }), ]; if (platform === 'win32') dirs.push(p.join(h, 'AppData'), env.APPDATA, env.LOCALAPPDATA); if (platform === 'darwin') dirs.push(p.join(h, 'Library', 'Application Support'), p.join(h, 'Library', 'Caches')); @@ -349,8 +357,8 @@ export function hostHealthInputPaths(cwd, env = process.env) { path.join(codex, 'requirements.toml'), '/etc/codex/config.toml', '/etc/codex/requirements.toml', ...(process.platform === 'win32' ? [path.join(env.ProgramData || 'C:\\ProgramData', 'OpenAI', 'Codex', 'config.toml')] : []), path.join(opencode, 'config.json'), path.join(opencode, 'opencode.json'), path.join(opencode, 'opencode.jsonc'), - path.join(env.XDG_STATE_HOME || path.join(home, '.local', 'state'), 'opencode', 'model.json'), - path.join(env.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'opencode', 'auth.json'), + path.join(xdgBase('XDG_STATE_HOME', path.join(home, '.local', 'state'), { env }), 'opencode', 'model.json'), + path.join(xdgBase('XDG_DATA_HOME', path.join(home, '.local', 'share'), { env }), 'opencode', 'auth.json'), env.OPENCODE_CONFIG, ].filter(Boolean); let root = path.resolve(cwd); diff --git a/src/lib/usage-opencode.mjs b/src/lib/usage-opencode.mjs index 47c2cb5d..abaacd05 100644 --- a/src/lib/usage-opencode.mjs +++ b/src/lib/usage-opencode.mjs @@ -34,11 +34,12 @@ import { addUsage, blankSession, noteContextSample, noteLatencySample, notePromptFingerprint, } from './usage-parsers.mjs'; import { normalizeMode } from './usage-modes.mjs'; +import { xdgBase } from './paths.mjs'; import { observeUsageProject } from './usage-project-evidence.mjs'; /** The live opencode store. Overridable via roots in tests. */ export function defaultOpencodeDbPath() { - const home = process.env.XDG_DATA_HOME ?? null; + const home = xdgBase('XDG_DATA_HOME', null); return home ? `${home}/opencode/opencode.db` : `${process.env.HOME ?? process.env.USERPROFILE}/.local/share/opencode/opencode.db`; diff --git a/tests/kit/xdg-relative.test.mjs b/tests/kit/xdg-relative.test.mjs new file mode 100644 index 00000000..c0b3cc1a --- /dev/null +++ b/tests/kit/xdg-relative.test.mjs @@ -0,0 +1,72 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import * as pathModule from '../../src/lib/paths.mjs'; +import { spawnEnv } from './helpers/home-sandbox.mjs'; +import { tempDir } from './helpers/temp-dir.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +test('xdgBase keeps only absolute XDG values for both path flavors', () => { + assert.equal(typeof pathModule.xdgBase, 'function'); + for (const [p, absolute] of [[path.posix, '/opt/cfg'], [path.win32, 'C:\\cfg']]) { + const fallback = p.join(absolute, 'fallback'); + for (const value of [undefined, '', 'rel/cfg', './cfg']) { + assert.equal(pathModule.xdgBase('XDG_CONFIG_HOME', fallback, { env: { XDG_CONFIG_HOME: value }, p }), fallback); + } + assert.equal(pathModule.xdgBase('XDG_CONFIG_HOME', fallback, { env: { XDG_CONFIG_HOME: absolute }, p }), absolute); + } +}); + +test('relative XDG values cannot redirect live paths or tool root discovery into cwd', { skip: process.platform === 'win32' }, (t) => { + const home = tempDir('ak-xdg-relative-home', t); + const cwd = path.join(home, 'work'); + fs.mkdirSync(cwd); + const pathsUrl = new URL('../../src/lib/paths.mjs', import.meta.url).href; + const footprintUrl = new URL('../../src/lib/footprint/index.mjs', import.meta.url).href; + const script = `import * as paths from ${JSON.stringify(pathsUrl)}; +import { knownFileSpecs } from ${JSON.stringify(footprintUrl)}; +console.log(JSON.stringify([paths.configDir(), paths.evidenceDir(), + ...knownFileSpecs().map((row) => row.path), ...paths.toolInternalDirs()]));`; + const child = spawnSync(process.execPath, ['--input-type=module', '-e', script], { + cwd, + env: spawnEnv(home, { + XDG_CONFIG_HOME: 'rel/cfg', XDG_STATE_HOME: 'rel/state', + XDG_DATA_HOME: 'rel/data', XDG_CACHE_HOME: 'rel/cache', + }), + encoding: 'utf8', + }); + assert.equal(child.status, 0, child.stderr); + const paths = JSON.parse(child.stdout); + assert.ok(paths.length > 10); + for (const candidate of paths) { + assert.ok(path.isAbsolute(candidate), candidate); + assert.ok(candidate.startsWith(`${home}${path.sep}`), candidate); + assert.doesNotMatch(candidate, /(?:^|[/\\])rel(?:[/\\]|$)/); + } + assert.ok(paths.includes(path.join(home, '.local', 'state', 'agentic-kit', 'runtime-debug.log'))); +}); + +test('new source readers use the shared XDG base validator', () => { + const allowed = new Set(['src/lib/paths.mjs', 'src/templates/statusline-footer.cjs', + 'src/lib/adapters/manifest.mjs']); + const matches = []; + const visit = (dir) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const at = path.join(dir, entry.name); + if (entry.isDirectory()) { visit(at); continue; } + if (!entry.isFile()) continue; + const relative = path.relative(root, at).split(path.sep).join('/'); + if (allowed.has(relative)) continue; + const source = fs.readFileSync(at, 'utf8') + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/[^\n]*/g, ''); + if (/\b(?:process\.)?env\.XDG_[A-Z_]+|env\[['"]XDG_/.test(source)) matches.push(relative); + } + }; + visit(path.join(root, 'src')); + assert.deepEqual(matches, []); +}); From 0af3d0912cbf76f8547a382707537d0b4e2f8c9a Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Mon, 28 Sep 2026 23:21:48 -0700 Subject: [PATCH 3/4] docs(plan): specify V4 follow-up mappings and decisions --- docs/plans/2026-09-28-follow-ups-v2.md | 56 +++++++++++++------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/plans/2026-09-28-follow-ups-v2.md b/docs/plans/2026-09-28-follow-ups-v2.md index 57d3bbfe..7c1bc897 100644 --- a/docs/plans/2026-09-28-follow-ups-v2.md +++ b/docs/plans/2026-09-28-follow-ups-v2.md @@ -1,33 +1,35 @@ # Follow ups v2: V4 branch plan -**Status:** Active. **Branch:** `fix/follow-ups-v2`. **Exact base:** `e2f9dcae0554ff63921df618a819fd5e6afe80d2` (develop bootstrap #272). +## Status -Source contract: [remediation program V4](2026-09-28-remediation-program-v2.md#v4-fixfollow-ups-v2-every-small-product-cli-and-upstream-item) and the archived Branch 9 plan. Each row is a separate test-first unit commit. This dispatch implements B1 only; later rows require controller review and assignment. File and test mappings below are intended scope, subject to source inspection when a row starts. +**Active.** Branch `fix/follow-ups-v2`; exact base `e2f9dcae0554ff63921df618a819fd5e6afe80d2` (develop bootstrap #272). B1 is complete in `fb54f02b`; other rows remain unimplemented. The controller reviews and assigns later rows. One test-first unit commit per row. -| Row | Likely files | Proof and prerequisite | +The [remediation program V4](2026-09-28-remediation-program-v2.md#v4-fixfollow-ups-v2-every-small-product-cli-and-upstream-item) defines scope. The [archived Branch 9 plan](../archive/2026-09-28-superpowers-plan-branch-9-follow-ups.md) supplies task details. Paths below name current source seams and focused test targets. After an explicit directory prefix, subsequent bare filenames in the same cell use that directory. A new test named below is a proposed file. Later implementers must verify dependencies before editing. + +| Row | Source or artifact mapping | Focused proof and prerequisite | | --- | --- | --- | -| A1 | `bin/agentic-kit.mjs`, `src/commands/{usage,models,host,audit,heal,telemetry,x}*` | CLI `--json` error tests; 6b parked item 2 | -| A2 | `src/commands/host*` | host dry-run JSON refusal/off/reset tests; m-4 | -| A3 | self-drift module, `docs/adr/ADR-0063*` | offline TTL edge tests; reconcile ADR path | -| A4 | refresh service/collector module | injected `refreshStages` and `service` test proves no collector | -| B1 | `src/lib/paths.mjs`, XDG readers listed in B1 brief, `tests/kit/xdg-relative.test.mjs` | relative XDG path, child process and source guard RED/GREEN; exact-head CI gate | -| B2 | `src/commands/x/{daemon-gc,host}.mjs`, `src/commands/setup.mjs` | repaired evidence re-record tests; Branch 9 Task 8 | -| B3 | Ruflo memory path module, `src/lib/paths.mjs` | dual-reason and equality boundary tests | -| B4 | N4 target per D-4 | D-4 decision first; corresponding focused test | -| B5 | AQE stray scan and Codex MCP hint modules | dot-folder, home-store and hint tests; upstream #757 | -| B6 | Ruflo component status module | applied row test; hooks row only after #3419 answer | -| B7 | daemon settings sync/config modules | dry-run/live parity and hidden YAML tests; F6/F7 | -| B8 | maintenance pause module | restart while paused test | -| B9 | process tree termination module | abort tree test; Windows CI required | -| B10 | persisted file-ID comparison modules | large ID serialization tests; identify all sites first | -| B11 | deja-vu module and temp-folder checks | skipped verdict and cleanup tests | -| B12 | setup probe module | disposable real Ruflo reproduction first; fix only if confirmed | -| B13 | sync AQE pin module | §2 step 2 evidence first; fix only if sync missed pin | -| C1 | temporary CI workflow, upstream registry | disposable Linux/Windows busy-rule and memory-routing live tests; remove temporary job | -| C2 | `docs/HOST-SUPPORT.md` | verify AQE 3.14.4 and live upstream #2356/#420 evidence | -| C3 | nightly workflow, `trace-ort.mjs` | artifact test; user approves exact log text before external post | -| C4 | upstream watch scripts, registry | N-5 M7/M8/minors tests; inspect deferred-minors report; M10 declined | -| C5 | upstream issue draft | D-6 A decision; user approves exact text before post; B0-16 only if D-16 B | -| C6 | support evidence/report | newest supported Ruflo, Codex, AQE releases at pre-PR check | +| A1 | `bin/agentic-kit.mjs`; `src/commands/usage.mjs`, `models.mjs`, `audit.mjs`, `heal.mjs`, `telemetry.mjs`, `x/host.mjs` | `tests/kit/cli-json-honesty.test.mjs`, `usage-cli.test.mjs`, `models-command.test.mjs`, `telemetry-cli.test.mjs`, `status-command.test.mjs`; include unknown models verb and status positional | +| A2 | `src/commands/x/host.mjs`; `bin/agentic-kit.mjs` | `tests/kit/host-dry-run.test.mjs`, `host-cli-migration.test.mjs`; pick refusal, off, reset-routes under `--dry-run --json` | +| A3 | `src/lib/versions.mjs`; `docs/adr/0063-evidence-store-and-refresh-vocabulary.md` | `tests/kit/version-lookup-record.test.mjs`, `drift-freshness.test.mjs`; offline tried-at TTL and ADR wording | +| A4 | `src/commands/status.mjs`; `src/lib/refresh.mjs` | `tests/kit/refresh.test.mjs`, `status-version-drift-refresh.test.mjs`; injected `refreshStages` plus `service` builds no collector | +| B1 | `src/lib/paths.mjs`; `src/lib/footprint/index.mjs`, `storage.mjs`, `consumers.mjs`, `storage-reclaim-detectors.mjs`, `install.mjs`; `src/lib/host-readiness-local.mjs`, `live/process-sessions.mjs`, `hook-audit/providers/opencode.mjs`, `usage-opencode.mjs`; `src/commands/uninstall.mjs` | `tests/kit/xdg-relative.test.mjs` and specified regressions; exact-head CI gate passed before edit; preserve nullable OpenCode fallback | +| B2 | `src/commands/x/daemon-gc.mjs`, `src/commands/x/host.mjs`, `src/commands/setup.mjs` | New `tests/kit/daemon-gc-rerecord.test.mjs`, `setup-host-rerecord.test.mjs`, `host-pick-rerecord.test.mjs`; Branch 9 Task 8 plus deferred host pick; compare `sync-host-repair.test.mjs` | +| B3 | `src/lib/ruflo-memory.mjs`, `paths.mjs` | `tests/kit/ruflo-memory-location.test.mjs`, `project-memory-status.test.mjs`; compose both unsuitable reasons and make `inside()` exclude equality | +| B4 | `src/commands/status/sections/project-memory.mjs`; `src/lib/ruflo-memory-contract.mjs`, `live-check-evidence.mjs` | D-4 **B approved**: `tests/kit/project-memory-status.test.mjs`, `live-check-evidence.test.mjs`, `verify-memory-routes.test.mjs`; info only after successful installed-version `memory-routes` evidence, warn on upgrade or failure | +| B5 | `src/lib/project-memory.mjs`, `aqe-readiness.mjs`; `src/commands/status/sections/project-memory.mjs`, `aqe.mjs` | `tests/kit/project-memory.test.mjs`, `aqe-readiness.test.mjs`, `project-memory-status.test.mjs`; dot-folder scan, real `~/.agentic-qe`, and agentic-qe#757 hint | +| B6 | `src/commands/status/sections/ruflo-components.mjs`; `src/lib/ruflo-components/states.mjs` | `tests/kit/ruflo-components-status.test.mjs`; applied-but-unverified row; hooks fix line requires #3419 answer first | +| B7 | `src/lib/ruflo-daemon-config.mjs`; `src/commands/sync.mjs`, `sync/plan-versions.mjs` | `tests/kit/sync-daemon-repair.test.mjs`, `sync-dry-run-preview.test.mjs`, `sync-skip-versions.test.mjs`; F6 hidden YAML keys and F7 versions-only preview parity | +| B8 | `src/lib/maintenance/discovery/orchestrator.mjs`, `history.mjs` | `tests/kit/maintenance-discovery-orchestrator.test.mjs`, `maintenance-recovery.test.mjs`; restart after pause shows paused history | +| B9 | `src/lib/exec.mjs`, `execution/process-tree.mjs` | `tests/kit/process-tree.test.mjs`; abort kills descendants; Windows CI required | +| B10 | `src/lib/maintenance/discovery/partitions.mjs`; inventory `src/lib/live/jsonl-tailer.mjs`, `live/transcript-streams.mjs`, `telemetry/store.mjs`, `maintenance/management/service-store.mjs` for additional persisted IDs | `tests/kit/file-identity-bigint.test.mjs`; distinguish IDs above `2^53`; enumerate the exact sites before edit | +| B11 | `src/lib/live-checks.mjs` | `tests/kit/live-checks.test.mjs`; skipped deja-vu check says skipped and check-created temp folders are cleaned | +| B12 | `src/commands/setup.mjs`; `src/lib/memory-probe-cleanup.mjs` | `tests/kit/setup-memory-probe.test.mjs`; disposable real Ruflo reproduction first, fix only if unused `agentdb-memory.db` appears | +| B13 | `src/commands/sync.mjs`; `src/lib/aqe-project-pin.mjs` | `tests/kit/sync-command.test.mjs`, `aqe-project-pin.test.mjs`; only if program §2 step 2 shows sync omitted the AQE pin | +| C1 | `.github/workflows/ci.yml`; `src/lib/aqe-readiness.mjs`; `src/lib/hook-audit/agentic-dependency-constraints.json` | `tests/live/ruflo-memory-routing.test.mjs`, `tests/kit/aqe-readiness.test.mjs`; disposable macOS and temporary Linux/Windows CI busy-rule evidence; remove temporary job before merge; #240 action follows result | +| C2 | `docs/host-support.md`; `src/lib/hook-audit/agentic-dependency-constraints.json` | `tests/kit/ruflo-support-window.test.mjs` plus link check; verify AQE 3.14.4 #528/#532/#535 and Ruflo #2356/#420 first | +| C3 | `.github/workflows/nightly.yml`; vidaunited's `trace-ort.mjs` hook (obtain and verify its exact script path before adding) | `tests/kit/upstream-watch-workflow.test.mjs` plus macOS artifact receipt; exact upstream #2885 post text requires user approval | +| C4 | `scripts/upstream-watch/classify.mjs`, `fetch.mjs`, `ledger.mjs`, `dispatch.mjs`, `render.mjs`; `src/lib/hook-audit/agentic-dependency-constraints.json` | `tests/kit/upstream-watch-script.test.mjs`, `upstream-watch-record.test.mjs`, `upstream-watch-dispatch.test.mjs`, `upstream-watch-registry.test.mjs`; use ignored `reports/n5-253-deferred-minors.md` §2 for M7/M8/minors 1–12; M10 declined | +| C5 | `src/lib/aqe-guidance.mjs`; `src/commands/setup.mjs`; ignored `.superpowers/sdd/2026-09-28-follow-ups-v2/c5-issue-draft.md` | D-6 **A approved**: controller's isolated AQE init reproduction is evidence handoff; draft issue with command/version/expected/actual, then obtain approval of exact posting text. B0-16 draft only if D-16 B | +| C6 | `docs/host-support.md`; `src/lib/ruflo-support-window.mjs`, `aqe-readiness.mjs`; `src/lib/hook-audit/agentic-dependency-constraints.json` | `tests/kit/ruflo-support-window.test.mjs`, `aqe-readiness.test.mjs`; pre-PR live checks against newest supported Ruflo `security secrets --path`, Codex read-only app-server flags, and AQE 3.14.x | -For B1: preserve OpenCode's nullable fallback; use `xdgBase` for all source readers except the documented statusline template and manifest name list. Keep scratch homes disposable, unset `FORCE_COLOR`, use guarded focused tests, regression tests, typecheck and changed-file lint. Record RED/GREEN and self-review in the ignored task report. No shared manifests, ADR index or decision log changes. +B1 used disposable homes, guarded focused tests, and the ignored B1 report at `.superpowers/sdd/2026-09-28-follow-ups-v2/b1-report.md`. No shared manifests, lockfiles, ADR index, or decision log change belongs to this plan update. The controller owns integration and the whole-branch gate. From 404642628d57eac1e42f26286f7db763f0fce704 Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Mon, 28 Sep 2026 23:36:11 -0700 Subject: [PATCH 4/4] fix(footprint): align deep runtime log root with state base --- src/lib/footprint/storage.mjs | 9 ++++---- src/lib/paths.mjs | 6 ++--- tests/kit/xdg-relative.test.mjs | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/lib/footprint/storage.mjs b/src/lib/footprint/storage.mjs index 0d76870a..db519ecd 100644 --- a/src/lib/footprint/storage.mjs +++ b/src/lib/footprint/storage.mjs @@ -52,7 +52,7 @@ // `detectWorktrees` is false. import fs from 'node:fs'; import path from 'node:path'; -import { home, claudeDir, codexDir, configDir, xdgBase } from '../paths.mjs'; +import { home, claudeDir, codexDir, configDir, stateBase } from '../paths.mjs'; import { defaultOpencodeDbPath } from '../usage-opencode.mjs'; import { decodeClaudeProjectDir, transcriptMetadata } from './project-sources.mjs'; import { classifyWorkingContext } from './working-context.mjs'; @@ -118,8 +118,9 @@ const flatDir = () => true; * * @returns {StorageRoot[]} */ -export function defaultStorageRoots({ env = process.env, projects = null } = {}) { - const stateRoot = xdgBase('XDG_STATE_HOME', path.join(home, '.local', 'state'), { env }); +export function defaultStorageRoots({ env = process.env, projects = null, + home: h = home, platform = process.platform, p = path } = {}) { + const stateRoot = stateBase({ env, home: h, platform, p }); const opencodeData = path.dirname(defaultOpencodeDbPath()); const claude = (name) => path.join(claudeDir(), name); const codex = (name) => path.join(codexDir(), name); @@ -183,7 +184,7 @@ export function defaultStorageRoots({ env = process.env, projects = null } = {}) { id: 'ak-runtime-debug', category: 'ledgers-and-logs', host: 'agentic-kit', label: 'runtime-debug.log', - path: path.join(stateRoot, 'agentic-kit', 'runtime-debug.log'), layout: 'tree', + path: p.join(stateRoot, 'agentic-kit', 'runtime-debug.log'), layout: 'tree', }, { id: 'ak-config', category: 'kit-caches', host: 'agentic-kit', diff --git a/src/lib/paths.mjs b/src/lib/paths.mjs index ce15f16e..134cd45b 100644 --- a/src/lib/paths.mjs +++ b/src/lib/paths.mjs @@ -21,9 +21,9 @@ function configBase() { if (isWindows) return process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); return xdgBase('XDG_CONFIG_HOME', path.join(home, '.config')); } -export function stateBase() { - if (isWindows) return process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); - return xdgBase('XDG_STATE_HOME', path.join(home, '.local', 'state')); +export function stateBase({ env = process.env, home: h = home, platform = process.platform, p = path } = {}) { + if (platform === 'win32') return env.LOCALAPPDATA || p.join(h, 'AppData', 'Local'); + return xdgBase('XDG_STATE_HOME', p.join(h, '.local', 'state'), { env, p }); } export const configDir = () => path.join(configBase(), 'agentic-kit'); export const telemetryDir = () => path.join(configDir(), 'telemetry'); diff --git a/tests/kit/xdg-relative.test.mjs b/tests/kit/xdg-relative.test.mjs index c0b3cc1a..b0ad4e42 100644 --- a/tests/kit/xdg-relative.test.mjs +++ b/tests/kit/xdg-relative.test.mjs @@ -5,6 +5,7 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import * as pathModule from '../../src/lib/paths.mjs'; +import { defaultStorageRoots } from '../../src/lib/footprint/storage.mjs'; import { spawnEnv } from './helpers/home-sandbox.mjs'; import { tempDir } from './helpers/temp-dir.mjs'; @@ -21,6 +22,44 @@ test('xdgBase keeps only absolute XDG values for both path flavors', () => { } }); +test('Windows deep runtime-log root follows LOCALAPPDATA with a distinct XDG state base', () => { + const home = 'C:\\Users\\Ada'; + const env = { + LOCALAPPDATA: 'C:\\Users\\Ada\\AppData\\Local', + XDG_STATE_HOME: 'D:\\xdg-state', + }; + const expected = 'C:\\Users\\Ada\\AppData\\Local\\agentic-kit\\runtime-debug.log'; + const options = { env, home, platform: 'win32', p: path.win32 }; + assert.equal(pathModule.stateBase(options), env.LOCALAPPDATA); + assert.equal(defaultStorageRoots(options).find((row) => row.id === 'ak-runtime-debug')?.path, expected); +}); + +test('runtime-log writer, known-file reader, and deep root agree with distinct native state bases', (t) => { + const home = tempDir('ak-xdg-state-agreement', t); + const local = path.join(home, 'native-local'); + const xdg = path.join(home, 'xdg-state'); + const pathsUrl = new URL('../../src/lib/paths.mjs', import.meta.url).href; + const footprintUrl = new URL('../../src/lib/footprint/index.mjs', import.meta.url).href; + const storageUrl = new URL('../../src/lib/footprint/storage.mjs', import.meta.url).href; + const script = `import path from 'node:path'; +import { stateBase } from ${JSON.stringify(pathsUrl)}; +import { knownFileSpecs } from ${JSON.stringify(footprintUrl)}; +import { defaultStorageRoots } from ${JSON.stringify(storageUrl)}; +console.log(JSON.stringify({ writer: path.join(stateBase(), 'agentic-kit', 'runtime-debug.log'), + known: knownFileSpecs().find((row) => row.id === 'ak-runtime-debug').path, + deep: defaultStorageRoots().find((row) => row.id === 'ak-runtime-debug').path }));`; + const child = spawnSync(process.execPath, ['--input-type=module', '-e', script], { + cwd: home, + env: spawnEnv(home, { LOCALAPPDATA: local, XDG_STATE_HOME: xdg }), + encoding: 'utf8', + }); + assert.equal(child.status, 0, child.stderr); + const paths = JSON.parse(child.stdout); + const base = process.platform === 'win32' ? local : xdg; + const expected = path.join(base, 'agentic-kit', 'runtime-debug.log'); + assert.deepEqual(paths, { writer: expected, known: expected, deep: expected }); +}); + test('relative XDG values cannot redirect live paths or tool root discovery into cwd', { skip: process.platform === 'win32' }, (t) => { const home = tempDir('ak-xdg-relative-home', t); const cwd = path.join(home, 'work');