Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c1103a1
fix(bin): translate Stop hook timeout signals into durable auto-arm f…
pablontiv Sep 14, 2026
0b9de13
fix(spawn): establish Claude task channel authority (#4464)
pablontiv Sep 14, 2026
a6618dd
fix(bin): refuse fm-control.sh exit when the composer holds unproven …
pablontiv Sep 14, 2026
c806c6a
fix(spawn): establish crewmate identity first (#4481)
pablontiv Sep 15, 2026
d499323
fix(bin): reconcile redundant secondmate divergence during updates (#…
pablontiv Sep 15, 2026
da5e658
feat: enable gpt-5.6-luna max reasoning for crew dispatch (#4497)
umeranjum17 Sep 15, 2026
616049a
feat(calm): render smooth Unicode swell with asymmetric two-color sai…
yasuhito Sep 15, 2026
aa92177
fix(bin): supersede stale scout delivery text in brief.md on promotio…
pablontiv Sep 15, 2026
b85e28b
fix(bin): make captain holds work on hosts with an older JSON::PP, an…
Marsjohn-11 Sep 15, 2026
8b10b61
fix(bin): read codex 0.154's idle braille starfield rows as composer …
tbillings28 Sep 15, 2026
2da3c5e
fix(bin): refuse empty text steers in fm-send (#4259)
pablontiv Sep 15, 2026
0f242b9
fix(calm): paint the working ship one yellow over all-blue water (#4554)
kunchenguid Sep 15, 2026
a8dd08d
fix(bin): stop aging a second mate's active turn from its launch (#4270)
tiago-peixoto Sep 15, 2026
8b944a1
feat(bin): add read-only PR blocker and reviewer discovery commands (…
tiago-peixoto Sep 15, 2026
db645b8
fix(bin): teach validation-round pauses in generated briefs (#2752)
tiago-peixoto Sep 15, 2026
9ad5fc4
docs(readme): add star history chart (#4558)
kunchenguid Sep 15, 2026
1bdfd8c
fix(bin): refuse teardown when a task's endpoint close fails (#4510)
aminry Sep 15, 2026
bdcacb9
feat(calm): add flag-gated Claude Code Calm mode (#4565)
kunchenguid Sep 15, 2026
b430bf5
fix(bin): honour a declared wait before wedge-escalating a quiet pane…
aminry Sep 16, 2026
7111081
fix(bin): report verified PR state for passed runs (#4624)
jokim1 Sep 16, 2026
af1f2ea
fix: restore published contribution follow-up (Fixes #4469) (#4627)
mremond Sep 16, 2026
36c9814
fix(bin): make remote report transfers explicit and fail-open (#4658)
kunchenguid Sep 16, 2026
6483df6
fix(calm): preserve substantive mid-turn responses (#4655)
kunchenguid Sep 16, 2026
baede47
fix(bin): preserve PR merge polls across volume remounts (#4656)
mremond Sep 16, 2026
9f8ad95
fix(bin): keep contribution records when the poll budget runs out (fo…
mremond Sep 16, 2026
b0877b4
fix(bin): clear parent pending-replies on local secondmate retirement…
thelad-dev Sep 16, 2026
795e5e4
fix(bin): accept Orca's composite worktree id when tearing down a tas…
JuanJoseGonGi Sep 16, 2026
69d660a
feat(bin): add opt-in typed dispatch resolution (#4692)
kunchenguid Sep 17, 2026
334fa12
fix(bin): read the latest status event so buried declarations and ope…
tiago-peixoto Sep 17, 2026
fa93097
fix(bin): launch codex crewmates with codex's hook layer disabled (#4…
codyjohnsontx Sep 17, 2026
3eb5b63
fix(bin): settle terminal contribution observations (Fixes #4669, Fix…
mremond Sep 17, 2026
f5d7f5f
fix: select authoritative no-mistakes runs (#4476)
mremond Sep 17, 2026
a221640
fix: distinguish captain outcomes from no-op updates (#4738)
kunchenguid Sep 17, 2026
5e879ba
fix(bin): let non-owner Claude Stops exit safely (#4777)
kunchenguid Sep 17, 2026
e213343
fix(bin): survive bash 3.2 empty-array expansion in watcher churn abs…
0x7067 Sep 17, 2026
b752ced
Make the foreign-owner turn-end repro create a Linux-readable session…
kunchenguid Sep 17, 2026
8d9d5da
fix: require complete captain-facing final responses (#4779)
kunchenguid Sep 17, 2026
888871d
fix: preserve substantive mid-turn text in Pi Calm (#4788)
kunchenguid Sep 17, 2026
eb0e8e4
Merge upstream kunchenguid/firstmate:main into main
vifar Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions .claude/mods/firstmate-calm/lib/fm-calm-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
// captain-facing contract and docs/configuration.md
// the persisted preference schema. Everything here is pure so tests run it under Node.
import { classifyFirstmateOperationalText } from "./fm-operational-input.ts";
import {
CALM_PRESERVE_MIN_CHARS,
calmTextIsSubstantive,
} from "./fm-calm-preservation.ts";

export { CALM_PRESERVE_MIN_CHARS } from "./fm-calm-preservation.ts";

/** The environment variables that select the effective Firstmate home, as the mod reads them. */
export type CalmHomeEnvironment = {
Expand Down Expand Up @@ -67,18 +73,6 @@ export type CalmStepOutcome = {
readonly toolUses: readonly unknown[];
};

/**
* Single-line narration in session history topped out around 215 characters, while
* substantive single-line content began around 270; every multi-line message was
* substantive, so this empirical boundary stays deliberately tunable.
*/
export const CALM_PRESERVE_MIN_CHARS = 240;

/** Whether text is substantive enough to preserve despite ending alongside a tool call. */
function shouldPreserveMidTurnText(text: string): boolean {
const trimmedText = text.trim();
return text.includes("\n") || trimmedText.length >= CALM_PRESERVE_MIN_CHARS;
}

/**
* Whether text from a model step is a mid-turn working note: the model did not end
Expand All @@ -88,7 +82,7 @@ function shouldPreserveMidTurnText(text: string): boolean {
*/
export function stepTextIsWorkingNote(step: CalmStepOutcome, text: string): boolean {
const midTurn = step.stopReason === "tool_use" || (step.stopReason === "max_tokens" && step.toolUses.length > 0);
return midTurn && !shouldPreserveMidTurnText(text);
return midTurn && !calmTextIsSubstantive(text);
}

/** A trimmed text key that retains whether the raw row contained a newline. */
Expand Down Expand Up @@ -130,7 +124,7 @@ export function classifyRestoredTranscript(rows: readonly CalmSessionRow[]): {
break;
}
}
if (followedByToolCall && shouldPreserveMidTurnText(row.text)) finalReplies.add(key);
if (followedByToolCall && calmTextIsSubstantive(row.text)) finalReplies.add(key);
else if (followedByToolCall) notes.add(key);
else finalReplies.add(key);
}
Expand Down
11 changes: 11 additions & 0 deletions .claude/mods/firstmate-calm/lib/fm-calm-preservation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Shared Calm policy for deciding whether mid-turn assistant text is substantive.
// Claude Code imports this file directly, while the Pi extension reaches the same
// implementation through its tracked symlink so both harnesses keep one threshold and rule.

/** The minimum trimmed text length preserved from a mid-turn assistant message. */
export const CALM_PRESERVE_MIN_CHARS = 240;

/** Whether mid-turn assistant text is substantive enough to remain visible. */
export function calmTextIsSubstantive(text: string): boolean {
return text.includes("\n") || text.trim().length >= CALM_PRESERVE_MIN_CHARS;
}
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,18 @@ jobs:
exit 1
}

# Same shape for the watcher's churn-deferral regression: an already-
# marked churn window expands an empty array that only stock Bash
# treats as an unbound variable under set -u.
churn_output=$(FM_TEST_ONLY=test_turn_ended_churn_existing_marker_absorbed \
/bin/bash tests/fm-watch-triage.test.sh)
printf '%s\n' "$churn_output"
churn_count=$(printf '%s\n' "$churn_output" | grep -c '^ok - ')
[ "$churn_count" -eq 1 ] || {
echo "::error::expected 1 watcher churn-deferral bash 3.2 regression, got $churn_count"
exit 1
}

invariants:
name: Repo invariants
runs-on: ubuntu-latest
Expand Down
18 changes: 14 additions & 4 deletions .pi/extensions/lib/fm-calm-assistant-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// updateContent method. installCalmAssistantLayout() probes that exact method and throws
// if it is missing; fm-calm.ts catches that and skips only this adapter with a diagnostic
// instead of blocking Calm or Pi.
// This layout removes collapsed thinking and the mid-turn assistant text blocks
// classified as "assistant-working-note" from a shallow presentation copy. The message
// This layout removes collapsed thinking and short mid-turn assistant text blocks
// classified as "assistant-working-note" from a shallow presentation copy. Substantive
// mid-turn text is preserved. The message
// itself, model context, session storage, and export rendering are never touched.
// ./fm-calm-visibility.ts owns which classes Calm hides.
import type { AssistantMessageComponent as PiAssistantMessageComponent } from "@earendil-works/pi-coding-agent";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
import { calmTextIsSubstantive } from "./fm-calm-preservation.ts";
import { calmPresentationHides } from "./fm-calm-visibility.ts";

type AssistantMessage = Parameters<PiAssistantMessageComponent["updateContent"]>[0];
Expand Down Expand Up @@ -75,15 +77,23 @@ export function installCalmAssistantLayout(): void {
state.hideThinkingBlock &&
patch.hidesThinking();
const hideWorkingNote =
patch.hidesWorkingNote() && isMidTurnAssistantMessage(message);
patch.hidesWorkingNote() &&
isMidTurnAssistantMessage(message) &&
message.content.some(
(block) => block.type === "text" && !calmTextIsSubstantive(block.text),
);
const presentationMessage =
hideThinking || hideWorkingNote
? {
...message,
content: message.content.filter(
(block) =>
!(hideThinking && block.type === "thinking") &&
!(hideWorkingNote && block.type === "text"),
!(
hideWorkingNote &&
block.type === "text" &&
!calmTextIsSubstantive(block.text)
),
),
}
: message;
Expand Down
1 change: 1 addition & 0 deletions .pi/extensions/lib/fm-calm-preservation.ts
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,10 @@ For the full `stuck-crewmate-recovery` trigger, including a live worker claiming

**Talk in outcomes, not mechanics.**
Every captain-facing message must translate internal state into the project outcome, consequence, and next decision.
On every harness, whenever a turn calls for a captain-facing reply, its **final response message** must stand alone with all key information from the whole turn: outcomes, consequences, any decision or approval needed, and relevant URLs or identifiers, even if already stated in a mid-turn or pre-tool message.
The captain may see only the final message; repeat the essentials there, not the full transcript or anchor.
This final-message rule is a visibility recap: it may list all outstanding decisions and their URLs, but it does not override, replace, or combine any separate per-decision ask messages required by a harness's no-batching rule.
Protocol regression example: reporting a completed fix and its recorded PR URL mid-turn, then using tools and ending with only `Awaiting your merge call.`, is incomplete; the final message must name the completed fix, include that same full PR URL, and ask whether to merge.
Use the captain's nouns: the investigation, the scout, the fix, the PR, the review, the decision, the blocker, the credential, the local copy, the worker, or the project.
Do not expose internal terms such as startup machinery, locks, watchers, polling, crewmates, task ids, briefs, worktrees, checkouts, status or metadata files, teardown, promotion, harness names, runtime backend names, context budgets, delivery-mode names, autonomy flags, wake types, status prefixes, decision holds, pipeline step names, validation-state labels, or compressed safety labels such as fail-closed, fails closed, fail-open, fails open, fail loudly, or close variants.
Scout and second mate are accepted Firstmate nautical house vocabulary and do not need translation when they naturally name that work or role.
Expand Down Expand Up @@ -513,10 +517,12 @@ Reach the captain immediately for:

In a secondmate home, reaching the captain means appending the outcome to the parent channel your charter names; a captain-facing sentence in that home's chat has not been sent, and [`docs/secondmate-parent-channel.md`](docs/secondmate-parent-channel.md) owns which outcomes the home's own scripts deliver there without you.
Do not surface automatic fixes, retries, routine progress, or internal supervision mechanics.
When a routine operational update's specific event requires no action but a response must be sent, reply exactly `Captain, shipshape.` without characterizing the visible session's unrelated decisions.
Reply exactly `Captain, shipshape.` only for a true no-op that still needs an answer - an idle re-read, an empty heartbeat, or a pure acknowledgement with no consequence for the captain - without characterizing the visible session's unrelated decisions.
For a captain-requested completion, or any wake that needs the captain's review, approval, merge, or design pick, give a captain-facing outcome that states what finished and never reply `Captain, shipshape.`; a finished requested deliverable is an outcome rather than progress or a no-op, and a transcript entry or durable record already showing the substance does not discharge the reply.
Ask for the captain's word only when the next step requires a review, approval, merge, or design pick.
Batch non-urgent updates into the next natural reply.
Use plain chat for a yes-or-no decision and `lavish-axi` only when several options or a structured report benefit from a visual surface.
Whenever a PR is mentioned, include its full `https://...` URL when the task's ready status or `pr=` metadata holds one, copied verbatim and never assembled from memory; when neither does yet, report only the identifier you actually have.
Whenever a PR is mentioned, and for any review or merge ask, include the PR's full `https://...` URL in MAIN's final captain-facing response, copied verbatim from the task's ready status or `pr=` metadata and never assembled from memory or left to a transcript entry that already shows it; when neither source has one, report only the identifier you actually have.
Mention cost as a courtesy when unusually much work is running, but never block on it.

## 10. Backlog contract
Expand Down
2 changes: 2 additions & 0 deletions GROK_BOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ Speak in outcomes and consequences, not internal mechanics.
When you bring a decision to the captain, send one message per decision. Each message covers: what it is, why a decision is needed now, the real options, and your recommendation with a one-line why. Put the options on a choice card so they can tap one. One card at a time. Do not batch unrelated decisions into one list.

Keep it simple for the captain. Focus on communicating outcomes, not mechanics. They scale by talking only to you; protect that.

Read and follow [AGENTS.md section 9](AGENTS.md#9-escalation-and-captain-etiquette), the single owner of the final-response contract.
2 changes: 1 addition & 1 deletion bin/fm-backlog-handoff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ remote_handoff() { # <secondmate-id> <keys...>
echo " nothing new was staged." >&2
return 1
fi
for key in "${to_move[@]}"; do
for key in "${to_move[@]+"${to_move[@]}"}"; do
while IFS= read -r line; do
printf 'error: refusing to hand off %s: non-2-space continuation line: %s\n' "$key" "$line" >&2
return 1
Expand Down
Loading
Loading