Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d69d559
feat: add lavish decision board skill
ruby-dlee Jul 21, 2026
6d408ce
no-mistakes(document): Align Lavish decision-board guidance
ruby-dlee Jul 21, 2026
6b650f3
docs: require dedicated lavish decision windows
ruby-dlee Jul 21, 2026
1c49e9a
no-mistakes(review): Fix Lavish board scope and layout verification
ruby-dlee Jul 22, 2026
0e97466
no-mistakes(review): Bind Lavish layout verification to served board
ruby-dlee Jul 22, 2026
bf41760
no-mistakes(review): Require completed Lavish audit before surfacing …
ruby-dlee Jul 22, 2026
c5838ab
no-mistakes(document): Clarify universal Lavish decision-board routing
ruby-dlee Jul 22, 2026
15007fc
no-mistakes(document): Route captain decisions through Lavish boards
ruby-dlee Jul 22, 2026
c4b4e7b
fix: scope lavish board guidance
ruby-dlee Jul 22, 2026
366c9d7
fix: prevent phantom Lavish submissions
ruby-dlee Jul 22, 2026
8697671
feat: add lavish decision board skill
ruby-dlee Jul 21, 2026
2279364
no-mistakes(document): Align Lavish decision-board guidance
ruby-dlee Jul 21, 2026
83377f0
docs: require dedicated lavish decision windows
ruby-dlee Jul 21, 2026
896c21d
no-mistakes(review): Fix Lavish board scope and layout verification
ruby-dlee Jul 22, 2026
048cb35
no-mistakes(review): Bind Lavish layout verification to served board
ruby-dlee Jul 22, 2026
dcea35a
no-mistakes(review): Require completed Lavish audit before surfacing …
ruby-dlee Jul 22, 2026
2323d9e
no-mistakes(document): Clarify universal Lavish decision-board routing
ruby-dlee Jul 22, 2026
d2502e3
no-mistakes(document): Route captain decisions through Lavish boards
ruby-dlee Jul 22, 2026
1f10a1a
fix: scope lavish board guidance
ruby-dlee Jul 22, 2026
c2554c8
fix: prevent phantom Lavish submissions
ruby-dlee Jul 22, 2026
9ad0f57
no-mistakes(review): Make Lavish decision submissions atomic and comp…
ruby-dlee Jul 22, 2026
9c0ea77
no-mistakes(review): Reject incomplete Lavish decision forms and answers
ruby-dlee Jul 22, 2026
fca31b2
docs: make Lavish plans actionable
ruby-dlee Jul 22, 2026
c877876
no-mistakes(review): Captain: validate Lavish decision controls befor…
ruby-dlee Jul 22, 2026
5a0fc28
no-mistakes(review): Captain: clear stale Lavish submission alerts
ruby-dlee Jul 22, 2026
ece5d0d
merge: preserve section 13 skill triggers
ruby-dlee Jul 22, 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
68 changes: 68 additions & 0 deletions .agents/skills/lavish-decision-boards/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: lavish-decision-boards
description: >-
Agent-only workflow for building actionable, captain-facing Lavish decision boards and approval or triage surfaces.
Use before creating or revising any Lavish board that asks the captain to choose options, approve a plan, triage findings, set scope, or provide structured feedback.
user-invocable: false
metadata:
internal: true
---

# lavish-decision-boards

Build a captain-facing decision surface as an actionable, layout-safe Lavish board.
A read-only status page is not a decision surface.

## Build

1. Copy `assets/lavish-board-template.html` from this skill directory into the board's working location.
Use the asset as the starting point instead of recreating its typography or layout reset.
Do not tighten its line heights, tracking, text padding, wrapping, or card spacing because those values are the layout-audit-safe baseline.
2. Run `lavish-axi playbook input` before writing the board, plus every other playbook that matches the content.
Treat `lavish-axi --help` as the authority for current CLI behavior and flags.
3. Give every decision a native radio group or select control, but hold all choices in local page state until one explicit `Send answers` action.
Never call `window.lavish.queuePrompt` on selection, change, or a per-question step; a partial selection must never be actionable.
The single send handler must first gather and validate all current form state, then call `window.lavish.queuePrompt` exactly once with one structured batch envelope and immediately call `window.lavish.sendQueuedPrompts()` in that same click handler.
The envelope must contain `submission: "explicit-send-batch"`, an `answers` array, and a manifest with the full expected question-key set and count.
Reject missing or duplicate question keys before queueing.
Acquire a one-shot in-flight lock and disable the send button before queueing so rapid clicks cannot duplicate the batch.
This keeps the Lavish queue empty before the explicit send, so a disconnect has nothing to auto-flush and in-progress input cannot reach the agent.
4. Choose a durable feedback destination before polling, such as the task spec, backlog note, or task data file.

## Show the actionable plan

- Include every in-flight item; silently omitted work makes the board incomplete.
- For each item, show the real end-to-end path to done: every remaining step, the owner or condition gating it, and the concrete downstream consequence when it lands; never substitute a bare status label.
- When work spans execution resources, show its capacity-aware distribution by lane, harness, and model using current capacity and quota without embedding values that will rot.
- State the pacing and concurrency limits being held to protect fleet health.
- Clearly separate work proceeding autonomously under standing rules from items that genuinely require the captain's decision, and give decision controls only to the latter.

## Serve and verify

1. Serve the board without auto-opening by passing `--no-open` or setting `LAVISH_AXI_NO_OPEN=1`.
2. Extract the printed session URL without its surrounding double quotes.
A safe extraction pattern is `grep -oE 'https?://[^ "]+'`; verify that no quote or punctuation trails the URL.
3. Open every board in its own dedicated Chrome window with `open -na "Google Chrome" --args --new-window "<url>"`.
4. Start `lavish-axi poll <file>` silently and leave it running while review continues.
Re-run it after every response while review continues.
Treat the board as not ready until the tool-authoritative connection signal confirms that it is genuinely connected.
The transition to connected can take several minutes and is expected; during that lag, do not act on a poll return or prematurely re-serve, re-open, abandon, or otherwise thrash the session.
Create and select a `chrome-devtools-axi` page for the exact served URL, then use a bounded retry to inspect that page's snapshots.
Pass only after the layout-audit-in-progress indicator has cleared and no layout-issue indicator is present, confirming zero error-severity `layout_warnings` for that board.
A returned snapshot alone is not success; if the bound expires while the audit remains in progress, treat the board as unverified and do not surface it.
Consult `chrome-devtools-axi --help` and the relevant command help for current commands and flags.
Do not announce the board as ready until that check passes.
If the audit finds an error while the layout gate is still holding the board, fix it and verify again before the captain can answer.
5. Name the board when surfacing it so the captain knows which decision surface is awaiting action.
Accompany it in the CLI with only a bare pointer to the board, never the substantive decision content.

## Protect answers

- Never edit a served board while the captain is answering because live reload clears in-progress input.
- When poll feedback arrives, write every annotation to the chosen durable file immediately, before interpreting it, acting on it, or doing anything else.
- Never rely on poll output or conversation memory as the only copy because ephemeral poll output can be reaped.
- Treat a `lavish-axi poll` return as transport or lifecycle output, not automatically as the captain's answer.
- Act only after genuine connection and receipt of one unambiguous batch with `submission: "explicit-send-batch"`, a nonempty structured `answers` array, and a manifest containing `expectedQuestionKeys` and `expectedCount`.
- Accept the batch only when the manifest keys are nonempty and unique, its count equals its key count, and every manifest key has exactly one structured answer entry with the same key and a nonempty `answer` value.
- Reject absent, empty, extra, or duplicate answer keys, every manifest mismatch, and every disconnect, UI flicker, re-poll, layout or audit return, session event, unmarked return, or ambiguous return; ignore them and keep waiting.
- If a return is ambiguous or lacks a clear explicit-decision payload, treat it as not submitted and do not act until the captain's actual answer is verified in the payload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<!doctype html>
<!--
LAVISH-SAFE DECISION BOARD TEMPLATE
Copy this file, replace the placeholder content, and verify that the browser
audit reports zero error-severity layout warnings before announcing it ready.

Preserve these layout constraints:
1. Text line-height is never 1. Display text is at least 1.2, body text is at
least 1.5, and headings are at least 1.35.
2. Large display text keeps 2px bottom padding and letter-spacing no tighter
than -0.01em so the glyph box is not clipped.
3. Headings remain sans-serif with neutral tracking, right padding, and
bottom spacing.
4. Every flex or grid child that holds text keeps min-width:0 and
overflow-wrap:anywhere.
5. Cards and rows keep enough vertical padding that line boxes do not touch
a border.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Decision board</title>
<style>
:root{
--bg:#f4f6f8; --panel:#fff; --panel-2:#fbfcfd; --ink:#141a1f; --ink-2:#4a5560;
--line:#e2e7ec; --line-2:#d3dae1; --accent:#1f6feb; --accent-soft:#e7f0ff;
--amber:#b7791f; --amber-soft:#fdf3e2; --green:#1a7f5a; --green-soft:#e3f5ec;
--grey:#6b7680; --grey-soft:#eef1f4; --red:#c0392b; --red-soft:#fbe9e6;
--shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05);
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
}
@media (prefers-color-scheme:dark){:root{
--bg:#0e1216; --panel:#161c22; --panel-2:#131920; --ink:#e6ebef; --ink-2:#9aa6b1;
--line:#232c35; --line-2:#2c3742; --accent:#589bff; --accent-soft:#16283f;
--amber:#e0b25a; --amber-soft:#2a2313; --green:#5fcf9e; --green-soft:#12271e;
--grey:#8b96a1; --grey-soft:#1c242c; --red:#f0857a; --red-soft:#2c1714;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28);
}}
:root[data-theme="light"]{color-scheme:light;
--bg:#f4f6f8;--panel:#fff;--panel-2:#fbfcfd;--ink:#141a1f;--ink-2:#4a5560;--line:#e2e7ec;--line-2:#d3dae1;
--accent:#1f6feb;--accent-soft:#e7f0ff;--amber:#b7791f;--amber-soft:#fdf3e2;--green:#1a7f5a;--green-soft:#e3f5ec;
--grey:#6b7680;--grey-soft:#eef1f4;--red:#c0392b;--red-soft:#fbe9e6;
--shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05);}
:root[data-theme="dark"]{color-scheme:dark;
--bg:#0e1216;--panel:#161c22;--panel-2:#131920;--ink:#e6ebef;--ink-2:#9aa6b1;--line:#232c35;--line-2:#2c3742;
--accent:#589bff;--accent-soft:#16283f;--amber:#e0b25a;--amber-soft:#2a2313;--green:#5fcf9e;--green-soft:#12271e;
--grey:#8b96a1;--grey-soft:#1c242c;--red:#f0857a;--red-soft:#2c1714;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28);}

*{box-sizing:border-box;min-width:0}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow-wrap:anywhere}
h1,h2,h3{font-family:var(--sans);letter-spacing:0}
h1{font-size:22px;line-height:1.3;margin:0}
h3{font-size:16.5px;line-height:1.38;font-weight:650;margin:0 0 9px;padding-right:2px}
p{margin:0 0 12px;overflow-wrap:anywhere}
.wrap{max-width:1060px;margin:0 auto;padding:32px 22px 80px}

.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:0 0 30px}
@media (max-width:720px){.summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
.stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow)}
.stat .n{font-size:26px;font-weight:700;line-height:1.2;letter-spacing:-.01em;padding-bottom:2px;font-variant-numeric:tabular-nums}
.stat .l{font-size:12px;color:var(--ink-2);margin-top:6px}

.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin:0 0 16px;box-shadow:var(--shadow)}
.card.decision{border-left:3px solid var(--amber)}
.pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:650;padding:2px 9px;border-radius:999px;white-space:nowrap}
.pill.amber{background:var(--amber-soft);color:var(--amber)} .pill.green{background:var(--green-soft);color:var(--green)}
.rec{background:var(--green-soft);border-radius:9px;padding:9px 12px;font-size:13.5px;margin:0 0 14px}
form.q{border-top:1px dashed var(--line-2);padding-top:13px;margin:0}
form.q .opts{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}
form.q label{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;border:1px solid var(--line);border-radius:9px;cursor:pointer;font-size:14px}
form.q label:has(input:checked){border-color:var(--accent);background:var(--accent-soft)}
form.q input[type=radio]{margin-top:2px;accent-color:var(--accent);width:16px;height:16px;flex:none}
.selection-status{font-size:13px;color:var(--ink-2);margin:0 0 10px;min-height:20px}
.submission-status{font-size:13px;color:var(--red);margin:10px 0 0;min-height:20px}
button{font-family:inherit;font-size:13.5px;font-weight:650;border-radius:9px;cursor:pointer;border:1px solid var(--accent);background:var(--accent);color:#fff;padding:8px 15px}
</style>
</head>
<body>
<div class="wrap">
<h1>Decision board title</h1>

<!-- Choices stay local until the single Send answers click queues and sends every answer synchronously. -->
<div class="card decision">
<span class="pill amber">decision needed</span>
<h3>What should be decided?</h3>
<p>State the relevant context in one or two sentences.</p>
<div class="rec"><b>Recommendation:</b> Choose option A because it best matches the stated goal.</div>
<form class="q" data-lavish-question="decision-key" data-lavish-label="Short decision label">
<div class="opts">
<label><input type="radio" name="decision-key" value="A - first option (recommended)" required> A. First option</label>
<label><input type="radio" name="decision-key" value="B - second option"> B. Second option</label>
</div>
<p class="selection-status" data-selection-status role="status" aria-live="polite">No answer selected.</p>
</form>
</div>

<button type="button" data-send-answers onclick="sendAnswers()">Send answers</button>
<p class="submission-status" data-submission-status role="alert" aria-live="assertive"></p>
</div>
<script>
document.querySelectorAll('form.q').forEach(function(form){
form.addEventListener('change',function(){
var key=(form.dataset.lavishQuestion||'').trim();
if(!key){
form.querySelector('[data-selection-status]').textContent=
'Configuration error: add a unique data-lavish-question value to this decision form.';
return;
}
var choice=new FormData(form).get(key);
form.querySelector('[data-selection-status]').textContent=
choice?'Selected locally (not sent): '+choice:'No answer selected.';
});
});

var submissionInFlight=false;

function sendAnswers(){
var forms=Array.from(document.querySelectorAll('form.q'));
var answers=[];
var firstIncomplete=null;
var sendButton=document.querySelector('[data-send-answers]');
var submissionStatus=document.querySelector('[data-submission-status]');
if(submissionInFlight)return;
function failConfiguration(message){
sendButton.disabled=true;
sendButton.textContent='Cannot send answers';
submissionStatus.textContent=message;
}
if(!forms.length){
failConfiguration('Configuration error: add at least one decision form before serving this board.');
return;
}
var keyedForms=forms.map(function(form,index){
return {form:form,key:(form.dataset.lavishQuestion||'').trim(),index:index};
});
var missingKey=keyedForms.find(function(item){return !item.key;});
if(missingKey){
failConfiguration('Configuration error: decision form '+(missingKey.index+1)+' needs a unique nonempty data-lavish-question value.');
return;
}
var expectedQuestionKeys=keyedForms.map(function(item){return item.key;});
var uniqueQuestionKeys=new Set(expectedQuestionKeys);
if(uniqueQuestionKeys.size!==expectedQuestionKeys.length){
failConfiguration('Configuration error: every decision form needs a unique data-lavish-question value.');
return;
}
keyedForms.forEach(function(item){
item.controls=Array.from(item.form.elements).filter(function(control){
return control.matches('input[type="radio"],select')&&
!control.matches(':disabled')&&control.name===item.key;
});
});
var missingControl=keyedForms.find(function(item){return !item.controls.length;});
if(missingControl){
failConfiguration('Configuration error: decision form '+(missingControl.index+1)+
' needs an enabled radio or select named "'+missingControl.key+'".');
return;
}
keyedForms.forEach(function(item){
var form=item.form;
var key=item.key;
var choice=new FormData(form).get(key);
if(typeof choice!=='string'||!choice.trim()){
firstIncomplete=firstIncomplete||item;
return;
}
answers.push({key:key,label:form.dataset.lavishLabel||key,answer:choice});
});
if(firstIncomplete){
var incompleteLabel=firstIncomplete.form.dataset.lavishLabel||firstIncomplete.key;
submissionStatus.textContent='Answer required: choose an option for "'+incompleteLabel+'".';
firstIncomplete.form.querySelector('[data-selection-status]').textContent=
'No answer selected. Choose an option before sending.';
firstIncomplete.form.reportValidity();
firstIncomplete.controls[0].focus();
return;
}
if(!window.lavish||!window.lavish.queuePrompt||!window.lavish.sendQueuedPrompts)return;
var batch={
submission:'explicit-send-batch',
manifest:{expectedQuestionKeys:expectedQuestionKeys,expectedCount:expectedQuestionKeys.length},
answers:answers
};
submissionInFlight=true;
sendButton.disabled=true;
submissionStatus.textContent='';
window.lavish.queuePrompt('Explicitly submitted decision batch: '+JSON.stringify(batch),
{tag:'decision-batch',text:'Explicitly submitted decision batch',element:sendButton,
queueKey:'explicit-decision-batch',data:batch});
window.lavish.sendQueuedPrompts();
forms.forEach(function(form,index){
form.querySelector('[data-selection-status]').textContent='Sent explicitly: '+answers[index].answer;
});
}
</script>
</body>
</html>
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ These skills are not captain-invocable; they are conditional operating reference
- `firstmate-codexapp` - load before coordinating a visible Codex Desktop thread, evaluating a Codex App backend request, or reconciling Codex Desktop host-tool smoke evidence for Firstmate work.
- `skill-authoring-standard` - load before authoring or substantially editing any skill in this repo or any project, and before briefing a project crew to do so.
- `firstmate-coding-guidelines` - load before changing firstmate's shared, tracked material, as defined by section 1's list, whether editing directly or briefing a crewmate for a firstmate-repo task.
- `lavish-decision-boards` - load before creating or revising a captain-facing Lavish board that asks the captain to make decisions or provide structured feedback.
- `eks-usage` - load before running `kubectl` or Amazon EKS commands, on an EKS IAM, authenticator, TLS, or connectivity error, or whenever the active cluster or context is uncertain.

## 14. X mode
Expand Down