Release v0.3 operational memory - #39
Conversation
There was a problem hiding this comment.
Pull request overview
Prepares the promexeus@0.3.5 release that turns mex from a drift-aware scaffold CLI into a small operational-memory layer. It adds an agent-memory setup mode, an Ink-based TUI, heartbeat checks, an append-only event log, a doctor command, shell completions, severity-grouped reporter output, and tunable .mex/config.json for staleness/watch/heartbeat.
Changes:
- New runtime surface:
mex tui,mex heartbeat,mex log,mex timeline,mex doctor,mex completion,mex watch --interval,mex setup --mode agent-memory. - New modules
src/tui.ts,src/heartbeat.ts,src/events.ts,src/doctor.ts;src/reporter.tsregrouped by severity with remediation hints; config loader extended forwatch/heartbeat. - Release bookkeeping: version
0.3.3 → 0.3.5, new agent-memory templates, GROW-loop rewording across tool configs, RELEASE_NOTES.md/CHANGELOG, new Ink/React deps and lockfile churn.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json / package-lock.json | Version bump to 0.3.5 and Ink/React dependency additions |
| CHANGELOG.md / RELEASE_NOTES.md / README.md | Release prose for 0.3 operational memory |
| src/cli.ts | Wires up tui, log, timeline, heartbeat, doctor, completion, setup --mode, watch --interval |
| src/tui.ts | New Ink-based dashboard, summary, status panels, log flow |
| src/heartbeat.ts | New heartbeat checker for scaffold staleness and memory cleanup |
| src/events.ts | New JSONL event log + timeline |
| src/doctor.ts | New friendly health summary |
| src/setup/index.ts, src/setup/prompts.ts | Agent-memory setup mode and prompt |
| src/watch.ts | New --interval heartbeat loop alongside hook install |
| src/reporter.ts | Group issues by severity, add remediation hints |
| src/config.ts, src/types.ts | Watch/heartbeat config types + loaders |
| src/drift/index.ts, src/drift/checkers/staleness.ts | last_updated frontmatter staleness signal, symlink-following scaffold scan |
| templates/* | GROW rewording, new agent-memory ROUTER/AGENTS/HEARTBEAT templates |
| test/*.test.ts | Tests for tui, events, heartbeat, staleness frontmatter, and watch/heartbeat config |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // ── Agent Memory Events ── | ||
| program | ||
| .command("log <message>") | ||
| .description("Append a decision, note, risk, or todo to the mex event log") | ||
| .option("--type <type>", "Event type: decision, note, risk, todo", "note") | ||
| .option("--file <path>", "Related file path (repeatable)", (value, prev: string[]) => [...prev, value], []) | ||
| .action(async (message, opts) => { | ||
| try { | ||
| const config = findConfig(); | ||
| const { runLog } = await import("./events.js"); | ||
| await runLog(config, message, { kind: opts.type, files: opts.file }); | ||
| } catch (err) { | ||
| console.error((err as Error).message); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
|
|
||
| program | ||
| .command("timeline") | ||
| .description("Show recent mex event log entries") | ||
| .option("--json", "Output events as JSON") | ||
| .option("--since <date>", "Filter from YYYY-MM-DD or relative Nd, e.g. 30d") | ||
| .option("--type <type>", "Filter by event type") | ||
| .option("--limit <n>", "Maximum number of entries", parsePositiveIntArg) | ||
| .action(async (opts) => { | ||
| try { | ||
| const config = findConfig(); | ||
| const { runTimeline } = await import("./events.js"); | ||
| await runTimeline(config, opts); | ||
| } catch (err) { | ||
| console.error((err as Error).message); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
|
|
||
| program | ||
| .command("heartbeat") | ||
| .description("Run lightweight agent-memory health checks once") | ||
| .option("--json", "Output heartbeat report as JSON") | ||
| .action(async (opts) => { | ||
| try { | ||
| const config = findConfig(); | ||
| const { runHeartbeat } = await import("./heartbeat.js"); | ||
| await runHeartbeat(config, { json: opts.json }); | ||
| } catch (err) { | ||
| console.error((err as Error).message); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
|
|
||
| program | ||
| .command("doctor") | ||
| .description("Run a friendly scaffold health diagnostic") | ||
| .action(async () => { | ||
| try { | ||
| const config = findConfig(); | ||
| const { runDoctor } = await import("./doctor.js"); | ||
| await runDoctor(config); | ||
| } catch (err) { | ||
| console.error((err as Error).message); | ||
| process.exit(1); | ||
| } | ||
| }); |
| function readPositiveNumber(v: unknown): number | undefined { | ||
| if (typeof v === "number" && Number.isFinite(v) && v > 0) return v; | ||
| return undefined; | ||
| } |
| function scaffoldHeartbeatFiles(scaffoldRoot: string): string[] { | ||
| const patterns = ["ROUTER.md", "AGENTS.md", "context/*.md", "patterns/*.md"]; | ||
| return patterns.flatMap((pattern) => | ||
| globSync(pattern, { | ||
| cwd: scaffoldRoot, | ||
| absolute: true, | ||
| follow: true, | ||
| nodir: true, | ||
| }), | ||
| ); | ||
| } |
| const staleFiles = scaffoldHeartbeatFiles(config.scaffoldRoot) | ||
| .map((file) => { | ||
| const fm = parseFrontmatter(file); | ||
| const days = daysSinceFrontmatterDate( | ||
| typeof fm?.last_updated === "string" ? fm.last_updated : undefined, | ||
| now, | ||
| ); | ||
| return days !== null && days > staleDays | ||
| ? { file: relative(config.scaffoldRoot, file), days } | ||
| : null; | ||
| }) | ||
| .filter((v): v is { file: string; days: number } => Boolean(v)); | ||
|
|
||
| const memoryCleanupDue = isMemoryCleanupDue(config.projectRoot, memoryCleanupDays, now); | ||
| const oldDailyMemoryFiles = oldMemoryFiles(config.projectRoot, dailyRetentionDays, now); | ||
|
|
||
| return { | ||
| ok: staleFiles.length === 0 && !memoryCleanupDue && oldDailyMemoryFiles.length === 0, | ||
| staleFiles, | ||
| memoryCleanupDue, | ||
| oldDailyMemoryFiles, | ||
| }; |
|
Addressed the actionable release-blocking review feedback in |
…#47) Per #43, mex log and mex timeline have direct module tests in test/events.test.ts but no Commander-level coverage. The risky areas called out in the Copilot review on #39 (invalid --type, repeated --file accumulator, --limit parsing through parsePositiveIntArg, parseAsync wiring) had no regression net. This PR adds test/cli.test.ts covering: - parsePositiveIntArg + parseIntArg direct tests for positive / zero / negative / non-numeric inputs. - mex log via parseAsync: default --type=note, repeated --file accumulates, --type decision propagates, invalid --type is rejected at the handler layer. - mex timeline via parseAsync: --limit parses as integer, --limit 0 and --limit foo throw, --json / --since / --type flags propagate. Each Commander test rebuilds the command locally rather than importing program from src/cli.ts (which calls program.parse() at import time). src/cli.ts exports parsePositiveIntArg and parseIntArg so the parser helpers can be tested directly. patterns/cli-option-parsing-tests.md captures the approach per the AGENTS.md After Every Task rule. Fixes #43 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Daksh Jaitly <thedakshjaitly@gmail.com>
Summary
Prepares the mex v0.3 operational-memory release on top of the stable v0.2 architecture.
This includes:
mex watch --intervalmex logandmex timelinemex doctor, shell completions, and improved check outputmexandmex tuipromexeus@0.3.5RELEASE_NOTES.mdRelease Notes
Full release notes are in
RELEASE_NOTES.mdand cover:Verification
npm run typechecknpm test— 138 passingnpm run buildnode dist/cli.js --version→0.3.5npm --cache /private/tmp/mex-npm-cache pack --dry-run→promexeus-0.3.5.tgzgit diff --checknode dist/cli.js tui