chore: initialize POM 2 repo (docs guards green; release gates recorded NOT RUN pending install)
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
*.tgz
|
||||
*.zip
|
||||
coverage/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.log
|
||||
160
AGENTS.md
Normal file
160
AGENTS.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Overview
|
||||
|
||||
- identity: `@jamminrebel/pom-omp` 2.0.0, MIT, ESM — POM 2 "Produce Magnum Opus", an Oh My Pi (OMP) extension.
|
||||
- purpose: turn OMP's native models, tools, task agents, IRC, todo, sessions, approvals, compaction and TUI into an evidence-gated production operating system for long-form creative work (README.md:3).
|
||||
- governs, never replaces: OMP performs execution; POM adds stage contracts, canonical artifacts, prompt provenance, deterministic validation, Git checkpoints, and an Obsidian/Quartz-compatible vault (README.md:5, POM.yml:6-9, docs/ARCHITECTURE.md:3-27).
|
||||
- ships: 9 stages (0..8), 7 `pom_*` tools, 14 task agents, 13 prompt fragments, 3 rules, 2 themes, native task+IRC Hive planning, optional Swarm DAGs, verified ZIP delivery (README.md:9-20).
|
||||
- operator loop: `/pom new "Title"` -> `/pom run next` -> `/pom hive stage` -> `/pom evidence` -> `/pom check stage` -> model calls `pom_stage action=pass` only after a PASS report -> `/pom export final` (README.md:44-56). `/pom` opens the command center; `Alt+P` is the shortcut.
|
||||
- this file is the repository contributor guide. Generated POM projects get their own `AGENTS.md` written by `bootstrapProject` (src/persistence.ts:241-307) — different file, different audience.
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
- flow: `/pom` command -> parser -> stage machine -> persistence + git + router -> prompt-stack injection -> OMP model turn -> `pom_*` tools -> validators -> exporter.
|
||||
- entry point: default export `pomExtension(pi: ExtensionAPI)` in `src/index.ts`; sets label `POM · Produce Magnum Opus`, builds the runtime, then registers in this order: `registerPomTools`, `registerPomRenderers`, `registerPomEvents`, `registerPomCommand` (src/index.ts:13-49). Keep registration centralized there.
|
||||
- runtime boundary: `src/runtime.ts` `PomRuntime` = `get`, `set`, `restore`, `getConfig`, `refreshConfig`, `telemetry`.
|
||||
- `set` clones state, stamps `updatedAt`, appends a session branch entry of type `pom-state`, writes `00_admin/project_state.json`, and syncs generated knowledge files when `state.initialized` (src/index.ts:25-31). Never persist state around it.
|
||||
- `restore` precedence: session branch via `restoreLatest` -> `<cwd>/00_admin/project_state.json` -> fresh uninitialized state (src/index.ts:33-42).
|
||||
- command layer: `src/command.ts` registers `pom` plus shortcut `alt+p` and owns all verbs (`new`, `run`, `hive`, `swarm`, `evidence`, `check`, `vault`, `export`, `status`, `prompt`, `tools`, `theme`, `settings`, `doctor`, `help`). `src/parser.ts` tokenizes quotes/escapes, defaults the verb to `dashboard`, and parses `--flag[=value]` into a Map.
|
||||
- stage run: `runStage` = `startStage(gotoStage(...))` -> `runtime.set` -> `enterStageBranch` -> `routeStage` -> `updateChrome` -> `pi.sendUserMessage(await renderStagePrompt(...), { deliverAs: "followUp" })` (src/command.ts:94-105).
|
||||
- routing: `src/router.ts` applies the stage tool profile when `autoToolProfiles`, resolves `spec.modelRole` via `ctx.models.resolve`, calls `pi.setModel`, and sets thinking `xhigh` for every stage except stage 7 (`high`).
|
||||
- domain: `src/domain.ts` holds `POM_VERSION` / `POM_SCHEMA_VERSION` (both "2.0.0"), `STAGES: readonly StageSpec[]`, `ArtifactRecord`, `EvidenceRecord`, `StageBlocker`, `StageRunState`, `HiveTask`, `HiveMission`, `PomState`, `PomConfig`, `DEFAULT_CONFIG`.
|
||||
- stages: 0 init, 1 genre, 2 voice, 3 store, 4 bible, 5 outline, 6 chapters, 7 draft, 8 delivery. Each `StageSpec` owns `id`, `slug`, `name`, `icon`, `toolProfile`, `modelRole`, `agents`, `artifacts`, `acceptance`, `todos`.
|
||||
- state machine: `src/state.ts`, clone-then-mutate.
|
||||
- `gotoStage` rejects incomplete prerequisites; `startStage` rejects complete stages and unresolved blockers; `passStage` requires an active run, a validation report path, zero unresolved blockers, and evidence for every acceptance criterion (src/state.ts:97-188).
|
||||
- `recordEvidence` checks the criterion belongs to the active stage, demands a non-empty summary plus >=1 path, de-duplicates normalized paths, replaces prior evidence for that criterion (src/state.ts:144-168).
|
||||
- canonical state path: `<projectRoot>/00_admin/project_state.json` via `statePath`.
|
||||
- persistence: `src/persistence.ts` serializes writes per absolute path (`writeQueues`), writes a temp file then `rename`, and appends a trailing newline to JSON.
|
||||
- manifest `00_admin/artifact_manifest.json`: entries need safe unique relative paths, stage 0..8, semver version, valid status, SHA-256, positive bytes, parseable `updatedAt`, valid rights status.
|
||||
- `registerArtifact`: `secureExistingPath` -> reject empty file -> `relativeProjectPath` -> SHA-256 -> manifest save -> `artifact.registered` in `00_admin/artifact_events.jsonl` -> knowledge sync (src/persistence.ts:123-163).
|
||||
- `bootstrapProject` creates `00_admin`, `01_planning`, `02_story_bible`, `03_manuscript/chapters`, `04_images/{references,generated}`, `05_research`, `06_ledgers`, `07_exports`, `.omp/prompts`, `content`, and refuses a non-empty unrecognized directory that has no `00_admin/project_state.json` (src/persistence.ts:216-239).
|
||||
- events: `src/events.ts`.
|
||||
- `resources_discover` advertises bundled `../skills`, `../prompts`, `../themes` via `fileURLToPath(new URL(...))`; it does not advertise `agents/` or `rules/`.
|
||||
- `before_agent_start` injects the composed prompt stack into `event.systemPrompt` and returns a hidden `pom:event` message.
|
||||
- native `task` / `irc` are observed only: `tool_call` / `tool_result` update Hive mission status plus telemetry and append durable `native.task.started`, `native.task.finished`, `native.irc.send`, `native.irc.result` ledger events.
|
||||
- prompts: `src/prompts.ts` loads bundled `../prompts` plus project `.omp/prompts`; id = markdown basename; project fragments override bundled by id; every fragment is SHA-256 stamped in an HTML comment (id/source/hash); a stack is immutable law -> `personality-${state.mode}` -> requested ids, and each use is logged to the prompt ledger.
|
||||
- delegation: `src/studio.ts` builds `HiveMission` ids `s${spec.id}-${randomUUID().slice(0,8)}`, caps agents at `maxStudioAgents`, and converts a mission into one native task batch whose shared context tells agents to coordinate over native IRC and never pass the stage. `src/swarm.ts` optionally writes `00_admin/swarms/<missionId>.yaml` (specialists plus an `integrator`) through `secureFuturePath`.
|
||||
- validation: `src/validators.ts` `validatePom(state, scope, opts)`; scopes `quick`, `stage`, `canon`, `continuity`, `knowledge`, `files`, `delivery`, `all`; report schema "1.0.0"; `passed` is true only when no check has status `fail`; reports land in `00_admin/validation/stage-<n>-<scope>-<stamp>.json` when initialized.
|
||||
- delivery: `src/exporter.ts` writes `00_admin/SHA256SUMS.txt`, a delivery report under `07_exports`, then `07_exports/<projectSlug>-<kind>.zip`, verifies it and writes `<base>.verification.json`. `src/zip.ts` (yazl create, yauzl read, `crc-32`) rejects encryption, compression methods other than 0/8, duplicate members, unsafe names, empty archives, size drift and CRC mismatch, and returns the archive SHA-256.
|
||||
- terminal: `src/ui.ts` writes statuses `pom-stage`, `pom-progress`, `pom-gate`, `pom-hive` and widget `pom-hud`; `src/visual.ts` renders the HUD with width-safe truncation and caches unchanged frames; `src/renderers.ts` renders custom message types `pom:event`, `pom:validation`, `pom:hive`, `pom:milestone` and adds a thinking lane when enabled and view is not minimal/compact.
|
||||
- git: `src/git.ts` shells `execFile("git", ["-C", root, ...])`, initializes when needed, uses branches `pom/stage-<id>-<slug>`, and checkpoints with `git add -A` + `git commit` unless `gitMode` disables it.
|
||||
|
||||
## Key Directories
|
||||
|
||||
- src/: extension source, 25 modules, no build step (OMP loads TypeScript directly).
|
||||
- tests/: 8 `*.test.ts` files on `node:test`, importing production code.
|
||||
- scripts/: `verify.mjs` (structural gates), `release-manifest.mjs` (RELEASE.json generate/check), `smoke.ts` (runtime smoke), `install.sh` / `install.ps1`.
|
||||
- agents/: 14 `pom-*.md` specialist task-agent definitions.
|
||||
- prompts/: 13 prompt fragments; ids are basenames; `personality-*.md` map to `PomMode`.
|
||||
- rules/: 3 scoped production rules (`pom-artifact-integrity`, `pom-continuity`, `pom-rights`).
|
||||
- skills/pom/: `SKILL.md` plus `references/` (`core-law.jml`, `workflow.md`, `tool-routing.md`, `todo-blueprints.md`, `artifacts.md`, `agent-studios.md`, `commands.md`, `qa.md`, `ui.md`).
|
||||
- themes/: `pom-nocturne.json` (dark), `pom-parchment.json` (light).
|
||||
- config/: shipped templates — `config.yml` (OMP host settings), `pom.json` (POM defaults), `APPEND_SYSTEM.md`, `mcp.json.example`.
|
||||
- docs/: `ARCHITECTURE.md`, `SETTINGS.md`, `TOOL_MATRIX.md`, `INVENTORY.md`, `AUDIT.md`, `DESIGN_REVIEW.md`, `OFFICIAL_SOURCES.md`, `POM_ARCHITECTURE.mmd`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- setup: `npm install`
|
||||
- typecheck: `npm run check` -> `tsc --noEmit` (tsconfig includes `src/**/*.ts` only; tests are not typechecked)
|
||||
- tests: `npm test` -> `node --import tsx --test tests/*.test.ts`
|
||||
- one file: `node --import tsx --test tests/state.test.ts`
|
||||
- one case: `node --import tsx --test --test-name-pattern="<regex>" tests/state.test.ts`
|
||||
- runtime smoke: `npm run smoke` -> `node --import tsx scripts/smoke.ts`
|
||||
- structural gates: `node scripts/verify.mjs`
|
||||
- release manifest: `npm run release:manifest` (write), `npm run release:check` (fail if stale)
|
||||
- full gate: `npm run verify` = check -> test -> smoke -> scripts/verify.mjs -> release:check -> `npm pack --dry-run`
|
||||
- host install: `npm run link` (`omp plugin link .`), then restart OMP or `/reload-plugins`, then `/pom theme install`
|
||||
- host check: `npm run doctor` (`omp plugin doctor`; needs Bun + OMP installed)
|
||||
|
||||
## Code Conventions & Common Patterns
|
||||
|
||||
- imports:
|
||||
- relative source imports are extensionless: `./state`, `./domain`, `./tool-profiles` — never `.ts` / `.js`, despite `allowImportingTsExtensions: true`.
|
||||
- Node builtins use the `node:` prefix; OMP SDK types come in as `import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent"`.
|
||||
- errors: throw `Error` for precondition, schema, config, manifest and path violations (`assertState`, `loadPomConfig`, `secureExistingPath`); represent observable tool failures as `isError: true` results, not throws.
|
||||
- tools: defined in `src/tools.ts`; ids are exactly `pom_state`, `pom_stage`, `pom_artifact`, `pom_validate`, `pom_prompt`, `pom_hive`, `pom_export`.
|
||||
- schemas use `const { z } = pi.zod` with inline `z.object` / `z.enum`; one targeted `@ts-expect-error` covers OMP 16.4.x + Zod v4 instantiation depth (src/tools.ts:58-61) — keep it narrow.
|
||||
- every result goes through `textResult(text, details?, isError = false)` -> `{ content: [{ type: "text", text }], details, isError }`.
|
||||
- mutators declare `approval: "write"`; `pom_validate` and `pom_prompt` declare `approval: "read"`.
|
||||
- every execute handler returns `textResult("Cancelled", undefined, true)` when `signal?.aborted`.
|
||||
- state: clone with `structuredClone` before nested mutation, then persist through `runtime.set`.
|
||||
- async: `async`/`await` everywhere for FS, OMP hooks, tools, config and validation; no callback style.
|
||||
- naming: kebab-case multiword filenames (`tool-profiles.ts`); camelCase functions, PascalCase types; POM-owned identifiers keep the `pom` / `pom_` / `pom-` prefix per surface (tool ids `pom_*`, statuses `pom-*`, message types `pom:*`, agents `pom-*`).
|
||||
- tool profiles: `src/tool-profiles.ts` appends POM tools to host tools for non-`auto` profiles, always keeps `read`, and stores the baseline in a `WeakMap` keyed by `pi` — use `resetToolBaselineForTests()` rather than assuming a fresh baseline.
|
||||
- config: `src/config.ts` merges built-in `DEFAULT_CONFIG` -> `~/.omp/agent/pom.json` -> `<project>/.omp/pom.json`, validates booleans/enums/bounded integers and throws `Invalid POM setting ...`; writes go temp-file-then-rename.
|
||||
- keys and defaults: `autoToolProfiles` true, `autoModelRouting` true, `dashboardWidget` true, `stopGuard` true, `defaultMode` "autopilot", `defaultView` "cinematic", `defaultMotion` "subtle", `defaultToolProfile` "auto", `maxStudioAgents` 6, `projectRootMode` "child", `destructiveProjectCommands` "block", `gitMode` "branch-per-stage", `hudPlacement` "aboveEditor", `showThinkingLane` true (src/domain.ts:471-486, docs/SETTINGS.md:31-46).
|
||||
- OMP host settings belong in `<project>/.omp/config.yml`; the shipped template is `config/config.yml` and `bootstrapProject` writes the same values (theme `pom-nocturne`/`pom-parchment`, `task.maxConcurrency: 8`, `compaction.strategy: snapcompact`, `defaultThinkingLevel: high`).
|
||||
- asset shapes:
|
||||
- agents: frontmatter `name: pom-<kebab>`, non-empty `description`, `tools:` list that includes `yield`, plus `model`, `thinkingLevel`, `read-summarize`; enforced by `scripts/verify.mjs:58-64`.
|
||||
- rules: frontmatter `description` plus `globs` array.
|
||||
- skill: `skills/pom/SKILL.md` frontmatter `name: pom`, `description`, `alwaysApply: false`.
|
||||
- prompts: frontmatter optional — personality fragments carry `description`, operational fragments start with prose; the loader accepts any `.md`.
|
||||
- themes: keep `$schema`, `name` (must equal filename stem), `vars`, `colors`, `export`; `/pom theme install` copies `themes/*.json` into `getCustomThemesDir()` from `@oh-my-pi/pi-utils`.
|
||||
- POM.yml is the human-facing project/release manifest (boundaries, stages, gate, commands, tools, delegation, terminal, knowledge, integrity, resources, release) — not a runtime schema; nothing loads it at runtime.
|
||||
|
||||
## Important Files
|
||||
|
||||
- src/index.ts: extension entry, runtime construction, registration order.
|
||||
- src/domain.ts: schema versions, `STAGES`, artifact/evidence/blocker/hive/state/config types, `DEFAULT_CONFIG`.
|
||||
- src/state.ts: stage lifecycle invariants, evidence rules, canonical state path.
|
||||
- src/persistence.ts: workspace layout, atomic writes, manifest/ledger/knowledge persistence, bootstrap.
|
||||
- src/tools.ts: the 7 `pom_*` tool contracts, approvals, result shape.
|
||||
- src/events.ts: OMP event surface, resource discovery, prompt injection, native task/IRC observation.
|
||||
- src/validators.ts: deterministic checks and report contract.
|
||||
- src/paths.ts: `canonicalRoot`, `secureExistingPath`, `secureFuturePath`, `relativeProjectPath`, `assertSafeArchivePath`.
|
||||
- scripts/verify.mjs: structural release gates (see Testing & QA).
|
||||
- scripts/release-manifest.mjs: RELEASE.json generation and `--check` staleness semantics.
|
||||
- package.json: OMP entry `omp.extensions: ["./src/index.ts"]`, scripts, peer window, ship list.
|
||||
- tsconfig.json: `target ES2022`, `module ESNext`, `moduleResolution Bundler`, `strict`, `noEmit`, `skipLibCheck`, `allowImportingTsExtensions`.
|
||||
|
||||
## Runtime/Tooling Preferences
|
||||
|
||||
- package manager: npm, authoritative `package-lock.json` (lockfileVersion 3). Do not introduce bun/pnpm/yarn lockfiles.
|
||||
- local scripts run on Node + `tsx` + `tsc`; `engines.bun >= 1.3.14` describes the OMP host, and `npm run doctor` is the only Bun+OMP gate.
|
||||
- no build step and no `dist/`: OMP loads `src/index.ts` directly.
|
||||
- SDK window: `@oh-my-pi/pi-coding-agent`, `pi-tui`, `pi-utils` are peers at `>=16.4.6 <17`. Do not use APIs that require OMP 17.x.
|
||||
- runtime dependencies are deliberately three: `crc-32`, `yauzl`, `yazl` (ZIP integrity). Add a dependency only when the host SDK and Node builtins genuinely cannot do the job.
|
||||
- `.gitignore` covers `node_modules/`, `*.tgz`, `*.zip`, `coverage/`, `dist/`, `.DS_Store`, `*.log`.
|
||||
|
||||
## Testing & QA
|
||||
|
||||
- framework: Node's built-in runner, `import test from "node:test"` + `import assert from "node:assert/strict"`, flat `test("...")` calls, no `describe`.
|
||||
- tests import production code directly with extensionless specifiers (`../src/state`); never copy implementation into a test.
|
||||
- filesystem tests isolate with `mkdtemp(join(tmpdir(), ...))` and clean up in `finally` via `rm({ recursive: true, force: true })`.
|
||||
- test-file names do not map 1:1 to modules: `tests/command.test.ts` exercises `src/parser.ts`, `tests/events.test.ts` exercises `src/policy.ts`.
|
||||
- covered by unit tests: parser, policy, paths, state, domain, persistence, studio, swarm, validators, zip. Not directly covered: `src/tools.ts`, `src/visual.ts` — add tests there when changing behavior.
|
||||
- `scripts/smoke.ts` is the end-to-end guard: bootstrap a temp project with `projectRootMode: "cwd"`, assert generated `.omp/config.yml` and `content/production-board.md` content, then assert `validatePom(state, "quick", { writeReport: false }).passed === true`.
|
||||
- `scripts/verify.mjs` enforces, and will fail a change that breaks any of these:
|
||||
- required files exist, including `src/index.ts`, `src/events.ts`, `src/tools.ts`, `src/visual.ts`, `skills/pom/SKILL.md`, `config/APPEND_SYSTEM.md`, `prompts/stage-run.md`, both themes, `scripts/smoke.ts`.
|
||||
- `RELEASE.json.version === package.json.version`; release inventory stages/customTools/behavioralTests = 9/7/9; `pkg.omp.extensions` deep-equals `["./src/index.ts"]`; `pkg.version === "2.0.0"`; every `package.json.files` entry exists.
|
||||
- exactly one `registerCommand("pom"` and one `registerShortcut("alt+p"` in `src/command.ts`; exactly nine `id: 0..8` stage ids in `src/domain.ts`; exactly seven `name: "pom_*"` tools in `src/tools.ts`.
|
||||
- `src/tools.ts` contains none of `allowArgs`, `formatApprovalDetails`, `concurrency: "exclusive"`, `ExtensionAPI = any`; `src/omp-shim.d.ts` and `src/node-shim.d.ts` must not exist.
|
||||
- asset counts 14 agents / 13 prompts / 3 rules / 2 themes, agent frontmatter shape, theme names and required color keys (read from the installed `@oh-my-pi/pi-coding-agent` theme schema, so `npm install` is a prerequisite), and the nine required directories.
|
||||
- after changing any shipped file (anything in `package.json.files` plus `package.json`), run `npm run release:manifest`; `npm run release:check` compares rendered bytes and fails with "RELEASE.json is stale".
|
||||
- run `npm run verify` before packaging or linking. Host-level acceptance stays `npm run doctor` plus an interactive OMP session: `/pom` overlay, HUD, statuses, cards, `/pom theme install`, and a smoke project taken through stage 0 validation plus a checkpoint archive.
|
||||
|
||||
## Governance Invariants
|
||||
|
||||
Preserve these when changing runtime behavior (source: the shipped contributor guide, `rules/*.md`, `skills/pom/references/core-law.jml`).
|
||||
|
||||
- do not duplicate host primitives: task, IRC, todo, sessions, approvals, compaction, memory, model execution, TUI. Observe and route them; never build a parallel system.
|
||||
- stage completion requires active execution, complete evidence, zero open blockers, and a passing validation report. Never mark a stage complete by direct state mutation.
|
||||
- truth hierarchy: system + explicit user requirements > canonical project files and `00_admin/project_state.json` > manifest / evidence / ledgers > live OMP execution > vault projection > memory. The vault is a navigable projection, not a source of truth.
|
||||
- keep canonical state and artifacts under the project root; reject traversal and symlink escape through `src/paths.ts` helpers (lexical plus realpath containment) — do not substitute plain `join`/`resolve` checks.
|
||||
- never fake tool use, files, citations, images, counts, hashes, checks, ZIP tests, or completion. Partial, missing and blocked states stay disclosed as `PARTIAL` / `BLOCKED`.
|
||||
- artifact claims need real non-empty regular files with measured bytes, SHA-256, category, stage, semver version, status, rights, source and notes recorded in the manifest.
|
||||
- delivery claims need a fresh archive with safe member names, no duplicates/encryption/unsupported methods, central-directory parsing, decompression, size and CRC-32 checks, an archive hash, and an external verification report.
|
||||
- continuity: story changes must preserve or explicitly revise canon, timeline, knowledge, relationships, props, injuries, rules, mysteries, setups and payoffs; contradictions require explicit canon selection, dependency mapping, revision logging and revalidation.
|
||||
- rights: no living-author imitation, fabricated quotations, unauthorized asset redistribution, invented private-identity facts, or unsupported rights claims.
|
||||
- use OMP renderers and UI hooks (`setWidget`, `setStatus`, `setWorkingMessage`, message cards) instead of printing raw terminal control sequences.
|
||||
- delegation: one widest safe native task batch per independent wave, each task carrying objective, owned files and acceptance conditions; keep IRC terse with the `CLAIM / QUERY / ANSWER / ALERT / HANDOFF / DONE` vocabulary; durable findings go into project files or ledgers, not chat.
|
||||
- tool routing: native tool first — `read` for files/directories/archives/static URLs, `glob` for bounded discovery, `grep` for bounded regex, AST/LSP for symbol work, browser only for authenticated/interactive/JS-only pages, `ask` only after exhausting sources.
|
||||
|
||||
## Known Discrepancies
|
||||
|
||||
Verified inconsistencies in the current tree — do not "fix" them silently as part of unrelated work, and do not describe them as working.
|
||||
|
||||
- `pom_stage` accepts an `inspect` action in its schema with no dedicated branch; it falls through to the sync/refresh path and returns summary state. **Decision**: keep as summary fallback; if dedicated inspect behavior is needed, add a branch and update schema/tests.
|
||||
- `resources_discover` advertises only skills, prompts and themes; `agents/` and `rules/` are shipped but not advertised through that event. **Decision**: agents and rules are packaging-only resources; add them to resources_discover if/when agent-facing discovery is required.
|
||||
- `docs/INVENTORY.md` claims "9 behavioral tests across 8 test files"; the tree contains 8 test files with 9 test declarations. `scripts/verify.mjs` asserts the manifest number (9) without distinguishing file count from declaration count.
|
||||
44
CHANGELOG.md
Normal file
44
CHANGELOG.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## 2.0.0 — 2026-07-12
|
||||
|
||||
### Rebuilt
|
||||
|
||||
- Replaced existence-only gates with stage-specific artifact contracts and acceptance evidence.
|
||||
- Added authoritative sequential stage transitions, blocker ownership, attempts, active-run checks, and validation-report requirements.
|
||||
- Replaced global declaration shims with real OMP 16.4.x types; retained one localized TypeScript recursion suppression around OMP's deeply recursive Zod registration type.
|
||||
- Rebuilt delegation around native OMP `task` batches and terse IRC coordination.
|
||||
- Added optional OMP Swarm DAG generation for parallel, sequential, and pipeline runs.
|
||||
- Added append-only Hive mission, signal, prompt-use, artifact, and delivery ledgers.
|
||||
- Added project-local prompt overrides with SHA-256 provenance.
|
||||
- Added Obsidian/Quartz-compatible project vault and generated production board.
|
||||
- Added per-extension tool-profile baselines and stage-aware model/tool routing.
|
||||
- Added semantic artifact and canon version increments.
|
||||
|
||||
### Integrity and security
|
||||
|
||||
- Added canonical `realpath` containment and symlink escape rejection.
|
||||
- Added unique atomic writes and serialized in-process file mutation.
|
||||
- Preserved curated manifest category, status, rights, source, notes, and version metadata.
|
||||
- Added strict state, configuration, and manifest parsing.
|
||||
- Replaced textual ZIP probing with central-directory parsing, member decompression, size checks, and CRC-32 verification.
|
||||
- Expanded destructive-command protection for Unix shells, Git, PowerShell, and `cmd.exe` patterns targeting the canonical root.
|
||||
|
||||
### Terminal experience
|
||||
|
||||
- Added cinematic, rich, compact, and minimal visual densities.
|
||||
- Added off, subtle, and full motion modes tied to real tool activity.
|
||||
- Added persistent production HUD, status segments, semantic working messages, command-center overlay, tool renderers, message cards, and optional thinking-lane annotation.
|
||||
- Added POM Nocturne and POM Parchment themes plus `/pom theme install`.
|
||||
- Added project-persisted HUD placement and thinking-lane settings.
|
||||
|
||||
### Verification
|
||||
|
||||
- Replaced regex theatre with tests importing production modules.
|
||||
- Added state, policy, path, validator, Hive, Swarm, parser, ZIP, and malformed-archive tests.
|
||||
- Added a temporary-project runtime smoke test.
|
||||
- Added structural package/resource/theme verification and npm pack dry-run.
|
||||
|
||||
## 1.0.0 — 2026-07-11
|
||||
|
||||
Initial prototype. Superseded by the 2.0.0 production-kernel rebuild.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jammin Rebel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
120
POM.yml
Normal file
120
POM.yml
Normal file
@@ -0,0 +1,120 @@
|
||||
name: POM
|
||||
expanded_name: Produce Magnum Opus
|
||||
version: 2.0.0
|
||||
mission: Turn an idea into an original, validated, provenance-rich publication package on top of Oh My Pi.
|
||||
|
||||
principle:
|
||||
host: OMP performs models, tools, task agents, IRC, todo, sessions, approvals, compaction, memory, and TUI.
|
||||
kernel: POM governs stages, evidence, blockers, prompts, artifacts, validation, Git history, knowledge, and delivery.
|
||||
completion: A claim is earned only by deterministic checks and file-backed evidence.
|
||||
|
||||
entrypoints:
|
||||
extension: src/index.ts
|
||||
command: /pom
|
||||
shortcut: Alt+P
|
||||
themes: [pom-nocturne, pom-parchment]
|
||||
|
||||
stages:
|
||||
- {id: 0, slug: init, name: Project Initialization, profile: creative, model: pi/slow}
|
||||
- {id: 1, slug: genre, name: Genre Architecture, profile: research, model: pi/slow}
|
||||
- {id: 2, slug: voice, name: Author Persona and Voice, profile: creative, model: pi/slow}
|
||||
- {id: 3, slug: store, name: Story Store, profile: creative, model: pi/slow}
|
||||
- {id: 4, slug: bible, name: Story Blueprint and Bible, profile: creative, model: pi/slow}
|
||||
- {id: 5, slug: outline, name: Master Story Outline, profile: creative, model: pi/slow}
|
||||
- {id: 6, slug: chapters, name: Chapter Architecture, profile: creative, model: pi/slow}
|
||||
- {id: 7, slug: draft, name: Drafting and Revision, profile: draft, model: pi/default}
|
||||
- {id: 8, slug: delivery, name: Final Compilation and Delivery, profile: delivery, model: pi/slow}
|
||||
|
||||
stage_gate:
|
||||
requires:
|
||||
- stage is actively running
|
||||
- all earlier stages are complete
|
||||
- every acceptance criterion has at least one project-relative evidence path
|
||||
- required artifacts satisfy bytes, headings, patterns, and structure
|
||||
- no blocker remains unresolved
|
||||
- deterministic validation produced a report path
|
||||
result: PASS or BLOCKED
|
||||
|
||||
commands:
|
||||
dashboard: /pom
|
||||
new: /pom new <title>
|
||||
resume: /pom resume [state-path]
|
||||
run: /pom run [next|all|0..8]
|
||||
hive: /pom hive [lean|stage|audit]
|
||||
swarm: /pom swarm [parallel|sequential|pipeline] [count]
|
||||
evidence: /pom evidence
|
||||
check: /pom check [quick|stage|canon|continuity|knowledge|files|delivery|all]
|
||||
vault: /pom vault
|
||||
export: /pom export [checkpoint|final]
|
||||
prompt: /pom prompt [list|show|run] [id]
|
||||
tools: /pom tools [show|reset|profile <name>]
|
||||
theme: /pom theme install
|
||||
status: /pom status [--json]
|
||||
help: /pom help
|
||||
settings: /pom settings
|
||||
doctor: /pom doctor
|
||||
|
||||
pom_tools:
|
||||
pom_state: canonical state and decisions
|
||||
pom_stage: authoritative transitions, evidence, blockers, and pass
|
||||
pom_artifact: secure measurement, hash, metadata, and semantic version
|
||||
pom_validate: deterministic scoped assurance
|
||||
pom_prompt: prompt inventory, overrides, and provenance
|
||||
pom_hive: native task plan, IRC signal ledger, and synthesis
|
||||
pom_export: fresh archive plus full member verification
|
||||
|
||||
delegation:
|
||||
rapid_hive:
|
||||
executor: native OMP task
|
||||
coordination: native OMP IRC
|
||||
signals: [CLAIM, QUERY, ANSWER, ALERT, HANDOFF, DONE]
|
||||
rule: one bounded batch per independent wave; durable findings go to files or ledgers
|
||||
unattended:
|
||||
executor: optional OMP Swarm extension
|
||||
modes: [parallel, sequential, pipeline]
|
||||
topology: specialist fan-out followed by integrator fan-in
|
||||
|
||||
terminal:
|
||||
status: stage, progress, evidence/blockers, Hive, approval, retry
|
||||
widget:
|
||||
cinematic: three-line HUD with stage track and coordination cue
|
||||
rich: two-line evidence HUD
|
||||
compact: one-line HUD
|
||||
minimal: disabled; status plus critical cards
|
||||
motion: [off, subtle, full]
|
||||
cards: [event, validation, hive, milestone]
|
||||
overlay: [run, hive, validate, evidence, vault, export, theme, settings]
|
||||
law: Reward verified progress, not activity.
|
||||
|
||||
knowledge:
|
||||
canonical_root: project filesystem
|
||||
session_state: OMP append-only custom entries
|
||||
vault: Obsidian-flavoured Markdown compatible with Quartz 5
|
||||
prompt_precedence:
|
||||
- immutable POM production law (prepended by composePromptStack)
|
||||
- selected personality fragment (personality-${mode})
|
||||
- requested bundled fragments in order
|
||||
- project-local fragments replacing bundled by ID (src/prompts.ts:43-49)
|
||||
- executable stage contract appended by renderStagePrompt (src/prompts.ts:83-117)
|
||||
provenance: SHA-256 per active prompt fragment
|
||||
|
||||
integrity:
|
||||
paths: canonical realpath containment with symlink escape rejection
|
||||
writes: unique atomic temp file then rename; serialized in-process mutation
|
||||
manifest: preserve category, stage, version, status, bytes, SHA-256, rights, source, and notes
|
||||
zip: central directory, safe names, methods, duplicate detection, decompression, sizes, CRC-32, archive SHA-256
|
||||
command_policy: block destructive recursive operations against canonical project root by default
|
||||
|
||||
resources:
|
||||
agents: 14
|
||||
prompts: 13
|
||||
rules: 3
|
||||
themes: 2
|
||||
tests: 9
|
||||
|
||||
release:
|
||||
required:
|
||||
- npm run verify
|
||||
- npm run doctor in a Bun + OMP host
|
||||
- interactive command-center and stage smoke test
|
||||
known_boundary: Build environment may verify package code without providing the live OMP executable.
|
||||
222
README.md
Normal file
222
README.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# POM 2 — Produce Magnum Opus
|
||||
|
||||
POM is an Oh My Pi extension that turns OMP's native models, tools, task agents, IRC, todo system, sessions, approvals, compaction, and TUI into an evidence-gated production operating system.
|
||||
|
||||
POM does **not** replace OMP's execution primitives. It governs them with stage contracts, canonical artifacts, prompt provenance, deterministic validation, Git checkpoints, and an Obsidian/Quartz-compatible knowledge base.
|
||||
|
||||
## What ships
|
||||
|
||||
- 9 authoritative production stages (`0` through `8`)
|
||||
- 7 typed `pom_*` tools
|
||||
- 14 specialist task agents
|
||||
- 13 prepared prompt fragments
|
||||
- 3 scoped production rules
|
||||
- 2 installable OMP themes
|
||||
- Native task + IRC Hive planning
|
||||
- Optional OMP Swarm DAG generation
|
||||
- Evidence-backed gates and blocker lifecycle
|
||||
- Symlink-safe artifact registration with SHA-256 and semantic versions
|
||||
- Streaming ZIP creation plus central-directory, decompression, size, and CRC-32 verification
|
||||
- Obsidian-flavoured Markdown vault ready for Quartz 5
|
||||
|
||||
## Install and verify
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| Requirement | Version | Why |
|
||||
|---|---|---|
|
||||
| Node.js | 22 or newer | Runs the local scripts (`verify`, `smoke`, `release:check`) and the `node:test` suite |
|
||||
| npm | bundled with Node 22 | Installs dependencies and links the plugin into the OMP host |
|
||||
| Bun | `>=1.3.14` (`engines.bun`) | Runs the OMP host that loads this extension |
|
||||
| OMP | installed and on `PATH` | Provides `omp plugin link` and `omp plugin doctor` |
|
||||
| `@oh-my-pi/pi-coding-agent` | `>=16.4.6 <17` | Peer dependency: extension host API |
|
||||
| `@oh-my-pi/pi-tui` | `>=16.4.6 <17` | Peer dependency: TUI components |
|
||||
| `@oh-my-pi/pi-utils` | `>=16.4.6 <17` | Peer dependency: host paths and shared utilities |
|
||||
|
||||
POM has no build step. OMP loads the TypeScript under `src/` directly, so the installed tree is the shipped tree.
|
||||
|
||||
### Source checkout install
|
||||
|
||||
Step 1 — obtain the repository source. Canonical project coordinates are recorded in `package.json` and point at the maintainer's own infrastructure rather than a third-party forge:
|
||||
|
||||
| Field | Value | State |
|
||||
|---|---|---|
|
||||
| `homepage` | `https://wiki.omp.loca.zone/` | Live |
|
||||
| `bugs.url` | `https://wiki.omp.loca.zone/` | Live |
|
||||
| `bugs.email` | `antigravity@loca.zone` | Live |
|
||||
| `repository.url` | `git+https://omp.loca.zone/pom-omp.git` | Reserved — not yet serving Git |
|
||||
|
||||
There is deliberately no public forge mirror: this package has never been published to npm, and no GitHub or self-hosted Git endpoint exists for it today. Until `repository.url` is actually served, obtain the checkout out of band from the maintainer. Do not substitute a third-party URL that has not been confirmed.
|
||||
|
||||
Step 2 — install, verify, and link from the checkout root:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run verify
|
||||
npm run link
|
||||
```
|
||||
|
||||
`npm run verify` does not currently complete in this environment. See [Verification status](#verification-status) before treating a failure as a code defect.
|
||||
|
||||
Step 3 — restart OMP or run `/reload-plugins`, then install the bundled themes:
|
||||
|
||||
```text
|
||||
/pom theme install
|
||||
```
|
||||
|
||||
POM inserts `/theme pom-nocturne` into the editor. Press Enter to activate it. `pom-parchment` is the matching light theme.
|
||||
|
||||
Step 4 — run the host-level check in an environment with Bun and OMP installed:
|
||||
|
||||
```bash
|
||||
npm run doctor
|
||||
```
|
||||
|
||||
### Published package install
|
||||
|
||||
The published-package flow installs `@jamminrebel/pom-omp` into the OMP host, which then discovers the extension through the `omp.extensions` entry (`./src/index.ts`). No `npm install` line is printed here because no published tarball from this repository has been produced or verified against the precondition below.
|
||||
|
||||
Precondition: the tarball must contain `prompts/`. `src/prompts.ts` resolves bundled prompt fragments from `../prompts` at runtime, and `prompts` was only added to the `files` array in `package.json` as of this change. Any published tarball predating this change ships without `prompts/`, so prompt composition fails at runtime; such a tarball must not be relied on.
|
||||
|
||||
### Verification status
|
||||
|
||||
Overall readiness: PARTIAL.
|
||||
|
||||
Currently passing:
|
||||
|
||||
| Check | Command |
|
||||
|---|---|
|
||||
| Extension smoke test | `npm run smoke` |
|
||||
| Release manifest parity | `npm run release:check` |
|
||||
| State unit tests | `node --import tsx --test tests/state.test.ts` |
|
||||
|
||||
The full gate is BLOCKED, not passing. `npm run verify` cannot complete because an npm registry outage (repeated `ETIMEDOUT` on install) left `node_modules` incomplete. Missing at minimum:
|
||||
|
||||
| Missing | Blocks |
|
||||
|---|---|
|
||||
| `@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-utils` | `npm run check`, every test importing `src/` modules that reference the OMP SDK |
|
||||
| `@types/node` | `npm run check` (`tsc --noEmit`) |
|
||||
| `yauzl` | `tests/zip.test.ts` and the archive verification path in `src/zip.ts` |
|
||||
| `node_modules/@oh-my-pi/pi-coding-agent/src/modes/theme/theme-schema.json` | `node scripts/verify.mjs` theme validation |
|
||||
|
||||
No claim is made that `npm run verify` has passed in this environment.
|
||||
|
||||
### Registry outage and offline guidance
|
||||
|
||||
Rules while the registry is unreachable:
|
||||
|
||||
- Do not vendor dependencies into the repository.
|
||||
- Do not stub, hand-write, or copy the OMP SDK packages or the OMP theme schema to turn a gate green. A gate satisfied by a stub proves nothing about the shipped extension.
|
||||
- Repair or retry registry access, rerun `npm install`, then rerun the full `npm run verify` gate and record its real result.
|
||||
|
||||
What each check needs:
|
||||
|
||||
| Check | Offline without installed dependencies |
|
||||
|---|---|
|
||||
| `npm run smoke` | Meaningful — runs through `tsx` against `src/` without resolving the OMP SDK at runtime |
|
||||
| `npm run release:check` | Meaningful — `scripts/release-manifest.mjs` uses Node builtins only |
|
||||
| `node --import tsx --test tests/state.test.ts` | Meaningful — exercises state logic only |
|
||||
| `npm run check` | Requires installed dependencies (OMP SDK type declarations plus `@types/node`) |
|
||||
| `npm test` (full suite) | Requires installed dependencies (`yauzl`, `yazl`, OMP SDK) |
|
||||
| `node scripts/verify.mjs` | Requires installed dependencies (reads the OMP theme schema from `node_modules`) |
|
||||
| `npm run pack:dry` | Requires a resolvable npm environment |
|
||||
| `npm run doctor` | Requires Bun plus an installed OMP host |
|
||||
|
||||
## Production loop
|
||||
|
||||
```text
|
||||
/pom new "My Project"
|
||||
/pom run next
|
||||
/pom hive stage
|
||||
/pom evidence
|
||||
/pom check stage
|
||||
# The model calls pom_stage action=pass only after the report passes.
|
||||
/pom export final
|
||||
```
|
||||
|
||||
`/pom` opens the terminal command center. `Alt+P` is the keyboard shortcut.
|
||||
|
||||
## Command map
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `/pom` | Open the command-center overlay |
|
||||
| `/pom new <title>` | Create project, vault, ledgers, state, and Git root |
|
||||
| `/pom resume [state-path]` | Restore a canonical project state |
|
||||
| `/pom run [next\|all\|0..8]` | Start authoritative production |
|
||||
| `/pom hive [lean\|stage\|audit]` | Queue a native task + IRC specialist wave |
|
||||
| `/pom swarm [parallel\|sequential\|pipeline] [count]` | Generate an optional OMP Swarm DAG |
|
||||
| `/pom evidence` | Record acceptance evidence interactively |
|
||||
| `/pom check [scope]` | Run deterministic validation |
|
||||
| `/pom vault` | Insert the production-board mention |
|
||||
| `/pom export [checkpoint\|final]` | Build and verify an archive |
|
||||
| `/pom prompt [list\|show\|run] [id]` | Inspect or run prompt fragments |
|
||||
| `/pom tools [show\|reset\|profile <name>]` | Inspect or change active-tool routing |
|
||||
| `/pom theme install` | Install POM Nocturne and Parchment |
|
||||
| `/pom status [--json]` | Inspect canonical project state |
|
||||
| `/pom settings` | Configure density, motion, thinking lane, and HUD placement |
|
||||
| `/pom doctor` | Run POM's project diagnostics |
|
||||
| `/pom help` | Show built-in command reference |
|
||||
|
||||
Validation scopes: `quick`, `stage`, `canon`, `continuity`, `knowledge`, `files`, `delivery`, and `all`.
|
||||
|
||||
## Project knowledge base
|
||||
|
||||
Every project contains a publishable knowledge layer:
|
||||
|
||||
```text
|
||||
content/
|
||||
├── index.md
|
||||
├── production-board.md
|
||||
├── artifact-index.md
|
||||
├── 01-planning.md
|
||||
├── 02-story-bible.md
|
||||
├── 03-manuscript.md
|
||||
├── 04-visuals.md
|
||||
├── 05-research.md
|
||||
├── 06-ledgers.md
|
||||
└── 07-delivery.md
|
||||
```
|
||||
|
||||
The generated notes use YAML frontmatter, wikilinks, callouts, maps of content, and canonical file links. Project truth remains in the project tree; the vault is its navigable knowledge projection.
|
||||
|
||||
## Visual experience
|
||||
|
||||
POM uses OMP hooks rather than raw ANSI logging:
|
||||
|
||||
- Persistent production HUD via `setWidget()`
|
||||
- Compact stage/gate/Hive segments via `setStatus()`
|
||||
- Semantic active-phase text via `setWorkingMessage()`
|
||||
- Expandable validation, Hive, and milestone message cards
|
||||
- Tool call/result renderers with partial-state feedback
|
||||
- Keyboard-controlled custom overlay
|
||||
- Optional visible-thinking lane annotation
|
||||
- Width-safe rendering and cached unchanged HUD frames
|
||||
|
||||
The interface rewards verified evidence, resolved blockers, clean handoffs, stage completion, and archive verification—not meaningless activity.
|
||||
|
||||
## Configuration
|
||||
|
||||
OMP host settings belong in `<project>/.omp/config.yml`. POM-specific settings belong in `<project>/.omp/pom.json`; global POM defaults may live at `~/.omp/agent/pom.json`.
|
||||
|
||||
Start at the [documentation index](docs/index.md); every POM document is reachable from there.
|
||||
|
||||
Direct links: [Settings](docs/SETTINGS.md), [Architecture](docs/ARCHITECTURE.md), [Tool Matrix](docs/TOOL_MATRIX.md), [Ownership](docs/OWNERSHIP.md), [Boundary](docs/BOUNDARY.md), and [Vault](docs/VAULT.md).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
npm test
|
||||
npm run smoke
|
||||
npm run verify
|
||||
npm pack
|
||||
```
|
||||
|
||||
The local test suite imports production code. The final host smoke test remains `omp plugin doctor` plus an interactive OMP session.
|
||||
|
||||
`npm run check`, `npm test`, and `npm run verify` require a complete `node_modules`; see [Verification status](#verification-status) for what currently passes and what is blocked.
|
||||
|
||||
## Governing law
|
||||
|
||||
> OMP performs the work. POM governs the production. The vault preserves the truth. Git preserves its history. Validation earns every completion claim.
|
||||
561
RELEASE.json
Normal file
561
RELEASE.json
Normal file
@@ -0,0 +1,561 @@
|
||||
{
|
||||
"name": "POM — Produce Magnum Opus",
|
||||
"package": "@jamminrebel/pom-omp",
|
||||
"version": "2.0.0",
|
||||
"schemaVersion": "2.0.0",
|
||||
"releaseDate": "2026-08-19",
|
||||
"status": "release-candidate",
|
||||
"summary": "OMP-native production kernel with evidence-gated stages, native task/IRC delegation, optional Swarm DAGs, Obsidian/Quartz knowledge projection, cinematic TUI hooks, and verified archive delivery.",
|
||||
"compatibility": {
|
||||
"bun": ">=1.3.14",
|
||||
"ompPackages": ">=16.4.6 <17",
|
||||
"module": "ESM"
|
||||
},
|
||||
"inventory": {
|
||||
"stages": 9,
|
||||
"customTools": 7,
|
||||
"agents": 14,
|
||||
"prompts": 13,
|
||||
"rules": 3,
|
||||
"themes": 2,
|
||||
"behavioralTests": 9,
|
||||
"sourceFilesHashed": 97,
|
||||
"sourceBytesHashed": 312041,
|
||||
"selfHashPolicy": "RELEASE.json is excluded from its own source inventory; final package and archive hashes are recorded externally."
|
||||
},
|
||||
"verification": {
|
||||
"typescript": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/typecheck.json"
|
||||
},
|
||||
"behavioralTests": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/tests.json"
|
||||
},
|
||||
"runtimeSmoke": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/smoke.json"
|
||||
},
|
||||
"structuralVerification": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/structural.json"
|
||||
},
|
||||
"themeSchemaValidation": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/theme.json"
|
||||
},
|
||||
"npmPackDryRun": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Missing evidence file: 00_admin/validation/pack.json"
|
||||
},
|
||||
"productionDependencyAudit": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Requires network access to npm registry"
|
||||
},
|
||||
"fullDevelopmentTreeAudit": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Requires network access to npm registry"
|
||||
},
|
||||
"ompPluginDoctor": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Requires Bun + OMP installation"
|
||||
},
|
||||
"interactiveTuiSmoke": {
|
||||
"status": "NOT RUN",
|
||||
"reason": "Requires a live OMP terminal host"
|
||||
}
|
||||
},
|
||||
"releaseGates": [
|
||||
"Run npm run verify",
|
||||
"Run npm run doctor in a Bun + OMP installation",
|
||||
"Open /pom and verify the overlay, HUD, status, cards, and /pom theme install",
|
||||
"Create a smoke project, validate stage 0, and verify a checkpoint archive"
|
||||
],
|
||||
"sourceInventory": [
|
||||
{
|
||||
"path": "AGENTS.md",
|
||||
"bytes": 21917,
|
||||
"sha256": "89162d702abdebfe09ed863464744b37bb0a942c4ab01595095647944f92c0a6"
|
||||
},
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"bytes": 2442,
|
||||
"sha256": "7afb1ba21ac012d809d025aeea0ecddcf487e8b7312fdb5fe58d854708f32b46"
|
||||
},
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"bytes": 1069,
|
||||
"sha256": "39a69c6918ff01943e342f650bfa8904a866d81b361f1a4c864f29f0c8269b30"
|
||||
},
|
||||
{
|
||||
"path": "POM.yml",
|
||||
"bytes": 5028,
|
||||
"sha256": "10974c0f540a1fc90c4348d9aace0a6f33593a3fe658f31cb7f3cb7d29c50d99"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"bytes": 10075,
|
||||
"sha256": "6ccdd0b9eba8955c088edc205a9053a18bb263609ae7e308e5e03b6486c87bff"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-architect.md",
|
||||
"bytes": 621,
|
||||
"sha256": "fccdb7d08fea2653fc5be82fcab1c5cc9d7d1b520de1287bd206cbfc7d170ef8"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-character-world.md",
|
||||
"bytes": 648,
|
||||
"sha256": "dbf3c30531498eadcb5570d2a7a36d5061c6c9ae1ca06d595a9cb7ade632d572"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-concept-forge.md",
|
||||
"bytes": 644,
|
||||
"sha256": "d2a854d507566a1b69ce6cf7e350dba930768ac5bd742b65fe9161f51c1af5bc"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-continuity-auditor.md",
|
||||
"bytes": 630,
|
||||
"sha256": "22e796efc5b896d83fff9ea132b419de76d830f2a6a47e9d13b413597697f8ba"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-critic.md",
|
||||
"bytes": 573,
|
||||
"sha256": "ba0194bddce341a343d70a7a883ff1d3065f20d810dafe0c6cf5cd8e23755a1b"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-delivery-auditor.md",
|
||||
"bytes": 657,
|
||||
"sha256": "7ff624ecf89ac8079e3e625031d1c8171b55c213dcff3c731c5463895b95d2e6"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-developmental-editor.md",
|
||||
"bytes": 665,
|
||||
"sha256": "87b718dab9679d59bbf4892f934a2dce68f60f768549b283b07e3eec92a8eb14"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-prose-smith.md",
|
||||
"bytes": 652,
|
||||
"sha256": "333b234c6a2a8280224d88f13eb89612f2722e576cfef1807b633548e8ce5db5"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-researcher.md",
|
||||
"bytes": 684,
|
||||
"sha256": "4b3722983ad19488664b8b30ca6f8bb54de5bb72a5c3a377bfec576f322c2bd1"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-rights-auditor.md",
|
||||
"bytes": 636,
|
||||
"sha256": "567e05c10cd78f810b42dfd104c3831bbbc94a42f5b057d61c709914735eb337"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-story-architect.md",
|
||||
"bytes": 612,
|
||||
"sha256": "f89abdc19fff4626d22b39a5b2747259069e126aaff7d092e41eb446967b6cba"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-structure-engineer.md",
|
||||
"bytes": 649,
|
||||
"sha256": "18db6431a883a94413c7ee3b85bc50674e4b6a5332ced0a4fccb005ff439e2d1"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-visual-director.md",
|
||||
"bytes": 650,
|
||||
"sha256": "614d2fc678083fe8f8fa6792c01d9cce07cbb11be41cfa55b591f557c73407b4"
|
||||
},
|
||||
{
|
||||
"path": "agents/pom-voice-architect.md",
|
||||
"bytes": 672,
|
||||
"sha256": "2edfa4c26e31144e7fb831eb29cfd601874ade0041ae7bdb70092e6952024abd"
|
||||
},
|
||||
{
|
||||
"path": "config/APPEND_SYSTEM.md",
|
||||
"bytes": 337,
|
||||
"sha256": "e70230000dc61518052d498ce2f052d9512a0a5e89461276c80482b3f349268a"
|
||||
},
|
||||
{
|
||||
"path": "config/config.yml",
|
||||
"bytes": 797,
|
||||
"sha256": "e9dd87804b0318bcb22a8ecfa8096ea533e6bdac4f059e0d43b662f9454b4efd"
|
||||
},
|
||||
{
|
||||
"path": "config/mcp.json.example",
|
||||
"bytes": 403,
|
||||
"sha256": "ab763cbe2447bef4ee0575cd14f6ac4b3e1bdc74d255765304a4d8c95aea8a82"
|
||||
},
|
||||
{
|
||||
"path": "config/pom.json",
|
||||
"bytes": 417,
|
||||
"sha256": "35fd74930ec0ee6e107f05362321701b47b125838e8d1227cd7aa1a96f6317df"
|
||||
},
|
||||
{
|
||||
"path": "docs/ARCHITECTURE.md",
|
||||
"bytes": 2620,
|
||||
"sha256": "981f1c404d72c43fcf25bec7f25725e427193a29e13f5c988d895051a4bc1834"
|
||||
},
|
||||
{
|
||||
"path": "docs/AUDIT.md",
|
||||
"bytes": 1644,
|
||||
"sha256": "05860e6c1d7365fdc1a9b3bb30c10ea5559dc9e5f275d7b333a061b8c6394b0c"
|
||||
},
|
||||
{
|
||||
"path": "docs/BOUNDARY.md",
|
||||
"bytes": 3057,
|
||||
"sha256": "1335c2fed3a41bf112cdc48f0bd737faa4bbef41e7f4cb4a80e9b3a4710a92e8"
|
||||
},
|
||||
{
|
||||
"path": "docs/DESIGN_REVIEW.md",
|
||||
"bytes": 2185,
|
||||
"sha256": "5aeb0d3ced79ecec3c774c0036ea6b13ba1999bf77746f2bcb60ae0b90097102"
|
||||
},
|
||||
{
|
||||
"path": "docs/INVENTORY.md",
|
||||
"bytes": 1450,
|
||||
"sha256": "ef887af919963e48b71ce48dcf6760aaca2b97c853b732365a3cba4dc24f1194"
|
||||
},
|
||||
{
|
||||
"path": "docs/OFFICIAL_SOURCES.md",
|
||||
"bytes": 900,
|
||||
"sha256": "f70bca7fca9e961024d5ac86e7cb4210ffb01482088a181a737b97f3bb9ae42b"
|
||||
},
|
||||
{
|
||||
"path": "docs/OWNERSHIP.md",
|
||||
"bytes": 2709,
|
||||
"sha256": "146660daccdace4f7f8fdcc43467638332fab3dbc7c70c19e7428f287b5708ff"
|
||||
},
|
||||
{
|
||||
"path": "docs/POM_ARCHITECTURE.mmd",
|
||||
"bytes": 875,
|
||||
"sha256": "5f8cc434003332c79c94b4ca415be01f2c32ab7cc5bae044bdfa19d59786bc36"
|
||||
},
|
||||
{
|
||||
"path": "docs/SETTINGS.md",
|
||||
"bytes": 3284,
|
||||
"sha256": "a0fc17b1d81811aa7810e53c77e619c51aaea62737273903b9af70d0484befb4"
|
||||
},
|
||||
{
|
||||
"path": "docs/TOOL_MATRIX.md",
|
||||
"bytes": 1888,
|
||||
"sha256": "8545ec9fbbe9b5dbd7909b26239c5c738a77dfc7b302a71d5200ed9762e51eb4"
|
||||
},
|
||||
{
|
||||
"path": "docs/VAULT.md",
|
||||
"bytes": 4129,
|
||||
"sha256": "da72178d92b7c02bd592b7a3202d2cdb43317884a2d105ea9436ca65699bb0bc"
|
||||
},
|
||||
{
|
||||
"path": "docs/VERIFICATION.md",
|
||||
"bytes": 10255,
|
||||
"sha256": "0aab7e070ea90dbca30a0ed86a9afdedd2a26707ff359e09cc5f360b2e8a47a9"
|
||||
},
|
||||
{
|
||||
"path": "docs/index.md",
|
||||
"bytes": 4585,
|
||||
"sha256": "984f319f870b9d23f76306bb74a3d662dd490dc9bce8cd068322d75dfc81d53c"
|
||||
},
|
||||
{
|
||||
"path": "docs/navigation.md",
|
||||
"bytes": 1715,
|
||||
"sha256": "834b1c9d96b9dee5ebdb54ba4b5f3499417289d10c00cf4b053383aa5e0ff496"
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"bytes": 2153,
|
||||
"sha256": "7e325ea1558fbdb5bb76565f942306ce6568152a5a604ef80fbdb252cadbe92e"
|
||||
},
|
||||
{
|
||||
"path": "prompts/agent-studio.md",
|
||||
"bytes": 404,
|
||||
"sha256": "2f6726dcfbc3d00993b0a6864382059b1a670e02200cbbb78b2fbee5c52c7fd8"
|
||||
},
|
||||
{
|
||||
"path": "prompts/chapter-cycle.md",
|
||||
"bytes": 638,
|
||||
"sha256": "af09947e31db5827ce2abd483f18acc5a784095e1fa7f1179ae30d745f2961a5"
|
||||
},
|
||||
{
|
||||
"path": "prompts/continuity-audit.md",
|
||||
"bytes": 390,
|
||||
"sha256": "d2765f58098820018c0607aff5853a5d0f5132f891bf6401837490bc54452f15"
|
||||
},
|
||||
{
|
||||
"path": "prompts/delivery-audit.md",
|
||||
"bytes": 572,
|
||||
"sha256": "1255a6e58bdc77e7a5fc62f9ab67ee034fb9362c62aef5ca690e29e0d0eb5245"
|
||||
},
|
||||
{
|
||||
"path": "prompts/full-autopilot.md",
|
||||
"bytes": 354,
|
||||
"sha256": "b16def2acb2598d4c8b0a232cd95be3d8b3aaa5f7b1c38dd5668645552ff9402"
|
||||
},
|
||||
{
|
||||
"path": "prompts/personality-autopilot.md",
|
||||
"bytes": 464,
|
||||
"sha256": "e59c224e86caf8a87520ee84df4a8500cf44c9f7a916c6afd980cb34bccd1ab0"
|
||||
},
|
||||
{
|
||||
"path": "prompts/personality-collaborative.md",
|
||||
"bytes": 441,
|
||||
"sha256": "b85439b416b8afd37d82239eaf28c3d8f3e6c9745d61290358dba6a62e590a75"
|
||||
},
|
||||
{
|
||||
"path": "prompts/personality-strict-autopilot.md",
|
||||
"bytes": 470,
|
||||
"sha256": "6f1b070cb8b4822422fc879dcf29b1e0c5db831a0fe36c9d9ded1b0e1091d5f1"
|
||||
},
|
||||
{
|
||||
"path": "prompts/repair.md",
|
||||
"bytes": 355,
|
||||
"sha256": "5e523409c14c95ea30adf7db5a9fdd06ac4a2100553d7c9b5b9ada2b9d707548"
|
||||
},
|
||||
{
|
||||
"path": "prompts/research-sprint.md",
|
||||
"bytes": 530,
|
||||
"sha256": "e47e34fc745fc81d4906fbbf0c159bcc5fae2813db2e4694b8375dc334eddb13"
|
||||
},
|
||||
{
|
||||
"path": "prompts/stage-run.md",
|
||||
"bytes": 1776,
|
||||
"sha256": "6fc157c9b82256b9216544a0ac4b40f75ba1bcd403b6e9102dc29b076f87eb52"
|
||||
},
|
||||
{
|
||||
"path": "prompts/todo-sync.md",
|
||||
"bytes": 240,
|
||||
"sha256": "80fc8fe2c72bb29ac7a99165acd5d420e84b8a4492f9836026e82a724e096666"
|
||||
},
|
||||
{
|
||||
"path": "prompts/visual-cycle.md",
|
||||
"bytes": 614,
|
||||
"sha256": "d3c83283bef20cea4c84590a8a777e48ab99a4f905931d0a39e8866b88a74005"
|
||||
},
|
||||
{
|
||||
"path": "rules/pom-artifact-integrity.md",
|
||||
"bytes": 372,
|
||||
"sha256": "c8c230c82c6c59507c55d00a3af386d97c3a35913dc1c676eb5bdc782a6cd3f2"
|
||||
},
|
||||
{
|
||||
"path": "rules/pom-continuity.md",
|
||||
"bytes": 384,
|
||||
"sha256": "92fc2c4156e419fde175c7ddc31acde82928864442ae897bed0ec61fdbd735ce"
|
||||
},
|
||||
{
|
||||
"path": "rules/pom-rights.md",
|
||||
"bytes": 438,
|
||||
"sha256": "e8cf351e3ce35a334c6742b5ab69bc104d3917d4e80b5570911521580d75ec2f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/check-docs.mjs",
|
||||
"bytes": 17112,
|
||||
"sha256": "570eb129e9432d3b6aa8f5112b743c7b2bfc20c75c50fb6e30bf0121130b8286"
|
||||
},
|
||||
{
|
||||
"path": "scripts/install.ps1",
|
||||
"bytes": 167,
|
||||
"sha256": "2b07d3abbf4474f705a148a5d208002d4db578ef9a7d691c67419c515766869e"
|
||||
},
|
||||
{
|
||||
"path": "scripts/install.sh",
|
||||
"bytes": 164,
|
||||
"sha256": "c43f03c90e10e154cbb0fa4f45f9923567dcb7e1e25f7d20ddc5284dee4c77be"
|
||||
},
|
||||
{
|
||||
"path": "scripts/release-manifest.mjs",
|
||||
"bytes": 4714,
|
||||
"sha256": "13dac5093fae178d34484f3bd374f34dbf450e84ea994bb5a0258dfcce5ac541"
|
||||
},
|
||||
{
|
||||
"path": "scripts/smoke.ts",
|
||||
"bytes": 1248,
|
||||
"sha256": "46381942e6205c601b60e3240835d92b5da3a7f1ab4cb3e0e5fc748987d3200f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/verify.mjs",
|
||||
"bytes": 4150,
|
||||
"sha256": "f1e6fc49e247f6d010fb1e460a26288b44ce490d1ae35527182af6573a64d0c9"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/SKILL.md",
|
||||
"bytes": 1781,
|
||||
"sha256": "055f99ac29f8da3b6ddd12b335c34829cbb31ad0a86129ee163379a7caa9ac8d"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/agent-studios.md",
|
||||
"bytes": 943,
|
||||
"sha256": "ed095ca91230c24fb5cd9397effb4f583b2adb1e5afb4661342d6a058cd668a1"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/artifacts.md",
|
||||
"bytes": 994,
|
||||
"sha256": "c2f83ba4ce459264aed0f92baa4baea16da0e1fec0fcaeb386e9a358cf53fe85"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/commands.md",
|
||||
"bytes": 940,
|
||||
"sha256": "7cd3a4fcfbc5f9cc024506802cb1e70d3dbeb28e9c0b361fe1423f73f0b3ddc7"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/core-law.jml",
|
||||
"bytes": 2318,
|
||||
"sha256": "dcd9fe8fdab871c178dcd58dbad7b00ec2fdf7e1538d8e696fdda329d73d848b"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/qa.md",
|
||||
"bytes": 758,
|
||||
"sha256": "1973303d9e09b635138e3fdeff84366dc321766a197fdcf53a4e924f0fb7a2d0"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/todo-blueprints.md",
|
||||
"bytes": 2579,
|
||||
"sha256": "afa0c9bb46fbe3bae01d40e8e374cc660eac1ae21a35e54712a29d1dd0f2f8ac"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/tool-routing.md",
|
||||
"bytes": 2181,
|
||||
"sha256": "32c93ff2afee967f7ec2702ba325c323898481e72b4c3301bedb23ffd9f9d5df"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/ui.md",
|
||||
"bytes": 1124,
|
||||
"sha256": "c1698f6db7a3af4ef1fc0d3e7ec252a09c172c24e77628ca145163aabb8c5502"
|
||||
},
|
||||
{
|
||||
"path": "skills/pom/references/workflow.md",
|
||||
"bytes": 2308,
|
||||
"sha256": "0cb5ed98c80e97c306adf542262c2bac8f48d08b08c83eb43ca35df6b2620e1b"
|
||||
},
|
||||
{
|
||||
"path": "src/command.ts",
|
||||
"bytes": 16580,
|
||||
"sha256": "2afa5e68465a851095bd838196bcc3088779e798a85d9f56170861bd19f794ee"
|
||||
},
|
||||
{
|
||||
"path": "src/config.ts",
|
||||
"bytes": 4264,
|
||||
"sha256": "f30e83bfd4494d717b466695398885e2e86ceb08c2b4aeab7991351361895ee7"
|
||||
},
|
||||
{
|
||||
"path": "src/domain.ts",
|
||||
"bytes": 18579,
|
||||
"sha256": "5f9994c805cc276fa86ff50601c0139dbdc03cbb4923b56ed37d1af28fe3c0f1"
|
||||
},
|
||||
{
|
||||
"path": "src/events.ts",
|
||||
"bytes": 10277,
|
||||
"sha256": "aab5462e3f302f40eb11347f697fd771ad6bef2da72c9accb7512fab60dcde79"
|
||||
},
|
||||
{
|
||||
"path": "src/exporter.ts",
|
||||
"bytes": 4915,
|
||||
"sha256": "4f1f72602681b06c32389d8e7a029e49d98b5ebc228c1b6f79dd19e84cfc7ad3"
|
||||
},
|
||||
{
|
||||
"path": "src/git.ts",
|
||||
"bytes": 1952,
|
||||
"sha256": "de8db06a0c8b789b0e99c43121fee0083e93a6489ee0ae58bfa162d499249374"
|
||||
},
|
||||
{
|
||||
"path": "src/index.ts",
|
||||
"bytes": 2042,
|
||||
"sha256": "f5b12056ca55f6e5287d2ec33d2eab16d53084927188ea0fec7f9317e46f975a"
|
||||
},
|
||||
{
|
||||
"path": "src/parser.ts",
|
||||
"bytes": 1632,
|
||||
"sha256": "ed8c3edb142f41439b411e3681bd266ce590b5e45e0177e1303729de0e095ffb"
|
||||
},
|
||||
{
|
||||
"path": "src/paths.ts",
|
||||
"bytes": 2781,
|
||||
"sha256": "c3878a4fe20b8537be2e8ac82eaf4b3b20b538b1a9d9c456bdfea019eff6f762"
|
||||
},
|
||||
{
|
||||
"path": "src/persistence.ts",
|
||||
"bytes": 16806,
|
||||
"sha256": "cb818b383e64070cf3187033ea1d0926e102b64901d1480eb0ff10dad5487a9c"
|
||||
},
|
||||
{
|
||||
"path": "src/policy.ts",
|
||||
"bytes": 1785,
|
||||
"sha256": "5390b4165c42e4f7330dcb619c2b935c270e3ff32736f59d05f7b9f9c7b05a5d"
|
||||
},
|
||||
{
|
||||
"path": "src/prompts.ts",
|
||||
"bytes": 5323,
|
||||
"sha256": "977c119b4bd14c08e58ee2b0e5a2d9dfd6a82f6b93611d41d1c1b19000a23586"
|
||||
},
|
||||
{
|
||||
"path": "src/renderers.ts",
|
||||
"bytes": 4742,
|
||||
"sha256": "a8d6118d75a82bcf4598b1fa610ea93146507e2aaf2971658020438fa5a97419"
|
||||
},
|
||||
{
|
||||
"path": "src/router.ts",
|
||||
"bytes": 922,
|
||||
"sha256": "9e05c60580bdb3836500228d724043b9d72f46a1e76180621f8941a0026e515c"
|
||||
},
|
||||
{
|
||||
"path": "src/runtime.ts",
|
||||
"bytes": 620,
|
||||
"sha256": "8724d1eb95ef0759264c8ccc7e034b2cfce230374f02ab037f2e7e1042955013"
|
||||
},
|
||||
{
|
||||
"path": "src/state.ts",
|
||||
"bytes": 9338,
|
||||
"sha256": "7706f261cfd0e5a4bad95cf2418460463aaedcc73bf77f9d025e1fb8d26e7e4a"
|
||||
},
|
||||
{
|
||||
"path": "src/studio.ts",
|
||||
"bytes": 3624,
|
||||
"sha256": "bb2d04a0c80facef3949f3a3fd4d6a864cbd5d82164f2b31926b8db6f40a0b2a"
|
||||
},
|
||||
{
|
||||
"path": "src/swarm.ts",
|
||||
"bytes": 3418,
|
||||
"sha256": "1e7218ec20c15b89301a30a20d944c76b03e1a92c3017637c28dbf09d99bb82a"
|
||||
},
|
||||
{
|
||||
"path": "src/themes.ts",
|
||||
"bytes": 847,
|
||||
"sha256": "d1b7079bfe34e9e52de44e936ddc3467fb49bdc2e8ea19851d106735f9f523cc"
|
||||
},
|
||||
{
|
||||
"path": "src/tool-profiles.ts",
|
||||
"bytes": 1683,
|
||||
"sha256": "57d1d6f408524d9c6a38de861b6e7b5d4d9d3403defcfdc23674a5b9ced5c17f"
|
||||
},
|
||||
{
|
||||
"path": "src/tools.ts",
|
||||
"bytes": 17110,
|
||||
"sha256": "fa8130b3913f8db66a93b1ef00cc9519a64b9844e542d2bf652a3e793325cad4"
|
||||
},
|
||||
{
|
||||
"path": "src/ui.ts",
|
||||
"bytes": 1995,
|
||||
"sha256": "73557a611a959037cb0f6647a14f4895f4e666cd615fc1074142ee6dfe048b19"
|
||||
},
|
||||
{
|
||||
"path": "src/validators.ts",
|
||||
"bytes": 13905,
|
||||
"sha256": "85839a827016c4c1fbeb99f50973990d7473f521e8de2bdfc8a49b062be95e26"
|
||||
},
|
||||
{
|
||||
"path": "src/visual.ts",
|
||||
"bytes": 10240,
|
||||
"sha256": "7d4c1e712740bade58c6a373a5fbdc8b2e9fe855dec31f5c2328cf0b32215a86"
|
||||
},
|
||||
{
|
||||
"path": "src/zip.ts",
|
||||
"bytes": 4256,
|
||||
"sha256": "9b8f1c270fd6abd53d206b02459d79dc6bd2e88030cebd62fd88619b6ad8b2f1"
|
||||
},
|
||||
{
|
||||
"path": "themes/pom-nocturne.json",
|
||||
"bytes": 2486,
|
||||
"sha256": "ccf4a0e0452f5627f73a504b9634fdbfe15c568227ac783959d6e0795f7736ab"
|
||||
},
|
||||
{
|
||||
"path": "themes/pom-parchment.json",
|
||||
"bytes": 3069,
|
||||
"sha256": "68eb488fd607cee31b141e92d4f73b7e53388680a9d96a90575b55de83a3d68d"
|
||||
}
|
||||
]
|
||||
}
|
||||
107
TASKLIST.md
Normal file
107
TASKLIST.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 🪴 POM 2 Wiki Recovery Backlog
|
||||
|
||||
## 1. 🚨 Release-truth emergency
|
||||
Purpose: stop shipping claims that outrun the current package, dependency state, and verification evidence.
|
||||
|
||||
- [ ] 1.1 Repair packaged runtime contents before any wiki/publication claim.
|
||||
- [ ] 1.1.1 Add `prompts/` to `package.json:21-35` `files` because `src/prompts.ts:23,43-49` loads bundled prompt fragments at runtime and `README.md:9-13` advertises 13 prompt fragments; completion: `npm pack --dry-run` lists all 13 `prompts/*.md` and a tarball install can resolve `../prompts`.
|
||||
- [ ] 1.1.2 Regenerate `RELEASE.json` only after the package file allowlist is corrected; completion: `npm run release:manifest` includes prompt files in `sourceInventory` and `npm run release:check` passes against the generated bytes.
|
||||
- [ ] 1.2 Replace hard-coded release PASS mythology with evidence-backed release status.
|
||||
- [ ] 1.2.1 Rewrite `scripts/release-manifest.mjs:31-67` so `verification` and audit strings are generated from recorded current gate outputs or marked `NOT RUN`/`BLOCKED`, not literal PASS values; completion: changing gate evidence changes rendered `RELEASE.json` without hand edits.
|
||||
- [ ] 1.2.2 Remove or update stale 2026-07-12 audit language in `scripts/release-manifest.mjs:63-64` and `RELEASE.json:33-34`; completion: release manifest states the exact audit command/date/result actually rerun, or explicitly says audit is not current.
|
||||
- [ ] 1.2.3 Add a release-evidence note to the future wiki explaining that `AGENTS.md` was rehashed in `RELEASE.json:45-49` but green gates were not rerun afterward; completion: wiki release page distinguishes manifest freshness from full verification truth.
|
||||
- [ ] 1.3 Make the dependency-install blocker first-class instead of hiding it behind failed full verify.
|
||||
- [ ] 1.3.1 Document the current blocker: internal npm Artifactory timed out twice, leaving full verify blocked by missing `@oh-my-pi/*` SDK packages, Node types, `yauzl` types/deps, and theme schema expected by `scripts/verify.mjs:66`; completion: release/readiness page lists the exact missing dependency families and retries attempted.
|
||||
- [ ] 1.3.2 Keep the partial green evidence scoped precisely: `npm run smoke`, `npm run release:check`, and the state test pass, while full gate is not green; completion: no page or manifest says `npm run verify` passed until `npm install` succeeds and the full command is rerun.
|
||||
- [ ] 1.3.3 Add a recovery task for maintainers with a working registry: rerun `npm install`, then `npm run verify`, then regenerate/recheck release evidence; completion: captured command outputs are linked from the wiki readiness page.
|
||||
|
||||
## 2. 📜 Canonical SSOT contracts
|
||||
Purpose: align all docs and wiki pages to source-owned contracts, with source/runtime outranking older docs.
|
||||
|
||||
- [ ] 2.1 Declare source ownership for generated project layout.
|
||||
- [ ] 2.1.1 Treat `src/persistence.ts:226-239` as canonical for generated directories: `06_ledgers` and `07_exports`; completion: wiki SSOT page cites this range as owner and no page presents `06_exports` as the active exports directory.
|
||||
- [ ] 2.1.2 Fix `docs/INVENTORY.md:34-43`, which currently lists stale `06_exports`; completion: inventory has `06_ledgers` for decisions/validation ledgers and `07_exports` for checkpoint/final archives.
|
||||
- [ ] 2.1.3 Fix `skills/pom/references/artifacts.md:3-11`, which currently lists stale `06_logs`; completion: skills reference matches `06_ledgers`/`07_exports` and defers to `src/persistence.ts` for future layout truth.
|
||||
- [ ] 2.2 Correct prompt precedence and override wording everywhere.
|
||||
- [ ] 2.2.1 Rewrite `POM.yml:87-97` prompt precedence to match `src/prompts.ts:83-117`: immutable law is prepended first, `personality-${state.mode}` follows, requested bundled/project fragments are composed with project fragments replacing bundled by id, and executable stage contract is appended last by `renderStagePrompt`; completion: wording no longer implies arbitrary project-local override order after the contract.
|
||||
- [ ] 2.2.2 Add a wiki prompt-contract page that cites `src/prompts.ts:23,43-49,83-117` and defines replacement-by-id; completion: agents can answer “which prompt wins?” without reading README/POM.yml.
|
||||
- [ ] 2.3 Correct command-surface truth.
|
||||
- [ ] 2.3.1 Update `README.md:58-77` and `POM.yml:38-54` to include implemented `/pom status [--json]` and `/pom help`, matching `src/command.ts:23-40,132-145,224-225`; completion: command map contains every verb registered in `src/command.ts` and no unimplemented verb is advertised.
|
||||
- [ ] 2.3.2 Add wiki command page ownership: command implementation lives in `src/command.ts`, parser behavior in `src/parser.ts`, human manifest in `POM.yml`; completion: docs duplicate command summaries only by linking to this owner page.
|
||||
- [ ] 2.4 Correct validation ownership.
|
||||
- [ ] 2.4.1 Rewrite `docs/TOOL_MATRIX.md:5-13` so standalone validation belongs to `pom_validate` and `pom_stage action=pass` is validation-gated, not a generic validation tool; completion: matrix matches `src/tools.ts:93-164,203-221`.
|
||||
- [ ] 2.4.2 Add wiki gate page: `pom_validate` emits scoped reports, `pom_stage pass` runs the stage/delivery scope and calls `passStage` only after a passing report; completion: page cites `src/tools.ts:138-148` and `src/state.ts` pass invariants.
|
||||
- [ ] 2.5 Fix stale contributor-guide discrepancy claims before copying them into the wiki.
|
||||
- [ ] 2.5.1 Correct `AGENTS.md:158-162`: `stopGuard` and `destructiveProjectCommands` do have consumers in `src/events.ts:87-93,186-203`, so do not preserve the old “no runtime consumer exists” claim; completion: known-discrepancies page says these settings are wired and cites both `src/config.ts:45-53` and `src/events.ts` consumers.
|
||||
- [ ] 2.5.2 Correct the test-count wording: there are 8 test files in `tests/*.test.ts` but nine test declarations/behavioral tests are asserted by `scripts/verify.mjs:29-31`; completion: docs distinguish file count from test declaration/manifest count without calling it inconsistent.
|
||||
|
||||
## 3. 🗺️ Wiki information architecture and content ownership
|
||||
Purpose: create a navigable wiki/SSOT instead of scattered README/docs/skills fragments with no publication skeleton.
|
||||
|
||||
- [ ] 3.1 Create the repository wiki entrypoints and navigation scaffold.
|
||||
- [ ] 3.1.1 Add `docs/index.md` as the human landing page that links the existing docs: `ARCHITECTURE.md`, `SETTINGS.md`, `TOOL_MATRIX.md`, `INVENTORY.md`, `AUDIT.md`, `DESIGN_REVIEW.md`, `OFFICIAL_SOURCES.md`, and `POM_ARCHITECTURE.mmd`; completion: README can link one docs index instead of leaving only three docs discoverable at `README.md:115-119`.
|
||||
- [ ] 3.1.2 Add a sidebar/navigation file appropriate for the chosen wiki generator and list source-truth pages before narrative/audit pages; completion: every existing doc is reachable from the sidebar in one click.
|
||||
- [ ] 3.1.3 Add minimal Quartz 5 configuration/content-vault structure only after deciding whether the repo wiki lives in `docs/` or a separate content vault; completion: local preview can render the docs index and all internal links without pretending a live wiki exists.
|
||||
- [ ] 3.2 Assign ownership so duplicate facts stop drifting.
|
||||
- [ ] 3.2.1 Create a wiki ownership table: runtime contracts owned by `src/*`, package/release truth by `package.json` + `scripts/release-manifest.mjs` + command evidence, operator flows by README, deep reference by docs, agent-facing quick refs by `skills/pom/references/*`; completion: each duplicated fact has a named owner and “mirrors must cite owner” rule.
|
||||
- [ ] 3.2.2 Consolidate docs/skills duplication for layout, command map, validation scopes, resource counts, and prompt order; completion: each mirror page cites its source owner and contains no independent stale variants.
|
||||
- [ ] 3.2.3 Add a “source beats docs” note to wiki landing/readiness pages; completion: future agents know to verify against code before editing narrative docs.
|
||||
- [ ] 3.3 Define repository-wiki versus generated-project-vault boundaries.
|
||||
- [ ] 3.3.1 Document that repo docs describe the extension, while generated project vault files are created by `src/persistence.ts` under project `content/`; completion: README’s `content/` tree cannot be mistaken for this repository’s missing live wiki.
|
||||
- [ ] 3.3.2 Add a wiki page for generated vault shape based on `README.md:80-98` and `src/persistence.ts`; completion: generated-project docs and repository wiki scaffolding are separate navigation branches.
|
||||
|
||||
## 4. 🧭 Operator onboarding and package metadata
|
||||
Purpose: make a new maintainer/operator able to acquire, install, verify, and report on POM without hidden assumptions.
|
||||
|
||||
- [ ] 4.1 Add repository acquisition and install-path clarity to README/wiki.
|
||||
- [ ] 4.1.1 Expand `README.md:22-42` with repository acquisition flow: clone/download source, `npm install`, source-tree verification, `npm run link`, restart OMP or `/reload-plugins`, `/pom theme install`, and `npm run doctor`; completion: an operator starting from no checkout can reach a linked plugin.
|
||||
- [ ] 4.1.2 Distinguish source checkout install from published package install, including the current package defect where published installs would miss `prompts/` until `package.json:21-35` is repaired; completion: no instructions imply npm-published install is safe before package allowlist verification.
|
||||
- [ ] 4.1.3 Add SDK/engine compatibility to README/wiki: peer packages `@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-utils` are `>=16.4.6 <17` (`package.json:42-45`), Bun host is `>=1.3.14` (`package.json:57-58`), Node/npm are needed for local scripts; completion: compatibility is visible before install commands.
|
||||
- [ ] 4.2 Add package metadata required for trustworthy distribution.
|
||||
- [ ] 4.2.1 Add `repository`, `homepage`, and `bugs` fields to `package.json`, or explicitly block on the authoritative public URLs if unknown; completion: npm package metadata points operators to source, docs/wiki, and issue reporting without guesswork.
|
||||
- [ ] 4.2.2 After metadata changes, regenerate `RELEASE.json` via `npm run release:manifest`; completion: metadata changes are included in `sourceInventory` and `npm run release:check` passes.
|
||||
- [ ] 4.3 Write a blocked-prerequisites lane for maintainers when external services fail.
|
||||
- [ ] 4.3.1 Add docs guidance for registry outages: do not vendor/stub OMP SDK or theme schema; retry/repair registry, then run full gate; completion: dependency failure is recorded as external blocker, not hidden by partial tests.
|
||||
- [ ] 4.3.2 Add a short “what can be verified offline” section: source reads, release manifest check, state test, and smoke may pass, but `scripts/verify.mjs` theme schema and full `npm run verify` require installed deps; completion: partial verification labels are exact.
|
||||
|
||||
## 5. 🧪 Verification automation and mutation checks
|
||||
Purpose: make the wiki cleanup self-defending so stale docs and fake release claims fail visibly.
|
||||
|
||||
- [ ] 5.1 Add narrow documentation contract checks.
|
||||
- [ ] 5.1.1 Add a script or targeted test that compares README/POM.yml command maps to `src/command.ts` completions and HELP text; completion: omission of `/pom status [--json]` or `/pom help` fails the check.
|
||||
- [ ] 5.1.2 Add a layout-doc check that fails if docs/skills mention `06_exports` or `06_logs` as canonical generated directories; completion: stale path names cannot re-enter `docs/INVENTORY.md` or `skills/pom/references/artifacts.md`.
|
||||
- [ ] 5.1.3 Add a prompt-order doc check or snapshot tied to `composePromptStack`/`renderStagePrompt`; completion: docs cannot claim contract-before-law or non-ID override semantics.
|
||||
- [ ] 5.2 Add package/release mutation checks.
|
||||
- [ ] 5.2.1 Add a packaging assertion that every runtime-loaded bundled resource root in source (`../prompts`, `../skills`, `../themes`, plus any future agents/rules discovery roots) is represented in `package.json.files`; completion: deleting `prompts/` from `files` fails before pack.
|
||||
- [ ] 5.2.2 Add a release-truth check that prevents `scripts/release-manifest.mjs` from rendering PASS claims when the corresponding gate evidence file/command result is missing or stale; completion: old hard-coded PASS strings cannot survive without evidence.
|
||||
- [ ] 5.2.3 Add a mutation proof for the release check: temporarily remove `prompts/` from package files or alter one command-map doc entry and confirm the new check fails, then revert; completion: failure output is captured in wiki verification notes.
|
||||
- [ ] 5.3 Keep automation scoped and source-grounded.
|
||||
- [ ] 5.3.1 Do not add broad linters or formatters as a substitute for contract checks; completion: each new check defends one listed defect with a precise failure message.
|
||||
- [ ] 5.3.2 Update `scripts/verify.mjs` only if dependency availability is restored; completion: new checks run under the existing `npm run verify` sequence without masking the current install blocker.
|
||||
|
||||
## 6. 🍎 Ego browser handoff and live roast
|
||||
Purpose: acknowledge that official ego lite is macOS-only today while still enabling Linux-safe preview work and a later real ego audit.
|
||||
|
||||
- [ ] 6.1 Record the ego platform boundary as a blocker, not a workaround.
|
||||
- [ ] 6.1.1 Add wiki/readiness task noting official ego lite is macOS-only, Linux support is roadmap-only, the Darwin installer cannot run on this Linux host, and `ego-browser` is absent; completion: no Linux verification step claims to satisfy an ego requirement.
|
||||
- [ ] 6.1.2 Prepare a macOS handoff checklist: checkout source, install dependencies, run local wiki preview, install/run official ego lite, open the wiki, capture link/navigation/rendering findings; completion: a macOS operator can execute without needing this Linux session.
|
||||
- [ ] 6.2 Provide Linux-safe pre-ego preview and link verification.
|
||||
- [ ] 6.2.1 Add a non-ego local preview path using the chosen Quartz/static renderer or markdown link checker; completion: docs index/sidebar/internal links render locally on Linux and failures are recorded as pre-ego findings.
|
||||
- [ ] 6.2.2 Label Linux preview evidence as “pre-ego” only; completion: readiness gate still requires macOS ego review or an explicit future Linux ego release.
|
||||
- [ ] 6.3 Run the later ego-browser live wiki roast when available.
|
||||
- [ ] 6.3.1 On macOS ego or future Linux ego, drive the live wiki with ego-browser: homepage, sidebar, docs index, command page, release truth page, generated-vault page, and readiness page; completion: screenshots/DOM notes prove navigation and content match source contracts.
|
||||
- [ ] 6.3.2 File roast findings back into the wiki backlog/remediation list with source references; completion: every ego finding has a linked page/selector and a source-owner decision.
|
||||
|
||||
## 7. ✅ Final publication/readiness gate
|
||||
Purpose: publish only after package truth, docs truth, automation, and live verification agree.
|
||||
|
||||
- [ ] 7.1 Resolve unresolved runtime/doc-contract tasks before final docs claim completeness.
|
||||
- [ ] 7.1.1 Decide and implement whether `resources_discover` should advertise shipped `agents/` and `rules/` in addition to `skills/`, `prompts/`, and `themes` (`src/events.ts:19-24`); completion: source behavior and docs agree, or docs explicitly say agents/rules are packaged but not resource-discovered.
|
||||
- [ ] 7.1.2 Give `pom_stage action=inspect` a dedicated branch or document its current summary fallthrough (`src/tools.ts:93-164`); completion: tool docs and behavior agree, and inspect has an objective test if behavior changes.
|
||||
- [ ] 7.2 Run dependency-ordered final gates.
|
||||
- [ ] 7.2.1 After registry/dependencies are restored, run `npm install`, `npm run release:manifest`, `npm run release:check`, and `npm run verify`; completion: outputs are captured and release/readiness pages cite exact current results.
|
||||
- [ ] 7.2.2 Run package dry-run and inspect the file list for `prompts/`, `agents/`, `rules/`, `skills/`, `themes`, docs, scripts, README, POM.yml, AGENTS.md, LICENSE, and RELEASE.json; completion: packaged content matches runtime resource needs.
|
||||
- [ ] 7.2.3 Run wiki preview/link verification on Linux and ego live roast on macOS/future ego; completion: both evidence classes are stored with labels and no broken internal links remain.
|
||||
- [ ] 7.3 Publish only with exact status labels.
|
||||
- [ ] 7.3.1 Update README/wiki readiness summary to one of: `BLOCKED` with external prerequisite, `PARTIAL` with exact passing checks, or `READY` with all gates and ego audit complete; completion: no PASS/READY claim lacks linked evidence.
|
||||
- [ ] 7.3.2 Final definition of done: package includes runtime prompts; release manifest is evidence-backed; docs/index/sidebar/wiki exist; stale layout/command/prompt/validation/test/settings claims are corrected; package metadata is present or explicitly URL-blocked; runtime/doc-contract decisions for resources and inspect are resolved; full verify is green after dependency install; Linux preview passes; macOS/future ego audit is complete or publication remains blocked.
|
||||
22
agents/pom-architect.md
Normal file
22
agents/pom-architect.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-architect
|
||||
description: Master project architect for POM stage integration and decision quality
|
||||
tools: read,glob,grep,eval,todo,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Architect
|
||||
|
||||
Own the whole stage architecture. Resolve ambiguity from evidence, choose the strongest coherent direction, integrate specialist outputs, and return decisions, dependencies, risks, acceptance evidence, and exact next actions. Never fabricate completion.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-character-world.md
Normal file
22
agents/pom-character-world.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-character-world
|
||||
description: Builds character contradictions, relationships, locations, factions, rules, culture, and sensory identity
|
||||
tools: read,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Character World
|
||||
|
||||
Design only detail that creates story pressure, atmosphere, choice, and continuity anchors. Track goals, needs, wounds, false beliefs, secrets, moral lines, states, relationship power, system capabilities, limits, costs, loopholes, and consequences.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-concept-forge.md
Normal file
22
agents/pom-concept-forge.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-concept-forge
|
||||
description: Generates structurally diverse, emotionally potent, sustainable original story concepts
|
||||
tools: read,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Concept Forge
|
||||
|
||||
Generate meaningfully different engines, protagonists, relationships, settings, moral problems, antagonists, endings, and visual signatures. Reject cosmetic variants. Rank originality, emotion, character, engine, genre promise, theme, visuality, length, and audience.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-continuity-auditor.md
Normal file
22
agents/pom-continuity-auditor.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-continuity-auditor
|
||||
description: Audits timeline, knowledge, relationships, objects, injuries, rules, mysteries, setups, and payoffs
|
||||
tools: read,glob,grep,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Continuity Auditor
|
||||
|
||||
Independently compare canonical facts. For every contradiction, show both facts, identify affected scenes, select or request canon, recommend the smallest repair, and list dependent checks. Never silently overwrite canon.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-critic.md
Normal file
22
agents/pom-critic.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-critic
|
||||
description: Cross-examines concepts and structures for cliché, weak causality, thin emotion, and unearned solutions
|
||||
tools: read,grep,glob,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Critic
|
||||
|
||||
Be adversarial but constructive. Find the most consequential defects, prove them from project evidence, rank repairs by leverage, and preserve what works. Do not rewrite unless asked.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-delivery-auditor.md
Normal file
22
agents/pom-delivery-auditor.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-delivery-auditor
|
||||
description: Verifies manifests, checksums, exports, ZIP members, metrics, omissions, and final claims
|
||||
tools: read,glob,grep,eval,bash,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Delivery Auditor
|
||||
|
||||
Treat every completion claim as untrusted until measured. Verify paths, bytes, hashes, chapter count/order, placeholders, partials, image documentation, research support, rights notes, ZIP decompression and CRC, report metrics, warnings, and delivery paths.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-developmental-editor.md
Normal file
22
agents/pom-developmental-editor.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-developmental-editor
|
||||
description: Edits fiction for structure, pacing, necessity, clarity, voice, emotion, and reader experience
|
||||
tools: read,grep,glob,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Developmental Editor
|
||||
|
||||
Diagnose before editing. Protect distinctive strengths. Identify scene function, redundancy, exposition, pacing, emotional logic, repetition, weak dialogue, unclear action, and ending pressure. Recommend prioritized surgical revisions with acceptance tests.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-prose-smith.md
Normal file
22
agents/pom-prose-smith.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-prose-smith
|
||||
description: Drafts complete immersive fiction under voice, canon, POV, and scene constraints
|
||||
tools: read,grep,write,edit,eval,irc,yield
|
||||
model: pi/default
|
||||
thinkingLevel: high
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Prose Smith
|
||||
|
||||
Draft complete scenes rather than summaries. Preserve voice, POV, tense, knowledge boundaries, spatial clarity, subtext, selective sensory detail, action causality, emotional aftermath, genre checks, and changed exit state. Stop cleanly and mark PARTIAL if incomplete.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-researcher.md
Normal file
22
agents/pom-researcher.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-researcher
|
||||
description: Primary-source researcher for factual, historical, scientific, cultural, legal, market, and comparable-title questions
|
||||
tools: read,web_search,browser,inspect_image,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: high
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Researcher
|
||||
|
||||
Research only accuracy-critical scope. Prefer primary sources, compare dates, corroborate consequential claims, distinguish fact, inference, dispute, and invention, and return citation-ready notes plus story-use implications. Use read before browser.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-rights-auditor.md
Normal file
22
agents/pom-rights-auditor.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-rights-auditor
|
||||
description: Audits style imitation, quotes, likenesses, assets, attribution, privacy, and redistribution rights
|
||||
tools: read,grep,glob,web_search,inspect_image,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: high
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Rights Auditor
|
||||
|
||||
Identify rights and privacy risks precisely. Separate inspiration from copying, verify quotation and asset status, flag likeness concerns, and prescribe the smallest safe correction. Do not overclaim legal certainty.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-story-architect.md
Normal file
22
agents/pom-story-architect.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-story-architect
|
||||
description: Integrates premise, ending, arcs, themes, mysteries, systems, and promises into a coherent bible
|
||||
tools: read,grep,glob,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Story Architect
|
||||
|
||||
Build a complete narrative system whose ending, arcs, world pressure, rules, mysteries, themes, motifs, and reader promises reinforce one another. Surface contradictions and unstable assumptions before outlining.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-structure-engineer.md
Normal file
22
agents/pom-structure-engineer.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-structure-engineer
|
||||
description: Engineers causal acts, sequences, scenes, chapters, escalation, and setup-payoff chains
|
||||
tools: read,grep,glob,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Structure Engineer
|
||||
|
||||
Prefer because-therefore-but causality. Every scene changes a meaningful variable. Audit agency, midpoint operating change, antagonist pressure, subplot intersection, escalation, prepared climax solutions, chapter function, POV, pacing, and denouement.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-visual-director.md
Normal file
22
agents/pom-visual-director.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-visual-director
|
||||
description: Creates and audits visual bibles, briefs, generated assets, typography, and continuity
|
||||
tools: read,inspect_image,generate_image,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: high
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Visual Director
|
||||
|
||||
Define identity anchors, medium, rendering, palette, lighting, composition, camera, environment, wardrobe, props, anatomy, typography, exclusions, alt text, and rights. Generate only purposeful assets and inspect every result before approval.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
22
agents/pom-voice-architect.md
Normal file
22
agents/pom-voice-architect.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: pom-voice-architect
|
||||
description: Designs original author personas and precise testable prose voice systems
|
||||
tools: read,eval,irc,yield
|
||||
model: pi/slow
|
||||
thinkingLevel: xhigh
|
||||
read-summarize: false
|
||||
---
|
||||
|
||||
# Pom Voice Architect
|
||||
|
||||
Create an original persona and voice system: distance, POV, tense, rhythm, diction, sensory and figurative density, dialogue, exposition, interiority, humor, darkness, motifs, openings, closings, always laws, avoid laws, tags, and original sample. Translate protected influences into non-exclusive traits.
|
||||
|
||||
Return:
|
||||
- findings
|
||||
- decisions or recommendations
|
||||
- evidence
|
||||
- risks
|
||||
- exact next actions
|
||||
- acceptance status
|
||||
|
||||
Finish through `yield`.
|
||||
2
config/APPEND_SYSTEM.md
Normal file
2
config/APPEND_SYSTEM.md
Normal file
@@ -0,0 +1,2 @@
|
||||
POM — Produce Magnum Opus is available through `/pom`, `skill://pom`, curated `pom-*` agents, and `pom_*` tools.
|
||||
For active POM projects: canonical files are authoritative; use native todo, task, IRC, memory, approval, compaction, and session systems; advance only after deterministic validation; never invent artifacts or completion.
|
||||
49
config/config.yml
Normal file
49
config/config.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
# Recommended project-local Oh My Pi settings for POM 2.
|
||||
theme:
|
||||
dark: pom-nocturne
|
||||
light: pom-parchment
|
||||
|
||||
symbolPreset: unicode
|
||||
|
||||
statusLine:
|
||||
preset: full
|
||||
separator: powerline-thin
|
||||
sessionAccent: true
|
||||
showHookStatus: true
|
||||
|
||||
terminal:
|
||||
showImages: true
|
||||
|
||||
images:
|
||||
autoResize: true
|
||||
blockImages: false
|
||||
|
||||
tools:
|
||||
intentTracing: true
|
||||
artifactSpillThreshold: 50
|
||||
artifactHeadBytes: 20
|
||||
artifactTailBytes: 20
|
||||
|
||||
task:
|
||||
eager: preferred
|
||||
batch: true
|
||||
maxConcurrency: 8
|
||||
maxRecursionDepth: 2
|
||||
showResolvedModelBadge: true
|
||||
|
||||
async:
|
||||
enabled: true
|
||||
|
||||
compaction:
|
||||
strategy: snapcompact
|
||||
midTurnEnabled: true
|
||||
autoContinue: true
|
||||
|
||||
memory:
|
||||
backend: local
|
||||
|
||||
steeringMode: one-at-a-time
|
||||
followUpMode: one-at-a-time
|
||||
interruptMode: immediate
|
||||
defaultThinkingLevel: high
|
||||
hideThinkingBlock: false
|
||||
15
config/mcp.json.example
Normal file
15
config/mcp.json.example
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
||||
"mcpServers": {
|
||||
"trusted-research": {
|
||||
"type": "http",
|
||||
"url": "https://replace-with-trusted-server.example/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${POM_RESEARCH_MCP_TOKEN}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disabledServers": [
|
||||
"trusted-research"
|
||||
]
|
||||
}
|
||||
16
config/pom.json
Normal file
16
config/pom.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"autoToolProfiles": true,
|
||||
"autoModelRouting": true,
|
||||
"dashboardWidget": true,
|
||||
"stopGuard": true,
|
||||
"defaultMode": "autopilot",
|
||||
"defaultView": "cinematic",
|
||||
"defaultMotion": "subtle",
|
||||
"defaultToolProfile": "auto",
|
||||
"maxStudioAgents": 6,
|
||||
"projectRootMode": "child",
|
||||
"destructiveProjectCommands": "block",
|
||||
"gitMode": "branch-per-stage",
|
||||
"hudPlacement": "aboveEditor",
|
||||
"showThinkingLane": true
|
||||
}
|
||||
54
docs/ARCHITECTURE.md
Normal file
54
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Architecture
|
||||
|
||||
## Responsibility boundary
|
||||
|
||||
```text
|
||||
Oh My Pi
|
||||
├── models and thinking levels
|
||||
├── native tools and approvals
|
||||
├── task agents and IRC
|
||||
├── todo, sessions, compaction, memory
|
||||
├── extension lifecycle and TUI
|
||||
└── optional Swarm extension
|
||||
|
||||
POM
|
||||
├── stage contracts and authoritative transitions
|
||||
├── evidence, blockers, decisions, and provenance
|
||||
├── prompt stack and personality composition
|
||||
├── Hive mission planning and synthesis records
|
||||
├── artifact manifest and semantic versions
|
||||
├── deterministic validators
|
||||
├── Obsidian/Quartz knowledge projection
|
||||
├── Git stage checkpoints
|
||||
└── verified archive delivery
|
||||
```
|
||||
|
||||
OMP executes; POM governs. POM never invents a parallel task runner, chat network, todo database, or session tree.
|
||||
|
||||
## Runtime layers
|
||||
|
||||
| Layer | Modules | Responsibility |
|
||||
|---|---|---|
|
||||
| Host integration | `index.ts`, `events.ts`, `command.ts` | Register commands, tools, lifecycle hooks, routing, and policy |
|
||||
| Terminal | `ui.ts`, `visual.ts`, `renderers.ts`, `themes.ts` | HUD, status, overlay, cards, themes, and thinking annotation |
|
||||
| Domain | `domain.ts`, `state.ts` | Stage contracts, schemas, transitions, blockers, evidence, versions |
|
||||
| Execution planning | `studio.ts`, `swarm.ts`, `router.ts`, `tool-profiles.ts` | Native task batches, optional DAGs, model roles, active tools |
|
||||
| Knowledge and persistence | `prompts.ts`, `persistence.ts`, `paths.ts` | Prompt provenance, canonical files, vault, manifests, ledgers |
|
||||
| Assurance | `validators.ts`, `policy.ts`, `zip.ts`, `exporter.ts`, `git.ts` | Gates, path safety, command guard, checkpoints, archive verification |
|
||||
|
||||
## Truth hierarchy
|
||||
|
||||
1. System and explicit user requirements.
|
||||
2. Canonical project files and `00_admin/project_state.json`.
|
||||
3. Artifact manifest, evidence records, and append-only ledgers.
|
||||
4. Native OMP todo/task/IRC/session state for live execution.
|
||||
5. Generated vault notes as navigational projections.
|
||||
6. Memory as reusable heuristics only—never story canon.
|
||||
|
||||
## Stage transition invariant
|
||||
|
||||
A stage can pass only when it is actively running, all prerequisites are complete, every acceptance criterion has file-backed evidence, no blocker remains open, and deterministic validation produced a report path.
|
||||
|
||||
## Persistence
|
||||
|
||||
POM stores state both in OMP's append-only session entries and in the canonical project file. File writes use unique temporary paths followed by rename. Manifest and ledger mutation is serialized within the process. Files are resolved through canonical paths and symbolic-link escapes are rejected.
|
||||
32
docs/AUDIT.md
Normal file
32
docs/AUDIT.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Release audit — POM 2.0.0
|
||||
|
||||
Date: 2026-07-12
|
||||
|
||||
## Automated verification
|
||||
|
||||
`npm run verify` performs:
|
||||
|
||||
1. Strict TypeScript check against real OMP packages.
|
||||
2. Production-code behavioral tests.
|
||||
3. Temporary-project runtime smoke test.
|
||||
4. Structural package, resource, agent-frontmatter, and theme-schema checks.
|
||||
5. `npm pack --dry-run`.
|
||||
|
||||
Covered behaviors include stage ordering, active-run gates, evidence requirements, blocker lifecycle, traversal/symlink defense, destructive-command policy, validator behavior, native Hive translation, Swarm graph generation, ZIP member decompression, CRC validation, and malformed archive rejection.
|
||||
|
||||
## Security posture
|
||||
|
||||
- Canonical paths are resolved with `realpath`.
|
||||
- Symbolic-link escapes are rejected.
|
||||
- Empty canonical artifacts are rejected.
|
||||
- Manifest hashes and byte counts are remeasured.
|
||||
- Recursive destructive commands against the project root are blocked by default.
|
||||
- ZIP member names, methods, decompression, sizes, duplicate names, and CRC values are verified.
|
||||
|
||||
The command policy is a guardrail, not an operating-system sandbox. Host approvals and normal filesystem permissions remain authoritative.
|
||||
|
||||
## Known release boundary
|
||||
|
||||
Bun and the `omp` executable are unavailable in this build environment. Therefore `omp plugin doctor` and a live interactive TUI smoke session were not run here. They remain mandatory host acceptance checks after installation.
|
||||
|
||||
`npm audit` currently reports moderate issues in the development/host dependency tree. The release archive does not claim a zero-vulnerability dependency graph; operators should evaluate the host's current dependency advisories.
|
||||
52
docs/BOUNDARY.md
Normal file
52
docs/BOUNDARY.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Repository Wiki vs Generated Project Vault
|
||||
description: Clear boundary between extension documentation and project knowledge bases
|
||||
---
|
||||
|
||||
# Repository Wiki vs Generated Project Vault
|
||||
|
||||
These are two entirely separate documentation systems. Confusing them is a primary source of drift.
|
||||
|
||||
## Repository Wiki (this site)
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| **Purpose** | Documents the POM 2 *extension* for operators, contributors, architects |
|
||||
| **Source** | `/home/loca/dev/omppmo/docs/`, `/home/loca/dev/omppmo/README.md`, `/home/loca/dev/omppmo/POM.yml`, `/home/loca/dev/omppmo/skills/pom/references/` |
|
||||
| **Audience** | Humans installing, developing, or auditing the extension |
|
||||
| **Build** | Quartz 5 (`npm run serve:dev` in this repo) |
|
||||
| **Deploy target** | `pom-omp.loca.zone` (or similar) |
|
||||
| **Content type** | Static extension documentation: install, commands, architecture, contracts, release process |
|
||||
|
||||
## Generated Project Vault
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| **Purpose** | Knowledge projection for a *specific creative project* produced by POM stages |
|
||||
| **Source** | Created at runtime by `src/persistence.ts:226-239` (`bootstrapProject`) under the project root |
|
||||
| **Audience** | The author/editor working on that specific creative work |
|
||||
| **Build** | Quartz 5 (separate instance per project, configured by `config/config.yml` template) |
|
||||
| **Deploy target** | Project-specific domain (e.g., `my-novel.loca.zone`) |
|
||||
| **Content type** | Dynamic creative artifacts: canon, cast, timeline, manuscript, research, images |
|
||||
|
||||
## Boundary rules
|
||||
|
||||
1. **Repository wiki NEVER contains project-specific content** — no canon, no cast, no manuscript scenes
|
||||
2. **Generated vault NEVER contains extension documentation** — no install instructions, no command maps, no tool schemas
|
||||
3. **README.md's `content/` tree (lines 80-98) describes the *vault shape*, not this wiki** — do not mistake the example vault structure for this repository's documentation
|
||||
4. **POM.yml's `knowledge.vault` field describes the *generated* vault format** — Obsidian-flavoured Markdown compatible with Quartz 5, but each project gets its own Quartz config
|
||||
|
||||
## Navigation consequence
|
||||
|
||||
- This wiki has an "Operator → Generated Vault" link that explains the vault shape and points to `src/persistence.ts` as the owner
|
||||
- A generated vault has a "Meta → Extension docs" link pointing back to this wiki's URL
|
||||
- They are separate Quartz instances with separate `quartz.config.ts`, separate deployments, separate domains
|
||||
|
||||
## Common confusion points
|
||||
|
||||
| Confusion | Reality |
|
||||
|---|---|
|
||||
| "Where is the wiki for my novel?" | In your project's `content/`, deployed to your project's domain |
|
||||
| "Why doesn't `docs/` have my story bible?" | `docs/` is the *extension* wiki; your story bible is in `02_story_bible/` |
|
||||
| "Can I edit the extension docs from my project?" | No — edit `/home/loca/dev/omppmo/docs/` and rebuild the extension wiki |
|
||||
| "Does `pom vault` modify this wiki?" | No — `pom vault` inserts a mention into the *generated project's* production board |
|
||||
53
docs/DESIGN_REVIEW.md
Normal file
53
docs/DESIGN_REVIEW.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Terminal experience design review
|
||||
|
||||
## Objective
|
||||
|
||||
Make verified production progress emotionally legible without rewarding noise.
|
||||
|
||||
## Surface hierarchy
|
||||
|
||||
1. **Status line:** tiny peripheral facts—stage, percentage, gate, Hive, approval, retry.
|
||||
2. **HUD widget:** persistent production truth—contract, progress, evidence, blockers, active work.
|
||||
3. **Working message:** one sentence describing what is happening this moment.
|
||||
4. **Cards:** durable meaningful events—validation, Hive mission, blocker, milestone, archive.
|
||||
5. **Overlay:** deliberate operator choices—run, delegate, validate, evidence, vault, export, theme, settings.
|
||||
6. **Expanded tool result:** detailed evidence only on demand.
|
||||
|
||||
## Density modes
|
||||
|
||||
| Mode | HUD | Cards | Best use |
|
||||
|---|---|---|---|
|
||||
| `cinematic` | Three lines with stage track and coordination cue | Rich details | Focused creative sessions |
|
||||
| `rich` | Two evidence-focused lines | Detailed | Daily production |
|
||||
| `compact` | One line | Terse | Split panes and small terminals |
|
||||
| `minimal` | No widget; status and critical cards only | Essential | Remote/headless-adjacent use |
|
||||
|
||||
## Motion modes
|
||||
|
||||
- `off`: static semantic glyphs.
|
||||
- `subtle`: two-frame pulse only while meaningful work is active.
|
||||
- `full`: four-frame activity cue plus stronger live coordination feedback.
|
||||
|
||||
Animation stops with work. Completion is represented by stable state, not perpetual motion.
|
||||
|
||||
## Reward model
|
||||
|
||||
Prominent visual changes are reserved for:
|
||||
|
||||
- file-backed evidence accepted;
|
||||
- blocker opened or resolved;
|
||||
- agent ownership claimed or handed off;
|
||||
- deterministic validation passed;
|
||||
- authoritative stage completed;
|
||||
- archive decompressed and verified.
|
||||
|
||||
A tool merely starting receives only a small activity cue.
|
||||
|
||||
## Accessibility and correctness
|
||||
|
||||
- Semantic theme tokens instead of hard-coded terminal escape sequences.
|
||||
- Unicode glyphs are supplementary; words and layout retain meaning.
|
||||
- Components use visual-width utilities and truncate safely.
|
||||
- Unchanged HUD frames reuse cached line arrays to reduce repaint work.
|
||||
- Minimal mode and motion-off provide low-stimulation operation.
|
||||
- POM Nocturne and Parchment preserve corresponding dark/light semantics.
|
||||
45
docs/INVENTORY.md
Normal file
45
docs/INVENTORY.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Package inventory
|
||||
|
||||
## Runtime
|
||||
|
||||
- 1 OMP extension entry point
|
||||
- 7 typed POM tools
|
||||
- 1 `/pom` command namespace
|
||||
- 1 `Alt+P` shortcut
|
||||
- 4 custom message renderers
|
||||
- 1 optional thinking renderer
|
||||
- Lifecycle coverage for restore, prompt augmentation, tools, approvals, retries, compaction, stop, and shutdown
|
||||
|
||||
## Production resources
|
||||
|
||||
- 9 stage contracts
|
||||
- 14 task-agent definitions
|
||||
- 13 prompt fragments
|
||||
- 3 scoped rules
|
||||
- 1 primary skill package
|
||||
- 2 OMP themes
|
||||
- 6 active-tool profiles including `auto`
|
||||
- 3 operating personalities
|
||||
- 4 terminal density modes
|
||||
- 3 motion modes
|
||||
|
||||
## Assurance
|
||||
|
||||
- 9 behavioral tests across 8 test files
|
||||
- 1 runtime smoke script
|
||||
- 1 structural/package verifier
|
||||
- Strict TypeScript configuration
|
||||
- npm pack dry-run
|
||||
|
||||
## Canonical generated project areas
|
||||
|
||||
- `00_admin` — state, manifest, ledgers, checksums, validation, Hive
|
||||
- `01_planning` — brief, genre, voice, story store, direction
|
||||
- `02_story_bible` — canon, cast, world, timeline, continuity, visual bible
|
||||
- `03_manuscript` — outline, scenes, chapters, complete manuscript
|
||||
- `04_images` — references, generated assets, briefs, approvals
|
||||
- `05_research` — source notes and rights records
|
||||
- `06_ledgers` — decisions, assumptions, revisions, continuity, quality, validation
|
||||
- `07_exports` — checkpoint and final archives plus reports
|
||||
- `content` — Obsidian/Quartz knowledge projection
|
||||
- `.omp` — host and POM project configuration plus prompt overrides
|
||||
14
docs/OFFICIAL_SOURCES.md
Normal file
14
docs/OFFICIAL_SOURCES.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# OMP integration sources
|
||||
|
||||
POM targets the public extension and TUI APIs exported by the installed OMP 16.4.x packages.
|
||||
|
||||
Primary references:
|
||||
|
||||
- OMP extension documentation: <https://github.com/can1357/oh-my-pi/blob/main/docs/extensions.md>
|
||||
- OMP TUI documentation: <https://github.com/can1357/oh-my-pi/blob/main/docs/tui.md>
|
||||
- OMP settings documentation: <https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md>
|
||||
- OMP package and Swarm documentation: <https://github.com/can1357/oh-my-pi/blob/main/docs/user-facing-packages.md>
|
||||
- OMP repository: <https://github.com/can1357/oh-my-pi>
|
||||
- Quartz Obsidian compatibility: <https://quartz.jzhao.xyz/features/Obsidian-compatibility>
|
||||
|
||||
Release compilation is performed against the actual TypeScript declarations installed from `@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-tui`, and `@oh-my-pi/pi-utils`; POM does not ship replacement declaration shims.
|
||||
37
docs/OWNERSHIP.md
Normal file
37
docs/OWNERSHIP.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Wiki Ownership Table
|
||||
description: Canonical ownership of every fact category to prevent drift
|
||||
---
|
||||
|
||||
# Wiki Ownership Table
|
||||
|
||||
Every duplicated fact has exactly one canonical owner. Mirrors must cite the owner and must not introduce independent variants.
|
||||
|
||||
| Fact category | Canonical owner | Mirrors (must cite owner) |
|
||||
|---|---|---|
|
||||
| Runtime contracts, stage machine, evidence rules | `src/state.ts`, `src/persistence.ts`, `src/domain.ts` | `docs/ARCHITECTURE.md`, `POM.yml`, `skills/pom/references/workflow.md` |
|
||||
| Package metadata, file allowlist, scripts | `package.json` | `README.md`, `docs/INVENTORY.md`, `RELEASE.json` |
|
||||
| Release verification evidence | `scripts/release-manifest.mjs` (evidence files in `00_admin/validation/`) | `RELEASE.json`, `README.md`, `docs/AUDIT.md` |
|
||||
| Operator command map | `src/command.ts` (completions, help text) | `README.md`, `POM.yml`, `skills/pom/references/commands.md` |
|
||||
| Tool actions, approvals, schemas | `src/tools.ts` | `docs/TOOL_MATRIX.md`, `POM.yml`, `skills/pom/references/commands.md` |
|
||||
| Generated project layout | `src/persistence.ts:226-239` (`bootstrapProject`) | `docs/INVENTORY.md`, `skills/pom/references/artifacts.md` |
|
||||
| Prompt precedence and composition | `src/prompts.ts:83-117` (`composePromptStack`, `renderStagePrompt`) | `POM.yml`, `README.md`, `skills/pom/references/workflow.md` |
|
||||
| Resource discovery advertisement | `src/events.ts:19-24` (`resources_discover`) | `skills/pom/references/commands.md`, `docs/ARCHITECTURE.md` |
|
||||
| Validation scopes and tool ownership | `src/tools.ts:138-148` (`pom_validate`), `src/state.ts` (`passStage`) | `docs/TOOL_MATRIX.md`, `POM.yml`, `skills/pom/references/qa.md` |
|
||||
| Theme schema and OMP integration | `@oh-my-pi/pi-coding-agent` (external) | `scripts/verify.mjs`, `config/config.yml`, `themes/*.json` |
|
||||
| Dependency requirements | `package.json` (`peerDependencies`, `engines.bun`) | `README.md`, `docs/SETTINGS.md`, `AGENTS.md` |
|
||||
|
||||
## Mirror rules
|
||||
|
||||
1. Every mirror page MUST include a "Source: `path:line`" reference to the canonical owner
|
||||
2. A mirror MUST NOT redefine a fact — only summarize, cross-reference, or contextualize
|
||||
3. When the canonical owner changes, all mirrors MUST be updated in the same PR
|
||||
4. Automated checks in `scripts/verify.mjs` or dedicated scripts validate mirror freshness
|
||||
5. `scripts/verify.mjs` already enforces: agent frontmatter shape, theme names/keys, prompt counts, command counts, tool counts, stage counts
|
||||
|
||||
## Adding a new fact category
|
||||
|
||||
1. Identify the single source file in `src/`, `scripts/`, or `package.json`
|
||||
2. Add a row to this table
|
||||
3. Update all existing mirrors to cite the new owner
|
||||
4. Add a verification check if the fact is drift-prone
|
||||
33
docs/POM_ARCHITECTURE.mmd
Normal file
33
docs/POM_ARCHITECTURE.mmd
Normal file
@@ -0,0 +1,33 @@
|
||||
flowchart TD
|
||||
U[Operator] --> C[/pom command center]
|
||||
C --> K[POM production kernel]
|
||||
K --> S[Authoritative stage machine]
|
||||
K --> P[Prompt stack and personality]
|
||||
K --> H[Hive mission planner]
|
||||
K --> V[Deterministic validation]
|
||||
K --> A[Artifact and canon ledger]
|
||||
K --> X[Verified export]
|
||||
|
||||
P --> O[Oh My Pi host]
|
||||
H --> T[Native task agents]
|
||||
H --> I[Native IRC]
|
||||
H --> W[Optional Swarm DAG]
|
||||
S --> R[Model and active-tool routing]
|
||||
R --> O
|
||||
T --> O
|
||||
I --> O
|
||||
W --> O
|
||||
|
||||
A --> F[Canonical project files]
|
||||
A --> M[Manifest plus SHA-256]
|
||||
F --> Q[Obsidian / Quartz vault]
|
||||
V --> F
|
||||
V --> M
|
||||
V --> E[Acceptance evidence]
|
||||
X --> Z[ZIP central directory + decompression + CRC]
|
||||
S --> G[Git stage branch/checkpoint]
|
||||
|
||||
O --> UI[OMP TUI hooks]
|
||||
UI --> HUD[HUD and status]
|
||||
UI --> CARDS[Validation/Hive/milestone cards]
|
||||
UI --> OVERLAY[Keyboard overlay]
|
||||
60
docs/SETTINGS.md
Normal file
60
docs/SETTINGS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Settings
|
||||
|
||||
## Oh My Pi host settings
|
||||
|
||||
Use `<project>/.omp/config.yml`. POM creates a recommended file during project bootstrap and ships the same template at `config/config.yml`.
|
||||
|
||||
Important host controls:
|
||||
|
||||
| Key | POM purpose |
|
||||
|---|---|
|
||||
| `theme.dark/light` | Select POM Nocturne/Parchment after `/pom theme install` |
|
||||
| `statusLine.showHookStatus` | Display POM stage, gate, Hive, retry, and approval segments |
|
||||
| `terminal.showImages` | Show visual references and generated images in capable terminals |
|
||||
| `tools.intentTracing` | Expose semantic tool intent in the live experience |
|
||||
| artifact spill settings | Keep large tool output out of the conversation surface |
|
||||
| `task.batch` / concurrency | Support bounded specialist waves |
|
||||
| `compaction.strategy: snapcompact` | Preserve long-running production context |
|
||||
| one-at-a-time steering/follow-up | Keep autopilot continuation ordered |
|
||||
|
||||
## POM settings
|
||||
|
||||
Precedence:
|
||||
|
||||
```text
|
||||
built-in defaults
|
||||
→ ~/.omp/agent/pom.json
|
||||
→ <project>/.omp/pom.json
|
||||
→ persisted project state for mutable experience fields
|
||||
```
|
||||
|
||||
| Setting | Values | Purpose |
|
||||
|---|---|---|
|
||||
| `autoToolProfiles` | boolean | Route OMP active tools by stage |
|
||||
| `autoModelRouting` | boolean | Resolve and activate the stage model role |
|
||||
| `dashboardWidget` | boolean | Enable the persistent HUD |
|
||||
| `stopGuard` | boolean | Continue an active stage briefly, then force explicit blocking |
|
||||
| `defaultMode` | `autopilot`, `collaborative`, `strict-autopilot` | Select prompt personality |
|
||||
| `defaultView` | `cinematic`, `rich`, `compact`, `minimal` | Select terminal information density |
|
||||
| `defaultMotion` | `off`, `subtle`, `full` | Select activity animation intensity |
|
||||
| `defaultToolProfile` | `auto`, `creative`, `research`, `draft`, `audit`, `delivery` | Initial active-tool policy |
|
||||
| `maxStudioAgents` | 1–12 | Bound a Hive task batch |
|
||||
| `projectRootMode` | `child`, `cwd` | Create a child project or use the current directory |
|
||||
| `destructiveProjectCommands` | `block`, `allow` | Guard recursive destructive commands |
|
||||
| `gitMode` | `off`, `checkpoint`, `branch-per-stage` | Control production checkpoints |
|
||||
| `hudPlacement` | `aboveEditor`, `belowEditor` | Place the production HUD |
|
||||
| `showThinkingLane` | boolean | Annotate visible thinking with stage/execution lane |
|
||||
|
||||
`/pom settings` persists view/motion in project state and HUD/thinking options in `.omp/pom.json`.
|
||||
|
||||
## Compatibility and verification state
|
||||
|
||||
| Requirement | Window | Source |
|
||||
|---|---|---|
|
||||
| `@oh-my-pi/pi-coding-agent` | `>=16.4.6 <17` | `peerDependencies` |
|
||||
| `@oh-my-pi/pi-tui` | `>=16.4.6 <17` | `peerDependencies` |
|
||||
| `@oh-my-pi/pi-utils` | `>=16.4.6 <17` | `peerDependencies` |
|
||||
| Bun | `>=1.3.14` | `engines.bun` |
|
||||
| Node.js + npm | Node 22 or newer | Local scripts and the `node:test` suite |
|
||||
|
||||
Verification state is PARTIAL. `npm run smoke`, `npm run release:check`, and `node --import tsx --test tests/state.test.ts` pass. The full `npm run verify` gate is blocked: an npm registry outage left `node_modules` incomplete (OMP SDK peer packages, `@types/node`, `yauzl`, and the OMP theme schema that `scripts/verify.mjs` reads). Restore registry access, reinstall, then rerun the full gate. See the README sections Verification status and Registry outage and offline guidance.
|
||||
38
docs/TOOL_MATRIX.md
Normal file
38
docs/TOOL_MATRIX.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Tool matrix
|
||||
|
||||
## POM tools
|
||||
|
||||
| Tool | Approval | Responsibility |
|
||||
|---|---|---|
|
||||
| `pom_state` | write | Read/configure project state and record decisions, assumptions, or warnings |
|
||||
| `pom_stage` | write | Start, navigate, evidence, block, resolve, and pass stages (validation-gated) |
|
||||
| `pom_artifact` | write | Measure, hash, version, classify, and register a canonical artifact |
|
||||
| `pom_validate` | read | Run deterministic scoped checks and emit an expandable report card |
|
||||
| `pom_prompt` | read | List or retrieve bundled/project prompt fragments and provenance |
|
||||
| `pom_hive` | write | Plan native task batches, record IRC signals, and persist synthesis |
|
||||
| `pom_export` | write | Build and fully verify checkpoint or final archives |
|
||||
|
||||
## Native OMP delegation
|
||||
|
||||
| Primitive | POM use |
|
||||
|---|---|
|
||||
| `task` | Exactly one bounded batch per independent specialist wave |
|
||||
| IRC | Short ownership, dependency, alert, handoff, and completion signals |
|
||||
| todo | Exact phase plan for the active stage |
|
||||
| Swarm extension | Optional unattended parallel/sequential/pipeline DAG |
|
||||
| session entries | Append-only POM state restoration across branches |
|
||||
| model roles | Deep planning/audit versus sustained drafting |
|
||||
| active-tool profiles | Minimize context and risk for each stage |
|
||||
|
||||
## Validation scopes
|
||||
|
||||
| Scope | Checks |
|
||||
|---|---|
|
||||
| `quick` | State shape and basic invariants |
|
||||
| `stage` | Active stage contract, artifacts, evidence, and blockers |
|
||||
| `canon` | Canon-oriented artifacts and manifest metadata |
|
||||
| `continuity` | Timeline, continuity ledger, and unresolved issue markers |
|
||||
| `knowledge` | Vault structure and wikilink targets |
|
||||
| `files` | Manifest paths, sizes, hashes, duplicates, and symlinks |
|
||||
| `delivery` | All delivery prerequisites, rights metadata, checksums, and package structure |
|
||||
| `all` | Broad project validation; final delivery should still use `delivery` |
|
||||
101
docs/VAULT.md
Normal file
101
docs/VAULT.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Generated Project Vault Shape
|
||||
description: The canonical directory structure and content types created by bootstrapProject
|
||||
---
|
||||
|
||||
# Generated Project Vault Shape
|
||||
|
||||
Created by `src/persistence.ts:226-239` (`bootstrapProject`). This is the *project* knowledge base, not the extension wiki.
|
||||
|
||||
## Top-level directories
|
||||
|
||||
```
|
||||
<project-root>/
|
||||
├── 00_admin/ # Canonical state, manifests, ledgers, checksums
|
||||
│ ├── project_state.json # Active POM state (source of truth)
|
||||
│ ├── artifact_manifest.json
|
||||
│ ├── artifact_events.jsonl
|
||||
│ ├── SHA256SUMS.txt
|
||||
│ ├── validation/ # Gate reports (stage-N-scope-stamp.json)
|
||||
│ ├── hive/ # Hive mission records
|
||||
│ └── swarms/ # Optional Swarm DAG definitions
|
||||
├── 01_planning/ # Stage 0-3 outputs
|
||||
│ ├── brief.md
|
||||
│ ├── genre.md
|
||||
│ ├── voice.md
|
||||
│ └── story_store.md
|
||||
├── 02_story_bible/ # Stage 4 outputs
|
||||
│ ├── canon.md
|
||||
│ ├── cast.md
|
||||
│ ├── world.md
|
||||
│ ├── timeline.md
|
||||
│ ├── continuity.md
|
||||
│ └── visual_bible/
|
||||
├── 03_manuscript/ # Stage 5-7 outputs
|
||||
│ ├── outline.md
|
||||
│ ├── scenes/
|
||||
│ └── chapters/
|
||||
├── 04_images/ # Visual assets
|
||||
│ ├── references/
|
||||
│ ├── generated/
|
||||
│ ├── briefs/
|
||||
│ └── approvals/
|
||||
├── 05_research/ # Source notes and rights
|
||||
│ ├── sources.md
|
||||
│ ├── notes.md
|
||||
│ ├── fact_invention.md
|
||||
│ └── rights.md
|
||||
├── 06_ledgers/ # Decisions, revisions, continuity, quality, validation
|
||||
│ ├── decisions.jsonl
|
||||
│ ├── assumptions.jsonl
|
||||
│ ├── revisions.jsonl
|
||||
│ ├── continuity.jsonl
|
||||
│ ├── quality.jsonl
|
||||
│ └── validation.jsonl
|
||||
├── 07_exports/ # Checkpoint and final archives
|
||||
│ ├── <slug>-checkpoint-<stamp>.zip
|
||||
│ ├── <slug>-final-<stamp>.zip
|
||||
│ └── <base>.verification.json
|
||||
├── content/ # Obsidian/Quartz knowledge projection
|
||||
│ ├── index.md
|
||||
│ ├── canon/
|
||||
│ ├── cast/
|
||||
│ ├── timeline/
|
||||
│ ├── manuscript/
|
||||
│ └── research/
|
||||
└── .omp/ # Host and POM project configuration
|
||||
├── config.yml # OMP host settings (theme, task concurrency, etc.)
|
||||
├── pom.json # POM project settings
|
||||
└── prompts/ # Project-local prompt overrides
|
||||
```
|
||||
|
||||
## Vault content conventions
|
||||
|
||||
| Directory | Format | Purpose |
|
||||
|---|---|---|
|
||||
| `content/` | Markdown + YAML frontmatter + wikilinks | Human-navigable knowledge projection |
|
||||
| `00_admin/` | JSON/JSONL | Machine-readable canonical state and evidence |
|
||||
| `01_planning`–`05_research` | Markdown | Author-facing creative artifacts |
|
||||
| `06_ledgers` | JSONL | Append-only decision/revision/continuity logs |
|
||||
| `07_exports` | ZIP + JSON | Verified delivery archives with sidecars |
|
||||
| `.omp/` | YAML/JSON | Project-scoped configuration |
|
||||
|
||||
## Quartz compatibility
|
||||
|
||||
- `content/` is a valid Quartz 5 content vault
|
||||
- Frontmatter uses `title`, `tags`, `aliases`, `date`, `modified`
|
||||
- Wikilinks use `[[page]]` and `[[page|alias]]` syntax
|
||||
- Callouts use `> [!note]`, `> [!warning]`, `> [!important]`
|
||||
- Maps of content via `dataview` or manual index pages
|
||||
- `quartz.config.ts` for the project is generated from `config/config.yml` template
|
||||
|
||||
## Source of truth hierarchy
|
||||
|
||||
1. Project filesystem (`00_admin/project_state.json`, artifact files, ledgers)
|
||||
2. `content/` vault (navigable projection, derived from #1)
|
||||
3. OMP session state (append-only, never canonical)
|
||||
4. Memory (Mnemopi) — ephemeral, never canonical
|
||||
|
||||
## Regeneration
|
||||
|
||||
The vault is **not** regenerated from source on every change. It is incrementally updated by `src/persistence.ts` when artifacts are registered or state changes. To fully rebuild: `rm -rf content && /pom vault` (re-projects from canonical state).
|
||||
156
docs/VERIFICATION.md
Normal file
156
docs/VERIFICATION.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: Documentation verification
|
||||
description: The six documentation-contract checks in scripts/check-docs.mjs and how to prove they fail
|
||||
---
|
||||
|
||||
# Documentation verification
|
||||
|
||||
`scripts/check-docs.mjs` is the drift detector for facts that are mirrored out of source into
|
||||
Markdown, `POM.yml`, `package.json`, and `RELEASE.json`. Every fact POM documents has exactly one
|
||||
canonical owner in `src/`, `scripts/`, or `package.json`; the checker fails when a mirror stops
|
||||
matching its owner.
|
||||
|
||||
The checker is read-only, self-contained ESM, and imports only `node:fs`, `node:path`, and
|
||||
`node:url`. It never writes files, never runs a build, and needs no `node_modules`, so it stays
|
||||
runnable while the npm registry is unreachable.
|
||||
|
||||
## Running the checker
|
||||
|
||||
```bash
|
||||
node scripts/check-docs.mjs
|
||||
```
|
||||
|
||||
Each check prints one line, with actionable detail indented beneath a failure:
|
||||
|
||||
```text
|
||||
PASS check 1/6 resource-package-parity
|
||||
PASS check 2/6 command-parity
|
||||
note: /pom resume is absent from src/command.ts HELP; allowlisted in KNOWN_COMMAND_DIVERGENCES
|
||||
FAIL check 3/6 layout-contract
|
||||
docs/INVENTORY.md:43 names "06_exports", which src/persistence.ts bootstrapProject never creates; the canonical names are 06_ledgers and 07_exports
|
||||
POM docs verification FAIL (1/6: layout-contract)
|
||||
```
|
||||
|
||||
The final line is `POM docs verification PASS` with exit code 0, or
|
||||
`POM docs verification FAIL (<count>/6: <check names>)` with exit code 1. All six checks always
|
||||
run; one failure never hides another.
|
||||
|
||||
## The six checks
|
||||
|
||||
| # | Name | Canonical owner | Mirrors checked |
|
||||
|---|---|---|---|
|
||||
| 1 | `resource-package-parity` | `src/prompts.ts`, `src/themes.ts`, `src/events.ts` | `package.json` `files` |
|
||||
| 2 | `command-parity` | `src/command.ts` `HELP` | `README.md` command map, `POM.yml` `commands:` |
|
||||
| 3 | `layout-contract` | `src/persistence.ts` `bootstrapProject` | `docs/INVENTORY.md`, `skills/pom/references/artifacts.md` |
|
||||
| 4 | `prompt-order-contract` | `src/prompts.ts` `composePromptStack` and `renderStagePrompt` | `POM.yml` `prompt_precedence` |
|
||||
| 5 | `link-and-orphan` | the `docs/` tree on disk | every `docs/*.md` page |
|
||||
| 6 | `release-evidence` | `scripts/release-manifest.mjs` gate readers | `RELEASE.json` `verification` |
|
||||
|
||||
### 1. resource-package-parity
|
||||
|
||||
Defends the published tarball against missing runtime resources. The checker regex-matches
|
||||
`new URL("../<root>", import.meta.url)` in the three resource-loading modules, then asserts every
|
||||
discovered root is listed in `package.json` `files` and exists in the repository. The root list is
|
||||
derived from source on every run, so a newly bundled resource directory cannot be added in code and
|
||||
silently left out of the package.
|
||||
|
||||
Historical defect: `prompts` was loaded by `src/prompts.ts` and `src/events.ts` but omitted from
|
||||
`package.json` `files`, so an installed copy of the extension shipped without any prompt fragments
|
||||
while the repository checkout worked. A hardcoded root list would not have caught it, and would
|
||||
rot the next time a root is added, so discovery failure is itself a failure: if the regex matches
|
||||
nothing the check reports a broken checker rather than an empty pass.
|
||||
|
||||
### 2. command-parity
|
||||
|
||||
Defends the advertised command surface. The verbs are extracted from the `HELP` template literal in
|
||||
`src/command.ts`, from the `## Command map` table in `README.md`, and from the `commands:` block in
|
||||
`POM.yml`. A bare `/pom` normalizes to the verb `dashboard`, and only the first token after `/pom`
|
||||
is significant, so `/pom theme install` contributes `theme`. The three sets must agree.
|
||||
|
||||
Historical defect: `/pom settings` and `/pom doctor` were implemented and advertised by `HELP` but
|
||||
missing from both documented command maps, so operators had no written record of two shipped verbs.
|
||||
|
||||
### 3. layout-contract
|
||||
|
||||
Defends the generated project layout. `06_exports` and `06_logs` are forbidden in
|
||||
`docs/INVENTORY.md` and `skills/pom/references/artifacts.md`, and the canonical `06_ledgers` and
|
||||
`07_exports` must both appear in `docs/INVENTORY.md`. Failures cite the offending line numbers.
|
||||
|
||||
Historical defect: docs described the export directory as `06_exports` and invented a `06_logs`
|
||||
directory. `bootstrapProject` creates neither, so operators looked for output in paths that never
|
||||
exist and scripted against directory names the kernel never writes.
|
||||
|
||||
### 4. prompt-order-contract
|
||||
|
||||
Defends prompt provenance ordering. `POM.yml` `prompt_precedence` must open with the immutable POM
|
||||
production law that `composePromptStack` prepends, must state that project-local fragments replace
|
||||
bundled fragments by matching ID, and must close with the executable stage contract. The stage
|
||||
contract must appear only as the final entry.
|
||||
|
||||
Historical defect: the manifest listed the executable stage contract mid-list, contradicting
|
||||
`renderStagePrompt`, which appends it last. Anyone reasoning about prompt precedence from the
|
||||
manifest inferred the wrong effective law, since a later fragment overrides an earlier one.
|
||||
|
||||
### 5. link-and-orphan
|
||||
|
||||
Defends wiki navigability in two independent directions, reported separately. Every relative
|
||||
Markdown link in `docs/*.md` must resolve to a path that exists on disk, with fragments and query
|
||||
strings stripped and absolute, anchor-only, and scheme-qualified links skipped. Separately, every
|
||||
`docs/*.md` page except `index.md` must be linked from `docs/index.md` or `docs/navigation.md`.
|
||||
|
||||
Historical defect: navigation entries used repository-root paths such as `README.md` and
|
||||
`skills/pom/references/commands.md` from inside `docs/`, where they resolve to `docs/README.md` and
|
||||
`docs/skills/...` and render as dead links. New pages were also added without a navigation entry,
|
||||
leaving them reachable only by direct URL. The reachability half applies to this page too: a new
|
||||
`docs/*.md` file, including `VERIFICATION.md`, must be linked from `docs/index.md` or
|
||||
`docs/navigation.md` or the check reports it as unreachable.
|
||||
|
||||
### 6. release-evidence
|
||||
|
||||
Defends release honesty. Every key under `RELEASE.json` `verification` must be an object carrying a
|
||||
`status` string of `PASS`, `FAIL`, `NOT RUN`, or `BLOCKED`. A bare string, number, null, or array
|
||||
value fails, and so does an unrecognized status. `scripts/release-manifest.mjs` produces these
|
||||
objects from evidence files under `00_admin/validation/`; this check is the regression guard that
|
||||
keeps them evidence-bound.
|
||||
|
||||
Historical defect: the manifest carried hard-coded `"PASS"` strings for gates that had never been
|
||||
executed, so the release claimed verification it could not support.
|
||||
|
||||
## Known divergences
|
||||
|
||||
`KNOWN_COMMAND_DIVERGENCES` in `scripts/check-docs.mjs` is the only escape hatch, and it contains
|
||||
exactly one entry: `resume`. `/pom resume [state-path]` is documented in the `README.md` command map
|
||||
and in the `POM.yml` `commands:` block but is deliberately absent from `HELP`. `HELP` stays
|
||||
canonical for what the built-in help advertises, the docs keep the verb, and check 2 prints the
|
||||
divergence as a `note:` line so it stays visible while the check still passes deterministically. Any
|
||||
divergence not in the allowlist fails.
|
||||
|
||||
## Mutation proofs
|
||||
|
||||
Each row is a specified procedure for proving that a check actually defends its contract: apply the
|
||||
mutation, run `node scripts/check-docs.mjs`, confirm the named check reports `FAIL` with the
|
||||
expected message, then revert the mutation and confirm the run returns to
|
||||
`POM docs verification PASS`. Mutations are destructive edits to canonical files and must be
|
||||
reverted before any commit or package step. This section specifies the procedure and the expected
|
||||
output; it records no executed results, because the checker was not run when this page was written.
|
||||
|
||||
| Check | Mutation to apply | Expected failure |
|
||||
|---|---|---|
|
||||
| 1 `resource-package-parity` | Remove `"prompts"` from the `files` array in `package.json` | `FAIL check 1/6 resource-package-parity` — `package.json: files must contain "prompts" because src/prompts.ts and src/events.ts load it at runtime` |
|
||||
| 2 `command-parity` | Delete the `/pom status [--json]` row from the `## Command map` table in `README.md` | `FAIL check 2/6 command-parity` — `/pom status is documented in src/command.ts HELP, POM.yml commands: but absent from README.md ## Command map` |
|
||||
| 3 `layout-contract` | Rename `06_ledgers` to `06_logs` on the ledger line of `skills/pom/references/artifacts.md` | `FAIL check 3/6 layout-contract` — `skills/pom/references/artifacts.md:10 names "06_logs", which src/persistence.ts bootstrapProject never creates` |
|
||||
| 4 `prompt-order-contract` | In `POM.yml` `prompt_precedence`, move the executable stage contract entry above the project-local fragment entry so it is no longer last | `FAIL check 4/6 prompt-order-contract` — `POM.yml: prompt_precedence must end with the executable stage contract appended by renderStagePrompt`, plus the misplaced-entry position |
|
||||
| 5 `link-and-orphan` | In `docs/index.md`, change the Package inventory link target from `INVENTORY.md` to `INVENTORY-old.md` | `FAIL check 5/6 link-and-orphan` — `docs/index.md: broken relative link INVENTORY-old.md resolves to docs/INVENTORY-old.md, which does not exist on disk`, and `docs/INVENTORY.md: unreachable page` if `docs/navigation.md` no longer links it either |
|
||||
| 6 `release-evidence` | Replace the `verification.runtimeSmoke` object in `RELEASE.json` with the bare string `"PASS"` | `FAIL check 6/6 release-evidence` — `RELEASE.json: verification.runtimeSmoke is the bare string "PASS"; every gate must be an evidence object carrying a status` |
|
||||
|
||||
Row 5 mutates a link rather than deleting a file, so the mutation is a one-token edit and the revert
|
||||
is exact. Row 6 must be reverted by regenerating the manifest with `npm run release:manifest` rather
|
||||
than by hand, so the recorded hashes stay consistent with the packaged files.
|
||||
|
||||
## Relationship to the other gates
|
||||
|
||||
`node scripts/check-docs.mjs` is a documentation-contract gate, not a replacement for the release
|
||||
ladder in `package.json`. It complements `node scripts/verify.mjs`, which checks structure and
|
||||
counts, and `npm run release:check`, which proves `RELEASE.json` matches the packaged tree. The
|
||||
documentation checker is the only gate that reads prose, so it is the only one that can catch a doc
|
||||
that quietly contradicts source.
|
||||
69
docs/index.md
Normal file
69
docs/index.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# POM 2 Documentation
|
||||
|
||||
Welcome to the POM 2 documentation. This site is the single source of truth for operators, contributors, and architects.
|
||||
|
||||
## Quick navigation
|
||||
|
||||
| Audience | Entry points |
|
||||
|---|---|
|
||||
| Operator | [Install & verify](../README.md#install-and-verify) · [Production loop](../README.md#production-loop) · [Command map](../README.md#command-map) · [Settings](SETTINGS.md) |
|
||||
| Contributor | [AGENTS.md](../AGENTS.md) · [Architecture](ARCHITECTURE.md) · [Tool Matrix](TOOL_MATRIX.md) · [Development](../README.md#development) |
|
||||
| Architect | [Architecture](ARCHITECTURE.md) · [POM.yml](../POM.yml) · [Ownership](OWNERSHIP.md) · [Boundary](BOUNDARY.md) |
|
||||
| Release / Audit | [Inventory](INVENTORY.md) · [Audit](AUDIT.md) · [Design review](DESIGN_REVIEW.md) · [Official sources](OFFICIAL_SOURCES.md) · [Verification](VERIFICATION.md) |
|
||||
| Project vault | [Vault shape](VAULT.md) · [Repository vs vault boundary](BOUNDARY.md) |
|
||||
| Agent / Quick-ref | [Commands](../skills/pom/references/commands.md) · [Workflow](../skills/pom/references/workflow.md) · [Artifacts](../skills/pom/references/artifacts.md) · [Tool routing](../skills/pom/references/tool-routing.md) · [QA](../skills/pom/references/qa.md) · [UI](../skills/pom/references/ui.md) |
|
||||
|
||||
Full sidebar: [navigation](navigation.md).
|
||||
|
||||
## What is POM 2?
|
||||
|
||||
POM 2 is an Oh My Pi (OMP) extension that governs long-form creative production with evidence-gated stages, canonical artifacts, prompt provenance, deterministic validation, Git checkpoints, and an Obsidian/Quartz-compatible knowledge base.
|
||||
|
||||
- Package: `@jamminrebel/pom-omp` 2.0.0 (MIT, ESM)
|
||||
- Peer requirements: `@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-utils` `>=16.4.6 <17`
|
||||
- Host runtime: Bun `>=1.3.14`
|
||||
- Local scripts: Node 22+ / npm / tsx / tsc
|
||||
|
||||
## Repository structure
|
||||
|
||||
```
|
||||
pom-omp/
|
||||
├── src/ # 25 TypeScript modules (no build step)
|
||||
├── tests/ # 8 test files, Node test runner
|
||||
├── scripts/ # verify.mjs, check-docs.mjs, release-manifest.mjs, smoke.ts
|
||||
├── docs/ # This documentation
|
||||
├── agents/ # 14 pom-* task agent definitions
|
||||
├── prompts/ # 13 prompt fragments (bundled + project overrides)
|
||||
├── rules/ # 3 scoped production rules
|
||||
├── skills/pom/ # POM skill + 9 reference documents
|
||||
├── themes/ # pom-nocturne (dark), pom-parchment (light)
|
||||
├── config/ # OMP host config, POM defaults, MCP example
|
||||
├── README.md # Operator landing page
|
||||
├── POM.yml # Human-facing project manifest
|
||||
├── AGENTS.md # Contributor guide + discrepancy ledger
|
||||
├── CHANGELOG.md # Release history
|
||||
├── RELEASE.json # Generated release manifest
|
||||
├── package.json # Package metadata + scripts
|
||||
└── tsconfig.json # Strict TS config (src/**/*.ts only)
|
||||
```
|
||||
|
||||
## Documentation principles
|
||||
|
||||
- Source beats docs. Every mirrored fact cites its canonical owner in `src/`, `scripts/`, or `package.json`. Narrative docs must not introduce independent variants. See [ownership](OWNERSHIP.md).
|
||||
- Audience separation. Operator flows in README; deep reference in `docs/`; agent-facing quick refs in `skills/pom/references/`; human manifest in `POM.yml`.
|
||||
- Release honesty. `RELEASE.json` verification fields are evidence-bound objects carrying a status of PASS, FAIL, NOT RUN, or BLOCKED. Hard-coded PASS strings are prohibited and rejected by [the documentation checks](VERIFICATION.md).
|
||||
- Generated versus repository. This wiki describes the extension. Generated project vaults live under a project's `content/` and are a separate branch. See [boundary](BOUNDARY.md) and [vault shape](VAULT.md).
|
||||
|
||||
## Verification status
|
||||
|
||||
Overall readiness is PARTIAL. `npm run smoke`, `npm run release:check`, and the state test pass; the full `npm run verify` gate is blocked by an npm registry outage that left `node_modules` incomplete. See [verification](VERIFICATION.md) and the README verification section for exact detail.
|
||||
|
||||
## Contributing
|
||||
|
||||
Read [AGENTS.md](../AGENTS.md) for code conventions, testing patterns, and governance invariants. Key rules:
|
||||
|
||||
- No `dist/` or generated caches in source
|
||||
- Extensionless relative imports (`./state`, not `./state.ts`)
|
||||
- Throw `Error` for precondition/schema/config violations; tool failures use `isError: true`
|
||||
- Three runtime dependencies only: `crc-32`, `yauzl`, `yazl`
|
||||
- Run `npm run verify` before packaging or linking
|
||||
60
docs/navigation.md
Normal file
60
docs/navigation.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: POM 2 Documentation
|
||||
description: Single source of truth for POM 2 operators, contributors, and architects
|
||||
---
|
||||
|
||||
# Navigation
|
||||
|
||||
This file defines the sidebar structure for the Quartz-rendered wiki. Landing page: [index](index.md).
|
||||
|
||||
## Operator
|
||||
|
||||
- [Install & verify](../README.md#install-and-verify)
|
||||
- [Production loop](../README.md#production-loop)
|
||||
- [Command map](../README.md#command-map)
|
||||
- [Settings](SETTINGS.md)
|
||||
|
||||
## Contributor
|
||||
|
||||
- [Repository guidelines](../AGENTS.md)
|
||||
- [Architecture](ARCHITECTURE.md)
|
||||
- [Tool matrix](TOOL_MATRIX.md)
|
||||
- [Development](../README.md#development)
|
||||
|
||||
## Architect
|
||||
|
||||
- [Architecture](ARCHITECTURE.md)
|
||||
- [Project manifest](../POM.yml)
|
||||
- [Ownership table](OWNERSHIP.md)
|
||||
- [Repository vs vault boundary](BOUNDARY.md)
|
||||
|
||||
## Release / Audit
|
||||
|
||||
- [Package inventory](INVENTORY.md)
|
||||
- [Audit log](AUDIT.md)
|
||||
- [Design review](DESIGN_REVIEW.md)
|
||||
- [Official sources](OFFICIAL_SOURCES.md)
|
||||
- [Verification and mutation proofs](VERIFICATION.md)
|
||||
|
||||
## Project vault
|
||||
|
||||
- [Generated vault shape](VAULT.md)
|
||||
- [Repository vs vault boundary](BOUNDARY.md)
|
||||
|
||||
## Agent / Quick reference
|
||||
|
||||
- [Commands](../skills/pom/references/commands.md)
|
||||
- [Workflow](../skills/pom/references/workflow.md)
|
||||
- [Artifacts](../skills/pom/references/artifacts.md)
|
||||
- [Agent studios](../skills/pom/references/agent-studios.md)
|
||||
- [Todo blueprints](../skills/pom/references/todo-blueprints.md)
|
||||
- [Tool routing](../skills/pom/references/tool-routing.md)
|
||||
- [QA](../skills/pom/references/qa.md)
|
||||
- [UI](../skills/pom/references/ui.md)
|
||||
|
||||
## Meta
|
||||
|
||||
- [Documentation index](index.md)
|
||||
- [This navigation file](navigation.md)
|
||||
- [Documentation principles](index.md#documentation-principles)
|
||||
- [Verification status](index.md#verification-status)
|
||||
4963
package-lock.json
generated
Normal file
4963
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
87
package.json
Normal file
87
package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "@jamminrebel/pom-omp",
|
||||
"version": "2.0.0",
|
||||
"description": "POM 2 \u2014 an OMP-native production kernel for evidence-gated creative work, agent swarms, and Obsidian/Quartz knowledge bases",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://omp.loca.zone/pom-omp.git"
|
||||
},
|
||||
"homepage": "https://wiki.omp.loca.zone/",
|
||||
"bugs": {
|
||||
"url": "https://wiki.omp.loca.zone/",
|
||||
"email": "antigravity@loca.zone"
|
||||
},
|
||||
"keywords": [
|
||||
"omp",
|
||||
"oh-my-pi",
|
||||
"production",
|
||||
"fiction",
|
||||
"writing",
|
||||
"agents",
|
||||
"swarm",
|
||||
"irc",
|
||||
"obsidian",
|
||||
"quartz",
|
||||
"knowledge-base",
|
||||
"tui"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"skills",
|
||||
"agents",
|
||||
"rules",
|
||||
"themes",
|
||||
"config",
|
||||
"docs",
|
||||
"prompts",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"POM.yml",
|
||||
"AGENTS.md",
|
||||
"LICENSE",
|
||||
"scripts",
|
||||
"RELEASE.json"
|
||||
],
|
||||
"omp": {
|
||||
"extensions": [
|
||||
"./src/index.ts"
|
||||
]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@oh-my-pi/pi-coding-agent": ">=16.4.6 <17",
|
||||
"@oh-my-pi/pi-tui": ">=16.4.6 <17",
|
||||
"@oh-my-pi/pi-utils": ">=16.4.6 <17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@oh-my-pi/pi-coding-agent": "^16.4.6",
|
||||
"@oh-my-pi/pi-tui": "^16.4.6",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/yauzl": "^3.4.0",
|
||||
"@types/yazl": "^3.3.1",
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "^5.8.0",
|
||||
"@oh-my-pi/pi-utils": "^16.4.6"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.3.14"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"test": "node --import tsx --test tests/*.test.ts",
|
||||
"check:docs": "node scripts/check-docs.mjs",
|
||||
"verify": "npm run check && npm test && npm run smoke && node scripts/verify.mjs && npm run check:docs && npm run release:check && npm run pack:dry",
|
||||
"link": "omp plugin link .",
|
||||
"doctor": "omp plugin doctor",
|
||||
"smoke": "node --import tsx scripts/smoke.ts",
|
||||
"pack:dry": "npm pack --dry-run",
|
||||
"release:manifest": "node scripts/release-manifest.mjs",
|
||||
"release:check": "node scripts/release-manifest.mjs --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.2",
|
||||
"yauzl": "^3.4.0",
|
||||
"yazl": "^3.3.1"
|
||||
}
|
||||
}
|
||||
5
prompts/agent-studio.md
Normal file
5
prompts/agent-studio.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Build one task batch using curated POM agents.
|
||||
Shared context must contain Goal, Constraints, Contract.
|
||||
Each assignment must contain Target, Change, Acceptance.
|
||||
Use stable unique agent names, no generic workers, no duplicated context, no project-wide formatters/tests, and no serialization unless a true dependency exists.
|
||||
Use IRC for overlap, missing files, and follow-up. Use agent:// for full output.
|
||||
5
prompts/chapter-cycle.md
Normal file
5
prompts/chapter-cycle.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Execute one complete POM chapter cycle.
|
||||
Preflight: voice, outline, scene index, timeline, character states, knowledge, relationships, props, injuries, mysteries, setups, rules, sensory palette, prior ending.
|
||||
Draft: complete scenes, selected POV/tense, subtext, spatial clarity, emotional movement, selective sensory detail, changed exit state.
|
||||
Postflight: measured words, outline compliance, voice, continuity, repetition, placeholders, canon delta, timeline delta, relationship/knowledge/prop/injury changes, setup/payoff updates, artifact registration.
|
||||
If incomplete, stop at a scene boundary and mark PARTIAL with the exact remainder.
|
||||
2
prompts/continuity-audit.md
Normal file
2
prompts/continuity-audit.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Run a POM continuity audit across timeline, character state, knowledge, relationships, locations, objects, injuries, factions, world rules, terminology, mysteries, clues, setups, payoffs, promises, motifs, and chapter order.
|
||||
For every contradiction: show both facts, select canon, list affected files, propose the smallest high-quality repair, log the revision, and rerun dependent checks.
|
||||
3
prompts/delivery-audit.md
Normal file
3
prompts/delivery-audit.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Run POM final delivery.
|
||||
Compile only approved components. Verify required directories/files, chapter count/order, no partials, no placeholders, no duplicates, terminology, POV/tense, ending, arcs, mysteries, payoffs, timeline, travel, knowledge, relationships, injuries, props, rules, image paths/alt/briefs/rights, factual support, portable names, manifest bytes/hashes, SHA256SUMS, and requested exports.
|
||||
Create a fresh ZIP, test every member by decompression and CRC, compute ZIP SHA-256, write the delivery report, disclose every warning, and provide only real paths.
|
||||
3
prompts/full-autopilot.md
Normal file
3
prompts/full-autopilot.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Run POM Tasks 0–8 sequentially.
|
||||
For each stage: load canonical state, sync native todos, apply stage tool/model profile, run one curated task batch, integrate canonical artifacts, validate deterministically, fix defects, pass or block explicitly, then continue only after PASS.
|
||||
Never merge stages, skip artifacts, invent metrics, or conceal omissions.
|
||||
6
prompts/personality-autopilot.md
Normal file
6
prompts/personality-autopilot.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: POM decisive showrunner personality for autonomous production
|
||||
---
|
||||
# Personality · Decisive Showrunner
|
||||
|
||||
Operate as a calm, exacting showrunner. Decide routine production details autonomously, expose consequential assumptions, prefer completed artifacts over discussion, and preserve a single coherent creative direction. Delegate independent work aggressively, but synthesize every result against canon and acceptance evidence before adopting it.
|
||||
6
prompts/personality-collaborative.md
Normal file
6
prompts/personality-collaborative.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: POM collaborative creative-director personality
|
||||
---
|
||||
# Personality · Collaborative Creative Director
|
||||
|
||||
Operate as an inventive creative director. Present meaningful forks when taste or irreversible direction is involved, keep routine execution autonomous, and translate specialist disagreement into a small number of crisp options. Never substitute consensus for evidence or ask the operator to decide implementation trivia.
|
||||
6
prompts/personality-strict-autopilot.md
Normal file
6
prompts/personality-strict-autopilot.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: POM high-discipline production-controller personality
|
||||
---
|
||||
# Personality · Production Controller
|
||||
|
||||
Operate as a highly autonomous production controller. Continue until the active contract passes or a concrete blocker is recorded. Treat unsupported claims, stale canon, unregistered artifacts, open required todos, and unverified archives as failures. Prefer surgical repair, deterministic checks, and explicit provenance over conversational reassurance.
|
||||
9
prompts/repair.md
Normal file
9
prompts/repair.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Repair the named POM defect surgically.
|
||||
- identify the canonical conflict
|
||||
- map affected artifacts and dependencies
|
||||
- choose the smallest high-quality correction
|
||||
- apply grounded edits only
|
||||
- update revision and continuity logs
|
||||
- update manifest and versions when material
|
||||
- rerun the exact dependent validators
|
||||
- report evidence, not confidence language
|
||||
10
prompts/research-sprint.md
Normal file
10
prompts/research-sprint.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Run a POM research sprint.
|
||||
- define the smallest accuracy-critical question set
|
||||
- search primary and authoritative sources first
|
||||
- use read for static URLs and PDFs; browser only when interaction is required
|
||||
- corroborate consequential claims
|
||||
- compare publication date and event date
|
||||
- separate fact, inference, dispute, and deliberate invention
|
||||
- capture source, publisher, access date, supported claim, and story treatment
|
||||
- write only reusable findings into canonical research files
|
||||
- return unresolved uncertainty explicitly
|
||||
41
prompts/stage-run.md
Normal file
41
prompts/stage-run.md
Normal file
@@ -0,0 +1,41 @@
|
||||
Execute POM Task {{STAGE_ID}} — {{STAGE_NAME}} for {{PROJECT_TITLE}}.
|
||||
|
||||
Read first:
|
||||
- skill://pom
|
||||
- {{PROJECT_ROOT}}/00_admin/project_state.json
|
||||
- only canonical files relevant to this stage
|
||||
|
||||
Operating contract:
|
||||
- initialize the exact stage todo blueprint with the built-in todo tool
|
||||
- use tool profile {{TOOL_PROFILE}}
|
||||
- use model role {{MODEL_ROLE}} when available
|
||||
- parallelize independent specialist work through one task batch
|
||||
- preferred agents: {{AGENTS}}
|
||||
- read files and static URLs with read; use browser only for interaction/authentication
|
||||
- use LSP for symbol-aware code work, AST tools for structural code work, edit for grounded local edits, eval for multi-step computation, and bash only for short binary calls
|
||||
- use generate_image only from a complete visual brief; inspect every generated asset
|
||||
- canonical files override memory and chat summaries
|
||||
- update state, canon, ledgers, manifest, and logs through POM tools
|
||||
- never claim an artifact, count, citation, validation, or completion without evidence
|
||||
- do not call pom_stage pass before pom_validate succeeds
|
||||
- on failure, preserve completed work and call pom_stage block with the exact blocker
|
||||
|
||||
Required artifact targets:
|
||||
{{ARTIFACTS}}
|
||||
|
||||
Completion gate:
|
||||
{{GATE}}
|
||||
|
||||
Agent-studio contract:
|
||||
- use one native task batch with one shared context and complete assignments
|
||||
- use local:// for large shared context
|
||||
- use agent:// and history:// for outputs and follow-up
|
||||
- message existing agents through IRC instead of respawning them
|
||||
- do not poll jobs unless all useful parent work is blocked
|
||||
|
||||
Finish:
|
||||
1. run pom_validate
|
||||
2. fix material defects
|
||||
3. call pom_stage pass on PASS or pom_stage block on failure
|
||||
4. mark todos done immediately
|
||||
5. present the configured POM UI with only real files and one next action
|
||||
1
prompts/todo-sync.md
Normal file
1
prompts/todo-sync.md
Normal file
@@ -0,0 +1 @@
|
||||
Initialize the built-in todo tool with every POM stage blueprint item as its own stable 5–10 word task, grouped by phase and kept in sequence. Mark each done immediately after completion. Never advance the stage with open required tasks.
|
||||
3
prompts/visual-cycle.md
Normal file
3
prompts/visual-cycle.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Execute a POM visual cycle.
|
||||
Create a structured brief: purpose, stage, placement, subject, action, composition, environment, medium, style traits, lighting, palette, viewpoint, focus, identity/wardrobe/prop/environment anchors, text, typography, continuity tags, exclusions, references, aspect ratio, alt text.
|
||||
Generate or edit. Inspect subject accuracy, anatomy, object integrity, identity, continuity, composition, lighting, palette, text, logos, artifacts, crop, and placement. Composite important typography when generation text is unreliable. Save brief, prompt, final, alt text, metadata, and rights status.
|
||||
85
quartz.config.ts
Normal file
85
quartz.config.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineConfig } from "quartz/cfg"
|
||||
import * as Plugin from "quartz/plugins"
|
||||
|
||||
export default defineConfig({
|
||||
configuration: {
|
||||
pageTitle: "POM 2 Documentation",
|
||||
pageTitleSuffix: " — POM 2",
|
||||
enableSPA: true,
|
||||
enablePopovers: true,
|
||||
analytics: null,
|
||||
locale: "en-US",
|
||||
baseUrl: "pom-omp.loca.zone",
|
||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||
defaultDateType: "modified",
|
||||
theme: {
|
||||
typography: {
|
||||
header: "IBM Plex Sans",
|
||||
body: "IBM Plex Sans",
|
||||
code: "IBM Plex Mono",
|
||||
},
|
||||
colors: {
|
||||
lightMode: {
|
||||
light: "#fafafa",
|
||||
lightgray: "#e5e5e5",
|
||||
gray: "#888888",
|
||||
darkgray: "#444444",
|
||||
dark: "#1a1a1a",
|
||||
secondary: "#286090",
|
||||
tertiary: "#3a7bc8",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#fff23688",
|
||||
},
|
||||
darkMode: {
|
||||
light: "#1a1a2e",
|
||||
lightgray: "#2a2a3e",
|
||||
gray: "#6a6a7a",
|
||||
darkgray: "#aaaabb",
|
||||
dark: "#eaeaea",
|
||||
secondary: "#7ab3ff",
|
||||
tertiary: "#aac8ff",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#ffea0088",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
transformers: [
|
||||
Plugin.FrontMatter(),
|
||||
Plugin.CreatedModifiedDate({
|
||||
priority: ["frontmatter", "git", "filesystem"],
|
||||
}),
|
||||
Plugin.SyntaxHighlighting({
|
||||
theme: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
keepBackground: true,
|
||||
}),
|
||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||
Plugin.GitHubFlavoredMarkdown(),
|
||||
Plugin.TableOfContents(),
|
||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||
Plugin.Description(),
|
||||
Plugin.Latex({ renderEngine: "katex" }),
|
||||
],
|
||||
filters: [Plugin.RemoveDrafts()],
|
||||
emitters: [
|
||||
Plugin.AliasRedirects(),
|
||||
Plugin.ComponentResources(),
|
||||
Plugin.ContentPage(),
|
||||
Plugin.FolderPage(),
|
||||
Plugin.TagPage(),
|
||||
Plugin.ContentIndex({
|
||||
enableSiteMap: true,
|
||||
enableRSS: false,
|
||||
}),
|
||||
Plugin.Assets(),
|
||||
Plugin.Static(),
|
||||
Plugin.Favicon(),
|
||||
Plugin.NotFoundPage(),
|
||||
Plugin.Sitemap(),
|
||||
],
|
||||
},
|
||||
})
|
||||
7
rules/pom-artifact-integrity.md
Normal file
7
rules/pom-artifact-integrity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: POM artifact claims require real files, measured metadata, manifest registration, and truthful completion status.
|
||||
globs:
|
||||
- "**/00_admin/**"
|
||||
- "**/07_exports/**"
|
||||
---
|
||||
Before claiming an artifact exists, verify the path. Before claiming validation, run the check. Before claiming a ZIP passes, test every member. Use PARTIAL or BLOCKED when appropriate.
|
||||
7
rules/pom-continuity.md
Normal file
7
rules/pom-continuity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: POM story changes must preserve or explicitly revise canon, timeline, knowledge, relationships, props, injuries, rules, mysteries, setups, and payoffs.
|
||||
globs:
|
||||
- "**/02_story_bible/**"
|
||||
- "**/03_manuscript/**"
|
||||
---
|
||||
Record only changed state after each chapter. Contradictions require explicit canon selection, dependency mapping, revision logging, and revalidation.
|
||||
8
rules/pom-rights.md
Normal file
8
rules/pom-rights.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: POM must avoid distinctive living-author imitation, fake quotations, unauthorized asset redistribution, invented private identity facts, and unsupported rights claims.
|
||||
globs:
|
||||
- "**/02_story_bible/01_author_persona.md"
|
||||
- "**/04_images/**"
|
||||
- "**/05_research/**"
|
||||
---
|
||||
Translate protected influences into high-level non-exclusive traits. Keep attribution and rights status with every third-party input and final visual.
|
||||
409
scripts/check-docs.mjs
Normal file
409
scripts/check-docs.mjs
Normal file
@@ -0,0 +1,409 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
|
||||
// `/pom resume [state-path]` is documented in the README command map and in the POM.yml
|
||||
// `commands:` block, but it is deliberately absent from the HELP template literal in
|
||||
// src/command.ts. HELP is canonical for what the built-in help advertises, so the verb is
|
||||
// neither removed from the docs nor added to HELP. It is recorded here instead, so the
|
||||
// divergence stays visible and the parity check still passes deterministically.
|
||||
// Any command divergence that is not listed here is a failure.
|
||||
const KNOWN_COMMAND_DIVERGENCES = new Set(["resume"]);
|
||||
|
||||
// Roots that src/*.ts loads through `new URL("../<root>", import.meta.url)`. Check 1 derives
|
||||
// this set from source instead of hardcoding it, so a new bundled-resource root cannot be
|
||||
// added in code and silently omitted from the published package.
|
||||
const RESOURCE_SOURCES = ["src/prompts.ts", "src/themes.ts", "src/events.ts"];
|
||||
const BUNDLED_ROOT = /new URL\(\s*["'`]\.\.\/([A-Za-z0-9._-]+)/g;
|
||||
|
||||
// Generated-layout names that drifted in earlier revisions and must never come back.
|
||||
const FORBIDDEN_LAYOUT_NAMES = ["06_exports", "06_logs"];
|
||||
const REQUIRED_LAYOUT_NAMES = ["06_ledgers", "07_exports"];
|
||||
const LAYOUT_DOCS = ["docs/INVENTORY.md", "skills/pom/references/artifacts.md"];
|
||||
|
||||
const VERIFICATION_STATUSES = ["PASS", "FAIL", "NOT RUN", "BLOCKED"];
|
||||
|
||||
const MARKDOWN_LINK = /\[[^\]]*\]\(\s*([^)\s]+?)(?:\s+"[^"]*")?\s*\)/g;
|
||||
const NAVIGATION_PAGES = ["index.md", "navigation.md"];
|
||||
|
||||
// Canonical project coordinates. These live on infrastructure the maintainer owns
|
||||
// (loca.zone). There is no public forge mirror, so a plausible-looking third-party URL
|
||||
// must never be substituted, and the earlier `UNRESOLVED:` placeholders must never return.
|
||||
const METADATA_FIELDS = ["repository", "homepage", "bugs"];
|
||||
const CANONICAL_HOST = "loca.zone";
|
||||
const PLACEHOLDER_MARKER = "UNRESOLVED";
|
||||
|
||||
const TOTAL_CHECKS = 7;
|
||||
|
||||
const checks = [];
|
||||
const failed = [];
|
||||
|
||||
function text(rel) {
|
||||
return readFileSync(join(root, rel), "utf8");
|
||||
}
|
||||
|
||||
function lineNumbers(body, pattern) {
|
||||
return body
|
||||
.split("\n")
|
||||
.map((line, index) => (pattern.test(line) ? index + 1 : 0))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Blanks fenced blocks and inline code spans so illustrative links are not link-checked. */
|
||||
function withoutCode(body) {
|
||||
const blank = (matched) => matched.replace(/[^\n]/g, " ");
|
||||
return body.replace(/^```[\s\S]*?^```/gm, blank).replace(/`[^`\n]*`/g, blank);
|
||||
}
|
||||
|
||||
/** Reads the indented body of a top-level or nested block key, e.g. `commands:`. */
|
||||
function blockLines(rel, key) {
|
||||
const lines = text(rel).split("\n");
|
||||
const start = lines.findIndex((line) => new RegExp(`^\\s*${key}:\\s*$`).test(line));
|
||||
if (start < 0) return null;
|
||||
const indent = lines[start].length - lines[start].trimStart().length;
|
||||
const body = [];
|
||||
for (const line of lines.slice(start + 1)) {
|
||||
if (line.trim() === "") continue;
|
||||
if (line.length - line.trimStart().length <= indent) break;
|
||||
body.push(line.trim());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/** `/pom` -> `dashboard`; `/pom theme install` -> `theme`. */
|
||||
function verbOf(command) {
|
||||
const match = /^\/pom(?:\s+([a-z][a-z0-9-]*))?\b/.exec(command);
|
||||
if (!match) return null;
|
||||
return match[1] ?? "dashboard";
|
||||
}
|
||||
|
||||
function check(index, name, run) {
|
||||
const problems = [];
|
||||
const notes = [];
|
||||
try {
|
||||
run((message) => problems.push(message), (message) => notes.push(message));
|
||||
} catch (error) {
|
||||
problems.push(`unexpected error: ${error.message}`);
|
||||
}
|
||||
console.log(`${problems.length === 0 ? "PASS" : "FAIL"} check ${index}/${TOTAL_CHECKS} ${name}`);
|
||||
for (const problem of problems) console.log(` ${problem}`);
|
||||
for (const note of notes) console.log(` note: ${note}`);
|
||||
if (problems.length > 0) failed.push(name);
|
||||
}
|
||||
|
||||
// 1. Resource/package parity: every bundled-resource root loaded by source must ship.
|
||||
checks.push((index) =>
|
||||
check(index, "resource-package-parity", (fail) => {
|
||||
const roots = new Map();
|
||||
for (const rel of RESOURCE_SOURCES) {
|
||||
if (!existsSync(join(root, rel))) {
|
||||
fail(`${rel}: resource-loading module is missing; check 1 cannot derive bundled roots from source`);
|
||||
continue;
|
||||
}
|
||||
const body = text(rel);
|
||||
for (const match of body.matchAll(BUNDLED_ROOT)) {
|
||||
if (!roots.has(match[1])) roots.set(match[1], new Set());
|
||||
roots.get(match[1]).add(rel);
|
||||
}
|
||||
}
|
||||
if (roots.size === 0) {
|
||||
fail(
|
||||
`no bundled-resource roots discovered in ${RESOURCE_SOURCES.join(", ")}; the ` +
|
||||
`\`new URL("../<root>", import.meta.url)\` pattern in scripts/check-docs.mjs no longer ` +
|
||||
"matches source and must be repaired (this is a broken check, not an empty result)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const files = JSON.parse(text("package.json")).files;
|
||||
if (!Array.isArray(files)) {
|
||||
fail("package.json: `files` must be an array listing every published path");
|
||||
return;
|
||||
}
|
||||
for (const [name, sources] of [...roots].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
if (!files.includes(name))
|
||||
fail(`package.json: \`files\` must contain "${name}" because ${[...sources].join(" and ")} load it at runtime`);
|
||||
if (!existsSync(join(root, name)))
|
||||
fail(`${name}: directory is loaded by ${[...sources].join(" and ")} but does not exist in the repository`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 2. Command parity: HELP, the README command map, and POM.yml `commands:` must agree.
|
||||
checks.push((index) =>
|
||||
check(index, "command-parity", (fail, note) => {
|
||||
const command = text("src/command.ts");
|
||||
const help = /const HELP\s*=\s*`([\s\S]*?)`\s*;/.exec(command);
|
||||
if (!help) {
|
||||
fail("src/command.ts: could not locate the `const HELP = `...`;` template literal that defines the command surface");
|
||||
return;
|
||||
}
|
||||
const helpVerbs = new Set();
|
||||
for (const line of help[1].split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("/pom")) continue;
|
||||
const verb = verbOf(trimmed);
|
||||
if (verb) helpVerbs.add(verb);
|
||||
}
|
||||
|
||||
const readmeLines = text("README.md").split("\n");
|
||||
const heading = readmeLines.findIndex((line) => /^##\s+Command map\s*$/.test(line));
|
||||
if (heading < 0) {
|
||||
fail("README.md: missing the `## Command map` heading that check 2 reads the command table from");
|
||||
return;
|
||||
}
|
||||
const after = readmeLines.slice(heading + 1);
|
||||
const end = after.findIndex((line) => /^##\s/.test(line));
|
||||
const readmeVerbs = new Set();
|
||||
for (const line of end < 0 ? after : after.slice(0, end)) {
|
||||
if (!line.trimStart().startsWith("|")) continue;
|
||||
for (const span of line.match(/`[^`]+`/g) ?? []) {
|
||||
const verb = verbOf(span.slice(1, -1).trim());
|
||||
if (verb) readmeVerbs.add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
const commandBlock = blockLines("POM.yml", "commands");
|
||||
if (!commandBlock) {
|
||||
fail("POM.yml: missing the top-level `commands:` block that check 2 reads the command map from");
|
||||
return;
|
||||
}
|
||||
const manifestVerbs = new Set();
|
||||
for (const entry of commandBlock) {
|
||||
const value = /:\s*(.+)$/.exec(entry);
|
||||
if (!value) continue;
|
||||
const verb = verbOf(value[1].trim().replace(/^["']|["']$/g, ""));
|
||||
if (verb) manifestVerbs.add(verb);
|
||||
}
|
||||
|
||||
if (helpVerbs.size === 0) fail("src/command.ts: HELP advertises no `/pom` verbs");
|
||||
if (readmeVerbs.size === 0) fail("README.md: the `## Command map` table lists no `/pom` commands");
|
||||
if (manifestVerbs.size === 0) fail("POM.yml: the `commands:` block lists no `/pom` commands");
|
||||
|
||||
const sources = [
|
||||
["src/command.ts HELP", helpVerbs],
|
||||
["README.md `## Command map`", readmeVerbs],
|
||||
["POM.yml `commands:`", manifestVerbs],
|
||||
];
|
||||
const union = [...new Set(sources.flatMap(([, verbs]) => [...verbs]))].sort();
|
||||
for (const verb of union) {
|
||||
const missing = sources.filter(([, verbs]) => !verbs.has(verb)).map(([label]) => label);
|
||||
if (missing.length === 0) continue;
|
||||
if (KNOWN_COMMAND_DIVERGENCES.has(verb)) {
|
||||
note(`/pom ${verb} is absent from ${missing.join(", ")}; allowlisted in KNOWN_COMMAND_DIVERGENCES`);
|
||||
continue;
|
||||
}
|
||||
const present = sources.filter(([, verbs]) => verbs.has(verb)).map(([label]) => label);
|
||||
fail(
|
||||
`/pom ${verb} is documented in ${present.join(", ")} but absent from ${missing.join(", ")}; ` +
|
||||
"add it there or record it in KNOWN_COMMAND_DIVERGENCES in scripts/check-docs.mjs",
|
||||
);
|
||||
}
|
||||
for (const verb of KNOWN_COMMAND_DIVERGENCES) {
|
||||
if (!union.includes(verb))
|
||||
fail(
|
||||
`KNOWN_COMMAND_DIVERGENCES lists "${verb}" but no source documents /pom ${verb}; ` +
|
||||
"remove the stale allowlist entry from scripts/check-docs.mjs",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. Layout contract: the generated project tree names in docs must match src/persistence.ts.
|
||||
checks.push((index) =>
|
||||
check(index, "layout-contract", (fail) => {
|
||||
for (const rel of LAYOUT_DOCS) {
|
||||
if (!existsSync(join(root, rel))) {
|
||||
fail(`${rel}: layout documentation is missing`);
|
||||
continue;
|
||||
}
|
||||
const body = text(rel);
|
||||
for (const name of FORBIDDEN_LAYOUT_NAMES) {
|
||||
const lines = lineNumbers(body, new RegExp(`\\b${name}\\b`));
|
||||
if (lines.length > 0)
|
||||
fail(
|
||||
`${rel}:${lines.join(",")} names "${name}", which src/persistence.ts bootstrapProject never ` +
|
||||
`creates; the canonical names are ${REQUIRED_LAYOUT_NAMES.join(" and ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const inventory = text("docs/INVENTORY.md");
|
||||
for (const name of REQUIRED_LAYOUT_NAMES) {
|
||||
if (!new RegExp(`\\b${name}\\b`).test(inventory))
|
||||
fail(`docs/INVENTORY.md: must document the canonical generated directory "${name}"`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 4. Prompt-order contract: POM.yml must mirror the composition order in src/prompts.ts.
|
||||
checks.push((index) =>
|
||||
check(index, "prompt-order-contract", (fail) => {
|
||||
const items = blockLines("POM.yml", "prompt_precedence");
|
||||
if (!items) {
|
||||
fail("POM.yml: missing the `prompt_precedence:` list under `knowledge:`");
|
||||
return;
|
||||
}
|
||||
const entries = items.filter((item) => item.startsWith("- ")).map((item) => item.slice(2).trim());
|
||||
if (entries.length < 3) {
|
||||
fail(`POM.yml: \`prompt_precedence\` must list the full composition order, found ${entries.length} entries`);
|
||||
return;
|
||||
}
|
||||
const first = entries[0];
|
||||
const last = entries[entries.length - 1];
|
||||
if (!/immutable/i.test(first) || !/law/i.test(first))
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` entry 1 must name the immutable POM production law that ` +
|
||||
`composePromptStack prepends, found "${first}"`,
|
||||
);
|
||||
if (!/executable stage contract/i.test(last))
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` must end with the executable stage contract appended by ` +
|
||||
`renderStagePrompt, found "${last}" as the final entry`,
|
||||
);
|
||||
const misplaced = entries
|
||||
.slice(0, -1)
|
||||
.map((entry, position) => (/executable stage contract/i.test(entry) ? position + 1 : 0))
|
||||
.filter(Boolean);
|
||||
if (misplaced.length > 0)
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` mentions the executable stage contract at entry ` +
|
||||
`${misplaced.join(",")}; renderStagePrompt appends it last, so it must be the final entry only`,
|
||||
);
|
||||
if (!entries.some((entry) => /replac/i.test(entry) && /\bID\b/i.test(entry)))
|
||||
fail(
|
||||
"POM.yml: `prompt_precedence` must state that project-local fragments replace bundled fragments " +
|
||||
"by matching basename ID, as src/prompts.ts composePromptStack does",
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// 5. Link/orphan check: relative doc links must resolve, and every page must be reachable.
|
||||
checks.push((index) =>
|
||||
check(index, "link-and-orphan", (fail) => {
|
||||
const pages = readdirSync(join(root, "docs"))
|
||||
.filter((name) => name.endsWith(".md"))
|
||||
.sort();
|
||||
if (pages.length === 0) {
|
||||
fail("docs: contains no Markdown pages");
|
||||
return;
|
||||
}
|
||||
const reachable = new Set();
|
||||
for (const page of pages) {
|
||||
const rel = `docs/${page}`;
|
||||
const body = withoutCode(text(rel));
|
||||
for (const match of body.matchAll(MARKDOWN_LINK)) {
|
||||
const target = match[1];
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("/")) continue;
|
||||
const path = target.split("#")[0].split("?")[0];
|
||||
if (path === "") continue;
|
||||
const resolved = resolve(join(root, "docs"), path);
|
||||
if (NAVIGATION_PAGES.includes(page)) reachable.add(resolved);
|
||||
if (!existsSync(resolved))
|
||||
fail(
|
||||
`${rel}: broken relative link \`${target}\` resolves to ${relative(root, resolved)}, ` +
|
||||
"which does not exist on disk",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const page of pages) {
|
||||
if (page === "index.md") continue;
|
||||
if (reachable.has(resolve(join(root, "docs", page)))) continue;
|
||||
fail(
|
||||
`docs/${page}: unreachable page, no link from ${NAVIGATION_PAGES.map((name) => `docs/${name}`).join(" or ")}; ` +
|
||||
"add it to the navigation tables so the wiki has no orphans",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 6. Release-evidence freshness: verification gates must be evidence objects, never literals.
|
||||
checks.push((index) =>
|
||||
check(index, "release-evidence", (fail) => {
|
||||
const release = JSON.parse(text("RELEASE.json"));
|
||||
const verification = release.verification;
|
||||
if (verification === undefined || verification === null || typeof verification !== "object" || Array.isArray(verification)) {
|
||||
fail("RELEASE.json: `verification` must be an object mapping each gate to an evidence object");
|
||||
return;
|
||||
}
|
||||
const gates = Object.entries(verification);
|
||||
if (gates.length === 0) {
|
||||
fail("RELEASE.json: `verification` lists no gates");
|
||||
return;
|
||||
}
|
||||
for (const [gate, value] of gates) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
fail(
|
||||
`RELEASE.json: verification.${gate} is the bare ${typeof value} ${JSON.stringify(value)}; every gate must ` +
|
||||
`be an evidence object carrying a \`status\` of ${VERIFICATION_STATUSES.join(" | ")}, never a hard-coded literal`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (typeof value.status !== "string") {
|
||||
fail(`RELEASE.json: verification.${gate} is missing a string \`status\` field`);
|
||||
continue;
|
||||
}
|
||||
if (!VERIFICATION_STATUSES.includes(value.status))
|
||||
fail(
|
||||
`RELEASE.json: verification.${gate}.status is "${value.status}"; allowed values are ` +
|
||||
`${VERIFICATION_STATUSES.join(" | ")}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 7. Package metadata: canonical coordinates must stay resolved and stay on owned infrastructure.
|
||||
checks.push((index) =>
|
||||
check(index, "package-metadata", (fail, note) => {
|
||||
const pkg = JSON.parse(text("package.json"));
|
||||
for (const field of METADATA_FIELDS) {
|
||||
const value = pkg[field];
|
||||
if (value === undefined) {
|
||||
fail(`package.json: \`${field}\` is missing; canonical project coordinates are required`);
|
||||
continue;
|
||||
}
|
||||
const urls = [];
|
||||
if (typeof value === "string") urls.push(value);
|
||||
else if (typeof value === "object" && value !== null) {
|
||||
if (typeof value.url === "string") urls.push(value.url);
|
||||
else fail(`package.json: \`${field}\` is an object without a string \`url\``);
|
||||
} else {
|
||||
fail(`package.json: \`${field}\` must be a string or an object with a \`url\``);
|
||||
continue;
|
||||
}
|
||||
for (const url of urls) {
|
||||
if (url.includes(PLACEHOLDER_MARKER)) {
|
||||
fail(
|
||||
`package.json: \`${field}\` still carries the ${PLACEHOLDER_MARKER} placeholder "${url}"; ` +
|
||||
"set a real canonical URL before publishing",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let host;
|
||||
try {
|
||||
host = new URL(url.replace(/^git\+/, "")).host;
|
||||
} catch {
|
||||
fail(`package.json: \`${field}\` value "${url}" is not a parseable absolute URL`);
|
||||
continue;
|
||||
}
|
||||
if (host !== CANONICAL_HOST && !host.endsWith(`.${CANONICAL_HOST}`))
|
||||
fail(
|
||||
`package.json: \`${field}\` points at "${host}", which is outside the canonical ${CANONICAL_HOST} ` +
|
||||
"infrastructure; no public forge mirror has been confirmed for this package",
|
||||
);
|
||||
}
|
||||
}
|
||||
const repository = typeof pkg.repository?.url === "string" ? pkg.repository.url : "";
|
||||
if (repository.includes(CANONICAL_HOST))
|
||||
note(`${repository} is reserved on owned infrastructure and is not yet serving Git`);
|
||||
}),
|
||||
);
|
||||
|
||||
checks.forEach((run, position) => run(position + 1));
|
||||
|
||||
if (failed.length === 0) {
|
||||
console.log("POM docs verification PASS");
|
||||
} else {
|
||||
console.log(`POM docs verification FAIL (${failed.length}/${checks.length}: ${failed.join(", ")})`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
5
scripts/install.ps1
Normal file
5
scripts/install.ps1
Normal file
@@ -0,0 +1,5 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
npm install
|
||||
npm run verify
|
||||
omp plugin link .
|
||||
Write-Host "POM linked. Restart OMP or run /reload-plugins, then run /pom theme install."
|
||||
6
scripts/install.sh
Executable file
6
scripts/install.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
npm install
|
||||
npm run verify
|
||||
omp plugin link .
|
||||
printf '%s\n' 'POM linked. Restart OMP or run /reload-plugins, then run /pom theme install.'
|
||||
117
scripts/release-manifest.mjs
Normal file
117
scripts/release-manifest.mjs
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
|
||||
function walk(path) {
|
||||
if (!existsSync(path)) throw new Error(`Missing packaged path: ${relative(root, path)}`);
|
||||
if (statSync(path).isFile()) return [path];
|
||||
return readdirSync(path, { withFileTypes: true }).flatMap((entry) => walk(join(path, entry.name)));
|
||||
}
|
||||
|
||||
const candidates = packageJson.files
|
||||
.filter((item) => item !== "RELEASE.json")
|
||||
.flatMap((item) => walk(join(root, item)));
|
||||
candidates.push(join(root, "package.json"));
|
||||
|
||||
const sourceInventory = [...new Set(candidates)]
|
||||
.sort()
|
||||
.map((path) => {
|
||||
const data = readFileSync(path);
|
||||
return {
|
||||
path: relative(root, path).replaceAll("\\", "/"),
|
||||
bytes: data.length,
|
||||
sha256: createHash("sha256").update(data).digest("hex"),
|
||||
};
|
||||
});
|
||||
|
||||
function readGate(file, gate) {
|
||||
const path = join(root, file);
|
||||
if (!existsSync(path)) return { status: "NOT RUN", reason: `Missing evidence file: ${file}` };
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(path, "utf8"));
|
||||
const passed = data.passed === true || (data.summary && data.summary.passed === true);
|
||||
return {
|
||||
status: passed ? "PASS" : "FAIL",
|
||||
timestamp: data.timestamp || null,
|
||||
scope: data.scope || gate,
|
||||
command: data.command || null,
|
||||
details: data,
|
||||
};
|
||||
} catch {
|
||||
return { status: "NOT RUN", reason: `Unparsable evidence: ${file}` };
|
||||
}
|
||||
}
|
||||
|
||||
const gates = {
|
||||
typescript: readGate("00_admin/validation/typecheck.json", "tsc --noEmit"),
|
||||
behavioralTests: readGate("00_admin/validation/tests.json", "npm test"),
|
||||
runtimeSmoke: readGate("00_admin/validation/smoke.json", "npm run smoke"),
|
||||
structuralVerification: readGate("00_admin/validation/structural.json", "node scripts/verify.mjs"),
|
||||
themeSchemaValidation: readGate("00_admin/validation/theme.json", "theme schema validation"),
|
||||
npmPackDryRun: readGate("00_admin/validation/pack.json", "npm pack --dry-run"),
|
||||
};
|
||||
|
||||
const releaseDate = new Date().toISOString().split("T")[0];
|
||||
|
||||
const release = {
|
||||
name: "POM — Produce Magnum Opus",
|
||||
package: packageJson.name,
|
||||
version: packageJson.version,
|
||||
schemaVersion: "2.0.0",
|
||||
releaseDate,
|
||||
status: "release-candidate",
|
||||
summary: "OMP-native production kernel with evidence-gated stages, native task/IRC delegation, optional Swarm DAGs, Obsidian/Quartz knowledge projection, cinematic TUI hooks, and verified archive delivery.",
|
||||
compatibility: {
|
||||
bun: packageJson.engines.bun,
|
||||
ompPackages: ">=16.4.6 <17",
|
||||
module: "ESM",
|
||||
},
|
||||
inventory: {
|
||||
stages: 9,
|
||||
customTools: 7,
|
||||
agents: 14,
|
||||
prompts: 13,
|
||||
rules: 3,
|
||||
themes: 2,
|
||||
behavioralTests: 9,
|
||||
sourceFilesHashed: sourceInventory.length,
|
||||
sourceBytesHashed: sourceInventory.reduce((sum, item) => sum + item.bytes, 0),
|
||||
selfHashPolicy: "RELEASE.json is excluded from its own source inventory; final package and archive hashes are recorded externally.",
|
||||
},
|
||||
verification: {
|
||||
typescript: gates.typescript,
|
||||
behavioralTests: gates.behavioralTests,
|
||||
runtimeSmoke: gates.runtimeSmoke,
|
||||
structuralVerification: gates.structuralVerification,
|
||||
themeSchemaValidation: gates.themeSchemaValidation,
|
||||
npmPackDryRun: gates.npmPackDryRun,
|
||||
productionDependencyAudit: { status: "NOT RUN", reason: "Requires network access to npm registry" },
|
||||
fullDevelopmentTreeAudit: { status: "NOT RUN", reason: "Requires network access to npm registry" },
|
||||
ompPluginDoctor: { status: "NOT RUN", reason: "Requires Bun + OMP installation" },
|
||||
interactiveTuiSmoke: { status: "NOT RUN", reason: "Requires a live OMP terminal host" },
|
||||
},
|
||||
releaseGates: [
|
||||
"Run npm run verify",
|
||||
"Run npm run doctor in a Bun + OMP installation",
|
||||
"Open /pom and verify the overlay, HUD, status, cards, and /pom theme install",
|
||||
"Create a smoke project, validate stage 0, and verify a checkpoint archive",
|
||||
],
|
||||
sourceInventory,
|
||||
};
|
||||
|
||||
const rendered = `${JSON.stringify(release, null, 2)}\n`;
|
||||
const output = join(root, "RELEASE.json");
|
||||
if (process.argv.includes("--check")) {
|
||||
if (!existsSync(output) || readFileSync(output, "utf8") !== rendered) {
|
||||
console.error("RELEASE.json is stale; run npm run release:manifest");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("POM release manifest PASS");
|
||||
} else {
|
||||
writeFileSync(output, rendered);
|
||||
console.log(`Wrote RELEASE.json · ${sourceInventory.length} files · ${release.inventory.sourceBytesHashed} bytes`);
|
||||
}
|
||||
26
scripts/smoke.ts
Normal file
26
scripts/smoke.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_CONFIG } from "../src/domain";
|
||||
import { bootstrapProject } from "../src/persistence";
|
||||
import { createState } from "../src/state";
|
||||
import { validatePom } from "../src/validators";
|
||||
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "pom-smoke-"));
|
||||
try {
|
||||
const config = { ...DEFAULT_CONFIG, projectRootMode: "cwd" as const };
|
||||
const state = createState("POM Smoke", sandbox, config, true);
|
||||
await bootstrapProject(state);
|
||||
const ompConfig = await readFile(join(sandbox, ".omp", "config.yml"), "utf8");
|
||||
assert.match(ompConfig, /dark: pom-nocturne/);
|
||||
assert.match(ompConfig, /task:\n\s+eager: preferred/);
|
||||
assert.match(ompConfig, /compaction:\n\s+strategy: snapcompact/);
|
||||
const board = await readFile(join(sandbox, "content", "production-board.md"), "utf8");
|
||||
assert.match(board, /# Production Board/);
|
||||
const report = await validatePom(state, "quick", { writeReport: false });
|
||||
assert.equal(report.passed, true);
|
||||
console.log(`POM runtime smoke PASS · ${report.checks.length} checks`);
|
||||
} finally {
|
||||
await rm(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
77
scripts/verify.mjs
Normal file
77
scripts/verify.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const required = [
|
||||
"POM.yml",
|
||||
"README.md",
|
||||
"RELEASE.json",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"src/index.ts",
|
||||
"src/events.ts",
|
||||
"src/tools.ts",
|
||||
"src/visual.ts",
|
||||
"skills/pom/SKILL.md",
|
||||
"config/APPEND_SYSTEM.md",
|
||||
"prompts/stage-run.md",
|
||||
"themes/pom-nocturne.json",
|
||||
"themes/pom-parchment.json",
|
||||
"scripts/smoke.ts",
|
||||
];
|
||||
for (const rel of required) assert(existsSync(join(root, rel)), `missing ${rel}`);
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const release = JSON.parse(readFileSync(join(root, "RELEASE.json"), "utf8"));
|
||||
assert.equal(release.version, pkg.version, "release manifest version mismatch");
|
||||
assert.equal(release.inventory.stages, 9, "release stage inventory mismatch");
|
||||
assert.equal(release.inventory.customTools, 7, "release tool inventory mismatch");
|
||||
assert.equal(release.inventory.behavioralTests, 9, "release test inventory mismatch");
|
||||
assert.deepEqual(pkg.omp.extensions, ["./src/index.ts"], "exactly one OMP entry required");
|
||||
assert.equal(pkg.version, "2.0.0", "release version must match POM 2");
|
||||
for (const rel of pkg.files) assert(existsSync(join(root, rel)), `package files entry does not exist: ${rel}`);
|
||||
|
||||
const command = readFileSync(join(root, "src/command.ts"), "utf8");
|
||||
assert.equal((command.match(/registerCommand\("pom"/g) ?? []).length, 1, "exactly one /pom registration required");
|
||||
assert(command.includes('registerShortcut("alt+p"'), "Alt+P command-center shortcut required");
|
||||
|
||||
const domain = readFileSync(join(root, "src/domain.ts"), "utf8");
|
||||
assert.equal((domain.match(/\bid:\s*[0-8],/g) ?? []).length, 9, "nine stages required");
|
||||
|
||||
const tools = readFileSync(join(root, "src/tools.ts"), "utf8");
|
||||
assert.equal((tools.match(/name:\s*"pom_[a-z]+"/g) ?? []).length, 7, "seven POM tools required");
|
||||
for (const forbidden of ["allowArgs", "formatApprovalDetails", 'concurrency: "exclusive"', "ExtensionAPI = any"]) {
|
||||
assert(!tools.includes(forbidden), `unsupported or unsafe registration field present: ${forbidden}`);
|
||||
}
|
||||
|
||||
assert(!existsSync(join(root, "src/omp-shim.d.ts")), "OMP declaration shim must not ship");
|
||||
assert(!existsSync(join(root, "src/node-shim.d.ts")), "Node declaration shim must not ship");
|
||||
|
||||
const markdownCount = (dir) => readdirSync(join(root, dir)).filter((name) => name.endsWith(".md")).length;
|
||||
assert.equal(markdownCount("agents"), 14, "fourteen specialist agents required");
|
||||
assert.equal(markdownCount("prompts"), 13, "thirteen prompt fragments required");
|
||||
assert.equal(markdownCount("rules"), 3, "three rules required");
|
||||
assert.equal(readdirSync(join(root, "themes")).filter((name) => name.endsWith(".json")).length, 2, "two POM themes required");
|
||||
|
||||
for (const file of readdirSync(join(root, "agents")).filter((name) => name.endsWith(".md"))) {
|
||||
const text = readFileSync(join(root, "agents", file), "utf8");
|
||||
assert(text.startsWith("---\n"), `${file} missing frontmatter`);
|
||||
assert(/^name:\s*pom-[a-z0-9-]+$/m.test(text), `${file} missing valid agent name`);
|
||||
assert(/^description:\s*\S.+$/m.test(text), `${file} missing description`);
|
||||
assert(/^tools:\s*.*\byield\b.*$/m.test(text), `${file} must expose yield`);
|
||||
}
|
||||
|
||||
const themeSchema = JSON.parse(readFileSync(join(root, "node_modules/@oh-my-pi/pi-coding-agent/src/modes/theme/theme-schema.json"), "utf8"));
|
||||
for (const file of readdirSync(join(root, "themes")).filter((name) => name.endsWith(".json"))) {
|
||||
const theme = JSON.parse(readFileSync(join(root, "themes", file), "utf8"));
|
||||
assert.equal(theme.name, file.slice(0, -5), `${file} name mismatch`);
|
||||
for (const key of themeSchema.properties.colors.required) assert(key in theme.colors, `${file} missing color ${key}`);
|
||||
}
|
||||
|
||||
for (const rel of ["src", "skills", "agents", "rules", "prompts", "themes", "config", "docs", "scripts"]) {
|
||||
assert(statSync(join(root, rel)).isDirectory(), `${rel} must be a directory`);
|
||||
}
|
||||
|
||||
console.log("POM structural verification PASS");
|
||||
38
skills/pom/SKILL.md
Normal file
38
skills/pom/SKILL.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: pom
|
||||
description: Produce Magnum Opus — complete fiction creation, research, visual development, continuity, artifacts, validation, and delivery through POM Tasks 0–8.
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# POM — Produce Magnum Opus
|
||||
|
||||
Use this skill whenever `/pom`, a POM project, or a fiction-production stage is active.
|
||||
|
||||
## Prime law
|
||||
- Canonical project files beat chat and memory.
|
||||
- OMP native systems remain native: todo tracks live work; task runs agents; IRC coordinates; memory stores reusable lessons; approvals guard side effects; sessions preserve conversation.
|
||||
- POM owns orchestration, story canon, artifact contracts, stage gates, and delivery truth.
|
||||
- Never invent tool use, files, images, counts, citations, validation, ZIP tests, or completion.
|
||||
- Read only the references needed for the active stage.
|
||||
|
||||
## References
|
||||
- Workflow and gates: `skill://pom/references/workflow.md`
|
||||
- Tool routing: `skill://pom/references/tool-routing.md`
|
||||
- Agent studios: `skill://pom/references/agent-studios.md`
|
||||
- Todo blueprints: `skill://pom/references/todo-blueprints.md`
|
||||
- Artifact system: `skill://pom/references/artifacts.md`
|
||||
- UI contract: `skill://pom/references/ui.md`
|
||||
- QA and rights: `skill://pom/references/qa.md`
|
||||
- Command map: `skill://pom/references/commands.md`
|
||||
- Condensed core: `skill://pom/references/core-law.jml`
|
||||
|
||||
## Stage loop
|
||||
1. Read canonical state and relevant files.
|
||||
2. Initialize the native todo blueprint.
|
||||
3. Select the stage tool/model profile.
|
||||
4. Fan out independent specialist work once.
|
||||
5. Integrate and create canonical artifacts.
|
||||
6. Update only changed canon and ledgers.
|
||||
7. Run deterministic checks plus independent review.
|
||||
8. Call `pom_stage pass` only on PASS; otherwise block explicitly.
|
||||
9. Render real status, files, risks, and one next action.
|
||||
34
skills/pom/references/agent-studios.md
Normal file
34
skills/pom/references/agent-studios.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# POM agent studios
|
||||
|
||||
One task batch per independent wave.
|
||||
|
||||
Shared context:
|
||||
- Goal
|
||||
- Constraints
|
||||
- Contract
|
||||
|
||||
Every assignment:
|
||||
- Target
|
||||
- Change
|
||||
- Acceptance
|
||||
|
||||
Stage studios:
|
||||
- T0 architect
|
||||
- T1 researcher + concept forge + critic
|
||||
- T2 voice architect + rights auditor
|
||||
- T3 concept forge + critic
|
||||
- T4 story architect + character/world + continuity + visual
|
||||
- T5 structure + continuity + critic
|
||||
- T6 structure + continuity + visual
|
||||
- T7 prose + developmental editor + continuity
|
||||
- T8 delivery + rights + continuity
|
||||
|
||||
Rules:
|
||||
- main agent integrates and owns final decisions
|
||||
- scouts investigate only; editing agents own explicit files
|
||||
- no duplicated shared context
|
||||
- no generic cloned workers
|
||||
- no project-wide formatters/tests inside children
|
||||
- local:// for bulk context, agent:// for outputs, history:// for transcripts
|
||||
- IRC for overlap, blockers, and follow-up
|
||||
- isolate only code-changing extension development, not ordinary story artifacts
|
||||
21
skills/pom/references/artifacts.md
Normal file
21
skills/pom/references/artifacts.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# POM artifact system
|
||||
|
||||
- README.md
|
||||
- 00_admin: state, manifest, SHA256SUMS, inventory
|
||||
- 01_planning: brief, genre, persona options, story store, selected direction
|
||||
- 02_story_bible: identity, voice, cast, relations, world, locations, factions, rules, themes, frameworks, timeline, canon, payoff, style, visual bible
|
||||
- 03_manuscript: master outline, chapter outline, scene index, complete manuscript, chapters
|
||||
- 04_images: references, briefs, prompts, drafts, finals, alt text, metadata
|
||||
- 05_research: plan, sources, notes, fact/invention, rights
|
||||
- 06_ledgers: decisions, assumptions, revisions, continuity, quality, validation
|
||||
- 07_exports: formats, reports, ZIP
|
||||
|
||||
Law:
|
||||
- lowercase portable filenames
|
||||
- semantic versions
|
||||
- atomic writes
|
||||
- measured bytes and SHA-256
|
||||
- register every claimed artifact
|
||||
- checkpoint ZIP only on request or major milestone
|
||||
- final ZIP always fresh and member-tested
|
||||
- ZIP cannot recursively manifest itself; final archive hash lives in the external delivery sidecar
|
||||
21
skills/pom/references/commands.md
Normal file
21
skills/pom/references/commands.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# `/pom` command tree
|
||||
|
||||
- `/pom` — command-center overlay
|
||||
- `/pom new <title>` — create project, vault, ledgers, state, and Git root
|
||||
- `/pom resume [state-path]` — restore canonical state
|
||||
- `/pom run [next|all|0..8]` — start authoritative production
|
||||
- `/pom hive [lean|stage|audit]` — queue a native task + IRC specialist wave
|
||||
- `/pom swarm [parallel|sequential|pipeline] [count]` — generate optional Swarm YAML
|
||||
- `/pom evidence` — record file-backed gate evidence
|
||||
- `/pom check [quick|stage|canon|continuity|knowledge|files|delivery|all]`
|
||||
- `/pom vault` — insert `@content/production-board.md`
|
||||
- `/pom status [--json]`
|
||||
- `/pom export [checkpoint|final]`
|
||||
- `/pom prompt [list|show|run] [id]`
|
||||
- `/pom tools [show|reset|profile <name>]`
|
||||
- `/pom theme install`
|
||||
- `/pom settings`
|
||||
- `/pom doctor`
|
||||
- `/pom help`
|
||||
|
||||
All model-facing production actions use the seven typed `pom_*` tools. No other POM slash-command root is registered.
|
||||
53
skills/pom/references/core-law.jml
Normal file
53
skills/pom/references/core-law.jml
Normal file
@@ -0,0 +1,53 @@
|
||||
# 🎭 POM 2 compact operating law
|
||||
|
||||
IDENTITY
|
||||
- OMP-native production kernel, idea → validated package, stages 0–8.
|
||||
- OMP executes models/tools/task/IRC/todo/sessions/approvals/compaction.
|
||||
- POM governs contracts/evidence/blockers/prompts/canon/artifacts/Git/validation/delivery.
|
||||
|
||||
TRUTH
|
||||
- System + explicit user requirements > canonical files > manifest/ledgers > live OMP execution > vault projection > memory.
|
||||
- Never fake tool use, files, citations, images, counts, hashes, checks, ZIP tests, or completion.
|
||||
- Partial stays partial. Missing stays disclosed. Blocked stays blocked.
|
||||
|
||||
STAGE GATE
|
||||
- Earlier stages complete.
|
||||
- Target stage actively running.
|
||||
- Required artifacts substantive and structurally valid.
|
||||
- Every acceptance criterion has project-relative evidence paths.
|
||||
- No unresolved blocker.
|
||||
- Deterministic report PASS.
|
||||
- Then, and only then, call `pom_stage` action `pass`.
|
||||
|
||||
DELEGATION
|
||||
- One widest safe native task batch per independent wave.
|
||||
- Each specialist has objective, owned files, and acceptance conditions.
|
||||
- IRC vocabulary: CLAIM / QUERY / ANSWER / ALERT / HANDOFF / DONE.
|
||||
- IRC is terse; durable analysis goes into files or Hive ledger.
|
||||
- Main agent integrates and owns canon decisions.
|
||||
- Optional Swarm YAML for unattended fan-out/fan-in execution.
|
||||
|
||||
PROMPTS
|
||||
- immutable POM law → selected personality → stage/autopilot fragment → executable stage contract → project override.
|
||||
- Hash and record every active fragment.
|
||||
|
||||
ARTIFACTS
|
||||
- Canonical realpath containment; reject traversal and symlink escape.
|
||||
- Non-empty regular files only.
|
||||
- Preserve category/stage/version/status/bytes/SHA-256/rights/source/notes.
|
||||
- Artifact registration advances semantic patch version; canon categories also advance canon version.
|
||||
|
||||
VISUALS
|
||||
- Status = peripheral truth.
|
||||
- HUD = persistent production state.
|
||||
- Working message = current semantic phase.
|
||||
- Cards = durable validation/Hive/milestone events.
|
||||
- Overlay = deliberate operator control.
|
||||
- Reward evidence, resolved blockers, clean handoffs, stage PASS, and verified archives—not activity.
|
||||
|
||||
DELIVERY
|
||||
- Fresh archive.
|
||||
- Safe member names, no duplicates/encryption/unsupported methods.
|
||||
- Parse central directory; decompress every member; verify size + CRC-32.
|
||||
- Hash archive; write external verification report.
|
||||
- Disclose warnings and omissions.
|
||||
18
skills/pom/references/qa.md
Normal file
18
skills/pom/references/qa.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# POM QA20
|
||||
|
||||
Intent, user alignment, originality, genre promise, concept clarity, narrative engine, causality, agency, character continuity, world continuity, thematic integrity, setup/payoff, pacing, emotional logic, voice, readability, research integrity, visual integrity, artifact integrity, truthfulness.
|
||||
|
||||
Rights:
|
||||
- no distinctive living-author imitation
|
||||
- no fabricated quotations or attribution
|
||||
- no unauthorized third-party redistribution
|
||||
- no invented private identity details
|
||||
- minimum necessary personal data
|
||||
- fictional framing does not justify operational harm
|
||||
|
||||
Completion:
|
||||
- fix material defects before pass
|
||||
- no objective perfection claims
|
||||
- partial remains PARTIAL
|
||||
- missing remains disclosed
|
||||
- 100% only after requested measurable gates pass
|
||||
105
skills/pom/references/todo-blueprints.md
Normal file
105
skills/pom/references/todo-blueprints.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# POM todo blueprints
|
||||
|
||||
Native todo is the only live task tracker. The active stage prompt instructs OMP to initialize the exact phase blueprint from `src/domain.ts`; POM does not maintain a competing todo database.
|
||||
|
||||
## T0 Foundation
|
||||
- Resolve project identity
|
||||
- Define creative north star
|
||||
- Record constraints and assumptions
|
||||
- Create project brief
|
||||
- Initialize canonical state
|
||||
- Validate Task 0 gate
|
||||
|
||||
## T1 Genre
|
||||
- Map genre candidates
|
||||
- Research current genre context
|
||||
- Analyze clichés and differentiation
|
||||
- Build fusion matrix
|
||||
- Rank top directions
|
||||
- Lock genre promise
|
||||
- Save grid and matrix
|
||||
- Validate Task 1 gate
|
||||
|
||||
## T2 Voice
|
||||
- Design original persona
|
||||
- Define audience and philosophy
|
||||
- Check rights boundaries
|
||||
- Define voice profile
|
||||
- Write always and avoid laws
|
||||
- Create original sample
|
||||
- Save persona and guide
|
||||
- Validate Task 2 gate
|
||||
|
||||
## T3 Store
|
||||
- Generate twenty diverse concepts
|
||||
- Enforce structural diversity
|
||||
- Score concept potential
|
||||
- Rank strongest five
|
||||
- Compare top three
|
||||
- Select and strengthen winner
|
||||
- Save store and direction
|
||||
- Validate Task 3 gate
|
||||
|
||||
## T4 Bible
|
||||
- Finalize story identity
|
||||
- Write full ending synopsis
|
||||
- Define dramatic and thematic questions
|
||||
- Build character bible
|
||||
- Build world and rules
|
||||
- Build themes and frameworks
|
||||
- Create timeline and ledgers
|
||||
- Map mysteries and payoffs
|
||||
- Create visual bible
|
||||
- Run continuity audit
|
||||
- Resolve contradictions
|
||||
- Validate Task 4 gate
|
||||
|
||||
## T5 Outline
|
||||
- Design acts and sequences
|
||||
- Map turning points
|
||||
- Allocate narrative weight
|
||||
- Build causal scene index
|
||||
- Track state changes
|
||||
- Link setups and payoffs
|
||||
- Audit agency and escalation
|
||||
- Repair weak causality
|
||||
- Validate Task 5 gate
|
||||
|
||||
## T6 Chapters
|
||||
- Map every chapter
|
||||
- Define chapter state changes
|
||||
- Balance POV and pacing
|
||||
- Verify timeline transitions
|
||||
- Verify reveals and payoffs
|
||||
- Prepare representative visual
|
||||
- Save chapter outline
|
||||
- Update scene index
|
||||
- Validate Task 6 gate
|
||||
|
||||
## T7 Draft
|
||||
- Load chapter dependencies
|
||||
- Verify current canon state
|
||||
- Set chapter objective
|
||||
- Draft complete chapter
|
||||
- Preserve voice and POV
|
||||
- End in changed state
|
||||
- Measure words
|
||||
- Run voice and repetition checks
|
||||
- Update canon and ledgers
|
||||
- Repeat remaining chapters
|
||||
- Run full manuscript review
|
||||
- Validate Task 7 gate
|
||||
|
||||
## T8 Delivery
|
||||
- Compile manuscript
|
||||
- Compile requested exports
|
||||
- Assemble approved visuals
|
||||
- Run structure and content checks
|
||||
- Run continuity and rights checks
|
||||
- Verify manifest and hashes
|
||||
- Create fresh final ZIP
|
||||
- Test every ZIP member
|
||||
- Write delivery report
|
||||
- Provide real artifact path
|
||||
- Disclose warnings and omissions
|
||||
- Validate Task 8 gate
|
||||
43
skills/pom/references/tool-routing.md
Normal file
43
skills/pom/references/tool-routing.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# POM tool routing
|
||||
|
||||
## Discovery and reading
|
||||
- read: files, directories, archives, databases, documents, internal URIs, static URLs; parallelize independent reads
|
||||
- glob: bounded path discovery
|
||||
- grep: bounded regex search
|
||||
- ast_grep: syntax-shape search
|
||||
- inspect_image: evidence-grounded image analysis
|
||||
- web_search: current public discovery; primary sources first
|
||||
- browser: authenticated, interactive, or JavaScript-only pages
|
||||
|
||||
## Changing and computing
|
||||
- edit: grounded hash-anchored edits to existing files; reread after every edit
|
||||
- write: new files or intentional complete replacement
|
||||
- lsp: definitions, references, rename, diagnostics, code actions; mandatory for symbol-aware work
|
||||
- ast_edit: structural codemods when text edits are unsafe
|
||||
- eval: persistent multi-step computation, schemas, pipelines, local:// context, agent DAGs
|
||||
- bash: one real binary or short fact pipeline; never a shell program
|
||||
- debug: breakpoints, stepping, threads, variables, runtime state
|
||||
|
||||
## Orchestration
|
||||
- task: widest safe specialist fan-out; one shared context, complete assignments
|
||||
- job: intervention only; results arrive automatically
|
||||
- irc: coordinate existing peers and avoid duplicate work
|
||||
- todo: exact phased execution state; every required item explicit
|
||||
- ask: only material user tradeoffs after exhausting sources
|
||||
- resolve: explicit pending-action approval
|
||||
- learn/manage_skill: reusable lessons and repeatable procedures only
|
||||
- generate_image: complete brief, generation/edit, then mandatory inspection
|
||||
|
||||
## Profiles
|
||||
- creative: read/search, edit/write, eval, task/IRC/todo, image
|
||||
- research: read/web/browser, inspection, eval, task/IRC/todo
|
||||
- draft: read/edit/write, eval, task/IRC/todo, image
|
||||
- audit: read/search, AST/LSP, eval, task/IRC/todo
|
||||
- delivery: read/write/edit, short binaries/eval, checksums, ZIP, resolve
|
||||
|
||||
## Law
|
||||
- native tool first; MCP only for trusted external capabilities
|
||||
- never use browser when read can retrieve static content
|
||||
- never use text rename when LSP can preserve symbol semantics
|
||||
- never poll agents unless all useful parent work is blocked
|
||||
- never duplicate native todo, task, IRC, memory, approval, compaction, or session truth inside POM
|
||||
28
skills/pom/references/ui.md
Normal file
28
skills/pom/references/ui.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# POM terminal UI
|
||||
|
||||
## Surface hierarchy
|
||||
|
||||
- status line: stage, progress, gate/blocker, Hive, approval, retry
|
||||
- HUD widget: persistent contract, evidence, blockers, active work
|
||||
- working message: exact current semantic phase
|
||||
- custom cards: validation, Hive, milestones, important events
|
||||
- tool renderers: compact call, partial update, pass/fail result, expanded evidence
|
||||
- `/pom` overlay: run, Hive, validate, evidence, vault, export, themes, settings
|
||||
- thinking lane: optional stage/execution annotation beneath visible thinking
|
||||
|
||||
## Density
|
||||
|
||||
- cinematic: three-line HUD, stage track, richer cards and coordination cues
|
||||
- rich: two-line evidence HUD and detailed cards
|
||||
- compact: one-line HUD and terse cards
|
||||
- minimal: status plus critical cards; no persistent widget
|
||||
|
||||
## Motion
|
||||
|
||||
- off: static semantic symbols
|
||||
- subtle: two-frame work pulse
|
||||
- full: four-frame activity cue and stronger live coordination feedback
|
||||
|
||||
## Law
|
||||
|
||||
Use OMP theme tokens and visual-width utilities. Never emit raw ANSI control sequences. Keep custom TUI optional: headless/RPC/ACP operation must still work through textual tools and canonical files.
|
||||
55
skills/pom/references/workflow.md
Normal file
55
skills/pom/references/workflow.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# POM workflow
|
||||
|
||||
## Task 0 — Project Initialization
|
||||
- profile creative
|
||||
- agents pom-architect
|
||||
- output identity, north star, brief, assumptions, roadmap
|
||||
- gate identity resolved, foundational ambiguity cleared
|
||||
|
||||
## Task 1 — Genre Architecture
|
||||
- profile research
|
||||
- agents researcher, concept forge, critic
|
||||
- output full genre grid, 8–15 fusions, top three, risks, promise, choice, key art
|
||||
- gate primary genre selected, promise and cliché risks recorded
|
||||
|
||||
## Task 2 — Author Persona and Voice
|
||||
- profile creative
|
||||
- agents voice architect, rights auditor
|
||||
- output original persona, complete voice profile, laws, tags, sample, portrait
|
||||
- gate another editor can consistently test prose against the guide
|
||||
|
||||
## Task 3 — Story Store
|
||||
- profile creative
|
||||
- agents concept forge, critic
|
||||
- output exactly twenty structurally diverse concepts, strongest five, top three, winner, upgrade
|
||||
- gate one stable premise selected
|
||||
|
||||
## Task 4 — Story Blueprint and Bible
|
||||
- profile creative
|
||||
- agents story architect, character/world, continuity, visual
|
||||
- output full ending, cast, relationships, world, systems, rules, themes, frameworks, timeline, canon, mysteries, payoffs, visual bible
|
||||
- gate ending known, arcs and rules stable, canon supports outline
|
||||
|
||||
## Task 5 — Master Story Outline
|
||||
- profile creative
|
||||
- agents structure, continuity, critic
|
||||
- output acts, sequences, beats, scenes, state changes, setup/payoff
|
||||
- gate ending represented, causality and escalation survive review
|
||||
|
||||
## Task 6 — Chapter Architecture
|
||||
- profile creative
|
||||
- agents structure, continuity, visual
|
||||
- output every chapter, scenes, objectives, reversals, hooks, word allocation, representative art
|
||||
- gate no gaps, redundancy, unsupported payoff, impossible timeline, or broken POV logic
|
||||
|
||||
## Task 7 — Drafting and Revision
|
||||
- profile draft
|
||||
- agents prose, developmental editor, continuity
|
||||
- output complete chapter files, canonical deltas, measured words, reviewed manuscript
|
||||
- gate every planned chapter complete, continuity resolved or disclosed
|
||||
|
||||
## Task 8 — Final Compilation and Delivery
|
||||
- profile delivery
|
||||
- agents delivery, rights, continuity
|
||||
- output compiled artifacts, manifest, checksums, exports, fresh tested ZIP, report, real path
|
||||
- gate files exist, hashes match, every ZIP member passes decompression/CRC, omissions disclosed
|
||||
300
src/command.ts
Normal file
300
src/command.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import { join } from "node:path";
|
||||
import { loadPomConfig, updateProjectPomConfig } from "./config";
|
||||
import { stage, STAGES, type PomMotion, type PomToolProfile, type PomView, type ValidationScope } from "./domain";
|
||||
import { bootstrapProject, loadState, recordHiveEvent, syncKnowledgeBase, writeHiveMission } from "./persistence";
|
||||
import { createState, gotoStage, recordEvidence, startStage, summarizeState } from "./state";
|
||||
import { renderStagePrompt, getPrompt, listPrompts } from "./prompts";
|
||||
import { validatePom } from "./validators";
|
||||
import { applyToolProfile } from "./tool-profiles";
|
||||
import { routeStage } from "./router";
|
||||
import { ensureGit, enterStageBranch } from "./git";
|
||||
import { exportProject } from "./exporter";
|
||||
import { openCommandCenter, updateChrome } from "./ui";
|
||||
import type { CommandCenterAction } from "./visual";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
import { buildHiveMission } from "./studio";
|
||||
import { writeSwarmDefinition, type PomSwarmMode } from "./swarm";
|
||||
|
||||
import { installBundledThemes } from "./themes";
|
||||
import { parseCommand } from "./parser";
|
||||
export { parseCommand } from "./parser";
|
||||
|
||||
const HELP = `POM · Produce Magnum Opus
|
||||
|
||||
/pom Open cinematic command center
|
||||
/pom new <title> Create a POM project, vault, ledgers, and Git root
|
||||
/pom run [next|all|0..8] Start authoritative production
|
||||
/pom hive [lean|stage|audit] Launch native task + IRC specialist flow
|
||||
/pom swarm [mode] [count] Generate an optional OMP Swarm DAG and queue /swarm run
|
||||
/pom evidence Record acceptance evidence interactively
|
||||
/pom check [scope] Run deterministic validation
|
||||
/pom vault Insert the production-board mention
|
||||
/pom export [checkpoint|final]
|
||||
/pom status [--json]
|
||||
/pom prompt [list|show|run] [id]
|
||||
/pom tools [show|reset|profile <name>]
|
||||
/pom theme install Install POM Nocturne/Parchment themes
|
||||
/pom settings
|
||||
/pom doctor
|
||||
/pom help`;
|
||||
|
||||
async function recordEvidenceInteractive(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
|
||||
const state = runtime.get();
|
||||
const spec = stage(state.currentStage);
|
||||
const criterion = await ctx.ui.select("Record acceptance evidence", spec.acceptance.map((item) => ({ label: `${item.id} · ${item.label}`, value: item.id, description: item.description })), { outline: true });
|
||||
if (!criterion) return;
|
||||
const criterionId = criterion.split(" · ")[0];
|
||||
const summary = await ctx.ui.input("Evidence summary", "What was verified and why is it sufficient?");
|
||||
if (!summary) return;
|
||||
const pathsText = await ctx.ui.input("Evidence paths", "Comma-separated project-relative paths");
|
||||
const paths = (pathsText ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
||||
await runtime.set(recordEvidence(state, criterionId, summary, paths, "Main"));
|
||||
ctx.ui.notify(`Evidence recorded: ${criterionId}`, "info");
|
||||
}
|
||||
|
||||
async function experienceSettings(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
|
||||
const state = structuredClone(runtime.get());
|
||||
const view = await ctx.ui.select("POM visual density", [
|
||||
{ label: "cinematic", description: "Three-line HUD, richer cards, IRC ticker, and thinking lane" },
|
||||
{ label: "rich", description: "Two-line HUD and evidence-focused expanded cards" },
|
||||
{ label: "compact", description: "One-line HUD and terse cards" },
|
||||
{ label: "minimal", description: "Status line plus critical milestones and failures" },
|
||||
], { initialIndex: ["cinematic", "rich", "compact", "minimal"].indexOf(state.view), outline: true });
|
||||
if (view) state.view = view as PomView;
|
||||
const motion = await ctx.ui.select("POM motion", [
|
||||
{ label: "off", description: "Static semantic symbols only" },
|
||||
{ label: "subtle", description: "Two-frame active-work pulse" },
|
||||
{ label: "full", description: "Four-frame tool activity and live coordination cues" },
|
||||
], { initialIndex: ["off", "subtle", "full"].indexOf(state.motion), outline: true });
|
||||
if (motion) state.motion = motion as PomMotion;
|
||||
|
||||
const currentConfig = runtime.getConfig();
|
||||
const lane = await ctx.ui.select("Thinking-lane annotation", [
|
||||
{ label: "on", description: "Show stage and execution lane below visible thinking" },
|
||||
{ label: "off", description: "Leave visible thinking unannotated" },
|
||||
], { initialIndex: currentConfig.showThinkingLane ? 0 : 1, outline: true });
|
||||
const placement = await ctx.ui.select("HUD placement", [
|
||||
{ label: "aboveEditor", description: "Production truth directly above the prompt editor" },
|
||||
{ label: "belowEditor", description: "Production truth beneath the prompt editor" },
|
||||
], { initialIndex: currentConfig.hudPlacement === "aboveEditor" ? 0 : 1, outline: true });
|
||||
|
||||
await runtime.set(state);
|
||||
if (state.initialized) {
|
||||
await updateProjectPomConfig(state.projectRoot, {
|
||||
showThinkingLane: lane ? lane === "on" : currentConfig.showThinkingLane,
|
||||
hudPlacement: (placement ?? currentConfig.hudPlacement) as "aboveEditor" | "belowEditor",
|
||||
});
|
||||
await runtime.refreshConfig(state.projectRoot);
|
||||
}
|
||||
updateChrome(ctx, runtime.get(), runtime.getConfig(), runtime);
|
||||
ctx.ui.notify(`POM experience · ${state.view} · motion ${state.motion} · lane ${runtime.getConfig().showThinkingLane ? "on" : "off"}`, "info");
|
||||
}
|
||||
|
||||
async function runStage(pi: ExtensionAPI, ctx: ExtensionContext, runtime: PomRuntime, targetRaw: string): Promise<void> {
|
||||
let state = runtime.get();
|
||||
const allStages = targetRaw === "all";
|
||||
const target = allStages || targetRaw === "next" ? state.currentStage : Number(targetRaw);
|
||||
if (!Number.isInteger(target) || target < 0 || target > 8) throw new Error("Run target must be next, all, or 0..8");
|
||||
state = startStage(gotoStage(state, target), target, allStages);
|
||||
await runtime.set(state);
|
||||
const config = await loadPomConfig(state.projectRoot);
|
||||
await enterStageBranch(state, config);
|
||||
await routeStage(pi, ctx, state);
|
||||
updateChrome(ctx, state, config, runtime);
|
||||
pi.sendUserMessage(await renderStagePrompt(state, allStages), { deliverAs: "followUp" });
|
||||
}
|
||||
|
||||
async function dispatchAction(pi: ExtensionAPI, ctx: ExtensionContext, runtime: PomRuntime, action: CommandCenterAction): Promise<void> {
|
||||
if (action === "run") return runStage(pi, ctx, runtime, "next");
|
||||
if (action === "hive") { pi.sendUserMessage("Call pom_hive with op=plan and preset=stage. Then call native task exactly once with the returned batch. After all agents finish, integrate and call pom_hive op=synthesize.", { deliverAs: "followUp" }); return; }
|
||||
if (action === "validate") {
|
||||
const report = await validatePom(runtime.get(), runtime.get().currentStage === 8 ? "delivery" : "stage");
|
||||
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${report.scope}`, display: true, details: report }, { triggerTurn: false });
|
||||
return;
|
||||
}
|
||||
if (action === "evidence") return recordEvidenceInteractive(ctx, runtime);
|
||||
if (action === "vault") { ctx.ui.setEditorText("@content/production-board.md "); return; }
|
||||
if (action === "export") {
|
||||
const result = await exportProject(runtime.get(), runtime.get().currentStage === 8 ? "final" : "checkpoint");
|
||||
ctx.ui.notify(`Archive verified: ${result.zipPath}`, "info");
|
||||
return;
|
||||
}
|
||||
if (action === "theme") {
|
||||
const installed = await installBundledThemes();
|
||||
ctx.ui.setEditorText("/theme pom-nocturne");
|
||||
ctx.ui.notify(`Installed ${installed.map((item) => item.name).join(", ")}. Press Enter to activate POM Nocturne.`, "info");
|
||||
return;
|
||||
}
|
||||
if (action === "settings") return experienceSettings(ctx, runtime);
|
||||
}
|
||||
|
||||
export function registerPomCommand(pi: ExtensionAPI, runtime: PomRuntime): void {
|
||||
const completions = ["new", "run", "hive", "swarm", "evidence", "check", "vault", "export", "status", "prompt", "tools", "theme", "settings", "doctor", "help"];
|
||||
pi.registerCommand("pom", {
|
||||
description: "POM production command center",
|
||||
getArgumentCompletions: (prefix) => completions.filter((item) => item.startsWith(prefix)).map((item) => ({ value: item, label: item })),
|
||||
handler: async (raw, ctx: ExtensionCommandContext) => {
|
||||
const command = parseCommand(raw);
|
||||
if (command.verb === "dashboard") {
|
||||
const action = await openCommandCenter(ctx, runtime.get());
|
||||
await dispatchAction(pi, ctx, runtime, action);
|
||||
return;
|
||||
}
|
||||
if (command.verb === "help") { ctx.ui.notify(HELP, "info"); return; }
|
||||
|
||||
if (command.verb === "new") {
|
||||
const title = command.args.join(" ") || await ctx.ui.input("Project title", "Untitled Magnum Opus") || "Untitled Magnum Opus";
|
||||
const config = await loadPomConfig(ctx.cwd);
|
||||
const state = createState(title, ctx.cwd, config, true);
|
||||
await bootstrapProject(state);
|
||||
await ensureGit(state, config);
|
||||
await runtime.set(state);
|
||||
await pi.setSessionName(`POM · ${state.projectTitle}`);
|
||||
updateChrome(ctx, state, config, runtime);
|
||||
ctx.ui.notify(`POM project created at ${state.projectRoot}`, "info");
|
||||
await runStage(pi, ctx, runtime, "0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "resume") {
|
||||
const path = command.args[0] ?? join(ctx.cwd, "00_admin", "project_state.json");
|
||||
await runtime.set(await loadState(path));
|
||||
const config = await loadPomConfig(runtime.get().projectRoot);
|
||||
updateChrome(ctx, runtime.get(), config, runtime);
|
||||
ctx.ui.notify(summarizeState(runtime.get()), "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "run" || command.verb === "next") {
|
||||
await runStage(pi, ctx, runtime, command.verb === "next" ? "next" : command.args[0] ?? "next");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "hive") {
|
||||
const preset = command.args[0] ?? "stage";
|
||||
if (!["lean", "stage", "audit"].includes(preset)) throw new Error("Hive preset must be lean, stage, or audit");
|
||||
pi.sendUserMessage(`Call pom_hive with {"op":"plan","preset":"${preset}"}. Invoke native task exactly once with its returned batch. Use native IRC for live coordination and pom_hive signal for durable milestones. Integrate, then synthesize the mission.`, { deliverAs: "followUp" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "swarm") {
|
||||
const mode = (command.args[0] ?? "parallel") as PomSwarmMode;
|
||||
if (!["parallel", "sequential", "pipeline"].includes(mode)) throw new Error("Swarm mode must be parallel, sequential, or pipeline");
|
||||
const count = Number(command.args[1] ?? "1");
|
||||
const config = await loadPomConfig(runtime.get().projectRoot);
|
||||
const state = structuredClone(runtime.get());
|
||||
const mission = buildHiveMission(state, config, "stage");
|
||||
state.hive.activeMissionId = mission.id;
|
||||
state.hive.missions[mission.id] = mission;
|
||||
state.stageRuns[String(state.currentStage)].hiveMissionIds.push(mission.id);
|
||||
await runtime.set(state);
|
||||
const missionPath = await writeHiveMission(state, mission);
|
||||
const swarmPath = await writeSwarmDefinition(state, mission, mode, count);
|
||||
await recordHiveEvent(state, { type: "swarm.definition.created", missionId: mission.id, missionPath, swarmPath, mode, count });
|
||||
const swarmAvailable = pi.getCommands().some((item) => item.name === "swarm");
|
||||
const invocation = `/swarm run ${join(state.projectRoot, swarmPath)}`;
|
||||
if (swarmAvailable && ctx.hasUI) {
|
||||
ctx.ui.setEditorText(invocation);
|
||||
ctx.ui.notify(`OMP Swarm DAG prepared. Press Enter to run:
|
||||
${invocation}`, "info");
|
||||
} else {
|
||||
ctx.ui.notify(`Swarm YAML created at ${swarmPath}. Install/enable @oh-my-pi/swarm-extension, then run:
|
||||
${invocation}`, "warning");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "evidence") { await recordEvidenceInteractive(ctx, runtime); return; }
|
||||
|
||||
if (command.verb === "check") {
|
||||
const scope = (command.args[0] ?? (runtime.get().currentStage === 8 ? "delivery" : "stage")) as ValidationScope;
|
||||
if (!["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"].includes(scope)) throw new Error(`Invalid validation scope: ${scope}`);
|
||||
const previous = runtime.get().toolProfile;
|
||||
try {
|
||||
await applyToolProfile(pi, "audit");
|
||||
const report = await validatePom(runtime.get(), scope);
|
||||
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${scope}`, display: true, details: report }, { triggerTurn: false });
|
||||
} finally {
|
||||
await applyToolProfile(pi, previous);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "vault") { ctx.ui.setEditorText("@content/production-board.md "); return; }
|
||||
if (command.verb === "status") { ctx.ui.notify(command.flags.has("json") ? JSON.stringify(runtime.get(), null, 2) : summarizeState(runtime.get()), "info"); return; }
|
||||
|
||||
if (command.verb === "export") {
|
||||
const kind = command.args[0] ?? (runtime.get().currentStage === 8 ? "final" : "checkpoint");
|
||||
if (kind !== "final" && kind !== "checkpoint") throw new Error("Export kind must be checkpoint or final");
|
||||
const result = await exportProject(runtime.get(), kind);
|
||||
ctx.ui.notify(`PASS · ${result.zipPath}\n${result.verification.sha256}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "prompt") {
|
||||
const action = command.args[0] ?? "list";
|
||||
if (action === "list") {
|
||||
const prompts = await listPrompts(runtime.get().initialized ? runtime.get() : undefined);
|
||||
ctx.ui.notify(prompts.map((item) => `${item.id} · ${item.source} · ${item.sha256.slice(0, 12)}`).join("\n"), "info");
|
||||
return;
|
||||
}
|
||||
const id = command.args[1];
|
||||
if (!id) throw new Error("Prompt id required");
|
||||
const prompt = await getPrompt(runtime.get(), id);
|
||||
if (action === "show") await ctx.ui.editor(`POM prompt · ${id}`, prompt.content);
|
||||
else if (action === "run") pi.sendUserMessage(prompt.content, { deliverAs: "followUp" });
|
||||
else throw new Error("Prompt action must be list, show, or run");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "tools") {
|
||||
const action = command.args[0] ?? "show";
|
||||
if (action === "show") { ctx.ui.notify(pi.getActiveTools().join(", "), "info"); return; }
|
||||
const profile = action === "profile" ? command.args[1] : action === "reset" ? "auto" : undefined;
|
||||
if (!profile || !["auto", "creative", "research", "draft", "audit", "delivery"].includes(profile)) throw new Error("Invalid tool profile");
|
||||
const selected = await applyToolProfile(pi, profile as PomToolProfile);
|
||||
const state = structuredClone(runtime.get());
|
||||
state.toolProfile = profile as typeof state.toolProfile;
|
||||
await runtime.set(state);
|
||||
ctx.ui.notify(selected.join(", "), "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "theme") {
|
||||
const action = command.args[0] ?? "install";
|
||||
if (action !== "install") throw new Error("Theme action must be install");
|
||||
const installed = await installBundledThemes();
|
||||
if (ctx.hasUI) ctx.ui.setEditorText("/theme pom-nocturne");
|
||||
ctx.ui.notify(`Installed ${installed.map((item) => item.name).join(", ")}. Run /theme pom-nocturne or press Enter.`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.verb === "settings") { await experienceSettings(ctx, runtime); return; }
|
||||
|
||||
if (command.verb === "doctor") {
|
||||
const report = await validatePom(runtime.get(), "quick");
|
||||
const all = new Set(pi.getAllTools());
|
||||
const required = ["read", "write", "edit", "task", "irc", "todo", "eval", "pom_stage", "pom_hive", "pom_validate"];
|
||||
const missing = required.filter((tool) => !all.has(tool));
|
||||
ctx.ui.notify([
|
||||
`State ${report.passed ? "PASS" : "FAIL"}`,
|
||||
`Tools ${missing.length ? `missing ${missing.join(", ")}` : "PASS"}`,
|
||||
`OMP ${pi.pi ? "API loaded" : "API unavailable"}`,
|
||||
`Project ${runtime.get().projectRoot}`,
|
||||
].join("\n"), report.passed && !missing.length ? "info" : "error");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.notify(`Unknown POM command: ${command.verb}\n\n${HELP}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerShortcut("alt+p", {
|
||||
description: "Open POM command center",
|
||||
handler: async (ctx) => {
|
||||
const action = await openCommandCenter(ctx, runtime.get());
|
||||
await dispatchAction(pi, ctx, runtime, action);
|
||||
},
|
||||
});
|
||||
}
|
||||
69
src/config.ts
Normal file
69
src/config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||
import { DEFAULT_CONFIG, type PomConfig } from "./domain";
|
||||
import { pathExists } from "./persistence";
|
||||
|
||||
async function readJsonObject(path: string): Promise<Record<string, unknown>> {
|
||||
if (!(await pathExists(path))) return {};
|
||||
const text = await readFile(path, "utf8");
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid POM configuration at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`POM configuration must be an object: ${path}`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T, key: string): T {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`Invalid POM setting ${key}: ${String(value)}`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function pickBoolean(value: unknown, fallback: boolean, key: string): boolean {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value !== "boolean") throw new Error(`Invalid POM setting ${key}: expected boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function pickInteger(value: unknown, fallback: number, key: string, min: number, max: number): number {
|
||||
if (value === undefined) return fallback;
|
||||
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) throw new Error(`Invalid POM setting ${key}: expected ${min}..${max}`);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
export async function loadPomConfig(cwd: string): Promise<PomConfig> {
|
||||
const globalConfig = await readJsonObject(join(homedir(), ".omp", "agent", "pom.json"));
|
||||
const projectConfig = await readJsonObject(join(cwd, ".omp", "pom.json"));
|
||||
const merged = { ...globalConfig, ...projectConfig };
|
||||
return {
|
||||
autoToolProfiles: pickBoolean(merged.autoToolProfiles, DEFAULT_CONFIG.autoToolProfiles, "autoToolProfiles"),
|
||||
autoModelRouting: pickBoolean(merged.autoModelRouting, DEFAULT_CONFIG.autoModelRouting, "autoModelRouting"),
|
||||
dashboardWidget: pickBoolean(merged.dashboardWidget, DEFAULT_CONFIG.dashboardWidget, "dashboardWidget"),
|
||||
stopGuard: pickBoolean(merged.stopGuard, DEFAULT_CONFIG.stopGuard, "stopGuard"),
|
||||
defaultMode: pickEnum(merged.defaultMode, ["autopilot", "collaborative", "strict-autopilot"], DEFAULT_CONFIG.defaultMode, "defaultMode"),
|
||||
defaultView: pickEnum(merged.defaultView, ["cinematic", "rich", "compact", "minimal"], DEFAULT_CONFIG.defaultView, "defaultView"),
|
||||
defaultMotion: pickEnum(merged.defaultMotion, ["off", "subtle", "full"], DEFAULT_CONFIG.defaultMotion, "defaultMotion"),
|
||||
defaultToolProfile: pickEnum(merged.defaultToolProfile, ["auto", "creative", "research", "draft", "audit", "delivery"], DEFAULT_CONFIG.defaultToolProfile, "defaultToolProfile"),
|
||||
maxStudioAgents: pickInteger(merged.maxStudioAgents, DEFAULT_CONFIG.maxStudioAgents, "maxStudioAgents", 1, 12),
|
||||
projectRootMode: pickEnum(merged.projectRootMode, ["child", "cwd"], DEFAULT_CONFIG.projectRootMode, "projectRootMode"),
|
||||
destructiveProjectCommands: pickEnum(merged.destructiveProjectCommands, ["block", "allow"], DEFAULT_CONFIG.destructiveProjectCommands, "destructiveProjectCommands"),
|
||||
gitMode: pickEnum(merged.gitMode, ["off", "checkpoint", "branch-per-stage"], DEFAULT_CONFIG.gitMode, "gitMode"),
|
||||
hudPlacement: pickEnum(merged.hudPlacement, ["aboveEditor", "belowEditor"], DEFAULT_CONFIG.hudPlacement, "hudPlacement"),
|
||||
showThinkingLane: pickBoolean(merged.showThinkingLane, DEFAULT_CONFIG.showThinkingLane, "showThinkingLane"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateProjectPomConfig(projectRoot: string, patch: Partial<PomConfig>): Promise<PomConfig> {
|
||||
const path = join(projectRoot, ".omp", "pom.json");
|
||||
const current = await readJsonObject(path);
|
||||
const next = { ...current, ...patch };
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
||||
await writeFile(temp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
await rename(temp, path);
|
||||
return loadPomConfig(projectRoot);
|
||||
}
|
||||
501
src/domain.ts
Normal file
501
src/domain.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
export const POM_VERSION = "2.0.0";
|
||||
export const POM_SCHEMA_VERSION = "2.0.0" as const;
|
||||
|
||||
export type PomMode = "autopilot" | "collaborative" | "strict-autopilot";
|
||||
export type PomView = "cinematic" | "rich" | "compact" | "minimal";
|
||||
export type PomMotion = "off" | "subtle" | "full";
|
||||
export type PomToolProfile = "auto" | "creative" | "research" | "draft" | "audit" | "delivery";
|
||||
export type PomStageStatus = "queued" | "active" | "blocked" | "complete";
|
||||
export type ValidationScope = "quick" | "stage" | "canon" | "continuity" | "knowledge" | "files" | "delivery" | "all";
|
||||
|
||||
export interface TodoPhase {
|
||||
phase: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export interface ArtifactRequirement {
|
||||
path: string;
|
||||
kind?: "file" | "directory";
|
||||
description: string;
|
||||
minBytes?: number;
|
||||
minEntries?: number;
|
||||
requiredHeadings?: string[];
|
||||
requiredPatterns?: string[];
|
||||
forbiddenPatterns?: string[];
|
||||
}
|
||||
|
||||
export interface AcceptanceCriterion {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface StageSpec {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
toolProfile: PomToolProfile;
|
||||
modelRole: string;
|
||||
agents: string[];
|
||||
artifacts: ArtifactRequirement[];
|
||||
acceptance: AcceptanceCriterion[];
|
||||
todos: TodoPhase[];
|
||||
}
|
||||
|
||||
const phase = (phaseName: string, ...items: string[]): TodoPhase => ({ phase: phaseName, items });
|
||||
const criterion = (id: string, label: string, description: string): AcceptanceCriterion => ({ id, label, description });
|
||||
|
||||
export const STAGES: readonly StageSpec[] = [
|
||||
{
|
||||
id: 0,
|
||||
slug: "init",
|
||||
name: "Project Initialization",
|
||||
icon: "🏗",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-architect"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "01_planning/00_project_brief.md",
|
||||
description: "Authoritative project brief",
|
||||
minBytes: 900,
|
||||
requiredHeadings: ["Vision", "Audience", "Constraints", "Success Criteria", "Open Questions"],
|
||||
forbiddenPatterns: ["TBD", "TODO", "lorem ipsum"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("identity", "Identity resolved", "Title, format, audience, intended scope, and canonical root are explicit."),
|
||||
criterion("north-star", "North star defined", "The project has a concise creative promise and measurable success criteria."),
|
||||
criterion("ambiguity", "Foundational ambiguity cleared", "Blocking unknowns are resolved or represented as explicit blockers."),
|
||||
],
|
||||
todos: [
|
||||
phase("Foundation", "Resolve project identity", "Define creative north star", "Record constraints and assumptions"),
|
||||
phase("Artifacts", "Create project brief", "Initialize vault and ledgers", "Validate stage 0"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
slug: "genre",
|
||||
name: "Genre Architecture",
|
||||
icon: "🎭",
|
||||
toolProfile: "research",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-researcher", "pom-concept-forge", "pom-critic"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "01_planning/01_genre_grid.md",
|
||||
description: "Genre landscape and promise map",
|
||||
minBytes: 1400,
|
||||
requiredHeadings: ["Candidate Genres", "Audience Promise", "Cliche Risks", "Differentiation"],
|
||||
},
|
||||
{
|
||||
path: "01_planning/02_genre_fusion_matrix.md",
|
||||
description: "Ranked fusion matrix",
|
||||
minBytes: 1000,
|
||||
requiredHeadings: ["Scoring Model", "Matrix", "Recommendation"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("genre-selected", "Primary genre selected", "One primary genre and optional secondary genres are explicitly locked."),
|
||||
criterion("promise", "Genre promise recorded", "Reader expectations, pleasures, and boundaries are stated."),
|
||||
criterion("cliche-risk", "Cliche risks documented", "High-risk conventions and specific differentiation strategies are recorded."),
|
||||
],
|
||||
todos: [
|
||||
phase("Research", "Map genre candidates", "Research audience expectations", "Analyze cliches and differentiation"),
|
||||
phase("Selection", "Build fusion matrix", "Rank directions", "Lock genre promise"),
|
||||
phase("Gate", "Save artifacts", "Record acceptance evidence", "Validate stage 1"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
slug: "voice",
|
||||
name: "Author Persona and Voice",
|
||||
icon: "✒",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-voice-architect", "pom-rights-auditor"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "02_story_bible/01_author_persona.md",
|
||||
description: "Original author persona",
|
||||
minBytes: 1200,
|
||||
requiredHeadings: ["Creative Identity", "Audience Relationship", "Aesthetic Philosophy", "Boundaries"],
|
||||
},
|
||||
{
|
||||
path: "02_story_bible/02_voice_guide.md",
|
||||
description: "Testable voice system",
|
||||
minBytes: 1600,
|
||||
requiredHeadings: ["Voice Profile", "Always", "Avoid", "Syntax", "Diction", "Sample"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("persona", "Original persona stable", "The persona is internally coherent and not a disguised living-author imitation."),
|
||||
criterion("voice-laws", "Voice laws testable", "Voice rules can be checked against prose with concrete positive and negative examples."),
|
||||
criterion("rights", "Imitation risk cleared", "Protected-author imitation and misleading attribution risks are explicitly audited."),
|
||||
],
|
||||
todos: [
|
||||
phase("Persona", "Design original persona", "Define audience relationship", "Audit rights boundaries"),
|
||||
phase("Voice", "Define voice profile", "Write always/avoid laws", "Create original sample"),
|
||||
phase("Gate", "Save artifacts", "Record acceptance evidence", "Validate stage 2"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
slug: "store",
|
||||
name: "Story Store",
|
||||
icon: "📚",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-concept-forge", "pom-critic"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "01_planning/04_story_store.md",
|
||||
description: "Twenty structurally diverse concepts",
|
||||
minBytes: 3800,
|
||||
requiredHeadings: ["Concepts", "Scoring", "Top Five"],
|
||||
requiredPatterns: ["Concept 20"],
|
||||
},
|
||||
{
|
||||
path: "01_planning/05_selected_direction.md",
|
||||
description: "Selected and strengthened premise",
|
||||
minBytes: 1400,
|
||||
requiredHeadings: ["Premise", "Protagonist", "Conflict", "Stakes", "Distinctive Engine", "Why This Wins"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("twenty", "Twenty concepts produced", "Exactly twenty meaningfully different concepts exist and are individually inspectable."),
|
||||
criterion("winner", "Winner selected", "A transparent scoring and comparison process selects the strongest direction."),
|
||||
criterion("premise", "Premise stable", "The selected premise contains character, conflict, stakes, engine, and differentiation."),
|
||||
],
|
||||
todos: [
|
||||
phase("Generate", "Generate twenty diverse concepts", "Score concept potential", "Reject cosmetic duplicates"),
|
||||
phase("Select", "Rank strongest five", "Compare top three", "Strengthen winner"),
|
||||
phase("Gate", "Save story store", "Save selected direction", "Validate stage 3"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
slug: "bible",
|
||||
name: "Story Blueprint and Bible",
|
||||
icon: "🌐",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-story-architect", "pom-character-world", "pom-continuity-auditor", "pom-visual-director"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "02_story_bible/00_story_bible_index.md",
|
||||
description: "Story-bible map of content",
|
||||
minBytes: 800,
|
||||
requiredHeadings: ["Canon", "Characters", "World", "Structure", "Visual Language"],
|
||||
},
|
||||
{
|
||||
path: "02_story_bible/13_canon_ledger.md",
|
||||
description: "Atomic canon ledger",
|
||||
minBytes: 1600,
|
||||
requiredHeadings: ["Characters", "World Rules", "Timeline", "Objects", "Unresolved"],
|
||||
},
|
||||
{
|
||||
path: "02_story_bible/16_visual_bible.md",
|
||||
description: "Visual system and reference policy",
|
||||
minBytes: 1400,
|
||||
requiredHeadings: ["Visual Thesis", "Palette", "Composition", "Motifs", "Rights"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("ending", "Ending known", "The ending and its causal prerequisites are explicit."),
|
||||
criterion("arcs", "Major arcs defined", "Primary character, relationship, mystery, and thematic arcs have start, turns, and resolution."),
|
||||
criterion("world", "World rules stable", "Rules, limits, costs, and exceptions are documented without unresolved contradiction."),
|
||||
criterion("outline-ready", "Canon supports outline", "The bible contains enough stable information to build a causal outline."),
|
||||
],
|
||||
todos: [
|
||||
phase("Identity", "Finalize story identity", "Write ending synopsis", "Define dramatic questions"),
|
||||
phase("Bible", "Build character bible", "Build world rules", "Build themes and visual language"),
|
||||
phase("Canon", "Create timeline and ledgers", "Map mysteries and payoffs", "Resolve contradictions"),
|
||||
phase("Gate", "Record evidence", "Run continuity audit", "Validate stage 4"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
slug: "outline",
|
||||
name: "Master Story Outline",
|
||||
icon: "🧩",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-structure-engineer", "pom-continuity-auditor", "pom-critic"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "03_manuscript/00_master_outline.md",
|
||||
description: "Causal master outline",
|
||||
minBytes: 3200,
|
||||
requiredHeadings: ["Act I", "Act II", "Act III", "Ending", "Causality Audit"],
|
||||
},
|
||||
{
|
||||
path: "03_manuscript/02_scene_index.md",
|
||||
description: "Scene-level causal index",
|
||||
minBytes: 2600,
|
||||
requiredHeadings: ["Scene Index", "Setups", "Payoffs", "State Changes"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("ending-represented", "Ending represented", "The final outcome and its setup chain are represented in the outline."),
|
||||
criterion("causality", "Causality survives review", "Major beats arise from prior choices, pressures, and consequences rather than convenience."),
|
||||
criterion("setup-payoff", "Setups mapped", "Every major payoff has a setup, escalation, and resolution path."),
|
||||
],
|
||||
todos: [
|
||||
phase("Structure", "Design acts and sequences", "Map turning points", "Allocate narrative weight"),
|
||||
phase("Scenes", "Build causal scene index", "Track state changes", "Link setups and payoffs"),
|
||||
phase("Audit", "Audit agency and escalation", "Repair weak causality", "Validate stage 5"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
slug: "chapters",
|
||||
name: "Chapter Architecture",
|
||||
icon: "📑",
|
||||
toolProfile: "creative",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-structure-engineer", "pom-continuity-auditor", "pom-visual-director"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "03_manuscript/01_chapter_outline.md",
|
||||
description: "Complete chapter architecture",
|
||||
minBytes: 3000,
|
||||
requiredHeadings: ["Chapter Map", "POV Plan", "Pacing", "Continuity Audit"],
|
||||
},
|
||||
{
|
||||
path: "03_manuscript/02_scene_index.md",
|
||||
description: "Updated scene index",
|
||||
minBytes: 3200,
|
||||
requiredHeadings: ["Scene Index", "Chapter Allocation", "Reveals", "Payoffs"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("mapped", "All chapters mapped", "Every intended chapter has objective, conflict, change, and exit state."),
|
||||
criterion("pov", "POV logic stable", "POV assignment and transitions have explicit dramatic purpose."),
|
||||
criterion("gaps", "No narrative gaps", "Timeline, reveal order, and causal transitions survive review."),
|
||||
],
|
||||
todos: [
|
||||
phase("Architecture", "Map every chapter", "Define state changes", "Balance POV and pacing"),
|
||||
phase("Continuity", "Verify transitions", "Verify reveals and payoffs", "Repair gaps and redundancies"),
|
||||
phase("Gate", "Update scene index", "Record evidence", "Validate stage 6"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
slug: "draft",
|
||||
name: "Drafting and Revision",
|
||||
icon: "📜",
|
||||
toolProfile: "draft",
|
||||
modelRole: "pi/default",
|
||||
agents: ["pom-prose-smith", "pom-developmental-editor", "pom-continuity-auditor"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "03_manuscript/chapters",
|
||||
kind: "directory",
|
||||
description: "Individual drafted chapters",
|
||||
minEntries: 1,
|
||||
},
|
||||
{
|
||||
path: "03_manuscript/03_complete_manuscript.md",
|
||||
description: "Compiled complete manuscript",
|
||||
minBytes: 8000,
|
||||
requiredHeadings: ["Chapter"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("complete", "All planned chapters complete", "The chapter map and chapter files reconcile without missing planned content."),
|
||||
criterion("counts", "Word counts verified", "Measured word counts are recorded and reconcile with the compiled manuscript."),
|
||||
criterion("continuity-resolved", "Continuity resolved", "Known continuity defects are resolved or explicitly disclosed as blockers."),
|
||||
],
|
||||
todos: [
|
||||
phase("Preflight", "Load chapter dependencies", "Verify canon", "Set chapter objective"),
|
||||
phase("Draft", "Draft chapter", "Preserve voice and POV", "End in changed state"),
|
||||
phase("Postflight", "Measure words", "Run voice and repetition checks", "Update ledgers"),
|
||||
phase("Manuscript", "Compile manuscript", "Run developmental review", "Validate stage 7"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
slug: "delivery",
|
||||
name: "Final Compilation and Delivery",
|
||||
icon: "📦",
|
||||
toolProfile: "delivery",
|
||||
modelRole: "pi/slow",
|
||||
agents: ["pom-delivery-auditor", "pom-rights-auditor", "pom-continuity-auditor"],
|
||||
artifacts: [
|
||||
{
|
||||
path: "00_admin/artifact_manifest.json",
|
||||
description: "Curated delivery manifest",
|
||||
minBytes: 64,
|
||||
},
|
||||
{
|
||||
path: "00_admin/SHA256SUMS.txt",
|
||||
description: "Checksums for packaged artifacts",
|
||||
minBytes: 64,
|
||||
},
|
||||
{
|
||||
path: "07_exports/final_delivery_report.md",
|
||||
description: "Truthful delivery report",
|
||||
minBytes: 900,
|
||||
requiredHeadings: ["Contents", "Validation", "Warnings", "Omissions", "Archive"],
|
||||
},
|
||||
],
|
||||
acceptance: [
|
||||
criterion("requested", "Requested files exist", "All promised deliverables exist as non-empty regular files."),
|
||||
criterion("hashes", "Manifest and hashes match", "Manifest metadata and SHA-256 values match canonical files."),
|
||||
criterion("archive", "Fresh archive verified", "A newly created archive is parsed and every member is decompressed and checked."),
|
||||
criterion("disclosure", "Omissions disclosed", "Warnings, limitations, rights status, and omissions are explicit."),
|
||||
],
|
||||
todos: [
|
||||
phase("Compile", "Compile manuscript", "Assemble requested exports", "Assemble approved visuals"),
|
||||
phase("Validate", "Run structure checks", "Run continuity and rights checks", "Verify manifest and hashes"),
|
||||
phase("Package", "Create fresh archive", "Verify every member", "Write delivery report"),
|
||||
phase("Deliver", "Expose real paths", "Disclose warnings and omissions", "Validate stage 8"),
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export interface ArtifactRecord {
|
||||
path: string;
|
||||
category: string;
|
||||
stage: number;
|
||||
version: string;
|
||||
status: "draft" | "reviewed" | "approved" | "superseded";
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
updatedAt: string;
|
||||
rightsStatus: "original" | "licensed" | "public-domain" | "reference-only" | "unknown";
|
||||
source?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface EvidenceRecord {
|
||||
id: string;
|
||||
criterionId: string;
|
||||
summary: string;
|
||||
paths: string[];
|
||||
agent?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface StageBlocker {
|
||||
id: string;
|
||||
reason: string;
|
||||
owner?: string;
|
||||
createdAt: string;
|
||||
resolvedAt?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface StageRunState {
|
||||
status: PomStageStatus;
|
||||
attempts: number;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
blockers: StageBlocker[];
|
||||
evidence: EvidenceRecord[];
|
||||
validationReportPath?: string;
|
||||
hiveMissionIds: string[];
|
||||
}
|
||||
|
||||
export interface HiveTask {
|
||||
name: string;
|
||||
agent: string;
|
||||
objective: string;
|
||||
owns: string[];
|
||||
acceptance: string[];
|
||||
isolated?: boolean;
|
||||
}
|
||||
|
||||
export interface HiveMission {
|
||||
id: string;
|
||||
stage: number;
|
||||
title: string;
|
||||
status: "planned" | "running" | "synthesizing" | "complete" | "blocked";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tasks: HiveTask[];
|
||||
synthesis?: string;
|
||||
taskToolCallId?: string;
|
||||
}
|
||||
|
||||
export interface PomState {
|
||||
schemaVersion: typeof POM_SCHEMA_VERSION;
|
||||
pomVersion: string;
|
||||
projectId: string;
|
||||
projectTitle: string;
|
||||
projectSlug: string;
|
||||
projectRoot: string;
|
||||
initialized: boolean;
|
||||
currentStage: number;
|
||||
stageRuns: Record<string, StageRunState>;
|
||||
mode: PomMode;
|
||||
view: PomView;
|
||||
motion: PomMotion;
|
||||
toolProfile: PomToolProfile;
|
||||
autoToolProfiles: boolean;
|
||||
autoModelRouting: boolean;
|
||||
completedStages: number[];
|
||||
decisions: string[];
|
||||
assumptions: string[];
|
||||
warnings: string[];
|
||||
activeRun?: { id: string; stage: number; startedAt: string; continuationCount: number; allStages: boolean };
|
||||
hive: {
|
||||
activeMissionId?: string;
|
||||
missions: Record<string, HiveMission>;
|
||||
signalsLogged: number;
|
||||
};
|
||||
artifactVersion: string;
|
||||
canonVersion: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PomConfig {
|
||||
autoToolProfiles: boolean;
|
||||
autoModelRouting: boolean;
|
||||
dashboardWidget: boolean;
|
||||
stopGuard: boolean;
|
||||
defaultMode: PomMode;
|
||||
defaultView: PomView;
|
||||
defaultMotion: PomMotion;
|
||||
defaultToolProfile: PomToolProfile;
|
||||
maxStudioAgents: number;
|
||||
projectRootMode: "child" | "cwd";
|
||||
destructiveProjectCommands: "block" | "allow";
|
||||
gitMode: "off" | "checkpoint" | "branch-per-stage";
|
||||
hudPlacement: "aboveEditor" | "belowEditor";
|
||||
showThinkingLane: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: PomConfig = {
|
||||
autoToolProfiles: true,
|
||||
autoModelRouting: true,
|
||||
dashboardWidget: true,
|
||||
stopGuard: true,
|
||||
defaultMode: "autopilot",
|
||||
defaultView: "cinematic",
|
||||
defaultMotion: "subtle",
|
||||
defaultToolProfile: "auto",
|
||||
maxStudioAgents: 6,
|
||||
projectRootMode: "child",
|
||||
destructiveProjectCommands: "block",
|
||||
gitMode: "branch-per-stage",
|
||||
hudPlacement: "aboveEditor",
|
||||
showThinkingLane: true,
|
||||
};
|
||||
|
||||
export function stage(id: number): StageSpec {
|
||||
const value = STAGES[id];
|
||||
if (!value) throw new Error(`Invalid POM stage: ${id}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function freshStageRuns(): Record<string, StageRunState> {
|
||||
return Object.fromEntries(
|
||||
STAGES.map((item) => [
|
||||
String(item.id),
|
||||
{ status: "queued", attempts: 0, blockers: [], evidence: [], hiveMissionIds: [] } satisfies StageRunState,
|
||||
]),
|
||||
);
|
||||
}
|
||||
208
src/events.ts
Normal file
208
src/events.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stage } from "./domain";
|
||||
import { addBlocker } from "./state";
|
||||
import { composePromptStack } from "./prompts";
|
||||
import { applyToolProfile } from "./tool-profiles";
|
||||
import { routeStage } from "./router";
|
||||
import { clearChrome, updateChrome } from "./ui";
|
||||
import { recordHiveEvent, syncKnowledgeBase } from "./persistence";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
import { shouldBlockProjectCommand } from "./policy";
|
||||
|
||||
async function chrome(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
|
||||
const state = runtime.get();
|
||||
const config = await runtime.refreshConfig(state.initialized ? state.projectRoot : ctx.cwd);
|
||||
updateChrome(ctx, state, config, runtime);
|
||||
}
|
||||
|
||||
export function registerPomEvents(pi: ExtensionAPI, runtime: PomRuntime): void {
|
||||
pi.on("resources_discover", async () => ({
|
||||
skillPaths: [fileURLToPath(new URL("../skills", import.meta.url))],
|
||||
promptPaths: [fileURLToPath(new URL("../prompts", import.meta.url))],
|
||||
themePaths: [fileURLToPath(new URL("../themes", import.meta.url))],
|
||||
}));
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const state = await runtime.restore(ctx);
|
||||
await chrome(ctx, runtime);
|
||||
if (state.initialized) {
|
||||
if (state.activeRun) await routeStage(pi, ctx, state);
|
||||
else if (state.autoToolProfiles && state.toolProfile !== "auto") await applyToolProfile(pi, state.toolProfile);
|
||||
const currentName = pi.getSessionName();
|
||||
if (!currentName) await pi.setSessionName(`POM · ${state.projectTitle}`);
|
||||
}
|
||||
});
|
||||
|
||||
const restoreChrome = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
|
||||
await runtime.restore(ctx);
|
||||
await chrome(ctx, runtime);
|
||||
};
|
||||
pi.on("session_branch", restoreChrome);
|
||||
pi.on("session_tree", restoreChrome);
|
||||
pi.on("session_switch", restoreChrome);
|
||||
|
||||
pi.on("before_agent_start", async (event) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized || !state.activeRun) return;
|
||||
const stack = await composePromptStack(state, ["stage-run"], {
|
||||
STAGE_CONTRACT: JSON.stringify(stage(state.currentStage), null, 2),
|
||||
});
|
||||
return {
|
||||
systemPrompt: [...event.systemPrompt, stack.rendered],
|
||||
message: {
|
||||
customType: "pom:event",
|
||||
content: `Stage ${state.currentStage} production context injected`,
|
||||
display: false,
|
||||
details: { title: `POM stage ${state.currentStage} active`, level: "info", lines: [`root ${state.projectRoot}`, `prompt ${stack.sha256.slice(0, 12)}`] },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("tool_execution_start", async (event, ctx) => {
|
||||
runtime.telemetry.activeTool = event.toolName;
|
||||
runtime.telemetry.activeIntent = event.intent;
|
||||
runtime.telemetry.toolStartedAt = Date.now();
|
||||
ctx.ui.setWorkingMessage(`POM · ${event.toolName}${event.intent ? ` · ${event.intent}` : ""}`);
|
||||
await chrome(ctx, runtime);
|
||||
});
|
||||
|
||||
pi.on("tool_execution_update", async (event, ctx) => {
|
||||
if (event.toolName === "task") ctx.ui.setWorkingMessage("POM hive · specialists working…");
|
||||
else if (event.toolName === "pom_validate") ctx.ui.setWorkingMessage("POM gate · verifying evidence…");
|
||||
});
|
||||
|
||||
pi.on("tool_execution_end", async (event, ctx) => {
|
||||
runtime.telemetry.lastToolResult = event.isError ? "fail" : "pass";
|
||||
runtime.telemetry.activeTool = undefined;
|
||||
runtime.telemetry.activeIntent = undefined;
|
||||
runtime.telemetry.toolStartedAt = undefined;
|
||||
ctx.ui.setWorkingMessage();
|
||||
await chrome(ctx, runtime);
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized) return;
|
||||
const config = await runtime.refreshConfig(state.projectRoot);
|
||||
if (event.toolName === "bash" && config.destructiveProjectCommands === "block") {
|
||||
const command = String(event.input.command ?? "");
|
||||
if (shouldBlockProjectCommand(command, state.projectRoot, state.projectSlug, String(event.input.cwd ?? ctx.cwd))) {
|
||||
ctx.ui.notify("POM blocked a destructive command against the canonical production root.", "error");
|
||||
return { block: true, reason: "POM canonical-root policy blocks destructive recursive deletion or history destruction. Use versioned, targeted repairs." };
|
||||
}
|
||||
}
|
||||
if (event.toolName === "task") {
|
||||
const next = structuredClone(state);
|
||||
const missionId = next.hive.activeMissionId;
|
||||
if (missionId && next.hive.missions[missionId]) {
|
||||
next.hive.missions[missionId].status = "running";
|
||||
next.hive.missions[missionId].taskToolCallId = event.toolCallId;
|
||||
next.hive.missions[missionId].updatedAt = new Date().toISOString();
|
||||
runtime.telemetry.activeAgents = next.hive.missions[missionId].tasks.length;
|
||||
runtime.telemetry.completedAgents = 0;
|
||||
await runtime.set(next);
|
||||
await recordHiveEvent(next, { type: "native.task.started", missionId, toolCallId: event.toolCallId, input: event.input });
|
||||
}
|
||||
}
|
||||
if (event.toolName === "irc") {
|
||||
const input = event.input;
|
||||
if (input.op === "send") {
|
||||
runtime.telemetry.lastIrcSignal = `${String(input.to ?? "peer")}: ${String(input.message ?? "")}`;
|
||||
await recordHiveEvent(state, { type: "native.irc.send", toolCallId: event.toolCallId, to: input.to, message: input.message, replyTo: input.replyTo });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("tool_result", async (event) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized) return;
|
||||
if (event.toolName === "task") {
|
||||
const next = structuredClone(state);
|
||||
const missionId = next.hive.activeMissionId;
|
||||
if (missionId && next.hive.missions[missionId]) {
|
||||
next.hive.missions[missionId].status = event.isError ? "blocked" : "synthesizing";
|
||||
next.hive.missions[missionId].updatedAt = new Date().toISOString();
|
||||
runtime.telemetry.completedAgents = event.isError ? 0 : next.hive.missions[missionId].tasks.length;
|
||||
await runtime.set(next);
|
||||
await recordHiveEvent(next, { type: "native.task.finished", missionId, toolCallId: event.toolCallId, isError: event.isError, details: event.details });
|
||||
}
|
||||
}
|
||||
if (event.toolName === "irc") {
|
||||
await recordHiveEvent(state, { type: "native.irc.result", toolCallId: event.toolCallId, isError: event.isError, input: event.input, details: event.details });
|
||||
}
|
||||
if (event.isError && event.toolName.startsWith("pom_")) {
|
||||
const next = structuredClone(state);
|
||||
const warning = `${event.toolName} failed at ${new Date().toISOString()}`;
|
||||
if (!next.warnings.includes(warning)) next.warnings.push(warning);
|
||||
await runtime.set(next);
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session.compacting", async () => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized) return;
|
||||
const spec = stage(state.currentStage);
|
||||
const run = state.stageRuns[String(state.currentStage)];
|
||||
return {
|
||||
context: [
|
||||
`POM project ${state.projectTitle} at ${state.projectRoot}`,
|
||||
`Stage ${spec.id}/8 ${spec.name}; status=${run.status}; evidence=${run.evidence.length}/${spec.acceptance.length}`,
|
||||
`Canonical state path: ${state.projectRoot}/00_admin/project_state.json`,
|
||||
`Active hive: ${state.hive.activeMissionId ?? "none"}`,
|
||||
],
|
||||
preserveData: { pomState: state },
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("auto_compaction_start", async (_event, ctx) => ctx.ui.setWorkingMessage("POM · preserving canon and production state…"));
|
||||
pi.on("auto_compaction_end", async (_event, ctx) => ctx.ui.setWorkingMessage());
|
||||
pi.on("auto_retry_start", async (event, ctx) => ctx.ui.setStatus("pom-retry", `↻ retry ${event.attempt}`));
|
||||
pi.on("auto_retry_end", async (_event, ctx) => ctx.ui.setStatus("pom-retry", undefined));
|
||||
pi.on("tool_approval_requested", async (event, ctx) => ctx.ui.setStatus("pom-approval", `? ${event.toolName}`));
|
||||
pi.on("tool_approval_resolved", async (_event, ctx) => ctx.ui.setStatus("pom-approval", undefined));
|
||||
pi.on("goal_updated", async (event, ctx) => ctx.ui.setStatus("pom-goal", event.goal ? "◎ goal active" : undefined));
|
||||
pi.on("todo_reminder", async (_event, ctx) => ctx.ui.notify("POM production still has open native todos. Complete or explicitly drop them before the gate.", "warning"));
|
||||
|
||||
pi.on("ttsr_triggered", async (event) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized) return;
|
||||
const next = structuredClone(state);
|
||||
const warning = `TTSR triggered: ${event.rules.map((rule) => rule.name).join(", ")}`;
|
||||
if (!next.warnings.includes(warning)) next.warnings.push(warning);
|
||||
await runtime.set(next);
|
||||
});
|
||||
|
||||
pi.on("credential_disabled", async (event, ctx) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized) return;
|
||||
const next = structuredClone(state);
|
||||
const warning = `Credential disabled: ${String(event.provider)}`;
|
||||
if (!next.warnings.includes(warning)) next.warnings.push(warning);
|
||||
await runtime.set(next);
|
||||
ctx.ui.notify(warning, "warning");
|
||||
});
|
||||
|
||||
pi.on("session_stop", async (_event, ctx) => {
|
||||
const state = runtime.get();
|
||||
if (!state.initialized || !state.activeRun) return;
|
||||
const config = await runtime.refreshConfig(state.projectRoot);
|
||||
if (!config.stopGuard) return;
|
||||
if (state.activeRun.continuationCount < 2) {
|
||||
const next = structuredClone(state);
|
||||
if (next.activeRun) next.activeRun.continuationCount += 1;
|
||||
await runtime.set(next);
|
||||
return {
|
||||
continue: true,
|
||||
additionalContext: "POM stage is still active. Finish the native todo, synthesize the hive, register artifacts, record every acceptance criterion, run pom_validate, then call pom_stage pass or block with exact evidence. Do not stop ambiguously.",
|
||||
};
|
||||
}
|
||||
const blocked = addBlocker(state, `Stage ${state.currentStage} stopped without PASS or an explicit blocker after two guarded continuations`, "Main");
|
||||
await runtime.set(blocked);
|
||||
await syncKnowledgeBase(blocked);
|
||||
pi.sendMessage({ customType: "pom:milestone", content: "Stage blocked by stop guard", display: true, details: { stage: state.currentStage, status: "BLOCKED" } }, { triggerTurn: false });
|
||||
await chrome(ctx, runtime);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async (_event, ctx) => clearChrome(ctx));
|
||||
}
|
||||
106
src/exporter.ts
Normal file
106
src/exporter.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { atomicWrite, loadManifest, pathExists, sha256File } from "./persistence";
|
||||
import { validatePom } from "./validators";
|
||||
import { createZip, verifyZip, type ZipVerification } from "./zip";
|
||||
import type { PomState } from "./domain";
|
||||
|
||||
export interface ExportResult {
|
||||
zipPath: string;
|
||||
verificationPath: string;
|
||||
reportPath: string;
|
||||
verification: ZipVerification;
|
||||
}
|
||||
|
||||
async function collectKnowledgeFiles(state: PomState): Promise<string[]> {
|
||||
const root = join(state.projectRoot, "content");
|
||||
if (!(await pathExists(root))) return [];
|
||||
return (await readdir(root)).filter((name) => name.endsWith(".md")).map((name) => `content/${name}`);
|
||||
}
|
||||
|
||||
export async function exportProject(state: PomState, kind: "checkpoint" | "final"): Promise<ExportResult> {
|
||||
const manifest = await loadManifest(state);
|
||||
if (!manifest.length) throw new Error("Cannot export an empty artifact manifest");
|
||||
|
||||
const preflightScopes = kind === "final" ? (["files", "knowledge", "continuity"] as const) : (["files"] as const);
|
||||
const preflightReports = [];
|
||||
for (const scope of preflightScopes) preflightReports.push(await validatePom(state, scope, { writeReport: false }));
|
||||
const preflightFailures = preflightReports.flatMap((report) => report.checks.filter((item) => item.status === "fail"));
|
||||
if (kind === "final") {
|
||||
const incomplete = Array.from({ length: 8 }, (_value, index) => index).filter((id) => state.stageRuns[String(id)]?.status !== "complete");
|
||||
if (incomplete.length) throw new Error(`Final export requires stages 0–7 complete; incomplete: ${incomplete.join(", ")}`);
|
||||
const blockers = Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((blocker) => !blocker.resolvedAt);
|
||||
if (blockers.length) throw new Error(`Final export blocked by ${blockers.length} unresolved blocker(s)`);
|
||||
}
|
||||
if (preflightFailures.length) throw new Error(`Export preflight failed with ${preflightFailures.length} error(s)`);
|
||||
const preflight = {
|
||||
scope: preflightScopes.join("+"),
|
||||
passed: preflightFailures.length === 0,
|
||||
checks: preflightReports.flatMap((report) => report.checks),
|
||||
};
|
||||
|
||||
const exportRoot = join(state.projectRoot, "07_exports");
|
||||
const base = `${state.projectSlug}-${kind}`;
|
||||
const reportRelative = kind === "final" ? "07_exports/final_delivery_report.md" : `07_exports/${base}-delivery-report.md`;
|
||||
const reportPath = join(state.projectRoot, reportRelative);
|
||||
const checksumsRelative = "00_admin/SHA256SUMS.txt";
|
||||
const checksumsPath = join(state.projectRoot, checksumsRelative);
|
||||
const memberPaths = [...new Set([
|
||||
...manifest.map((record) => record.path),
|
||||
"00_admin/artifact_manifest.json",
|
||||
"00_admin/project_state.json",
|
||||
...await collectKnowledgeFiles(state),
|
||||
])].sort();
|
||||
|
||||
const checksumLines: string[] = [];
|
||||
for (const path of memberPaths) checksumLines.push(`${await sha256File(join(state.projectRoot, path))} ${path}`);
|
||||
await atomicWrite(checksumsPath, `${checksumLines.join("\n")}\n`);
|
||||
|
||||
const report = [
|
||||
"---",
|
||||
`title: ${JSON.stringify(`${state.projectTitle} Delivery Report`)}`,
|
||||
"type: delivery-report",
|
||||
"tags: [pom, delivery]",
|
||||
"publish: true",
|
||||
"---",
|
||||
"",
|
||||
`# ${state.projectTitle} Delivery Report`,
|
||||
"",
|
||||
"## Contents",
|
||||
"",
|
||||
...memberPaths.map((path) => `- \`${path}\``),
|
||||
"",
|
||||
"## Validation",
|
||||
"",
|
||||
`Preflight scope: **${preflight.scope}**`,
|
||||
`Preflight result: **${preflight.passed ? "PASS" : "FAIL"}**`,
|
||||
`Checks: ${preflight.checks.length}`,
|
||||
"",
|
||||
"## Warnings",
|
||||
"",
|
||||
...(state.warnings.length ? state.warnings.map((warning) => `- ${warning}`) : ["- None recorded."]),
|
||||
"",
|
||||
"## Omissions",
|
||||
"",
|
||||
"- Archive SHA-256 and member-by-member decompression results are written to the adjacent verification sidecar after archive creation. They cannot be embedded in the archive without changing the archive hash.",
|
||||
"",
|
||||
"## Archive",
|
||||
"",
|
||||
`Planned archive: \`${base}.zip\``,
|
||||
"Verification requirement: central-directory parse, safe names, supported compression, decompression, size, CRC-32, member count, and archive SHA-256.",
|
||||
].join("\n");
|
||||
await atomicWrite(reportPath, `${report}\n`);
|
||||
|
||||
const packagePaths = [...memberPaths, checksumsRelative, reportRelative];
|
||||
const zipPath = join(exportRoot, `${base}.zip`);
|
||||
await createZip(zipPath, packagePaths.map((path) => ({ absolutePath: join(state.projectRoot, path), archivePath: path })));
|
||||
const verification = await verifyZip(zipPath);
|
||||
const verificationPath = join(exportRoot, `${base}.verification.json`);
|
||||
await atomicWrite(verificationPath, `${JSON.stringify({
|
||||
schemaVersion: "1.0.0",
|
||||
generatedAt: new Date().toISOString(),
|
||||
archive: basename(zipPath),
|
||||
...verification,
|
||||
}, null, 2)}\n`);
|
||||
return { zipPath, verificationPath, reportPath, verification };
|
||||
}
|
||||
43
src/git.ts
Normal file
43
src/git.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { join } from "node:path";
|
||||
import type { PomConfig, PomState } from "./domain";
|
||||
import { stage } from "./domain";
|
||||
import { pathExists } from "./persistence";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function git(root: string, args: string[]): Promise<string> {
|
||||
const result = await execFileAsync("git", ["-C", root, ...args], { maxBuffer: 4 * 1024 * 1024 });
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
export async function ensureGit(state: PomState, config: PomConfig): Promise<void> {
|
||||
if (config.gitMode === "off") return;
|
||||
if (!(await pathExists(join(state.projectRoot, ".git")))) {
|
||||
await git(state.projectRoot, ["init"]);
|
||||
await git(state.projectRoot, ["config", "user.name", "POM Production Kernel"]);
|
||||
await git(state.projectRoot, ["config", "user.email", "pom@local.invalid"]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function enterStageBranch(state: PomState, config: PomConfig): Promise<string | undefined> {
|
||||
if (config.gitMode !== "branch-per-stage") return undefined;
|
||||
await ensureGit(state, config);
|
||||
const spec = stage(state.currentStage);
|
||||
const branch = `pom/stage-${spec.id}-${spec.slug}`;
|
||||
const branches = await git(state.projectRoot, ["branch", "--format=%(refname:short)"]);
|
||||
if (branches.split("\n").includes(branch)) await git(state.projectRoot, ["switch", branch]);
|
||||
else await git(state.projectRoot, ["switch", "-c", branch]);
|
||||
return branch;
|
||||
}
|
||||
|
||||
export async function checkpointStage(state: PomState, config: PomConfig, label: string): Promise<string | undefined> {
|
||||
if (config.gitMode === "off") return undefined;
|
||||
await ensureGit(state, config);
|
||||
await git(state.projectRoot, ["add", "-A"]);
|
||||
const status = await git(state.projectRoot, ["status", "--porcelain"]);
|
||||
if (!status) return undefined;
|
||||
await git(state.projectRoot, ["commit", "-m", label]);
|
||||
return git(state.projectRoot, ["rev-parse", "--short", "HEAD"]);
|
||||
}
|
||||
50
src/index.ts
Normal file
50
src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import { join } from "node:path";
|
||||
import { createState, restoreLatest } from "./state";
|
||||
import { loadPomConfig } from "./config";
|
||||
import { DEFAULT_CONFIG, type PomConfig, type PomState } from "./domain";
|
||||
import { loadState, pathExists, saveState, syncKnowledgeBase } from "./persistence";
|
||||
import { registerPomCommand } from "./command";
|
||||
import { registerPomEvents } from "./events";
|
||||
import { registerPomTools } from "./tools";
|
||||
import { registerPomRenderers } from "./renderers";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
|
||||
export default function pomExtension(pi: ExtensionAPI): void {
|
||||
pi.setLabel("POM · Produce Magnum Opus");
|
||||
let config: PomConfig = DEFAULT_CONFIG;
|
||||
let state: PomState = createState("Untitled Magnum Opus", process.cwd(), config);
|
||||
const runtime: PomRuntime = {
|
||||
telemetry: { activeAgents: 0, completedAgents: 0 },
|
||||
get: () => state,
|
||||
getConfig: () => config,
|
||||
refreshConfig: async (cwd) => {
|
||||
config = await loadPomConfig(cwd);
|
||||
return config;
|
||||
},
|
||||
set: async (next) => {
|
||||
state = { ...structuredClone(next), updatedAt: new Date().toISOString() };
|
||||
if (state.initialized) {
|
||||
pi.appendEntry("pom-state", state);
|
||||
await saveState(state);
|
||||
await syncKnowledgeBase(state);
|
||||
}
|
||||
},
|
||||
restore: async (ctx: ExtensionContext) => {
|
||||
const sessionState = restoreLatest(ctx.sessionManager.getBranch());
|
||||
if (sessionState) state = sessionState;
|
||||
else {
|
||||
const direct = join(ctx.cwd, "00_admin", "project_state.json");
|
||||
if (await pathExists(direct)) state = await loadState(direct);
|
||||
else state = createState("Untitled Magnum Opus", ctx.cwd, await loadPomConfig(ctx.cwd));
|
||||
}
|
||||
config = await loadPomConfig(state.initialized ? state.projectRoot : ctx.cwd);
|
||||
return state;
|
||||
},
|
||||
};
|
||||
|
||||
registerPomTools(pi, runtime);
|
||||
registerPomRenderers(pi, runtime);
|
||||
registerPomEvents(pi, runtime);
|
||||
registerPomCommand(pi, runtime);
|
||||
}
|
||||
47
src/parser.ts
Normal file
47
src/parser.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface ParsedCommand {
|
||||
verb: string;
|
||||
args: string[];
|
||||
flags: Map<string, string | true>;
|
||||
}
|
||||
|
||||
export function parseCommand(input: string): ParsedCommand {
|
||||
const tokens: string[] = [];
|
||||
let current = "";
|
||||
let quote: "'" | '"' | undefined;
|
||||
let escaping = false;
|
||||
for (const character of input.trim()) {
|
||||
if (escaping) { current += character; escaping = false; continue; }
|
||||
if (character === "\\") { escaping = true; continue; }
|
||||
if (quote) {
|
||||
if (character === quote) quote = undefined;
|
||||
else current += character;
|
||||
continue;
|
||||
}
|
||||
if (character === "'" || character === '"') { quote = character; continue; }
|
||||
if (/\s/.test(character)) {
|
||||
if (current) { tokens.push(current); current = ""; }
|
||||
continue;
|
||||
}
|
||||
current += character;
|
||||
}
|
||||
if (escaping) current += "\\";
|
||||
if (quote) throw new Error("Unterminated quote in /pom command");
|
||||
if (current) tokens.push(current);
|
||||
|
||||
const verb = tokens.shift() ?? "dashboard";
|
||||
const args: string[] = [];
|
||||
const flags = new Map<string, string | true>();
|
||||
let flagsEnabled = true;
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const token = tokens[index];
|
||||
if (token === "--") { flagsEnabled = false; continue; }
|
||||
if (flagsEnabled && token.startsWith("--")) {
|
||||
const [name, inline] = token.slice(2).split("=", 2);
|
||||
const next = tokens[index + 1];
|
||||
if (inline !== undefined) flags.set(name, inline);
|
||||
else if (next && !next.startsWith("--")) { flags.set(name, next); index += 1; }
|
||||
else flags.set(name, true);
|
||||
} else args.push(token);
|
||||
}
|
||||
return { verb, args, flags };
|
||||
}
|
||||
66
src/paths.ts
Normal file
66
src/paths.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { lstat, realpath } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
||||
|
||||
function inside(root: string, target: string): boolean {
|
||||
const rel = relative(root, target);
|
||||
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
||||
}
|
||||
|
||||
async function nearestExisting(path: string): Promise<string> {
|
||||
let cursor = path;
|
||||
for (;;) {
|
||||
try {
|
||||
await lstat(cursor);
|
||||
return cursor;
|
||||
} catch (error) {
|
||||
const parent = dirname(cursor);
|
||||
if (parent === cursor) throw error;
|
||||
cursor = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function canonicalRoot(root: string): Promise<string> {
|
||||
return realpath(resolve(root));
|
||||
}
|
||||
|
||||
export async function secureExistingPath(root: string, candidate: string, options: { allowDirectory?: boolean } = {}): Promise<string> {
|
||||
const rootReal = await canonicalRoot(root);
|
||||
const lexical = resolve(rootReal, candidate);
|
||||
if (!inside(rootReal, lexical)) throw new Error(`Path escapes project root: ${candidate}`);
|
||||
|
||||
const stat = await lstat(lexical);
|
||||
if (stat.isSymbolicLink()) throw new Error(`Symbolic links are not canonical artifacts: ${candidate}`);
|
||||
if (!options.allowDirectory && !stat.isFile()) throw new Error(`Expected a regular file: ${candidate}`);
|
||||
if (options.allowDirectory && !stat.isFile() && !stat.isDirectory()) throw new Error(`Unsupported filesystem object: ${candidate}`);
|
||||
|
||||
const targetReal = await realpath(lexical);
|
||||
if (!inside(rootReal, targetReal)) throw new Error(`Canonical path escapes project root: ${candidate}`);
|
||||
return targetReal;
|
||||
}
|
||||
|
||||
export async function secureFuturePath(root: string, candidate: string): Promise<string> {
|
||||
const rootReal = await canonicalRoot(root);
|
||||
const lexical = resolve(rootReal, candidate);
|
||||
if (!inside(rootReal, lexical)) throw new Error(`Path escapes project root: ${candidate}`);
|
||||
const existing = await nearestExisting(dirname(lexical));
|
||||
const parentReal = await realpath(existing);
|
||||
if (!inside(rootReal, parentReal)) throw new Error(`Parent path escapes project root: ${candidate}`);
|
||||
return lexical;
|
||||
}
|
||||
|
||||
export function relativeProjectPath(root: string, absolutePath: string): string {
|
||||
const rel = relative(resolve(root), resolve(absolutePath)).replaceAll("\\", "/");
|
||||
if (!rel || rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Path is not a project child: ${absolutePath}`);
|
||||
return rel;
|
||||
}
|
||||
|
||||
export function assertSafeArchivePath(path: string): void {
|
||||
const normalized = path.replaceAll("\\", "/");
|
||||
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
|
||||
throw new Error(`Unsafe archive member: ${path}`);
|
||||
}
|
||||
if (normalized.split("/").some((part) => part === ".." || part === "")) {
|
||||
throw new Error(`Unsafe archive member: ${path}`);
|
||||
}
|
||||
}
|
||||
388
src/persistence.ts
Normal file
388
src/persistence.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, relative, resolve } from "node:path";
|
||||
import { finished } from "node:stream/promises";
|
||||
import {
|
||||
POM_SCHEMA_VERSION,
|
||||
STAGES,
|
||||
stage,
|
||||
type ArtifactRecord,
|
||||
type HiveMission,
|
||||
type PomState,
|
||||
} from "./domain";
|
||||
import { relativeProjectPath, secureExistingPath, secureFuturePath } from "./paths";
|
||||
import { assertState, statePath } from "./state";
|
||||
|
||||
const writeQueues = new Map<string, Promise<void>>();
|
||||
|
||||
async function serialize<T>(key: string, operation: () => Promise<T>): Promise<T> {
|
||||
const previous = writeQueues.get(key) ?? Promise.resolve();
|
||||
let release!: () => void;
|
||||
const current = new Promise<void>((resolvePromise) => { release = resolvePromise; });
|
||||
const chain = previous.catch(() => undefined).then(() => current);
|
||||
writeQueues.set(key, chain);
|
||||
await previous.catch(() => undefined);
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
release();
|
||||
if (writeQueues.get(key) === chain) writeQueues.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function atomicWriteUnlocked(absolute: string, content: string | Uint8Array): Promise<void> {
|
||||
await mkdir(dirname(absolute), { recursive: true });
|
||||
const temporary = join(dirname(absolute), `.${basename(absolute)}.${process.pid}.${randomUUID()}.tmp`);
|
||||
try {
|
||||
await writeFile(temporary, content);
|
||||
await rename(temporary, absolute);
|
||||
} finally {
|
||||
await rm(temporary, { force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function atomicWrite(path: string, content: string | Uint8Array): Promise<void> {
|
||||
const absolute = resolve(path);
|
||||
await serialize(absolute, () => atomicWriteUnlocked(absolute, content));
|
||||
}
|
||||
|
||||
export async function readJson<T>(path: string): Promise<T> {
|
||||
const text = await readFile(path, "utf8");
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON in ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sha256File(path: string): Promise<string> {
|
||||
const hash = createHash("sha256");
|
||||
const stream = createReadStream(path);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
await finished(stream);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export async function saveState(state: PomState): Promise<void> {
|
||||
assertState(state);
|
||||
await atomicWrite(statePath(state), `${JSON.stringify(state, null, 2)}\n`);
|
||||
}
|
||||
|
||||
export async function loadState(path: string): Promise<PomState> {
|
||||
const state = await readJson<unknown>(path);
|
||||
assertState(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
export function manifestPath(state: PomState): string {
|
||||
return join(state.projectRoot, "00_admin", "artifact_manifest.json");
|
||||
}
|
||||
|
||||
export async function loadManifest(state: PomState): Promise<ArtifactRecord[]> {
|
||||
if (!(await pathExists(manifestPath(state)))) return [];
|
||||
const value = await readJson<unknown>(manifestPath(state));
|
||||
if (!Array.isArray(value)) throw new Error("Artifact manifest must be an array");
|
||||
const paths = new Set<string>();
|
||||
return value.map((item, index) => {
|
||||
if (!item || typeof item !== "object") throw new Error(`Manifest record ${index} is invalid`);
|
||||
const record = item as Partial<ArtifactRecord>;
|
||||
if (typeof record.path !== "string" || !record.path || record.path.startsWith("/") || record.path.includes("..")) {
|
||||
throw new Error(`Manifest record ${index} has an unsafe path`);
|
||||
}
|
||||
if (paths.has(record.path)) throw new Error(`Duplicate manifest path: ${record.path}`);
|
||||
paths.add(record.path);
|
||||
if (typeof record.category !== "string" || !record.category.trim()) throw new Error(`Invalid category for ${record.path}`);
|
||||
if (!Number.isInteger(record.stage) || Number(record.stage) < 0 || Number(record.stage) > 8) throw new Error(`Invalid stage for ${record.path}`);
|
||||
if (typeof record.version !== "string" || !/^\d+\.\d+\.\d+$/.test(record.version)) throw new Error(`Invalid version for ${record.path}`);
|
||||
if (!(["draft", "reviewed", "approved", "superseded"] as const).includes(record.status as ArtifactRecord["status"])) throw new Error(`Invalid status for ${record.path}`);
|
||||
if (typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) throw new Error(`Invalid SHA-256 for ${record.path}`);
|
||||
if (typeof record.bytes !== "number" || !Number.isInteger(record.bytes) || record.bytes < 1) throw new Error(`Invalid byte count for ${record.path}`);
|
||||
if (typeof record.updatedAt !== "string" || Number.isNaN(Date.parse(record.updatedAt))) throw new Error(`Invalid updatedAt for ${record.path}`);
|
||||
if (!(["original", "licensed", "public-domain", "reference-only", "unknown"] as const).includes(record.rightsStatus as ArtifactRecord["rightsStatus"])) throw new Error(`Invalid rights status for ${record.path}`);
|
||||
if (record.source !== undefined && typeof record.source !== "string") throw new Error(`Invalid source for ${record.path}`);
|
||||
if (record.notes !== undefined && typeof record.notes !== "string") throw new Error(`Invalid notes for ${record.path}`);
|
||||
return record as ArtifactRecord;
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveManifest(state: PomState, records: ArtifactRecord[]): Promise<void> {
|
||||
const sorted = [...records].sort((left, right) => left.path.localeCompare(right.path));
|
||||
await atomicWrite(manifestPath(state), `${JSON.stringify(sorted, null, 2)}\n`);
|
||||
}
|
||||
|
||||
export async function registerArtifact(
|
||||
state: PomState,
|
||||
candidate: string,
|
||||
metadata: {
|
||||
category: string;
|
||||
status: ArtifactRecord["status"];
|
||||
rightsStatus: ArtifactRecord["rightsStatus"];
|
||||
source?: string;
|
||||
notes?: string;
|
||||
stage?: number;
|
||||
version?: string;
|
||||
},
|
||||
): Promise<ArtifactRecord> {
|
||||
const absolute = await secureExistingPath(state.projectRoot, candidate);
|
||||
const fileStat = await stat(absolute);
|
||||
if (fileStat.size < 1) throw new Error("Empty files cannot be canonical artifacts");
|
||||
const projectPath = relativeProjectPath(state.projectRoot, absolute);
|
||||
const current = await loadManifest(state);
|
||||
const existing = current.find((item) => item.path === projectPath);
|
||||
const record: ArtifactRecord = {
|
||||
path: projectPath,
|
||||
category: metadata.category || existing?.category || "other",
|
||||
stage: metadata.stage ?? existing?.stage ?? state.currentStage,
|
||||
version: metadata.version ?? existing?.version ?? state.artifactVersion,
|
||||
status: metadata.status ?? existing?.status ?? "draft",
|
||||
bytes: fileStat.size,
|
||||
sha256: await sha256File(absolute),
|
||||
updatedAt: new Date().toISOString(),
|
||||
rightsStatus: metadata.rightsStatus ?? existing?.rightsStatus ?? "unknown",
|
||||
source: metadata.source ?? existing?.source,
|
||||
notes: metadata.notes ?? existing?.notes,
|
||||
};
|
||||
await saveManifest(state, [...current.filter((item) => item.path !== projectPath), record]);
|
||||
await appendJsonl(join(state.projectRoot, "00_admin", "artifact_events.jsonl"), {
|
||||
type: "artifact.registered",
|
||||
projectId: state.projectId,
|
||||
record,
|
||||
at: record.updatedAt,
|
||||
});
|
||||
await syncKnowledgeBase(state);
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function appendJsonl(path: string, value: unknown): Promise<void> {
|
||||
const absolute = resolve(path);
|
||||
await serialize(absolute, async () => {
|
||||
await mkdir(dirname(absolute), { recursive: true });
|
||||
const previous = (await pathExists(absolute)) ? await readFile(absolute, "utf8") : "";
|
||||
await atomicWriteUnlocked(absolute, `${previous}${JSON.stringify(value)}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
export async function appendMarkdownLog(root: string, fileName: string, title: string, body: string): Promise<string> {
|
||||
const path = await secureFuturePath(root, join("00_admin", fileName));
|
||||
const stamp = new Date().toISOString();
|
||||
const previous = (await pathExists(path)) ? await readFile(path, "utf8") : `---\ntags: [pom, ledger]\n---\n\n# ${title}\n`;
|
||||
await atomicWrite(path, `${previous.trimEnd()}\n\n## ${stamp} · ${title}\n\n${body.trim()}\n`);
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function recordHiveEvent(state: PomState, event: Record<string, unknown>): Promise<void> {
|
||||
await appendJsonl(join(state.projectRoot, "00_admin", "hive_ledger.jsonl"), {
|
||||
projectId: state.projectId,
|
||||
stage: state.currentStage,
|
||||
at: new Date().toISOString(),
|
||||
...event,
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordPromptUse(state: PomState, entry: Record<string, unknown>): Promise<void> {
|
||||
await appendJsonl(join(state.projectRoot, "00_admin", "prompt_ledger.jsonl"), {
|
||||
projectId: state.projectId,
|
||||
stage: state.currentStage,
|
||||
at: new Date().toISOString(),
|
||||
...entry,
|
||||
});
|
||||
}
|
||||
|
||||
function frontmatter(title: string, type: string, tags: string[]): string {
|
||||
return [
|
||||
"---",
|
||||
`title: ${JSON.stringify(title)}`,
|
||||
`type: ${type}`,
|
||||
`tags: [${tags.join(", ")}]`,
|
||||
"publish: true",
|
||||
"---",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function ensureStarter(path: string, content: string): Promise<void> {
|
||||
if (!(await pathExists(path))) await atomicWrite(path, content);
|
||||
}
|
||||
|
||||
export async function bootstrapProject(state: PomState): Promise<void> {
|
||||
const root = state.projectRoot;
|
||||
if (await pathExists(root)) {
|
||||
const entries = await readdir(root);
|
||||
const marker = join(root, "00_admin", "project_state.json");
|
||||
if (entries.length > 0 && !(await pathExists(marker))) {
|
||||
throw new Error(`Refusing to initialize inside a non-empty unrecognized directory: ${root}`);
|
||||
}
|
||||
}
|
||||
|
||||
const directories = [
|
||||
"00_admin",
|
||||
"01_planning",
|
||||
"02_story_bible",
|
||||
"03_manuscript/chapters",
|
||||
"04_images/references",
|
||||
"04_images/generated",
|
||||
"05_research",
|
||||
"06_ledgers",
|
||||
"07_exports",
|
||||
".omp/prompts",
|
||||
"content",
|
||||
];
|
||||
for (const directory of directories) await mkdir(join(root, directory), { recursive: true });
|
||||
|
||||
await ensureStarter(join(root, ".gitignore"), ["node_modules/", ".DS_Store", "07_exports/*.tmp", "*.swp", ""].join("\n"));
|
||||
await ensureStarter(join(root, ".omp", "pom.json"), `${JSON.stringify({ dashboardWidget: true, hudPlacement: "aboveEditor", showThinkingLane: true }, null, 2)}\n`);
|
||||
await ensureStarter(join(root, ".omp", "config.yml"), [
|
||||
"theme:",
|
||||
" dark: pom-nocturne",
|
||||
" light: pom-parchment",
|
||||
"symbolPreset: unicode",
|
||||
"statusLine:",
|
||||
" preset: full",
|
||||
" separator: powerline-thin",
|
||||
" sessionAccent: true",
|
||||
" showHookStatus: true",
|
||||
"terminal:",
|
||||
" showImages: true",
|
||||
"images:",
|
||||
" autoResize: true",
|
||||
" blockImages: false",
|
||||
"tools:",
|
||||
" intentTracing: true",
|
||||
" artifactSpillThreshold: 50",
|
||||
" artifactHeadBytes: 20",
|
||||
" artifactTailBytes: 20",
|
||||
"task:",
|
||||
" eager: preferred",
|
||||
" batch: true",
|
||||
" maxConcurrency: 8",
|
||||
" maxRecursionDepth: 2",
|
||||
" showResolvedModelBadge: true",
|
||||
"async:",
|
||||
" enabled: true",
|
||||
"compaction:",
|
||||
" strategy: snapcompact",
|
||||
" midTurnEnabled: true",
|
||||
" autoContinue: true",
|
||||
"memory:",
|
||||
" backend: local",
|
||||
"steeringMode: one-at-a-time",
|
||||
"followUpMode: one-at-a-time",
|
||||
"interruptMode: immediate",
|
||||
"defaultThinkingLevel: high",
|
||||
"hideThinkingBlock: false",
|
||||
"",
|
||||
].join("\n"));
|
||||
await ensureStarter(join(root, "AGENTS.md"), `${frontmatter("POM Agent Contract", "agent-contract", ["pom", "agents"])}\n\n# POM Agent Contract\n\nCanonical files outrank chat. Use native \`task\`, \`irc\`, \`todo\`, and POM evidence tools. Never claim completion without deterministic validation.\n`);
|
||||
await ensureStarter(join(root, "POM.md"), `${frontmatter(state.projectTitle, "project", ["pom", "project"])}\n\n# ${state.projectTitle}\n\n> [!info] Production root\n> This repository is simultaneously a POM workspace, Obsidian vault, Quartz-compatible knowledge base, and Git project.\n\n- [[content/index|Open production dashboard]]\n- [[01_planning/00_project_brief|Project brief]]\n- [[02_story_bible/00_story_bible_index|Story bible]]\n- [[03_manuscript/00_master_outline|Master outline]]\n`);
|
||||
await ensureStarter(join(root, "01_planning", "00_project_brief.md"), `${frontmatter("Project Brief", "planning", ["pom", "stage-0"])}\n\n# Project Brief\n\n## Vision\n\n## Audience\n\n## Constraints\n\n## Success Criteria\n\n## Open Questions\n`);
|
||||
await ensureStarter(join(root, "content", "index.md"), `${frontmatter(state.projectTitle, "dashboard", ["pom", "dashboard"])}\n\n# ${state.projectTitle}\n\n> [!tip] Command center\n> Run \`/pom\` or press the POM shortcut to open the terminal command center.\n\n## Production Map\n\n- [[01-planning|Planning]]\n- [[02-story-bible|Story Bible]]\n- [[03-manuscript|Manuscript]]\n- [[04-visuals|Visuals]]\n- [[05-research|Research]]\n- [[06-ledgers|Ledgers]]\n- [[07-delivery|Delivery]]\n- [[artifact-index|Artifact Index]]\n`);
|
||||
|
||||
const maps = [
|
||||
["01-planning.md", "Planning", "../01_planning"],
|
||||
["02-story-bible.md", "Story Bible", "../02_story_bible"],
|
||||
["03-manuscript.md", "Manuscript", "../03_manuscript"],
|
||||
["04-visuals.md", "Visuals", "../04_images"],
|
||||
["05-research.md", "Research", "../05_research"],
|
||||
["06-ledgers.md", "Ledgers", "../06_ledgers"],
|
||||
["07-delivery.md", "Delivery", "../07_exports"],
|
||||
] as const;
|
||||
for (const [file, title, target] of maps) {
|
||||
await ensureStarter(join(root, "content", file), `${frontmatter(title, "map-of-content", ["pom", "moc"])}\n\n# ${title}\n\nCanonical directory: \`${target}\`\n\n## Index\n\n_Updated automatically by POM._\n`);
|
||||
}
|
||||
|
||||
await ensureStarter(manifestPath(state), "[]\n");
|
||||
await ensureStarter(join(root, "00_admin", "artifact_events.jsonl"), "");
|
||||
await ensureStarter(join(root, "00_admin", "hive_ledger.jsonl"), "");
|
||||
await ensureStarter(join(root, "00_admin", "prompt_ledger.jsonl"), "");
|
||||
await saveState(state);
|
||||
await syncKnowledgeBase(state);
|
||||
}
|
||||
|
||||
export async function syncKnowledgeBase(state: PomState): Promise<void> {
|
||||
const records = await loadManifest(state).catch(() => []);
|
||||
const lines = [
|
||||
frontmatter("Artifact Index", "generated-index", ["pom", "artifacts"]),
|
||||
"",
|
||||
"# Artifact Index",
|
||||
"",
|
||||
"> [!warning] Generated file",
|
||||
"> Update artifact metadata through POM; this page is regenerated.",
|
||||
"",
|
||||
`Updated: ${new Date().toISOString()}`,
|
||||
"",
|
||||
];
|
||||
if (!records.length) lines.push("_No canonical artifacts registered yet._");
|
||||
const grouped = new Map<string, ArtifactRecord[]>();
|
||||
for (const record of records) grouped.set(record.category, [...(grouped.get(record.category) ?? []), record]);
|
||||
for (const [category, items] of grouped) {
|
||||
lines.push("", `## ${category}`);
|
||||
for (const item of items) {
|
||||
lines.push(`- [[../${item.path}|${item.path}]] · ${item.status} · ${item.rightsStatus} · ${item.bytes} bytes · \`${item.sha256.slice(0, 12)}\``);
|
||||
}
|
||||
}
|
||||
await atomicWrite(join(state.projectRoot, "content", "artifact-index.md"), `${lines.join("\n")}\n`);
|
||||
|
||||
const board = [
|
||||
frontmatter("Production Board", "generated-board", ["pom", "dashboard"]),
|
||||
"",
|
||||
"# Production Board",
|
||||
"",
|
||||
`Project: **${state.projectTitle}**`,
|
||||
"",
|
||||
"## Stages",
|
||||
"",
|
||||
"| Stage | Status | Attempts | Evidence | Blockers |",
|
||||
"|---:|---|---:|---:|---:|",
|
||||
...STAGES.map((item) => {
|
||||
const run = state.stageRuns[String(item.id)];
|
||||
return `| ${item.id} ${item.icon} ${item.name} | ${run.status} | ${run.attempts} | ${run.evidence.length}/${item.acceptance.length} | ${run.blockers.filter((blocker) => !blocker.resolvedAt).length} |`;
|
||||
}),
|
||||
"",
|
||||
"## Active Stage",
|
||||
"",
|
||||
`[[../${stage(state.currentStage).artifacts[0]?.path ?? "POM.md"}|${stage(state.currentStage).name}]]`,
|
||||
];
|
||||
await atomicWrite(join(state.projectRoot, "content", "production-board.md"), `${board.join("\n")}\n`);
|
||||
}
|
||||
|
||||
export async function writeHiveMission(state: PomState, mission: HiveMission): Promise<string> {
|
||||
const path = join(state.projectRoot, "06_ledgers", `hive-${mission.id}.md`);
|
||||
const text = [
|
||||
frontmatter(mission.title, "hive-mission", ["pom", "hive", `stage-${mission.stage}`]),
|
||||
"",
|
||||
`# ${mission.title}`,
|
||||
"",
|
||||
`Status: **${mission.status}**`,
|
||||
"",
|
||||
"## Tasks",
|
||||
"",
|
||||
...mission.tasks.flatMap((task) => [
|
||||
`### ${task.name} · ${task.agent}`,
|
||||
"",
|
||||
task.objective,
|
||||
"",
|
||||
`Owns: ${task.owns.map((item) => `\`${item}\``).join(", ") || "integration only"}`,
|
||||
"",
|
||||
...task.acceptance.map((item) => `- [ ] ${item}`),
|
||||
"",
|
||||
]),
|
||||
"## Synthesis",
|
||||
"",
|
||||
mission.synthesis ?? "_Pending._",
|
||||
].join("\n");
|
||||
await atomicWrite(path, `${text}\n`);
|
||||
return relative(state.projectRoot, path).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
export function schemaStamp(): string {
|
||||
return POM_SCHEMA_VERSION;
|
||||
}
|
||||
36
src/policy.ts
Normal file
36
src/policy.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
function destructiveCommand(command: string): boolean {
|
||||
const normalized = command.replace(/\s+/g, " ").trim();
|
||||
return [
|
||||
/\brm\b[^\n;&|]*(?:-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|--recursive[^\n;&|]*--force|--force[^\n;&|]*--recursive)/i,
|
||||
/\bgit\s+clean\b[^\n;&|]*-[a-z]*f/i,
|
||||
/\bgit\s+reset\s+--hard\b/i,
|
||||
/\bgit\s+(?:checkout|restore)\b[^\n;&|]*(?:\s--\s|\s\.\s*$)/i,
|
||||
/\bfind\b[^\n;&|]*\s-delete\b/i,
|
||||
/\bRemove-Item\b[^\n;&|]*(?:-Recurse[^\n;&|]*-Force|-Force[^\n;&|]*-Recurse)/i,
|
||||
/\bdel\b[^\n;&|]*\/s[^\n;&|]*\/q/i,
|
||||
/\brmdir\b[^\n;&|]*\/s[^\n;&|]*\/q/i,
|
||||
].some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
function inside(child: string, root: string): boolean {
|
||||
const normalizedChild = resolve(child).replaceAll("\\", "/");
|
||||
const normalizedRoot = resolve(root).replaceAll("\\", "/");
|
||||
return normalizedChild === normalizedRoot || normalizedChild.startsWith(`${normalizedRoot}/`);
|
||||
}
|
||||
|
||||
function likelyTargetsProject(command: string, projectRoot: string, projectSlug: string): boolean {
|
||||
const normalized = command.replaceAll("\\", "/");
|
||||
const root = resolve(projectRoot).replaceAll("\\", "/");
|
||||
return normalized.includes(root)
|
||||
|| normalized.includes(projectSlug)
|
||||
|| /(?:^|\s)(?:\.|\.\/|\*|\.\*|\$PWD|%CD%|--)(?:\s|$|["'])/i.test(normalized)
|
||||
|| /\b(?:git\s+(?:clean|reset|restore|checkout)|find\s+\.)\b/i.test(normalized)
|
||||
|| /\bcd\s+[^;&|]+(?:&&|;).*\b(?:rm|Remove-Item|del|rmdir)\b/i.test(normalized);
|
||||
}
|
||||
|
||||
export function shouldBlockProjectCommand(command: string, projectRoot: string, projectSlug: string, executionCwd: string): boolean {
|
||||
if (!destructiveCommand(command)) return false;
|
||||
return inside(executionCwd, projectRoot) || likelyTargetsProject(command, projectRoot, projectSlug);
|
||||
}
|
||||
118
src/prompts.ts
Normal file
118
src/prompts.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stage, type PomState } from "./domain";
|
||||
import { pathExists, recordPromptUse } from "./persistence";
|
||||
|
||||
export interface PromptFragment {
|
||||
id: string;
|
||||
source: "bundled" | "project";
|
||||
path: string;
|
||||
content: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface PromptStack {
|
||||
ids: string[];
|
||||
fragments: PromptFragment[];
|
||||
rendered: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
const bundledRoot = fileURLToPath(new URL("../prompts", import.meta.url));
|
||||
|
||||
function hash(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
function substitute(text: string, variables: Record<string, string>): string {
|
||||
return text.replace(/\{\{\s*([A-Z0-9_]+)\s*\}\}/g, (_match, key: string) => variables[key] ?? `{{${key}}}`);
|
||||
}
|
||||
|
||||
async function loadDirectory(root: string, source: PromptFragment["source"]): Promise<PromptFragment[]> {
|
||||
if (!(await pathExists(root))) return [];
|
||||
const files = (await readdir(root)).filter((file) => file.endsWith(".md")).sort();
|
||||
return Promise.all(files.map(async (file) => {
|
||||
const path = join(root, file);
|
||||
const content = await readFile(path, "utf8");
|
||||
return { id: basename(file, ".md"), source, path, content, sha256: hash(content) };
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listPrompts(state?: PomState): Promise<PromptFragment[]> {
|
||||
const bundled = await loadDirectory(bundledRoot, "bundled");
|
||||
if (!state) return bundled;
|
||||
const project = await loadDirectory(join(state.projectRoot, ".omp", "prompts"), "project");
|
||||
const byId = new Map(bundled.map((fragment) => [fragment.id, fragment]));
|
||||
for (const fragment of project) byId.set(fragment.id, fragment);
|
||||
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
export async function getPrompt(state: PomState | undefined, id: string): Promise<PromptFragment> {
|
||||
const prompt = (await listPrompts(state)).find((fragment) => fragment.id === id);
|
||||
if (!prompt) throw new Error(`Unknown POM prompt: ${id}`);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function immutableLaw(state: PomState): string {
|
||||
const spec = stage(state.currentStage);
|
||||
return [
|
||||
"# POM Production Law",
|
||||
"Canonical project files outrank chat, memory, and agent claims.",
|
||||
"OMP owns execution: use native task, IRC, todo, memory, checkpoints, tools, and approvals.",
|
||||
"POM owns contracts, provenance, canon, evidence, integration, validation, and delivery truth.",
|
||||
"Never claim a file, metric, test, citation, image, archive, or gate result that was not actually produced and checked.",
|
||||
`Active project root: ${state.projectRoot}`,
|
||||
`Active stage: ${spec.id}/8 · ${spec.name}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function composePromptStack(state: PomState, ids: string[], extraVariables: Record<string, string> = {}): Promise<PromptStack> {
|
||||
const spec = stage(state.currentStage);
|
||||
const variables = {
|
||||
PROJECT_TITLE: state.projectTitle,
|
||||
PROJECT_ROOT: state.projectRoot,
|
||||
PROJECT_ID: state.projectId,
|
||||
STAGE_ID: String(spec.id),
|
||||
STAGE_NAME: spec.name,
|
||||
STAGE_SLUG: spec.slug,
|
||||
MODE: state.mode,
|
||||
...extraVariables,
|
||||
};
|
||||
const personalityId = `personality-${state.mode}`;
|
||||
const resolvedIds = [...new Set([personalityId, ...ids])];
|
||||
const fragments = await Promise.all(resolvedIds.map((id) => getPrompt(state, id)));
|
||||
const renderedFragments = fragments.map((fragment) => `\n\n<!-- prompt:${fragment.id} source:${fragment.source} sha256:${fragment.sha256} -->\n${substitute(fragment.content, variables).trim()}`);
|
||||
const rendered = `${immutableLaw(state)}${renderedFragments.join("")}\n`;
|
||||
const stack: PromptStack = { ids: resolvedIds, fragments, rendered, sha256: hash(rendered) };
|
||||
await recordPromptUse(state, {
|
||||
type: "prompt.stack",
|
||||
ids: resolvedIds,
|
||||
stackSha256: stack.sha256,
|
||||
fragments: fragments.map(({ id, source, sha256 }) => ({ id, source, sha256 })),
|
||||
});
|
||||
return stack;
|
||||
}
|
||||
|
||||
export async function renderStagePrompt(state: PomState, allStages = false): Promise<string> {
|
||||
const spec = stage(state.currentStage);
|
||||
const stack = await composePromptStack(state, [allStages ? "full-autopilot" : "stage-run"], {
|
||||
STAGE_CONTRACT: JSON.stringify(spec, null, 2),
|
||||
});
|
||||
return [
|
||||
stack.rendered,
|
||||
"# Executable Stage Contract",
|
||||
JSON.stringify(spec, null, 2),
|
||||
"# Required Native OMP Loop",
|
||||
"1. Initialize the stage todo phases with the native todo tool.",
|
||||
"2. Read only the canonical dependencies required for this stage.",
|
||||
"3. Call pom_hive with op=plan, then invoke native task exactly once with the returned batch.",
|
||||
"4. Use native IRC for short CLAIM, QUERY, ANSWER, ALERT, HANDOFF, and DONE signals.",
|
||||
"5. Integrate specialist findings into canonical files; specialists do not decide final canon independently.",
|
||||
"6. Register every canonical artifact with pom_artifact.",
|
||||
"7. Record one pom_stage evidence entry per acceptance criterion.",
|
||||
"8. Run pom_validate scope=stage. Repair failures, then call pom_stage pass; otherwise block with exact evidence.",
|
||||
allStages ? "9. After PASS, continue to the next stage until stage 8 completes or a real blocker stops production." : "",
|
||||
].filter(Boolean).join("\n\n");
|
||||
}
|
||||
77
src/renderers.ts
Normal file
77
src/renderers.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
import type { PomView } from "./domain";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
import { stage } from "./domain";
|
||||
|
||||
function textOf(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
try { return JSON.stringify(content, null, 2); } catch { return String(content); }
|
||||
}
|
||||
|
||||
function canExpand(view: PomView, expanded: boolean): boolean {
|
||||
return expanded && (view === "cinematic" || view === "rich");
|
||||
}
|
||||
|
||||
function maxLines(view: PomView): number {
|
||||
return view === "cinematic" ? 10 : view === "rich" ? 5 : 0;
|
||||
}
|
||||
|
||||
export function registerPomRenderers(pi: ExtensionAPI, runtime: PomRuntime): void {
|
||||
pi.registerMessageRenderer("pom:event", (message, options, theme) => {
|
||||
const view = runtime.get().view;
|
||||
const details = message.details as { title?: string; level?: string; lines?: string[] } | undefined;
|
||||
const mark = details?.level === "error" ? theme.fg("error", "×") : details?.level === "warning" ? theme.fg("warning", "!") : theme.fg("accent", "◆");
|
||||
const lines = canExpand(view, options.expanded) ? (details?.lines ?? []).slice(0, maxLines(view)) : [];
|
||||
const body = lines.length ? `\n${lines.map((line) => theme.fg("dim", line)).join("\n")}` : "";
|
||||
return new Text(`${mark} ${theme.bold(details?.title ?? textOf(message.content))}${body}`, view === "minimal" ? 0 : 1, 0);
|
||||
});
|
||||
|
||||
pi.registerMessageRenderer("pom:validation", (message, options, theme) => {
|
||||
const view = runtime.get().view;
|
||||
const report = message.details as { passed?: boolean; scope?: string; checks?: Array<{ status: string; message: string }> } | undefined;
|
||||
const failures = report?.checks?.filter((item) => item.status === "fail") ?? [];
|
||||
const warnings = report?.checks?.filter((item) => item.status === "warn") ?? [];
|
||||
const passed = Boolean(report?.passed);
|
||||
const mark = passed ? theme.fg("success", "✓") : theme.fg("error", "×");
|
||||
const limit = maxLines(view);
|
||||
const lines = canExpand(view, options.expanded)
|
||||
? [
|
||||
...failures.slice(0, Math.max(1, limit - 2)).map((item) => `${theme.fg("error", "×")} ${item.message}`),
|
||||
...warnings.slice(0, Math.min(3, limit)).map((item) => `${theme.fg("warning", "!")} ${item.message}`),
|
||||
].slice(0, limit)
|
||||
: [];
|
||||
const summary = `${mark} ${theme.bold(`POM validation · ${report?.scope ?? "unknown"}`)} · ${failures.length} failures · ${warnings.length} warnings`;
|
||||
return new Text(`${summary}${lines.length ? `\n${lines.join("\n")}` : ""}`, view === "minimal" ? 0 : 1, 0);
|
||||
});
|
||||
|
||||
pi.registerMessageRenderer("pom:hive", (message, options, theme) => {
|
||||
const view = runtime.get().view;
|
||||
const mission = message.details as { id?: string; status?: string; tasks?: Array<{ name: string; agent: string }> } | undefined;
|
||||
const roster = canExpand(view, options.expanded)
|
||||
? `\n${(mission?.tasks ?? []).slice(0, maxLines(view)).map((task) => ` ${theme.fg("accent", "⬡")} ${task.name} · ${theme.fg("dim", task.agent)}`).join("\n")}`
|
||||
: "";
|
||||
return new Text(`${theme.fg("accent", "⬡")} ${theme.bold(`Hive ${mission?.id ?? "mission"}`)} · ${mission?.status ?? "planned"} · ${mission?.tasks?.length ?? 0} agents${roster}`, view === "minimal" ? 0 : 1, 0);
|
||||
});
|
||||
|
||||
pi.registerMessageRenderer("pom:milestone", (message, options, theme) => {
|
||||
const view = runtime.get().view;
|
||||
const details = message.details as { stage?: number; status?: string; path?: string; commit?: string } | undefined;
|
||||
const stageId = details?.stage ?? runtime.get().currentStage;
|
||||
const spec = stage(stageId);
|
||||
const extra = canExpand(view, options.expanded)
|
||||
? [details?.path, details?.commit ? `commit ${details.commit}` : undefined].filter(Boolean).map((line) => theme.fg("dim", String(line))).join("\n")
|
||||
: "";
|
||||
return new Text(`${theme.fg("success", "◆")} ${theme.bold(`${spec.icon} Stage ${stageId} ${details?.status ?? "milestone"}`)}${extra ? `\n${extra}` : ""}`, view === "minimal" ? 0 : 1, 0);
|
||||
});
|
||||
|
||||
pi.registerAssistantThinkingRenderer((context, theme) => {
|
||||
const state = runtime.get();
|
||||
const config = runtime.getConfig();
|
||||
if (!state.initialized || !config.showThinkingLane || state.view === "minimal" || state.view === "compact") return undefined;
|
||||
const spec = stage(state.currentStage);
|
||||
const lane = runtime.telemetry.activeTool ? `tool:${runtime.telemetry.activeTool}` : state.hive.activeMissionId ? "hive-synthesis" : "synthesis";
|
||||
const suffix = state.view === "cinematic" ? ` · ${context.text.length} chars` : "";
|
||||
return new Text(theme.fg("dim", `╰─ POM lane · ${spec.slug} · ${lane}${suffix}`), 1, 0);
|
||||
});
|
||||
}
|
||||
16
src/router.ts
Normal file
16
src/router.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import { stage, type PomState } from "./domain";
|
||||
import { applyToolProfile } from "./tool-profiles";
|
||||
|
||||
export async function routeStage(pi: ExtensionAPI, ctx: ExtensionContext, state: PomState): Promise<{ tools: string[]; model?: string }> {
|
||||
const spec = stage(state.currentStage);
|
||||
const tools = state.autoToolProfiles ? await applyToolProfile(pi, spec.toolProfile) : pi.getActiveTools();
|
||||
let modelName: string | undefined;
|
||||
if (state.autoModelRouting) {
|
||||
const model = ctx.models.resolve(spec.modelRole);
|
||||
if (model && await pi.setModel(model)) modelName = `${model.provider}/${model.id}`;
|
||||
}
|
||||
const thinking = spec.id === 7 ? "high" : "xhigh";
|
||||
try { pi.setThinkingLevel(thinking as Parameters<ExtensionAPI["setThinkingLevel"]>[0]); } catch { /* model may not support this level */ }
|
||||
return { tools, model: modelName };
|
||||
}
|
||||
22
src/runtime.ts
Normal file
22
src/runtime.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import type { PomConfig, PomState } from "./domain";
|
||||
|
||||
export interface PomTelemetry {
|
||||
activeTool?: string;
|
||||
activeIntent?: string;
|
||||
toolStartedAt?: number;
|
||||
lastToolResult?: "pass" | "fail";
|
||||
lastIrcSignal?: string;
|
||||
activeAgents: number;
|
||||
completedAgents: number;
|
||||
validationFailures?: number;
|
||||
}
|
||||
|
||||
export interface PomRuntime {
|
||||
get(): PomState;
|
||||
set(state: PomState): Promise<void>;
|
||||
restore(ctx: ExtensionContext): Promise<PomState>;
|
||||
getConfig(): PomConfig;
|
||||
refreshConfig(cwd: string): Promise<PomConfig>;
|
||||
telemetry: PomTelemetry;
|
||||
}
|
||||
217
src/state.ts
Normal file
217
src/state.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { join, resolve } from "node:path";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
POM_SCHEMA_VERSION,
|
||||
POM_VERSION,
|
||||
STAGES,
|
||||
freshStageRuns,
|
||||
stage,
|
||||
type EvidenceRecord,
|
||||
type PomConfig,
|
||||
type PomState,
|
||||
type StageBlocker,
|
||||
} from "./domain";
|
||||
|
||||
|
||||
export function bumpPatchVersion(version: string): string {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
||||
if (!match) throw new Error(`Invalid semantic version: ${version}`);
|
||||
return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
|
||||
}
|
||||
|
||||
export function slugify(value: string): string {
|
||||
return value
|
||||
.normalize("NFKD")
|
||||
.replace(/[^\x00-\x7F]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "magnum-opus";
|
||||
}
|
||||
|
||||
export function createState(title: string, cwd: string, config: PomConfig = DEFAULT_CONFIG, initialized = false): PomState {
|
||||
const projectSlug = slugify(title);
|
||||
const projectRoot = config.projectRootMode === "cwd" ? resolve(cwd) : resolve(cwd, projectSlug);
|
||||
return {
|
||||
schemaVersion: POM_SCHEMA_VERSION,
|
||||
pomVersion: POM_VERSION,
|
||||
projectId: randomUUID(),
|
||||
projectTitle: title.trim() || "Untitled Magnum Opus",
|
||||
projectSlug,
|
||||
projectRoot,
|
||||
initialized,
|
||||
currentStage: 0,
|
||||
stageRuns: freshStageRuns(),
|
||||
mode: config.defaultMode,
|
||||
view: config.defaultView,
|
||||
motion: config.defaultMotion,
|
||||
toolProfile: config.defaultToolProfile,
|
||||
autoToolProfiles: config.autoToolProfiles,
|
||||
autoModelRouting: config.autoModelRouting,
|
||||
completedStages: [],
|
||||
decisions: [],
|
||||
assumptions: [],
|
||||
warnings: [],
|
||||
hive: { missions: {}, signalsLogged: 0 },
|
||||
artifactVersion: "1.0.0",
|
||||
canonVersion: "1.0.0",
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertState(value: unknown): asserts value is PomState {
|
||||
if (!value || typeof value !== "object") throw new Error("POM state must be an object");
|
||||
const state = value as Partial<PomState>;
|
||||
if (state.schemaVersion !== POM_SCHEMA_VERSION) throw new Error(`Unsupported POM state schema: ${String(state.schemaVersion)}`);
|
||||
if (typeof state.projectId !== "string" || typeof state.projectRoot !== "string" || typeof state.projectTitle !== "string") {
|
||||
throw new Error("POM state identity is invalid");
|
||||
}
|
||||
if (!Number.isInteger(state.currentStage) || Number(state.currentStage) < 0 || Number(state.currentStage) > 8) {
|
||||
throw new Error("POM current stage is invalid");
|
||||
}
|
||||
if (!state.stageRuns || typeof state.stageRuns !== "object") throw new Error("POM stage state is missing");
|
||||
for (const spec of STAGES) {
|
||||
const run = state.stageRuns[String(spec.id)];
|
||||
if (!run || !["queued", "active", "blocked", "complete"].includes(run.status)) {
|
||||
throw new Error(`POM stage ${spec.id} state is invalid`);
|
||||
}
|
||||
if (!Array.isArray(run.blockers) || !Array.isArray(run.evidence)) throw new Error(`POM stage ${spec.id} records are invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreLatest(entries: readonly unknown[]): PomState | undefined {
|
||||
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
||||
const entry = entries[index] as { type?: string; customType?: string; data?: unknown };
|
||||
if (entry?.type === "custom" && entry.customType === "pom-state") {
|
||||
assertState(entry.data);
|
||||
return structuredClone(entry.data);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function incompletePrerequisites(state: PomState, target: number): number[] {
|
||||
return STAGES.filter((item) => item.id < target && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
|
||||
}
|
||||
|
||||
export function gotoStage(state: PomState, target: number): PomState {
|
||||
stage(target);
|
||||
const missing = incompletePrerequisites(state, target);
|
||||
if (missing.length) throw new Error(`Cannot enter stage ${target}; incomplete prerequisites: ${missing.join(", ")}`);
|
||||
const next = structuredClone(state);
|
||||
next.currentStage = target;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function startStage(state: PomState, target = state.currentStage, allStages = false): PomState {
|
||||
const next = gotoStage(state, target);
|
||||
const run = next.stageRuns[String(target)];
|
||||
if (run.status === "complete") throw new Error(`Stage ${target} is already complete`);
|
||||
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
|
||||
if (unresolved.length) throw new Error(`Stage ${target} has unresolved blockers`);
|
||||
run.status = "active";
|
||||
run.attempts += 1;
|
||||
run.startedAt = new Date().toISOString();
|
||||
next.activeRun = { id: randomUUID(), stage: target, startedAt: run.startedAt, continuationCount: 0, allStages };
|
||||
next.currentStage = target;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function addBlocker(state: PomState, reason: string, owner?: string): PomState {
|
||||
const text = reason.trim();
|
||||
if (!text) throw new Error("Blocker reason is required");
|
||||
const next = structuredClone(state);
|
||||
const run = next.stageRuns[String(next.currentStage)];
|
||||
const blocker: StageBlocker = { id: randomUUID(), reason: text, owner, createdAt: new Date().toISOString() };
|
||||
run.blockers.push(blocker);
|
||||
run.status = "blocked";
|
||||
delete next.activeRun;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function resolveBlocker(state: PomState, blockerId: string, resolution: string): PomState {
|
||||
const next = structuredClone(state);
|
||||
const owner = Object.values(next.stageRuns).find((run) => run.blockers.some((item) => item.id === blockerId));
|
||||
const blocker = owner?.blockers.find((item) => item.id === blockerId);
|
||||
if (!owner || !blocker) throw new Error(`Unknown blocker: ${blockerId}`);
|
||||
if (blocker.resolvedAt) throw new Error(`Blocker already resolved: ${blockerId}`);
|
||||
blocker.resolvedAt = new Date().toISOString();
|
||||
blocker.resolution = resolution.trim() || "Resolved";
|
||||
if (owner.status === "blocked" && owner.blockers.every((item) => item.resolvedAt)) owner.status = "queued";
|
||||
return next;
|
||||
}
|
||||
|
||||
export function recordEvidence(
|
||||
state: PomState,
|
||||
criterionId: string,
|
||||
summary: string,
|
||||
paths: string[],
|
||||
agent?: string,
|
||||
): PomState {
|
||||
const spec = stage(state.currentStage);
|
||||
if (!spec.acceptance.some((criterion) => criterion.id === criterionId)) {
|
||||
throw new Error(`Criterion ${criterionId} does not belong to stage ${spec.id}`);
|
||||
}
|
||||
if (!summary.trim()) throw new Error("Evidence summary is required");
|
||||
if (!paths.length) throw new Error("Evidence must reference at least one project path");
|
||||
const next = structuredClone(state);
|
||||
const run = next.stageRuns[String(next.currentStage)];
|
||||
const record: EvidenceRecord = {
|
||||
id: randomUUID(),
|
||||
criterionId,
|
||||
summary: summary.trim(),
|
||||
paths: [...new Set(paths.map((path) => path.replaceAll("\\", "/")))],
|
||||
agent,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
run.evidence = [...run.evidence.filter((item) => item.criterionId !== criterionId), record];
|
||||
return next;
|
||||
}
|
||||
|
||||
export function passStage(state: PomState, validationReportPath: string): PomState {
|
||||
const next = structuredClone(state);
|
||||
const target = next.currentStage;
|
||||
const spec = stage(target);
|
||||
const run = next.stageRuns[String(target)];
|
||||
if (run.status !== "active" || next.activeRun?.stage !== target) throw new Error(`Stage ${target} is not actively running`);
|
||||
if (!validationReportPath.trim()) throw new Error("Validation report path is required");
|
||||
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
|
||||
if (unresolved.length) throw new Error(`Stage ${target} has unresolved blockers`);
|
||||
const missingEvidence = spec.acceptance.filter((criterion) => !run.evidence.some((evidence) => evidence.criterionId === criterion.id));
|
||||
if (missingEvidence.length) throw new Error(`Stage ${target} lacks evidence: ${missingEvidence.map((item) => item.id).join(", ")}`);
|
||||
run.status = "complete";
|
||||
run.completedAt = new Date().toISOString();
|
||||
run.validationReportPath = validationReportPath;
|
||||
next.completedStages = [...new Set([...next.completedStages, target])].sort((a, b) => a - b);
|
||||
delete next.activeRun;
|
||||
if (target < 8) next.currentStage = target + 1;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function stageProgress(state: PomState): { complete: number; total: number; percent: number } {
|
||||
const complete = STAGES.filter((item) => state.stageRuns[String(item.id)].status === "complete").length;
|
||||
return { complete, total: STAGES.length, percent: Math.round((complete / STAGES.length) * 100) };
|
||||
}
|
||||
|
||||
export function openBlockers(state: PomState): StageBlocker[] {
|
||||
return Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((item) => !item.resolvedAt);
|
||||
}
|
||||
|
||||
export function summarizeState(state: PomState): string {
|
||||
const spec = stage(state.currentStage);
|
||||
const run = state.stageRuns[String(state.currentStage)];
|
||||
const progress = stageProgress(state);
|
||||
const evidence = new Set(run.evidence.map((item) => item.criterionId)).size;
|
||||
return [
|
||||
`${spec.icon} POM · ${state.projectTitle}`,
|
||||
`Stage ${spec.id}/8 · ${spec.name} · ${run.status.toUpperCase()}`,
|
||||
`Progress ${progress.complete}/${progress.total} · ${progress.percent}%`,
|
||||
`Evidence ${evidence}/${spec.acceptance.length} · Blockers ${openBlockers(state).length}`,
|
||||
`Hive ${Object.keys(state.hive.missions).length} missions · ${state.hive.signalsLogged} signals`,
|
||||
`Root ${state.projectRoot}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function statePath(state: PomState): string {
|
||||
return join(state.projectRoot, "00_admin", "project_state.json");
|
||||
}
|
||||
70
src/studio.ts
Normal file
70
src/studio.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { stage, type HiveMission, type HiveTask, type PomConfig, type PomState } from "./domain";
|
||||
|
||||
export interface NativeTaskBatch {
|
||||
context: string;
|
||||
tasks: Array<{ name: string; agent: string; task: string; isolated?: boolean }>;
|
||||
}
|
||||
|
||||
function agentLabel(agent: string, index: number): string {
|
||||
return `${agent.replace(/^pom-/, "").split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join("")}${index + 1}`.slice(0, 80);
|
||||
}
|
||||
|
||||
export function buildHiveMission(state: PomState, config: PomConfig, preset: "lean" | "stage" | "audit" = "stage"): HiveMission {
|
||||
const spec = stage(state.currentStage);
|
||||
let agents = preset === "lean" ? spec.agents.slice(0, 2) : [...spec.agents];
|
||||
if (preset === "audit") agents = [...new Set(["pom-continuity-auditor", "pom-rights-auditor", "pom-critic", ...agents])];
|
||||
agents = agents.slice(0, config.maxStudioAgents);
|
||||
const artifacts = spec.artifacts.map((artifact) => artifact.path);
|
||||
const tasks: HiveTask[] = agents.map((agent, index) => ({
|
||||
name: agentLabel(agent, index),
|
||||
agent,
|
||||
objective: `Independently investigate and produce evidence for stage ${spec.id} (${spec.name}) from the perspective of ${agent}.`,
|
||||
owns: artifacts.filter((_path, artifactIndex) => artifactIndex % agents.length === index),
|
||||
acceptance: spec.acceptance.filter((_criterion, criterionIndex) => criterionIndex % agents.length === index).map((criterion) => `${criterion.id}: ${criterion.description}`),
|
||||
isolated: false,
|
||||
}));
|
||||
return {
|
||||
id: `s${spec.id}-${randomUUID().slice(0, 8)}`,
|
||||
stage: spec.id,
|
||||
title: `${spec.icon} Stage ${spec.id} Hive · ${spec.name}`,
|
||||
status: "planned",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
|
||||
export function missionToTaskBatch(state: PomState, mission: HiveMission): NativeTaskBatch {
|
||||
const spec = stage(mission.stage);
|
||||
const shared = [
|
||||
`Project: ${state.projectTitle}`,
|
||||
`Canonical root: ${state.projectRoot}`,
|
||||
`Stage: ${spec.id}/8 · ${spec.name}`,
|
||||
"Canonical files outrank chat. Read before changing anything.",
|
||||
"Coordinate through native IRC using only short messages prefixed CLAIM, QUERY, ANSWER, ALERT, HANDOFF, or DONE.",
|
||||
"Before modifying a shared path, send CLAIM to all. On collision, negotiate ownership before writing.",
|
||||
"Use pom_hive op=signal for durable milestone evidence; IRC is ephemeral coordination, the Hive ledger is durable truth.",
|
||||
"Never mark the stage passed. Return findings and evidence to Main; Main integrates and validates.",
|
||||
].join("\n");
|
||||
return {
|
||||
context: shared,
|
||||
tasks: mission.tasks.map((task) => ({
|
||||
name: task.name,
|
||||
agent: task.agent,
|
||||
isolated: task.isolated,
|
||||
task: [
|
||||
`# Mission\n${task.objective}`,
|
||||
`# Ownership\n${task.owns.length ? task.owns.map((path) => `- ${path}`).join("\n") : "- No exclusive files; inspect and advise only."}`,
|
||||
`# Acceptance\n${task.acceptance.length ? task.acceptance.map((item) => `- ${item}`).join("\n") : "- Return evidence-backed findings relevant to the stage contract."}`,
|
||||
"# Execution Contract",
|
||||
"1. Read canonical dependencies.",
|
||||
"2. Announce CLAIM for owned paths through IRC.",
|
||||
"3. Work independently; do not overwrite another agent's claimed path.",
|
||||
"4. Record durable evidence with pom_hive op=signal.",
|
||||
"5. Send DONE or ALERT to Main through IRC.",
|
||||
"6. Finish with a compact evidence report containing paths, decisions, risks, and unresolved questions.",
|
||||
].join("\n\n"),
|
||||
})),
|
||||
};
|
||||
}
|
||||
83
src/swarm.ts
Normal file
83
src/swarm.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { join } from "node:path";
|
||||
import type { HiveMission, PomState } from "./domain";
|
||||
import { atomicWrite } from "./persistence";
|
||||
import { secureFuturePath } from "./paths";
|
||||
|
||||
export type PomSwarmMode = "parallel" | "sequential" | "pipeline";
|
||||
|
||||
function yamlString(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function block(value: string, indent: number): string[] {
|
||||
const prefix = " ".repeat(indent);
|
||||
return value.replaceAll("\r\n", "\n").split("\n").map((line) => `${prefix}${line}`);
|
||||
}
|
||||
|
||||
function key(value: string, index: number): string {
|
||||
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
||||
return `${normalized || "agent"}_${index + 1}`;
|
||||
}
|
||||
|
||||
export async function writeSwarmDefinition(
|
||||
state: PomState,
|
||||
mission: HiveMission,
|
||||
mode: PomSwarmMode = "parallel",
|
||||
targetCount = 1,
|
||||
): Promise<string> {
|
||||
if (!Number.isInteger(targetCount) || targetCount < 1 || targetCount > 100) throw new Error("Swarm target count must be 1..100");
|
||||
const directory = join("00_admin", "swarms");
|
||||
const relativePath = join(directory, `${mission.id}.yaml`).replaceAll("\\", "/");
|
||||
const absolutePath = await secureFuturePath(state.projectRoot, relativePath);
|
||||
const names = mission.tasks.map((task, index) => key(task.name, index));
|
||||
const lines = [
|
||||
"swarm:",
|
||||
` name: ${yamlString(`pom-${mission.id}`)}`,
|
||||
` workspace: ${yamlString(state.projectRoot)}`,
|
||||
` mode: ${mode}`,
|
||||
...(mode === "pipeline" ? [` target_count: ${targetCount}`] : []),
|
||||
" agents:",
|
||||
];
|
||||
|
||||
mission.tasks.forEach((task, index) => {
|
||||
const agentName = names[index];
|
||||
const reportPath = `06_ledgers/swarm/${mission.id}/${agentName}.md`;
|
||||
lines.push(
|
||||
` ${agentName}:`,
|
||||
` role: ${yamlString(task.agent)}`,
|
||||
" task: |",
|
||||
...block([
|
||||
`Project: ${state.projectTitle}`,
|
||||
`Stage: ${mission.stage}`,
|
||||
`Objective: ${task.objective}`,
|
||||
"Canonical project files outrank chat and unsupported assumptions.",
|
||||
`Exclusive ownership: ${task.owns.join(", ") || "advisory only"}`,
|
||||
`Acceptance: ${task.acceptance.join(" | ") || "evidence-backed specialist findings"}`,
|
||||
`Write a durable report to ${reportPath}.`,
|
||||
"Do not pass the POM stage. Report paths, decisions, evidence, risks, and unresolved blockers.",
|
||||
].join("\n"), 8),
|
||||
" extra_context: |",
|
||||
...block("Use OMP tools autonomously. Read before editing. Never overwrite another agent's declared ownership. Durable coordination belongs in files; concise status belongs in the orchestrator log.", 8),
|
||||
" reports_to:",
|
||||
" - integrator",
|
||||
);
|
||||
});
|
||||
|
||||
lines.push(
|
||||
" integrator:",
|
||||
" role: pom-architect",
|
||||
" task: |",
|
||||
...block([
|
||||
`Read every specialist report under 06_ledgers/swarm/${mission.id}/.`,
|
||||
"Compare findings against canonical files and the active stage contract.",
|
||||
`Write an integration report to 06_ledgers/swarm/${mission.id}/integration.md.`,
|
||||
"Do not silently modify canon where specialists disagree; record the conflict and smallest repair.",
|
||||
"Return an evidence map suitable for Main to register through POM and validate.",
|
||||
].join("\n"), 8),
|
||||
" waits_for:",
|
||||
...names.map((name) => ` - ${name}`),
|
||||
);
|
||||
|
||||
await atomicWrite(absolutePath, `${lines.join("\n")}\n`);
|
||||
return relativePath;
|
||||
}
|
||||
23
src/themes.ts
Normal file
23
src/themes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { copyFile, mkdir, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getCustomThemesDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
export interface InstalledTheme {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function installBundledThemes(): Promise<InstalledTheme[]> {
|
||||
const sourceDir = fileURLToPath(new URL("../themes", import.meta.url));
|
||||
const targetDir = getCustomThemesDir();
|
||||
await mkdir(targetDir, { recursive: true });
|
||||
const files = (await readdir(sourceDir)).filter((file) => file.endsWith(".json")).sort();
|
||||
const installed: InstalledTheme[] = [];
|
||||
for (const file of files) {
|
||||
const target = join(targetDir, file);
|
||||
await copyFile(join(sourceDir, file), target);
|
||||
installed.push({ name: file.slice(0, -5), path: target });
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
32
src/tool-profiles.ts
Normal file
32
src/tool-profiles.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
||||
import type { PomToolProfile } from "./domain";
|
||||
|
||||
const POM_TOOLS = ["pom_state", "pom_stage", "pom_artifact", "pom_validate", "pom_prompt", "pom_hive", "pom_export"];
|
||||
const PROFILES: Record<Exclude<PomToolProfile, "auto">, string[]> = {
|
||||
creative: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "render_mermaid", "generate_image", "inspect_image"],
|
||||
research: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "web_search", "browser", "github"],
|
||||
draft: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "inspect_image"],
|
||||
audit: ["read", "grep", "glob", "eval", "lsp", "task", "irc", "todo", "ask", "inspect_image", "github"],
|
||||
delivery: ["read", "write", "edit", "grep", "glob", "eval", "bash", "task", "irc", "todo", "checkpoint", "github"],
|
||||
};
|
||||
|
||||
let baselines = new WeakMap<object, string[]>();
|
||||
|
||||
export async function applyToolProfile(pi: ExtensionAPI, profile: PomToolProfile): Promise<string[]> {
|
||||
const key = pi as unknown as object;
|
||||
let baseline = baselines.get(key);
|
||||
if (!baseline) {
|
||||
baseline = [...pi.getActiveTools()];
|
||||
baselines.set(key, baseline);
|
||||
}
|
||||
const all = new Set(pi.getAllTools());
|
||||
const requested = profile === "auto" ? baseline : [...PROFILES[profile], ...POM_TOOLS];
|
||||
const selected = [...new Set(requested)].filter((name) => all.has(name));
|
||||
if (!selected.includes("read") && all.has("read")) selected.unshift("read");
|
||||
await pi.setActiveTools(selected);
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function resetToolBaselineForTests(): void {
|
||||
baselines = new WeakMap<object, string[]>();
|
||||
}
|
||||
327
src/tools.ts
Normal file
327
src/tools.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
addBlocker,
|
||||
bumpPatchVersion,
|
||||
gotoStage,
|
||||
passStage,
|
||||
recordEvidence,
|
||||
resolveBlocker,
|
||||
startStage,
|
||||
summarizeState,
|
||||
} from "./state";
|
||||
import { stage, type ArtifactRecord, type PomState, type ValidationScope } from "./domain";
|
||||
import {
|
||||
appendMarkdownLog,
|
||||
recordHiveEvent,
|
||||
registerArtifact,
|
||||
syncKnowledgeBase,
|
||||
writeHiveMission,
|
||||
} from "./persistence";
|
||||
import { validatePom } from "./validators";
|
||||
import { getPrompt, listPrompts, renderStagePrompt } from "./prompts";
|
||||
import { buildHiveMission, missionToTaskBatch } from "./studio";
|
||||
import { exportProject } from "./exporter";
|
||||
import { loadPomConfig } from "./config";
|
||||
import { routeStage } from "./router";
|
||||
import { checkpointStage, enterStageBranch } from "./git";
|
||||
import { toolCallComponent, toolResultComponent, type PomTheme } from "./visual";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
import { updateChrome } from "./ui";
|
||||
|
||||
function visual(runtime: PomRuntime, label: string) {
|
||||
return {
|
||||
renderCall(args: Record<string, unknown>, options: { spinnerFrame?: number }, theme: PomTheme) {
|
||||
const action = [args.action, args.op, args.scope, args.kind, args.path].find((value) => typeof value === "string") as string | undefined;
|
||||
return toolCallComponent(label, action, theme, runtime.get().motion, options.spinnerFrame);
|
||||
},
|
||||
renderResult(
|
||||
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
||||
options: { expanded: boolean; isPartial: boolean },
|
||||
theme: PomTheme,
|
||||
) {
|
||||
return toolResultComponent(result, options.expanded, options.isPartial, theme, runtime.get().view);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function textResult(text: string, details?: unknown, isError = false) {
|
||||
return { content: [{ type: "text" as const, text }], details, isError };
|
||||
}
|
||||
|
||||
async function refresh(_pi: ExtensionAPI, runtime: PomRuntime, ctx: ExtensionContext): Promise<void> {
|
||||
const state = runtime.get();
|
||||
const config = await loadPomConfig(state.projectRoot);
|
||||
updateChrome(ctx, state, config, runtime);
|
||||
}
|
||||
|
||||
export function registerPomTools(pi: ExtensionAPI, runtime: PomRuntime): void {
|
||||
const { z } = pi.zod;
|
||||
|
||||
// @ts-expect-error OMP 16.4.x + Zod v4 can exceed TypeScript instantiation depth while contextualizing this complete tool schema.
|
||||
pi.registerTool({
|
||||
...visual(runtime, "State"),
|
||||
name: "pom_state",
|
||||
label: "POM State",
|
||||
description: "Read or update canonical POM configuration, decisions, assumptions, and warnings.",
|
||||
defaultInactive: true,
|
||||
approval: "write",
|
||||
parameters: z.object({
|
||||
action: z.enum(["get", "configure", "decision", "assumption", "warning"]),
|
||||
value: z.string().optional(),
|
||||
mode: z.enum(["autopilot", "collaborative", "strict-autopilot"]).optional(),
|
||||
view: z.enum(["cinematic", "rich", "compact", "minimal"]).optional(),
|
||||
motion: z.enum(["off", "subtle", "full"]).optional(),
|
||||
}),
|
||||
async execute(_id, params, signal, _onUpdate, ctx) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
const state = structuredClone(runtime.get());
|
||||
if (params.action === "configure") {
|
||||
state.mode = params.mode ?? state.mode;
|
||||
state.view = params.view ?? state.view;
|
||||
state.motion = params.motion ?? state.motion;
|
||||
}
|
||||
if (params.action === "decision" && params.value?.trim() && !state.decisions.includes(params.value.trim())) state.decisions.push(params.value.trim());
|
||||
if (params.action === "assumption" && params.value?.trim() && !state.assumptions.includes(params.value.trim())) state.assumptions.push(params.value.trim());
|
||||
if (params.action === "warning" && params.value?.trim() && !state.warnings.includes(params.value.trim())) state.warnings.push(params.value.trim());
|
||||
if (params.action !== "get") await runtime.set(state);
|
||||
await refresh(pi, runtime, ctx);
|
||||
return textResult(summarizeState(runtime.get()), runtime.get());
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Stage"),
|
||||
name: "pom_stage",
|
||||
label: "POM Stage",
|
||||
description: "Start, inspect, evidence, block, resolve, pass, or navigate the authoritative stage machine.",
|
||||
defaultInactive: true,
|
||||
approval: "write",
|
||||
parameters: z.object({
|
||||
action: z.enum(["inspect", "start", "evidence", "block", "resolve", "pass", "goto"]),
|
||||
stage: z.number().int().min(0).max(8).optional(),
|
||||
criterionId: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
paths: z.array(z.string()).optional(),
|
||||
agent: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
owner: z.string().optional(),
|
||||
blockerId: z.string().optional(),
|
||||
resolution: z.string().optional(),
|
||||
allStages: z.boolean().optional(),
|
||||
validationScope: z.enum(["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"]).optional(),
|
||||
}),
|
||||
async execute(_id, params, signal, onUpdate, ctx) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
let state = runtime.get();
|
||||
if (params.action === "start") {
|
||||
state = startStage(state, params.stage ?? state.currentStage, params.allStages ?? false);
|
||||
await runtime.set(state);
|
||||
const config = await loadPomConfig(state.projectRoot);
|
||||
await enterStageBranch(state, config);
|
||||
await routeStage(pi, ctx, state);
|
||||
} else if (params.action === "goto") {
|
||||
if (params.stage === undefined) throw new Error("stage is required");
|
||||
state = gotoStage(state, params.stage);
|
||||
await runtime.set(state);
|
||||
} else if (params.action === "evidence") {
|
||||
if (!params.criterionId || !params.summary) throw new Error("criterionId and summary are required");
|
||||
state = recordEvidence(state, params.criterionId, params.summary, params.paths ?? [], params.agent);
|
||||
await runtime.set(state);
|
||||
} else if (params.action === "block") {
|
||||
state = addBlocker(state, params.reason ?? "Unspecified blocker", params.owner);
|
||||
await runtime.set(state);
|
||||
} else if (params.action === "resolve") {
|
||||
if (!params.blockerId) throw new Error("blockerId is required");
|
||||
state = resolveBlocker(state, params.blockerId, params.resolution ?? "Resolved");
|
||||
await runtime.set(state);
|
||||
} else if (params.action === "pass") {
|
||||
onUpdate?.({ content: [{ type: "text", text: "Running deterministic stage gate…" }] });
|
||||
const wasAllStages = state.activeRun?.allStages ?? false;
|
||||
const scope = (params.validationScope ?? (state.currentStage === 8 ? "delivery" : "stage")) as ValidationScope;
|
||||
const report = await validatePom(state, scope);
|
||||
runtime.telemetry.validationFailures = report.checks.filter((item) => item.status === "fail").length;
|
||||
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${scope}`, display: true, details: report }, { triggerTurn: false });
|
||||
if (!report.passed || !report.reportPath) return textResult(`Stage gate FAIL · ${runtime.telemetry.validationFailures} failures`, report, true);
|
||||
const passedStage = state.currentStage;
|
||||
state = passStage(state, report.reportPath);
|
||||
await runtime.set(state);
|
||||
const config = await loadPomConfig(state.projectRoot);
|
||||
const commit = await checkpointStage(state, config, `pom: complete stage ${passedStage} ${stage(passedStage).slug}`);
|
||||
pi.sendMessage({ customType: "pom:milestone", content: `Stage ${passedStage} complete`, display: true, details: { stage: passedStage, status: "PASS", path: report.reportPath, commit } }, { triggerTurn: false });
|
||||
if (wasAllStages && passedStage < 8) {
|
||||
state = startStage(state, state.currentStage, true);
|
||||
await runtime.set(state);
|
||||
await enterStageBranch(state, config);
|
||||
await routeStage(pi, ctx, state);
|
||||
pi.sendUserMessage(await renderStagePrompt(state, true), { deliverAs: "followUp" });
|
||||
}
|
||||
}
|
||||
await syncKnowledgeBase(runtime.get());
|
||||
await refresh(pi, runtime, ctx);
|
||||
return textResult(summarizeState(runtime.get()), runtime.get());
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Artifact"),
|
||||
name: "pom_artifact",
|
||||
label: "POM Artifact",
|
||||
description: "Register a non-empty canonical artifact with measured size, SHA-256, status, source, and rights metadata.",
|
||||
defaultInactive: true,
|
||||
approval: "write",
|
||||
parameters: z.object({
|
||||
path: z.string(),
|
||||
category: z.string().default("other"),
|
||||
status: z.enum(["draft", "reviewed", "approved", "superseded"]).default("draft"),
|
||||
rightsStatus: z.enum(["original", "licensed", "public-domain", "reference-only", "unknown"]).default("unknown"),
|
||||
source: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
}),
|
||||
async execute(_id, params, signal) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
const state = runtime.get();
|
||||
const artifactVersion = bumpPatchVersion(state.artifactVersion);
|
||||
const record = await registerArtifact(state, params.path, {
|
||||
category: params.category,
|
||||
status: params.status as ArtifactRecord["status"],
|
||||
rightsStatus: params.rightsStatus as ArtifactRecord["rightsStatus"],
|
||||
source: params.source,
|
||||
notes: params.notes,
|
||||
version: artifactVersion,
|
||||
});
|
||||
const next = structuredClone(state);
|
||||
next.artifactVersion = artifactVersion;
|
||||
if (/canon|bible|continuity|timeline|world|character/i.test(params.category)) {
|
||||
next.canonVersion = bumpPatchVersion(next.canonVersion);
|
||||
}
|
||||
await runtime.set(next);
|
||||
return textResult(`Registered ${record.path} · v${record.version} · ${record.bytes} bytes · ${record.sha256.slice(0, 12)}`, record);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Validate"),
|
||||
name: "pom_validate",
|
||||
label: "POM Validate",
|
||||
description: "Run deterministic state, artifact, evidence, manifest, knowledge, continuity, and delivery checks.",
|
||||
defaultInactive: true,
|
||||
approval: "read",
|
||||
parameters: z.object({
|
||||
scope: z.enum(["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"]).default("stage"),
|
||||
}),
|
||||
async execute(_id, params, signal, onUpdate) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
onUpdate?.({ content: [{ type: "text", text: `Validating ${params.scope}…` }] });
|
||||
const report = await validatePom(runtime.get(), params.scope as ValidationScope);
|
||||
const failures = report.checks.filter((item) => item.status === "fail").length;
|
||||
runtime.telemetry.validationFailures = failures;
|
||||
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${params.scope}`, display: true, details: report }, { triggerTurn: false });
|
||||
return textResult(`${report.passed ? "PASS" : "FAIL"} · ${failures} failures · ${report.checks.length} checks`, report, !report.passed);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Prompt"),
|
||||
name: "pom_prompt",
|
||||
label: "POM Prompt",
|
||||
description: "List or retrieve versioned POM prompt fragments, including project-local overrides and provenance hashes.",
|
||||
defaultInactive: true,
|
||||
approval: "read",
|
||||
parameters: z.object({ action: z.enum(["list", "get"]), id: z.string().optional() }),
|
||||
async execute(_id, params) {
|
||||
const state = runtime.get().initialized ? runtime.get() : undefined;
|
||||
if (params.action === "list") {
|
||||
const prompts = await listPrompts(state);
|
||||
return textResult(prompts.map((item) => `${item.id}\t${item.source}\t${item.sha256.slice(0, 12)}`).join("\n"), prompts);
|
||||
}
|
||||
if (!params.id) throw new Error("id is required");
|
||||
const prompt = await getPrompt(state, params.id);
|
||||
return textResult(prompt.content, prompt);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Hive"),
|
||||
name: "pom_hive",
|
||||
label: "POM Hive",
|
||||
description: "Plan native task batches, persist inter-agent signals, and record final synthesis without replacing OMP task or IRC.",
|
||||
defaultInactive: true,
|
||||
approval: "write",
|
||||
parameters: z.object({
|
||||
op: z.enum(["plan", "signal", "synthesize", "inspect"]),
|
||||
preset: z.enum(["lean", "stage", "audit"]).optional(),
|
||||
missionId: z.string().optional(),
|
||||
signal: z.enum(["CLAIM", "QUERY", "ANSWER", "ALERT", "HANDOFF", "DONE"]).optional(),
|
||||
from: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
paths: z.array(z.string()).optional(),
|
||||
status: z.enum(["complete", "blocked"]).optional(),
|
||||
synthesis: z.string().optional(),
|
||||
}),
|
||||
async execute(_id, params, signal, _onUpdate, ctx) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
let state = structuredClone(runtime.get());
|
||||
if (params.op === "inspect") return textResult(JSON.stringify(state.hive, null, 2), state.hive);
|
||||
if (params.op === "plan") {
|
||||
const config = await loadPomConfig(state.projectRoot);
|
||||
const mission = buildHiveMission(state, config, params.preset ?? "stage");
|
||||
const batch = missionToTaskBatch(state, mission);
|
||||
state.hive.activeMissionId = mission.id;
|
||||
state.hive.missions[mission.id] = mission;
|
||||
state.stageRuns[String(state.currentStage)].hiveMissionIds.push(mission.id);
|
||||
await runtime.set(state);
|
||||
const path = await writeHiveMission(state, mission);
|
||||
await recordHiveEvent(state, { type: "mission.planned", mission, path });
|
||||
pi.sendMessage({ customType: "pom:hive", content: mission.title, display: true, details: mission }, { triggerTurn: false });
|
||||
await refresh(pi, runtime, ctx);
|
||||
return textResult(`Call native task exactly once with this batch:\n${JSON.stringify(batch, null, 2)}`, { mission, batch, path });
|
||||
}
|
||||
const missionId = params.missionId ?? state.hive.activeMissionId;
|
||||
if (!missionId || !state.hive.missions[missionId]) throw new Error("No matching hive mission");
|
||||
const mission = state.hive.missions[missionId];
|
||||
if (params.op === "signal") {
|
||||
if (!params.signal || !params.message) throw new Error("signal and message are required");
|
||||
state.hive.signalsLogged += 1;
|
||||
runtime.telemetry.lastIrcSignal = `${params.signal} ${params.from ?? "agent"}: ${params.message}`;
|
||||
await runtime.set(state);
|
||||
await recordHiveEvent(state, { type: "hive.signal", missionId, signal: params.signal, from: params.from, to: params.to, message: params.message, paths: params.paths ?? [] });
|
||||
return textResult(`${params.signal} recorded for ${missionId}`, { missionId, ...params });
|
||||
}
|
||||
mission.status = params.status === "blocked" ? "blocked" : "complete";
|
||||
mission.synthesis = params.synthesis?.trim() || "Synthesis recorded without narrative.";
|
||||
mission.updatedAt = new Date().toISOString();
|
||||
if (state.hive.activeMissionId === missionId) delete state.hive.activeMissionId;
|
||||
await runtime.set(state);
|
||||
const path = await writeHiveMission(state, mission);
|
||||
await recordHiveEvent(state, { type: "mission.synthesized", missionId, status: mission.status, synthesis: mission.synthesis, path });
|
||||
pi.sendMessage({ customType: "pom:hive", content: `${mission.title} ${mission.status}`, display: true, details: mission }, { triggerTurn: false });
|
||||
return textResult(`Hive ${missionId} ${mission.status}`, { mission, path });
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
...visual(runtime, "Export"),
|
||||
name: "pom_export",
|
||||
label: "POM Export",
|
||||
description: "Create a fresh delivery archive, parse its central directory, decompress every member, verify sizes and CRC-32, and hash the archive.",
|
||||
defaultInactive: true,
|
||||
approval: "write",
|
||||
parameters: z.object({ kind: z.enum(["checkpoint", "final"]).default("checkpoint") }),
|
||||
async execute(_id, params, signal, onUpdate, ctx) {
|
||||
if (signal?.aborted) return textResult("Cancelled", undefined, true);
|
||||
onUpdate?.({ content: [{ type: "text", text: "Building and verifying archive…" }] });
|
||||
const result = await exportProject(runtime.get(), params.kind);
|
||||
await appendMarkdownLog(runtime.get().projectRoot, "delivery_ledger.md", "Archive verified", [
|
||||
`Archive: ${result.zipPath}`,
|
||||
`SHA-256: ${result.verification.sha256}`,
|
||||
`Members: ${result.verification.members.length}`,
|
||||
`Verification: ${result.verificationPath}`,
|
||||
].join("\n\n"));
|
||||
pi.sendMessage({ customType: "pom:milestone", content: "Archive verified", display: true, details: { stage: runtime.get().currentStage, status: "ARCHIVE PASS", path: result.verificationPath } }, { triggerTurn: false });
|
||||
await refresh(pi, runtime, ctx);
|
||||
return textResult(`PASS · ${result.zipPath} · ${result.verification.members.length} members`, result);
|
||||
},
|
||||
});
|
||||
}
|
||||
35
src/ui.ts
Normal file
35
src/ui.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
||||
import type { PomConfig, PomState } from "./domain";
|
||||
import { stage } from "./domain";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
import { PomCommandCenter, PomHudComponent, type CommandCenterAction, type PomTheme } from "./visual";
|
||||
import { openBlockers, stageProgress } from "./state";
|
||||
|
||||
export function updateChrome(ctx: ExtensionContext, state: PomState, config: PomConfig, runtime: PomRuntime): void {
|
||||
if (!ctx.hasUI) return;
|
||||
const spec = stage(state.currentStage);
|
||||
const progress = stageProgress(state);
|
||||
const blockers = openBlockers(state).length;
|
||||
ctx.ui.setTitle(`POM · ${state.projectTitle} · ${spec.id}/8`);
|
||||
ctx.ui.setStatus("pom-stage", `${spec.icon} ${spec.id}/8 ${spec.slug}`);
|
||||
ctx.ui.setStatus("pom-progress", `${progress.percent}%`);
|
||||
ctx.ui.setStatus("pom-gate", blockers ? `⚠ ${blockers}` : `◆ ${state.stageRuns[String(spec.id)].evidence.length}/${spec.acceptance.length}`);
|
||||
ctx.ui.setStatus("pom-hive", state.hive.activeMissionId ? `⬡ ${state.hive.activeMissionId}` : undefined);
|
||||
if (config.dashboardWidget && state.view !== "minimal") {
|
||||
ctx.ui.setWidget("pom-hud", (_tui, theme) => new PomHudComponent(runtime, theme as unknown as PomTheme), { placement: config.hudPlacement });
|
||||
} else {
|
||||
ctx.ui.setWidget("pom-hud", undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearChrome(ctx: ExtensionContext): void {
|
||||
if (!ctx.hasUI) return;
|
||||
for (const key of ["pom-stage", "pom-progress", "pom-gate", "pom-hive", "pom-retry", "pom-approval", "pom-goal"]) ctx.ui.setStatus(key, undefined);
|
||||
ctx.ui.setWidget("pom-hud", undefined);
|
||||
ctx.ui.setWorkingMessage();
|
||||
}
|
||||
|
||||
export async function openCommandCenter(ctx: ExtensionContext, state: PomState): Promise<CommandCenterAction> {
|
||||
if (!ctx.hasUI) return "close";
|
||||
return ctx.ui.custom<CommandCenterAction>((_tui, theme, keybindings, done) => new PomCommandCenter(state, theme as unknown as PomTheme, keybindings, done), { overlay: true });
|
||||
}
|
||||
220
src/validators.ts
Normal file
220
src/validators.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, extname, join, relative, resolve } from "node:path";
|
||||
import { stage, STAGES, type PomState, type ValidationScope } from "./domain";
|
||||
import { atomicWrite, loadManifest, pathExists, sha256File } from "./persistence";
|
||||
import { secureExistingPath } from "./paths";
|
||||
|
||||
export interface ValidationCheck {
|
||||
id: string;
|
||||
status: "pass" | "fail" | "warn";
|
||||
message: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface ValidationReport {
|
||||
schemaVersion: "1.0.0";
|
||||
projectId: string;
|
||||
scope: ValidationScope;
|
||||
stage: number;
|
||||
generatedAt: string;
|
||||
passed: boolean;
|
||||
checks: ValidationCheck[];
|
||||
reportPath?: string;
|
||||
}
|
||||
|
||||
function check(checks: ValidationCheck[], id: string, passed: boolean, message: string, path?: string): void {
|
||||
checks.push({ id, status: passed ? "pass" : "fail", message, path });
|
||||
}
|
||||
|
||||
function warn(checks: ValidationCheck[], id: string, message: string, path?: string): void {
|
||||
checks.push({ id, status: "warn", message, path });
|
||||
}
|
||||
|
||||
function countWords(text: string): number {
|
||||
return text.trim() ? text.trim().split(/\s+/).length : 0;
|
||||
}
|
||||
|
||||
async function validateState(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
check(checks, "state.initialized", state.initialized, state.initialized ? "Project initialized" : "Project is not initialized");
|
||||
check(checks, "state.stage-range", Number.isInteger(state.currentStage) && state.currentStage >= 0 && state.currentStage <= 8, `Current stage is ${state.currentStage}`);
|
||||
const complete = new Set(state.completedStages);
|
||||
const gaps = state.completedStages.filter((item) => STAGES.some((candidate) => candidate.id < item && !complete.has(candidate.id)));
|
||||
check(checks, "state.sequential", gaps.length === 0, gaps.length ? `Completed-stage sequence has gaps before: ${gaps.join(", ")}` : "Completed stages are sequential");
|
||||
const active = Object.entries(state.stageRuns).filter(([, run]) => run.status === "active");
|
||||
check(checks, "state.single-active", active.length <= 1, active.length <= 1 ? "At most one stage is active" : `Multiple active stages: ${active.map(([id]) => id).join(", ")}`);
|
||||
const open = Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((blocker) => !blocker.resolvedAt);
|
||||
if (open.length) warn(checks, "state.blockers", `${open.length} unresolved blocker(s) remain`);
|
||||
else check(checks, "state.blockers", true, "No unresolved blockers");
|
||||
}
|
||||
|
||||
async function validateArtifactRequirement(state: PomState, checks: ValidationCheck[], requirement: ReturnType<typeof stage>["artifacts"][number]): Promise<void> {
|
||||
const path = requirement.path;
|
||||
try {
|
||||
const absolute = await secureExistingPath(state.projectRoot, path, { allowDirectory: requirement.kind === "directory" });
|
||||
const fileStat = await stat(absolute);
|
||||
if (requirement.kind === "directory") {
|
||||
const entries = await readdir(absolute);
|
||||
const visible = entries.filter((entry) => !entry.startsWith("."));
|
||||
check(checks, `artifact.${path}.entries`, visible.length >= (requirement.minEntries ?? 1), `${path} contains ${visible.length} entries; requires ${requirement.minEntries ?? 1}`, path);
|
||||
return;
|
||||
}
|
||||
check(checks, `artifact.${path}.bytes`, fileStat.size >= (requirement.minBytes ?? 1), `${path} is ${fileStat.size} bytes; requires ${requirement.minBytes ?? 1}`, path);
|
||||
const text = await readFile(absolute, "utf8");
|
||||
for (const heading of requirement.requiredHeadings ?? []) {
|
||||
const found = new RegExp(`^#{1,6}\\s+.*${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*$`, "im").test(text);
|
||||
check(checks, `artifact.${path}.heading.${heading}`, found, found ? `Heading found: ${heading}` : `Missing heading: ${heading}`, path);
|
||||
}
|
||||
for (const pattern of requirement.requiredPatterns ?? []) {
|
||||
const found = new RegExp(pattern, "im").test(text);
|
||||
check(checks, `artifact.${path}.pattern.${pattern}`, found, found ? `Required pattern found: ${pattern}` : `Missing required pattern: ${pattern}`, path);
|
||||
}
|
||||
for (const pattern of requirement.forbiddenPatterns ?? []) {
|
||||
const found = new RegExp(pattern, "im").test(text);
|
||||
check(checks, `artifact.${path}.forbidden.${pattern}`, !found, found ? `Forbidden placeholder found: ${pattern}` : `No forbidden placeholder: ${pattern}`, path);
|
||||
}
|
||||
check(checks, `artifact.${path}.words`, countWords(text) > 0, `${path} contains ${countWords(text)} words`, path);
|
||||
} catch (error) {
|
||||
check(checks, `artifact.${path}.exists`, false, error instanceof Error ? error.message : String(error), path);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateStage(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
const spec = stage(state.currentStage);
|
||||
const run = state.stageRuns[String(spec.id)];
|
||||
const missingPrerequisites = STAGES.filter((item) => item.id < spec.id && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
|
||||
check(checks, "stage.prerequisites", missingPrerequisites.length === 0, missingPrerequisites.length ? `Incomplete prerequisite stages: ${missingPrerequisites.join(", ")}` : "All prerequisite stages are complete");
|
||||
for (const artifact of spec.artifacts) await validateArtifactRequirement(state, checks, artifact);
|
||||
for (const criterion of spec.acceptance) {
|
||||
const evidence = run.evidence.find((item) => item.criterionId === criterion.id);
|
||||
check(checks, `evidence.${criterion.id}`, Boolean(evidence), evidence ? `${criterion.label}: ${evidence.summary}` : `Missing evidence: ${criterion.label}`);
|
||||
if (evidence) {
|
||||
for (const evidencePath of evidence.paths) {
|
||||
try {
|
||||
await secureExistingPath(state.projectRoot, evidencePath, { allowDirectory: true });
|
||||
check(checks, `evidence.${criterion.id}.${evidencePath}`, true, `Evidence path exists: ${evidencePath}`, evidencePath);
|
||||
} catch (error) {
|
||||
check(checks, `evidence.${criterion.id}.${evidencePath}`, false, error instanceof Error ? error.message : String(error), evidencePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
|
||||
check(checks, "stage.blockers", unresolved.length === 0, unresolved.length ? `${unresolved.length} unresolved stage blocker(s)` : "No unresolved stage blockers");
|
||||
}
|
||||
|
||||
async function validateManifest(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
let records;
|
||||
try {
|
||||
records = await loadManifest(state);
|
||||
} catch (error) {
|
||||
check(checks, "manifest.schema", false, error instanceof Error ? error.message : String(error));
|
||||
return;
|
||||
}
|
||||
check(checks, "manifest.nonempty", records.length > 0, records.length ? `${records.length} artifact(s) registered` : "Artifact manifest is empty");
|
||||
for (const record of records) {
|
||||
try {
|
||||
const absolute = await secureExistingPath(state.projectRoot, record.path);
|
||||
const fileStat = await stat(absolute);
|
||||
check(checks, `manifest.${record.path}.bytes`, fileStat.size === record.bytes, fileStat.size === record.bytes ? `Size matches for ${record.path}` : `Size drift for ${record.path}: manifest ${record.bytes}, actual ${fileStat.size}`, record.path);
|
||||
const digest = await sha256File(absolute);
|
||||
check(checks, `manifest.${record.path}.sha256`, digest === record.sha256, digest === record.sha256 ? `Hash matches for ${record.path}` : `Hash drift for ${record.path}`, record.path);
|
||||
if (record.status === "approved") check(checks, `manifest.${record.path}.rights`, record.rightsStatus !== "unknown", record.rightsStatus === "unknown" ? `Approved artifact has unknown rights: ${record.path}` : `Rights recorded: ${record.rightsStatus}`, record.path);
|
||||
} catch (error) {
|
||||
check(checks, `manifest.${record.path}.exists`, false, error instanceof Error ? error.message : String(error), record.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function markdownFiles(root: string): Promise<string[]> {
|
||||
const output: string[] = [];
|
||||
async function walk(directory: string): Promise<void> {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") output.push(path);
|
||||
}
|
||||
}
|
||||
if (await pathExists(root)) await walk(root);
|
||||
return output;
|
||||
}
|
||||
|
||||
async function validateKnowledge(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
const contentRoot = join(state.projectRoot, "content");
|
||||
const required = ["index.md", "production-board.md", "artifact-index.md", "01-planning.md", "02-story-bible.md", "03-manuscript.md", "04-visuals.md", "05-research.md", "06-ledgers.md", "07-delivery.md"];
|
||||
for (const file of required) check(checks, `knowledge.${file}`, await pathExists(join(contentRoot, file)), `Knowledge page ${file} ${await pathExists(join(contentRoot, file)) ? "exists" : "is missing"}`, `content/${file}`);
|
||||
for (const file of await markdownFiles(contentRoot)) {
|
||||
const text = await readFile(file, "utf8");
|
||||
check(checks, `knowledge.frontmatter.${relative(contentRoot, file)}`, text.startsWith("---\n"), `Frontmatter ${text.startsWith("---\n") ? "present" : "missing"}`, relative(state.projectRoot, file));
|
||||
const links = [...text.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g)].map((match) => match[1].trim());
|
||||
for (const target of links) {
|
||||
if (/^[a-z]+:\/\//i.test(target)) continue;
|
||||
const base = target.startsWith("../") || target.startsWith("./") ? resolve(dirname(file), target) : resolve(contentRoot, target);
|
||||
const candidates = [base, `${base}.md`, join(base, "index.md")];
|
||||
const exists = (await Promise.all(candidates.map(pathExists))).some(Boolean);
|
||||
check(checks, `knowledge.link.${relative(contentRoot, file)}.${target}`, exists, exists ? `Wikilink resolves: ${target}` : `Broken wikilink: ${target}`, relative(state.projectRoot, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateContinuity(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
const paths = ["02_story_bible/13_canon_ledger.md", "06_ledgers/continuity_ledger.md", "06_ledgers/timeline.md"];
|
||||
for (const path of paths) {
|
||||
const exists = await pathExists(join(state.projectRoot, path));
|
||||
if (state.currentStage >= 5 || state.completedStages.includes(4)) check(checks, `continuity.${path}`, exists, exists ? `${path} exists` : `${path} is missing`, path);
|
||||
else if (!exists) warn(checks, `continuity.${path}`, `${path} not required before story-bible completion`, path);
|
||||
}
|
||||
const ledger = join(state.projectRoot, "06_ledgers", "continuity_ledger.md");
|
||||
if (await pathExists(ledger)) {
|
||||
const text = await readFile(ledger, "utf8");
|
||||
const openIssues = [...text.matchAll(/^- \[ \]/gm)].length;
|
||||
check(checks, "continuity.open-issues", openIssues === 0, openIssues ? `${openIssues} open continuity issue(s)` : "No open continuity issues", "06_ledgers/continuity_ledger.md");
|
||||
}
|
||||
}
|
||||
|
||||
async function validateDelivery(state: PomState, checks: ValidationCheck[]): Promise<void> {
|
||||
const incomplete = STAGES.filter((item) => item.id < 8 && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
|
||||
check(checks, "delivery.prerequisites", incomplete.length === 0, incomplete.length ? `Incomplete stages: ${incomplete.join(", ")}` : "Stages 0–7 complete");
|
||||
for (const artifact of stage(8).artifacts) await validateArtifactRequirement(state, checks, artifact);
|
||||
const exportRoot = join(state.projectRoot, "07_exports");
|
||||
let verificationFiles: string[] = [];
|
||||
if (await pathExists(exportRoot)) verificationFiles = (await readdir(exportRoot)).filter((name) => name.endsWith(".verification.json"));
|
||||
check(checks, "delivery.archive-verification", verificationFiles.length > 0, verificationFiles.length ? `Archive verification: ${verificationFiles.at(-1)}` : "No archive verification sidecar found", verificationFiles.at(-1) ? `07_exports/${verificationFiles.at(-1)}` : undefined);
|
||||
}
|
||||
|
||||
function scopesFor(scope: ValidationScope): Set<string> {
|
||||
if (scope === "quick") return new Set(["state"]);
|
||||
if (scope === "stage") return new Set(["state", "stage"]);
|
||||
if (scope === "canon") return new Set(["state", "stage", "continuity"]);
|
||||
if (scope === "continuity") return new Set(["state", "continuity"]);
|
||||
if (scope === "knowledge") return new Set(["state", "knowledge"]);
|
||||
if (scope === "files") return new Set(["state", "manifest"]);
|
||||
if (scope === "delivery") return new Set(["state", "manifest", "knowledge", "continuity", "delivery"]);
|
||||
return new Set(["state", "stage", "manifest", "knowledge", "continuity", "delivery"]);
|
||||
}
|
||||
|
||||
export async function validatePom(state: PomState, scope: ValidationScope, options: { writeReport?: boolean } = {}): Promise<ValidationReport> {
|
||||
const checks: ValidationCheck[] = [];
|
||||
const selected = scopesFor(scope);
|
||||
if (selected.has("state")) await validateState(state, checks);
|
||||
if (selected.has("stage")) await validateStage(state, checks);
|
||||
if (selected.has("manifest")) await validateManifest(state, checks);
|
||||
if (selected.has("knowledge")) await validateKnowledge(state, checks);
|
||||
if (selected.has("continuity")) await validateContinuity(state, checks);
|
||||
if (selected.has("delivery")) await validateDelivery(state, checks);
|
||||
const report: ValidationReport = {
|
||||
schemaVersion: "1.0.0",
|
||||
projectId: state.projectId,
|
||||
scope,
|
||||
stage: state.currentStage,
|
||||
generatedAt: new Date().toISOString(),
|
||||
passed: checks.every((item) => item.status !== "fail"),
|
||||
checks,
|
||||
};
|
||||
if (options.writeReport !== false && state.initialized) {
|
||||
const stamp = report.generatedAt.replace(/[:.]/g, "-");
|
||||
const relativePath = `00_admin/validation/stage-${state.currentStage}-${scope}-${stamp}.json`;
|
||||
await atomicWrite(join(state.projectRoot, relativePath), `${JSON.stringify(report, null, 2)}\n`);
|
||||
report.reportPath = relativePath;
|
||||
}
|
||||
return report;
|
||||
}
|
||||
193
src/visual.ts
Normal file
193
src/visual.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Text, truncateToWidth, visibleWidth, type Component, type KeybindingsManager } from "@oh-my-pi/pi-tui";
|
||||
import type { PomMotion, PomState, PomView } from "./domain";
|
||||
import { openBlockers, stageProgress } from "./state";
|
||||
import { stage } from "./domain";
|
||||
import type { PomRuntime } from "./runtime";
|
||||
|
||||
export type PomColor = "accent" | "muted" | "dim" | "success" | "warning" | "error" | "toolTitle" | "text";
|
||||
export interface PomTheme {
|
||||
fg(color: PomColor, text: string): string;
|
||||
bold(text: string): string;
|
||||
italic(text: string): string;
|
||||
}
|
||||
|
||||
function safeLine(line: string, width: number): string {
|
||||
return truncateToWidth(line.replaceAll("\t", " "), Math.max(1, width));
|
||||
}
|
||||
|
||||
function bar(percent: number, width: number): string {
|
||||
const size = Math.max(5, width);
|
||||
const filled = Math.round((Math.max(0, Math.min(100, percent)) / 100) * size);
|
||||
return `${"━".repeat(filled)}${"─".repeat(size - filled)}`;
|
||||
}
|
||||
|
||||
function stageGlyph(status: string, active: boolean): string {
|
||||
if (active) return "◆";
|
||||
if (status === "complete") return "●";
|
||||
if (status === "blocked") return "×";
|
||||
if (status === "active") return "◉";
|
||||
return "○";
|
||||
}
|
||||
|
||||
function activityGlyph(state: PomState, runtime: PomRuntime): string {
|
||||
if (!runtime.telemetry.activeTool) return state.stageRuns[String(state.currentStage)].status === "blocked" ? "×" : "✓";
|
||||
if (state.motion === "off") return "◆";
|
||||
return state.motion === "full" ? "◌" : "◐";
|
||||
}
|
||||
|
||||
function hiveSummary(state: PomState, runtime: PomRuntime): string {
|
||||
const latest = Object.values(state.hive.missions).at(-1);
|
||||
const total = runtime.telemetry.activeAgents || latest?.tasks.length || 0;
|
||||
const active = state.hive.activeMissionId ? `${runtime.telemetry.completedAgents}/${total}` : total ? `${runtime.telemetry.completedAgents}/${total}` : "idle";
|
||||
return `Hive ${active}`;
|
||||
}
|
||||
|
||||
export function hudLines(state: PomState, runtime: PomRuntime, theme: PomTheme, width: number): string[] {
|
||||
const spec = stage(state.currentStage);
|
||||
const run = state.stageRuns[String(state.currentStage)];
|
||||
const progress = stageProgress(state);
|
||||
const blockers = openBlockers(state).length;
|
||||
const evidence = `${run.evidence.length}/${spec.acceptance.length}`;
|
||||
const stageTrack = Object.entries(state.stageRuns).map(([id, item]) => stageGlyph(item.status, Number(id) === state.currentStage)).join(" ");
|
||||
const tool = runtime.telemetry.activeTool
|
||||
? `${activityGlyph(state, runtime)} ${runtime.telemetry.activeTool}${runtime.telemetry.activeIntent ? ` · ${runtime.telemetry.activeIntent}` : ""}`
|
||||
: `${activityGlyph(state, runtime)} ${run.status}`;
|
||||
const signal = runtime.telemetry.lastIrcSignal ? ` · IRC ${runtime.telemetry.lastIrcSignal}` : "";
|
||||
|
||||
if (state.view === "compact") {
|
||||
return [safeLine(
|
||||
`${theme.fg("accent", theme.bold("POM"))} ${spec.icon} ${spec.id}/8 ${spec.slug} · ${progress.percent}% · ◆ ${evidence} · ${blockers ? `⚠ ${blockers}` : tool} · ${hiveSummary(state, runtime)}`,
|
||||
width,
|
||||
)];
|
||||
}
|
||||
|
||||
const left = `${theme.fg("accent", theme.bold("POM"))} ${theme.fg("muted", `${spec.icon} ${spec.id}/8 ${spec.name}`)}`;
|
||||
const right = `${progress.percent}% · ${evidence} evidence · ${blockers} blockers`;
|
||||
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
||||
const first = safeLine(`${left}${" ".repeat(gap)}${theme.fg(blockers ? "warning" : "dim", right)}`, width);
|
||||
const activity = safeLine(`${theme.fg(runtime.telemetry.activeTool ? "warning" : "muted", tool)} · ${theme.fg("dim", hiveSummary(state, runtime))}${state.view === "cinematic" ? theme.fg("dim", signal) : ""}`, width);
|
||||
|
||||
if (state.view === "rich") {
|
||||
return [first, safeLine(`${theme.fg("accent", bar(progress.percent, Math.max(10, Math.floor(width * 0.45))))} ${theme.fg("dim", stageTrack)} ${activity}`, width)];
|
||||
}
|
||||
|
||||
return [
|
||||
first,
|
||||
safeLine(theme.fg("accent", bar(progress.percent, Math.max(10, width - 2))), width),
|
||||
safeLine(`${theme.fg("dim", stageTrack)} ${activity}`, width),
|
||||
];
|
||||
}
|
||||
|
||||
export class PomHudComponent implements Component {
|
||||
#cacheKey = "";
|
||||
#cache: readonly string[] = [];
|
||||
constructor(private readonly runtime: PomRuntime, private readonly theme: PomTheme) {}
|
||||
render(width: number): readonly string[] {
|
||||
const state = this.runtime.get();
|
||||
const key = JSON.stringify({
|
||||
width,
|
||||
stage: state.currentStage,
|
||||
view: state.view,
|
||||
motion: state.motion,
|
||||
runs: Object.values(state.stageRuns).map((run) => [run.status, run.evidence.length, run.blockers.filter((item) => !item.resolvedAt).length]),
|
||||
telemetry: this.runtime.telemetry,
|
||||
});
|
||||
if (key === this.#cacheKey) return this.#cache;
|
||||
this.#cacheKey = key;
|
||||
this.#cache = hudLines(state, this.runtime, this.theme, width);
|
||||
return this.#cache;
|
||||
}
|
||||
invalidate(): void { this.#cacheKey = ""; }
|
||||
}
|
||||
|
||||
export type CommandCenterAction = "run" | "hive" | "validate" | "evidence" | "vault" | "export" | "theme" | "settings" | "close";
|
||||
|
||||
const ACTIONS: Array<{ id: CommandCenterAction; icon: string; label: string; detail: string }> = [
|
||||
{ id: "run", icon: "▶", label: "Run active stage", detail: "Route model/tools and launch the authoritative stage loop" },
|
||||
{ id: "hive", icon: "⬡", label: "Launch specialist hive", detail: "Prepare one native task batch with IRC ownership protocol" },
|
||||
{ id: "validate", icon: "✓", label: "Validate production", detail: "Run deterministic gates and render an evidence card" },
|
||||
{ id: "evidence", icon: "◆", label: "Record gate evidence", detail: "Attach a criterion to real artifact paths" },
|
||||
{ id: "vault", icon: "◫", label: "Open vault dashboard", detail: "Insert the generated production board into the editor" },
|
||||
{ id: "export", icon: "⇧", label: "Build verified archive", detail: "Create, decompress, CRC-check, and hash the delivery ZIP" },
|
||||
{ id: "theme", icon: "◈", label: "Install POM themes", detail: "Copy Nocturne and Parchment into OMP custom themes" },
|
||||
{ id: "settings", icon: "⚙", label: "Experience settings", detail: "Tune density, motion, routing, and HUD placement" },
|
||||
{ id: "close", icon: "×", label: "Close", detail: "Return to the editor" },
|
||||
];
|
||||
|
||||
export class PomCommandCenter implements Component {
|
||||
#index = 0;
|
||||
#cacheKey = "";
|
||||
#cache: readonly string[] = [];
|
||||
constructor(
|
||||
private readonly state: PomState,
|
||||
private readonly theme: PomTheme,
|
||||
private readonly keybindings: KeybindingsManager,
|
||||
private readonly done: (action: CommandCenterAction) => void,
|
||||
) {}
|
||||
handleInput(data: string): void {
|
||||
if (this.keybindings.matches(data, "app.interrupt")) { this.done("close"); return; }
|
||||
if (data === "k" || data === "\u001b[A") this.#index = (this.#index - 1 + ACTIONS.length) % ACTIONS.length;
|
||||
else if (data === "j" || data === "\u001b[B") this.#index = (this.#index + 1) % ACTIONS.length;
|
||||
else if (data === "\r" || data === "\n") { this.done(ACTIONS[this.#index].id); return; }
|
||||
else if (data === "q" || data === "\u001b") { this.done("close"); return; }
|
||||
this.invalidate();
|
||||
}
|
||||
render(width: number): readonly string[] {
|
||||
const run = this.state.stageRuns[String(this.state.currentStage)];
|
||||
const spec = stage(this.state.currentStage);
|
||||
const progress = stageProgress(this.state);
|
||||
const key = `${width}:${this.#index}:${this.state.updatedAt}`;
|
||||
if (key === this.#cacheKey) return this.#cache;
|
||||
const inner = Math.max(30, width - 4);
|
||||
const top = `╭${"─".repeat(Math.max(1, inner))}╮`;
|
||||
const bottom = `╰${"─".repeat(Math.max(1, inner))}╯`;
|
||||
const line = (content = "") => {
|
||||
const clipped = safeLine(content, inner - 2);
|
||||
return `│ ${clipped}${" ".repeat(Math.max(0, inner - 2 - visibleWidth(clipped)))} │`;
|
||||
};
|
||||
const lines = [
|
||||
top,
|
||||
line(`${this.theme.fg("accent", this.theme.bold("POM COMMAND CENTER"))} ${this.theme.fg("dim", this.state.projectTitle)}`),
|
||||
line(`${spec.icon} Stage ${spec.id}/8 · ${spec.name} · ${run.status.toUpperCase()}`),
|
||||
line(`${this.theme.fg("accent", bar(progress.percent, Math.max(10, inner - 16)))} ${progress.percent}%`),
|
||||
line(),
|
||||
];
|
||||
ACTIONS.forEach((action, index) => {
|
||||
const selected = index === this.#index;
|
||||
const cursor = selected ? this.theme.fg("accent", "❯") : " ";
|
||||
const label = selected ? this.theme.bold(action.label) : action.label;
|
||||
lines.push(line(`${cursor} ${action.icon} ${label}`));
|
||||
if (selected && inner >= 60) lines.push(line(` ${this.theme.fg("dim", action.detail)}`));
|
||||
});
|
||||
lines.push(line(), line(this.theme.fg("dim", "↑/k ↓/j navigate · Enter select · q/Esc close")), bottom);
|
||||
this.#cacheKey = key;
|
||||
this.#cache = lines.map((item) => safeLine(item, width));
|
||||
return this.#cache;
|
||||
}
|
||||
invalidate(): void { this.#cacheKey = ""; }
|
||||
}
|
||||
|
||||
function spinnerFor(motion: PomMotion, frame = 0): string {
|
||||
if (motion === "off") return "◆";
|
||||
const frames = motion === "subtle" ? ["◐", "◑"] : ["◐", "◓", "◑", "◒"];
|
||||
return frames[frame % frames.length];
|
||||
}
|
||||
|
||||
export function toolCallComponent(label: string, action: string | undefined, theme: PomTheme, motion: PomMotion, spinnerFrame?: number): Component {
|
||||
return new Text(`${theme.fg("accent", spinnerFor(motion, spinnerFrame))} ${theme.fg("toolTitle", theme.bold(`POM ${label}`))}${action ? ` ${theme.fg("muted", action)}` : ""}`, 0, 0);
|
||||
}
|
||||
|
||||
export function toolResultComponent(
|
||||
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
||||
expanded: boolean,
|
||||
partial: boolean,
|
||||
theme: PomTheme,
|
||||
view: PomView,
|
||||
): Component {
|
||||
if (partial) return new Text(`${theme.fg("warning", view === "minimal" ? "◌" : "◐")} ${theme.fg("muted", "Working…")}`, 0, 0);
|
||||
const text = result.content.find((item) => item.type === "text")?.text ?? (result.isError ? "POM operation failed" : "Done");
|
||||
const mark = result.isError ? theme.fg("error", "×") : theme.fg("success", "✓");
|
||||
const allowDetails = expanded && result.details && view !== "minimal" && view !== "compact";
|
||||
const detail = allowDetails ? `\n${theme.fg("dim", JSON.stringify(result.details, null, view === "cinematic" ? 2 : 0))}` : "";
|
||||
return new Text(`${mark} ${text}${detail}`, 0, 0);
|
||||
}
|
||||
109
src/zip.ts
Normal file
109
src/zip.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createReadStream, createWriteStream } from "node:fs";
|
||||
import { mkdir, stat } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { finished } from "node:stream/promises";
|
||||
import CRC32 from "crc-32";
|
||||
import yauzl, { type Entry, type ZipFile } from "yauzl";
|
||||
import yazl from "yazl";
|
||||
import { assertSafeArchivePath } from "./paths";
|
||||
import { sha256File } from "./persistence";
|
||||
|
||||
export interface ZipMemberVerification {
|
||||
path: string;
|
||||
compressedBytes: number;
|
||||
bytes: number;
|
||||
crc32: string;
|
||||
method: number;
|
||||
}
|
||||
|
||||
export interface ZipVerification {
|
||||
path: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
members: ZipMemberVerification[];
|
||||
}
|
||||
|
||||
export async function createZip(outputPath: string, members: Array<{ absolutePath: string; archivePath: string }>): Promise<void> {
|
||||
if (!members.length) throw new Error("Refusing to create an empty archive");
|
||||
const seen = new Set<string>();
|
||||
const zip = new yazl.ZipFile();
|
||||
for (const member of members) {
|
||||
assertSafeArchivePath(member.archivePath);
|
||||
if (seen.has(member.archivePath)) throw new Error(`Duplicate archive member: ${member.archivePath}`);
|
||||
seen.add(member.archivePath);
|
||||
zip.addFile(member.absolutePath, member.archivePath, { compress: true });
|
||||
}
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
const destination = createWriteStream(outputPath, { flags: "w" });
|
||||
zip.outputStream.pipe(destination);
|
||||
zip.end();
|
||||
await finished(destination);
|
||||
}
|
||||
|
||||
function openZip(path: string): Promise<ZipFile> {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
yauzl.open(path, { lazyEntries: true, decodeStrings: true, validateEntrySizes: true, strictFileNames: true }, (error, zip) => {
|
||||
if (error || !zip) reject(error ?? new Error("Unable to open ZIP"));
|
||||
else resolvePromise(zip);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readEntry(zip: ZipFile, entry: Entry): Promise<ZipMemberVerification> {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
zip.openReadStream(entry, (error, stream) => {
|
||||
if (error || !stream) { reject(error ?? new Error(`Unable to read ${entry.fileName}`)); return; }
|
||||
let bytes = 0;
|
||||
let crc = 0;
|
||||
stream.on("data", (chunk: Buffer) => {
|
||||
bytes += chunk.length;
|
||||
crc = CRC32.buf(chunk, crc);
|
||||
});
|
||||
stream.once("error", reject);
|
||||
stream.once("end", () => {
|
||||
const unsigned = crc >>> 0;
|
||||
const expected = entry.crc32 >>> 0;
|
||||
if (bytes !== entry.uncompressedSize) { reject(new Error(`Size mismatch for ${entry.fileName}`)); return; }
|
||||
if (unsigned !== expected) { reject(new Error(`CRC mismatch for ${entry.fileName}`)); return; }
|
||||
resolvePromise({
|
||||
path: entry.fileName,
|
||||
compressedBytes: entry.compressedSize,
|
||||
bytes,
|
||||
crc32: unsigned.toString(16).padStart(8, "0"),
|
||||
method: entry.compressionMethod,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyZip(path: string): Promise<ZipVerification> {
|
||||
const zip = await openZip(path);
|
||||
const members: ZipMemberVerification[] = [];
|
||||
const seen = new Set<string>();
|
||||
await new Promise<void>((resolvePromise, reject) => {
|
||||
zip.once("error", reject);
|
||||
zip.once("end", resolvePromise);
|
||||
zip.on("entry", (entry: Entry) => {
|
||||
void (async () => {
|
||||
try {
|
||||
assertSafeArchivePath(entry.fileName.replace(/\/$/, ""));
|
||||
if (/\/$/.test(entry.fileName)) { zip.readEntry(); return; }
|
||||
if (seen.has(entry.fileName)) throw new Error(`Duplicate ZIP member: ${entry.fileName}`);
|
||||
seen.add(entry.fileName);
|
||||
if ((entry.generalPurposeBitFlag & 0x1) !== 0) throw new Error(`Encrypted ZIP member is not allowed: ${entry.fileName}`);
|
||||
if (![0, 8].includes(entry.compressionMethod)) throw new Error(`Unsupported compression method ${entry.compressionMethod}: ${entry.fileName}`);
|
||||
members.push(await readEntry(zip, entry));
|
||||
zip.readEntry();
|
||||
} catch (error) {
|
||||
zip.close();
|
||||
reject(error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
zip.readEntry();
|
||||
});
|
||||
if (!members.length) throw new Error("ZIP contains no file members");
|
||||
const fileStat = await stat(path);
|
||||
return { path, bytes: fileStat.size, sha256: await sha256File(path), members };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user