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):
- 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/.
- 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.
- Persist build log entries and decisions to local JSONL files.
- Log material decisions to
decisions.jsonl.
- Write journal via
forge.journal.
- 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.
- Log material decisions to
decisions.jsonl.
- 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:
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):
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.
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:
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)
1---2name: ocas-forge3description: 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).4license: MIT5---67Forge 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.89## When to Use1011- Building new OCAS skills from scratch12- Skill architecture and design review13- Bulk skill library updates and synchronization14- Skill consolidation and deprecation15- When a new capability needs a permanent skill home16- Create a new Agent Skill from a goal or capability description17- Review or critique an existing skill package18- Repair broken or defective skill packages19- Validate a skill package against OCAS standards2021## When NOT to Use2223- One-off task execution (use the appropriate existing skill)24- Skill evaluation/scoring (use Mentor or skilllab)25- Content generation or research26- System health monitoring (use Custodian)27- Authentication and service wiring (use ocas-auth)28- Building non-skill artifacts29- Web research — use Sift30- **Skill structure reference / frontmatter template lookup** — use `write-a-skill` for quick31 reference on field requirements, file layout, and description conventions. Forge is for building.3233## Responsibility boundary3435Forge owns skill design, construction, consolidation, update verification,36compliance auditing, and repo-sync. Forge's `forge.validate` handles quick37structural checks; deep quality scoring and iterative improvement is now38owned by `skilllab` (Critique procedure, merged from ocas-critique).3940Forge does not own: skill quality scoring and iteration (skilllab), skill41evaluation or variant testing (Mentor), behavioral pattern analysis,42behavioral refinement (Praxis), experimentation (Fellow), system health and43skill initialization (Custodian), runtime orchestration and delegation (the44agent harness), authentication and MCP wiring (ocas-auth).4546**Note on `review-skill` vs skilllab's Critique:** The 3rd-party `review-skill`47(agentskill-sh) provides lightweight quick-check scoring. skilllab's Critique48procedure is the full OCAS quality engine with 6-phase pipeline, batch mode,49iteration loops, and autonomous improvement. Use skilllab for all OCAS skill50scoring. Use `review-skill` only for quick structural checks on non-OCAS51skills. Never52push `review-skill` to GitHub — it's 3rd-party.5354Forge receives VariantProposal and VariantDecision files from Mentor. It builds variant packages and applies promotion decisions.5556## Ontology types5758- **Concept/Event** — projects, tasks, skill performance evaluations, OKR cycles59- **Concept/Idea** — improvement proposals, behavioral patterns60- **Thing/DigitalArtifact** — project state records, task graphs, evaluation reports6162Mentor does not emit entity signals directly. Journal outputs are ingested by Chronicle for knowledge persistence.6364## Commands6566- `forge.build` — design, scope, build, and validate a complete skill package67- `forge.critique` — review a package and identify defects68- `forge.repair` — fix broken files in an existing package69- `forge.classify` — classify a proposed skill (shortcut, workflow, system)70- `forge.validate` — run validation checks on a package71- `forge.scaffold` — generate a minimal package skeleton72- `forge.consolidate` — merge an orphan or duplicate skill into its natural parent73- `forge.verify-update` — check whether a skill is at the latest version from its GitHub source74- `forge.sync` — sync local skill changes to the canonical repository via PR75- `forge.audit` — audit one or more skills for OCAS compliance, apply fixes, and sync to GitHub76- `forge.status` — current build state if multi-step build in progress77- `forge.journal` — write journal for the current run; called at end of every run78- `forge.update` — pull latest from GitHub source; preserves journals and data7980## Mandatory design pipeline8182Run all phases before writing files. Full phase detail including existence gates83(parent search, standalone test, absorption test), **research (skill library → GitHub search → deep-read → compare)**,84classification, scoping, architecture, plan, build, and validation procedures: see85`references/design_pipeline.md`.8687Key rule: **absorption first.** If an existing skill already owns the domain,88add content to it as a `references/` doc or `scripts/` file — do not create a89new skill. See `references/enforcement_durability.md` for the absorption90decision framework.9192**Research rule:** After the existence gate passes, you MUST research before93classifying. Search GitHub repos (via `gh search repos`) AND search the skill94library (via the APIs below) to understand what already exists. The goal is95NOT to copy existing skills but to **understand how they work** and **synthesize**96that knowledge into a new, better skill. Review at least 10 repos or skills97before deciding to build.9899## Naming and Authorship Rules100101See `references/naming-and-authorship.md` for the full naming convention and authorship tagging rules. Key points:102103- **Never create `ocas-*` or rename to `ocas-*`/`util-*` without explicit user authorization.**104- **Auto-generated skills** must use `author: autogenerated` in metadata.105- **Auto-generated thin wrappers** are candidates for deletion — see references for criteria.106107## Skill type classification108109- **Shortcut** — narrow tool wrapper. 20-120 line SKILL.md.110- **Workflow** — multi-step process. 80-250 line SKILL.md.111- **System** — durable behavior system. 150-300 line SKILL.md, deeper material in references.112113## Package rules114115Minimum 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.116117**Run completion**118119After every Forge command (build, critique, repair, validate, audit):1201211. 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/`.1222. 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.1233. Persist build log entries and decisions to local JSONL files.1244. Log material decisions to `decisions.jsonl`.1255. Write journal via `forge.journal`.1264. **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.1275. Log material decisions to `decisions.jsonl`.1286. Write journal via `forge.journal`.129**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.130131## Cross-platform portability132133Skills that hardcode `~/.hermes/` paths will NOT work on other agent harnesses (OpenClaw, Claude Code, Cursor, etc.). When building a new skill:134135- **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.136- **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.137- **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.138- **Document Hermes-specific tool dependencies** in a "Required tools" section so future porters know what to adapt.139140## Anti-patterns to reject141142- **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.143- Vague or overly broad scope144- Generic descriptions that don't route well145- SKILL.md bloated with background explanation146- Support folders created for aesthetics147- Plans returned instead of packages148- Template residue and placeholders149- Storage inside skill package directories150- Undocumented inter-skill interfaces151- **`## 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.152- **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.153154## Gotchas155156- **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.157- **Stale files in `processed/` vs `intake/processed/`** — check both locations during journal-scan158- **`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.159- **Missing `includes:` in frontmatter** — required when references/ or scripts/ dir exists160- **Scope boundary for sync** — `forge.sync`/`forge.audit` only on `ocas-*` skills161- **Doing more than asked** — match work to the scope of the request162- **Incorrect Naming** — NEVER create/rename `ocas-*` without user authorization163- **Non-durable fixes** — put rules in skill's own git repo or MEMORY.md, not hermes core164- **Runaway repo creation** — check for 3rd-party skills before `gh repo create`165- **YAML block scalar truncation** — `description: >` / `|` contain newlines; use `read_file` + `patch`166- **`action` field is polymorphic in forge journals** — guard with `isinstance` before every access167- **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.168- **`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.169- **Appending to JSONL files** — ALWAYS use `echo >>`, NEVER heredoc with `>`170- **`write_file` escapes quotes in Python files** — use `terminal()` with heredoc for .py files171- **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.172- **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.173- **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.174- **`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.175- **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.176- **Heredoc `$(date)` timestamp mismatch** — compose TS into a variable first, use for both filename and content177- **Research is not optional for improvements** — MUST run Phase 1.5 research before touching files178- **`forge.update` vs `git pull` divergence** — cross-check git log against SKILL.md version field179- **`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.180- **`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.181- **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.)182183The Forge build pipeline:184185- [ ] Existence gate — check if skill already exists186- [ ] Research — search GitHub, arxiv, skill registries for patterns187- [ ] Classify — determine skill type and scope188- [ ] Scope — define boundaries and interfaces189- [ ] Architecture — design the package structure190- [ ] Plan — create implementation plan191- [ ] Build — implement the skill192- [ ] Validate — run critique and verify 50/50193194## Inter-skill interfaces195196Forge reads variant proposals and decisions from Mentor journals.197198File types received:199- `{proposal_id}.json` — VariantProposal200- `{decision_id}.json` — VariantDecision201202After processing each file, move to the consumer's ingestion log.203204See `references/interfaces.md` for full handoff contracts.205206## Storage layout207208See `references/storage-layout.md`.209210## OKRs211212See `references/okrs.md`.213214## Optional skill cooperation215216- Critique (skilllab) — the evaluation complement to Forge's build pipeline217- Mentor — receives VariantProposal and VariantDecision files via journal payload218- Fellow — Forge may build experiment harnesses for Fellow benchmarks219- Custodian — initializes skills built by Forge during system health passes220- **Chronicle** — skill metadata and journal entries ingested for knowledge persistence221222## Journal outputs223224Action Journal — every build, critique, repair, validation, audit, and variant processing run.225226## Initialization227228On first invocation of any Forge command, run `forge.init`. Creates data229directories, writes default config, registers the `forge.update` cron job, and230logs the initialization decision. See `references/init_procedure.md` for the231exact sequence.232233## Dispatch / Cron Integration234235When 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`):236237- [ ] Check for unprocessed `vp_*.json` / `vd_*.json` in data root, `proposals/`, `intake/`238- [ ] Cross-reference against `intake/processed/` and `processed/` to skip already-processed files239- [ ] Process new files: build variants, apply fixes, queue for Mentor240- [ ] Move processed files to `processed/`241- [ ] If no unprocessed files: write no-op journal and exit242- [ ] Perform phantom file cleanup: after every dispatch run, `ls` journal dirs and fix empty/double/malformed timestamps (see `references/phantom-file-cleanup.md`)243244**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.245246**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).247- **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 `**`.248- **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`.249- **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`.250- **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`.251252**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`.253254255## Self-update256257`forge.update` pulls the latest package from the `source:` URL in frontmatter.258Runs silently unless version changed or error.259260**Drift detection procedure** (added 2026-06-23, extended 2026-06-29): Two distinct drift scenarios exist — handle BOTH:261262**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.263264**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.265266## Skill consolidation267268`forge.consolidate` merges an orphan or duplicate skill into its natural parent.269See `references/builder_workflows.md` for the full workflow.270271**Core rule:** fold merged content into the parent's existing section structure.272Do NOT wrap in `## Integrated:` sections.273274**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`.275276## GitHub repo creation277278Before creating any GitHub repo, verify the skill is OCAS-authored. See279`references/github_repo_guardrails.md` for the full guardrail checklist.280281## Skill library search APIs282283When Phase 1.5 triggers skill library research, use these APIs to understand approaches and patterns — not to copy verbatim.284285- **SkillsMP** — `sk_liv...370I` key, https://skillsmp.com/docs/api286- **AgentSkill.sh** — `agentskill` CLI, https://agentskill.sh287- **LobeHub** — `lobehub` CLI, https://lobehub.com/cli288- **Skills.sh** — https://www.skills.sh/docs/api289- **OpenClaw** — `clawhub` CLI, https://docs.openclaw.ai/clawhub/cli290- **GitHub OCAS repos** — `gh search repos "ocas-* user:<agent-handle>" --json fullName,description,url`291292**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.293294When asked to audit sync state of all OCAS skills, or when running a scheduled295sync check, use the workflow in `references/sync_audit_procedure.md`.296297## Skill audit298299`forge.audit` audits one or more existing OCAS skills for architecture compliance,300applies fixes, and syncs to GitHub.301302**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.303304Run the automated check first:305```bash306python3 scripts/forge_audit_skills.py --skill <ocas-name> # 0 exit = clean307```308It 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-requiremen309310…(truncated)