Skip to content

Finalize the workflow-paradigm rewrite (red-team fixes applied) - #2

Merged
DQ4443 merged 15 commits into
mainfrom
fm/workflow-paradigm-rewrite
Jul 6, 2026
Merged

DQ4443 merged 15 commits into
mainfrom
fm/workflow-paradigm-rewrite

Conversation

@DQ4443

@DQ4443 DQ4443 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

This swaps firstmate's operating model from the tmux-crewmate supervision stack to native Workflow and Agent dispatch. Instead of spawning crewmates in tmux windows and babysitting them with fm-watch.sh, firstmate sizes a Workflow to the task, watches structured returns, and brings you outcomes. The old scripts stay on disk as a documented escape hatch (AGENTS.md section 12); nothing is deleted. Board polling, merge polling, and stall detection move off the hand-armed watcher onto a launchd job (fm-poll.sh, com.firstmate.poller), so "the pollers must never lapse" becomes a property of launchd rather than of a session remembering to arm something. A broadened PreToolUse write fence replaces the old html-only block, and board check-in stamps are written deterministically by the orchestration script.

The four commits on top of the paradigm work are the red-team pass. They fix two findings that undercut the stated guarantees and were not in the builder's own open-issues list. The write fence structurally missed projects/: those are firstmate's own real clones, not the symlinks into ~/dev/work the docs claimed, so the ~/dev/work rule never covered them. And fm-poll.sh trusted a bare kill -0 on its pidfile, so a recycled pid could read as "already running" and wedge the poller in a silent KeepAlive respawn loop that never polls the board. Both are fixed with tests. The rest of the pass reconciles AGENTS.md and docs/scripts.md with what the code actually does, and implements the fm-teardown.sh --worktree mode the docs already named but nobody had built.

Main thing to check before the cutover: fm-write-fence.sh now blocks writes under <FM_ROOT>/projects/** as well as ~/dev/work/**, allowing only the isolated .claude/worktrees/ trees inside each. If any real workflow legitimately writes into a projects/ clone outside a worktree, the fence would block it once wired. I believe nothing does (projects/ has always been read-only to firstmate), but that is the assumption worth a sanity-check.

Seven decisions are baked into this branch, each independently vetoable:

  1. Address is David, plainly. The old "captain" address is out of the operating manual.
  2. The data/captain.md filename is kept as-is. It is historical; renaming it churns every reference for no gain.
  3. The launchd poller (fm-poll.sh) replaces the hand-armed watcher (fm-watch.sh) for daily duty. The watcher stays as the escape hatch and carries the pollers until the cutover.
  4. Budgets come from data/budgets.md, with auto-resume-once on exhaustion and stop-and-ask on a second exhaustion.
  5. The kronos-mvp-tracker sync flow stays the only standing merge authorization. Everything else needs your explicit word. The red team wanted this stripped; I kept it because the tracker-sync skill genuinely grants autonomous merge after your per-run change-list okay, and removing it would have made AGENTS.md contradict the skill. Rule 1 is only clarified to note the per-run okay gates it.
  6. The write fence broadens now, covering all of ~/dev/work and the projects/ clones, not on a later pass.
  7. Red team runs for the standard and large tiers only. A trivial one-liner leans on the no-mistakes review instead.

Go-live is gated, not automatic. The launchd plist ships unloaded with placeholder paths and a header describing the human cutover: substitute the real paths, load the plist, confirm the synthetic startup wake drains, round-trip one real board thread and one action through the durable queue, wire the write fence into local settings, and only then disarm fm-watch.sh. Three smoke tests still to run at that cutover: the synthetic startup wake arrives through the wake queue, a real board event round-trips, and a kill -9 of the poller is restarted by launchd inside the throttle window. None of that has run yet. This branch is the code, not the cutover.

Rollback is clean. Revert this PR, unload the plist with launchctl bootout if it was ever loaded, and re-arm fm-watch.sh. The escape-hatch scripts were never removed, so the old paradigm is one arm command away the entire time.

One caveat on validation: the no-mistakes pipeline did not run. It refuses on a dirty working tree, and this checkout arrived with ten uncommitted files, a prettier reformat of skill and doc markdown (.agents/skills//SKILL.md, README.md, docs/-backend.md, docs/turnend-guard.md, skills/stow/SKILL.md) that I did not create and was not allowed to touch or commit. Rather than stash someone else's in-flight work through a pipeline that rebases, I pushed the branch and opened this PR by hand. What I did verify locally: shellcheck on the exact CI command (bin/.sh bin/backends/.sh tests/.sh) exits clean, bash -n passes on every changed script, the two new test files pass (fm-write-fence, 10 cases; fm-teardown-worktree, 6 cases), and the existing fm-teardown and fm-tangle-guard suites still pass after the teardown change. The full tests/.test.sh e2e suite, which needs tmux, was not run end to end; that is the part the pipeline would have covered. Commit or discard those ten files and git push no-mistakes fm/workflow-paradigm-rewrite will run the whole gate.

kunchenguid and others added 14 commits July 6, 2026 00:37
* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring
* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style
…henguid#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs
* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs
Replace the crewmate/tmux-window dispatch model with in-session dynamic
workflows. Remove harness adapters, crew spawn/teardown/supervision, pane
peeking, stale-crew detection, and multi-backend machinery. Promote the
board-v2 interface and the design/red-team/merge gates to first-class
sections. Preserve firstmate persistence in tmux, the board pollers and
watcher for board input, delivery modes and the no-mistakes gate,
secondmates (flagged for David's confirmation), captain.md-at-startup,
bootstrap/recovery, backlog, self-update, and X mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconcile the d3d3c7a rewrite down to the final workflow-era manual: the
finalA draft as the base, folding in the load-bearing board contract from
d3d3c7a (section semantics as whose-turn, one item one row, MVP-tracker
naming, scan-all-threads-before-seen) and applying the synthesis amendments
(Kronos tickets plus cite-repo-files-by-path in section 3, the built-in
verify then project-verify then no-mistakes done stack in section 5, the
transfer-file maintenance discipline in section 10, the reconciled
escape-hatch and skill dispositions in section 12).

Secondmates are dropped outright; the tmux-crewmate and watcher stack moves
to the documented escape hatch. CLAUDE.md is a symlink to this file.
fm-poll.sh is the launchd-supervised (com.firstmate.poller, KeepAlive)
board/merge/stall poller that replaces the agent-armed watcher for daily
duty: it loops state/*.check.sh on the check contract, delivers wakes
through the durable queue in fm-wake-lib.sh, keeps its own
state/.last-poller-beat beacon and a home-scoped singleton guard, and
injects a synthetic startup check event so a wake confirms the delivery
path end to end.

fm-write-fence.sh is the broadened PreToolUse hook: it blocks writes under
~/dev/work except the agent-worktree allowlist
(~/dev/work/*/.claude/worktrees and ~/.treehouse), failing open on
unreadable payloads.

fm-board-checkin.sh writes a real per-item stamp into
state/board-checkins.json atomically at each phase boundary.

launchd/com.firstmate.poller.plist ships UNLOADED with a header comment:
loading it is the human-verified cutover step, with __FM_ROOT__/__FM_HOME__
placeholders substituted then.
Mark the retire-marked scripts from the disposition table with a DEPRECATED
header: the watcher/guard stack (fm-guard, fm-tangle-lib), the crewmate
scaffolding (fm-brief, fm-promote), the afk daemon (fm-supervise-daemon),
and the secondmate stack (fm-home-seed, fm-backlog-handoff, fm-config-push,
fm-config-inherit-lib, fm-marker-lib).

Each stays functional on disk and is removed only in the cleanup PR after
the Opus 4.8 transition settles, so rollback stays real.
Restructure the toolbelt reference into New, Kept, Escape hatch, Retired,
and Inert sections so each script's disposition is explicit, add the three
new scripts plus the launchd plist and the generated stall check, and note
that nothing is deleted until the cleanup PR.
…links

The projects/<name> entries are firstmate's own real clones, not symlinks into
~/dev/work, so the ~/dev/work rule never covered them. Add an explicit block on
<FM_ROOT>/projects/** (allowing only projects/<name>/.claude/worktrees/**), and
make resolve() walk up to the nearest existing ancestor so a new file created
under a symlinked directory still resolves into the real fenced tree instead of
slipping past as an unresolved literal. Add colocated behavior tests.
…up wake

A bare kill -0 on the pidfile trusted a recycled pid: after a kill -9 left a
stale pidfile whose pid got reused, the launchd instance read it as "already
running" and exited, and KeepAlive + the 10s throttle wedged the poller in a
silent respawn loop that never polled the board. Record the process identity
(start time + command) alongside the pid and honor the pidfile only when the
identity still matches, so a reused pid is treated as dead. Also rate-limit the
synthetic startup wake so a crash-respawn loop cannot flood the durable wake
queue; a genuine restart still emits it.
…ktrees

AGENTS.md sections 4 and 6 name bin/fm-teardown.sh --worktree <path> as the
sanctioned disposal for a changed worktree, but only the task-id path existed;
a workflow worktree has no state/<id>.meta, so the documented command errored.
Add a --worktree mode that runs the same landed check keyed on the worktree
path (refusing dirty or unlanded work, refusing a main checkout, removing only
on a pass or with --force), independent of any meta record. Add colocated tests.
- Rule 2 and section 3: projects/<name> are firstmate's own clones (real dirs),
  not symlinks into ~/dev/work; describe the write fence as wired at the poller
  cutover and best-effort behind isolation (fails open), not present-tense
  sole enforcement. Document the fence-wiring as a hard step of that cutover in
  the plist header, kept out of tracked settings for this shared-template repo.
- Rule 1: the tracker-sync merge authorization is gated on David's per-run
  okay of the change list; keep the standing merge authority the skill grants.
- Budgets and stall detection: mark data/budgets.md and
  state/workflow-runs.check.sh as built at the cutover, with an interim path.
- docs/scripts.md: fence covers projects/ clones; poller pidfile identity check
  and rate-limited startup wake.
@DQ4443
DQ4443 force-pushed the fm/workflow-paradigm-rewrite branch from 7b113ba to d891159 Compare July 6, 2026 07:41
@DQ4443
DQ4443 merged commit 3e32f3a into main Jul 6, 2026
3 of 4 checks passed
@DQ4443
DQ4443 deleted the fm/workflow-paradigm-rewrite branch July 6, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants