diff --git a/.claude/settings.json b/.claude/settings.json index 0be379c46b..8041f94cb6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -51,5 +51,6 @@ ] } ] - } + }, + "autoCompactEnabled": true } diff --git a/.pi/extensions/fm-calm.ts b/.pi/extensions/fm-calm.ts index eb009fd8e3..7fa7c825af 100644 --- a/.pi/extensions/fm-calm.ts +++ b/.pi/extensions/fm-calm.ts @@ -1,6 +1,6 @@ // Firstmate's home-persistent Pi transcript presentation toggle. // -// Compatibility boundary: Pi 0.81.1 and 0.82.0 expose built-in ToolDefinitions, per-slot +// Compatibility boundary: Pi 0.81.1 through 0.82.1 expose built-in ToolDefinitions, per-slot // renderers, renderShell: "self", session_start replacement reasons, // ExtensionUIContext.setToolsExpanded(), setWorkingVisible(), and // setHiddenThinkingLabel(). The focused tests pin those assumptions. Version-bounded diff --git a/.pi/extensions/lib/fm-calm-operational-user-layout.ts b/.pi/extensions/lib/fm-calm-operational-user-layout.ts index 82c69eda01..4167c6055d 100644 --- a/.pi/extensions/lib/fm-calm-operational-user-layout.ts +++ b/.pi/extensions/lib/fm-calm-operational-user-layout.ts @@ -1,4 +1,4 @@ -// Pi 0.81.1 and 0.82.0 add the ordinary-user spacer and row together. +// Pi 0.81.1 through 0.82.1 add the ordinary-user spacer and row together. // This version-bounded adapter changes only that presentation and never message delivery. import { InteractiveMode, diff --git a/AGENTS.md b/AGENTS.md index 82bcbe8e55..1d0e44e78f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,11 @@ Use light nautical seasoning only when it fits: the occasional "aye", "on deck", Keep that seasoning optional and never let it obscure technical content; never use it in commits, briefs, PRs, or anything crewmates or other tools read; drop the playful flavor entirely when delivering bad news or relaying serious findings. For captain-facing escalation style and outcome phrasing, see section 9. +Voice is opted in for this home and carries the interaction, not chat text (captain's explicit preference, 2026-07-19, reinforced 2026-07-20 with "I need to hear you or else I won't use you that much" - take that at face value, this is a retention-critical preference, not a nice-to-have): the captain should know what's going on from the spoken line alone, and only read chat for detail, a reminder, or a reference. +Run Alfred's global NATIVE-mode voice protocol (`~/.claude/CLAUDE.md`, acknowledge / update / conclude) on every response even while operating as first mate. Bias toward speaking: fire a spoken line, written like a standalone CONCLUDE (self-sufficient, no "read the text for context"), on every turn that carries any real content - a wake handled, a PR opened, a merge landed, a worker dispatched or blocked, a decision surfaced, a finding, a plan, an answer to a question. Only skip the voice call on a genuinely contentless re-arm cycle (an idle-pane flag with zero news, back to back with the last one) - when in doubt, speak. +Every spoken line still passes through this file's own rules first: address the captain as above, and translate through section 9's outcome language before speaking it, exactly as if it were chat text. +Never speak an internal term section 9 would forbid in chat. + ## 1. Identity and prime directives You are the captain's only point of contact for all software work across all of their projects. @@ -37,7 +42,7 @@ 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`, `.github/workflows/`, `bin/`, `.agents/skills/`, `evals/`, 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. Ship shared tracked changes through this repo's no-mistakes pipeline and PR path, with the same merge authority as any other project. @@ -62,6 +67,7 @@ README.md public overview and development notes .claude/skills symlink to .agents/skills for claude compatibility skills/ standalone public installer-facing skills, committed; not loaded by firstmate bin/ helper scripts, committed; read each script's header before first use +evals/ shared crewmate self-grading checks, committed; ported from the Modern AI Productivity Pack (resources/modern-ai-productivity-pack/03-evals/ in the command-center repo); see evals/README.md; run_eval.sh works standalone and bin/fm-brief.sh's ship-mode scaffold wires the relevant eval(s) into the generated Definition of done .env optional X-mode pairing token; LOCAL, gitignored; presence-gates section 14 config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate. Inherited as the literal file: a concrete primary adapter value also controls a secondmate home's own crewmates (section 4) config/crew-dispatch.json optional crewmate dispatch profiles; LOCAL, gitignored; firstmate-maintained but human-editable natural-language rules that choose a per-task harness/model/effort profile (section 4). Inherited by secondmate homes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c3a1cab18..79bceaedb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ 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`, `.github/workflows/`, `bin/`, `.agents/skills/`, `evals/`, 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. 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"). @@ -56,7 +56,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star ## Development -Tracked changes to firstmate itself - `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. +Tracked changes to firstmate itself - `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, `evals/`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. Before making any such change, load the agent-only `firstmate-coding-guidelines` skill (`.agents/skills/firstmate-coding-guidelines/SKILL.md`). It has the knowledge-placement rules that keep `AGENTS.md` from regrowing after each diet pass. There is no reliable way for `bin/fm-brief.sh`'s scaffold to detect that a task's repo is firstmate itself, so firstmate adds this skill's load line to firstmate-repo briefs by hand. diff --git a/Plans/bubbly-stargazing-quilt.md b/Plans/bubbly-stargazing-quilt.md new file mode 100644 index 0000000000..42296bb558 --- /dev/null +++ b/Plans/bubbly-stargazing-quilt.md @@ -0,0 +1,45 @@ +# Merge firstmate's operating discipline into Alfred (global) + +## Status: COMPLETE (verified 2026-07-27) + +All four items shipped, confirmed by directly reading the live files rather than trusting memory: +- `~/.claude/SOUL.md` #9 — captain-facing translation rule, broadened to cover both delegated work and Alfred's own tool use. +- `~/.claude/SOUL.md` #10 — "Investigation is evidence, not authorization" (diagnostic-reasoning discipline). +- `~/.claude/SOUL.md` #11 — "Never let an open decision go silent" (decision-hold-lifecycle). +- `~/.claude/CLAUDE.md` — "KNOWLEDGE-PLACEMENT DISCIPLINE" rule, correctly cross-referencing SOUL.md rather than duplicating the behavioral commitments. + +## Context + +Brandon has been running this session with me acting *as* firstmate — a separate persona/repo (`ops/firstmate/`, loaded from its own `AGENTS.md`) that orchestrates a fleet of crewmate agents with heavy operational discipline: supervision loops, receipt-on-done rigor, plain-language escalation, project-write boundaries, decision tracking. He liked how that discipline felt and wants Alfred — his always-on assistant across every project — to run with "all of firstmate's best and most vital systems and logic," permanently, not just inside this repo. + +I read Alfred's actual global files (`~/.claude/IDENTITY.md`, `SOUL.md`, `USER.md`) to see what's already there before proposing changes. Finding: **a lot of firstmate's discipline is already ported.** Command-center's project `CLAUDE.md` already has "DEFINITION OF DONE — TWO GATES" (Schema-First + Receipt-on-Done, mirroring firstmate's Ground-Truth-Contract), "NO BLIND TURN-END" (mirrors firstmate's "no turn ends blind while work is under way"), full git safety rules, and a structured memory system (mirrors firstmate's `learnings.md`/`captain.md`/`captain-shared.md`). SOUL.md commitment #9 ("Report delegated work in outcomes and decisions, never in watcher/queue/task-id mechanics") is already a condensed version of firstmate's section 9 translation contract. + +So this isn't a wholesale import — it's identifying the **genuine gaps**: firstmate discipline that is *not yet* codified anywhere in Alfred's global behavior, cleanly portable (doesn't depend on firstmate's fleet-specific infrastructure — worktrees, herdr/treehouse backends, tasks-axi, crewmate spawn/supervise loop, which have no equivalent surface for Alfred and shouldn't be cargo-culted in without the underlying tech). + +## What NOT to port (and why) + +- Crew/task lifecycle mechanics (`fm-spawn.sh`, worktree isolation, herdr/treehouse backends, tasks-axi backlog) — this is fleet-orchestration tooling. Alfred's closest equivalent is the `Agent`/`Workflow` tools, which already have their own dispatch/verification conventions (see the Agent tool's "Trust but verify" guidance already in Alfred's system prompt). +- The full "captain etiquette" jargon-translation table verbatim — firstmate's list translates firstmate-specific terms (worktree, herdr, tasks-axi). Alfred needs the *principle*, not that literal table. +- Watcher/supervision loop (`fm-watch-arm.sh`) — this exists because firstmate runs a fleet with async background completion the primary session must not miss. Alfred's analogous risk is background `Agent`/`Workflow` dispatch, and command-center's CLAUDE.md **already** has "NO BLIND TURN-END" covering exactly this. No new mechanism needed. + +## What to actually add (the real gaps) + +1. **Diagnostic-reasoning discipline** — firstmate's rule that a scout/investigation/audit finding is evidence, never authorization to implement; a separate go-ahead is required before code changes. Alfred's global files have nothing like this today — worth a short, universal addition since Alfred routinely produces analyses/audits across every project. + +2. **Decision-hold-lifecycle** — firstmate never lets an unresolved decision discovered mid-task quietly vanish; it's recorded and tracked to resolution. Alfred has no equivalent principle stated anywhere. Worth adding as a short universal rule: any open question/decision surfaced mid-task gets explicitly named to Brandon before the task is considered done, not buried in a wall of text or silently dropped. + +3. **Sharpen the captain-facing translation contract** — SOUL.md commitment #9 already exists but is narrowly scoped to "delegated work." Broaden it into a general rule: never surface tool/infra mechanics (background task IDs, internal hook names, raw file paths unless needed to act) in conversation with Brandon — always translate to outcome + consequence + next decision. This is the one piece of firstmate's section 9 genuinely worth generalizing. + +4. **Knowledge-placement discipline for Alfred's own memory system** — firstmate's `firstmate-coding-guidelines` skill has a real, useful meta-system: a decision tree for where a new fact belongs (inline in the always-loaded file vs. a skill vs. docs vs. script help), plus a "one-owner rule" (state a contract once, cross-reference everywhere else, never duplicate). Alfred's own auto-memory system (described in this session's system prompt) doesn't yet state this discipline explicitly. Worth adding as a short principle so Alfred's memory files stay lean as they accumulate, instead of the CLAUDE.md-bloat problem firstmate had to fix. + +## Files to change + +- **`~/.claude/SOUL.md`** — extend commitment #9 into the broader translation-contract rule (item 3); add diagnostic-reasoning (item 1) and decision-hold (item 2) as new short commitments (10, 11, 12). +- **`~/.claude/CLAUDE.md`** — add a short "Knowledge placement discipline" note near the memory-system instructions (item 4), pointing back to SOUL.md for the behavioral commitments so the rule isn't duplicated in two places (practicing the one-owner rule while adding it). + +Each addition will be **short** (2-4 lines, matching the existing terse style of SOUL.md's numbered commitments) — not a copy of firstmate's much longer prose, since Alfred's files are deliberately compact and firstmate's own discipline says don't bloat the always-loaded file. + +## Verification + +- Read back the edited `SOUL.md` and `CLAUDE.md` in full to confirm the new commitments read naturally alongside existing ones, use Alfred's existing voice/style, and don't duplicate content already covered by "DEFINITION OF DONE — TWO GATES" / "NO BLIND TURN-END" in command-center's project CLAUDE.md. +- No code changes, no repo behavior changes — this is a persona/instruction-file edit only, so "testing" means confirming the files parse as clean markdown and the numbering/cross-references are consistent. diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 3a28daaa33..95e3e5a005 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -2333,8 +2333,8 @@ fm_backend_herdr_events_capable() { # case "$protocol" in ''|*[!0-9]*) return 1 ;; esac [ "$protocol" -ge "$FM_BACKEND_HERDR_MIN_EVENTS_PROTOCOL" ] || return 1 schema=$(herdr api schema --json 2>/dev/null) || return 1 - printf '%s' "$schema" | grep -Fq 'events.subscribe' || return 1 - printf '%s' "$schema" | grep -Fq 'pane.agent_status_changed' || return 1 + case "$schema" in *events.subscribe*) ;; *) return 1 ;; esac + case "$schema" in *pane.agent_status_changed*) ;; *) return 1 ;; esac return 0 } diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 8125aa2e9e..4b5b7e0677 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -281,6 +281,14 @@ read -r MODE _ <\`. +\`completeness\` applies to every task before Done; add \`tov\` for anything written in Brandon's voice, \`principles\` for a plan/research/recommendation deliverable, \`visual-asset\` for generated imagery, and \`publish-safety\` (last) for anything customer-facing or public. +A FAIL means loop and fix - never report done anyway. +EOF +EVAL_STEP=${EVAL_STEP%$'\n'} + case "$MODE" in direct-PR) SETUP2="" @@ -289,6 +297,7 @@ case "$MODE" in # Definition of done This project ships **direct-PR**: you raise the PR yourself, without the no-mistakes pipeline. The task is complete only when committed on your branch. +$EVAL_STEP When it is implemented and committed, push your branch and open a PR with \`gh-axi\`, then append \`done: PR {url}\` to the status file and stop. Do NOT run /no-mistakes. The configured merge authority decides whether to merge the PR; firstmate relays the outcome. EOF @@ -301,6 +310,7 @@ EOF This project ships **local-only**: no remote, no PR, no pipeline. The task is complete only when committed on your branch \`fm/$ID\`. Do NOT push, do NOT open a PR, do NOT merge. Keep your branch a clean fast-forward onto the current default branch - if \`main\` has advanced, rebase onto it so the eventual merge stays a fast-forward. +$EVAL_STEP When it is implemented and committed, append \`done: ready in branch fm/$ID\` to the status file and stop. The configured merge authority approves the ready branch, then firstmate merges it into local \`main\` through the guarded fast-forward path. EOF @@ -312,6 +322,7 @@ EOF IFS= read -r -d '' DOD < Vercel webhook -> Supabase queue row (pending) +// -> this poller claims it (compare-and-swap on status='pending') and: +// 1. best-effort creates a Notion Task under the matching business +// Project (plan-before-dispatch; skipped gracefully if no mapping +// exists yet for that business - never blocks dispatch) +// 2. scaffolds a crewmate brief via fm-brief.sh +// 3. spawns a crewmate via fm-spawn.sh +// 4. moves the Linear issue to Doing and posts a "starting" comment +// 5. marks the Supabase row dispatched (or failed, with the error kept +// in the row for inspection) +// +// Run manually (one pass) or wire into a periodic wakeup; this script does +// not loop or daemonize itself - one invocation is one pass over pending rows. +// +// Routing is by Linear TEAM, not Project (2026-07-20 restructure: every +// business is now its own Linear team with the standard 5-state/3-label +// shape - see bin/fm-linear-team-scaffold.mjs / fm-linear-team-reshape.mjs - +// rather than one shared team holding a Project per business). The webhook +// is workspace-wide (allPublicTeams) so it fires for issues in any team. + +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const FM_ROOT = path.resolve(fileURLToPath(import.meta.url), "..", ".."); +const COMMAND_CENTER_ROOT = path.resolve(FM_ROOT, "..", ".."); +const ENV_PATH = path.join(COMMAND_CENTER_ROOT, "_config", ".env"); + +// Linear team id -> where a crewmate should work + which Notion Project gets +// the plan-before-dispatch Task. All 13 teams have a Notion Project as of +// 2026-07-21 (the first 9 businesses shipped with the standard Project +// template's Nav+Tasks+Notes views; the other 9 do NOT - Notion's +// template-apply API was reproducibly broken that session, see the +// yellow-callout note on each of those pages and re-apply template +// `1f66723139938114b8bae87d6a26e0f2` once it's confirmed working again). +const TEAM_ROUTING = { + "2371d781-641f-4fb9-9664-812a2b9ba01a": { name: "Command Center", repo: "claude-skills", business: null, notionProject: "https://app.notion.com/p/3a36723139938117832dcdc360491d11" }, + "f4d9d1c1-8cf9-4d17-82da-6a8591bf9544": { name: "BuyBox", repo: "command-center", business: "businesses/on-hold/buybox/", notionProject: "https://app.notion.com/p/3a467231399381298d1dc8d032cc45d5" }, + "69890873-b361-445d-a39f-f9f470d4aa9e": { name: "Firstmate", repo: "claude-skills", business: null, notionProject: "https://app.notion.com/p/3a36723139938117832dcdc360491d11" }, + "92040d34-1452-4921-8e2a-38efa678f9ff": { name: "Cold Email", repo: "command-center", business: "businesses/cold-email/", notionProject: "https://app.notion.com/p/3a467231399381d8a49ae92824b3540f" }, + "3f86ee73-9df5-4eeb-87d5-aa447725adb1": { name: "Upwork", repo: "command-center", business: "businesses/upwork/", notionProject: "https://app.notion.com/p/3a467231399381c7a5f6f009ba3d1979" }, + "2b259fd2-bae3-4574-9d4f-f1932d8483e5": { name: "EpicOS", repo: "command-center", business: "businesses/epic-os/", notionProject: "https://app.notion.com/p/3a467231399381ebbc39fe0cc55cd9bd" }, + "1a76e9c8-2227-4da7-b73d-26b3193e8e41": { name: "UltraStays", repo: "command-center", business: "businesses/ultra-stays/", notionProject: "https://app.notion.com/p/3a467231399381a5b145fcb1122a1d47" }, + "2d7acd3b-f909-4876-aa29-27e9c84880d7": { name: "BrandonQ", repo: "command-center", business: "businesses/brandonq/", notionProject: "https://app.notion.com/p/3a4672313993818f9e39fc271e34fb38" }, + "c7976784-da5b-4459-aea8-465433b30c02": { name: "OmniFlows", repo: "command-center", business: "businesses/omniflows/", notionProject: "https://app.notion.com/p/3a467231399381ae90fcc57e04da16b0" }, + "ee5b177c-7fca-44ee-b2f8-774541ae656a": { name: "MonetizedMind", repo: "command-center", business: "businesses/monetized-mind/", notionProject: "https://app.notion.com/p/124672313993802f9cb2dc949f59947f" }, + "bd9a7a03-d1d6-4cd0-8041-571a7595bd91": { name: "Real Estate", repo: "command-center", business: "businesses/real-estate/", notionProject: "https://app.notion.com/p/3a367231399381648a3ed28272e1e717" }, + "a1b2d189-a7ce-4603-ac63-57c0554376f0": { name: "Acquisitions", repo: "command-center", business: "businesses/acquisitions/", notionProject: "https://app.notion.com/p/3a467231399381f6a3fcc48c46eb8f0d" }, + "a8ea0815-4072-4cbc-9921-4b02462c0fc0": { name: "Content Machine", repo: "command-center", business: "businesses/content-machine/", notionProject: "https://app.notion.com/p/3a46723139938191937ffa5a8c7ff6b0" }, +}; + +const NOTION_TASKS_DATA_SOURCE_ID = "1f667231-3993-81da-a58d-000bdacdb111"; + +function loadEnv(envPath) { + const env = {}; + if (!existsSync(envPath)) return env; + for (const line of readFileSync(envPath, "utf8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq === -1) continue; + env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + return env; +} + +const env = loadEnv(ENV_PATH); +const LINEAR_API_KEY = env.LINEAR_API_KEY; +const SUPABASE_URL = env.BH1VE_SUPABASE_URL; +const SUPABASE_SERVICE_KEY = env.BH1VE_SUPABASE_SERVICE_KEY; +const NOTION_API_KEY = env.NOTION_API_KEY; + +function fail(message) { + process.stderr.write(`fm-linear-poll: ${message}\n`); + process.exitCode = 1; +} + +if (!LINEAR_API_KEY || !SUPABASE_URL || !SUPABASE_SERVICE_KEY) { + fail("missing LINEAR_API_KEY / BH1VE_SUPABASE_URL / BH1VE_SUPABASE_SERVICE_KEY in _config/.env"); + process.exit(1); +} +// NOTION_API_KEY is optional: if absent, createNotionTask logs and skips +// rather than blocking dispatch (Notion documentation matters, but a missing +// key should never be the reason a real crewmate dispatch doesn't happen). + +async function supabase(pathAndQuery, init = {}) { + const res = await fetch(`${SUPABASE_URL}/rest/v1/${pathAndQuery}`, { + ...init, + headers: { + apikey: SUPABASE_SERVICE_KEY, + Authorization: `Bearer ${SUPABASE_SERVICE_KEY}`, + "Content-Type": "application/json", + Prefer: "return=representation", + ...(init.headers || {}), + }, + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`supabase ${init.method || "GET"} ${pathAndQuery} -> ${res.status}: ${body}`); + } + return res.json(); +} + +async function linearGraphQL(query, variables) { + const res = await fetch("https://api.linear.app/graphql", { + method: "POST", + headers: { Authorization: LINEAR_API_KEY, "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + }); + const json = await res.json(); + if (json.errors) throw new Error(`linear graphql error: ${JSON.stringify(json.errors)}`); + return json.data; +} + +const stateIdCache = new Map(); +async function stateIdByName(teamId, name) { + const key = `${teamId}:${name}`; + if (stateIdCache.has(key)) return stateIdCache.get(key); + const data = await linearGraphQL( + `query($teamId: String!) { team(id: $teamId) { states { nodes { id name } } } }`, + { teamId }, + ); + for (const state of data.team.states.nodes) stateIdCache.set(`${teamId}:${state.name}`, state.id); + if (!stateIdCache.has(key)) throw new Error(`no Linear state named "${name}" on team ${teamId}`); + return stateIdCache.get(key); +} + +async function claimPendingRows() { + // Compare-and-swap: only rows still status='pending' actually update, so two + // concurrent poll runs can never both claim the same row. + const claimedAt = new Date().toISOString(); + const rows = await supabase( + `linear_dispatch_queue?status=eq.pending&order=created_at.asc&limit=10`, + ); + const claimed = []; + for (const row of rows) { + const updated = await supabase( + `linear_dispatch_queue?id=eq.${row.id}&status=eq.pending`, + { + method: "PATCH", + body: JSON.stringify({ status: "claimed", claimed_at: claimedAt, claimed_by: "firstmate-primary" }), + }, + ); + if (updated.length > 0) claimed.push(updated[0]); + } + return claimed; +} + +async function markRow(id, fields) { + await supabase(`linear_dispatch_queue?id=eq.${id}`, { method: "PATCH", body: JSON.stringify(fields) }); +} + +function slugify(identifier) { + return `linear-${identifier.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; +} + +function run(cmd, args, opts = {}) { + return execFileSync(cmd, args, { cwd: FM_ROOT, encoding: "utf8", ...opts }); +} + +function notionProjectPageId(notionProjectUrl) { + // Notion page URLs from notion-create-pages/notion-fetch look like + // https://app.notion.com/p/<32-hex-no-dashes>; the raw REST API wants a + // UUID (dashes optional, but insert them for clarity/consistency with how + // Notion itself renders ids elsewhere in this repo's memory files). + const hex = notionProjectUrl.split("/p/")[1]; + if (!hex || hex.length < 32) throw new Error(`unrecognized Notion page URL: ${notionProjectUrl}`); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; +} + +async function createNotionTask(routing, row) { + if (!routing.notionProject) return null; + if (!NOTION_API_KEY) { + process.stdout.write(`notion task skipped for ${row.linear_identifier}: NOTION_API_KEY not set in _config/.env\n`); + return null; + } + // Real creation via Notion's REST API directly (not the notion-workspace + // MCP tools - this is a plain node script, not an agent turn, so it has no + // MCP access). This is Gate 1 of the plan-before-dispatch rule in + // data/captain.md: the Task must actually exist before the crewmate spawns, + // not a request-for-later. + const res = await fetch("https://api.notion.com/v1/pages", { + method: "POST", + headers: { + Authorization: `Bearer ${NOTION_API_KEY}`, + "Notion-Version": "2025-09-03", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + parent: { data_source_id: NOTION_TASKS_DATA_SOURCE_ID }, + properties: { + Name: { title: [{ text: { content: `Linear ${row.linear_identifier}: ${row.title}` } }] }, + Status: { status: { name: "To Do" } }, + Project: { relation: [{ id: notionProjectPageId(routing.notionProject) }] }, + }, + }), + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`notion task create failed: ${res.status} ${body}`); + } + const page = await res.json(); + return page.id; +} + +async function dispatchOne(row) { + const routing = TEAM_ROUTING[row.linear_team_id]; + if (!routing) { + await markRow(row.id, { status: "failed" }); + process.stdout.write(`skipped ${row.linear_identifier}: no TEAM_ROUTING entry for team id "${row.linear_team_id}"\n`); + return; + } + + const taskId = slugify(row.linear_identifier); + try { + await createNotionTask(routing, row); + + run("bin/fm-brief.sh", [taskId, routing.repo]); + const briefPath = path.join(FM_ROOT, "data", taskId, "brief.md"); + const brief = readFileSync(briefPath, "utf8"); + const businessLine = routing.business ? `Business area: \`${routing.business}\`.\n\n` : ""; + const taskText = + `${businessLine}${row.title}\n\n${row.description || "(no description on the Linear issue)"}\n\n` + + `Source: Linear issue ${row.linear_identifier} — ${row.linear_url}\n` + + `When done, the result should also be summarized as a comment back on that Linear issue ` + + `(firstmate posts it after you report done — you do not need Linear access yourself).`; + writeFileSync(briefPath, brief.replace("{TASK}", taskText)); + + run("bin/fm-spawn.sh", [taskId, `projects/${routing.repo}`, "--effort", "medium"]); + + const doingId = await stateIdByName(row.linear_team_id, "Doing"); + await linearGraphQL( + `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success } }`, + { id: row.linear_issue_id, input: { stateId: doingId } }, + ); + await linearGraphQL( + `mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success } }`, + { input: { issueId: row.linear_issue_id, body: `Starting — dispatched to Firstmate crewmate \`${taskId}\`.` } }, + ); + + await markRow(row.id, { status: "dispatched", firstmate_task_id: taskId }); + process.stdout.write(`dispatched ${row.linear_identifier} -> ${taskId}\n`); + } catch (error) { + await markRow(row.id, { status: "failed" }); + process.stdout.write(`failed ${row.linear_identifier}: ${error.message}\n`); + } +} + +async function main() { + const claimed = await claimPendingRows(); + if (claimed.length === 0) { + process.stdout.write("no pending Linear dispatch rows\n"); + return; + } + for (const row of claimed) { + await dispatchOne(row); + } +} + +main().catch((error) => { + fail(error.stack || error.message); + process.exit(1); +}); diff --git a/bin/fm-linear-team-reshape.mjs b/bin/fm-linear-team-reshape.mjs new file mode 100644 index 0000000000..34469fbf33 --- /dev/null +++ b/bin/fm-linear-team-reshape.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +// Apply the standard Firstmate dispatch shape (5-state workflow + 3 labels) to +// an EXISTING Linear team by ID, without creating a new team. Companion to +// fm-linear-team-scaffold.mjs (which creates a brand-new team) — use this one +// when a team already exists (e.g. was created before this pipeline existed). +// +// Usage: node bin/fm-linear-team-reshape.mjs + +import { readFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const FM_ROOT = path.resolve(fileURLToPath(import.meta.url), "..", ".."); +const ENV_PATH = path.join(FM_ROOT, "..", "..", "_config", ".env"); + +function loadEnv(envPath) { + const env = {}; + if (!existsSync(envPath)) return env; + for (const line of readFileSync(envPath, "utf8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq === -1) continue; + env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + return env; +} + +const env = loadEnv(ENV_PATH); +const LINEAR_API_KEY = env.LINEAR_API_KEY; +if (!LINEAR_API_KEY) { + process.stderr.write("fm-linear-team-reshape: missing LINEAR_API_KEY in _config/.env\n"); + process.exit(1); +} + +async function gql(query, variables) { + const res = await fetch("https://api.linear.app/graphql", { + method: "POST", + headers: { Authorization: LINEAR_API_KEY, "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + }); + const json = await res.json(); + if (json.errors) throw new Error(`linear graphql error: ${JSON.stringify(json.errors)}`); + return json.data; +} + +const RENAME_MAP = { Backlog: "Inbox", Todo: "Next", "In Progress": "Doing", "In Review": "Waiting" }; +const WANTED_LABELS = [ + { name: "agent-ready", color: "#5e6ad2", description: "An AI agent MAY work this task once it is also moved to Next" }, + { name: "waiting-on-me", color: "#f2c94c", description: "Agent finished or got stuck, needs Brandon input" }, + { name: "waiting-external", color: "#eb5757", description: "Blocked on a third party" }, +]; + +async function reshapeTeam(teamId) { + const detail = await gql( + `query($id: String!) { team(id: $id) { id name states { nodes { id name type } } labels { nodes { id name } } } }`, + { id: teamId }, + ); + const team = detail.team; + + const states = {}; + for (const state of team.states.nodes) { + const target = RENAME_MAP[state.name]; + if (target) { + await gql( + `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success } }`, + { id: state.id, input: { name: target } }, + ); + states[target] = state.id; + } else if (state.name === "Done") { + states.Done = state.id; + } + } + + for (const label of team.labels.nodes) { + await gql(`mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: label.id }); + } + + const labels = {}; + for (const label of WANTED_LABELS) { + // Skip create if a label with this name somehow survived (shouldn't, since + // we just deleted every label on the team, but stay idempotent on re-run). + const result = await gql( + `mutation($input: IssueLabelCreateInput!) { issueLabelCreate(input: $input) { success issueLabel { id name } } }`, + { input: { teamId, ...label } }, + ); + labels[label.name] = result.issueLabelCreate.issueLabel.id; + } + + return { name: team.name, id: team.id, states, labels }; +} + +async function main() { + const teamId = process.argv[2]; + if (!teamId) { + process.stderr.write("usage: fm-linear-team-reshape.mjs \n"); + process.exit(1); + } + const result = await reshapeTeam(teamId); + process.stdout.write(JSON.stringify(result) + "\n"); +} + +main().catch((error) => { + process.stderr.write(`fm-linear-team-reshape: ${error.stack || error.message}\n`); + process.exit(1); +}); diff --git a/bin/fm-linear-team-scaffold.mjs b/bin/fm-linear-team-scaffold.mjs new file mode 100644 index 0000000000..2f1ccf1830 --- /dev/null +++ b/bin/fm-linear-team-scaffold.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +// Create one or more Linear teams with the standard Firstmate dispatch shape: +// the same 5-state workflow (Inbox/Next/Doing/Waiting/Done) and 3 labels +// (agent-ready/waiting-on-me/waiting-external) as the "Command Center" team, +// so every team is a valid dispatch target for the Modern AI Productivity +// Pack pipeline (see ops/firstmate/data/learnings.md for the full architecture). +// +// Usage: node bin/fm-linear-team-scaffold.mjs "Team One" "Team Two" ... +// Prints one JSON line per created team: {name, id, key, states: {...}, labels: {...}} + +import { readFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const FM_ROOT = path.resolve(fileURLToPath(import.meta.url), "..", ".."); +const ENV_PATH = path.join(FM_ROOT, "..", "..", "_config", ".env"); + +function loadEnv(envPath) { + const env = {}; + if (!existsSync(envPath)) return env; + for (const line of readFileSync(envPath, "utf8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq === -1) continue; + env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + return env; +} + +const env = loadEnv(ENV_PATH); +const LINEAR_API_KEY = env.LINEAR_API_KEY; +if (!LINEAR_API_KEY) { + process.stderr.write("fm-linear-team-scaffold: missing LINEAR_API_KEY in _config/.env\n"); + process.exit(1); +} + +async function gql(query, variables) { + const res = await fetch("https://api.linear.app/graphql", { + method: "POST", + headers: { Authorization: LINEAR_API_KEY, "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + }); + const json = await res.json(); + if (json.errors) throw new Error(`linear graphql error: ${JSON.stringify(json.errors)}`); + return json.data; +} + +const RENAME_MAP = { Backlog: "Inbox", Todo: "Next", "In Progress": "Doing", "In Review": "Waiting" }; +const WANTED_LABELS = [ + { name: "agent-ready", color: "#5e6ad2", description: "An AI agent MAY work this task once it is also moved to Next" }, + { name: "waiting-on-me", color: "#f2c94c", description: "Agent finished or got stuck, needs Brandon input" }, + { name: "waiting-external", color: "#eb5757", description: "Blocked on a third party" }, +]; + +function keyFromName(name) { + const compact = name.toUpperCase().replace(/[^A-Z]/g, ""); + return compact.slice(0, 5) || "TEAM"; +} + +async function scaffoldTeam(name) { + const created = await gql( + `mutation($input: TeamCreateInput!) { teamCreate(input: $input) { success team { id name key } } }`, + { input: { name, key: keyFromName(name), description: `Firstmate dispatch team for ${name}. See resources/modern-ai-productivity-pack.` } }, + ); + const team = created.teamCreate.team; + + const detail = await gql( + `query($id: String!) { team(id: $id) { states { nodes { id name type } } labels { nodes { id name } } } }`, + { id: team.id }, + ); + + const states = {}; + for (const state of detail.team.states.nodes) { + const target = RENAME_MAP[state.name]; + if (target) { + await gql( + `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success } }`, + { id: state.id, input: { name: target } }, + ); + states[target] = state.id; + } else if (state.name === "Done") { + states.Done = state.id; + } + } + + for (const label of detail.team.labels.nodes) { + await gql(`mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: label.id }); + } + + const labels = {}; + for (const label of WANTED_LABELS) { + const result = await gql( + `mutation($input: IssueLabelCreateInput!) { issueLabelCreate(input: $input) { success issueLabel { id name } } }`, + { input: { teamId: team.id, ...label } }, + ); + labels[label.name] = result.issueLabelCreate.issueLabel.id; + } + + return { name: team.name, id: team.id, key: team.key, states, labels }; +} + +async function main() { + const names = process.argv.slice(2); + if (names.length === 0) { + process.stderr.write("usage: fm-linear-team-scaffold.mjs \"Team One\" \"Team Two\" ...\n"); + process.exit(1); + } + for (const name of names) { + const result = await scaffoldTeam(name); + process.stdout.write(JSON.stringify(result) + "\n"); + } +} + +main().catch((error) => { + process.stderr.write(`fm-linear-team-scaffold: ${error.stack || error.message}\n`); + process.exit(1); +}); diff --git a/evals/README.md b/evals/README.md new file mode 100644 index 0000000000..460d188431 --- /dev/null +++ b/evals/README.md @@ -0,0 +1,31 @@ +# Evals + +Standardized checks that a crewmate runs on its own output BEFORE reporting a ship-mode +task done. Fail = loop and fix; never report done on a known-bad deliverable. This is how +the captain stops spending the day doing QA. Ported from the Modern AI Productivity Pack +(`resources/modern-ai-productivity-pack/03-evals/` in the command-center repo) so every +crewmate this firstmate home dispatches can reach the same eval set regardless of target +project — evals live here, not in one project, because firstmate briefs many projects. + +## The set + +| Eval | Grades | Runs when | +|---|---|---| +| `task-definition-eval` | the TASK, not the output | at dispatch (Next + agent-ready) | +| `principles-eval` | the reasoning/approach | any plan, research, or recommendation | +| `tov-eval` | the writing voice | anything written in Brandon Quijano's voice | +| `completeness-eval` | spec adherence | before any task moves to Done | +| `visual-asset-eval` | generated imagery | before renders reach the pick folder | +| `publish-safety-eval` | public/customer exposure | LAST, before anything leaves the building | + +## Run one manually + +```bash +./run_eval.sh tov some_draft.md +./run_eval.sh principles research_report.md +``` + +## The loop + +TASK -> [task-definition] -> crewmate works -> [principles + tov/visual + completeness] -> +fails loop back automatically -> [publish-safety] -> the captain sees only cleared work. diff --git a/evals/completeness-eval.md b/evals/completeness-eval.md new file mode 100644 index 0000000000..5a573b2c59 --- /dev/null +++ b/evals/completeness-eval.md @@ -0,0 +1,19 @@ +# EVAL: Completeness — "Did the deliverable meet the spec?" + +**Applies to:** any deliverable claiming to be done. The eval that runs before an agent moves a task to Done. + +## Checks + +1. **Every stated requirement met.** Walk the original task line by line; each constraint gets a ✓ or ✗ with evidence (quote/path/screenshot). One unaddressed requirement = FAIL. +2. **Claimed artifacts exist.** Every file path, URL, and ID in the summary actually resolves. A summary that says "saved to X" where X doesn't exist = instant FAIL. +3. **Tested, not just built.** If it's code/automation: was it actually run once end-to-end? Output of the run included? +4. **QA-able in under 2 minutes.** The handback includes absolute paths, direct links, and a preview/summary so Nick can verify without reconstructing context. +5. **Failure honesty.** Anything skipped, partial, or flaky is stated plainly at the top, not buried or omitted. + +## Output contract + +``` +VERDICT: DONE | NOT DONE +REQUIREMENTS TABLE: [requirement -> ✓/✗ -> evidence] +UNVERIFIED CLAIMS: [anything asserted but not demonstrated] +``` diff --git a/evals/principles-eval.md b/evals/principles-eval.md new file mode 100644 index 0000000000..796c659fa0 --- /dev/null +++ b/evals/principles-eval.md @@ -0,0 +1,26 @@ +# EVAL: Principles — "Was this done the way Nick thinks?" + +**Applies to:** any completed task, plan, recommendation, or research deliverable. Grades the REASONING and APPROACH, not the prose. + +## The five questions (score each 0-2; total <7 = FAIL) + +1. **First principles.** Did the work reason from the actual mechanics of the problem, or pattern-match to "what people usually do"? Cargo-culted best practices without a stated reason = 0. +2. **EV discipline.** Are the conclusions/choices high expected value: Impact × P(success) / Effort? Was the impact ceiling estimated BEFORE effort was spent (quantify before optimizing)? A polished low-ceiling deliverable = 0. +3. **Nick's time minimized.** Could any part of this have been done WITHOUT Nick in the loop? Is the thing being handed back a decision (good) or homework (bad)? Deliverables that create >5 min of Nick-work when 1 min was possible = 0. +4. **Verified, not plausible.** Was every claim/option checked against reality (docs read, API tested, price confirmed) before being presented? "Sounds right" presented as fact = 0. +5. **Leverage check.** Was there a 10x lever ignored: an existing skill/script, an automation, a delegation to agents, a way to make this reusable instead of one-off? Rebuilding what exists = 0. + +## Auto-fails (regardless of score) + +- Invented statistics or unverified citations. +- Presented an option that is infeasible for Nick's actual constraints (location, stack, existing infra). +- Optimized something whose impact ceiling was never estimated. + +## Output contract + +``` +VERDICT: PASS | FAIL +SCORES: [1..5 with one-line justification each] +BIGGEST MISS: [the single highest-EV improvement to how this was done] +NICK-WORK CREATED: [estimated minutes of Nick's time this deliverable demands] +``` diff --git a/evals/publish-safety-eval.md b/evals/publish-safety-eval.md new file mode 100644 index 0000000000..68e9120c24 --- /dev/null +++ b/evals/publish-safety-eval.md @@ -0,0 +1,19 @@ +# EVAL: Publish Safety — "Can this leave the building?" + +**Applies to:** anything customer-facing or public: community posts, ads, emails to real people, site changes, social posts. Runs LAST, after all other evals pass. + +## Hard gates (any ONE = HOLD FOR NICK) + +1. **Public/customer-facing + not explicitly pre-approved** = HOLD. Agents stop before the send/publish click; Nick fires it. No exceptions, ever. +2. **Credentials/secrets.** No API keys, tokens, internal URLs, .env contents, or client names that weren't already public. +3. **Real numbers only.** Every stat, price, and claim traceable to a source. Invented or misremembered figures = HOLD. +4. **Commitments.** Nothing that promises Nick's time, money, or delivery dates he didn't set. +5. **Identity.** Nothing published AS Nick (his voice, his accounts) that he hasn't read. Drafts yes, sends no. + +## Output contract + +``` +VERDICT: CLEARED | HOLD +GATE TRIGGERED: [which gate and the offending content] +SAFE VERSION: [if fixable by redaction, the redacted version] +``` diff --git a/evals/run_eval.sh b/evals/run_eval.sh new file mode 100755 index 0000000000..cf88dbe158 --- /dev/null +++ b/evals/run_eval.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Run any eval against any deliverable, headless. +# Usage: ./run_eval.sh +# e.g. ./run_eval.sh tov draft_post.md +# The grader is Claude in -p (print) mode; the eval file IS the grading spec. + +set -euo pipefail +EVAL_DIR="$(cd "$(dirname "$0")" && pwd)" +EVAL_FILE="$EVAL_DIR/${1%.md}-eval.md" +[ -f "$EVAL_FILE" ] || EVAL_FILE="$EVAL_DIR/$1.md" +TARGET="$2" + +[ -f "$EVAL_FILE" ] || { echo "no such eval: $1 (looked for $EVAL_FILE)"; exit 1; } +[ -f "$TARGET" ] || { echo "no such file: $TARGET"; exit 1; } + +claude -p "You are an eval grader. Grade the DELIVERABLE strictly against the EVAL SPEC below. Follow the spec's output contract exactly. Be harsh; a borderline case fails. + +=== EVAL SPEC === +$(cat "$EVAL_FILE") + +=== DELIVERABLE === +$(cat "$TARGET")" diff --git a/evals/task-definition-eval.md b/evals/task-definition-eval.md new file mode 100644 index 0000000000..6373a3176a --- /dev/null +++ b/evals/task-definition-eval.md @@ -0,0 +1,19 @@ +# EVAL: Task Definition — "Is this task actually delegatable?" + +**Applies to:** the TASK ITSELF, before an agent picks it up. Run at dispatch time (Next + agent-ready). Being productive today is defining constraints better than everyone else; this eval enforces that. + +## Checklist (any miss = FAIL, bounce back to Nick with questions) + +1. **Done-state is testable.** A stranger could look at the output and say yes/no it's done. "Improve the landing page" = FAIL. "Rewrite the hero section to lead with the $4M→$6M case study, under 40 words" = PASS. +2. **Constraints stated.** What must NOT change, what tools to use/avoid, budget/length/format limits. +3. **Context linked.** The task names its inputs (files, URLs, prior issues) instead of assuming the agent will guess. +4. **Escalation defined.** What the agent should do when blocked: comment and move to Waiting, never silently stall or improvise around a paywall/permission. +5. **Right-sized.** Completable in one agent session (<2h equivalent). Bigger = should be split. + +## Output contract + +``` +VERDICT: DELEGATABLE | BOUNCE +MISSING: [numbered list of what the task needs before an agent can run it] +SHARPENED REWRITE: [the task description rewritten to pass, best guess flagged with ASSUMPTION tags] +``` diff --git a/evals/tov-eval.md b/evals/tov-eval.md new file mode 100644 index 0000000000..89ffbd8ffd --- /dev/null +++ b/evals/tov-eval.md @@ -0,0 +1,84 @@ + + +# EVAL: Tone of Voice — "Does this sound like Brandon?" + +**Applies to:** any written deliverable in Brandon's voice (LinkedIn/X posts, newsletter or +magnum-opus copy, video scripts, community replies, cold email, ad copy). +**Reference corpus:** `shared/content-machine/voice.md` + `tone.md` (master pillars) and the +relevant `/voice.md` overlay for the brand being written for, all in the +command-center repo. Read those before grading, not just this file — this eval is a +compressed checklist, they are the source of truth. + +## Hard fails (any ONE = FAIL) + +1. **Banned vocabulary.** "Leverage" (used unironically), "synergy", "holistic", "empower", + "unlock", "skyrocket", "game-changer"/"game-changing"/"revolutionary", "journey" (unless + a literal client business journey), "in today's [adjective] world/landscape/era", "let me + ask you a question", "I'll never forget the day". One instance = FAIL. +2. **Throat-clearing open.** First sentence is not stakes, a contrarian frame, or a specific + number — e.g. it warms up, apologizes, or scene-sets before getting to the point. +3. **Hedging.** "Perhaps", "potentially", "it could be argued", "I think maybe", "it is + important to note that". Brandon states what he knows and cuts the rest. +4. **Passive voice on Brandon's own actions.** "A framework was built" where it should be "I + built a framework." Operators do things; they don't have things done to them. +5. **Begging-engagement closer.** "What are your thoughts?" / "Drop a 🔥 if this resonates" / + "Let me know if this resonates" with no real question behind it. Closers are a statement + or a specific CTA. +6. **Em-dash overload.** More than one em dash in a single paragraph. (Brandon uses them — + earned, to signal a pivot — not as a tic; one per paragraph max, not zero.) + +## Soft checks (2+ misses = FAIL) + +- **Sentence length.** Working zone is 6-14 words; short-short-then-longer rhythm. A run of + long, unbroken sentences is off. +- **Paragraph length.** 1-4 sentences, and at least one single-sentence paragraph on a + load-bearing line for a piece of any length. +- **Specificity.** Dollar figures, time windows, named mechanics beat vague claims ("$14K/mo" + not "a lot", "in 5 days" not "fast"). +- **At least one of the Five Anchors** visible in anything substantive: the Clock (the + 18-month deadline), the Math (specific numbers), the Gap (a named diagnostic frame, e.g. + "the Delivery Gap"), the Named Mechanic (a proper-noun framework), or In Public (real + deals/numbers as proof, not claims). +- **At least one structural pattern used on purpose**, not accidentally: stakes-first open, + negation stack ("Not X. Not Y. Not Z. [affirmative]"), anaphora (repeated opener across + lines, each carrying new content — not empty rule-of-three padding), a bold single-line + standalone sentence, or a hard finisher. +- **Reads aloud cleanly.** If you can't imagine Brandon saying it out loud without editing, + it's off — voice is the conversation, text is footnotes. +- **Peer register, not guru register.** "Here's what I'd check" / "here's what I ran", never + "you should" or "let me teach you." + +## Notes on what this eval deliberately does NOT hard-fail + +Unlike the original Nick-voice eval, this version does not zero-tolerance em dashes or treat +"It's not X, it's Y" contrast constructions as an automatic tell — Brandon's own reference +docs and sampled writing show him using negation stacks and earned em dashes on purpose as +named structural patterns, not as accidental LLM artifacts. Formalized abbreviations +(tl;dr, imo) and contraction habits are not documented in the source material either way; +do not hard-fail on them until Brandon's own samples confirm a rule. + +## Output contract + +``` +VERDICT: PASS | FAIL +VIOLATIONS: [list each with the offending quote] +REWRITE: [if FAIL: the corrected version, changing ONLY what violated] +``` diff --git a/evals/visual-asset-eval.md b/evals/visual-asset-eval.md new file mode 100644 index 0000000000..91a982edd2 --- /dev/null +++ b/evals/visual-asset-eval.md @@ -0,0 +1,19 @@ +# EVAL: Visual Asset — "Is this render usable?" + +**Applies to:** thumbnails, diagrams, and generated imagery before they reach Nick's pick folder. + +## Checks + +1. **Text renders clean.** No garbled/melted characters, no misspelled labels. Read every word in the image. One garbled label = FAIL (re-roll). +2. **Style contract.** Diagrams: ink editorial (black ink on pure white, serif headlines, no borders). Main-channel thumbnails: TEXTLESS, always (Nick adds text in Canva). +3. **Legible at thumbnail size.** Squint test at ~320px wide: the hero element still reads. +4. **Faithful to the brief.** Every element the description required is present; nothing embarrassing hallucinated in (extra fingers, phantom logos, wrong numbers). +5. **Right shape.** Correct aspect/deliverable format (1280x720 thumbnails, 1920x1080 padded diagrams). + +## Output contract + +``` +VERDICT: PASS | RE-ROLL +FAILED CHECKS: [numbered, with what exactly is wrong] +RE-ROLL NOTES: [what to change in the generation prompt] +``` diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index bed170b741..fa8af209a8 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -507,6 +507,28 @@ test_scout_and_secondmate_load_decision_hold_policy() { pass "fm-brief.sh: investigation and visual-review completions load the shared decision policy" } +# Every ship delivery mode's Definition of done must instruct the crewmate to +# run the relevant eval(s) via evals/run_eval.sh before reporting done, and +# make clear a FAIL means loop and fix rather than reporting done anyway. +test_ship_modes_include_eval_step() { + local home id brief + home="$TMP_ROOT/eval-step-home" + write_registry "$home" + + for id_proj in "brief-eval-nomistakes-e1:no-registry-proj" "brief-eval-directpr-e2:direct-proj" "brief-eval-localonly-e3:local-proj"; do + id=${id_proj%%:*} + proj=${id_proj##*:} + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" "$proj" >/dev/null 2>&1 + brief="$home/data/$id/brief.md" + assert_present "$brief" "$id: brief was not scaffolded" + assert_grep "run_eval.sh" "$brief" \ + "$id: Definition of done is missing the run_eval.sh eval step" + assert_grep "A FAIL means loop and fix" "$brief" \ + "$id: eval step lost the loop-and-fix-on-FAIL instruction" + done + pass "fm-brief.sh: every ship mode's Definition of done wires in the eval step" +} + # Scout and secondmate paths still scaffold well-formed briefs. test_scout_and_secondmate_scaffold() { local brief @@ -531,6 +553,7 @@ test_script_parses test_no_heredoc_in_command_substitution test_help_includes_entire_header test_ship_modes_generate_clean_briefs +test_ship_modes_include_eval_step test_faster_paths_use_configured_authority_without_stacked_review test_no_mistakes_dod_wording test_ship_project_memory_wording diff --git a/tests/fm-calm-pi-extension.test.sh b/tests/fm-calm-pi-extension.test.sh index b63661ad7a..be6b3fb603 100755 --- a/tests/fm-calm-pi-extension.test.sh +++ b/tests/fm-calm-pi-extension.test.sh @@ -16,7 +16,7 @@ PI_OPERATIONAL_INPUT="$ROOT/.pi/extensions/lib/fm-operational-input.ts" PI_PACKAGE_DIR=${FM_PI_PACKAGE_DIR:-"$(npm root -g 2>/dev/null)/@earendil-works/pi-coding-agent"} TMUX_SOCKET="fm-calm-$$" TMUX_SESSION="fm-calm-e2e" -PI_COMPAT_VERSIONS="0.81.1 0.82.0" +PI_COMPAT_VERSIONS="0.81.1 0.82.0 0.82.1" require_pi_compat_version() { local version=$1 context=$2