austere-axi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ {
2
+ "format": 1,
3
+ "pack": "@austere-axi/pack-mermaid",
4
+ "version": "0.3.0",
5
+ "groups": [
6
+ {
7
+ "name": "Content",
8
+ "notes": [
9
+ "- Mermaid for sequences, state machines, ER, gantt and quick flowcharts."
10
+ ]
11
+ }
12
+ ],
13
+ "components": [
14
+ {
15
+ "name": "Mermaid",
16
+ "group": "Content",
17
+ "signature": "Mermaid(source: string, caption?: string)",
18
+ "description": "Diagram from Mermaid source (flowchart, sequence, state, class, ER, gantt). source is a raw multi-line string. Keep node labels to a few words; clicked nodes reach the agent by label.",
19
+ "tier": "core",
20
+ "pack": "@austere-axi/pack-mermaid",
21
+ "examples": [
22
+ "requestPath = Mermaid(\"flowchart LR\n client[Client] --> api[API]\n api --> pg[(Postgres)]\", \"Request path\")"
23
+ ],
24
+ "slots": [
25
+ "Card.children",
26
+ "TabItem.content",
27
+ "AccordionItem.content"
28
+ ]
29
+ }
30
+ ],
31
+ "api": 1,
32
+ "integrity": "sha384-acnBDiVCLohQNVzIFcx+s7FgXMLlpNvaupoEOLmQ2v0zzt4spAHbKQDpP3sWsqpS",
33
+ "size": 5782,
34
+ "css": true,
35
+ "defs": {
36
+ "Mermaid": {
37
+ "type": "object",
38
+ "properties": {
39
+ "source": {
40
+ "type": "string"
41
+ },
42
+ "caption": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "source"
48
+ ],
49
+ "additionalProperties": false,
50
+ "description": "Diagram from Mermaid source (flowchart, sequence, state, class, ER, gantt). source is a raw multi-line string. Keep node labels to a few words; clicked nodes reach the agent by label."
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,25 @@
1
+ (()=>{var F=Object.create;var B=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var K=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty;var p=(e,t)=>()=>{try{return t||e((t={exports:{}}).exports,t),t.exports}catch(r){throw t=0,r}};var U=(e,t,r,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of W(t))!O.call(e,n)&&n!==r&&B(e,n,{get:()=>t[n],enumerable:!(a=I(t,n))||a.enumerable});return e};var l=(e,t,r)=>(r=e!=null?F(K(e)):{},U(t||!e||!e.__esModule?B(r,"default",{value:e,enumerable:!0}):r,e));var E=p((te,T)=>{var k=globalThis.__austereHost;if(!k)throw new Error("this austere-axi component pack needs the austere-axi runtime on the page");T.exports=k.React});var S=p((re,q)=>{var M=globalThis.__austereHost;if(!M)throw new Error("this austere-axi component pack needs the austere-axi runtime on the page");q.exports=M.zod});var w=p((ae,_)=>{var R=globalThis.__austereHost;if(!R)throw new Error("this austere-axi component pack needs the austere-axi runtime on the page");_.exports=R.pack});var b=p((oe,L)=>{var A=globalThis.__austereHost;if(!A)throw new Error("this austere-axi component pack needs the austere-axi runtime on the page");L.exports=A.jsxRuntime});var c=l(E()),f=l(S()),m=l(w()),i=l(b()),P="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",C=null,G=0;function y(e){return new Promise((t,r)=>{let a=document.createElement("script");a.src=e,a.async=!0,a.onload=()=>t(),a.onerror=()=>r(new Error(`failed to load ${e}`)),document.head.appendChild(a)})}function J(){if(window.mermaid)return Promise.resolve(z(window.mermaid));if(!C){let e=(0,m.assetUrl)("mermaid.min.js");C=(e?y(e).catch(()=>y(P)):y(P)).then(()=>z(window.mermaid))}return C}var j=!1;function z(e){return j||(j=!0,e.initialize({startOnLoad:!1,securityLevel:"strict",theme:"base",fontFamily:"Inter Variable, Inter, ui-sans-serif, system-ui, sans-serif",themeVariables:{darkMode:!0,background:"#121214",fontSize:"14px",primaryColor:"#202024",primaryTextColor:"#ececef",primaryBorderColor:"#4a4a52",secondaryColor:"#1a1a1e",secondaryTextColor:"#ececef",secondaryBorderColor:"#3a3a40",tertiaryColor:"#18181b",tertiaryTextColor:"#ececef",tertiaryBorderColor:"#3a3a40",lineColor:"#8b8b94",textColor:"#d4d4d8",mainBkg:"#202024",nodeBorder:"#4a4a52",clusterBkg:"#18181b",clusterBorder:"#34343a",edgeLabelBackground:"#121214",noteBkgColor:"#2a2614",noteTextColor:"#ececef",noteBorderColor:"#5c5326",actorBkg:"#202024",actorBorder:"#4a4a52",actorTextColor:"#ececef",signalColor:"#a1a1aa",signalTextColor:"#d4d4d8",labelBoxBkgColor:"#202024",labelBoxBorderColor:"#4a4a52",labelTextColor:"#ececef",loopTextColor:"#d4d4d8",activationBkgColor:"#2a2a30",activationBorderColor:"#5a5a64",sequenceNumberColor:"#121214"},flowchart:{htmlLabels:!1,curve:"basis",padding:12},gantt:{axisFormat:"%m-%d",fontSize:12,barHeight:22}})),e}function Q(e){let t=/^```(?:mermaid)?\s*\n([\s\S]*?)\n?```\s*$/.exec(e);return t?t[1]:e}function H(e){let t=e.querySelectorAll("tspan, div, span, p");return(t.length?Array.from(t,a=>a.children.length?"":a.textContent):[e.textContent]).join(" ").replace(/\s+/g," ").trim()}function X(e){let t=(r,a)=>{a&&r.setAttribute("data-austere-part",a.slice(0,80))};e.querySelectorAll("g.node, g.cluster, g.edgeLabel").forEach(r=>{let a=r.querySelector(".classTitle, .label-group, .er.entityLabel, .name");t(r,H(a||r))}),e.querySelectorAll("text.actor, text.messageText, text.noteText, text.taskText").forEach(r=>t(r,H(r)))}function Y({props:e,statementId:t}){let r=(0,c.useRef)(null),[a,n]=(0,c.useState)(null),g=String(e.source??"").trim();return(0,c.useEffect)(()=>{let h=!1;if(!g)return;let v=`austere-mmd-${++G}`;return J().then(async d=>{let{svg:u,bindFunctions:D}=await d.render(v,Q(g));if(h||!r.current)return;r.current.innerHTML=u;let o=r.current.querySelector("svg");if(o){o.removeAttribute("height"),o.style.height="auto",o.style.maxWidth||(o.style.maxWidth="100%");let x=o.viewBox&&o.viewBox.baseVal?o.viewBox.baseVal.width:0;x>0&&(o.style.minWidth=`${Math.round(Math.min(x,x*.6))}px`)}X(r.current),D?.(r.current),n(null)}).catch(d=>{if(h)return;let u=String(d?.message||d).split(`
2
+ `).slice(0,4).join(" ").split(/\s*Expecting /)[0];n(u),(0,m.reportRuntimeError)({source:"mermaid",statementId:t,component:"Mermaid",message:u}),document.getElementById(`d${v}`)?.remove()}),()=>{h=!0}},[g,t]),(0,i.jsxs)("figure",{className:"austere-mermaid",children:[a?(0,i.jsxs)("div",{className:"austere-render-error",children:[(0,i.jsx)("strong",{children:"Diagram failed to render."})," ",a]}):null,(0,i.jsx)("div",{className:"austere-mermaid-svg",ref:r}),e.caption?(0,i.jsx)("figcaption",{children:e.caption}):null]})}var N=(0,m.austereComponent)({name:"Mermaid",group:"Content",props:f.z.object({source:f.z.string(),caption:f.z.string().optional()}),description:"Diagram from Mermaid source (flowchart, sequence, state, class, ER, gantt). source is a raw multi-line string. Keep node labels to a few words; clicked nodes reach the agent by label.",notes:["- Mermaid for sequences, state machines, ER, gantt and quick flowcharts."],renderer:Y,example:`requestPath = Mermaid("flowchart LR
3
+ client[Client] --> api[API]
4
+ api --> pg[(Postgres)]", "Request path")`});var V=`.austere-mermaid {
5
+ margin: 0;
6
+ width: 100%;
7
+ min-width: 0;
8
+ }
9
+ .austere-mermaid-svg {
10
+ display: flex;
11
+ justify-content: safe center;
12
+ overflow-x: auto;
13
+ padding: 8px 0;
14
+ }
15
+ .austere-mermaid-svg svg {
16
+ max-width: 100%;
17
+ height: auto;
18
+ }
19
+ .austere-mermaid figcaption {
20
+ margin-top: 8px;
21
+ text-align: center;
22
+ font-size: 12.5px;
23
+ color: var(--austere-muted);
24
+ }
25
+ `;var s={components:[N],css:V};var $=l(w());(0,$.registerPack)({name:"@austere-axi/pack-mermaid",version:"0.3.0",api:1,components:[].concat(s&&s.components?s.components:s),css:s&&s.css});})();
package/dist/prompt.md CHANGED
@@ -57,59 +57,45 @@ Mermaid(source: string, caption?: string) — Diagram from Mermaid source (flowc
57
57
  ### Data
58
58
  Table(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.
59
59
  Col(label: string, data: any, type?: "string" | "number" | "action") — Column definition — holds label + data array
60
- EditableTable(name?: string, columns?: {type: "text" | "number" | "date-single" | "select" | "url", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[]) — Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field
61
60
  EntityList(rows?: {left: string, right: string, rightVariant?: "text" | "number"}[], size?: "small" | "default", header?: {left: string, right: string, rightVariant?: "text" | "number"}, footer?: {left: string, right: string, rightVariant?: "text" | "number"}) — Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.
62
61
  Tag(text: string, icon?: Icon, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional Icon and variant
63
- TagBlock(tags: string[], size?: "sm" | "md" | "lg") — tags is an array of strings; optional size sm | md | lg
64
62
  Icon(name: string, category?: string) — A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.
65
63
  OverviewCardBlock(items: OverviewCardItem[], layout?: "grid" | "carousel", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.
66
64
  OverviewCardItem(id?: string, top: IconText | Text, bottom?: MetricIndicatorInline) — One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.
67
- IconText(icon: Icon, iconVariant?: "neutral" | "info" | "success" | "warning" | "danger" | "inverted" | "filled" | "soft", iconSize?: "xs" | "s" | "m" | "l" | "xl" | "sm" | "md" | "lg", title: string, subtitle?: string, bold?: boolean, layout?: "horizontal" | "vertical") — An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.
68
65
  Text(variant?: "text" | "number", value: string, subtext?: string, subtextVariant?: "text" | "number" | "metric", size?: "xs" | "sm" | "md" | "lg") — Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.
69
- BoldText(variant?: "text" | "number", value: string, subtext?: string, subtextVariant?: "text" | "number" | "metric", size?: "xs" | "sm" | "md" | "lg") — Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.
70
66
  MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: "up" | "down", value: number}) — Headline metric value with an optional +/- percentage trend and subtext, all on one line.
71
67
  FileTree(entries: string[]) — File tree, one path per entry, optionally prefixed with a git status code (A new, M modified, D deleted, R old -> new) and suffixed with " # note". Folders are inferred. Clicked files and folders reach the agent by path.
72
68
  - Table is COLUMN-oriented: Table([Col("Option", names), Col("Effort", efforts), Col("Risk", risks)]). Col data may mix strings and components, e.g. ["n/a", Tag("high", null, "sm", "danger")]. Cells are plain text (no markdown) and wrap; Col type "number" for figures.
73
69
  - EntityList for key/value facts; OverviewCardBlock + OverviewCardItem + MetricIndicatorInline for a KPI row.
74
70
  - EditableTable when the reviewer should correct or fill in values; edits arrive with the next @ToAssistant from its Form.
75
71
  - Icon names are lucide kebab-case (circle-check, triangle-alert, git-branch).
72
+ - Also in Data (`austere-axi components show <Name>` for the signature): EditableTable, TagBlock, IconText, BoldText.
76
73
 
77
74
  ### Charts
78
75
  BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string, height?: number) — Vertical bars; use for comparing values across categories with one or more series
79
76
  LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string, height?: number) — Lines over categories; use for trends and continuous data over time
80
- AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string, height?: number) — Filled area under lines; use for cumulative totals or volume trends over time
81
- HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists
82
77
  Series(category: string, values: number[]) — One data series
83
78
  PieChart(labels: string[], values: number[], variant?: "pie" | "donut", appearance?: "circular" | "semiCircular") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)
84
- ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering
85
- ScatterSeries(name: string, points: Point[]) — Named dataset
86
- Point(x: number, y: number, z?: number) — Data point with numeric coordinates
87
79
  - Charts take column arrays: BarChart(labels, [Series("p50", [..]), Series("p99", [..])], "grouped", "Week", "ms").
88
80
  - PieChart(labels, values) with plain number arrays.
81
+ - Also in Charts (`austere-axi components show <Name>` for the signature): AreaChart, HorizontalBarChart, ScatterChart, ScatterSeries, Point.
89
82
 
90
83
  ### Forms
91
84
  Form(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons
92
85
  FormControl(label: string, input: Input | TextArea | Select | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text
93
- Label(text: string) — Text label
94
86
  Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
95
87
  TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
96
88
  Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>, size?: "small" | "medium" | "large")
97
89
  SelectItem(value: string, label: string) — Option for Select
98
- Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<number[]>) — Numeric slider input; supports continuous and discrete (stepped) variants
99
- CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<Record<string, boolean>>)
100
- CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)
101
90
  RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
102
91
  RadioItem(label: string, description: string, value: string)
103
- SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?: $binding<Record<string, boolean>>) — Group of switch toggles
104
- SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle
105
- Chips(name: string, type?: "single" | "multiple", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.
106
- ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean) — A single selectable chip inside a Chips group, with a value, label and optional icon.
107
92
  OptionCards(name: string, type?: "single" | "multiple", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.
108
93
  OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean) — A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.
109
94
  - Form(name, buttons, fields). Define each FormControl as its own statement. Never nest a Form in a Form.
110
95
  - To ask the reviewer to decide: OptionCards or RadioGroup inside a Form whose button is Button("Send decision", @ToAssistant("Decision: rollout strategy"), "primary"). The values arrive with that message as form: {<formName>: {<fieldName>: value}}; no context needed. Reviewers can always annotate and message, so add a notes field only when it belongs with the decision.
111
96
  - OptionCards("pick", "single", [OptionCard("a", "Option A", "why", Icon("zap")), OptionCard("b", "Option B")], {required: true}, "a") — the last argument pre-selects.
112
97
  - rules: {required: true, minLength: 10, ...}; the renderer shows the messages.
98
+ - Also in Forms (`austere-axi components show <Name>` for the signature): Label, Slider, CheckBoxGroup, CheckBoxItem, SwitchGroup, SwitchItem, Chips, ChipItem.
113
99
 
114
100
  ### Buttons
115
101
  Button(label: string, action?: ActionExpression, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button
@@ -202,4 +188,5 @@ Before finishing, walk your output and verify:
202
188
  - Pass null to skip an optional argument before one you set: Tag("high", null, "sm", "danger").
203
189
  - Strings may span lines (use that for Mermaid, Diff, CodeBlock and long markdown) and accept \n escapes; $ inside strings is literal. Escape " inside double-quoted strings, or use single quotes. Never write </script> inside the program.
204
190
  - Artifacts are static: no Query, Mutation or tools, and you never need $variables (omit $binding props).
191
+ - Need something not listed? `austere-axi components search <words>` covers every component, including installed packs.
205
192
  - Size: one idea per section, 3-7 sections, tables up to ~12 rows with phrase-length cells. A page that could have been a paragraph should have been a paragraph.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "austere-axi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-authored review artifacts in OpenUI Lang: a few hundred tokens instead of a whole HTML page, with statement-level annotations fed back to the agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "build": "node scripts/build.mjs",
21
21
  "pretest": "node scripts/build.mjs spec",
22
22
  "test": "node --test test/*.test.js",
23
- "test:browser": "node scripts/build.mjs runtime mermaid spec chrome && node --test test/browser/*.test.js",
23
+ "test:browser": "node scripts/build.mjs packs runtime mermaid spec chrome && node --test test/browser/*.test.js",
24
24
  "signatures": "node scripts/signatures.mjs",
25
25
  "gallery": "node scripts/gallery.mjs",
26
26
  "shot": "node scripts/shot.mjs",
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: austere-setup
3
+ description: Set up austere-axi on this machine for the user. Checks Node, installs the `austere` skill, optionally installs the CLI globally and turns on the hub, then proves the review loop works with a test page. Use when the user asks to install, set up or configure austere-axi.
4
+ license: MIT
5
+ ---
6
+
7
+ # austere-setup
8
+
9
+ Set up austere-axi for the user. Do the steps in order. Stop and tell the user when a step fails.
10
+
11
+ Ask the user only the questions in steps 3 and 4. Use the default for each if they do not answer.
12
+
13
+ ## 1. Check Node
14
+
15
+ Run `node --version`.
16
+
17
+ - Node 20 or later is necessary.
18
+ - The hub (step 4) needs Node 22.13 or later.
19
+
20
+ If Node is older than 20, stop. Tell the user to install a current Node LTS.
21
+
22
+ ## 2. Check the CLI
23
+
24
+ Run `npx -y austere-axi --help`. It must print the `WORKFLOW` section. If npm cannot reach the registry, stop and show the error.
25
+
26
+ ## 3. Install the `austere` skill
27
+
28
+ This skill tells agents when to make a page, and adds the `/austere` command.
29
+
30
+ Ask: install it for all projects (global), or only this project? Default: global.
31
+
32
+ ```sh
33
+ npx skills add MandalorianBatman/austere-axi --skill austere -g # global
34
+ npx skills add MandalorianBatman/austere-axi --skill austere # this project
35
+ ```
36
+
37
+ If the `skills` tool asks which agents to install for, pick the agent the user works with.
38
+
39
+ ## 4. Optional choices
40
+
41
+ Ask the user about each item. Default: no.
42
+
43
+ - **Global command.** `npm install -g austere-axi`. Then `austere-axi` works without `npx -y`. Update it later with `npm update -g austere-axi`.
44
+ - **Hub.** `npx -y austere-axi hub on`. It keeps the server running and lists every page at `http://127.0.0.1:4388/`. Needs Node 22.13 or later.
45
+ - **Phone review.** If Tailscale is running, the server also listens on the tailnet, and `open` prints a phone link. Nothing to install. Run `tailscale status` to check.
46
+
47
+ On a machine with no screen (SSH, CI, container), tell the user to set `AUSTERE_AXI_NO_OPEN=1`, so the CLI prints links and does not try to open a browser.
48
+
49
+ ## 5. Prove it works
50
+
51
+ Write `austere-setup-test.html` in the current directory:
52
+
53
+ ```html
54
+ <!doctype html>
55
+ <html><head><meta charset="utf-8"><title>austere-axi works</title>
56
+ <script src="https://cdn.jsdelivr.net/npm/austere-axi@0/dist/austere.js"></script></head>
57
+ <body><script type="text/openui">
58
+ root = Stack([card], "column", "l")
59
+ card = Card([CardHeader("austere-axi works", "Setup test"), text, buttons])
60
+ text = MarkDownRenderer("Click a part of this page to annotate it, or press the button.")
61
+ buttons = Buttons([Button("It works")])
62
+ </script></body></html>
63
+ ```
64
+
65
+ Then:
66
+
67
+ 1. `npx -y austere-axi check austere-setup-test.html` must report no errors.
68
+ 2. `npx -y austere-axi austere-setup-test.html` opens the review. Give the user the printed link.
69
+ 3. `npx -y austere-axi poll austere-setup-test.html` waits for the user. Ask them to press **It works**. The poll returns an item for the button.
70
+ 4. `npx -y austere-axi end austere-setup-test.html`, then delete the file.
71
+
72
+ If the user cannot open the page, read the server log in `~/.austere-axi/` and report it.
73
+
74
+ ## 6. Report
75
+
76
+ Tell the user, in a short list:
77
+
78
+ - the Node version
79
+ - where the `austere` skill is installed
80
+ - which optional items you set up
81
+ - that the test page worked
82
+
83
+ To make a page later, the user asks for one, or types `/austere <what to show>`.