UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

86 lines (85 loc) • 3.6 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Run-scoped write provenance (#652). * * # Why this exists * * `akm improve`'s end-of-run auto-sync used to infer "what did this run write?" * by diffing two Git dirty-path snapshots: everything dirty at end-of-run minus * everything dirty when the run acquired its lock. That inference is wrong in * both directions: * * - an unrelated managed-dir edit made by a human DURING a long run appears in * the end-of-run diff and gets swept into akm's commit; * - a path that was ALREADY dirty when the run started and is then rewritten by * the run is subtracted out and never committed. * * The journal below replaces the inference with a record. Every akm write path * that mutates a file on disk calls {@link recordWrittenPath}; a run opens a * journal for its duration and reads back the exact set of paths it touched. * * # Contract * * - A *write* and a *removal* are recorded identically — the journal records * "this run mutated this path", not what the mutation was. The final on-disk * state is what gets staged (`git add -A -- <path>` stages a deletion just as * happily as a modification), so a path written and then reverted, purged, or * rolled back needs no special handling: it is journaled once and the stager * sees whatever survived. * - Recording is a no-op when no journal is open, so non-improve command paths * pay nothing but a `Set.size` check. * - Journals nest: every open journal observes every recorded path. Concurrent * in-process runs (tests) therefore over-report rather than cross-attribute * writes to the wrong run, which is the safe direction — over-reporting stages * a path that has no diff, under-reporting loses a write. * - Nothing here throws. A provenance failure must never break a write. */ import path from "node:path"; const activeJournals = new Set(); /** Open a journal. The caller MUST close it (`end()`) in a `finally`. */ export function beginWriteProvenance() { const state = { touched: new Set() }; activeJournals.add(state); const snapshot = () => [...state.touched].sort(); return { writtenPaths: snapshot, end: () => { activeJournals.delete(state); return snapshot(); }, }; } /** True while at least one journal is open. */ export function isWriteProvenanceActive() { return activeJournals.size > 0; } /** * Record that the current run mutated `filePath` (write, create, rename, or * delete). No-op when no journal is open. Never throws. */ export function recordWrittenPath(filePath) { if (activeJournals.size === 0 || !filePath) return; let absolute; try { absolute = path.resolve(filePath); } catch { return; } for (const journal of activeJournals) journal.touched.add(absolute); } /** * Normalize an absolute journaled path against `root`: POSIX-relative when the * path is inside `root`, `undefined` otherwise (the caller decides whether an * out-of-root write is reportable or, for staging, simply out of scope). */ export function relativeWrittenPath(root, absolutePath) { const relative = path.relative(root, absolutePath).replaceAll(path.sep, "/"); if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return undefined; return relative; }