mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-29 18:33:01 +00:00
codeburn guard install|uninstall|status|refresh|allow plus the internal hook/statusline handlers. Off by default, fully local, cleanly removable. - Settings edits go through the action journal (guard-install / guard-uninstall) with expectedHash, appending our entries and removing exactly ours by command prefix; a byte-identical uninstall is asserted by test. - PreToolUse budget cap (soft warn once, hard block with a per-session allow override), Stop yield checkpoint (expensive with no edits and no commit, once), and a flagged-project SessionStart opener built from the optimize detectors. - Incremental per-session cache keyed by session id: resumes the transcript parse from the last complete-line byte offset via readSessionLines, folding only the tail into running totals. Warm invocation ~0.28s against a 90MB transcript, dominated by CLI startup; the tail parse itself is negligible. - All handlers fail open: any error, malformed stdin, or missing transcript exits 0 with no output so a broken guard can never block a session. - Hook protocol verified against the live docs (dated block at the top of hooks.ts); zero new dependencies.
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
import { mkdir, readFile, writeFile } from 'fs/promises'
|
|
import { sep } from 'path'
|
|
import type { ProjectSummary } from '../types.js'
|
|
import { flagsPath, guardDir } from './store.js'
|
|
|
|
// Per-project flag list computed at install / `guard refresh` time and read on
|
|
// SessionStart. The resolved opener text is stored here (not a code path into
|
|
// optimize) so the hot SessionStart handler never imports the analyzer.
|
|
export type ProjectFlag = { path: string; openers: string[] }
|
|
export type GuardFlags = { generatedAt: string; projects: ProjectFlag[] }
|
|
|
|
export const FLAG_STALE_MS = 7 * 24 * 60 * 60 * 1000
|
|
|
|
// optimize is loaded lazily so importing this module (for readFlags/matchFlag,
|
|
// which the SessionStart hook needs) does not pull the analyzer.
|
|
export async function buildFlags(projects: ProjectSummary[]): Promise<GuardFlags> {
|
|
const { findLowWorthCandidates, findContextBloatCandidates, LOW_WORTH_OPENER, CONTEXT_HEAVY_OPENER } =
|
|
await import('../optimize.js')
|
|
const lowWorth = new Set(findLowWorthCandidates(projects).map(c => c.project))
|
|
const contextHeavy = new Set(findContextBloatCandidates(projects).map(c => c.project))
|
|
const flags: ProjectFlag[] = []
|
|
for (const project of projects) {
|
|
const openers: string[] = []
|
|
if (lowWorth.has(project.project)) openers.push(LOW_WORTH_OPENER)
|
|
if (contextHeavy.has(project.project)) openers.push(CONTEXT_HEAVY_OPENER)
|
|
if (openers.length > 0) flags.push({ path: project.projectPath, openers })
|
|
}
|
|
return { generatedAt: new Date().toISOString(), projects: flags }
|
|
}
|
|
|
|
export async function readFlags(base?: string): Promise<GuardFlags | null> {
|
|
let raw: string
|
|
try {
|
|
raw = await readFile(flagsPath(base), 'utf-8')
|
|
} catch {
|
|
return null
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(raw) as GuardFlags
|
|
if (typeof parsed?.generatedAt !== 'string' || !Array.isArray(parsed.projects)) return null
|
|
return parsed
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function writeFlags(flags: GuardFlags, base?: string): Promise<void> {
|
|
await mkdir(guardDir(base), { recursive: true })
|
|
await writeFile(flagsPath(base), JSON.stringify(flags, null, 2) + '\n', 'utf-8')
|
|
}
|
|
|
|
export function flagsAgeMs(flags: GuardFlags): number {
|
|
const t = Date.parse(flags.generatedAt)
|
|
return Number.isNaN(t) ? Number.POSITIVE_INFINITY : Date.now() - t
|
|
}
|
|
|
|
function norm(p: string): string {
|
|
return p.length > 1 && p.endsWith(sep) ? p.slice(0, -1) : p
|
|
}
|
|
|
|
// Openers for the flagged project the cwd sits in (exact match or a subdir of
|
|
// it); most-specific project wins. Empty when the cwd is not flagged.
|
|
export function matchFlag(flags: GuardFlags, cwd: string): string[] {
|
|
const target = norm(cwd)
|
|
let best: ProjectFlag | null = null
|
|
for (const flag of flags.projects) {
|
|
const base = norm(flag.path)
|
|
if (target === base || target.startsWith(base + sep)) {
|
|
if (!best || base.length > norm(best.path).length) best = flag
|
|
}
|
|
}
|
|
return best ? best.openers : []
|
|
}
|