feat: record_directive — the PM's ledger in pm-state (P4, completes Reginald)

The last agent tool. When the PM states standing intent ("pilots come first"),
Reginald logs it verbatim to an append-only JSONL ledger in the pm-state repo —
a directive is intent; its effects still land through propose_change. This
completes Reginald's tool surface: query_project · propose_change · capture_work
· record_directive.

core (@commitea/core):
- directives/record-directive-v0: schema (kind/quote/target/params/rationale +
  id/ts/status), serialize/parseDirectiveLog (ts-ordered, seq computed on read,
  corrupt lines skipped), appendDirective (concatenation merge), toDirectiveInput.
- RECORD_DIRECTIVE_TOOL + system prompt update ("log standing intent; never claim
  a change is applied").
- gitea client: getFile/putFile (contents API, base64-agnostic) for the pm-state repo.

app:
- main: a pm-state client (same token, `commitea-pm-state` repo — the purity
  split, D4); appendDirectiveEntry (read→append→write, id/ts stamped here),
  readDirectives. model:chat executes record_directive; pmstate:directives reads
  the ledger. Degrades cleanly when the pm-state repo is absent.
- Directives screen shows the real ledger when present, the fixture demo otherwise.

Note: the pm-state repo isn't created yet — my token lacks write:user (repo
creation). Create `commitea-pm-state` (private) to activate the live path; all the
code + tests are in place. Override with COMMITEA_PMSTATE_REPO.

Verified: 116 core tests green (8 directive + 2 contents-API added), desktop
typecheck clean, 14 fixture e2e green. Gated live test: the real gemma-4-26b calls
record_directive for "pilots come first" (logs intent, doesn't claim to apply it);
the append/read + POST/PUT contents paths are unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-08 22:39:12 -04:00
parent 6e8a6a15bc
commit ba9ea43b4c
12 changed files with 426 additions and 6 deletions

View File

@@ -22,10 +22,11 @@ import {
REGINALD_SYSTEM,
REGINALD_TOOLS,
runAgentTurn,
toDirectiveInput,
} from '@commitea/core'
import { ipcMain } from 'electron'
import { getGiteaClient, reconcileSnapshot } from './gitea.js'
import { appendDirectiveEntry, getGiteaClient, getPmStateClient, reconcileSnapshot } from './gitea.js'
/** Small local model for prose + the read tool; big model reserved for later decomposition. */
function resolveModelRouter(): ModelRouter | null {
@@ -106,6 +107,16 @@ export function registerModelIpc(): void {
? { proposed: built.map((p) => ({ issue: a.issue, diff: describeChange(p.plan) })) }
: { proposed: [], note: 'no change — already at that value' }
}
if (name === 'record_directive') {
const pm = getPmStateClient()
if (!pm) return { error: 'pm-state is not configured' }
try {
const entry = await appendDirectiveEntry(pm, toDirectiveInput(args))
return { recorded: { kind: entry.kind, quote: entry.quote } }
} catch (e) {
return { error: `could not record — is the pm-state repo created? (${e instanceof Error ? e.message : e})` }
}
}
return { error: `unknown tool: ${name}` }
}