@spinabot/brigade 1.16.0 → 1.17.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ws-subscription-filter.d.ts","sourceRoot":"","sources":["../../src/core/ws-subscription-filter.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CACjC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,EAC1C,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,EAC5C,IAAI,EAAE,SAAS,GACb,OAAO,CAOT;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAO5D"}
1
+ {"version":3,"file":"ws-subscription-filter.d.ts","sourceRoot":"","sources":["../../src/core/ws-subscription-filter.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CACjC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,EAC1C,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,EAC5C,IAAI,EAAE,SAAS,GACb,OAAO,CAkBT;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAO5D"}
@@ -20,8 +20,19 @@ export function shouldDeliverFrame(agentSubs, sessionSubs, tags) {
20
20
  return true;
21
21
  if (agentId && agentSubs?.has(agentId))
22
22
  return true;
23
- if (sessionId && sessionSubs?.has(sessionId))
24
- return true;
23
+ if (sessionId && sessionSubs) {
24
+ // Exact match OR a sub-agent DESCENDANT session. A spawned sub-agent runs
25
+ // under a child key (`<parent>:subagent:<id>`, see routing/session-key.ts),
26
+ // so its pi frames + approval prompts must still reach the operator
27
+ // watching the parent turn — otherwise sub-agent activity is invisible and
28
+ // its `bash` approval prompt never surfaces (the turn then hangs on the
29
+ // approval timeout). The trailing ":" stops a sibling like `…:main2` from
30
+ // matching `…:main`.
31
+ for (const sub of sessionSubs) {
32
+ if (sessionId === sub || sessionId.startsWith(`${sub}:`))
33
+ return true;
34
+ }
35
+ }
25
36
  return false;
26
37
  }
27
38
  /** Cheap discriminator: pulls optional agentId/sessionId off a payload. */
@@ -1 +1 @@
1
- {"version":3,"file":"ws-subscription-filter.js","sourceRoot":"","sources":["../../src/core/ws-subscription-filter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,uEAAuE;AACvE,qEAAqE;AACrE,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,sEAAsE;AAOtE,qEAAqE;AACrE,MAAM,UAAU,kBAAkB,CACjC,SAA0C,EAC1C,WAA4C,EAC5C,IAAe;IAEf,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IACpC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,OAAO,IAAI,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,SAAS,IAAI,WAAW,EAAE,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,OAAO,KAAK,CAAC;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAChD,MAAM,GAAG,GAAG,OAA4D,CAAC;IACzE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;QAAE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAChE,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;QAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"ws-subscription-filter.js","sourceRoot":"","sources":["../../src/core/ws-subscription-filter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,uEAAuE;AACvE,qEAAqE;AACrE,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,sEAAsE;AAOtE,qEAAqE;AACrE,MAAM,UAAU,kBAAkB,CACjC,SAA0C,EAC1C,WAA4C,EAC5C,IAAe;IAEf,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IACpC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,OAAO,IAAI,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,SAAS,IAAI,WAAW,EAAE,CAAC;QAC9B,0EAA0E;QAC1E,4EAA4E;QAC5E,oEAAoE;QACpE,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,qBAAqB;QACrB,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC/B,IAAI,SAAS,KAAK,GAAG,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QACvE,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAChD,MAAM,GAAG,GAAG,OAA4D,CAAC;IACzE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;QAAE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAChE,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;QAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinabot/brigade",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "Brigade — your personal AI crew",
5
5
  "homepage": "https://brigade.spinabot.com",
6
6
  "author": "Spinabot <hello@brigade-agent.ai>",
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: docx
3
+ description: Create and edit Microsoft Word (.docx) documents to a professional standard — reports, proposals, contracts, letters, memos — with custom styles, multi-level lists, styled/merged tables, headers/footers, table of contents, hyperlinks, footnotes, tracked changes and comments. Use when the user asks Brigade to make, write, draft, fill, redline, or edit a Word document or .docx file.
4
+ metadata:
5
+ {
6
+ "brigade":
7
+ {
8
+ "emoji": "📝"
9
+ }
10
+ }
11
+ ---
12
+
13
+ # docx — professional Word documents
14
+
15
+ Pick the lightest path that meets the need. **Never declare a document done without opening/validating the result at least once** (see *Verify*).
16
+
17
+ | Need | Path |
18
+ |------|------|
19
+ | Simple structured doc (headings, paragraphs, bullets, a basic table, an image) | **Path 1 — `make_document` tool** |
20
+ | Anything richer (custom styles, numbered/multi-level lists, merged/shaded table cells, headers/footers, TOC, hyperlinks, footnotes, inline bold/italic/color, tracked changes, comments) | **Path 2 — script the `docx` library via `brigade exec-node`** |
21
+ | Fill / redline an EXISTING branded `.docx` without disturbing its styling | **Path 3 — OOXML round-trip** |
22
+ | Markdown → branded Word inheriting a corporate template | **Path 4 — `pandoc` (optional)** |
23
+
24
+ ---
25
+
26
+ ## Path 1 — quick structured doc (`make_document` tool)
27
+
28
+ ```
29
+ make_document(format="docx", content={ title, sections:[{heading, level, paragraphs, bullets, table:{rows}, image:{path}}] })
30
+ ```
31
+ Good for a fast first draft. It is deliberately limited (single-level bullets, string-only tables, no inline formatting). The moment you need more, go to Path 2 — don't fight the schema.
32
+
33
+ ## Path 2 — full power: script the `docx` library
34
+
35
+ Brigade bundles the **`docx`** library (dolanmiu/docx). Write a CommonJS script and run it with **`brigade exec-node`**, which makes Brigade's bundled libraries `require()`-able from anywhere (no install):
36
+
37
+ 1. `write` a file `gen.cjs`.
38
+ 2. Run: `brigade exec-node gen.cjs`
39
+
40
+ ```js
41
+ // gen.cjs — illustrative; adapt to the content
42
+ const fs = require("node:fs");
43
+ const {
44
+ Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType,
45
+ Table, TableRow, TableCell, WidthType, BorderStyle, ShadingType,
46
+ LevelFormat, Header, Footer, PageNumber, TableOfContents, ExternalHyperlink,
47
+ } = require("docx");
48
+
49
+ const doc = new Document({
50
+ styles: { default: { document: { run: { font: "Calibri", size: 22 } } }, // size is half-points (22 = 11pt)
51
+ paragraphStyles: [{ id: "Body", name: "Body", run: { size: 22 }, paragraph: { spacing: { after: 160 } } }] },
52
+ numbering: { config: [{ reference: "nums", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.START }] }] },
53
+ sections: [{
54
+ properties: { page: { size: { width: 12240, height: 15840 } } }, // US Letter, in DXA (1440 = 1 inch)
55
+ headers: { default: new Header({ children: [new Paragraph("Acme Corp — Confidential")] }) },
56
+ footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })] })] }) },
57
+ children: [
58
+ new Paragraph({ text: "Q3 Business Review", heading: HeadingLevel.TITLE }),
59
+ new TableOfContents("Contents", { hyperlink: true, headingStyleRange: "1-3" }),
60
+ new Paragraph({ text: "Summary", heading: HeadingLevel.HEADING_1 }),
61
+ new Paragraph({ children: [ new TextRun("Revenue was "), new TextRun({ text: "$1.2M", bold: true }), new TextRun(" — up 18%.") ] }),
62
+ new Paragraph({ text: "First item", numbering: { reference: "nums", level: 0 } }),
63
+ new Paragraph({ children: [ new ExternalHyperlink({ children: [new TextRun({ text: "Full data", style: "Hyperlink" })], link: "https://example.com" }) ] }),
64
+ new Table({
65
+ columnWidths: [4680, 4680], // DXA, sum = content width
66
+ rows: [
67
+ new TableRow({ tableHeader: true, children: ["Metric","Value"].map((t) =>
68
+ new TableCell({ width: { size: 4680, type: WidthType.DXA }, shading: { type: ShadingType.CLEAR, fill: "D9E2F3" },
69
+ children: [new Paragraph({ children: [new TextRun({ text: t, bold: true })] })] })) }),
70
+ new TableRow({ children: ["Revenue","$1.2M"].map((t) =>
71
+ new TableCell({ width: { size: 4680, type: WidthType.DXA }, children: [new Paragraph(t)] })) }),
72
+ ],
73
+ }),
74
+ ],
75
+ }],
76
+ });
77
+ Packer.toBuffer(doc).then((buf) => { fs.writeFileSync(process.argv[2] || "out.docx", buf); console.log("wrote", process.argv[2] || "out.docx"); });
78
+ ```
79
+
80
+ The `docx` library also supports: footnotes (`FootnoteReferenceRun` + `footnotes`), comments, **tracked changes** (`InsertedTextRun`/`DeletedTextRun` with `author`/`date`), multi-section layouts, page breaks, columns, internal bookmarks, and images inside table cells. Read its API as needed — anything Word can express, this can author.
81
+
82
+ ## Path 3 — fill / redline an EXISTING `.docx` (highest fidelity)
83
+
84
+ To fill a branded company template or surgically edit a real document **without regenerating it** (which would drop the theme), use the OOXML round-trip — unzip, edit the XML, rezip. For plain placeholder/text swaps the **`edit_document` tool** (`replace_text` / `fill_template` / `append`) already does exactly this and is the first choice.
85
+
86
+ For edits beyond text, script it via `brigade exec-node` with **`fflate`**:
87
+
88
+ ```js
89
+ // edit.cjs — unzip → edit word/document.xml → rezip (styles/headers/theme untouched)
90
+ const fs = require("node:fs");
91
+ const { unzipSync, zipSync, strToU8, strFromU8 } = require("fflate");
92
+ const zip = unzipSync(fs.readFileSync(process.argv[2]));
93
+ let xml = strFromU8(zip["word/document.xml"]);
94
+ xml = xml.replaceAll("{{client}}", "Acme Corp"); // fill placeholders in <w:t> runs
95
+ zip["word/document.xml"] = strToU8(xml);
96
+ fs.writeFileSync(process.argv[3], zipSync(zip));
97
+ ```
98
+
99
+ Rules for raw OOXML edits:
100
+ - Safe: replacing text inside `<w:t>`; pointing a run/paragraph at an **already-defined** `styleId`; appending `<w:p>`/`<w:r>` that reference existing styles.
101
+ - **Do NOT** add a new part (image, hyperlink target, header) by hand — that also needs `[Content_Types].xml` + the matching `_rels/*.rels` updated atomically, or Word reports corruption. For those, regenerate with the `docx` library instead.
102
+ - Tracked changes are `<w:ins>`/`<w:del>` as *siblings* of `<w:r>` (never inside a run); copy the original `<w:rPr>` into the change runs; keep edits minimal so an "accept all" yields exactly the intended text. Author yourself as "Brigade".
103
+
104
+ ## Path 4 — Markdown → branded Word (optional, needs `pandoc`)
105
+
106
+ For long prose where the brand styling lives in a corporate `.docx`, if `pandoc` is installed it inherits every style/margin/header:
107
+ ```bash
108
+ command -v pandoc >/dev/null 2>&1 && pandoc input.md --reference-doc=brand-template.docx -o out.docx
109
+ ```
110
+ Detect first; else fall back to Path 2. `pandoc` is GPL — a separate program shelled out to, never bundled.
111
+
112
+ ## Conventions that make output professional
113
+
114
+ - **Page size is explicit** — US Letter `12240×15840` DXA (1440 DXA = 1"); landscape = portrait dims + `orientation` flip.
115
+ - **Tables:** give both `columnWidths` (table) and per-cell `width` in **`WidthType.DXA`** (PERCENTAGE breaks some viewers), summing to content width; `ShadingType.CLEAR` (never SOLID → black boxes); add cell margins; never use a table as a horizontal rule (use a paragraph bottom border).
116
+ - **Lists:** real numbering config for numbered/multi-level lists; never paste literal "1." / "•" characters.
117
+ - Prefer bullets + tables over walls of text; one `TITLE`, then `HEADING_1..3` with proper outline levels (required for a working TOC).
118
+ - Use typographic quotes (' ' " ") and en/em dashes.
119
+
120
+ ## Verify (required)
121
+
122
+ After writing, confirm the file exists and is non-trivial. If LibreOffice is available, render to PDF and **look at it** — fix overflow/placeholder leftovers, then re-verify:
123
+ ```bash
124
+ command -v soffice >/dev/null 2>&1 && soffice --headless --convert-to pdf out.docx
125
+ ```
126
+ TOC/page-number fields show blank until Word (or a `soffice` convert) recalculates them — note this to the user if you can't run the convert. Never declare success without at least one look-and-fix pass.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: pdf
3
+ description: Create and edit PDFs to a professional standard — generate from content, draw vector/precise layouts, embed custom (incl. CJK/Unicode) fonts, CREATE and fill AcroForm fields (text/checkbox/radio/dropdown), flatten, merge/split/rotate, stamp/watermark, encrypt, and extract text. Use when the user asks Brigade to make a PDF, fill or build a PDF form, or combine/split/stamp/secure PDFs.
4
+ metadata:
5
+ {
6
+ "brigade":
7
+ {
8
+ "emoji": "📄"
9
+ }
10
+ }
11
+ ---
12
+
13
+ # pdf — professional PDFs
14
+
15
+ | Need | Path |
16
+ |------|------|
17
+ | Simple content PDF (title, headings, paragraphs, image) | **Path 1 — `make_document` tool** |
18
+ | Fill an existing form, merge/split/stamp/watermark | **Path 2 — `edit_document` tool** |
19
+ | Form-field CREATION, vector drawing, embedded fonts (bold/CJK), precise layout, encryption, page surgery | **Path 3 — script `@cantoo/pdf-lib` via `brigade exec-node`** |
20
+ | Pixel-perfect, brand-styled layout from HTML/CSS | **Path 4 — HTML→PDF (optional binary)** |
21
+
22
+ > Note: PDF is a *final* format with no reflow engine. If the recipient must edit, deliver the editable source (`.docx`/`.xlsx`) **and** the PDF.
23
+
24
+ ---
25
+
26
+ ## Path 1 — quick content PDF (`make_document` tool)
27
+
28
+ ```
29
+ make_document(format="pdf", content={ title, pages:[{ heading, paragraphs, image:{path} }] })
30
+ ```
31
+ Flow layout, US-Letter, single column, word-wrapped. Good for reports/summaries/letters. For forms, precise layout, or custom fonts → Path 3.
32
+
33
+ ## Path 2 — edit an existing PDF (`edit_document` tool)
34
+
35
+ - `fill_form {fields}` — fill existing AcroForm fields by name (text/checkbox/dropdown).
36
+ - `merge {pdfs}` / `add_pages {pdfs}` · `split {pages}` · `remove_pages {pages}`.
37
+ - `stamp {text}` · `watermark {text}` (45° translucent).
38
+
39
+ **Form decision:** first detect whether the PDF has real fillable fields. If it does → fill them (clean, type-validated). If it's a flat/scanned form (no fields) → you must draw text at coordinates instead (Path 3, annotations) — never assume fields exist.
40
+
41
+ ## Path 3 — full power: script `@cantoo/pdf-lib`
42
+
43
+ Brigade bundles **`@cantoo/pdf-lib`** (a maintained pdf-lib fork — note the scoped name) plus **`@pdf-lib/fontkit`** (custom-font embedding) and **`unpdf`** (text extraction). `write` a `gen.cjs`, run `brigade exec-node gen.cjs`:
44
+
45
+ ```js
46
+ // gen.cjs — illustrative
47
+ const fs = require("node:fs");
48
+ const { PDFDocument, StandardFonts, rgb } = require("@cantoo/pdf-lib");
49
+
50
+ (async () => {
51
+ const pdf = await PDFDocument.create();
52
+
53
+ // embed a custom font (real bold / CJK / Unicode) — requires fontkit
54
+ // const fontkit = require("@pdf-lib/fontkit"); pdf.registerFontkit(fontkit);
55
+ // const font = await pdf.embedFont(fs.readFileSync("Brand-Bold.ttf"));
56
+ const font = await pdf.embedFont(StandardFonts.HelveticaBold);
57
+
58
+ const page = pdf.addPage([612, 792]); // US Letter; origin is BOTTOM-LEFT, y-up
59
+ page.drawText("Invoice", { x: 54, y: 720, size: 22, font, color: rgb(0.1, 0.1, 0.1) });
60
+ page.drawRectangle({ x: 54, y: 700, width: 504, height: 1, color: rgb(0.8, 0.8, 0.8) }); // vector rule
61
+
62
+ // CREATE fillable form fields
63
+ const form = pdf.getForm();
64
+ const name = form.createTextField("client.name");
65
+ name.addToPage(page, { x: 120, y: 640, width: 300, height: 18 });
66
+ const agree = form.createCheckBox("agree");
67
+ agree.addToPage(page, { x: 54, y: 600, width: 14, height: 14 });
68
+ // form.flatten(); // bake fields into static content if no further filling
69
+
70
+ fs.writeFileSync(process.argv[2] || "out.pdf", await pdf.save());
71
+ console.log("wrote");
72
+ })();
73
+ ```
74
+
75
+ `@cantoo/pdf-lib` covers: draw text/lines/rects/ellipses/SVG paths/images; embed custom TTF/OTF fonts (subset, full Unicode); **create AND fill** AcroForm text/checkbox/radio/dropdown/option-list fields; flatten; copy/merge/split/rotate pages; metadata; **password encryption** (the reason this fork is bundled). Extract text/structure with `unpdf`.
76
+
77
+ **Coordinate gotcha:** pdf-lib origin is **bottom-left, y-up**. If you author field positions from a top-left image/spec, convert: `y_pdf = pageHeight - y_top - height`. One coordinate convention per script.
78
+
79
+ ## Path 4 — pixel-perfect / branded PDF from HTML+CSS (optional)
80
+
81
+ pdf-lib has **no HTML/CSS layout engine** — for a designed, brand-styled document, render HTML→PDF. Use whatever is present (detect first):
82
+ ```bash
83
+ command -v soffice >/dev/null 2>&1 && soffice --headless --convert-to pdf brand.html # LibreOffice route
84
+ ```
85
+ A headless browser (Puppeteer/Playwright `page.pdf()`) is the higher-fidelity alternative if installed. Prefer Path 1/3 unless the brand design genuinely requires CSS.
86
+
87
+ ## Conventions
88
+
89
+ - Use real AcroForm fields when filling a fillable PDF; only fall back to drawn-text annotations for flat/scanned forms.
90
+ - Embed a font for anything beyond basic Latin — standard PDF fonts lack bold-as-a-face and all CJK/emoji (missing glyphs render as boxes).
91
+ - Keep ≥0.5" margins; align to a grid; don't rely on text reflowing (there's no engine — you place it).
92
+
93
+ ## Verify (required)
94
+
95
+ Confirm the output exists and opens. For filled/created forms, re-open and read back the field values (or render to an image with `soffice`/a viewer and look at placement) before declaring done — never fill or place blind.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: xlsx
3
+ description: Build and edit professional Excel (.xlsx) spreadsheets, workbooks, and simple financial models — live formulas, number/date/currency formats, cell styling, conditional formatting, data-validation dropdowns, named ranges, frozen panes, merged cells, multi-sheet. Use when the user asks Brigade to make, fill, update, model, or edit a spreadsheet, workbook, or .xlsx file.
4
+ metadata:
5
+ {
6
+ "brigade":
7
+ {
8
+ "emoji": "📊"
9
+ }
10
+ }
11
+ ---
12
+
13
+ # xlsx — professional spreadsheets & models
14
+
15
+ | Need | Path |
16
+ |------|------|
17
+ | Simple table / multi-sheet dump (headers + rows, optional per-column number format) | **Path 1 — `make_document` tool** |
18
+ | Live formulas, cell styling, conditional formatting, dropdowns, named ranges, freeze panes, merged cells, dates | **Path 2 — script the `exceljs` library via `brigade exec-node`** |
19
+ | Surgical edits to an existing workbook | **Path 3 — `edit_document` tool** |
20
+ | Guarantee no formula errors / get computed values | **Path 4 — recalc-verify loop (optional `soffice`)** |
21
+
22
+ **The two non-negotiable rules** (they separate a real model from a hack):
23
+ 1. **Formulas, never hardcoded results.** Write the Excel formula string (`=B5*(1+$B$6)`), never compute the number in code and paste a literal — so the sheet stays live when inputs change. This applies to every total, percentage, ratio, and growth.
24
+ 2. **Numbers are numbers.** Store `1200000`, format for display (`$#,##0`) — never the string `"$1.2M"`, or it won't sum or sort.
25
+
26
+ ---
27
+
28
+ ## Path 1 — quick table (`make_document` tool)
29
+
30
+ ```
31
+ make_document(format="xlsx", content={ sheets:[{ name, header, rows, numberFormats }] })
32
+ ```
33
+ Cells may be `string | number | {formula, numFmt}`. Fine for a straight data table. For styling, validation, charts, or a model → Path 2.
34
+
35
+ ## Path 2 — full power: script the `exceljs` library
36
+
37
+ Brigade bundles **`exceljs`**. `write` a `gen.cjs`, then run `brigade exec-node gen.cjs`:
38
+
39
+ ```js
40
+ // gen.cjs — illustrative
41
+ const ExcelJS = require("exceljs");
42
+ const wb = new ExcelJS.Workbook();
43
+ const ws = wb.addWorksheet("Model", { views: [{ state: "frozen", ySplit: 1 }] }); // freeze header row
44
+
45
+ ws.columns = [
46
+ { header: "Item", key: "item", width: 28 },
47
+ { header: "FY24 ($)", key: "v", width: 16, style: { numFmt: "$#,##0" } },
48
+ ];
49
+ ws.getRow(1).font = { bold: true };
50
+ ws.getRow(1).fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFD9E2F3" } };
51
+
52
+ // Assumptions block — blue font marks hardcoded INPUTS (banker convention)
53
+ ws.getCell("E1").value = "Growth"; ws.getCell("E2").value = 0.18;
54
+ ws.getCell("E2").numFmt = "0.0%"; ws.getCell("E2").font = { color: { argb: "FF0000FF" } };
55
+ wb.definedNames.add("Model!$E$2", "growth"); // named range
56
+
57
+ ws.addRow({ item: "Revenue", v: 1200000 });
58
+ ws.addRow({ item: "Next year", v: { formula: "B2*(1+growth)" } }); // FORMULA, references the named input
59
+ ws.getCell("B3").font = { color: { argb: "FF000000" } }; // black = formula
60
+
61
+ // dropdown + conditional formatting
62
+ ws.getCell("A6").dataValidation = { type: "list", allowBlank: false, formulae: ['"Low,Med,High"'] };
63
+ ws.addConditionalFormatting({ ref: "B2:B3", rules: [
64
+ { type: "cellIs", operator: "lessThan", formulae: ["0"], style: { font: { color: { argb: "FFFF0000" } } } } ]});
65
+
66
+ wb.xlsx.writeFile(process.argv[2] || "out.xlsx").then(() => console.log("wrote"));
67
+ ```
68
+
69
+ `exceljs` covers: number/date formats, font/fill/border/alignment, conditional formatting, data-validation dropdowns, named ranges, freeze panes, autofilter, merged cells, images, sheet protection, multi-sheet. Dates: pass a real `new Date(...)` and set a date `numFmt` (don't pass a string).
70
+
71
+ ## Path 3 — edit an existing workbook (`edit_document` tool)
72
+
73
+ - `set_cells {sheet?, cells:[{ref|row,col, value, numFmt?}]}` — surgical edits; other sheets untouched.
74
+ - `append_rows {sheet?, rows}` — grow a table.
75
+ When editing someone's workbook, **match its existing conventions exactly** (column order, units, formats) — the template always wins over the defaults here.
76
+
77
+ ## Path 4 — recalc-verify loop (the quality guarantee)
78
+
79
+ `exceljs` stores formula **strings but does not evaluate them** — a typo (`#REF!`, `#DIV/0!`) is invisible until the file is opened. If LibreOffice is present, force a recalc and inspect; otherwise hand-check ranges and keep formulas simple.
80
+
81
+ ```bash
82
+ command -v soffice >/dev/null 2>&1 && soffice --headless --convert-to pdf --outdir /tmp out.xlsx # then read the PDF: no #REF!/#DIV/0! anywhere
83
+ ```
84
+ Loop: build → recalc → if any error token appears, fix the formula → recalc again. Target: **zero formula errors** in the delivered file.
85
+
86
+ ## Conventions (banker-grade, optional but professional)
87
+
88
+ - Cell-color convention: **blue font = hardcoded inputs**, black = formulas, green = links to other sheets, red = links to external files; yellow fill = key assumptions.
89
+ - Isolate assumptions in their own cells and reference them **absolutely** (`$B$6`) or by **named range**; document any sourced hardcode in a cell comment ("Source: 10-K FY24 p.45").
90
+ - Number formats: currency `$#,##0` with units in the header; percentages `0.0%`; multiples `0.0x`; negatives in parentheses; years as text.
91
+
92
+ ## Verify (required)
93
+
94
+ Re-open with `edit_document`/read or confirm the file unzips with the expected sheet names; if you used formulas, run the Path-4 recalc and confirm zero errors before declaring done.