chore: enforce Ask Matt workflow guardrails

This commit is contained in:
LLLin000 2026-07-18 14:39:37 +08:00
parent 4ef9e9834a
commit 6dfb209de4
4 changed files with 329 additions and 8 deletions

11
.omp/RULES.md Normal file
View file

@ -0,0 +1,11 @@
# Matt Workflow Guardrails
- Ask Matt owns the engineering lifecycle: route with `/ask-matt`, implement one agent-ready issue with `/implement`, then run `/review`. Do not introduce a second orchestration workflow.
- One implementation session handles one issue in one authoritative worktree. Start a fresh session before changing issues; use `/handoff` only when context must cross sessions.
- Before editing, pin the issue, parent PRD, issue `updated_at`, base SHA, worktree root, scope, acceptance criteria, and verification commands. If the issue changes, stop and refresh this contract.
- Use one writer. Parallel `task` batches may contain only read-only `scout`, `reviewer`, or `librarian` agents. Never let parallel agents edit shared files or the same worktree.
- Never copy implementation files between the main checkout and a worktree. All reads, writes, tests, reviews, and commits for an issue use its authoritative worktree.
- Follow Matt `/tdd` at pre-agreed seams. Run focused checks while implementing and the issue-specific final gate once after the last mutation; UI work also needs a real-browser smoke test.
- Review exactly once on Matt's two axes. Consolidate findings into one repair pass and one re-review. If an important defect remains, return to the acceptance contract or split the issue; do not create repeated Final/Definitive review loops.
- Commit, merge, push, create/merge a PR, or close an issue only after verification succeeds after the last mutation. Update `PROJECT-MANAGEMENT.md` and the active queue according to project rules before closeout.
- The removed Superpowers workflows are not part of this project. Do not reinstall, invoke, emulate, or delegate through them.

View file

@ -0,0 +1,90 @@
import { execFileSync } from "node:child_process";
import path from "node:path";
import { describe, expect, test } from "bun:test";
import register from "./matt-guard";
type Handler = (
event: Record<string, unknown>,
context?: { cwd: string },
) => unknown | Promise<unknown>;
const root = path.resolve(import.meta.dir, "../../..");
function handlers() {
const registered: Record<string, Handler> = {};
register({
on: (event: string, handler: Handler) => {
registered[event] = handler;
},
} as unknown as Parameters<typeof register>[0]);
return registered;
}
function foreignWorktree(): string | undefined {
const current = path.normalize(root).toLowerCase();
return execFileSync("git", ["worktree", "list", "--porcelain"], {
cwd: root,
encoding: "utf8",
})
.split(/\r?\n/)
.filter((line) => line.startsWith("worktree "))
.map((line) => path.normalize(line.slice("worktree ".length)))
.find((worktree) => worktree.toLowerCase() !== current);
}
describe("Matt workflow guard", () => {
test("allows parallel read-only agents and blocks a writer", async () => {
const hook = handlers();
const context = { cwd: root };
expect(
await hook.tool_call(
{
toolName: "task",
input: { tasks: [{ agent: "reviewer" }, { agent: "scout" }] },
},
context,
),
).toBeUndefined();
expect(
await hook.tool_call(
{
toolName: "task",
input: { tasks: [{ agent: "reviewer" }, {}] },
},
context,
),
).toMatchObject({ block: true });
});
test("requires verification after the latest mutation", async () => {
const hook = handlers();
const context = { cwd: root };
const commit = { toolName: "bash", input: { command: "git commit --dry-run" } };
expect(await hook.tool_call(commit, context)).toMatchObject({ block: true });
await hook.tool_result({
toolName: "bash",
input: { command: "npm test" },
isError: false,
details: { exitCode: 0 },
});
expect(await hook.tool_call(commit, context)).toBeUndefined();
await hook.tool_call({ toolName: "edit", input: "mutation" }, context);
expect(await hook.tool_call(commit, context)).toMatchObject({ block: true });
});
test.skipIf(!foreignWorktree())("blocks writes into another linked worktree", async () => {
const hook = handlers();
const target = path.join(foreignWorktree()!, "CONTEXT.md");
expect(
await hook.tool_call(
{ toolName: "write", input: { path: target } },
{ cwd: root },
),
).toMatchObject({ block: true });
});
});

View file

@ -0,0 +1,211 @@
import { execFileSync } from "node:child_process";
import path from "node:path";
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
const READ_ONLY_AGENTS: Record<string, true> = {
scout: true,
reviewer: true,
librarian: true,
};
const RELEASE_COMMAND =
/\bgit(?:\.exe)?\b[^\r\n;&|]*\b(?:commit|merge|push)\b|\bgh(?:\.exe)?\s+(?:issue\s+close|pr\s+(?:create|merge))\b/i;
const VERIFICATION_COMMAND =
/\b(?:pytest|unittest|vitest|jest|ruff|mypy|pyright|tsc)\b|\b(?:npm|pnpm|yarn|bun)\s+(?:test|run\s+(?:test|typecheck|build|lint))\b|\bcargo\s+(?:test|check)\b|\bgo\s+test\b|\bdotnet\s+test\b/i;
const URI = /^[a-z][a-z0-9+.-]*:\/\//i;
interface GuardState {
verifiedAfterMutation: boolean;
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
}
function json(value: unknown): Record<string, unknown> {
try {
return record(JSON.parse(String(value ?? "")));
} catch {
return {};
}
}
function runGit(cwd: string, args: string[]): string {
try {
return execFileSync("git", args, {
cwd,
encoding: "utf8",
windowsHide: true,
stdio: ["ignore", "pipe", "ignore"],
}).trim();
} catch {
return "";
}
}
function normalized(value: string): string {
const resolved = path.resolve(value);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
function isWithin(root: string, candidate: string): boolean {
const relative = path.relative(root, candidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function crossesWorktrees(candidate: string, cwd: string): boolean {
if (!candidate || URI.test(candidate)) return false;
const current = runGit(cwd, ["rev-parse", "--show-toplevel"]);
if (!current) return false;
const target = normalized(path.resolve(cwd, candidate));
const currentRoot = normalized(current);
const worktrees = runGit(cwd, ["worktree", "list", "--porcelain"])
.split(/\r?\n/)
.filter((line) => line.startsWith("worktree "))
.map((line) => normalized(line.slice("worktree ".length)));
return worktrees.some(
(root) => root !== currentRoot && isWithin(root, target),
);
}
function pathsFromCall(toolName: string, input: unknown): string[] {
const fields = record(input);
if (toolName === "edit") {
const patch = typeof input === "string" ? input : String(fields.input ?? "");
return [...patch.matchAll(/^\[([^#\r\n]+)#[0-9A-F]{4}\]$/gm)].map(
(match) => match[1],
);
}
if (toolName === "ast_edit") {
return Array.isArray(fields.paths) ? fields.paths.map(String) : [];
}
if (toolName === "lsp") {
return typeof fields.file === "string" ? [fields.file] : [];
}
if (toolName !== "write") return [];
const target = String(fields.path ?? "");
if (target === "xd://ast_edit") {
const payload = json(fields.content);
return Array.isArray(payload.paths) ? payload.paths.map(String) : [];
}
if (target === "xd://lsp") {
const payload = json(fields.content);
return typeof payload.file === "string" ? [payload.file] : [];
}
return URI.test(target) ? [] : [target];
}
function tasksFromCall(input: unknown): Record<string, unknown>[] {
const tasks = record(input).tasks;
return Array.isArray(tasks) ? tasks.map(record) : [];
}
function hasWriterTask(input: unknown): boolean {
return tasksFromCall(input).some(
(task) => READ_ONLY_AGENTS[String(task.agent ?? "")] !== true,
);
}
function isMutatingCall(toolName: string, input: unknown): boolean {
if (toolName === "edit" || toolName === "ast_edit") return true;
if (toolName === "task") return hasWriterTask(input);
if (toolName === "lsp") {
const fields = record(input);
return (
fields.action === "rename" ||
fields.action === "rename_file" ||
(fields.action === "code_actions" && fields.apply === true)
);
}
if (toolName !== "write") return false;
const fields = record(input);
const target = String(fields.path ?? "");
if (!URI.test(target)) return true;
if (target === "xd://ast_edit") return true;
if (target !== "xd://lsp") return false;
const payload = json(fields.content);
return (
payload.action === "rename" ||
payload.action === "rename_file" ||
(payload.action === "code_actions" && payload.apply === true)
);
}
function releaseAction(toolName: string, input: unknown): boolean {
const fields = record(input);
if (toolName === "bash") return RELEASE_COMMAND.test(String(fields.command ?? ""));
if (toolName === "github") {
return ["pr_create", "pr_push"].includes(String(fields.op ?? ""));
}
if (toolName !== "write" || fields.path !== "xd://github") return false;
return ["pr_create", "pr_push"].includes(String(json(fields.content).op ?? ""));
}
function successfulVerification(event: Record<string, unknown>): boolean {
if (event.toolName !== "bash" || event.isError === true) return false;
const input = record(event.input);
if (!VERIFICATION_COMMAND.test(String(input.command ?? ""))) return false;
const details = record(event.details);
const exitCode = details.exitCode ?? details.exit_code ?? details.code;
if (typeof exitCode === "number") return exitCode === 0;
const content = Array.isArray(event.content) ? event.content : [];
const text = content.map((chunk) => String(record(chunk).text ?? "")).join("\n");
return !/command exited with code\s+[1-9]\d*/i.test(text);
}
export default function mattGuard(pi: ExtensionAPI): void {
const state: GuardState = { verifiedAfterMutation: false };
pi.on("tool_call", async (event, ctx) => {
const toolName = String(event.toolName);
const input = event.input;
if (toolName === "task") {
const tasks = tasksFromCall(input);
if (tasks.length > 1 && hasWriterTask(input)) {
return {
block: true,
reason:
"Matt guard: parallel task batches may contain only scout, reviewer, or librarian agents. Use one writer.",
};
}
}
const crossWorktree = pathsFromCall(toolName, input).find((candidate) =>
crossesWorktrees(candidate, ctx.cwd),
);
if (crossWorktree) {
return {
block: true,
reason: `Matt guard: refusing write into another Git worktree: ${crossWorktree}`,
};
}
if (releaseAction(toolName, input) && !state.verifiedAfterMutation) {
return {
block: true,
reason:
"Matt guard: run the issue-specific verification gate after the last mutation before commit, merge, push, PR creation, or issue close.",
};
}
if (isMutatingCall(toolName, input)) state.verifiedAfterMutation = false;
});
pi.on("tool_result", async (event) => {
if (successfulVerification(event as unknown as Record<string, unknown>)) {
state.verifiedAfterMutation = true;
}
});
}

View file

@ -1,13 +1,13 @@
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-15
> **Active work:** [Control-center PRD #74](https://github.com/LLLin000/PaperForge/issues/74) is split into eight dependency-linked implementation issues (#75#82). Canonical setup/config migration (#75) and the Installation/Help capability tracer (#76) are implemented; Managed Runtime lifecycle (#77) is the next production slice.
> **Branch:** `master` | **Last Updated:** 2026-07-18
> **Active work:** [Control-center PRD #74](https://github.com/LLLin000/PaperForge/issues/74) is split into eight dependency-linked implementation issues (#75#82). Setup/config (#75), capability envelopes (#76), and Managed Runtime/navigation (#77) are implemented; Library/OCR/Memory capability tracers (#78) are next.
>
> ---
>
> **Current state:** Retrieval recovery is merged and live. OCR multi-key rebuild/redo streams progress and refreshes canonical maintenance state. The control plane now has schema-v1 capability envelopes from Python through the Obsidian settings surface: Installation and Help use real probes, stale/malformed cached evidence fails closed, and Library/OCR/Memory/Maintenance remain explicit placeholders for later tracers.
> **Current state:** Retrieval recovery is merged and live. OCR multi-key rebuild/redo streams progress and refreshes canonical maintenance state. The control plane now includes schema-v1 capability envelopes plus a machine-local Managed Runtime with immutable slots, atomic activation, rollback, cancellation, and managed-first command dispatch.
>
> #71/#72 prototypes and #73 migration contract are complete. PRD #74 defines the six-module production program; #75 and #76 are implemented. The approved navigation refinement preserves the Wayfinder overview and stages `概览 / 模块详情 / 维护 / 帮助` across #77/#78/#80 instead of creating dead placeholder detail pages.
> #71/#72 prototypes and #73 migration contract are complete. PRD #74 defines the six-module production program; #75#77 are implemented. The Wayfinder overview and `概览 / 模块详情 / 维护 / 帮助` shell are live; #78 adds real Library/OCR/Memory detail tracers without placeholder pages.
>
> Next: implement [#77](https://github.com/LLLin000/PaperForge/issues/77) in a fresh Matt `/implement` session.
> Next: implement [#78](https://github.com/LLLin000/PaperForge/issues/78) in a fresh Matt `/implement` session.
## 1. Architecture
### 1.1 The problem (pre-v2)
@ -60,6 +60,8 @@ raw observations → structural signatures → stable anchors/families → zone
| Maintenance regression action model | **19/19 passed** ✅ (per-row canonical action routing, redo confirmation gate, cache manifest preservation) |
| Canonical setup/config migration (#75) | **61 passed, 0 failed** ✅ (fresh/v1/v2 config, CLI routing, path forwarding, failure exit, idempotent rerun) |
| Installation/Help capability tracer (#76) | **21 backend tests + 169 plugin tests passed; typecheck/build clean; independent review PASS** ✅ |
| Managed Runtime lifecycle + navigation (#77) | **192 focused + 289 full passed; typecheck/build clean** ✅ |
| OMP Matt workflow guard | **Fresh OMP auto-discovery confirmed; 7/7 deterministic guard cases passed** ✅ |
</br>
</br>
@ -85,6 +87,8 @@ raw observations → structural signatures → stable anchors/families → zone
| **Control-plane contracts** | ✅ Orthogonal capability/activity/attention model and managed-runtime immutable-slot architecture chosen; documented in `docs/research/2026-07-14-capability-state-action-contract.md` and `docs/research/2026-07-14-managed-runtime-architecture.md` |
| **Setup/config migration** | ✅ Bare, headless, and modular setup share `SetupPlan`; schema-v2 `vault_config` is authoritative with warned v1 read fallback |
| **Capability tracer** | ✅ Installation/Help backend envelopes feed the six-module Overview; malformed/stale persistence fails closed; setup/update actions route to the setup flow |
| **Managed Runtime** | ✅ Immutable machine-local slots, atomic pointer activation, rollback/cancel/retention, managed-first command dispatch |
| **Agent workflow controls** | ✅ `.omp/RULES.md` + `matt-guard.ts` enforce one-writer Matt flow, worktree isolation, and post-mutation verification before release operations |
### 2.3 Fix Status
@ -121,6 +125,7 @@ raw observations → structural signatures → stable anchors/families → zone
| 32 | — | OCR rebuild progress + maintenance selection contract | Feature | Added flushed, prefix-separated rebuild/redo streams; full keyed redo; cooperative stop; canonical `needs_derived_rebuild`; All/Recommended filters; selected batch progress UI | `d7b0a527`, `3a516add`, `e556c8ba` |
| 33 | — | OCR maintenance canonical per-row action model | Fix | Added `maintenanceActionForRow()` for `display_action`→verb routing, `maintenanceActionRequiresConfirmation()` redo confirmation gate, batch-action filtering by canonical verb, and cache-manifest preservation. Batch actions now follow the canonical backend action instead of raw `can_rebuild`/`can_redo` booleans; destructive redo requires user confirmation; cache refresh preserves the backend manifest. | `d7b0a527` |
| 34 | — | Setup/config paths had conflicting precedence, duplicate engines, dropped user paths, and false-success exits | Migration/prefactor | Unified CLI dispatch on `SetupPlan`; v2-only writes with v1 read fallback; complete path forwarding; visible deprecation; non-zero required-step failures | `af849699`, `7b747423`, `906b3caf` |
| 35 | — | Agent sessions could bypass the Matt issue lifecycle, fan out writers, cross worktree boundaries, or release after stale verification | Workflow guard | Added sticky Matt rules plus a project hook that blocks parallel writer batches, cross-worktree mutations, and commit/merge/push/PR/issue-close operations until a recognized verification command succeeds after the latest mutation. | — |
## 3. Remaining Issues — Release-Readiness Layers
@ -150,7 +155,7 @@ raw observations → structural signatures → stable anchors/families → zone
[Wayfinder: Restore PaperForge retrieval end to end](https://github.com/LLLin000/PaperForge/issues/45) completed and the resulting retrieval fixes are merged to `master`. The live Literature-hub vault has a healthy 2560-dimensional vec0 index; M and @ search paths are operational.
### Layer 3: Plugin UI
The OCR maintenance slice has a canonical All/Recommended state model, selected batch actions, streaming progress, cooperative stop, and canonical per-row action routing with confirmation gates for destructive operations. Issue #76 now provides the first production capability-envelope tracer: real Installation/Help probes, a six-module Overview, strict persisted-envelope validation, stale fail-closed behavior, and backend-owned actions. Library/OCR/Memory/Maintenance remain explicit placeholders until #78/#80. The approved Wayfinder navigation refinement is recorded on PRD #74 and staged across #77/#78/#80.
The OCR maintenance slice has a canonical All/Recommended state model, selected batch actions, streaming progress, cooperative stop, and canonical per-row action routing with confirmation gates for destructive operations. Issues #76#77 now provide production capability envelopes, the four-destination navigation shell, and the machine-local Managed Runtime lifecycle. Installation/Help are real probes; Library/OCR/Memory/Maintenance remain explicit placeholders pending #78/#80. Stale or malformed persisted evidence fails closed.
### Layer 4: Downstream Tools
`chunker.py` uses hardcoded section regex + fixed 3-paragraph groups. OCR has rich structured output (sections, headings, figures, tables with captions) — chunker should consume this structure directly. Figures/tables should support separate embedding (text + future vision).
@ -158,7 +163,7 @@ The OCR maintenance slice has a canonical All/Recommended state model, selected
Remaining legacy OCR issues (carried forward):
## 4. Active Queue
1. 🔴 **[Control-center PRD #74](https://github.com/LLLin000/PaperForge/issues/74)** — published and split into eight native dependency-linked issues (#75#82). #75 and #76 are implemented; #77 Managed Runtime lifecycle is the next unblocked production tracer.
1. 🔴 **[Control-center PRD #74](https://github.com/LLLin000/PaperForge/issues/74)** — published and split into eight native dependency-linked issues (#75#82). #75#77 are implemented; #78 Library/OCR/Memory capability tracers are the next unblocked slice.
2. ✅ **[Capability-state vocabulary](https://github.com/LLLin000/PaperForge/issues/69)** — resolved at `issuecomment-4971161072`. Orthogonal availability/activity/attention axes, 6-state capability ordinal, 12 canonical verbs, backend-owned severity and primary actions, maintenance projection.
3. ✅ **[Managed runtime](https://github.com/LLLin000/PaperForge/issues/70)** — resolved at `issuecomment-4971239398`. Plugin-managed immutable runtime slots, system-Python bootstrap with validated-triplet fallback, single `active-runtime.json` pointer, `ManagedRuntime` class with `current()`/`status()`/`ensure()`, fail-closed command resolution.
4. ✅ **[Control-center prototype](https://github.com/LLLin000/PaperForge/issues/71)** — resolved with independent Critical/Important PASS review and browser verification at 768px. Six-module control-center HTML prototype covers 5 scenarios with plain-button switcher, primary attention zone, responsive layout, and capability-gated actions. Design decisions recorded in `docs/prototypes/2026-07-14-six-module-control-center.html/.md`. No production implementation before #73.
@ -184,7 +189,8 @@ Remaining legacy OCR issues (carried forward):
- [x] Publish PRD #74 and eight agent-ready issues (#75#82) with native dependencies
- [x] Canonicalize setup/config migration ([#75](https://github.com/LLLin000/PaperForge/issues/75)) — 61 focused tests; spec PASS; quality APPROVED
- [x] Implement Installation/Help capability tracer ([#76](https://github.com/LLLin000/PaperForge/issues/76)) — 21 backend tests; 169 plugin tests; typecheck/build clean; independent review PASS
- [ ] Implement Managed Runtime lifecycle and the approved Installation-detail navigation shell ([#77](https://github.com/LLLin000/PaperForge/issues/77))
- [x] Implement Managed Runtime lifecycle and the approved Installation-detail navigation shell ([#77](https://github.com/LLLin000/PaperForge/issues/77)) — 192 focused + 289 full tests; typecheck/build clean; merged to `master`
- [ ] Expose Library, OCR, and Memory capabilities end to end ([#78](https://github.com/LLLin000/PaperForge/issues/78))
---
## 5. Key File Map
@ -341,6 +347,7 @@ Remaining legacy OCR issues (carried forward):
| 2026-07-15 | Canonical setup is one `SetupPlan`; configuration writes converge to schema v2 | Duplicate headless/modular/bare engines dropped path inputs and disagreed on success. One engine plus `vault_config`-first reads makes migration observable, idempotent, and reversible through the warned v1 read fallback. |
| 2026-07-15 | Capability integration advances by real tracer, never frontend optimism | #76 exposes only Installation and Help as real probes; Library/OCR/Memory/Maintenance remain explicit placeholders until their backend envelopes ship. Persisted malformed or stale evidence becomes unknown/invalid rather than ready. |
| 2026-07-15 | Module detail navigation extends Wayfinder instead of replacing it | Preserve the primary-attention zone and concrete backend action. Stage `概览 / 模块详情 / 维护 / 帮助` across #77/#78/#80; use explicit module-title navigation, top ordinary buttons, and no dead placeholder details. |
| 2026-07-18 | Enforce Matt with sticky rules plus a minimal deterministic hook | Prose alone cannot prevent orchestration drift. Keep lifecycle guidance in `.omp/RULES.md`; use the hook only for mechanically provable boundaries: one writer, worktree isolation, and verification-after-last-mutation before release operations. |
---
@ -444,6 +451,8 @@ python -m ruff check paperforge/worker/ocr_*.py
| 2026-07-15 | Six-module control-center + maintenance inbox prototypes completed | Closed #71 (six-module HTML prototype, 5 scenarios, plain-button switcher, responsive 768px) with independent Critical/Important PASS review and browser verification. Closed #72 (actionable-only inbox, inline issue-draft review, confirmation-first report) with same review gate. Both prototype pairs passed all review dimensions. No production code changed. | `docs/prototypes/2026-07-14-six-module-control-center.{html,md}`, `docs/prototypes/2026-07-14-maintenance-issue-reporting.{html,md}` |
| 2026-07-15 | Control-center PRD split + first production slice | Published PRD #74 and eight native dependency-linked issues (#75#82). Implemented #75: one SetupPlan for all setup entry points, schema-v2 `vault_config`, warned v1 read fallback, complete path forwarding, visible deprecation, and non-zero required-step failures. Verification: 61/61 focused tests; independent Spec PASS / Quality APPROVED. | [PRD #74](https://github.com/LLLin000/PaperForge/issues/74), [Issue #75](https://github.com/LLLin000/PaperForge/issues/75) |
| 2026-07-15 | Installation/Help capability tracer + navigation refinement | Implemented schema-v1 probe envelopes, six-module Overview, setup-complete migration, strict persistence/TTL validation, backend-owned action labels and dispatch, responsive/focus-visible UI, and generated bundle. Verification: 21 backend tests, 169 plugin tests, typecheck/build, live Obsidian stale-cache/action-label smoke test, independent review PASS. Matt flow refined PRD #74 and existing #77/#78/#80 without duplicating issues. | [Issue #76](https://github.com/LLLin000/PaperForge/issues/76), [PRD refinement](https://github.com/LLLin000/PaperForge/issues/74#issuecomment-4980322098) |
| 2026-07-15 | Managed Runtime lifecycle + final navigation shell | Completed #77 with immutable runtime slots, synchronous fail-closed `current`, probed `status`, install/repair/update/rollback/cancel/retention, managed-first dispatch, Release-N fallback, four-destination navigation, Installation detail, Agent integration, and Help focus restoration. Verification: 192 focused + 289 full tests; typecheck/build clean. Merged to `master` in `173a4e8..4ef9e98`. | [Issue #77](https://github.com/LLLin000/PaperForge/issues/77) |
| 2026-07-18 | OMP Matt workflow enforcement | Removed all discoverable Superpowers installations, retained canonical Ask Matt skills, added sticky project rules, and installed an auto-discovered hook for one-writer batches, worktree isolation, and fresh verification gates. Fresh OMP smoke reached the hook; 7/7 deterministic guard cases passed. | `.omp/RULES.md`, `.omp/hooks/pre/matt-guard.ts` |
## 9. Historical Detail Archive