Migrate historical /goals + lock down the authoring workflow

Status: ✓ shipped · Date: 2026-05-25 · PR: #47 · Commit: 5ce83fe

Related posts: sama-v2-goal-chain-gap


Goal: Recover every /goal command this session and earlier sessions produced from the PR descriptions, commit bodies, and conversation context that captured them; populate goals/ with one .md per recoverable goal classified honestly by recovery fidelity; mark unrecoverable goals as status: lost in the registry without polluting /goals with stub files; and lock down the authoring workflow so future /goals automatically land as committed files BEFORE any code is written AND get embedded verbatim into the PR body so they survive even if goals/ is later corrupted. Closes the "we lose /goals" gap with eyes open about historical losses.

Extend GoalStatus first (one-line type change):

  • src/a31_goals.ts: type GoalStatus = "pending" | "shipped" | "lossy" | "lost" | "abandoned"
  • Semantics:
    • shipped — /goal text recovered VERBATIM from the PR body or conversation. The goals/.md file contains the exact text the user typed. Audit-grade.
    • lossy — /goal text recovered from conversation context that was itself paraphrased (e.g. the prior-session summary at the start of this conversation). File exists with the recovered text PLUS an explicit note: "> ⚠ Recovered from conversation summary, not verbatim from PR body. Original text may differ in wording from what the user typed."
    • lost — no recoverable text. Entry in ALL_GOALS, NO file on disk. The detail page renders metadata-only (PR link, commit link, related posts) with an honest "the /goal text could not be recovered from any source" message.
    • pending — in-flight goal, not yet merged. mergeSha and prNumber are null.
    • abandoned — the work was started but the PR was closed without merging. Used for future cases, no historical entries expected.

Detail handler change (src/d21_handlers_goals.ts):

  • For status: "lost", DO NOT call Bun.file().exists() → skip the file-read entirely. Render the badges header + a "Source could not be recovered" callout block + the related_posts links. Return 200, not 404.
  • For status: "shipped" and "lossy", keep the existing file-read path. For "lossy", prepend the warning block to the rendered body.

Done when:

  • gh pr list --state merged --limit 30 --json number,title,body,headRefName,mergedAt,mergeCommit fetched and inspected. Every PR classified as one of: a. /goal-driven AND verbatim /goal text in body → shipped b. /goal-driven AND only summary in body BUT recoverable from conversation context → lossy c. /goal-driven AND no recoverable text → lost d. NOT /goal-driven (Containerfile hotfix, image redesigns, blog-post-only PRs, typo fixes) → excluded from migration entirely
  • Classification rule for "is this PR /goal-driven?": the conversation-or-PR-body source text MUST start with literal "Goal:" AND contain "Done when:" AND contain "Constraints" or "Constraints (anti-fudge):" AND contain "Load-bearing files" or "Load-bearing files to read FIRST:". All four markers required — anything missing → not a /goal-driven PR, excluded.
  • For every shipped + lossy entry: a goals/.md file exists with full frontmatter (slug, title, date from mergedAt, branch from headRefName, pr_number, merge_sha as 7-char short, status, related_posts).
  • For every lost entry: a registry-only entry in ALL_GOALS with status: lost, NO file on disk. Detail page renders metadata-only at 200.
  • ALL_GOALS in src/a31_goals.ts contains every classified entry, sorted by date descending. The git-url-drop-owner entry already in ALL_GOALS stays.
  • /goals index shows every entry with its status badge (✓ shipped / ⚠ lossy / ✗ lost / ⏳ pending / ✗ abandoned).
  • /goals/ for a lost goal returns 200 with metadata-only content, not 404.
  • /goals/ for a lossy goal returns 200 with the warning banner prepended to the recovered body.
  • src/b32_goals_meta.test.ts gains 2 new test cases: status: "lossy" parses correctly; status: "lost" parses correctly.

Workflow lock-in (defense-in-depth):

  • Write a NEW feedback memory file at /var/home/scri/.claude/projects/-var-home-scri-Documents-tdd-md/memory/feedback_goal_authoring_workflow.md with this content (paraphrased — the memory file itself should be in your normal feedback-memory style with Why: + How to apply: sections): Rule: When the user fires a /goal slash command, the agent's FIRST tool call (before any Read, Bash, or other Edit) is to write the verbatim /goal body to goals/.md with frontmatter status: pending, merge_sha: null, pr_number: null, date: , branch: , title: , related_posts: []. Commit this on a new branch as the FIRST commit of the PR. Additionally, when creating the PR via gh pr create, the --body MUST include the verbatim /goal text as the first section (under a "## /goal" heading), followed by the existing "## Summary" + "## Test plan" sections. This is the defense-in-depth: even if goals/ is corrupted, the PR body always has the verbatim text. After merge, the agent's FINAL commit before deploy updates merge_sha + flips status to shipped in the same goals/.md file. Why: the empirical chain has historically had a hole the shape of goal.md (see /blog/2026-05/sama-v2-goal-chain-gap). The two redundant captures — goals/ file AND PR body — close the hole twice. How to apply: triggered on the literal token "/goal" in user message OR when user pastes a "Goal: ... Done when: ... Constraints: ... Load-bearing files:"-shaped block. Skip if user is asking ABOUT a /goal rather than firing one (e.g. "should I fire this /goal?", "look at this /goal" — those are questions not invocations).
  • Add a one-line index entry in MEMORY.md pointing at the new file, between feedback_jolo_mode and feedback_flatpak_host_tools (the related JOLO pacing memory + the github-flow memory are this rule's neighbors).
  • Add /goal.md (the repo-root scratch file) to .gitignore so it can never accidentally land in a commit. Do NOT delete goal.md — it may have in-flight content; just ignore it.

Containerfile anti-fudge (lessons-learned from PR #46):

  • Verify that no new top-level directory was added during this /goal's work. If one was (e.g. an inadvertent goals_archive/ or similar), it MUST have a corresponding COPY <dir> ./<dir> line in Containerfile and a live-verify step that fetches a file from that directory after deploy.
  • The existing goals/ COPY line from PR #46 stays. We don't need a second one; this clause is forward-looking for any future migration.

Anti-fudge constraints:

  • Recovered text is what it is. Don't paraphrase to look better. lossy means lossy; mark it with the warning banner.
  • Don't invent /goal text for PRs that don't have any. lost is lost.
  • The 4-marker classification rule (Goal: + Done when: + Constraints + Load-bearing files) is the SINGLE filter. Don't add fuzzy "this feels like a /goal" judgments.
  • New feedback memory is ADDITIVE — does not edit feedback_jolo_mode or feedback_bypass_permissions_pacing.
  • goal.md at repo root: .gitignore'd, not deleted.
  • Site language English-only.
  • GitHub flow via flatpak-spawn (branch → PR → merge → push p620 → deploy).
  • /sama/v2/verify still 7/7 ✓ after deploy (anti-fudge).
  • All 400+ tests still pass; new tests for lossy/lost status parsing.

Live-verify after deploy:

  • curl https://tdd.md/goals → 200 with multiple rows, each with a status badge
  • For at least one lost entry: curl https://tdd.md/goals/ → 200 with "Source could not be recovered" message
  • For at least one lossy entry: curl https://tdd.md/goals/ → 200 with the "Recovered from conversation summary" warning visible
  • curl https://tdd.md/sitemap.xml | grep -c /goals/ → matches the count of shipped + lossy + lost entries (lost entries DO get sitemap URLs — they're still indexable pages)
  • /sama/v2/verify → 7/7 ✓

Load-bearing files to read FIRST:

  • src/a31_goals.ts (GoalStatus type — extend with lossy + lost)
  • src/b32_goals_meta.ts (frontmatter parser — verify it accepts the two new status values mechanically once STATUS_VALUES is extended)
  • src/b32_goals_meta.test.ts (extend with two new status-parsing cases)
  • src/d21_handlers_goals.ts (detail handler — branch on status: "lost" before file-read)
  • The output of: flatpak-spawn --host gh pr list --state merged --limit 30 --json number,title,body,headRefName,mergedAt,mergeCommit
  • The output of: flatpak-spawn --host gh pr view --json body for any PR whose body field in the list output looks /goal-shaped (saves a second fetch for the long body text)
  • /var/home/scri/.claude/projects/-var-home-scri-Documents-tdd-md/memory/MEMORY.md (the auto-memory index)
  • /var/home/scri/.claude/projects/-var-home-scri-Documents-tdd-md/memory/feedback_jolo_mode.md (existing pacing memory — new workflow memory's neighbor; cite it as related)
  • .gitignore at repo root (add /goal.md line)
  • content/blog/sama-v2-goal-chain-gap.md (the drama post that motivates this — re-read so the recovery narrative stays consistent)