Skip to content

Commit 609cf8b

Browse files
authored
feat: add safe actionable Lavish decision boards (#11)
* feat: add lavish decision board skill * no-mistakes(document): Align Lavish decision-board guidance * docs: require dedicated lavish decision windows * no-mistakes(review): Fix Lavish board scope and layout verification * no-mistakes(review): Bind Lavish layout verification to served board * no-mistakes(review): Require completed Lavish audit before surfacing boards * no-mistakes(document): Clarify universal Lavish decision-board routing * no-mistakes(document): Route captain decisions through Lavish boards * fix: scope lavish board guidance * fix: prevent phantom Lavish submissions * feat: add lavish decision board skill * no-mistakes(document): Align Lavish decision-board guidance * docs: require dedicated lavish decision windows * no-mistakes(review): Fix Lavish board scope and layout verification * no-mistakes(review): Bind Lavish layout verification to served board * no-mistakes(review): Require completed Lavish audit before surfacing boards * no-mistakes(document): Clarify universal Lavish decision-board routing * no-mistakes(document): Route captain decisions through Lavish boards * fix: scope lavish board guidance * fix: prevent phantom Lavish submissions * no-mistakes(review): Make Lavish decision submissions atomic and complete * no-mistakes(review): Reject incomplete Lavish decision forms and answers * no-mistakes(review): Captain: validate Lavish decision controls before submission * no-mistakes(review): Captain: clear stale Lavish submission alerts
1 parent 6be4c7c commit 609cf8b

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
name: lavish-decision-boards
3+
description: >-
4+
Agent-only workflow for building actionable, captain-facing Lavish decision boards and approval or triage surfaces.
5+
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.
6+
user-invocable: false
7+
metadata:
8+
internal: true
9+
---
10+
11+
# lavish-decision-boards
12+
13+
Build a captain-facing decision surface as an actionable, layout-safe Lavish board.
14+
A read-only status page is not a decision surface.
15+
16+
## Build
17+
18+
1. Copy `assets/lavish-board-template.html` from this skill directory into the board's working location.
19+
Use the asset as the starting point instead of recreating its typography or layout reset.
20+
Do not tighten its line heights, tracking, text padding, wrapping, or card spacing because those values are the layout-audit-safe baseline.
21+
2. Run `lavish-axi playbook input` before writing the board, plus every other playbook that matches the content.
22+
Treat `lavish-axi --help` as the authority for current CLI behavior and flags.
23+
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.
24+
Never call `window.lavish.queuePrompt` on selection, change, or a per-question step; a partial selection must never be actionable.
25+
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.
26+
The envelope must contain `submission: "explicit-send-batch"`, an `answers` array, and a manifest with the full expected question-key set and count.
27+
Reject missing or duplicate question keys before queueing.
28+
Acquire a one-shot in-flight lock and disable the send button before queueing so rapid clicks cannot duplicate the batch.
29+
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.
30+
4. Choose a durable feedback destination before polling, such as the task spec, backlog note, or task data file.
31+
32+
## Show the actionable plan
33+
34+
- Include every in-flight item; silently omitted work makes the board incomplete.
35+
- 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.
36+
- 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.
37+
- State the pacing and concurrency limits being held to protect fleet health.
38+
- 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.
39+
40+
## Serve and verify
41+
42+
1. Serve the board without auto-opening by passing `--no-open` or setting `LAVISH_AXI_NO_OPEN=1`.
43+
2. Extract the printed session URL without its surrounding double quotes.
44+
A safe extraction pattern is `grep -oE 'https?://[^ "]+'`; verify that no quote or punctuation trails the URL.
45+
3. Open every board in its own dedicated Chrome window with `open -na "Google Chrome" --args --new-window "<url>"`.
46+
4. Start `lavish-axi poll <file>` silently and leave it running while review continues.
47+
Re-run it after every response while review continues.
48+
Treat the board as not ready until the tool-authoritative connection signal confirms that it is genuinely connected.
49+
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.
50+
Create and select a `chrome-devtools-axi` page for the exact served URL, then use a bounded retry to inspect that page's snapshots.
51+
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.
52+
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.
53+
Consult `chrome-devtools-axi --help` and the relevant command help for current commands and flags.
54+
Do not announce the board as ready until that check passes.
55+
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.
56+
5. Name the board when surfacing it so the captain knows which decision surface is awaiting action.
57+
Accompany it in the CLI with only a bare pointer to the board, never the substantive decision content.
58+
59+
## Protect answers
60+
61+
- Never edit a served board while the captain is answering because live reload clears in-progress input.
62+
- When poll feedback arrives, write every annotation to the chosen durable file immediately, before interpreting it, acting on it, or doing anything else.
63+
- Never rely on poll output or conversation memory as the only copy because ephemeral poll output can be reaped.
64+
- Treat a `lavish-axi poll` return as transport or lifecycle output, not automatically as the captain's answer.
65+
- 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`.
66+
- 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.
67+
- 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.
68+
- 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.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<!doctype html>
2+
<!--
3+
LAVISH-SAFE DECISION BOARD TEMPLATE
4+
Copy this file, replace the placeholder content, and verify that the browser
5+
audit reports zero error-severity layout warnings before announcing it ready.
6+
7+
Preserve these layout constraints:
8+
1. Text line-height is never 1. Display text is at least 1.2, body text is at
9+
least 1.5, and headings are at least 1.35.
10+
2. Large display text keeps 2px bottom padding and letter-spacing no tighter
11+
than -0.01em so the glyph box is not clipped.
12+
3. Headings remain sans-serif with neutral tracking, right padding, and
13+
bottom spacing.
14+
4. Every flex or grid child that holds text keeps min-width:0 and
15+
overflow-wrap:anywhere.
16+
5. Cards and rows keep enough vertical padding that line boxes do not touch
17+
a border.
18+
-->
19+
<html lang="en">
20+
<head>
21+
<meta charset="utf-8">
22+
<meta name="viewport" content="width=device-width, initial-scale=1">
23+
<title>Decision board</title>
24+
<style>
25+
:root{
26+
--bg:#f4f6f8; --panel:#fff; --panel-2:#fbfcfd; --ink:#141a1f; --ink-2:#4a5560;
27+
--line:#e2e7ec; --line-2:#d3dae1; --accent:#1f6feb; --accent-soft:#e7f0ff;
28+
--amber:#b7791f; --amber-soft:#fdf3e2; --green:#1a7f5a; --green-soft:#e3f5ec;
29+
--grey:#6b7680; --grey-soft:#eef1f4; --red:#c0392b; --red-soft:#fbe9e6;
30+
--shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05);
31+
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
32+
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
33+
}
34+
@media (prefers-color-scheme:dark){:root{
35+
--bg:#0e1216; --panel:#161c22; --panel-2:#131920; --ink:#e6ebef; --ink-2:#9aa6b1;
36+
--line:#232c35; --line-2:#2c3742; --accent:#589bff; --accent-soft:#16283f;
37+
--amber:#e0b25a; --amber-soft:#2a2313; --green:#5fcf9e; --green-soft:#12271e;
38+
--grey:#8b96a1; --grey-soft:#1c242c; --red:#f0857a; --red-soft:#2c1714;
39+
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28);
40+
}}
41+
:root[data-theme="light"]{color-scheme:light;
42+
--bg:#f4f6f8;--panel:#fff;--panel-2:#fbfcfd;--ink:#141a1f;--ink-2:#4a5560;--line:#e2e7ec;--line-2:#d3dae1;
43+
--accent:#1f6feb;--accent-soft:#e7f0ff;--amber:#b7791f;--amber-soft:#fdf3e2;--green:#1a7f5a;--green-soft:#e3f5ec;
44+
--grey:#6b7680;--grey-soft:#eef1f4;--red:#c0392b;--red-soft:#fbe9e6;
45+
--shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05);}
46+
:root[data-theme="dark"]{color-scheme:dark;
47+
--bg:#0e1216;--panel:#161c22;--panel-2:#131920;--ink:#e6ebef;--ink-2:#9aa6b1;--line:#232c35;--line-2:#2c3742;
48+
--accent:#589bff;--accent-soft:#16283f;--amber:#e0b25a;--amber-soft:#2a2313;--green:#5fcf9e;--green-soft:#12271e;
49+
--grey:#8b96a1;--grey-soft:#1c242c;--red:#f0857a;--red-soft:#2c1714;
50+
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28);}
51+
52+
*{box-sizing:border-box;min-width:0}
53+
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
54+
font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow-wrap:anywhere}
55+
h1,h2,h3{font-family:var(--sans);letter-spacing:0}
56+
h1{font-size:22px;line-height:1.3;margin:0}
57+
h3{font-size:16.5px;line-height:1.38;font-weight:650;margin:0 0 9px;padding-right:2px}
58+
p{margin:0 0 12px;overflow-wrap:anywhere}
59+
.wrap{max-width:1060px;margin:0 auto;padding:32px 22px 80px}
60+
61+
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:0 0 30px}
62+
@media (max-width:720px){.summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
63+
.stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow)}
64+
.stat .n{font-size:26px;font-weight:700;line-height:1.2;letter-spacing:-.01em;padding-bottom:2px;font-variant-numeric:tabular-nums}
65+
.stat .l{font-size:12px;color:var(--ink-2);margin-top:6px}
66+
67+
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin:0 0 16px;box-shadow:var(--shadow)}
68+
.card.decision{border-left:3px solid var(--amber)}
69+
.pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:650;padding:2px 9px;border-radius:999px;white-space:nowrap}
70+
.pill.amber{background:var(--amber-soft);color:var(--amber)} .pill.green{background:var(--green-soft);color:var(--green)}
71+
.rec{background:var(--green-soft);border-radius:9px;padding:9px 12px;font-size:13.5px;margin:0 0 14px}
72+
form.q{border-top:1px dashed var(--line-2);padding-top:13px;margin:0}
73+
form.q .opts{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}
74+
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}
75+
form.q label:has(input:checked){border-color:var(--accent);background:var(--accent-soft)}
76+
form.q input[type=radio]{margin-top:2px;accent-color:var(--accent);width:16px;height:16px;flex:none}
77+
.selection-status{font-size:13px;color:var(--ink-2);margin:0 0 10px;min-height:20px}
78+
.submission-status{font-size:13px;color:var(--red);margin:10px 0 0;min-height:20px}
79+
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}
80+
</style>
81+
</head>
82+
<body>
83+
<div class="wrap">
84+
<h1>Decision board title</h1>
85+
86+
<!-- Choices stay local until the single Send answers click queues and sends every answer synchronously. -->
87+
<div class="card decision">
88+
<span class="pill amber">decision needed</span>
89+
<h3>What should be decided?</h3>
90+
<p>State the relevant context in one or two sentences.</p>
91+
<div class="rec"><b>Recommendation:</b> Choose option A because it best matches the stated goal.</div>
92+
<form class="q" data-lavish-question="decision-key" data-lavish-label="Short decision label">
93+
<div class="opts">
94+
<label><input type="radio" name="decision-key" value="A - first option (recommended)" required> A. First option</label>
95+
<label><input type="radio" name="decision-key" value="B - second option"> B. Second option</label>
96+
</div>
97+
<p class="selection-status" data-selection-status role="status" aria-live="polite">No answer selected.</p>
98+
</form>
99+
</div>
100+
101+
<button type="button" data-send-answers onclick="sendAnswers()">Send answers</button>
102+
<p class="submission-status" data-submission-status role="alert" aria-live="assertive"></p>
103+
</div>
104+
<script>
105+
document.querySelectorAll('form.q').forEach(function(form){
106+
form.addEventListener('change',function(){
107+
var key=(form.dataset.lavishQuestion||'').trim();
108+
if(!key){
109+
form.querySelector('[data-selection-status]').textContent=
110+
'Configuration error: add a unique data-lavish-question value to this decision form.';
111+
return;
112+
}
113+
var choice=new FormData(form).get(key);
114+
form.querySelector('[data-selection-status]').textContent=
115+
choice?'Selected locally (not sent): '+choice:'No answer selected.';
116+
});
117+
});
118+
119+
var submissionInFlight=false;
120+
121+
function sendAnswers(){
122+
var forms=Array.from(document.querySelectorAll('form.q'));
123+
var answers=[];
124+
var firstIncomplete=null;
125+
var sendButton=document.querySelector('[data-send-answers]');
126+
var submissionStatus=document.querySelector('[data-submission-status]');
127+
if(submissionInFlight)return;
128+
function failConfiguration(message){
129+
sendButton.disabled=true;
130+
sendButton.textContent='Cannot send answers';
131+
submissionStatus.textContent=message;
132+
}
133+
if(!forms.length){
134+
failConfiguration('Configuration error: add at least one decision form before serving this board.');
135+
return;
136+
}
137+
var keyedForms=forms.map(function(form,index){
138+
return {form:form,key:(form.dataset.lavishQuestion||'').trim(),index:index};
139+
});
140+
var missingKey=keyedForms.find(function(item){return !item.key;});
141+
if(missingKey){
142+
failConfiguration('Configuration error: decision form '+(missingKey.index+1)+' needs a unique nonempty data-lavish-question value.');
143+
return;
144+
}
145+
var expectedQuestionKeys=keyedForms.map(function(item){return item.key;});
146+
var uniqueQuestionKeys=new Set(expectedQuestionKeys);
147+
if(uniqueQuestionKeys.size!==expectedQuestionKeys.length){
148+
failConfiguration('Configuration error: every decision form needs a unique data-lavish-question value.');
149+
return;
150+
}
151+
keyedForms.forEach(function(item){
152+
item.controls=Array.from(item.form.elements).filter(function(control){
153+
return control.matches('input[type="radio"],select')&&
154+
!control.matches(':disabled')&&control.name===item.key;
155+
});
156+
});
157+
var missingControl=keyedForms.find(function(item){return !item.controls.length;});
158+
if(missingControl){
159+
failConfiguration('Configuration error: decision form '+(missingControl.index+1)+
160+
' needs an enabled radio or select named "'+missingControl.key+'".');
161+
return;
162+
}
163+
keyedForms.forEach(function(item){
164+
var form=item.form;
165+
var key=item.key;
166+
var choice=new FormData(form).get(key);
167+
if(typeof choice!=='string'||!choice.trim()){
168+
firstIncomplete=firstIncomplete||item;
169+
return;
170+
}
171+
answers.push({key:key,label:form.dataset.lavishLabel||key,answer:choice});
172+
});
173+
if(firstIncomplete){
174+
var incompleteLabel=firstIncomplete.form.dataset.lavishLabel||firstIncomplete.key;
175+
submissionStatus.textContent='Answer required: choose an option for "'+incompleteLabel+'".';
176+
firstIncomplete.form.querySelector('[data-selection-status]').textContent=
177+
'No answer selected. Choose an option before sending.';
178+
firstIncomplete.form.reportValidity();
179+
firstIncomplete.controls[0].focus();
180+
return;
181+
}
182+
if(!window.lavish||!window.lavish.queuePrompt||!window.lavish.sendQueuedPrompts)return;
183+
var batch={
184+
submission:'explicit-send-batch',
185+
manifest:{expectedQuestionKeys:expectedQuestionKeys,expectedCount:expectedQuestionKeys.length},
186+
answers:answers
187+
};
188+
submissionInFlight=true;
189+
sendButton.disabled=true;
190+
submissionStatus.textContent='';
191+
window.lavish.queuePrompt('Explicitly submitted decision batch: '+JSON.stringify(batch),
192+
{tag:'decision-batch',text:'Explicitly submitted decision batch',element:sendButton,
193+
queueKey:'explicit-decision-batch',data:batch});
194+
window.lavish.sendQueuedPrompts();
195+
forms.forEach(function(form,index){
196+
form.querySelector('[data-selection-status]').textContent='Sent explicitly: '+answers[index].answer;
197+
});
198+
}
199+
</script>
200+
</body>
201+
</html>

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ These skills are not captain-invocable; they are conditional operating reference
783783
- `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.
784784
- `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.
785785
- `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.
786+
- `lavish-decision-boards` - load before creating or revising a captain-facing Lavish board that asks the captain to make decisions or provide structured feedback.
786787
- `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.
787788

788789
## 14. X mode

0 commit comments

Comments
 (0)