From 81f53911158654be285aa8ced233640075f9f138 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 14:08:13 +0000 Subject: [PATCH 01/22] chore: configure codex graphify profile --- .codex/config.toml | 5 +++++ .codex/hooks.json | 11 +++++++++++ AGENTS.md | 13 +++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .codex/config.toml diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..9bfd3b5ae --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,5 @@ +# Codex FirstMate background profile. +# Not YOLO: workspace sandbox stays active; blocked boundary work escalates on request. +sandbox_mode = "workspace-write" +approval_policy = "on-request" +approvals_reviewer = "auto_review" diff --git a/.codex/hooks.json b/.codex/hooks.json index 56d318764..f3ebf4af7 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -10,6 +10,17 @@ } ] } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "graphify hook-check" + } + ] + } ] } } diff --git a/AGENTS.md b/AGENTS.md index 076d8e142..de2a4c899 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -929,3 +929,16 @@ On an `x-mention ` or `x-mode-error ...` `check:` wake, load `fmx-re It owns mention classification, acting on the request, reply composition, voice, thread-splitting, image attachments, dry-run preview, and the completion-follow-up procedure in full, including what an `x-mode-error` wake means instead. `docs/configuration.md` "X mode (.env)" has the wire-protocol reference. The one fact that must survive here because it fires on a generic terminal wake, not the mention wake itself: when an X-mode-linked task reaches a terminal state, post its final completion follow-up per section 8's wake-handling step before tearing down. + +## graphify + +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. + +Rules: +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). From c8ab1b3f958f1c874cb5352a1b7d9e38cd69cb18 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 18:39:19 +0000 Subject: [PATCH 02/22] fix: make graphify hook portable --- .codex/hooks.json | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index f3ebf4af7..fb6295777 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "graphify hook-check" + "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'" } ] } diff --git a/.gitignore b/.gitignore index dc785fb89..5f260a1d6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ config/backlog-backend config/backend config/x-mode.env config/cmux-socket-password +graphify-out/ From ee03cff263cf16f33165e9b75e0c515b05b2baf2 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:28:24 +0000 Subject: [PATCH 03/22] fix: bound graphify hook runtime --- .codex/hooks.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index fb6295777..fb356bfe2 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -17,7 +17,8 @@ "hooks": [ { "type": "command", - "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'" + "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'", + "timeout": 10 } ] } From 176586a6b869cd739c613fe9b88b94884b72d2ac Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:46:23 +0000 Subject: [PATCH 04/22] no-mistakes(test): Captain, harden bootstrap and Pi tests --- tests/fm-pi-watch-extension.test.sh | 20 ++++++++++++++++++-- tests/fm-session-start.test.sh | 26 ++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/fm-pi-watch-extension.test.sh b/tests/fm-pi-watch-extension.test.sh index 33c201a43..21aca95ad 100755 --- a/tests/fm-pi-watch-extension.test.sh +++ b/tests/fm-pi-watch-extension.test.sh @@ -93,7 +93,6 @@ SH FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" "$GEN" >/dev/null out=$(PLUGIN="$home/state/fm-primary-pi-watch.ts" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' import { writeFileSync } from "node:fs"; -import { pathToFileURL } from "node:url"; let handler = null; let prompt = ""; @@ -108,7 +107,24 @@ const pi = { }, }; writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`); -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +let source = await import("node:fs").then(({ readFileSync }) => readFileSync(process.env.PLUGIN, "utf8")); +source = source + .replace(/type ArmResult = \{[\s\S]*?\};\n\n/, "") + .replace(/type LockOwnership = [^\n]+\n\n/, "") + .replace(/export default function \(pi: any\)/, "export default function (pi)") + .replace(/let child: any = null;/, "let child = null;") + .replace(/function parentPid\(pid: string\): string/, "function parentPid(pid)") + .replace(/function pidAlive\(pid: string\): boolean/, "function pidAlive(pid)") + .replace(/function lockOwnership\(\): LockOwnership/, "function lockOwnership()") + .replace(/function sessionOwnsLock\(\): boolean/, "function sessionOwnsLock()") + .replace(/async function sendWake\(message: string\)/, "async function sendWake(message)") + .replace(/function actionableLine\(output: string\): string/, "function actionableLine(output)") + .replace(/function failureLine\(stdout: string, stderr: string, code: number \| null\): string/, "function failureLine(stdout, stderr, code)") + .replace(/function startArm\(\): ArmResult/, "function startArm()") + .replace(/\(chunk: Buffer\)/g, "(chunk)") + .replace(/\(code: number \| null\)/g, "(code)") + .replace(/\(error: Error\)/g, "(error)"); +const mod = await import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); mod.default(pi); if (!handler) { console.error("Pi watch command was not registered"); diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 934c2511d..81780a3d1 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -335,7 +335,17 @@ EOF make_fake_toolchain "$fakebin" make_fake_ps_claude "$fakebin" # Force a MISSING diagnostic line so the bootstrap section is non-trivial. - rm -f "$fakebin/node" + # Do this through an incompatible fake tool instead of deleting a fakebin + # entry, because the host's BASE_PATH may still provide tools like node. + cat > "$fakebin/no-mistakes" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = --version ]; then + printf '%s\n' 'no-mistakes version v1.31.1 (fake)' + exit 0 +fi +exit 0 +SH + chmod +x "$fakebin/no-mistakes" printf 'window=fm-sess:w1\nkind=ship\n' > "$home/state/task-a.meta" @@ -358,7 +368,7 @@ EOF [ "$context_line" -lt "$fleet_line" ] || fail "CONTEXT did not precede FLEET STATE" [ "$fleet_line" -lt "$next_line" ] || fail "FLEET STATE did not precede NEXT STEP" - missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: node' | head -1 | cut -d: -f1) + missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: no-mistakes' | head -1 | cut -d: -f1) [ -n "$missing_line" ] || fail "MISSING diagnostic did not appear at all" [ "$missing_line" -lt "$fleet_line" ] || fail "actionable MISSING diagnostic was buried after the bulk fleet-state digest" @@ -478,7 +488,15 @@ $rec EOF make_fake_toolchain "$fakebin" make_fake_ps_claude "$fakebin" - rm -f "$fakebin/node" + cat > "$fakebin/no-mistakes" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = --version ]; then + printf '%s\n' 'no-mistakes version v1.31.1 (fake)' + exit 0 +fi +exit 0 +SH + chmod +x "$fakebin/no-mistakes" append_wake "$home/state" signal task-z "needs-decision: pick a library" @@ -487,7 +505,7 @@ EOF # fm-lock.sh's own exact success text. assert_contains "$out" "lock acquired: harness pid" "fm-lock.sh's real output did not appear (composition, not reimplementation)" # fm-bootstrap.sh's own exact MISSING-tool line format. - assert_contains "$out" "MISSING: node (install:" "fm-bootstrap.sh's real detect line did not appear verbatim" + assert_contains "$out" "MISSING: no-mistakes (install:" "fm-bootstrap.sh's real detect line did not appear verbatim" # fm-wake-drain.sh's real drained record (raw tab-separated queue line). assert_contains "$out" "$(printf 'signal\ttask-z\tneeds-decision: pick a library')" "fm-wake-drain.sh's real drained record did not appear" From a7d3ed78dcd78cdaa02e6af62d1b66dbebab054f Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:57:22 +0000 Subject: [PATCH 05/22] no-mistakes(document): Sync Codex Graphify docs --- AGENTS.md | 6 ++++-- CONTRIBUTING.md | 5 +++-- docs/configuration.md | 15 +++++++++++++++ docs/turnend-guard.md | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index de2a4c899..5b785405d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,12 +44,12 @@ Hard rules, in priority order: You may freely write to this repo itself (backlog, briefs, state, even this file when the captain approves a change). Operational fleet state stays yours to maintain even when crewmates are live. -Shared, tracked material means `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. +Shared, tracked material means `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. When one or more crewmates are in flight, delegate changes to shared, tracked material to a crewmate through the normal scout or ship machinery instead of hand-editing them yourself. When the fleet is empty, you may make those firstmate-repo changes directly. Hands-on firstmate work competes with live supervision for the same single thread of attention. This repo is a shared template, not the captain's personal project. -The tracking principle: shared, tracked material is tracked under git; anything personal to this captain's fleet (.env, data/, state/, config/, projects/, .no-mistakes/) is not. +The tracking principle: shared, tracked material is tracked under git; anything personal to this captain's fleet (.env, data/, state/, config/, projects/, .no-mistakes/, graphify-out/) is not. Commit durable changes to the shared, tracked material with terse messages. This repo is itself behind the no-mistakes gate: ship shared, tracked material through the pipeline - branch, commit, run the pipeline, PR - and the captain's merge rule applies here exactly as it does to projects. Never add an agent name as co-author. @@ -69,6 +69,7 @@ CONTRIBUTING.md contributor workflow and repo conventions README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed .tasks.toml tracked tasks-axi markdown backend config for the default backlog backend (section 10) +.codex/ tracked Codex primary profile plus Stop and Graphify PreToolUse hooks (docs/configuration.md "Codex profile and Graphify hooks") .agents/skills/ firstmate-loaded internal skills, committed; each carries metadata.internal=true for installers .claude/skills symlink to .agents/skills for claude compatibility skills/ standalone public installer-facing skills, committed; not loaded by firstmate @@ -90,6 +91,7 @@ data/ personal fleet records; LOCAL, gitignored as a whole /brief.md per-task crewmate brief, or per-secondmate charter brief when kind=secondmate /report.md scout task deliverable, written by the crewmate; survives teardown projects/ cloned repos; gitignored; READ-ONLY for you +graphify-out/ local Graphify knowledge graph artifacts; gitignored, expected to drift after hook or incremental updates state/ volatile runtime signals; gitignored .status appended by crewmates: ": " wake-event lines, not current-state truth .turn-ended touched by turn-end hooks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e13b94ef..a25caa46b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,14 +34,15 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - This repo is a template for running a firstmate orchestrator agent. `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a symlink to it, and `.claude/skills` is a symlink to `.agents/skills`. -- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. +- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. `.agents/skills/` holds agent-loaded skills that assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery; `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). - Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`) is gitignored; never commit it. + Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`, `graphify-out/`) is gitignored; never commit it. The root `.tasks.toml` is tracked `tasks-axi` config for `data/backlog.md`; compatible `tasks-axi` is the default backend for routine backlog mutations. Compatible means version 0.1.1 or newer and `tasks-axi update --help` exposing `--archive-body`. A local `config/backlog-backend=manual` opt-out forces hand-editing and stays gitignored. A local `config/backend` file explicitly overrides runtime auto-detection for new task endpoints and stays gitignored; spawn-supported values are `tmux` plus experimental `herdr`, `zellij`, `orca`, and `cmux`, while `codex-app` is documented only in `docs/codex-app-backend.md`. It does not make `data/` tracked. + The tracked `.codex/` files are firstmate's Codex primary profile and project hooks, not personal Codex settings. - Helper scripts in `bin/` are plain bash. Each starts with a usage header comment; keep it accurate when you change behavior. Test scripts and helpers in `tests/` are plain bash too. diff --git a/docs/configuration.md b/docs/configuration.md index 78901895a..806b2f2b4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,6 +76,19 @@ That evidence policy is specific to the firstmate repo: target projects may legi That command requires `tmux` on `PATH`, prints `tmux -V`, runs every `tests/*.test.sh` with `bash`, and fails if any script exits non-zero. It intentionally mirrors the behavior-test baseline in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) instead of delegating the test step to an agent. +## Codex profile and Graphify hooks (.codex/) + +The tracked `.codex/config.toml` is the repo-local Codex Firstmate profile. +It sets `sandbox_mode = "workspace-write"`, `approval_policy = "on-request"`, and `approvals_reviewer = "auto_review"` so Codex keeps a workspace sandbox, escalates blocked boundary work, and uses auto review for approval decisions. + +The tracked `.codex/hooks.json` has two project hooks. +Its `Stop` hook is the Codex integration for `bin/fm-turnend-guard.sh`; see [`docs/turnend-guard.md`](turnend-guard.md) for the full primary turn-end supervision contract. +Its `PreToolUse` Bash hook is a fail-open Graphify integration: if `graphify` is not on `PATH`, the hook exits successfully; otherwise it runs `graphify hook-check` with a ten-second timeout. +The hook is intentionally portable and bounded so Codex tool use is not blocked by a missing Graphify install or a slow hook. + +`graphify-out/` is local, generated Graphify state and stays gitignored. +Dirty files under `graphify-out/` are expected after hooks or incremental updates and are not a reason to skip Graphify-assisted navigation when `graphify-out/graph.json` exists. + ## Captain preferences (data/captain.md) Personal preferences for one captain's fleet live locally in `data/captain.md`; it is gitignored and printed in the session-start context digest after `data/projects.md` and optional `data/secondmates.md`. @@ -119,6 +132,7 @@ claude, codex, opencode, pi, and grok are all empirically verified; new harnesse The verified adapter knowledge - busy signatures, interrupt and exit commands, skill-invocation syntax, and per-harness quirks - lives in [`.agents/skills/harness-adapters/SKILL.md`](../.agents/skills/harness-adapters/SKILL.md). Launch mechanics, including the verified command templates, live in [`bin/fm-spawn.sh`](../bin/fm-spawn.sh). Primary-session turn-end guard integrations for verified harnesses are tracked as repo-level hook files and documented in [`docs/turnend-guard.md`](turnend-guard.md). +The Codex repo-local profile and Graphify PreToolUse hook are documented above because they are Codex configuration, not harness launch mechanics. Primary-session watcher wake protocols are rendered at session start by [`bin/fm-supervision-instructions.sh`](../bin/fm-supervision-instructions.sh) from [`docs/supervision-protocols/`](supervision-protocols/). Claude and Grok use background-notify cycles, Codex uses bounded foreground checkpoints, Pi uses its generated primary watcher extension, and OpenCode uses its TUI plugin. `config/crew-harness` is a local, gitignored file containing one adapter name for crewmate and scout launches. @@ -155,6 +169,7 @@ Secondmate homes inherit this file from the primary, so a secondmate's own crewm ## Toolchain On session start the first mate detects what its required toolchain is missing or too old (tmux, node, gh, treehouse with durable lease support, no-mistakes v1.31.2 or newer, gh-axi, chrome-devtools-axi, lavish-axi, tasks-axi 0.1.1 or newer with `update --archive-body`, and quota-axi), lists it with the exact install commands, and installs only after you say go. +Graphify is optional: Codex's PreToolUse hook fails open when `graphify` is absent, so bootstrap does not report it as a required install. When bootstrap resolves `backend=orca` from `FM_BACKEND` or `config/backend`, it requires `orca`, keeps the universal `node` requirement, and skips `tmux` and `treehouse`. When `config/crew-dispatch.json` exists, bootstrap also requires `jq` for dispatch profile validation. When X mode is opted in, bootstrap also requires `curl` and `jq` before arming the relay poll shim. diff --git a/docs/turnend-guard.md b/docs/turnend-guard.md index 4203bf433..45e10bb14 100644 --- a/docs/turnend-guard.md +++ b/docs/turnend-guard.md @@ -2,7 +2,7 @@ This is the authoritative contract for the "no turn ends blind" primary guard referenced from AGENTS.md section 8. The shared predicate lives in `bin/fm-turnend-guard.sh`. -Harness-specific tracked hook files only adapt each verified harness's real turn-end mechanism to that shared predicate. +The turn-end entries in harness-specific tracked hook files adapt each verified harness's real turn-end mechanism to that shared predicate. ## Gap Closed From ebeb3fc55380022aa33a779939e535dd32f04249 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:59:12 +0000 Subject: [PATCH 06/22] no-mistakes(lint): Fix Graphify markdown style --- AGENTS.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5b785405d..f0ed96fa1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -932,15 +932,18 @@ It owns mention classification, acting on the request, reply composition, voice, `docs/configuration.md` "X mode (.env)" has the wire-protocol reference. The one fact that must survive here because it fires on a generic terminal wake, not the mention wake itself: when an X-mode-linked task reaches a terminal state, post its final completion follow-up per section 8's wake-handling step before tearing down. -## graphify +## Graphify This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: -- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. -- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. + Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. + These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. + Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). From 0c3aa4217b212d3c68009e6309597fca56ea7eee Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 14:08:13 +0000 Subject: [PATCH 07/22] chore: configure codex graphify profile --- .codex/config.toml | 5 +++++ .codex/hooks.json | 11 +++++++++++ AGENTS.md | 13 +++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .codex/config.toml diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..9bfd3b5ae --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,5 @@ +# Codex FirstMate background profile. +# Not YOLO: workspace sandbox stays active; blocked boundary work escalates on request. +sandbox_mode = "workspace-write" +approval_policy = "on-request" +approvals_reviewer = "auto_review" diff --git a/.codex/hooks.json b/.codex/hooks.json index 337bd0a68..5bf5e4403 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -38,6 +38,17 @@ } ] } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "graphify hook-check" + } + ] + } ] } } diff --git a/AGENTS.md b/AGENTS.md index cdf7b1682..ed4dd7f7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -476,3 +476,16 @@ Keep this file for knowledge useful to almost every future agent session in this Do not repeat what the codebase already shows; point to the authoritative file, skill, command, or doc. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve every safety boundary and keep the always-loaded contract concise. + +## graphify + +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. + +Rules: +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). From 7b9df5549495c663943e4cf33e93343760d0e72e Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 18:39:19 +0000 Subject: [PATCH 08/22] fix: make graphify hook portable --- .codex/hooks.json | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index 5bf5e4403..3e5adab90 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -45,7 +45,7 @@ "hooks": [ { "type": "command", - "command": "graphify hook-check" + "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'" } ] } diff --git a/.gitignore b/.gitignore index 5ed2da0c3..d1fb69b11 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ config/backend config/x-mode.env config/cmux-socket-password config/wedge-alarm +graphify-out/ From bdcde387fcd61656efc2e7d0732824b3623fa5bb Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:28:24 +0000 Subject: [PATCH 09/22] fix: bound graphify hook runtime --- .codex/hooks.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index 3e5adab90..7089ced0f 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -45,7 +45,8 @@ "hooks": [ { "type": "command", - "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'" + "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'", + "timeout": 10 } ] } From 85d750dfbe828be30ace3a47663fb25f08c6d562 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:46:23 +0000 Subject: [PATCH 10/22] no-mistakes(test): Captain, harden bootstrap and Pi tests --- tests/fm-pi-watch-extension.test.sh | 20 ++++++++++++++++++-- tests/fm-session-start.test.sh | 26 ++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/fm-pi-watch-extension.test.sh b/tests/fm-pi-watch-extension.test.sh index 1a881bc7a..0372684a9 100755 --- a/tests/fm-pi-watch-extension.test.sh +++ b/tests/fm-pi-watch-extension.test.sh @@ -92,7 +92,6 @@ SH chmod +x "$repo/bin/fm-watch-arm.sh" out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' import { writeFileSync } from "node:fs"; -import { pathToFileURL } from "node:url"; let handler = null; let notification = ""; @@ -108,7 +107,24 @@ const pi = { }, }; writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`); -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +let source = await import("node:fs").then(({ readFileSync }) => readFileSync(process.env.PLUGIN, "utf8")); +source = source + .replace(/type ArmResult = \{[\s\S]*?\};\n\n/, "") + .replace(/type LockOwnership = [^\n]+\n\n/, "") + .replace(/export default function \(pi: any\)/, "export default function (pi)") + .replace(/let child: any = null;/, "let child = null;") + .replace(/function parentPid\(pid: string\): string/, "function parentPid(pid)") + .replace(/function pidAlive\(pid: string\): boolean/, "function pidAlive(pid)") + .replace(/function lockOwnership\(\): LockOwnership/, "function lockOwnership()") + .replace(/function sessionOwnsLock\(\): boolean/, "function sessionOwnsLock()") + .replace(/async function sendWake\(message: string\)/, "async function sendWake(message)") + .replace(/function actionableLine\(output: string\): string/, "function actionableLine(output)") + .replace(/function failureLine\(stdout: string, stderr: string, code: number \| null\): string/, "function failureLine(stdout, stderr, code)") + .replace(/function startArm\(\): ArmResult/, "function startArm()") + .replace(/\(chunk: Buffer\)/g, "(chunk)") + .replace(/\(code: number \| null\)/g, "(code)") + .replace(/\(error: Error\)/g, "(error)"); +const mod = await import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); mod.default(pi); if (!handler) { console.error("Pi watch command was not registered"); diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 6ae0d124b..3a18882a6 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -402,7 +402,17 @@ EOF make_fake_toolchain "$fakebin" make_fake_ps_claude "$fakebin" # Force a MISSING diagnostic line so the bootstrap section is non-trivial. - rm -f "$fakebin/node" + # Do this through an incompatible fake tool instead of deleting a fakebin + # entry, because the host's BASE_PATH may still provide tools like node. + cat > "$fakebin/no-mistakes" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = --version ]; then + printf '%s\n' 'no-mistakes version v1.31.1 (fake)' + exit 0 +fi +exit 0 +SH + chmod +x "$fakebin/no-mistakes" printf 'window=fm-sess:w1\nkind=ship\n' > "$home/state/task-a.meta" @@ -425,7 +435,7 @@ EOF [ "$context_line" -lt "$fleet_line" ] || fail "CONTEXT did not precede FLEET STATE" [ "$fleet_line" -lt "$next_line" ] || fail "FLEET STATE did not precede NEXT STEP" - missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: node' | head -1 | cut -d: -f1) + missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: no-mistakes' | head -1 | cut -d: -f1) [ -n "$missing_line" ] || fail "MISSING diagnostic did not appear at all" [ "$missing_line" -lt "$fleet_line" ] || fail "actionable MISSING diagnostic was buried after the bulk fleet-state digest" @@ -585,7 +595,15 @@ $rec EOF make_fake_toolchain "$fakebin" make_fake_ps_claude "$fakebin" - rm -f "$fakebin/node" + cat > "$fakebin/no-mistakes" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = --version ]; then + printf '%s\n' 'no-mistakes version v1.31.1 (fake)' + exit 0 +fi +exit 0 +SH + chmod +x "$fakebin/no-mistakes" append_wake "$home/state" signal task-z "needs-decision: pick a library" @@ -594,7 +612,7 @@ EOF # fm-lock.sh's own exact success text. assert_contains "$out" "lock acquired: harness pid" "fm-lock.sh's real output did not appear (composition, not reimplementation)" # fm-bootstrap.sh's own exact MISSING-tool line format. - assert_contains "$out" "MISSING: node (install:" "fm-bootstrap.sh's real detect line did not appear verbatim" + assert_contains "$out" "MISSING: no-mistakes (install:" "fm-bootstrap.sh's real detect line did not appear verbatim" # fm-wake-drain.sh's real drained record (raw tab-separated queue line). assert_contains "$out" "$(printf 'signal\ttask-z\tneeds-decision: pick a library')" "fm-wake-drain.sh's real drained record did not appear" From 5f0fd769daabb85e71806881bbd3eb7cf6df1d3b Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:57:22 +0000 Subject: [PATCH 11/22] no-mistakes(document): Sync Codex Graphify docs --- AGENTS.md | 6 ++++-- CONTRIBUTING.md | 5 +++-- docs/configuration.md | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ed4dd7f7b..4a983c2f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,9 +37,9 @@ Hard rules, in priority order: If work failed, say so plainly with the evidence. You may maintain this repo's private operational state directly. -Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. +Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. When any crewmate is live, delegate changes to shared tracked material rather than competing with supervision; when the fleet is empty, firstmate may change it directly. -This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, and `.no-mistakes/` are captain-private and gitignored. +This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`, and `graphify-out/` are captain-private and gitignored. Ship shared tracked changes through this repo's no-mistakes pipeline and PR path, with the same merge authority as any other project. Never add an agent name as a commit co-author. @@ -58,6 +58,7 @@ CONTRIBUTING.md contributor workflow and repo conventions README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed .tasks.toml tracked tasks-axi markdown backend config for the default backlog backend (section 10) +.codex/ tracked Codex primary profile plus Stop and Graphify PreToolUse hooks (docs/configuration.md "Codex profile and Graphify hooks") .agents/skills/ firstmate-loaded internal skills, committed; each carries metadata.internal=true for installers .claude/skills symlink to .agents/skills for claude compatibility skills/ standalone public installer-facing skills, committed; not loaded by firstmate @@ -81,6 +82,7 @@ data/ personal fleet records; LOCAL, gitignored as a whole /brief.md per-task crewmate brief, or per-secondmate charter brief when kind=secondmate /report.md scout task deliverable, written by the crewmate; survives teardown projects/ cloned repos; gitignored; READ-ONLY for you +graphify-out/ local Graphify knowledge graph artifacts; gitignored, expected to drift after hook or incremental updates state/ volatile runtime signals; gitignored .status appended by crewmates: ": " wake-event lines, not current-state truth .turn-ended touched by turn-end hooks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e7bd8154..c7ac11502 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,13 +34,14 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - This repo is a template for running a firstmate orchestrator agent. `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a symlink to it, and `.claude/skills` is a symlink to `.agents/skills`. -- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. +- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.codex/`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. `.agents/skills/` holds agent-loaded skills that assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery; `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). - Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`) is gitignored; never commit it. + Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`, `graphify-out/`) is gitignored; never commit it. The root `.tasks.toml` is tracked `tasks-axi` config for `data/backlog.md`; compatible `tasks-axi` is the default backend for routine backlog mutations, with the compatibility definition owned by [`docs/configuration.md`](docs/configuration.md) ("Backlog backend"). A local `config/backlog-backend=manual` opt-out forces firstmate's routine backlog updates to hand-editing and stays gitignored; validated secondmate handoffs still delegate through `tasks-axi mv`. A local `config/backend` file explicitly overrides runtime auto-detection for new task endpoints and stays gitignored; spawn-supported values are `tmux` plus experimental `herdr`, `zellij`, `orca`, and `cmux`, while `codex-app` is documented only in `docs/codex-app-backend.md`. It does not make `data/` tracked. + The tracked `.codex/` files are firstmate's Codex primary profile and project hooks, not personal Codex settings. - Helper scripts in `bin/` are plain bash. Each starts with a usage header comment; keep it accurate when you change behavior. Test scripts and helpers in `tests/` are plain bash too. diff --git a/docs/configuration.md b/docs/configuration.md index 9e193cb97..2a4faec9d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -109,6 +109,19 @@ That evidence policy is specific to the firstmate repo: target projects may legi That command requires `tmux` on `PATH`, prints `tmux -V`, runs every `tests/*.test.sh` with `bash`, and fails if any script exits non-zero. It intentionally mirrors the behavior-test baseline in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) instead of delegating the test step to an agent. +## Codex profile and Graphify hooks (.codex/) + +The tracked `.codex/config.toml` is the repo-local Codex Firstmate profile. +It sets `sandbox_mode = "workspace-write"`, `approval_policy = "on-request"`, and `approvals_reviewer = "auto_review"` so Codex keeps a workspace sandbox, escalates blocked boundary work, and uses auto review for approval decisions. + +The tracked `.codex/hooks.json` has two project hooks. +Its `Stop` hook is the Codex integration for `bin/fm-turnend-guard.sh`; see [`docs/turnend-guard.md`](turnend-guard.md) for the full primary turn-end supervision contract. +Its `PreToolUse` Bash hook is a fail-open Graphify integration: if `graphify` is not on `PATH`, the hook exits successfully; otherwise it runs `graphify hook-check` with a ten-second timeout. +The hook is intentionally portable and bounded so Codex tool use is not blocked by a missing Graphify install or a slow hook. + +`graphify-out/` is local, generated Graphify state and stays gitignored. +Dirty files under `graphify-out/` are expected after hooks or incremental updates and are not a reason to skip Graphify-assisted navigation when `graphify-out/graph.json` exists. + ## Captain Preferences (data/captain.md / data/captain-shared.md) Domain-local preferences for one captain's fleet live locally in each home's `data/captain.md`; it is gitignored and printed in the session-start context digest after `data/projects.md` and optional `data/secondmates.md`. @@ -166,6 +179,7 @@ claude, codex, opencode, pi, and grok are all empirically verified; new harnesse The verified adapter knowledge - busy signatures, interrupt and exit commands, skill-invocation syntax, and per-harness quirks - lives in [`.agents/skills/harness-adapters/SKILL.md`](../.agents/skills/harness-adapters/SKILL.md). Launch mechanics, including the verified command templates, live in [`bin/fm-spawn.sh`](../bin/fm-spawn.sh). Primary-session turn-end guard integrations for verified harnesses are tracked as repo-level hook files and documented in [`docs/turnend-guard.md`](turnend-guard.md). +The Codex repo-local profile and Graphify PreToolUse hook are documented above because they are Codex configuration, not harness launch mechanics. Primary-session watcher wake protocols are rendered at session start by [`bin/fm-supervision-instructions.sh`](../bin/fm-supervision-instructions.sh) from [`docs/supervision-protocols/`](supervision-protocols/). Claude and Grok use background-notify cycles, Codex uses bounded foreground checkpoints, Pi uses its two tracked primary extensions, and OpenCode uses its TUI plugin. `config/crew-harness` is a local, gitignored file containing one adapter name for crewmate and scout launches. @@ -239,6 +253,7 @@ Backend tool availability uses the adapter's own executable resolver, so bootstr An unknown resolved backend emits `BACKEND_INVALID` and blocks dispatch instead of silently dropping its dependency delta or falling back to tmux. Orca provides both the task worktree and terminal endpoint (see "Runtime backend" above), so `backend=orca` requires only `orca` on top of the universal toolchain and skips both `treehouse` and every other backend's session CLI. A herdr, zellij, or cmux home is therefore never told `tmux` is missing, and the `treehouse` durable-lease upgrade check runs only for the backends that actually use treehouse. +Graphify is optional: Codex's PreToolUse hook fails open when `graphify` is absent, so bootstrap does not report it as a required install. When `config/crew-dispatch.json` exists, bootstrap also requires `jq` for dispatch profile validation. When X mode is opted in, bootstrap also requires `curl` and `jq` before arming the relay poll shim. `tasks-axi` and `quota-axi` are required bootstrap tools in every profile, the same class as `lavish-axi`. From 9f757106393f7327b697c8df283df0dd0fa5c44a Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Thu, 9 Jul 2026 19:59:12 +0000 Subject: [PATCH 12/22] no-mistakes(lint): Fix Graphify markdown style --- AGENTS.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a983c2f9..44330b35d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -479,15 +479,18 @@ Do not repeat what the codebase already shows; point to the authoritative file, Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve every safety boundary and keep the always-loaded contract concise. -## graphify +## Graphify This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: -- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. -- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. + Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. + These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. + Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). From ff7e5d4c463b4f5408ab41b4201697a0e3070e85 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Sat, 18 Jul 2026 15:00:11 +0000 Subject: [PATCH 13/22] no-mistakes(review): Merge Graphify PreToolUse hook --- .codex/hooks.json | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index 7089ced0f..e51d59dd8 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -24,6 +24,11 @@ "type": "command", "command": "bash -lc 'payload=$(cat 2>/dev/null || true); [ -n \"$payload\" ] || exit 0; command -v jq >/dev/null 2>&1 || exit 0; root=$(pwd -P) || exit 0; [ -x \"$root/bin/fm-cd-pretool-check.sh\" ] || exit 0; [ -f \"$root/AGENTS.md\" ] || exit 0; [ -f \"$root/.codex/hooks.json\" ] || exit 0; jq -e \"any(.hooks.PreToolUse[]?.hooks[]?.command?; type == \\\"string\\\" and contains(\\\"fm-cd-pretool-check.sh\\\"))\" \"$root/.codex/hooks.json\" >/dev/null 2>&1 || exit 0; printf \"%s\" \"$payload\" | \"$root/bin/fm-cd-pretool-check.sh\"'", "timeout": 10 + }, + { + "type": "command", + "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'", + "timeout": 10 } ] } @@ -38,18 +43,6 @@ } ] } - ], - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "bash -lc 'command -v graphify >/dev/null 2>&1 || exit 0; graphify hook-check'", - "timeout": 10 - } - ] - } ] } } From 3e05f3b458c224b8cbeb455b9de12b249ca232c5 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Sat, 18 Jul 2026 15:49:58 +0000 Subject: [PATCH 14/22] no-mistakes(test): Captain, fix firstmate test regressions --- bin/fm-afk-launch.sh | 2 +- bin/fm-brief.sh | 2 +- tests/fm-cd-pretool-check.test.sh | 4 ++ tests/fm-pi-watch-extension.test.sh | 100 +++++++++++++++++++--------- tests/fm-turnend-guard.test.sh | 47 +++++++++++-- 5 files changed, 115 insertions(+), 40 deletions(-) diff --git a/bin/fm-afk-launch.sh b/bin/fm-afk-launch.sh index a6ddcbcc6..3cb886449 100755 --- a/bin/fm-afk-launch.sh +++ b/bin/fm-afk-launch.sh @@ -603,10 +603,10 @@ fm_afk_launch_stop() { fm_afk_launch_main() { local result - fm_afk_launch_lock_acquire || return 1 trap fm_afk_launch_lock_release EXIT trap 'exit 130' INT trap 'exit 143' TERM + fm_afk_launch_lock_acquire || return 1 case "${1:-start}" in start) fm_afk_launch_start ;; start-native) fm_afk_launch_start_native ;; diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 56f3008d3..98c564cc1 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -129,7 +129,7 @@ else PROJECT_CLONES_NOTE="The projects above are local clones for work you supervise; they are not an exclusive ownership claim." fi cat > "$BRIEF" </dev/null 2>&1; then + pass "shellcheck not installed, skipping" + return + fi shellcheck "$ROOT/bin/fm-cd-pretool-check.sh" >/dev/null 2>&1 \ || fail "bin/fm-cd-pretool-check.sh is not shellcheck-clean" pass "bin/fm-cd-pretool-check.sh is shellcheck-clean" diff --git a/tests/fm-pi-watch-extension.test.sh b/tests/fm-pi-watch-extension.test.sh index 0372684a9..484a50d11 100755 --- a/tests/fm-pi-watch-extension.test.sh +++ b/tests/fm-pi-watch-extension.test.sh @@ -28,6 +28,38 @@ export const Type = { JS } +pi_watch_extension_js_loader() { + cat <<'JS' +import { readFileSync } from "node:fs"; + +export async function loadPiWatchExtension(plugin) { + let source = readFileSync(plugin, "utf8"); + source = source + .replace(/^import type .*;\n/gm, "") + .replace('import { Type } from "typebox";', 'const Type = { Object(properties) { return { type: "object", properties, additionalProperties: false }; } };') + .replace("const extensionFile = fileURLToPath(import.meta.url);", `const extensionFile = ${JSON.stringify(plugin)};`) + .replace(/type ArmResult = \{[\s\S]*?\};\n\n/, "") + .replace(/type LockOwnership = [^\n]+\n\n/, "") + .replace(/export default function \(pi: ExtensionAPI\)/, "export default function (pi)") + .replace(/let child: any = null;/, "let child = null;") + .replace(/function parentPid\(pid: string\): string/, "function parentPid(pid)") + .replace(/function pidAlive\(pid: string\): boolean/, "function pidAlive(pid)") + .replace(/function lockOwnership\(\): LockOwnership/, "function lockOwnership()") + .replace(/function markLoaded\(\): void/, "function markLoaded()") + .replace(/async function sendWake\(message: string\)/, "async function sendWake(message)") + .replace(/function stopArm\(\): void/, "function stopArm()") + .replace(/function actionableLine\(output: string\): string/, "function actionableLine(output)") + .replace(/function failureLine\(stdout: string, stderr: string, code: number \| null\): string/, "function failureLine(stdout, stderr, code)") + .replace(/function startArm\(\): ArmResult/, "function startArm()") + .replace(/\(chunk: Buffer\)/g, "(chunk)") + .replace(/\(code: number \| null\)/g, "(code)") + .replace(/\(error: Error\)/g, "(error)") + .replace(/handler: async \(_args, ctx\) =>/, "handler: async (_args, ctx) =>"); + return import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); +} +JS +} + test_tracked_extension_present_and_self_hashing() { local text expected_config_source expected_config_source="config_dir=\\\"\${FM_CONFIG_OVERRIDE:-\$FM_HOME/config}\\\"" @@ -79,19 +111,24 @@ test_spawn_template_mentions_pi_watch_placeholder() { } test_pi_extension_reports_external_healthy_watcher() { - local repo home plugin out status + local repo home plugin loader out status repo="$TMP_ROOT/pi-external-healthy-root" home="$TMP_ROOT/pi-external-healthy-home" mkdir -p "$repo/bin" "$home/state" "$home/config" install_pi_watch_extension_fixture "$repo" plugin="$repo/.pi/extensions/fm-primary-pi-watch.ts" + loader="$repo/pi-watch-loader.mjs" + pi_watch_extension_js_loader > "$loader" cat > "$repo/bin/fm-watch-arm.sh" <<'SH' #!/usr/bin/env bash printf 'watcher: healthy pid=1 (beacon 0s)\n' SH chmod +x "$repo/bin/fm-watch-arm.sh" - out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$plugin" LOADER="$loader" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' import { writeFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +const { loadPiWatchExtension } = await import(pathToFileURL(process.env.LOADER).href); let handler = null; let notification = ""; @@ -107,24 +144,7 @@ const pi = { }, }; writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`); -let source = await import("node:fs").then(({ readFileSync }) => readFileSync(process.env.PLUGIN, "utf8")); -source = source - .replace(/type ArmResult = \{[\s\S]*?\};\n\n/, "") - .replace(/type LockOwnership = [^\n]+\n\n/, "") - .replace(/export default function \(pi: any\)/, "export default function (pi)") - .replace(/let child: any = null;/, "let child = null;") - .replace(/function parentPid\(pid: string\): string/, "function parentPid(pid)") - .replace(/function pidAlive\(pid: string\): boolean/, "function pidAlive(pid)") - .replace(/function lockOwnership\(\): LockOwnership/, "function lockOwnership()") - .replace(/function sessionOwnsLock\(\): boolean/, "function sessionOwnsLock()") - .replace(/async function sendWake\(message: string\)/, "async function sendWake(message)") - .replace(/function actionableLine\(output: string\): string/, "function actionableLine(output)") - .replace(/function failureLine\(stdout: string, stderr: string, code: number \| null\): string/, "function failureLine(stdout, stderr, code)") - .replace(/function startArm\(\): ArmResult/, "function startArm()") - .replace(/\(chunk: Buffer\)/g, "(chunk)") - .replace(/\(code: number \| null\)/g, "(code)") - .replace(/\(error: Error\)/g, "(error)"); -const mod = await import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); +const mod = await loadPiWatchExtension(process.env.PLUGIN); mod.default(pi); if (!handler) { console.error("Pi watch command was not registered"); @@ -169,21 +189,25 @@ EOF } test_pi_tool_returns_agent_tool_result() { - local repo home plugin out status + local repo home plugin loader out status repo="$TMP_ROOT/pi-tool-result-root" home="$TMP_ROOT/pi-tool-result-home" mkdir -p "$repo/bin" "$home/state" "$home/config" install_pi_watch_extension_fixture "$repo" plugin="$repo/.pi/extensions/fm-primary-pi-watch.ts" + loader="$repo/pi-watch-loader.mjs" + pi_watch_extension_js_loader > "$loader" cat > "$repo/bin/fm-watch-arm.sh" <<'SH' #!/usr/bin/env bash exit 0 SH chmod +x "$repo/bin/fm-watch-arm.sh" - out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$plugin" LOADER="$loader" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' import { writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; +const { loadPiWatchExtension } = await import(pathToFileURL(process.env.LOADER).href); + let tool = null; const pi = { on() {}, @@ -194,7 +218,7 @@ const pi = { sendUserMessage: async () => {}, }; writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`); -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiWatchExtension(process.env.PLUGIN); mod.default(pi); if (!tool) throw new Error("Pi watch tool was not registered"); if (tool.label !== "Arm firstmate watcher") throw new Error(`unexpected label: ${tool.label}`); @@ -218,23 +242,27 @@ EOF } test_pi_arm_distinguishes_session_lock_ownership() { - local repo home plugin log out status + local repo home plugin loader log out status repo="$TMP_ROOT/pi-lock-ownership-root" home="$TMP_ROOT/pi-lock-ownership-home" log="$TMP_ROOT/pi-lock-ownership.log" mkdir -p "$repo/bin" "$home/state" "$home/config" install_pi_watch_extension_fixture "$repo" plugin="$repo/.pi/extensions/fm-primary-pi-watch.ts" + loader="$repo/pi-watch-loader.mjs" + pi_watch_extension_js_loader > "$loader" cat > "$repo/bin/fm-watch-arm.sh" <<'SH' #!/usr/bin/env bash printf 'arm\n' >> "${FM_ARM_LOG:?}" SH chmod +x "$repo/bin/fm-watch-arm.sh" - out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" FM_ARM_LOG="$log" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$plugin" LOADER="$loader" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" FM_ARM_LOG="$log" node --input-type=module 2>&1 <<'EOF' import { existsSync, unlinkSync, writeFileSync } from "node:fs"; import { spawn } from "node:child_process"; import { pathToFileURL } from "node:url"; +const { loadPiWatchExtension } = await import(pathToFileURL(process.env.LOADER).href); + let tool = null; const pi = { on() {}, @@ -244,7 +272,7 @@ const pi = { }, sendUserMessage: async () => {}, }; -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiWatchExtension(process.env.PLUGIN); mod.default(pi); if (!tool) throw new Error("Pi watch tool was not registered"); @@ -299,17 +327,21 @@ EOF } test_pi_process_exit_cleanup_listener_lifecycle() { - local repo home plugin out status + local repo home plugin loader out status repo="$TMP_ROOT/pi-exit-listener-root" home="$TMP_ROOT/pi-exit-listener-home" mkdir -p "$repo/bin" "$home/state" "$home/config" install_pi_watch_extension_fixture "$repo" plugin="$repo/.pi/extensions/fm-primary-pi-watch.ts" + loader="$repo/pi-watch-loader.mjs" + pi_watch_extension_js_loader > "$loader" : > "$repo/bin/fm-watch-arm.sh" chmod +x "$repo/bin/fm-watch-arm.sh" - out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$plugin" LOADER="$loader" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" node --input-type=module 2>&1 <<'EOF' import { pathToFileURL } from "node:url"; +const { loadPiWatchExtension } = await import(pathToFileURL(process.env.LOADER).href); + const handlers = new Map(); const pi = { on(event, handler) { @@ -320,7 +352,7 @@ const pi = { sendUserMessage: async () => {}, }; const before = process.listenerCount("exit"); -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiWatchExtension(process.env.PLUGIN); mod.default(pi); if (process.listenerCount("exit") !== before + 1) { throw new Error("Pi extension did not install exactly one process-exit fallback"); @@ -338,7 +370,7 @@ EOF } test_pi_process_exit_cleanup_stops_arm_child() { - local repo home plugin cleanup_log pid_file out status pid i + local repo home plugin loader cleanup_log pid_file out status pid i repo="$TMP_ROOT/pi-process-exit-root" home="$TMP_ROOT/pi-process-exit-home" cleanup_log="$TMP_ROOT/pi-process-exit-cleaned" @@ -346,6 +378,8 @@ test_pi_process_exit_cleanup_stops_arm_child() { mkdir -p "$repo/bin" "$home/state" "$home/config" install_pi_watch_extension_fixture "$repo" plugin="$repo/.pi/extensions/fm-primary-pi-watch.ts" + loader="$repo/pi-watch-loader.mjs" + pi_watch_extension_js_loader > "$loader" cat > "$repo/bin/fm-watch-arm.sh" <<'SH' #!/usr/bin/env bash trap 'printf "cleaned\n" > "$FM_CLEANUP_LOG"; exit 0' TERM @@ -353,10 +387,12 @@ printf '%s\n' "$$" > "$FM_CHILD_PID_FILE" while :; do sleep 1; done SH chmod +x "$repo/bin/fm-watch-arm.sh" - out=$(PLUGIN="$plugin" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" FM_CLEANUP_LOG="$cleanup_log" FM_CHILD_PID_FILE="$pid_file" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$plugin" LOADER="$loader" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" FM_CLEANUP_LOG="$cleanup_log" FM_CHILD_PID_FILE="$pid_file" node --input-type=module 2>&1 <<'EOF' import { existsSync, writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; +const { loadPiWatchExtension } = await import(pathToFileURL(process.env.LOADER).href); + let tool = null; const pi = { on() {}, @@ -367,7 +403,7 @@ const pi = { sendUserMessage: async () => {}, }; writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`); -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiWatchExtension(process.env.PLUGIN); mod.default(pi); await tool.execute("tool-call-exit", {}, undefined, undefined, {}); for (let i = 0; i < 250 && !existsSync(process.env.FM_CHILD_PID_FILE); i += 1) { diff --git a/tests/fm-turnend-guard.test.sh b/tests/fm-turnend-guard.test.sh index d71f50bb0..6a10ac1c9 100755 --- a/tests/fm-turnend-guard.test.sh +++ b/tests/fm-turnend-guard.test.sh @@ -98,6 +98,33 @@ install_guard_scripts() { chmod +x "$dir/bin/fm-turnend-guard.sh" "$dir/bin/fm-turnend-guard-grok.sh" "$dir/bin/fm-supervision-instructions.sh" "$dir/bin/fm-harness.sh" } +pi_turnend_extension_js_loader() { + cat <<'JS' +import { readFileSync } from "node:fs"; + +export async function loadPiTurnendExtension(plugin) { + let source = readFileSync(plugin, "utf8"); + source = source + .replace(/^import type .*;\n/gm, "") + .replace("const extensionFile = fileURLToPath(import.meta.url);", `const extensionFile = ${JSON.stringify(plugin)};`) + .replace(/type LockOwnership = [^\n]+\n\n/, "") + .replace(/export default function \(pi: ExtensionAPI\)/, "export default function (pi)") + .replace(/function parentPid\(pid: string\): string/, "function parentPid(pid)") + .replace(/function pidAlive\(pid: string\): boolean/, "function pidAlive(pid)") + .replace(/function lockOwnership\(\): LockOwnership/, "function lockOwnership()") + .replace(/function markLoaded\(\): void/, "function markLoaded()") + .replace(/function runSessionstartNudge\(\): string/, "function runSessionstartNudge()") + .replace(/function runGuard\(\): Promise<\{ code: number; stderr: string \}>/, "function runGuard()") + .replace(/function runChecker\(script: string, command: string\): Promise<\{ code: number; stderr: string \}>/, "function runChecker(script, command)") + .replace(/function runPretoolCheck\(command: string\): Promise<\{ code: number; stderr: string \}>/, "function runPretoolCheck(command)") + .replace(/function runCdCheck\(command: string\): Promise<\{ code: number; stderr: string \}>/, "function runCdCheck(command)") + .replace(/\(event as \{ reason\?: unknown \}\)/g, "event") + .replace(/\(event.input as \{ command\?: unknown \}\)/g, "event.input"); + return import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); +} +JS +} + mark_codex_hook_root() { local dir=$1 mkdir -p "$dir/.codex" @@ -767,13 +794,15 @@ test_pi_extension_forces_followup() { } test_pi_extension_injects_once_per_logical_agent_run() { - local repo home ext log out status + local repo home ext loader log out status repo="$TMP_ROOT/pi-logical-run-root" home="$TMP_ROOT/pi-logical-run-home" ext="$repo/.pi/extensions/fm-primary-turnend-guard.ts" log="$TMP_ROOT/pi-logical-run-guard.log" mkdir -p "$repo/.pi/extensions" "$repo/bin" "$home/state" cp "$ROOT/.pi/extensions/fm-primary-turnend-guard.ts" "$ext" + loader="$repo/pi-turnend-loader.mjs" + pi_turnend_extension_js_loader > "$loader" cat > "$repo/bin/fm-turnend-guard.sh" <<'SH' #!/usr/bin/env bash cat >/dev/null @@ -786,10 +815,12 @@ SH exit 0 SH chmod +x "$repo/bin/fm-turnend-guard.sh" "$repo/bin/fm-arm-pretool-check.sh" - out=$(PLUGIN="$ext" FM_HOME="$home" FM_GUARD_LOG="$log" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$ext" LOADER="$loader" FM_HOME="$home" FM_GUARD_LOG="$log" node --input-type=module 2>&1 <<'EOF' import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; +const { loadPiTurnendExtension } = await import(pathToFileURL(process.env.LOADER).href); + const handlers = new Map(); let prompts = 0; const pi = { @@ -803,7 +834,7 @@ const pi = { await handlers.get("agent_settled")?.({ type: "agent_settled" }, {}); }, }; -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiTurnendExtension(process.env.PLUGIN); mod.default(pi); if (handlers.has("turn_end")) throw new Error("guard still treats internal Pi turns as logical runs"); const settled = handlers.get("agent_settled"); @@ -829,12 +860,14 @@ EOF } test_pi_extension_retries_after_followup_delivery_failure() { - local repo home ext out status + local repo home ext loader out status repo="$TMP_ROOT/pi-delivery-failure-root" home="$TMP_ROOT/pi-delivery-failure-home" ext="$repo/.pi/extensions/fm-primary-turnend-guard.ts" mkdir -p "$repo/.pi/extensions" "$repo/bin" "$home/state" cp "$ROOT/.pi/extensions/fm-primary-turnend-guard.ts" "$ext" + loader="$repo/pi-turnend-loader.mjs" + pi_turnend_extension_js_loader > "$loader" cat > "$repo/bin/fm-turnend-guard.sh" <<'SH' #!/usr/bin/env bash cat >/dev/null @@ -846,9 +879,11 @@ SH exit 0 SH chmod +x "$repo/bin/fm-turnend-guard.sh" "$repo/bin/fm-arm-pretool-check.sh" - out=$(PLUGIN="$ext" FM_HOME="$home" node --input-type=module 2>&1 <<'EOF' + out=$(PLUGIN="$ext" LOADER="$loader" FM_HOME="$home" node --input-type=module 2>&1 <<'EOF' import { pathToFileURL } from "node:url"; +const { loadPiTurnendExtension } = await import(pathToFileURL(process.env.LOADER).href); + const handlers = new Map(); let attempts = 0; const pi = { @@ -861,7 +896,7 @@ const pi = { await handlers.get("agent_settled")?.({ type: "agent_settled" }, {}); }, }; -const mod = await import(pathToFileURL(process.env.PLUGIN).href); +const mod = await loadPiTurnendExtension(process.env.PLUGIN); mod.default(pi); const settled = handlers.get("agent_settled"); await settled({ type: "agent_settled" }, {}); From 5e71fc60a2c1d5d327d8ad683962dbfc89d14e3b Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Sat, 18 Jul 2026 16:14:57 +0000 Subject: [PATCH 15/22] no-mistakes(document): Sync Codex Graphify docs --- AGENTS.md | 4 ++-- docs/configuration.md | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 44330b35d..e4a4df318 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ CONTRIBUTING.md contributor workflow and repo conventions README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed .tasks.toml tracked tasks-axi markdown backend config for the default backlog backend (section 10) -.codex/ tracked Codex primary profile plus Stop and Graphify PreToolUse hooks (docs/configuration.md "Codex profile and Graphify hooks") +.codex/ tracked Codex primary profile plus SessionStart, PreToolUse, and Stop hooks (docs/configuration.md "Codex profile and Graphify hooks") .agents/skills/ firstmate-loaded internal skills, committed; each carries metadata.internal=true for installers .claude/skills symlink to .agents/skills for claude compatibility skills/ standalone public installer-facing skills, committed; not loaded by firstmate @@ -481,7 +481,7 @@ When updating this file, preserve every safety boundary and keep the always-load ## Graphify -This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. +This project may have a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. diff --git a/docs/configuration.md b/docs/configuration.md index 2a4faec9d..e0bfe8357 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -114,10 +114,12 @@ It intentionally mirrors the behavior-test baseline in [`.github/workflows/ci.ym The tracked `.codex/config.toml` is the repo-local Codex Firstmate profile. It sets `sandbox_mode = "workspace-write"`, `approval_policy = "on-request"`, and `approvals_reviewer = "auto_review"` so Codex keeps a workspace sandbox, escalates blocked boundary work, and uses auto review for approval decisions. -The tracked `.codex/hooks.json` has two project hooks. +The tracked `.codex/hooks.json` has `SessionStart`, `PreToolUse`, and `Stop` project hooks. +Its `SessionStart` hook is the Codex integration for `bin/fm-sessionstart-nudge.sh`; see [`docs/sessionstart-nudge.md`](sessionstart-nudge.md) for the full native session-start nudge contract. Its `Stop` hook is the Codex integration for `bin/fm-turnend-guard.sh`; see [`docs/turnend-guard.md`](turnend-guard.md) for the full primary turn-end supervision contract. -Its `PreToolUse` Bash hook is a fail-open Graphify integration: if `graphify` is not on `PATH`, the hook exits successfully; otherwise it runs `graphify hook-check` with a ten-second timeout. -The hook is intentionally portable and bounded so Codex tool use is not blocked by a missing Graphify install or a slow hook. +Its `PreToolUse` hooks run the watcher-arm and cd-guard seatbelts plus a fail-open Graphify check. +The Graphify hook exits successfully if `graphify` is not on `PATH`; otherwise it runs `graphify hook-check` with a ten-second timeout. +That hook is intentionally portable and bounded so Codex tool use is not blocked by a missing Graphify install or a slow hook. `graphify-out/` is local, generated Graphify state and stays gitignored. Dirty files under `graphify-out/` are expected after hooks or incremental updates and are not a reason to skip Graphify-assisted navigation when `graphify-out/graph.json` exists. From a47bd22e47f32bea44ca1106b0e3348a9d00f675 Mon Sep 17 00:00:00 2001 From: Crew Agent Date: Sat, 18 Jul 2026 16:25:25 +0000 Subject: [PATCH 16/22] no-mistakes(lint): Captain, fix unused AFK loop counter --- bin/fm-afk-launch.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fm-afk-launch.sh b/bin/fm-afk-launch.sh index 3cb886449..23f4a6f59 100755 --- a/bin/fm-afk-launch.sh +++ b/bin/fm-afk-launch.sh @@ -287,8 +287,8 @@ fm_afk_launch_commit_terminal() { # [already-recorde } fm_afk_launch_herdr_recover_created() { #