Skip to content
Merged
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
9 changes: 7 additions & 2 deletions .agents/skills/bearings/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ For a contribution wake or linked-issue filing, go directly to Contribution foll
Never use hold-reason or body prose to classify or place a decision.
A `live` hold appears in Captain's Call; `blocked`, `dated`, and `aged` holds appear as disclosed Charted Next gates stating their structured reason.
Use `--all-decisions` to reveal every captain hold available within the bounded snapshot and remove each revealed gate from Charted Next so the buckets remain exclusive.
Aging is only a presentation safety net, and re-holding with `--until` remains the durable deferral.
Aging is only a presentation safety net, and the durable deferral is the recorded answer: `bin/fm-captain-hold.sh answer <id> --defer-until <date>`, or the keyed intake's `defer` mode with its date.
Do not scrape reports, visual-review artifacts, raw status-event tails, or visible conversation history to supplement current state.
A queued item under `gates` only becomes "next work" when its blocker is gone and its time/date gate has arrived.
Until then it stays queued with the reason.
Expand Down Expand Up @@ -105,6 +105,9 @@ Compose the payload from the same snapshot with the same ranking judgment as the
- Decision cards carry agent-authored copy: a short noun-phrase title, one-line `about` and `decide` context rows, and option labels with hints, with the recommended option marked.
- Card `type` (decision, merge, credential) is your composing judgment from the row's content; no backlog field types a card for you.
- When the card's task is a captain-gated WORK item (the answer should free it to proceed rather than complete it), set the card's `close: "release"` so the answer lifts the hold instead of closing the task; question-shaped items omit it.
- When one authored option means "later", put an explicit `until: "YYYY-MM-DD"` on that option so only that selection records the answer and dates the hold.
- Do not reserve a `defer` answer value: the option's `until` carries the behavior, while the option value remains the captain-facing answer and only `reconcile` stays reserved.
- Give every option on a card a distinct `value`; two options sharing one value are indistinguishable to the answer the captain submits, so `build` refuses the payload and names the repeated value. Two "later" choices with different dates need two values.
- A Charted Next row's optional `kind` separates work from alarms: omit it (or set `"queued"`) for real queued work, and set `"warning"` on every action-free fleet-integrity notice - the `(main-inventory)` gate, the `(return-catchup)` gate, an unavailable secondmate home, and an inventory-mismatch repair notice. The board badges a warning row `needs repair` instead of `waiting` and leaves it out of the Charted Next count, so those rows never read as dispatchable queued work.
- `charted_more` counts omitted queued rows only, while `charted_warning_more` counts omitted warning rows only; keep both counts separate whenever the board payload truncates Charted Next.
- Every Underway row copies the task-identifying `in_flight.name` from the snapshot into an explicit `name` field, which the board leads with while keeping the run status on its second line.
Expand All @@ -122,7 +125,9 @@ Never run `lavish-axi poll` for the board yourself: the armed source's supervise
### Handling a board wake

A board answer arrives as an ordinary `procevent lavish <source-id> <sequence>` check wake. Identify it by comparing the wake source id with `bin/fm-procevent-lavish.sh source-id "$(bin/fm-bearings-board.sh path)"`, regardless of which answer kinds the result contains; then load `process-event-sources` and follow its contract for the result read, adapter classification, and the handled acknowledgement.
Decision answers need no routing from you: the runner feeds the board's binding into `bin/fm-captain-hold.sh`'s one keyed-answer intake, which closes or releases each answered captain-held task at answer time; reconcile any `skipped:` key yourself with a direct `answer`, and when the captain's answer is "later", record it as a deferral with `bin/fm-captain-hold.sh hold <id> --reason "<reason>" --until <date>` instead of a closure.
Decision answers need no routing from you: the runner feeds the board's binding into `bin/fm-captain-hold.sh`'s one keyed-answer intake, which closes, releases, or dates each answered captain-held task at answer time; reconcile any `skipped:` key yourself with a direct `answer` using the same close mode and date.
One skip reason is the exception: a defer key skipped because its date is not later than UTC today was authored on an earlier bearings pass and has since elapsed, so the date is the stale part and replaying it is refused identically.
Ask the captain for a fresh date and answer with that; never substitute a date of your own, and never leave the call closed as though it were answered.
A current structured Reconcile selection closes nothing: the versioned board context carries its exact selected option separately from any typed note, and the adapter routes that selection only into a durable re-check request while preserving the note as provenance.
The rollout-compatible old context still feeds ordinary non-reconcile answers, but its bare or separator-annotated reconcile values and every structurally uncertain choice feed neither intake and remain announced for deliberate handling.
Verify the call's latest state, then retire the request through `bin/fm-captain-hold.sh reconcile close <id> --evidence-file <path>` when it turns out to be moot, or `reconcile note <id> --note-file <path>` when it is genuinely still open.
Expand Down
15 changes: 13 additions & 2 deletions .agents/skills/bearings/assets/board-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
.bb-opt__body { min-width: 0; flex: 1 1 auto; }
.bb-opt__label { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--text-strong); line-height: 1.3; }
.bb-opt__hint { display: block; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.35; margin-top: 1px; }
.bb-opt__until { display: block; font-size: var(--fs-xs); font-weight: 700; color: var(--rust-500); line-height: 1.35; margin-top: 1px; }
.bb-opt__rec { flex: none; align-self: center; font-size: var(--fs-2xs); font-weight: 800; text-transform: uppercase;
letter-spacing: 0.07em; color: var(--navy-700); background: var(--gold-300);
border: 1px solid var(--gold-600); border-radius: var(--radius-xs); padding: 3px 7px 2px; }
Expand Down Expand Up @@ -526,6 +527,7 @@
var body = el("span", "bb-opt__body");
body.appendChild(el("span", "bb-opt__label", o.label));
if (o.hint) body.appendChild(el("span", "bb-opt__hint", o.hint));
if (o.until) body.appendChild(el("span", "bb-opt__until", "deferred until " + o.until));
lab.appendChild(body);
if (item.recommend_value && o.value === item.recommend_value) {
lab.appendChild(el("span", "bb-opt__rec", "rec"));
Expand Down Expand Up @@ -559,23 +561,32 @@
var fd = new FormData(form);
var value = fd.get("answer");
var note = (fd.get("note") || "").trim();
var selectedOption = null;
item.options.forEach(function (option) {
if (option.value === value) selectedOption = option;
});
var deferUntil = selectedOption && selectedOption.until ? selectedOption.until : "";
var displayAnswer = value ? (note ? value + " - " + note : value) : note;
if (!displayAnswer) return;
if (utf8ByteLength(displayAnswer) > 512) {
answerLimit.textContent = "Answer is too long to queue (512 bytes maximum).";
answerLimit.classList.add("is-visible");
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
return;
}
if (deferUntil) displayAnswer += " (deferred until " + deferUntil + ")";
if (window.lavish && window.lavish.queuePrompt) {
/* The versioned context keeps the selected option separate from its
note, while close carries the composer-declared completion mode. */
note. A selected option's own until is a dated defer; the card's
close remains the default for ordinary completion and release. */
var ctxData = {
schema: "fm-bearings-answer.v1",
question: item.key,
selection: value || "",
note: note
};
if (item.close) ctxData.close = item.close;
var close = deferUntil ? "defer" : item.close;
if (close) ctxData.close = close;
if (deferUntil) ctxData.until = deferUntil;
window.lavish.queuePrompt(
"Captain's Call answer - " + item.title + ": " + displayAnswer,
{ tag: "choice", text: item.title + " -> " + displayAnswer, element: form,
Expand Down
9 changes: 6 additions & 3 deletions .agents/skills/captain-hold-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Never close anything the captain owns without recording what he actually said: `
A merge approval uses that existing release path because approval permits the merge to proceed; cleanup closes the work only after it lands and records what shipped.
Closing a held row at merge approval instead records completion before landing, so the backlog claims completion before the work actually ships.
When the answer changes what a task must build, follow `AGENTS.md` section 7's Validate contract to preserve the captain's words in the brief and steer the worker.
When the captain says "later", that is an answer too: re-hold with `bin/fm-captain-hold.sh hold <id> --reason "<reason>" --until <date>` so the item leaves the live Captain's Call and resurfaces on its date, instead of leaving a live-looking card or fabricating a closure.
When the captain says "later", that is an answer too: give the keyed-answer intake its `defer` close mode and a YYYY-MM-DD date, or use `answer --defer-until <date>` for a direct answer, so the captain's exact words are recorded before the existing `tasks-axi hold --until` gate leaves the item dated and held on the same call, with its original age basis intact.
A recorded-answer defer date must be strictly later than today's UTC date; past and same-day dates are refused before the captain's words are recorded, while bare `hold --until` remains the calendar-only scheduling primitive.
A defer without a date is refused rather than assigned a default.
"A keyed answer resolves its matching captain-held task" is one capability with one owner, `bin/fm-captain-hold.sh answers`, and every channel that carries a captain answer feeds it the same task id and answer; a channel never maps keys to tasks, records a decision, or resolves anything itself.
Chat already feeds it through `bin/fm-send.sh --resolve-key`, and a captured-answer source feeds it once bound with `bin/fm-captain-hold.sh bind <source-id>`; bind before arming the source, and key each structured question by the held task's id.
An unbound source and a key that names no captain-held task both simply feed nothing: the answer is still captured and firstmate is still woken, and closing falls back to the direct command above.
Expand All @@ -40,7 +42,8 @@ A bound captured source uses a separate seam: its adapter omits reconcile from k
A remote-secondmate card whose task is absent from the main backlog therefore remains announced but cannot create a main-home request; owner-aware request and mutation routing to the authoritative secondmate home is a separate follow-up.
That board-created request is yours to work off in the turn that receives it: `bin/fm-captain-hold.sh reconcile close <id> --evidence-file <path>` records the EVIDENCE and closes a moot call, while `reconcile note <id> --note-file <path>` annotates a genuinely active call and leaves it held.
Both outcomes refuse unless that task still has the pending request created by the captain's board selection, so neither is a standalone way to mutate a captain call.
A normal captain answer also retires any pending request because the call is settled, including close, release, and idempotent replay paths.
A terminal captain answer retires any pending request because the call is settled, including close, release, and idempotent replay paths.
A defer leaves the call open, so it keeps any pending reconcile request for the still-owed re-check.
A retirement failure makes the command fail without reversing the already-durable answer, close, or note, and `reconcile list` keeps the surviving request visible for retry.
`reconcile list` names every request still outstanding.
Never use `answer` for an evidence-only moot call: `answer` records what the captain said, while `reconcile close` records verified evidence.
Expand All @@ -61,7 +64,7 @@ The absence of a routed work item is not a divergence and the guard never requir
3. Hold that task - or create one captain-held task for the review's open questions - with a concise reason carrying the question and options.
4. Run `complete` with the full captain-held inventory for that review pass.
5. Relay the choices to the captain as decisions from Bearings' Captain's Call section under `AGENTS.md` section 9; do not use the word hold in captain chat.
6. Close each call only through `answer` (or a channel that feeds `answers`), close a board-requested moot call through evidence-backed `reconcile close`, record a still-active reconciliation through `reconcile note`, use `--until` when the captain defers it, or confirm a channel already closed it.
6. Resolve each call only through `answer` (or a channel that feeds `answers`), close a board-requested moot call through evidence-backed `reconcile close`, record a still-active reconciliation through `reconcile note`, use the dated `defer` mode when the captain postpones it, or confirm a channel already resolved it.
7. Confirm Bearings reflects the outcome: answered or reconciled-moot calls leave Captain's Call, released work resumes, active reconciliations remain held, and deferred calls sit in Charted Next with their date.

`bin/fm-captain-hold.sh --help` owns command syntax, close modes, legacy-identity compatibility, completion attestation, retry behavior, and close ordering.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ Mention cost as a courtesy when unusually much work is running, but never block
The configured `tasks-axi` backend is the durable queue; the tracked default is `data/backlog.md`.
It tracks work items only, never agents; persistent secondmates never appear as backlog items.
Work routed to a secondmate is recorded in that secondmate home's own backlog, not the main backlog.
A decision is simply a task held for the captain: create the task with `bin/fm-tasks-axi.sh add` when needed, then always hold it through `bin/fm-captain-hold.sh hold <id> --reason "<reason>"`, with `--until <date>` when the captain defers it.
A decision is simply a task held for the captain: create the task with `bin/fm-tasks-axi.sh add` when needed, then always hold it through `bin/fm-captain-hold.sh hold <id> --reason "<reason>"`, adding `--until <date>` only when the call itself should stay gated until that date; a captain's own "later" is a recorded answer, never a bare re-hold.
When a main-side thread such as a pending captain decision or relay reminder is worth durable tracking, file it as its own work item and hold it through that wrapper.
Captain calls discovered by investigations or visual reviews follow `captain-hold-lifecycle`, which owns their completion gate and recorded-answer rules.
When the automatic transition gate applies, dispatch and completion move the item themselves - `bin/fm-spawn.sh` and `bin/fm-teardown.sh` own those transitions and refuse rather than report success without them - so what remains yours is filing the item before dispatch, recording decisions, and keeping notes current; `docs/configuration.md` owns gate applicability and the manual-backend exception.
Expand Down
35 changes: 32 additions & 3 deletions bin/fm-bearings-board.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
# carries no card type. Its meaning, and the reason it can never reach the
# keyed-answer intake as a blind close, are owned by
# docs/captain-hold-lifecycle.md.
# A defer is not a second reserved answer value: an authored option carries its
# explicit `until: "YYYY-MM-DD"`, and the renderer emits the dated defer only
# when that option is selected.
#
# Validation is fail-closed: the payload must be valid JSON with
# schema=fm-bearings-board.v1 and every renderer-consumed field must satisfy
Expand Down Expand Up @@ -125,6 +128,11 @@ validate_payload() { # <data.json>
then try ((fromdateiso8601 | strftime("%Y-%m-%dT%H:%M:%SZ")) == $filed) catch false
else try (((. + "T00:00:00Z") | fromdateiso8601 | strftime("%Y-%m-%d")) == $filed) catch false
end);
def valid_day:
. as $day
| type == "string"
and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
and (try (((. + "T00:00:00Z") | fromdateiso8601 | strftime("%Y-%m-%d")) == $day) catch false);
def optional_filed:
(has("filed") | not) or (.filed == null) or (.filed | valid_filed);
def optional_string($name): (has($name) | not) or (.[$name] | type == "string");
Expand Down Expand Up @@ -153,7 +161,8 @@ validate_payload() { # <data.json>
| type == "object"
and (.value | slug(128))
and (.label | nonempty_string)
and optional_string("hint")] | all)
and optional_string("hint")
and ((has("until") | not) or (.until | valid_day))] | all)
and (optional_string("about"))
and (optional_string("decide"))
and (optional_string("detail"))
Expand All @@ -168,6 +177,7 @@ validate_payload() { # <data.json>
and (.recommend_value as $recommend
| ([.options[].value] | index($recommend) != null))))
and ([.options[].value] | index("reconcile") == null)
and ([.options[].value] | length == (unique | length))
and (if .type == "merge" then (.risk | nonempty_string) else true end);
def underway_item:
type == "object" and repo_marker and name_marker and (.id | nonempty_string)
Expand Down Expand Up @@ -204,6 +214,20 @@ validate_payload() { # <data.json>
' "$1" >/dev/null
}

# The schema gate is one boolean, so it refuses a repeated option value without
# saying which one. This names the first repeat for the refusal message.
duplicate_option_value() { # <data.json>
jq -r '
[.captains_call[]?
| select(type == "object" and (.options | type == "array"))
| (.key | tostring) as $key
| [.options[]? | select(type == "object") | .value | strings]
| group_by(.)
| map(select(length > 1) | "\(.[0]) (card \($key))")[]]
| first // ""
' "$1" 2>/dev/null
}

# --- Lavish session liveness -------------------------------------------------
# Verified against lavish-axi 0.1.61. `lavish-axi <file>` EXITS 0 even when it
# refuses to reopen a session the captain ended from the browser, reporting
Expand Down Expand Up @@ -358,12 +382,17 @@ await_source_owner() { # <source-id>
}

command_build() {
local data=${1-} board json tmp sid extracted effective owner version pre_reopen_owner
local data=${1-} board json tmp sid extracted effective owner version pre_reopen_owner duplicate
[ "$#" -eq 1 ] || { usage >&2; exit 2; }
command -v jq >/dev/null 2>&1 || fail "jq is required"
[ -f "$data" ] || fail "board data does not exist: $data"
jq empty "$data" 2>/dev/null || fail "board data is not valid JSON: $data"
validate_payload "$data" || fail "board data does not satisfy $BOARD_SCHEMA: $data"
if ! validate_payload "$data"; then
duplicate=$(duplicate_option_value "$data")
[ -z "$duplicate" ] \
|| fail "board data repeats the option value $duplicate; each option value must be unique within its card: $data"
fail "board data does not satisfy $BOARD_SCHEMA: $data"
fi
[ -f "$TEMPLATE" ] && [ ! -L "$TEMPLATE" ] || fail "board template is missing: $TEMPLATE"
[ "$(grep -cxF "$PLACEHOLDER" "$TEMPLATE")" -eq 1 ] \
|| fail "board template does not carry exactly one data slot: $TEMPLATE"
Expand Down
4 changes: 2 additions & 2 deletions bin/fm-bearings-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
# floored age), and are counted in omitted[].
# --all-decisions reveals every captain hold available within the bounded snapshot
# and drops its gate, so a hold is never in both Captain's Call and Charted Next.
# Aging is a projection safety net only; the durable
# deferral remains re-holding with --until.
# Aging is a projection safety net only; the durable deferral is the recorded
# `answer --defer-until` (the keyed intake's `defer` mode).
#
# Ordinary Charted Next gates are ordered by durable filed date, newest first,
# before the FM_BEARINGS_GATES bound is applied. Gates without a comparable filed
Expand Down
Loading
Loading