Skip to content

feat(bin): stamp status appends with UTC time and report state age - #1262

Open
tomharper wants to merge 3 commits into
kunchenguid:mainfrom
tomharper:fm/status-timestamps
Open

feat(bin): stamp status appends with UTC time and report state age#1262
tomharper wants to merge 3 commits into
kunchenguid:mainfrom
tomharper:fm/status-timestamps

Conversation

@tomharper

Copy link
Copy Markdown

Intent

Crewmate status lines carried no timestamp, so firstmate could not tell a fresh state from a stale one. Real failures on 2026-07-29: firstmate read a worker's 'done:' line and reported the task complete when that line was old and the work was actually unpushed with a cancelled pipeline; and repeated 'shipshape' reports came from status lines whose underlying run had failed hours earlier. File mtime only ever dated the LAST line of a status log, so every earlier event was undated.

The user asked for four things: (1) timestamp every status append emitted by the generated scaffolds in bin/fm-brief.sh (ship, scout, and secondmate charter), keeping the line parseable by the existing consumers bin/fm-classify-lib.sh and bin/fm-crew-state.sh by UPDATING those parsers rather than breaking them, and preferring a leading ISO-8601 UTC stamp; (2) keep the scaffolded status instruction ONE copy-pasteable line a worker can run, explicitly not a helper script the worker has to find; (3) make bin/fm-crew-state.sh report how old the state it returns actually is, so a 'done' from two hours ago and one from ten seconds ago do not look identical; (4) parse existing legacy untimestamped lines as 'age unknown' rather than crashing or guessing. Constraints: do not change the status file's sparseness contract or its wake behaviour, and bin/fm-brief.sh is fragile (an unescaped apostrophe in a heredoc broke it in issue #1069) so both ship and scout briefs must actually be generated and confirmed after editing.

Decisions made while implementing, which a reviewer reading only the diff would not know:

  • A leading stamp was chosen over a trailing one per the user's preference, which forced a parser change: the stamp's own colons would otherwise be read as the verb separator by status_line_verb, status_line_note, and _fm_decision_key (a line like '2026-07-29T09:44:07Z done: x' would parse its verb as '2026-07-29T09'). All three parsers now strip a recognized leading stamp first.
  • bin/fm-classify-lib.sh is the deliberate single owner of the stamp format (one-owner rule from the firstmate-coding-guidelines skill): the format string, the literal copy-pasteable command the brief scaffold embeds, the fm_status_stamp writer helper, and the strippers all live there. fm-brief.sh interpolates FM_STATUS_STAMP_CMD_DEFAULT rather than hardcoding a second copy of the format.
  • The scaffold embeds the literal command substitution 'date -u +%Y-%m-%dT%H:%M:%SZ' inside the echo instead of calling a helper script, deliberately, because requirement (2) says the worker must be able to copy one self-contained line with nothing else on its PATH.
  • Epoch conversion is pure integer arithmetic (days_from_civil) instead of date(1), because GNU and BSD date parse flags differ and a silently misparsed stamp reporting a confidently wrong age would be worse than no stamp at all. It is pinned in tests against known instants including a leap day.
  • _fm_status_strip_stamp writes to a _FM_STATUS_BODY scratch global instead of returning through a command substitution. That is a deliberate exception to this file's documented purity, because the verb/note/key parsers run once per line of whole-file folds (status_open_decisions, status_open_activities) and a subshell per line per parser would triple the forks. It is documented as the one scratch global.
  • fm-crew-state.sh reports 'age' as the age of the EVIDENCE behind the verdict, not simply the status line: 'live' when the verdict came from a live run-step or pane read, the line's own elapsed time when it came from the status log, and 'unknown' for an untimestamped legacy line or no source at all. Guessing an age from file mtime was deliberately rejected because mtime only dates the last line, which is the original defect.
  • The age field is placed AFTER 'source:' in the canonical line rather than between state and source (the user's example showed it second) because bin/fm-fleet-snapshot.sh's crew_state_json parses on the literal ' . source: ' separator; putting age second would have broken that parser. The snapshot was updated to expose age as its own JSON field so it never pollutes 'detail', and bin/fm-fleet-view.sh renders it in the Current column, since the fleet view is a place firstmate actually reads state.
  • Scope was widened slightly beyond the scaffold to the four firstmate-side writers that also append status lines (spawn failure, decision-hold captain-held transfer, pending-reply escalation, and the optional secondmate report helper), so the age signal is not silently 'unknown' for lines firstmate itself wrote. The pending-reply escalation's dedup grep was changed from whole-line (-Fqx) to substring (-Fq) because the stamp means the payload is no longer alone on the line.
  • Legacy untimestamped lines are first-class throughout, never migrated or rewritten: they classify exactly as before and report an unknown age. Verified against the 26 real legacy status logs in the operator's primary home, plus the .agents/skills/firstmate-codexapp Codex Desktop status instruction, which was updated to ask for the same stamp.
  • Tests were added to the existing suites rather than a new runner, per repo convention: fm-brief.test.sh runs the scaffolded command for real and feeds the result back through the classifier; fm-watch-triage.test.sh pins the stamp/epoch/age primitives and the mixed stamped+legacy folds; fm-crew-state.test.sh covers old/fresh/legacy/live-pane/live-run-step/no-source ages. The 'fresh' assertion checks the seconds SCALE rather than an exact value so it cannot race the clock.
  • Documentation went to docs/architecture.md and the fm-crew-state.sh header per the knowledge-placement tree; AGENTS.md got a single-line format update only, to respect its size discipline.

Pre-existing unrelated failures in this environment, confirmed identical on the unmodified baseline by stashing: tests/fm-pi-watch-extension.test.sh, tests/fm-bearings-snapshot.test.sh, and tests/fm-secondmate-harness.test.sh (the last resolves harness ancestry as 'claude' because the suite runs under a claude harness).

What Changed

  • bin/fm-classify-lib.sh becomes the single owner of a leading ISO-8601 UTC status stamp: it defines the format, the literal copy-pasteable date -u command the brief scaffolds embed, an fm_status_stamp writer helper, and stamp/epoch/age primitives (epoch conversion is pure integer arithmetic rather than date(1)). status_line_verb, status_line_note, _fm_decision_key, and status_is_captain_relevant now strip a recognized stamp before parsing, so the stamp's own colons are not read as the verb separator; untimestamped legacy lines classify exactly as before and report an unknown age.
  • bin/fm-brief.sh emits the stamp inside each generated status instruction for the ship, scout, and secondmate scaffolds, keeping it one self-contained line a worker can copy. The four firstmate-side writers that also append status lines — spawn failure, decision-hold captain-held transfer, pending-reply escalation, and fm-secondmate-report.sh — stamp their appends too; the escalation dedup grep moves from whole-line to substring since the payload is no longer alone on the line.
  • bin/fm-crew-state.sh adds an age field reporting how old the evidence behind its verdict is (live for a run-step or pane read, elapsed time for a status-log line, unknown for legacy or no source), placed after source: so fm-fleet-snapshot.sh's separator parsing keeps working; the snapshot exposes age as its own JSON field and fm-fleet-view.sh renders it in the Current column. Tests were added to the existing fm-brief, fm-watch-triage, fm-crew-state, fm-fleet-snapshot-view, and fm-pending-reply suites, all passing.

Risk Assessment

✅ Low: The follow-up commit removes a hazardous writer-only override and corrects one stale header comment without touching any parsing or output path — the three briefs regenerate byte-identically — leaving a well-bounded, backward-compatible change whose legacy-line and stamp primitives are directly test-pinned.

Testing

I ran the five suites that own this change (fm-brief, fm-watch-triage, fm-crew-state, fm-fleet-snapshot-view, fm-pending-reply) plus the decision-hold, instruction-owners and supervision-instructions suites for the other stamped writers, and all passed. Because passing unit tests alone would not show the reported failure being fixed, I also built a scripted end-to-end walkthrough: it generates a real ship brief, has a simulated worker copy-paste and run the scaffolded status line verbatim, then reads the result through fm-crew-state.sh, fm-fleet-view.sh and fm-fleet-snapshot.sh, with the unmodified base commit run side by side. The transcript shows the original defect (an untimestamped done: reads identically whether seconds or hours old) and the fix (age: 2h14m vs age: 7s, age: unknown for a legacy line that still classifies, age: live for pane/run-step reads, and age as its own fleet-view column and JSON field). This change is CLI/text-surface only with no rendered UI, so the reviewer-visible evidence is the CLI transcript rather than a screenshot.

Evidence: End-to-end transcript: stale vs fresh vs legacy status age (before/after base commit)

=== 1. What the crewmate is actually handed (real generated ship brief) === 4. Report status by appending one line: echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '.../state/status-age-demo.status' States: working, needs-decision, blocked, paused, done, failed. Keep the leading UTC stamp on every append: it is how firstmate tells a report you just wrote from one that has been sitting in the log for hours. === 2. The crewmate copies that ONE line and runs it verbatim === status log now: 2026-07-29T19:28:38Z working: reproduced the stale-done report === 3. BEFORE (base commit 99533c5): the incident - a stale done is unreadable as stale === status log (untimestamped, as crewmates used to write it): done: PR #1233 checks green base fm-crew-state.sh reports: state: done · source: status-log · PR #1233 checks green and that line reads exactly the same whether it was written 10 seconds or 2 hours ago - which is how an old done: was relayed as current work. === 4. AFTER: the same two dones, one 2h14m old and one 7s old === state: done · source: status-log · age: 2h14m · PR .../pull/1233 checks green state: done · source: status-log · age: 7s · PR .../pull/1233 checks green === 5. Legacy untimestamped status log: parsed as before, age unknown (never guessed) === working: started needs-decision: postgres or sqlite? state: parked · source: status-log · age: unknown · postgres or sqlite? === 6. Firstmate-side writers stamp too (secondmate report helper) === 2026-07-29T19:28:39Z done [corr=00112233445566aa]: delegated scope handled (via-helper) === 7. Where firstmate actually reads it: fleet view + snapshot JSON === | ID | Current (state / source / age) | Kind | Repo/Project | Backend | ... | legacy-task | working / status-log / unknown | scout | ... | status-age-demo | done / status-log / 2h14m | scout | ... {"id":"status-age-demo","current_state":{"state":"done","source":"status-log","age":"2h14m","detail":"PR .../pull/1233 checks green",...}}


=== 1. What the crewmate is actually handed (real generated ship brief) ===
warn: no registry at /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQERFGY3WGVBWNXRBX8GDTV/sandbox/fm-home/data/projects.md; defaulting firstmate to no-mistakes off
25:4. Report status by appending one line:
26-   `echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQERFGY3WGVBWNXRBX8GDTV/sandbox/fm-home/state/status-age-demo.status'`
27-   States: working, needs-decision, blocked, paused, done, failed.
28-   Keep the leading UTC stamp on every append: it is how firstmate tells a report you
29-   just wrote from one that has been sitting in the log for hours.
30-   Each append wakes firstmate, so report sparingly: only phase changes a supervisor

=== 2. The crewmate copies that ONE line and runs it verbatim ===
$ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) working: reproduced the stale-done report" >> '/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQERFGY3WGVBWNXRBX8GDTV/sandbox/fm-home/state/status-age-demo.status'
status log now:
2026-07-29T19:28:38Z working: reproduced the stale-done report

=== 3. BEFORE (base commit 99533c5): the incident - a stale done is unreadable as stale ===
status log (untimestamped, as crewmates used to write it):
done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green
base fm-crew-state.sh reports:
  state: done · source: status-log · PR https://github.com/kunchenguid/firstmate/pull/1233 checks green
and that line reads exactly the same whether it was written 10 seconds
or 2 hours ago - which is how an old done: was relayed as current work.

=== 4. AFTER: the same two dones, one 2h14m old and one 7s old ===
stale log:
2026-07-29T17:14:39Z done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green
reported:
  state: done · source: status-log · age: 2h14m · PR https://github.com/kunchenguid/firstmate/pull/1233 checks green

fresh log:
2026-07-29T19:28:32Z done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green
reported:
  state: done · source: status-log · age: 7s · PR https://github.com/kunchenguid/firstmate/pull/1233 checks green

=== 5. Legacy untimestamped status log: parsed as before, age unknown (never guessed) ===
status log:
working: started
needs-decision: postgres or sqlite?
reported:
  state: parked · source: status-log · age: unknown · postgres or sqlite?

=== 6. Firstmate-side writers stamp too (secondmate report helper) ===
2026-07-29T19:28:39Z done [corr=00112233445566aa]: delegated scope handled (via-helper)

=== 7. Where firstmate actually reads it: fleet view + snapshot JSON ===
| ID | Current (state / source / age) | Kind | Repo/Project | Backend | Endpoint | Artifact | Path | Watch / return channel |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| legacy-task | working / status-log / unknown | scout | - | tmux | present | - | /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQERFGY3WGVBWNXRBX8GDTV/sandbox/wt | bin/fm-peek.sh fm-legacy-task |
| status-age-demo | done / status-log / 2h14m | scout | - | tmux | present | https://github.com/kunchenguid/firstmate/pull/1233 | /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQERFGY3WGVBWNXRBX8GDTV/sandbox/wt | bin/fm-peek.sh fm-status-age-demo |


snapshot JSON current_state rows:
{"id":"legacy-task","current_state":{"state":"working","source":"status-log","age":"unknown","detail":"mid-flight, written before this change","raw":"state: working · source: status-log · age: unknown · mid-flight, written before this change","observed_at":"2026-07-29T19:28:40Z","freshness":"fresh"}}
{"id":"status-age-demo","current_state":{"state":"done","source":"status-log","age":"2h14m","detail":"PR https://github.com/kunchenguid/firstmate/pull/1233 checks green","raw":"state: done · source: status-log · age: 2h14m · PR https://github.com/kunchenguid/firstmate/pull/1233 checks green","observed_at":"2026-07-29T19:28:40Z","freshness":"fresh"}}
Evidence: Scaffolded status line is identical and single-line in the scout and secondmate briefs

scout brief: echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '$FM_HOME/state/brief-scout.status' secondmate brief: echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '$FM_HOME/state/brief-secondmate.status'

scout brief:
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '$FM_HOME/state/brief-scout.status'
secondmate brief:
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '$FM_HOME/state/brief-secondmate.status'
Evidence: Reproducible demo script used to produce the transcript
#!/usr/bin/env bash
# End-to-end demonstration of the status-timestamp / state-age change,
# walked through exactly as the two end users experience it:
#   - a crewmate, who is handed a status instruction in its brief, and
#   - firstmate, who reads bin/fm-crew-state.sh / bin/fm-fleet-view.sh.
#
# Reproduces the 2026-07-29 incident: a `done:` line read out of a status log
# and relayed as current work when the line was hours old.
set -u

ROOT=${ROOT:?set ROOT to the repo root}
EV=${EV:?set EV to the evidence dir}
SANDBOX="$EV/sandbox"
rm -rf "$SANDBOX"
HOME_DIR="$SANDBOX/fm-home"
mkdir -p "$HOME_DIR/data" "$HOME_DIR/state" "$SANDBOX/bin" "$SANDBOX/base"
# The unmodified base commit, for a side-by-side of what firstmate used to read.
git -C "$ROOT" archive 99533c5 | tar -x -C "$SANDBOX/base"

hdr() { printf '\n=== %s ===\n' "$*"; }

stamp_at() { date -u -r "$1" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ; }

# A real crew worktree on a branch, so branch attribution resolves normally.
git init -q "$SANDBOX/wt"
git -C "$SANDBOX/wt" -c user.name=demo -c user.email=demo@example.invalid commit -q --allow-empty -m init
git -C "$SANDBOX/wt" checkout -q -b fm/status-timestamps

# Idle pane, so the current-state read falls through to the status log - the
# exact path that produced the incident.
cat > "$SANDBOX/bin/tmux" <<'SH'
#!/usr/bin/env bash
case "${1:-}" in
  display-message) printf '%%1\n' ;;
  capture-pane) printf 'all quiet\n> \n' ;;
esac
exit 0
SH
chmod +x "$SANDBOX/bin/tmux"
export PATH="$SANDBOX/bin:$PATH"

ID=status-age-demo
cat > "$HOME_DIR/state/$ID.meta" <<META
window=fm:fm-$ID
worktree=$SANDBOX/wt
kind=scout
META
LOG="$HOME_DIR/state/$ID.status"

crew_state() { FM_HOME="$HOME_DIR" "$ROOT/bin/fm-crew-state.sh" "$ID"; }

hdr "1. What the crewmate is actually handed (real generated ship brief)"
FM_HOME="$HOME_DIR" "$ROOT/bin/fm-brief.sh" "$ID" firstmate >/dev/null
grep -n -A5 'Report status by appending one line' "$HOME_DIR/data/$ID/brief.md" | head -12

hdr "2. The crewmate copies that ONE line and runs it verbatim"
INSTRUCTION=$(grep -o 'echo "[^"]*" >> [^`]*' "$HOME_DIR/data/$ID/brief.md" | head -1)
RUNNABLE=${INSTRUCTION/\{state\}: \{one short line\}/working: reproduced the stale-done report}
printf '$ %s\n' "$RUNNABLE"
eval "$RUNNABLE"
printf 'status log now:\n'
cat "$LOG"

hdr "3. BEFORE (base commit 99533c5): the incident - a stale done is unreadable as stale"
printf 'done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green\n' > "$LOG"
printf 'status log (untimestamped, as crewmates used to write it):\n'; cat "$LOG"
printf 'base fm-crew-state.sh reports:\n  '
FM_HOME="$HOME_DIR" "$SANDBOX/base/bin/fm-crew-state.sh" "$ID"
printf 'and that line reads exactly the same whether it was written 10 seconds\n'
printf 'or 2 hours ago - which is how an old done: was relayed as current work.\n'

hdr "4. AFTER: the same two dones, one 2h14m old and one 7s old"
printf '%s done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green\n' \
  "$(stamp_at $(( $(date -u +%s) - 8040 )))" > "$LOG"
printf 'stale log:\n'; cat "$LOG"
printf 'reported:\n  '; crew_state
printf '%s done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green\n' \
  "$(stamp_at $(( $(date -u +%s) - 7 )))" > "$LOG"
printf '\nfresh log:\n'; cat "$LOG"
printf 'reported:\n  '; crew_state

hdr "5. Legacy untimestamped status log: parsed as before, age unknown (never guessed)"
printf 'working: started\nneeds-decision: postgres or sqlite?\n' > "$LOG"
printf 'status log:\n'; cat "$LOG"
printf 'reported:\n  '; crew_state

hdr "6. Firstmate-side writers stamp too (secondmate report helper)"
: > "$LOG"
FM_HOME="$HOME_DIR" "$ROOT/bin/fm-secondmate-report.sh" \
  "$LOG" done 00112233445566aa "delegated scope handled"
cat "$LOG"

hdr "7. Where firstmate actually reads it: fleet view + snapshot JSON"
printf '%s done: PR https://github.com/kunchenguid/firstmate/pull/1233 checks green\n' \
  "$(stamp_at $(( $(date -u +%s) - 8040 )))" > "$LOG"
cat > "$HOME_DIR/state/legacy-task.meta" <<META
window=fm:fm-legacy-task
worktree=$SANDBOX/wt
kind=scout
META
printf 'working: mid-flight, written before this change\n' > "$HOME_DIR/state/legacy-task.status"
FM_HOME="$HOME_DIR" "$ROOT/bin/fm-fleet-view.sh" | sed -n '/^| ID /,/^$/p'
printf '\nsnapshot JSON current_state rows:\n'
FM_HOME="$HOME_DIR" "$ROOT/bin/fm-fleet-snapshot.sh" \
  | jq -c '.tasks[] | {id, current_state}'

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed ✅
  • ⚠️ bin/fm-brief.sh:76 - STATUS_STAMP_CMD=${FM_STATUS_STAMP_CMD:-$FM_STATUS_STAMP_CMD_DEFAULT} is a writer-side-only override with no parser counterpart, so it can silently defeat every parser this change relies on. Unlike FM_CLASSIFY_PAUSED_VERB, which both the brief writer and the classifiers read, _FM_STATUS_STAMP_GLOB in bin/fm-classify-lib.sh is a hardcoded literal that cannot follow the override. Concrete failure: with FM_STATUS_STAMP_CMD=&#39;date -u&#39; the scaffold emits Wed Jul 29 12:00:00 UTC 2026 done: shipped; the glob does not match, _fm_status_strip_stamp leaves the line intact, and status_line_verb returns Wed Jul 29 12. Every crewmate report from that home then loses its terminal verb: status_is_captain_relevant falls through to the free-text regex, status_is_paused never fires, status_open_decisions files each event under a garbage key, and fm-crew-state.sh maps the line to unknown. The knob is undocumented (absent from docs/configuration.md) and unused by any test, so nothing catches the breakage. Either drop the override, or have fm-brief.sh validate the override's output against _FM_STATUS_STAMP_GLOB and fall back to FM_STATUS_STAMP_CMD_DEFAULT when it does not match.
  • ℹ️ bin/fm-classify-lib.sh:434 - crew_absorb_class's header still documents fm-crew-state.sh's canonical line as &#34;state: &lt;s&gt; · source: &lt;src&gt; · &lt;detail&gt;&#34;, which no longer matches what the helper emits now that age: sits between source and detail (bin/fm-crew-state.sh:107). The parsing code below it is correct — src=${line#*source: }; src=${src%% *} still yields the source token — so this is documentation drift only, but it is the sole remaining in-repo copy of the old format and this file is the declared owner of the status vocabulary.
  • ℹ️ bin/fm-bearings-snapshot.sh:377 - The bearings projection builds its in-flight rows as {id, kind, state, doing} and never reads the new current_state.age field the snapshot now exposes, so on the orient surface a done from two hours ago and one from ten seconds ago still render identically — the exact ambiguity behind the 2026-07-29 incident. This is not a regression (the age was never there) and it is outside the intent's stated scope, which named fm-crew-state.sh, the snapshot JSON, and fm-fleet-view.sh; the snapshot already carries age as its own field, so surfacing it in bearings is a one-line follow-up rather than anything blocking here.
  • ℹ️ bin/fm-supervise-daemon.sh:346 - The daemon's .subsuper-seen-status-&lt;task&gt; dedup compares the stored marker against the current last status line verbatim. Because every append now carries a distinct stamp, two textually identical reports (e.g. blocked: waiting on review appended at 10:00 and again at 11:00) are no longer collapsed into one escalation the way they were before. Per-wake enqueueing is driven by file writes rather than content, so the wake contract the intent asked to preserve is unchanged, and re-surfacing a genuinely new event is the more correct behaviour — noting it only because it is a behavioural consequence of the stamp that the diff does not call out.

🔧 Fix: drop unusable status stamp override, fix stale format comment
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-brief.test.sh (includes the new one-line stamped-scaffold test across ship/scout/secondmate)
  • bash tests/fm-watch-triage.test.sh (includes test_status_timestamp_contract: stamp/epoch/age primitives, mixed stamped+legacy folds)
  • bash tests/fm-crew-state.test.sh (includes test_reported_state_carries_evidence_age: old/fresh/legacy/live-pane/live-run-step/no-source)
  • bash tests/fm-fleet-snapshot-view.test.sh (age as its own JSON field, age rendered in the fleet-view Current column)
  • bash tests/fm-pending-reply.test.sh (stamped escalation append still classifies as blocked; substring dedup)
  • bash tests/fm-decision-hold-lifecycle.test.sh, bash tests/fm-instruction-owners.test.sh, bash tests/fm-supervision-instructions.test.sh (other stamped writers + instruction ownership)
  • Manual end-to-end: generated a real ship brief with bin/fm-brief.sh, ran the scaffolded status command verbatim as a worker would, then read it back via bin/fm-crew-state.sh, bin/fm-fleet-view.sh, bin/fm-fleet-snapshot.sh
  • Manual before/after: extracted base commit 99533c5 with git archive and ran its bin/fm-crew-state.sh on the same status log to show the stale-done: ambiguity the change removes
  • Manual: bin/fm-secondmate-report.sh &lt;log&gt; done &lt;corr&gt; &lt;note&gt; to confirm firstmate-side writers also stamp
  • Manual: captured the scaffolded status line from the scout and secondmate briefs to confirm one identical copy-pasteable line per scaffold
⚠️ **Document** - 1 info
  • ℹ️ .agents/skills/firstmate-codexapp/SKILL.md:64 - The Codex Desktop status instruction in .agents/skills/firstmate-codexapp/SKILL.md hand-copies the literal stamp format date -u +%Y-%m-%dT%H:%M:%SZ, which bin/fm-classify-lib.sh owns as FM_STATUS_STAMP_FORMAT / FM_STATUS_STAMP_CMD_DEFAULT. Unlike bin/fm-brief.sh, which interpolates the owner's variable, this surface is prose in a skill and cannot interpolate, so the two copies can silently drift if the format ever changes. Left as-is: it is the necessary form for a prompt a human pastes into Desktop, and closing the drift risk properly means a test or lint assertion (executable change), which is out of scope for the documentation phase. Follow-up worth considering: pin the SKILL.md literal against FM_STATUS_STAMP_CMD_DEFAULT in tests/fm-watch-triage.test.sh.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A crewmate status line carried no time, so firstmate could not tell a
fresh event from one that had sat in the log for hours: a `done:` line
was relayed as current work after the worker had already stopped with
the change unpushed, and repeated "shipshape" reports came from status
lines whose underlying run had failed hours earlier. File mtime only
ever dated the last line.

- bin/fm-classify-lib.sh becomes the single owner of an ISO-8601 UTC
  stamp format, the copy-pasteable command that emits it, a
  firstmate-side writer helper, and the parsers that strip it. The verb,
  note, and decision-key parsers drop a leading stamp first so its own
  colons are never read as the verb separator.
- bin/fm-brief.sh scaffolds the stamp into the ship, scout, and
  secondmate status instructions, still as one self-contained line that
  needs no helper on the worker's PATH.
- bin/fm-crew-state.sh reports the age of the evidence behind its
  verdict: `live` for a run-step or pane read, the line's own elapsed
  time for a status-log read. The fleet snapshot exposes it as a field
  and the fleet view renders it.
- Firstmate-side writers (spawn failure, captain-held transfer,
  pending-reply escalation, secondmate report helper) stamp too.

Legacy untimestamped lines keep classifying exactly as before and report
an unknown age rather than a guess; verified against the 26 real status
logs in the primary home.
@tomharper
tomharper force-pushed the fm/status-timestamps branch from 2962a95 to 8c81c17 Compare July 29, 2026 22:53
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.

1 participant