# Ocas Forge

> Skill architect and builder. Designs, builds, and validates complete Agent Skill packages through a mandatory eight-phase pipeline: existence gate, research, classify, scope, architecture, plan, build, validate. Default output is the finished installable package. Not for skill evaluation (use skilllab's Critique procedure) or variant proposals (use ocas-mentor).

- Skill: `indigokarasu/ocas-forge` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add indigokarasu/ocas-forge`
- Raw SKILL.md: https://api.skillmd.com/api/skills/indigokarasu/ocas-forge/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: indigokarasu (https://skillmd.com/u/indigokarasu)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/indigokarasu/ocas-forge

---


Forge is the system's skill architect — given a capability idea or broken existing package, it runs a mandatory eight-phase internal pipeline covering existence gate, research, classification, scoping, architecture, planning, construction, and validation before writing a single file. The default output is the finished, installable package with all file contents written; Forge never returns design briefs or plans in place of the real artifact.

## When to Use

- Building new OCAS skills from scratch
- Skill architecture and design review
- Bulk skill library updates and synchronization
- Skill consolidation and deprecation
- When a new capability needs a permanent skill home
- Create a new Agent Skill from a goal or capability description
- Review or critique an existing skill package
- Repair broken or defective skill packages
- Validate a skill package against OCAS standards

## When NOT to Use

- One-off task execution (use the appropriate existing skill)
- Skill evaluation/scoring (use Mentor or skilllab)
- Content generation or research
- System health monitoring (use Custodian)
- Authentication and service wiring (use ocas-auth)
- Building non-skill artifacts
- Web research — use Sift
- **Skill structure reference / frontmatter template lookup** — use `write-a-skill` for quick
  reference on field requirements, file layout, and description conventions. Forge is for building.

## Responsibility boundary

Forge owns skill design, construction, consolidation, update verification,
compliance auditing, and repo-sync. Forge's `forge.validate` handles quick
structural checks; deep quality scoring and iterative improvement is now
owned by `skilllab` (Critique procedure, merged from ocas-critique).

Forge does not own: skill quality scoring and iteration (skilllab), skill
evaluation or variant testing (Mentor), behavioral pattern analysis,
behavioral refinement (Praxis), experimentation (Fellow), system health and
skill initialization (Custodian), runtime orchestration and delegation (the
agent harness), authentication and MCP wiring (ocas-auth).

**Note on `review-skill` vs skilllab's Critique:** The 3rd-party `review-skill`
(agentskill-sh) provides lightweight quick-check scoring. skilllab's Critique
procedure is the full OCAS quality engine with 6-phase pipeline, batch mode,
iteration loops, and autonomous improvement. Use skilllab for all OCAS skill
scoring. Use `review-skill` only for quick structural checks on non-OCAS
skills. Never
push `review-skill` to GitHub — it's 3rd-party.

Forge receives VariantProposal and VariantDecision files from Mentor. It builds variant packages and applies promotion decisions.

## Ontology types

- **Concept/Event** — projects, tasks, skill performance evaluations, OKR cycles
- **Concept/Idea** — improvement proposals, behavioral patterns
- **Thing/DigitalArtifact** — project state records, task graphs, evaluation reports

Mentor does not emit entity signals directly. Journal outputs are ingested by Chronicle for knowledge persistence.

## Commands

- `forge.build` — design, scope, build, and validate a complete skill package
- `forge.critique` — review a package and identify defects
- `forge.repair` — fix broken files in an existing package
- `forge.classify` — classify a proposed skill (shortcut, workflow, system)
- `forge.validate` — run validation checks on a package
- `forge.scaffold` — generate a minimal package skeleton
- `forge.consolidate` — merge an orphan or duplicate skill into its natural parent
- `forge.verify-update` — check whether a skill is at the latest version from its GitHub source
- `forge.sync` — sync local skill changes to the canonical repository via PR
- `forge.audit` — audit one or more skills for OCAS compliance, apply fixes, and sync to GitHub
- `forge.status` — current build state if multi-step build in progress
- `forge.journal` — write journal for the current run; called at end of every run
- `forge.update` — pull latest from GitHub source; preserves journals and data

## Mandatory design pipeline

Run all phases before writing files. Full phase detail including existence gates
(parent search, standalone test, absorption test), **research (skill library → GitHub search → deep-read → compare)**,
classification, scoping, architecture, plan, build, and validation procedures: see
`references/design_pipeline.md`.

Key rule: **absorption first.** If an existing skill already owns the domain,
add content to it as a `references/` doc or `scripts/` file — do not create a
new skill. See `references/enforcement_durability.md` for the absorption
decision framework.

**Research rule:** After the existence gate passes, you MUST research before
classifying. Search GitHub repos (via `gh search repos`) AND search the skill
library (via the APIs below) to understand what already exists. The goal is
NOT to copy existing skills but to **understand how they work** and **synthesize**
that knowledge into a new, better skill. Review at least 10 repos or skills
before deciding to build.

## Naming and Authorship Rules

See `references/naming-and-authorship.md` for the full naming convention and authorship tagging rules. Key points:

- **Never create `ocas-*` or rename to `ocas-*`/`util-*` without explicit user authorization.**
- **Auto-generated skills** must use `author: autogenerated` in metadata.
- **Auto-generated thin wrappers** are candidates for deletion — see references for criteria.

## Skill type classification

- **Shortcut** — narrow tool wrapper. 20-120 line SKILL.md.
- **Workflow** — multi-step process. 80-250 line SKILL.md.
- **System** — durable behavior system. 150-300 line SKILL.md, deeper material in references.

## Package rules

Minimum package: SKILL.md with agentskills.io frontmatter. Add references/, scripts/, assets/ only when justified. Read `references/enforcement_durability.md` for full guidance on how to make rules durable across updates. See also `references/package_patterns.md` for package shape guidance and `references/authoring_rules.md` for full authoring standards.

**Run completion**

After every Forge command (build, critique, repair, validate, audit):

1. Check `{agent_root}/commons/data/ocas-forge/` for unprocessed VariantProposal and VariantDecision JSON files. Cross-reference proposal IDs against `intake/processed/` and `processed/` directories to skip already-processed files. Process any new files — build variant packages, apply fixes, or queue for Mentor evaluation as appropriate. After processing, move files to `processed/`.
2. Check journal payload fields (see interfaces specification) for VariantProposal and VariantDecision files from Mentor received via journal; process and move to the consumer's ingestion log.
3. Persist build log entries and decisions to local JSONL files.
4. Log material decisions to `decisions.jsonl`.
5. Write journal via `forge.journal`.
4. **Validation gate:** do NOT report success to the user after a build/create/repair until validation has actually executed and its result is in hand. `write_file` or `skill_manage(action='create')` completing is not validation. Run the applicable checker (`forge.validate`, `scripts/forge_audit_skills.py`, or the skilllab critique runner), summarize findings, and only then declare the package complete. If validation cannot run, say so explicitly instead of implying completion.
5. Log material decisions to `decisions.jsonl`.
6. Write journal via `forge.journal`.
**When to apply fixes directly vs. build variants:** If a proposal has strong evidence (≥3 consecutive proposals for the same issue, ≥50 runs analyzed, ≥7 days of consistent data, and the fix is low-risk), Forge may apply the fix directly without a full A/B evaluation cycle. Otherwise, queue for Fellow evaluation. Document the rationale in the action journal.

## Cross-platform portability

Skills that hardcode `~/.hermes/` paths will NOT work on other agent harnesses (OpenClaw, Claude Code, Cursor, etc.). When building a new skill:

- **Use `{agent_root}`** as the base for all paths inside the skill's storage layout diagrams. This variable resolves to whatever harness the skill runs on.
- **NEVER hardcode `~/.hermes/`** in file paths, storage diagrams, or operational descriptions. Even for Hermes-native skills, use `{agent_root}/sessions/`, `{agent_root}/skills/`, `{agent_root}/references/` instead.
- **Mention the target harness** in the frontmatter with a `requires:` field if the skill depends on Hermes-specific tools (`memory`, `skill_manage`, `session_search`, `cronjob`). Example: `requires: hermes`. This tells other harnesses to skip the skill.
- **Document Hermes-specific tool dependencies** in a "Required tools" section so future porters know what to adapt.

## Anti-patterns to reject

- **Skipping research on skill improvement.** Phase 1.5 (Research) is mandatory for ALL forge operations — not just new builds. When asked to "improve" or "update" an existing skill, you MUST still research external sources (GitHub, arxiv, community patterns) to find new patterns, techniques, and taxonomies that could improve the skill. The user correction "did you do the research phase?" is a signal that you skipped Phase 1.5. Research is not optional just because the skill already exists — the whole point of improvement is to find what you don't already know.
- Vague or overly broad scope
- Generic descriptions that don't route well
- SKILL.md bloated with background explanation
- Support folders created for aesthetics
- Plans returned instead of packages
- Template residue and placeholders
- Storage inside skill package directories
- Undocumented inter-skill interfaces
- **`## Integrated:` wrapper sections:** when folding content into a parent skill, do NOT wrap it in `## Integrated:` sections. Refactor the content into the parent's existing section structure instead.
- **Advisory-only enforcement doesn't work:** writing "use Forge instead of skill_manage" in MEMORY.md is advisory and easily skipped. The hard gates must be in the Forge SKILL.md itself (phase 1 checks A/B/C), because that's the artifact that gets loaded and followed. Never rely on memory notes as the sole enforcement mechanism for behavioral rules.

## Gotchas

- **Fixed argparse nargs error in run_dispatch_pipeline.py** — Changed `nargs='[]'` to `nargs='*'` for the --new-files argument to accept zero or more arguments. This fixed a ValueError that occurred when the script was called during dispatch processing.
- **Stale files in `processed/` vs `intake/processed/`** — check both locations during journal-scan
- **`proposals/` AND top-level `processed/` are SOURCE MIRRORS, not pending work (confirmed 2026-07-13, RE-CONFIRMED 2026-07-16):** When scanning for unprocessed variant proposals (`vp_*.json` / `vd_*.json`), count ONLY files in `commons/data/ocas-forge/intake/` that are NOT already in `intake/processed/`. Do NOT count files under `commons/data/ocas-forge/proposals/` (a source mirror already copied into `intake/processed/`) NOR the top-level `commons/data/ocas-forge/processed/` dir (another mirror). A naive recursive `find` / `os.walk` over the WHOLE `ocas-forge` tree sweeps up BOTH mirrors and overcounts, falsely flipping a `routine_no_op` dispatch into a `genuine` variant-build dispatch — this bit a 2026-07-16 closure orchestrator that recursive-walked the whole tree, found 11 copies in `proposals/` + 11 in top-level `processed/`, and wrote `unprocessed_proposals: 11` / `action: genuine` when the true value was 0. **USE `scripts/forge_count_unprocessed.py`** (bounded walk of `intake/` only) instead of any hand-rolled recursive count. After counting, cross-check against the prior `forge-scan-*.json` journal's `unprocessed_proposals` field; if the prior scan said 0 and no new variant work arrived, the real count is 0. If you already wrote a false-positive journal, patch it to `unprocessed_proposals: 0` / `action: routine_no_op` BEFORE bridging.
- **Missing `includes:` in frontmatter** — required when references/ or scripts/ dir exists
- **Scope boundary for sync** — `forge.sync`/`forge.audit` only on `ocas-*` skills
- **Doing more than asked** — match work to the scope of the request
- **Incorrect Naming** — NEVER create/rename `ocas-*` without user authorization
- **Non-durable fixes** — put rules in skill's own git repo or MEMORY.md, not hermes core
- **Runaway repo creation** — check for 3rd-party skills before `gh repo create`
- **YAML block scalar truncation** — `description: >` / `|` contain newlines; use `read_file` + `patch`
- **`action` field is polymorphic in forge journals** — guard with `isinstance` before every access
- **Dispatcher `new_files` paths lack prefix — AND the dispatch-wave journal resolves to the WRONG tree (confirmed 2026-07-16)** — The dispatcher's `details.new_files` prints `ocas-dispatch/2026-07-16/dispatch-wave-20260716T195230Z.json`. Treated as profile-relative, that resolves to `commons/data/ocas-dispatch/<DATE>/` — but that directory holds only LEGACY mirror dispatch-wave JSONs (2026-06 era). The LIVE on-disk journal is at `commons/journals/ocas-dispatch/2026-07-16/dispatch-wave-20260716T195230Z.json`. A `read_file` on the dispatcher-printed path returns "File not found." Route all dispatch-wave journal reads to the `commons/journals/` tree (the same tree `verify_genuine_gap_profile.py` / `closure_convergence_sweep.py` walk). Use `find <hermes-home> -name 'dispatch-wave-*.json'` to locate the real path.
- **`verify_eval_no_phantoms.py` reports PRE-EXISTING historical phantoms — NEVER run destructive `--fix` during a closure pass (confirmed 2026-07-16)** — The script walks the ENTIRE eval store (not date-scoped) and reports 1,800+ phantoms: historical 2026-06 → 2026-07-14 entries, malformed arg-flag leaks (`--help`, `--apply`, `--path`), and mis-namespaced relpaths (e.g. `ocas-dispatch/dispatch-wave-20260716T012901Z.json` missing its date dir). NONE belong to the current wave. Running `--fix` would mass-delete historical eval lines — OUT OF SCOPE and RISKY during a dispatch closure. Correct scope check for a closure: (a) verify every entry THIS run bridged was `os.path.exists()`-confirmed BEFORE bridging, and (b) assert `GENUINE GAP = 0` via `verify_genuine_gap_profile.py --date <DATE>`. Leave historical phantom cleanup to a dedicated audit, not a closure pass.
- **Appending to JSONL files** — ALWAYS use `echo >>`, NEVER heredoc with `>`
- **`write_file` escapes quotes in Python files** — use `terminal()` with heredoc for .py files
- **Programmatic SKILL.md surgery can delete the frontmatter `---` fence (confirmed 2026-07-16 grind):** When you use a Python slice to delete/replace a block of `SKILL.md` — e.g. extracting inline operational detail into `references/` to cut the line count — a stray `del lines[a:b]` or a `header + pointer + rest` reassembly that accidentally includes the frontmatter's closing `---` will silently remove the YAML delimiter. The next parse treats the body as frontmatter (or raises `yaml.ScannerError`), and the 10khr scorer drops **D1 → 1** (name/description unparseable) and **D2 → 3**. This is invisible until you re-score. **Fix:** (1) After ANY programmatic SKILL.md edit, immediately re-run `scripts/critique_10khr_runner.py --report-only` (or call `score_skill()`) and watch D1/D2 — a sudden drop from 5 to ≤3 means the fence is gone. (2) Guard the slice to stop before the `---` line; if eaten, re-insert `---` at the frontmatter/body boundary. (3) Verify before declaring done: `yaml.safe_load(content.split('---')[1])` must succeed and return `name`/`description`. The same guard applies to `forge.build`/`forge.repair` edits done via `terminal()` heredoc slices.
- **Eval-store manual grep must use the BARE relpath — NOT `commons/journals/`-prefixed** — When you manually `grep` the praxis eval store (`commons/data/ocas-praxis/journals_evaluated.jsonl`, key `journal_id`) to check whether a journal was already evaluated, grep with the bare relpath `ocas-mentor/2026-07-16/mentor-light-XXXX.json`, NOT the prefix-inclusive `commons/journals/ocas-mentor/2026-07-16/...` path. The store keys by the bare relpath (matching what `bridge_eval_inline.py` / `closure_convergence_sweep.py` write). Grepping with the `commons/journals/` prefix returns ABSENT even for journals that ARE present, which mis-diagnoses a clean re-detection as a genuine gap and can trigger spurious re-bridging. Confirmed 2026-07-16 residual-gap closure: a prefix-inclusive grep reported ABSENT; the bare-relpath grep (and the closure scripts) confirmed the journal was already in both stores. Always use `grep -c 'ocas-mentor/2026-07-16/<file>.json'` (no leading dir) for manual verification; the closure scripts (`verify_genuine_gap_profile.py`, `bridge_eval_inline.py`) parse JSON and never hallucinate key-format gaps, so prefer them over raw grep.
- **Placeholder-then-patch anti-pattern** — never write placeholder strings intending to fix later. Confirmed variant: scripts with `<hermes-home>` path placeholders (`closure_closeout_check.py`, `verify_genuine_gap_profile.py`, `closure_convergence_sweep.py`, `bridge_eval_inline.py`) crash at runtime with `FileNotFoundError` because they contain the literal `<hermes-home>` string instead of resolving the actual profile path via `os.path.expanduser('~')` or a config lookup. When these scripts fail, do NOT retry — manually verify gates and bridge instead: (1) Gate [1]: grep bare relpath in both praxis and dispatch eval stores. (2) Gate [2]: recompute max journal mtime programmatically, advance both monitor copies + praxis `ingest_state.json`. (3) Gate [3]: re-assert `verified_second_wave` on all 4 state files. (4) Manual bridge: append JSONL entries directly via Python one-liner. See `references/closure-script-path-placeholder-bug.md` for the full per-gate manual procedure and the expanded list of affected scripts.
- **`run_mixed_wave_closure.py` crashes at module load with `NameError: name 'os' is not defined` (confirmed 2026-07-27):** The script uses `os.environ` on line 2 before `import os` is declared. This is a real source bug — retrying will fail identically. When the script crashes, handle the closure manually: verify each dispatched journal in both eval stores, bridge missing ones via manual JSONL append, skip `bridge_eval_inline.py` (also has `<hermes-home>` placeholder bug — see `references/closure-script-path-placeholder-bug.md`), and run `verify_genuine_gap_profile.py --date <DATE>` + `closure_closeout_check.py` manually. See `references/run-mixed-wave-closure-crash-os-import.md` for full manual bridge recipe. When a mixed wave carries genuine email Path B gaps (threads `NOT_IN_EVIDENCE` in `commons/data/ocas-dispatch/evidence.jsonl`), close them SEPARATELY BEFORE running the runner: append a structured `dispatch-evidence-v2` record with per-thread `triage_decisions[].action` to BOTH `commons/data/ocas-dispatch/evidence.jsonl` and `owner/evidence.jsonl`, then run `scripts/verify_evidence_threads.py <tids>` and require `in_evidence(structured) action=action:none`. The runner then writes its `dispatch-wave-*.json` with `emails: {path_a:0, path_b:0, mode:"path_a_skip_verify_evidence"}` — PATCH that block afterward to record the real email work (path_a/path_b counts, `mode:"path_b_gap_closure"`, detail arrays, `results.email_threads_closed`). Closure gates still pass (email proof lives in evidence.jsonl), but the wave journal must reflect reality for integrity-of-record.
- **JSON structural edits: use `write_file`, not `patch` (confirmed live 2026-07-24):** When changing a JSON file's structure (adding/nesting/removing keys), `patch` can match an inner substring and produce a malformed document — e.g. nested `"emails": {"emails": {...}}` with a trailing-comma `JSONDecodeError` — that the tool reports as 'success' until validation. Rewrite the whole file with `write_file` instead, then validate with `python3 -c "import json; json.load(open(path))"`. Reserve `patch` for single atomic value replacements where the surrounding structure is unchanged.
- **Heredoc `$(date)` timestamp mismatch** — compose TS into a variable first, use for both filename and content
- **Research is not optional for improvements** — MUST run Phase 1.5 research before touching files
- **`forge.update` vs `git pull` divergence** — cross-check git log against SKILL.md version field
- **`forge_audit_skills.py` is now functional (corrected 2026-07-14)** — The script at `scripts/forge_audit_skills.py` is a REAL compliance audit: it scans every `ocas-*` skill's `scripts/` for forbidden non-secret env-var config reads (`GENIE_*`, `<NAME>_MAX_AGE_DAYS`, `<NAME>_PATH`, `<NAME>_ENABLED`, etc.) and its `SKILL.md` body for env-var config tables, and exits non-zero on any blocking (ERROR) issue. Run `python3 scripts/forge_audit_skills.py` (or `--skill ocas-<name>` for one). It skips `.bak` files. When `forge.audit` is invoked, run this first — it is the standing gate, not a manual-only task. The pre-2026-07-14 stub note is obsolete.
- **`nargs='[]'` is invalid in argparse** — When defining command-line arguments, `nargs='[]'` is not a valid option and will cause a ValueError. To accept zero or more arguments, use `nargs='*'` instead. This caused the run_dispatch_pipeline.py script to fail until corrected.
- **Tool-layer flake: `skill_view` / `read_file` / `write_file` / `search_files` all return `DaemonThreadPoolExecutor` API error (observed 2026-07-16 dispatch + this session's mixed-wave closure)** — When loading a skill (`skill_view`), reading a `SKILL.md` / journal JSON (`read_file`), OR writing a file (`write_file`), the tool layer can intermittently fail with `Error during OpenAI-compatible API call #N: 'DaemonThreadPoolExecutor' object has no attribute '_initializer'`. `terminal()` is NOT affected. **Read fallback:** read the same file directly with `terminal()` — use `find <dir> -name '*.md'` / `-name '*.json'` to locate it, then `cat <path>`. **Write fallback:** use `terminal()` `cat > file << 'EOF'` heredoc (JSON) or `cat > script.py << 'PYEOF'` heredoc — the same pattern ocas-mentor/ocas-praxis already mandate for their cron JSON/journal writes. Avoid `${...}` / `$(...)` inside heredocs (bash expansion pitfall) and watch for the Praxis double-Z filename trap. **Search fallback:** once `search_files` flakes, route around it with terminal `find <dir> -name '*.py'` (file search) or `grep -rn 'PATTERN' <dir>` (content search) — do NOT burn 3+ retries. **Do NOT burn 3–4 retries** on `skill_view`/`write_file`/`search_files`; after 1–2 identical failures, switch to terminal. When the flake hits MULTIPLE skills in one run, don't retry each individually — batch-read ALL remaining needed SKILL.md files in ONE terminal `cat` (e.g. `cd skills && for s in ocas-mentor ocas-praxis ocas-dispatch; do echo "== $s =="; cat $s/SKILL.md; done`) instead of burning N `skill_view` retries; the 2026-07-16 dispatch wave closed a combined re-detection this way after `skill_view` succeeded once then failed 3× in a row. CRITICAL non-determinism: the flake is NOT consistent even within one session — `write_file` succeeded for several `/tmp/*.py` writes earlier in 2026-07-16 closure, then failed twice in a row on a later `/tmp/update_state.py` + `last_email_check.json` write before the `terminal() cat >` fallback succeeded. So neither a prior success nor a prior failure predicts the next call; route around it on first flake. Skill files live under `$HERMES_HOME/../indigo/skills/<name>/` and journals under `$HERMES_HOME/../indigo/commons/journals/<skill>/<date>/` — the filesystem path is authoritative. The flake is in the tool layer, not the skill content; treat it as transient and route around it. (Confirmed live 2026-07-16: `skill_view` ×3 and `read_file` ×4 all failed identically; `terminal() cat`/`cat >`/`find`/`grep` succeeded on every attempt. The flake had cleared by the review pass, so it is genuinely intermittent, not persistent.)

The Forge build pipeline:

- [ ] Existence gate — check if skill already exists
- [ ] Research — search GitHub, arxiv, skill registries for patterns
- [ ] Classify — determine skill type and scope
- [ ] Scope — define boundaries and interfaces
- [ ] Architecture — design the package structure
- [ ] Plan — create implementation plan
- [ ] Build — implement the skill
- [ ] Validate — run critique and verify 50/50

## Inter-skill interfaces

Forge reads variant proposals and decisions from Mentor journals.

File types received:
- `{proposal_id}.json` — VariantProposal
- `{decision_id}.json` — VariantDecision

After processing each file, move to the consumer's ingestion log.

See `references/interfaces.md` for full handoff contracts.

## Storage layout

See `references/storage-layout.md`.

## OKRs

See `references/okrs.md`.

## Optional skill cooperation

- Critique (skilllab) — the evaluation complement to Forge's build pipeline
- Mentor — receives VariantProposal and VariantDecision files via journal payload
- Fellow — Forge may build experiment harnesses for Fellow benchmarks
- Custodian — initializes skills built by Forge during system health passes
- **Chronicle** — skill metadata and journal entries ingested for knowledge persistence

## Journal outputs

Action Journal — every build, critique, repair, validation, audit, and variant processing run.

## Initialization

On first invocation of any Forge command, run `forge.init`. Creates data
directories, writes default config, registers the `forge.update` cron job, and
logs the initialization decision. See `references/init_procedure.md` for the
exact sequence.

## Dispatch / Cron Integration

When triggered by dispatcher or `forge:journal-scan` cron, run these steps (full operational detail + all recovery/closure gotchas in `references/dispatch-integration-detail.md`; canonical decision procedure in `references/dispatch-pipeline-guide.md`):

- [ ] Check for unprocessed `vp_*.json` / `vd_*.json` in data root, `proposals/`, `intake/`
- [ ] Cross-reference against `intake/processed/` and `processed/` to skip already-processed files
- [ ] Process new files: build variants, apply fixes, queue for Mentor
- [ ] Move processed files to `processed/`
- [ ] If no unprocessed files: write no-op journal and exit
- [ ] Perform phantom file cleanup: after every dispatch run, `ls` journal dirs and fix empty/double/malformed timestamps (see `references/phantom-file-cleanup.md`)

**Critical dispatch rules (all detailed in the references file):** explicit-run prompt overrides the no-op shortcut; second-wave = add gaps + advance state, never write new journals; bridge all output journals into BOTH eval stores idempotently by full relative path (never basename); advance `last_ingest_run` past max mtime of ALL touched journals INCLUDING post-dispatch mentor-cron heartbeats, then RE-SWEEP to GENUINE GAP = 0; recoveries REWRITE the existing wave journal (never mint a new one); for post-state closure, iterate `scripts/closure_convergence_sweep.py --date <DATE>` to 0 additions then assert `GENUINE GAP = 0` via `scripts/verify_genuine_gap_profile.py --date <DATE>` (both confirmed present on disk 2026-07-15; the prior note that `reconcile_*`/`verify_*`/`closure_*` helpers "do NOT exist on disk" is obsolete for these two — see `references/dispatch-closure-sequence.md`). NEVER `rm -rf` the `home/` tree — only the doubled `home/.hermes` node.

**Monitor gate state is a SEPARATE file — and it has TWO copies (confirmed 2026-07-16 closure):** the journal re-fire is gated by `monitor_journals.py` reading the PROFILE-relative monitor state at `$HERMES_HOME/../indigo/commons/data/monitor_state/journal_ingest_state.json` (`latest_mtime`) — NOT `ocas-praxis/ingest_state.json`. Closing a re-detection MUST advance BOTH monitor copies AND the praxis `last_ingest_run`, all past the max journal mtime. Always `json.load`+overwrite BOTH copies (`<hermes-home>/commons/data/monitor_state/journal_ingest_state.json` and `<hermes-home>/profiles/indigo/commons/data/monitor_state/journal_ingest_state.json`). **CORRECTED 2026-07-17:** `scripts/closure_closeout_check.py` (an ocas-FORGE script, lives only in `ocas-forge/scripts/`, NOT ocas-dispatch) was updated to read BOTH monitor copies and to assert gate [2] against the profile copy (the load-bearing one); the old version read only the root copy and gave a false "closed". The verifier's gate [2] reporting True IS now proof the monitor is satisfied (both copies checked). **Email gate fix (2026-07-17):** the verifier no longer requires the two top-level GWS-snapshot files (`last_email_check.json`, `last_email_check_<account-identity>_gmail_com.json`) — those stay `null` under the monitor re-fire bug and are the known un-closeable gate; it now REQUIRES only the dispatch-owned account copies (`owner/last_email_check.json`, `last_email_check_owner.json`, indigo equivalents) and WARNs on the top-level snapshots. See `references/redetection-stale-state-closure-oneshot.md` (Monitor gate state section).
- **Never recursive-glob `commons/journals/**/*.json` during closure diagnosis (confirmed 2026-07-16):** a symlink/dir loop nests `commons/journals/journals/journals/...` to arbitrary depth, so a recursive `**/` glob over the journals tree returns millions of duplicate paths and can dump 30+ MB into one tool result, burying the real answer. The closure scripts (`closure_convergence_sweep.py`, `verify_genuine_gap_profile.py`) already use a BOUNDED per-skill `os.listdir(skill)/<DATE>` walk — copy that pattern. For ad-hoc discovery, glob `commons/journals/<skill>/<DATE>/*.json` per skill, never `**`.
- **Re-detection recurs because the wave writer doesn't advance gates atomically (07-13→07-14→07-15→07-16 all re-fired):** each `dispatch-wave-*.json` is written but the monitor `latest_mtime` + praxis `last_ingest_run` are left at pre-wave values, so `monitor_journals.py` re-enqueues the same files next cycle until a manual closure runs. Real fix: have the wave writer advance both gates in the same transaction as the journal write (or invoke the closure Closeout at wave end). Manual `closure_closeout_check.py` + dual-copy state advance is the stopgap, not the cure — note it in any closure journal so the recurring pattern is visible. **Mtime-advance truncation trap (confirmed 2026-07-16 closure):** when advancing these gates, NEVER hand-type the float/ISO mtime literal — a hand-typed `1784232657.4915` TRUNCATES below the true `1784232657.4915047` (4.7e-6 under), so `state < max_journal_mtime`, `closure_closeout_check.py` reports `monitor>=max: False`, and the dispatcher RE-FIRES forever. This is the SAME failure class as `redetection-epoch0-pitfall.md` (state < max) but a DIFFERENT cause (hand-typed literal truncation vs. bad arithmetic on a 0-gap sweep). ALWAYS recompute `max(os.path.getmtime(p) for p in glob('commons/journals/*/<DATE>/*.json'))` programmatically and add `>= 1.0s` pad so any heartbeat landing during the write is also covered. See `references/redetection-mtime-truncation-pitfall.md`.
- **Scheduled dispatcher RE-FIRES mid-closure and CLOBBERS the two top-level GWS-snapshot email-state files (confirmed 2026-07-16T2145Z):** a manual Mode-C closure re-flagging `verified_second_wave=True` on `commons/data/ocas-dispatch/last_email_check.json` + `last_email_check_<account-identity>_gmail_com.json` got overwritten ~60s later when the scheduled `dispatcher.py`/`monitor_email.py` fired AGAIN during the run, rewriting those two files as pure gws snapshots (raw `new_threads`) with `verified_second_wave` dropped to Null. The dispatch-OWNED owner copies (`owner/last_email_check.json`, `last_email_check_owner.json`) survive intact (True). **Mitigation:** after re-flagging, immediately re-run `scripts/closure_closeout_check.py` in the SAME script (subprocess) so the verifier reads the flag before the next dispatcher tick (minimize clobber window); if `[3]` still shows None, the dispatcher landed between your write and the verifier — just re-flag the two top-level files and re-verify. Do NOT chase a permanently-green `[3]` on those two files; they are the known un-closeable gate under the monitor re-fire bug (see `references/dispatch-wave-email-state-topology.md`, Caveat). The LOAD-BEARING gates are `[1]` (named journal in both eval stores) and `[2]` (state advanced past max mtime), which DO stay green once advanced. Full recipe in `references/closure-email-state-refire-pitfalls.md`.
- **Recursive `**/last_email_check*.json` glob OVER-PERTAINS `indigo/last_email_check.json` during a single-account closure (confirmed 2026-07-16T2145Z):** that file's basename is `last_email_check.json` (identical to the owner top-level snapshot), so a blanket recursive glob + flip-to-True wrongly set indigo's `verified_second_wave` to True, corrupting indigo's genuine `false` state (it was a real `action:none` triage from 20260716T161444Z, not second-wave). This is the REVERSE of the `dispatch-wave-email-state-topology.md` "key on path not account" advice. **Mitigation:** during a owner-specific closure, target owner's files by EXPLICIT paths only — `last_email_check.json`, `last_email_check_<account-identity>_gmail_com.json`, `owner/last_email_check.json`, `last_email_check_owner.json` — never a recursive `**/last_email_check*.json` blanket. If you over-flip, restore the other account's file from its prior true value. Full detail in `references/closure-email-state-refire-pitfalls.md`.

**The expanded critical dispatch rules, recovery recipes, and the full 7-pattern eval-gap catalog** are in `references/dispatch-integration-pitfalls-skillmd.md` — read it before running any dispatch pipeline or recovery. It is the companion to the summary above and the canonical `references/dispatch-pipeline-guide.md`.


## Self-update

`forge.update` pulls the latest package from the `source:` URL in frontmatter.
Runs silently unless version changed or error.

**Drift detection procedure** (added 2026-06-23, extended 2026-06-29): Two distinct drift scenarios exist — handle BOTH:

**Scenario A — Origin ahead (upstream has new commits):** Dev installs can accumulate local commits that diverge from origin, causing `git pull` to falsely report "Already up to date." Before declaring up-to-date: (1) `git fetch origin`, (2) `git log --oneline HEAD..origin/main` — any output = upstream ahead, (3) `git diff --stat origin/main` — any diff = content drift regardless of commit history, (4) cross-check SKILL.md `version:` field against `git log origin/main --oneline -1` — frontmatter can lag commit messages. See Gotchas for conflict resolution when local modifications contradict upstream direction.

**Scenario B — Local drift (working tree dirty, origin NOT ahead):** When `git log HEAD..origin/main` is EMPTY but `git diff --stat origin/main` shows changes, OR when `git status` lists untracked files, the local working tree has accumulated modifications that origin doesn't have. This commonly happens when: session journals are written into `references/`, SKILL.md gets edited locally without committing, or new support files are added but not pushed. **Diagnostic:** `git status --short` — if output is non-empty with origin at same commit, you have local drift. **Response:** (1) Separate operational artifacts (session journals in `references/`) from skill content changes (SKILL.md edits, new reference docs). (2) Session journals should be moved to `commons/data/ocas-forge/journals/` — they do NOT belong in the skill repo. (3) Skill content changes should be committed and pushed, or stashed if experimental. (4) NEVER `git add .` blindly — this commits operational logs into the skill's git history. Confirmed 2026-06-29: 100+ session journals accumulated in `references/` over 7 days, causing `git diff --stat` to show 100+ untracked files with origin at same commit.

## Skill consolidation

`forge.consolidate` merges an orphan or duplicate skill into its natural parent.
See `references/builder_workflows.md` for the full workflow.

**Core rule:** fold merged content into the parent's existing section structure.
Do NOT wrap in `## Integrated:` sections.

**Consolidation hazard — a SKILL.md-less `ocas-*` dir is NOT automatically a removable orphan (confirmed 2026-07-13):** When auditing for orphans, finding a directory named `ocas-*` or `util-*` WITHOUT a `SKILL.md` does NOT mean it is safe to delete. It may be **live state storage** hard-coded as a `STATE_FILE` (or similar) by a sibling skill. Before removing any such dir: (1) `grep -rln "skills/<name>" . --include=*.py --include=*.json` across the whole tree — if any script references the path, it is load-bearing; (2) check the dir's contents' mtime — a recently-modified `*.json` inside means a running cron writes there. Real case from this audit: `ocas-critique/` had NO `SKILL.md` (already merged into `ocas-skilllab`, confirmed by `merged-from: ocas-critique` in skilllab's frontmatter) yet `ocas-skilllab/scripts/critique_10khr_runner.py` hard-codes `STATE_FILE = os.path.join(_HERMES_ROOT, "skills", "ocas-critique", "commons", "data", "ocas-critique", "10khr-state.json")`, and that file's mtime was 2026-07-12 (written by a cron) — deleting the dir would have broken the 10khr engine. Contrast the genuine orphan found same session: `ocas-10xeng-autofix/` (top-level) had no SKILL.md, only a stale `last_run.json` dated 2026-07-01, ZERO path references anywhere, and a real canonical copy at `software-development/ocas-10xeng-autofix/` — safe to move to `.archive/`. **Decision rule: only remove a SKILL.md-less `ocas-*` dir after BOTH grep-for-path returns no script/config references AND the dir's contents are not recently written by a cron.** Session detail: `references/session-20260713-audit-orphan-state-storage.md`.

## GitHub repo creation

Before creating any GitHub repo, verify the skill is OCAS-authored. See
`references/github_repo_guardrails.md` for the full guardrail checklist.

## Skill library search APIs

When Phase 1.5 triggers skill library research, use these APIs to understand approaches and patterns — not to copy verbatim.

- **SkillsMP** — `sk_liv...370I` key, https://skillsmp.com/docs/api
- **AgentSkill.sh** — `agentskill` CLI, https://agentskill.sh
- **LobeHub** — `lobehub` CLI, https://lobehub.com/cli
- **Skills.sh** — https://www.skills.sh/docs/api
- **OpenClaw** — `clawhub` CLI, https://docs.openclaw.ai/clawhub/cli
- **GitHub OCAS repos** — `gh search repos "ocas-* user:<agent-handle>" --json fullName,description,url`

**Output of library search:** For each relevant skill found, note: name, description, how it works (architecture), what patterns it uses, and what the new skill can learn from it. Synthesize — don't clone.

When asked to audit sync state of all OCAS skills, or when running a scheduled
sync check, use the workflow in `references/sync_audit_procedure.md`.

## Skill audit

`forge.audit` audits one or more existing OCAS skills for architecture compliance,
applies fixes, and syncs to GitHub.

**Mandatory Configuration Policy gate (blocks submission to the Nous optional-skills catalog):** behavioral settings (thresholds, retention windows, feature flags, display prefs, paths) MUST NOT be read from environment variables — the hermes-sweeper auto-closes such PRs under the `env-var-for-config` policy. Correct mechanism: declare each setting in `metadata.hermes.config`, read it at runtime from `$HERMES_HOME/config.yaml` under `skills.config.<key>` (via PyYAML — `telephony.py` is the reference impl), document `skills.config.<key>` in SKILL.md (never env-var names), and let CLI flags override. Only secrets go in `.env`; only `HERMES_HOME`/`HERMES_PROFILE` locate the runtime.

Run the automated check first:
```bash
python3 scripts/forge_audit_skills.py --skill <ocas-name>   # 0 exit = clean
```
It flags any `GENIE_*` / non-secret env-var config read in `scripts/` or env-var config table in `SKILL.md`. The full written standard lives in `ocas-skilllab`'s `references/nous-skill-requiremen

…(truncated)
