Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 18 additions & 5 deletions .agents/skills/bearings/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ A digest/build invocation is operationally read-only apart from those explicit p
During that invocation it never tears down a task, merges a PR, dispatches new work, steers a worker, answers a decision, cleans up work, or mutates backlog or task state.
Board answers are acted on later under the normal authority rules; this skill's board-wake section explicitly owns the guarded routing at that time.

## Sprache (Captain)

Chat-Digest und Dateibericht ausschließlich auf Deutsch mit echten Umlauten.
Anrede mindestens einmal: Kapitän.
Die vier Abschnittsüberschriften bleiben Englisch (Contract).
Dateibericht-Titel bei Morgenbrief: nur „Morgenlage“ (kein englisches Morning status).
Leere Abschnitte auf Deutsch rendern:

- Captain's Call: „Derzeit brauchst du keine Aktion, Kapitän.“
- Recently Landed: „In der aktuellen Baseline gibt es keine frischen Abschlüsse.“
- Underway: „Nichts ist unterwegs.“
- Charted Next: „Nichts steht in der Warteschlange.“

## Invocation modes

- Plain `/bearings` gathers a fresh bounded snapshot and renders the four-section chat digest without creating, deleting, reading, or replacing `data/status-report-<YYYY-MM-DD>.md`.
Expand Down Expand Up @@ -61,7 +74,7 @@ Board answers are acted on later under the normal authority rules; this skill's
If today's file already exists, delete it first, then create a new file from scratch.
This is the only file-mode write allowed by the skill.
The detailed report includes:
- **Title** - `# Bearings - <day> <YYYY-MM-DD>` (use "Morning status" only when the captain specifically asks for a morning brief), followed by two or three sentences framing where things stand.
- **Title** - `# Bearings - <day> <YYYY-MM-DD>` (use „Morgenlage“ only when the captain specifically asks for a morning brief), followed by two or three sentences framing where things stand.
- **Captain's Call** - every open decision summarized with its options from the structured decision record, plus each PR ready to merge and each needed credential or login, every PR with the full `https://...` URL, never a bare `#number`.
- **Recently Landed** - the bounded current recent-completions baseline from structured state across the main fleet and every registered secondmate home, rendered in full on every run.
- **Underway** - each live direct report making progress, with its current state, and the plans or main pickup pointers worth reopening (`data/<id>/report.md` files, `.lavish/*.html` boards).
Expand Down Expand Up @@ -111,13 +124,13 @@ This skill is the one owner of the `/bearings` chat-response format; the snapsho
Every `/bearings` chat response renders EXACTLY these four sections, in THIS order, and nothing else structural (there is no At Anchor section):

1. **Captain's Call** - ONLY items that need the captain's own action now: a decision to make, a PR to approve or merge, a credential or login to provide, or a blocker only the captain can clear.
Empty-state: "Nothing needs your action right now."
Empty-state (Deutsch): „Derzeit brauchst du keine Aktion, Kapitän.“
2. **Recently Landed** - the bounded current recent-completions baseline: merged PRs, completed scouts, and finished local-only merges across the main fleet and every registered secondmate home.
Empty-state: "No recent completions are in the current baseline."
Empty-state (Deutsch): „In der aktuellen Baseline gibt es keine frischen Abschlüsse.“
3. **Underway** - live work progressing on its own, one line of current state per direct report.
Empty-state: "Nothing is underway."
Empty-state (Deutsch): „Nichts ist unterwegs.“
4. **Charted Next** - queued or gated work waiting on the fleet or a date, plus action-free fleet-integrity warnings, never on the captain.
Empty-state: "Nothing is queued."
Empty-state (Deutsch): „Nichts steht in der Warteschlange.“

Rules that keep the contract unambiguous:

Expand Down
6 changes: 4 additions & 2 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ export default function (pi: ExtensionAPI) {
const names = contested.map((tool) => `"${tool.name}"`).join(", ");
const plural = contested.length > 1;
ui.notify(
`Firstmate Calm: the ${names} built-in tool${plural ? "s are" : " is"} already provided by another extension, so Calm may not fully function for ${plural ? "them" : "it"} this session.`,
plural
? `Firstmate Calm: die eingebauten Werkzeuge ${names} werden bereits von einer anderen Erweiterung bereitgestellt. Calm wirkt dafür in dieser Sitzung möglicherweise nicht vollständig.`
: `Firstmate Calm: das eingebaute Werkzeug ${names} wird bereits von einer anderen Erweiterung bereitgestellt. Calm wirkt dafür in dieser Sitzung möglicherweise nicht vollständig.`,
Comment on lines +377 to +379

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Plural Warning Lacks Coverage

The new plural notification branch has no focused regression coverage: the collision fixture contests only bash and checks general message content, so changes to multi-tool names or German plural wording can pass the suite unnoticed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"warning",
);
for (const tool of contested) {
Expand Down Expand Up @@ -474,7 +476,7 @@ export default function (pi: ExtensionAPI) {
});

pi.registerCommand("calm", {
description: "Toggle Firstmate's supported conversation-only transcript presentation.",
description: "Schaltet Firstmates Calm-Gesprächsdarstellung um.",
handler: async (_args, ctx) => {
const active = !calmPresentationIsActive();
persistCalmPreference(active);
Expand Down
160 changes: 156 additions & 4 deletions tests/fm-calm-pi-extension.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -611,15 +611,23 @@ for (const name of ["read", "edit", "write", "grep", "find", "ls"]) {
}

// Part C: a single, prominent, user-facing warning naming the contested tool, not
// merely a console diagnostic.
// merely a console diagnostic. Singular German wording is part of the captain-facing
// contract (plural coverage lives in test_calm_plural_contested_tools_warning).
if (notifications.length !== 1) {
throw new Error(`expected exactly one contested-tool notification, saw ${JSON.stringify(notifications)}`);
}
if (notifications[0].type !== "warning") {
throw new Error(`contested-tool notification was not type "warning": ${JSON.stringify(notifications[0])}`);
}
if (!notifications[0].message.includes("bash") || !notifications[0].message.toLowerCase().includes("calm")) {
throw new Error(`contested-tool notification did not name the tool clearly: ${JSON.stringify(notifications[0])}`);
const singularWarning = notifications[0].message;
if (
!singularWarning.includes('"bash"') ||
!singularWarning.includes("das eingebaute Werkzeug") ||
!singularWarning.includes("wird bereits von einer anderen Erweiterung bereitgestellt") ||
singularWarning.includes("die eingebauten Werkzeuge") ||
singularWarning.includes("werden bereits")
) {
throw new Error(`contested-tool notification missed singular German wording: ${JSON.stringify(notifications[0])}`);
}
const sawBashDiagnostic = diagnostics.some((line) => line.includes("bash"));
if (!sawBashDiagnostic) {
Expand Down Expand Up @@ -656,6 +664,149 @@ JS
pass "Calm's first same-session /calm activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns prominently and logs the contested name, and only rows constructed before that activation - the documented bound - fail to retroactively collapse"
}

test_calm_plural_contested_tools_warning() {
local fixture out output_file status
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
echo "skip: node or npm not found for Pi calm plural contested warning test"
return 0
fi
if [ ! -f "$PI_PACKAGE_DIR/package.json" ]; then
echo "skip: installed @earendil-works/pi-coding-agent package not found"
return 0
fi

fixture="$TMP_ROOT/plural-contested-warning"
mkdir -p \
"$fixture/project/.pi/extensions/lib" \
"$fixture/project/node_modules/@earendil-works" \
"$fixture/home/config"
cp "$EXT" "$fixture/project/.pi/extensions/fm-calm.ts"
cp "$ASSISTANT_LAYOUT" "$fixture/project/.pi/extensions/lib/fm-calm-assistant-layout.ts"
cp "$OPERATIONAL_USER_LAYOUT" "$fixture/project/.pi/extensions/lib/fm-calm-operational-user-layout.ts"
cp "$VISIBILITY" "$fixture/project/.pi/extensions/lib/fm-calm-visibility.ts"
cp "$WORKING_SHIP" "$fixture/project/.pi/extensions/lib/fm-calm-working-ship.ts"
cp "$PI_OPERATIONAL_INPUT" "$fixture/project/.pi/extensions/lib/fm-operational-input.ts"
ln -s "$PI_PACKAGE_DIR" "$fixture/project/node_modules/@earendil-works/pi-coding-agent"
ln -s "$PI_PACKAGE_DIR/node_modules/@earendil-works/pi-tui" "$fixture/project/node_modules/@earendil-works/pi-tui"
ln -s "$PI_PACKAGE_DIR/node_modules/typebox" "$fixture/project/node_modules/typebox"
printf '%s\n' '{"type":"module"}' >"$fixture/project/package.json"

output_file="$fixture/node-output"
(cd "$fixture/project" && \
EXT="$fixture/project/.pi/extensions/fm-calm.ts" \
FM_HOME="$fixture/home" \
node --input-type=module) >"$output_file" 2>&1 <<'JS'
import { fileURLToPath, pathToFileURL } from "node:url";

const foreignPath = fileURLToPath(pathToFileURL(`${process.cwd()}/foreign-owner.ts`).href);
const foreignBash = {
name: "bash",
label: "Foreign bash",
description: "Foreign bash",
parameters: { type: "object", properties: {} },
async execute() {
return { content: [{ type: "text", text: "bash" }], details: {}, isError: false };
},
};
const foreignGrep = {
name: "grep",
label: "Foreign grep",
description: "Foreign grep",
parameters: { type: "object", properties: {} },
async execute() {
return { content: [{ type: "text", text: "grep" }], details: {}, isError: false };
},
};

const registry = new Map([
["bash", { tool: foreignBash, ownerPath: foreignPath }],
["grep", { tool: foreignGrep, ownerPath: foreignPath }],
]);
const notifications = [];
const diagnostics = [];
const originalConsoleError = console.error;
console.error = (...args) => diagnostics.push(args.join(" "));
const handlers = new Map();
let calmCommand;
const extPath = fileURLToPath(pathToFileURL(process.env.EXT).href);
const pi = {
events: { emit() {}, on() {} },
on(event, handler) {
handlers.set(event, handler);
},
registerCommand(name, command) {
if (name === "calm") calmCommand = command;
},
registerEntryRenderer() {},
registerTool(tool) {
if (!registry.has(tool.name)) {
registry.set(tool.name, { tool, ownerPath: extPath });
}
},
getAllTools() {
return Array.from(registry.entries()).map(([name, { ownerPath }]) => ({
name,
sourceInfo: { source: "extension", path: ownerPath },
}));
},
};

const extension = await import(`${pathToFileURL(process.env.EXT).href}?plural=${Date.now()}`);
extension.default(pi);
if (!calmCommand) {
console.error = originalConsoleError;
throw new Error("Calm did not register /calm");
}

await calmCommand.handler("", {
ui: {
getEditorText: () => "",
getToolsExpanded: () => false,
onTerminalInput: () => () => {},
setHiddenThinkingLabel() {},
setStatus() {},
setToolsExpanded() {},
setWorkingVisible() {},
notify(message, type) {
notifications.push({ message, type });
},
},
});
console.error = originalConsoleError;

if (notifications.length !== 1) {
throw new Error(`expected exactly one plural contested-tool notification, saw ${JSON.stringify(notifications)}`);
}
if (notifications[0].type !== "warning") {
throw new Error(`plural contested-tool notification was not type "warning": ${JSON.stringify(notifications[0])}`);
}
const pluralWarning = notifications[0].message;
if (
!pluralWarning.includes('"bash"') ||
!pluralWarning.includes('"grep"') ||
!pluralWarning.includes("die eingebauten Werkzeuge") ||
!pluralWarning.includes("werden bereits von einer anderen Erweiterung bereitgestellt") ||
pluralWarning.includes("das eingebaute Werkzeug") ||
pluralWarning.includes(" wird bereits ")
) {
throw new Error(`plural contested-tool notification missed German plural wording: ${JSON.stringify(notifications[0])}`);
}
if (registry.get("bash")?.tool !== foreignBash || registry.get("grep")?.tool !== foreignGrep) {
throw new Error("Calm replaced a foreign contested built-in during the plural collision path");
}
const sawBashDiagnostic = diagnostics.some((line) => line.includes("bash"));
const sawGrepDiagnostic = diagnostics.some((line) => line.includes("grep"));
if (!sawBashDiagnostic || !sawGrepDiagnostic) {
throw new Error(`expected console diagnostics naming both skipped built-ins; saw: ${JSON.stringify(diagnostics)}`);
}
JS
status=$?
out=$(cat "$output_file")
[ "$status" -eq 0 ] || fail "Pi calm plural contested warning path failed: $out"
[ -z "$out" ] || fail "Pi calm plural contested warning test printed output: $out"
pass "Calm's first activation with multiple contested built-ins emits one German plural warning naming each contested tool and leaves those foreign registrations intact"
}

test_rendering_and_session_lifecycle() {
local fixture out output_file status version
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
Expand Down Expand Up @@ -797,7 +948,7 @@ if (handlers.has("input")) {
}
if (
calmCommand.description !==
"Toggle Firstmate's supported conversation-only transcript presentation."
"Schaltet Firstmates Calm-Gesprächsdarstellung um."
) {
throw new Error(`unexpected calm command description: ${calmCommand.description}`);
}
Expand Down Expand Up @@ -3961,6 +4112,7 @@ test_pi_compat_degraded_adapter
test_pi_compat_missing_adapter_exports
test_builtin_gate_load_time
test_calm_activation_collision_and_regression_bound
test_calm_plural_contested_tools_warning
test_rendering_and_session_lifecycle
test_calm_mid_turn_working_notes
test_operational_followup_turn_e2e
Expand Down
Loading