@hybridlabor-api/aos 4.13.2 → 4.14.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.
Files changed (151) hide show
  1. package/.agents/AGENTS.md +8 -0
  2. package/.agents/nodes.json +5 -2
  3. package/.claude/hooks/conventional-commits.mjs +14 -15
  4. package/.claude/hooks/env-file-protection.mjs +14 -15
  5. package/.claude/hooks/go-gate.mjs +152 -10
  6. package/.claude/hooks/go-token.mjs +55 -0
  7. package/.claude/hooks/memb-inject.mjs +75 -62
  8. package/.claude/hooks/trail-autostart.mjs +27 -0
  9. package/.claude/hooks/trail-relay.mjs +1 -0
  10. package/.claude/settings.json +22 -4
  11. package/.claude/workflows/startcycle-dispatch.mjs +11 -4
  12. package/.opencode/plugins/bdb-aos.js +98 -121
  13. package/.opencode/plugins/lib/trail-autostart.js +38 -0
  14. package/CLAUDE.md +1 -1
  15. package/README.de.md +6 -6
  16. package/README.md +6 -6
  17. package/README.pt.md +6 -6
  18. package/THIRD_PARTY_NOTICES.md +19 -3
  19. package/assets/header-v5.png +0 -0
  20. package/bin/aos-acp.mjs +211 -0
  21. package/bin/aos-doctor.mjs +1 -1
  22. package/bin/aos-uninstall.mjs +2 -2
  23. package/docs/master-session-acp.md +51 -0
  24. package/installer.js +314 -42
  25. package/mcps/mcsc/packages/mcp/server.js +6 -7
  26. package/package.json +4 -3
  27. package/scripts/validate-skills.mjs +76 -0
  28. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  29. package/skills/basic/godmode-shipping/SKILL.md +3 -0
  30. package/skills/basic/master-session/SKILL.md +89 -0
  31. package/skills/basic/startcycle/SKILL.md +1 -1
  32. package/skills/basic/startcycle-graph/SKILL.md +2 -2
  33. package/skills/basic/startcycle-graph-user/SKILL.md +1 -1
  34. package/skills/basic/teamwork-preview/SKILL.md +1 -1
  35. package/skills/bdbrainstorm/SKILL.md +7 -1
  36. package/skills/global_config/agentic-harness-patterns/SKILL.md +257 -0
  37. package/skills/global_config/agentic-harness-patterns/metadata.json +10 -0
  38. package/skills/global_config/agentic-harness-patterns/references/agent-orchestration-pattern.md +97 -0
  39. package/skills/global_config/agentic-harness-patterns/references/bootstrap-sequence-pattern.md +106 -0
  40. package/skills/global_config/agentic-harness-patterns/references/context-engineering/compress-pattern.md +78 -0
  41. package/skills/global_config/agentic-harness-patterns/references/context-engineering/isolate-pattern.md +82 -0
  42. package/skills/global_config/agentic-harness-patterns/references/context-engineering/select-pattern.md +86 -0
  43. package/skills/global_config/agentic-harness-patterns/references/context-engineering-pattern.md +29 -0
  44. package/skills/global_config/agentic-harness-patterns/references/hook-lifecycle-pattern.md +111 -0
  45. package/skills/global_config/agentic-harness-patterns/references/memory-persistence-pattern.md +109 -0
  46. package/skills/global_config/agentic-harness-patterns/references/permission-gate-pattern.md +111 -0
  47. package/skills/global_config/agentic-harness-patterns/references/skill-runtime-pattern.md +104 -0
  48. package/skills/global_config/agentic-harness-patterns/references/task-decomposition-pattern.md +92 -0
  49. package/skills/global_config/agentic-harness-patterns/references/tool-registry-pattern.md +101 -0
  50. package/skills/global_config/agenttrail/SKILL.md +8 -0
  51. package/skills/global_config/agenttrail/bin/agenttrail.mjs +14 -0
  52. package/skills/global_config/agenttrail/bin/ensure.mjs +118 -0
  53. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  54. package/skills/global_config/bdb-visual-edit/SKILL.md +51 -0
  55. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +59 -0
  56. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +27 -0
  57. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +123 -0
  58. package/skills/global_config/factory-collect/SKILL.md +74 -0
  59. package/skills/global_config/factory-human-digest/SKILL.md +92 -0
  60. package/skills/global_config/factory-lookback/SKILL.md +95 -0
  61. package/skills/global_config/factory-review-prs/SKILL.md +63 -0
  62. package/skills/global_config/git-pr-review/SKILL.md +3 -0
  63. package/skills/global_config/grilling/SKILL.md +2 -0
  64. package/skills/global_config/mcsc/SKILL.md +1 -1
  65. package/skills/global_config/plan-arbiter/SKILL.md +125 -0
  66. package/skills/global_config/plan-canvas/SKILL.md +62 -5
  67. package/skills/global_config/plan-canvas/scripts/lib/loopback-guard.js +19 -3
  68. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +285 -0
  69. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/agent-trail.js +129 -0
  70. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/board-client.js +124 -0
  71. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/canvas.mdx +19 -0
  72. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/plan.mdx +18 -0
  73. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/recap-demo/plan.mdx +72 -0
  74. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/README.md +29 -0
  75. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/architecture.json +30 -0
  76. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/00_architecture.html +14950 -0
  77. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/canvas.mdx +511 -0
  78. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/plan.mdx +208 -0
  79. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/recap/plan.mdx +102 -0
  80. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/standard/plan.md +136 -0
  81. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/canvas.mdx +124 -0
  82. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/plan.mdx +37 -0
  83. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/index.js +188 -0
  84. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/kit.js +123 -0
  85. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/mdx.js +411 -0
  86. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +1291 -0
  87. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/meta.json +1 -0
  88. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/plan.mdx +195 -0
  89. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/standard.md +95 -0
  90. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/meta.json +1 -0
  91. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/plan.mdx +105 -0
  92. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/standard.md +76 -0
  93. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/canvas.mdx +81 -0
  94. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/meta.json +1 -0
  95. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/plan.mdx +145 -0
  96. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/standard.md +76 -0
  97. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/meta.json +1 -0
  98. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/plan.mdx +172 -0
  99. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/standard.md +100 -0
  100. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/meta.json +1 -0
  101. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/plan.mdx +67 -0
  102. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/standard.md +49 -0
  103. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/canvas.mdx +63 -0
  104. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/meta.json +1 -0
  105. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/plan.mdx +49 -0
  106. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/standard.md +39 -0
  107. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/meta.json +1 -0
  108. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/plan.mdx +118 -0
  109. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/standard.md +57 -0
  110. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/meta.json +1 -0
  111. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/plan.mdx +173 -0
  112. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/standard.md +96 -0
  113. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/meta.json +1 -0
  114. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/plan.mdx +91 -0
  115. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/standard.md +54 -0
  116. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/canvas.mdx +53 -0
  117. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/meta.json +1 -0
  118. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/plan.mdx +225 -0
  119. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/standard.md +111 -0
  120. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/theme.css +472 -0
  121. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/trail.js +216 -0
  122. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/markdown.js +1 -1
  123. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +37 -4
  124. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +125 -29
  125. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +196 -8
  126. package/skills/global_config/pr-recap/SKILL.md +47 -0
  127. package/skills/global_config/pr-recap/scripts/pr-recap.mjs +200 -0
  128. package/skills/global_config/quick-recap/SKILL.md +55 -0
  129. package/skills/global_config/stay-within-limits/SKILL.md +85 -0
  130. package/skills/global_config/triage/SKILL.md +3 -0
  131. package/skills/global_config/visual-edit/README.md +96 -0
  132. package/skills/global_config/visual-edit/SKILL.md +615 -0
  133. package/skills/global_config/visual-plan/README.md +93 -0
  134. package/skills/global_config/visual-plan/SKILL.md +544 -0
  135. package/skills/global_config/visual-plan/references/canvas.md +139 -0
  136. package/skills/global_config/visual-plan/references/connection.md +51 -0
  137. package/skills/global_config/visual-plan/references/document-quality.md +186 -0
  138. package/skills/global_config/visual-plan/references/exemplar.md +62 -0
  139. package/skills/global_config/visual-plan/references/local-files.md +99 -0
  140. package/skills/global_config/visual-plan/references/wireframe.md +319 -0
  141. package/skills/global_config/visual-recap/README.md +103 -0
  142. package/skills/global_config/visual-recap/SKILL.md +560 -0
  143. package/skills/global_config/visual-recap/references/connection.md +51 -0
  144. package/skills/global_config/visual-recap/references/local-files.md +99 -0
  145. package/skills/global_config/visual-recap/references/wireframe.md +319 -0
  146. package/skills/playbooks/pb-ci-fix/SKILL.md +49 -0
  147. package/skills/playbooks/pb-event-tracker/SKILL.md +45 -0
  148. package/skills/playbooks/pb-meeting-actions/SKILL.md +42 -0
  149. package/skills/playbooks/pb-project-new/SKILL.md +48 -0
  150. package/skills/playbooks/pb-week-plan/SKILL.md +45 -0
  151. package/assets/header-v4.jpg +0 -0
@@ -0,0 +1,285 @@
1
+ # BDB Plan Builder
2
+
3
+ Renders an Agent-Native-style **plan folder** (local-files format) into ONE
4
+ self-contained HTML file in the BDB look, which `aos-plan-canvas` then opens
5
+ through its normal `.html` artifact path — so annotation, chat and verdict
6
+ already work with no canvas changes.
7
+
8
+ CommonJS, zero dependencies, no network at build time.
9
+
10
+ ## Input / output
11
+
12
+ | Path | Role |
13
+ |---|---|
14
+ | `<plan-dir>/plan.mdx` | **required** — frontmatter + document blocks |
15
+ | `<plan-dir>/canvas.mdx` | optional — appended as a "Canvas" section |
16
+ | `<plan-dir>/prototype.mdx` | optional — appended as a "Prototype" section |
17
+ | `<plan-dir>/.plan-state.json` | optional — `title`, `status`/`kind`, `localOnly` |
18
+ | `<plan-dir>/plan.builder.html` | **output** — written next to the plan |
19
+
20
+ The output lives inside the folder because plan-canvas only opens artifacts
21
+ inside the workspace root.
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Build and open in one step
27
+ aos-plan-canvas open <plan-dir> --mode bdb-plan-builder
28
+ aos-plan-canvas open <plan-dir>/plan.mdx --mode bdb-plan-builder --no-open
29
+
30
+ # Then listen for the review verdict
31
+ aos-plan-canvas await <plan-dir>/plan.builder.html
32
+ ```
33
+
34
+ Rebuild by re-running `open`: edit the MDX, run `open` again. A path with no
35
+ `plan.mdx` exits **2** with the reason on stderr and stdout.
36
+
37
+ Library:
38
+
39
+ ```js
40
+ const { renderPlanFolder, renderPlanSource, parseMdx } = require('./index');
41
+
42
+ renderPlanFolder('./plans/my-plan'); // { html, warnings, outFile }
43
+ renderPlanSource({ plan, canvas, state }); // { html, warnings }
44
+ parseMdx('<Code code={"x"} />'); // block array
45
+ ```
46
+
47
+ `renderPlanFolder` reports a missing `plan.mdx` as `error` in the return value;
48
+ it never throws.
49
+
50
+ ## Supported tags
51
+
52
+ Tag names are the block-registry MDX names from `visual-plan` / `visual-recap`.
53
+ The lowercase conceptual names are accepted as aliases.
54
+
55
+ | Tag (aliases) | Props read | Renders as |
56
+ |---|---|---|
57
+ | `Diagram` (`diagram`) | `data.html`, `data.css`, `data.source`, `data.nodes`, `data.edges`, `frame`, `label` | sandboxed frame for HTML; Mermaid for `source`; table for nodes/edges |
58
+ | `Mermaid` (`mermaid`) | `source`, `code`, `label` | `<pre class="mermaid">` + pinned ESM loader |
59
+ | `FileTree` (`file-tree`) | `entries[]` with `path`, `change`, `note`, `snippet`, `depth` | change-badged monospace tree |
60
+ | `WireframeBlock` (`wireframe`), `Screen` (`screen`) | `html`, `surface`, `css`, `label`, `caption`, `height`, or wireframe-kit children | `html`: sandboxed frame (no `allow-scripts`); kit children: low-fi markup, see [Wireframe kit](#wireframe-kit) |
61
+ | `Diff` (`diff`) | `before`, `after`, `filename`, `language`, `mode`, `summary`, `annotations[]` | split or unified two-pane diff |
62
+ | `Code` (`code`), `AnnotatedCode` (`annotated-code`) | `code`, `filename`, `language`, `annotations[]` | code block + margin notes |
63
+ | `Endpoint` (`endpoint`, `api-endpoint`, `ApiEndpoint`) | `method`, `path`, `params[]`, `examples[]`, children prose | method/path header, param table, JSON examples |
64
+ | `DataModel` (`data-model`) | `entities[].fields[]` with `name`, `type`, `change`, `was`, `note` | nested entity cards |
65
+ | `QuestionForm` (`question-form`) | `title`, `questions[]` with `title`, `mode`, `options[]` | Open Questions card, recommended option marked |
66
+ | `Columns` (`columns`) | `columns[].label` + nested blocks | two-column grid, stacked on phones |
67
+ | `TabsBlock` (`tabs`, `Tabs`) | `tabs[].label` + nested blocks | stacked labelled groups (no click JS — the annotation layer owns clicks) |
68
+ | `Archify` (`archify`) | `src` (relative `.html` inside the plan folder), `label`, `height` | delivered Archify diagram in `<iframe sandbox="allow-scripts">` (never `allow-same-origin`), caption and link row; see [Archify](#archify) |
69
+ | `AgentTrail` (`agent-trail`) | `live` (http(s) URL on localhost/127.0.0.1), `embed` (flag, needs `live`) | static dependency graph of the plan's components (columns by `needs` depth, scaled with CSS to the container width, edges only for stated needs, task progress, expandable tasks) derived from the same folder's `{#id}` headings, `ImplementationMap` and `Checklist`; `live` adds an "Open live agent trail" link, `embed` a sandboxed iframe (`allow-scripts allow-same-origin`); works inside `<Artboard surface="web">`; no components gives a visible card and a warning |
70
+ | `CustomHtml` (`custom-html`) | `html`, `css`, `label`, `height` | sandboxed frame, **no `allow-scripts`** |
71
+ | `RichText` (`rich-text`) | `title`, markdown children | prose |
72
+ | `Callout` | `tone`, `title`, markdown children | bordered card |
73
+ | `Checklist` (`checklist`) | `title`, `items[]` (strings or `{label, checked, note}`), children: `- [x] item` lines or `<Item checked>` tags | checkbox list |
74
+ | `Table` (`table`) | `title`, `columns[]`, `rows[]` (arrays, or objects keyed by column), or a markdown table as children | bordered table, inline markdown in cells |
75
+ | `CodeTabs` (`code-tabs`) | `tabs[]` with `label`, `language`, `code` | one card, every tab stacked under its filename |
76
+ | `Decision` (`decision`) | `title`, `question`, `options[]` (`label`, `detail`, `recommended`), `recommended` (id, label or index), `rationale` or children prose | question, option cards with a `recommended` badge, rationale |
77
+ | `HtmlBlock` (`html-block`) | `html` or children, `title`, `css`, `height` | sandboxed frame, **no `allow-scripts`** (same as `CustomHtml`) |
78
+ | `ImplementationMap` (`implementation-map`) | `title`, `files[]` (`path`, `title`, `note`, `change`, `snippet`), or children lines | file list with change badges; an unparsable `files` shows its escaped raw text in a visible card plus a warning |
79
+ | `Compare` (`compare`) | `before`, `after` (markdown, `{html}`), `beforeLabel`, `afterLabel`, or `<Before>`/`<After>` children, or exactly two child blocks | Before / After two-column comparison |
80
+ | `Json`, `OpenApiSpec` (`openapi`) | `code`/`data`, `spec` | code block |
81
+ | `DesignBoard`, `Section` | children | pass-through (canvas.mdx containers) |
82
+ | `Artboard` | `id`, `label`/`title`, `surface`, `x`, `y`, `width`, `height`, `order`, children | card wrapping the artboard's `Screen`; absolutely positioned when `x`/`y` are set, see [Absolute board layout](#absolute-board-layout) |
83
+ | `Annotation` | `title`, markdown children or `text`, `targetId`, `placement`, `x`, `y` | small muted note with an arrow glyph |
84
+ | `Connector` | `label`/`text` | connector label card |
85
+
86
+ **Unknown tag → visible card.** Any tag not in the table renders as a bordered
87
+ "unsupported block" card showing the tag name plus its escaped raw source, and
88
+ adds a line to `warnings` (also shown in a banner at the top of the page).
89
+ Content is never dropped silently. A block that fails to render becomes an error
90
+ card instead of taking the document down.
91
+
92
+ ## Prop syntax
93
+
94
+ `{...}` attribute values are JSON **or** a data-only JavaScript literal:
95
+ unquoted keys, single quotes, trailing commas, comments and `'a' + 'b'` string
96
+ concatenation all parse. Nothing is evaluated; an expression that is neither
97
+ becomes a warning and the raw text is kept.
98
+
99
+ ## Wireframe kit
100
+
101
+ A `<Screen>` (or `<FrameScreen>`) without an `html` prop whose children are tags
102
+ renders the kit below as plain markup in the page (no iframe). Low-fi look:
103
+ muted greys on dark, 1px borders, the BDB accent only for `active`/`primary`/done
104
+ states, and the same UI font stack as plan-canvas and the AOS store (no handwriting font). An empty `<Screen>` renders an empty frame.
105
+
106
+ | Tag | Props | Renders |
107
+ |---|---|---|
108
+ | `FrameScreen`, `Main`, `Col`, `Row` | `full` | flex containers; `full` fills the remaining space |
109
+ | `Box`, `Card` | `dashed`, `full` | thin bordered container |
110
+ | `Lines` | `n`, `widths[]` (percent) | `n` text bars |
111
+ | `IconSquare` | `active` | small rounded square, accent when active |
112
+ | `Divider`, `StatusBar` | | rule; phone status strip |
113
+ | `TaskRow` | `title`, `done`, `note` | checkbox row |
114
+ | `Text` | `value` (or children), `tone="muted"`, `weight="bold"` | wireframe text |
115
+ | `Title`, `SectionLabel`, `Btn`, `Chips` | `text`, `label`, `label`/`primary`, `items[]` | heading, caps label, button, chip row |
116
+ | `Skeleton` | `lines` + `widths`, or `width`/`height` | placeholder bars |
117
+
118
+ An unknown kit tag renders a small labelled placeholder (`<Name>`) and adds a
119
+ warning. Semantic `<Screen html={...}/>` is unchanged: sandboxed iframe.
120
+
121
+ ## Absolute board layout
122
+
123
+ If any `<Artboard>` carries numeric `x` and `y`, the board switches from numbered
124
+ flow rows to an absolute canvas with a dotted grid:
125
+
126
+ - Artboards sit at `x`/`y` with `width`/`height` (defaults from `surface`), sorted in
127
+ the DOM by `order` (shown as a small number chip); the canvas is the bounding box
128
+ of everything plus a margin.
129
+ - The artboard `label` sits above the frame, a `Screen` `caption` under it, and a
130
+ `Section` `title`/`subtitle` becomes a numbered label above its artboards.
131
+ - `Annotation` goes under (`placement="bottom"`, default), above, left or right of
132
+ its `targetId` artboard, or at its own `x`/`y`. Notes for the same target stack.
133
+ - Artboards without coordinates, annotations without a target or position, and
134
+ non-artboard blocks are listed in a tray below the canvas.
135
+ - Connectors are unchanged: only stated `transitions` / `Connector` / `edges`.
136
+ A coordinate-free canvas keeps the flow-row layout.
137
+
138
+ ## Visual recap
139
+
140
+ A plan whose frontmatter has `kind: recap` (or whose `.plan-state.json` has
141
+ `"kind": "recap"`) gets a recap header instead of the plain title: the eyebrow
142
+ `VISUAL RECAP`, a large title, a subtitle, and a chip row.
143
+
144
+ | Frontmatter | Chip |
145
+ |---|---|
146
+ | `title`, `subtitle` (or `summary`) | title, subtitle |
147
+ | `pr` | `PR #214` |
148
+ | `branch`, `base` | `branch feat/x → main` |
149
+ | `commit`, `author`, `date` | commit, `by …`, date |
150
+ | `files` | `4 files` |
151
+ | `additions`, `deletions` | `+186`, `−41` |
152
+
153
+ Fields that are missing are skipped. Use `<Compare>` for the Before / After
154
+ comparison:
155
+
156
+ ```mdx
157
+ <Compare>
158
+ <Before><Screen surface="mobile">...</Screen></Before>
159
+ <After><Screen surface="mobile">...</Screen></After>
160
+ </Compare>
161
+ ```
162
+
163
+ `<Compare before="..." after="..." />` takes markdown, and a `<Compare>` with
164
+ exactly two child blocks (paragraphs count) pairs them as before and after.
165
+
166
+ ## Board view
167
+
168
+ Visual blocks render on a dark, pan/zoomable **board**: numbered rows of
169
+ fixed-width cards (`1 · Auth Entry`), SVG arrows with small muted labels, a
170
+ zoom control bottom-left, and a `VISUAL PLAN` mark in the footer.
171
+
172
+ **Board markup rule** (everything else stays document-style):
173
+
174
+ 1. Everything in `canvas.mdx` is a board. `<Section title>` (or a Markdown
175
+ heading) starts a numbered row; `<DesignBoard>` is pass-through.
176
+ 2. In `plan.mdx`, a heading tagged `{#board}` (`## Flow {#board}`) hands its
177
+ section, up to the next heading of the same or a higher level, to a board.
178
+ The tag is stripped from the heading text.
179
+ 3. A block carrying a `board` prop (`<Mermaid board ... />`) is boarded; a run
180
+ of such blocks shares one board.
181
+ 4. A plan with none of these renders exactly as before and ships no board JS.
182
+
183
+ **Arrows** are drawn only from relations the plan states, never inferred:
184
+ `transitions={[{from, to, label}]}` on any container, `<Connector from to
185
+ label />`, or `data.edges` of a node/edge `Diagram` (its nodes become cards).
186
+ `from`/`to` match a card `id` (or `blockId`, or the slug of its title; `source`,
187
+ `target`, `fromId`, `toId` are accepted too). An unknown endpoint adds a warning
188
+ and draws no arrow.
189
+
190
+ **Interaction** (`board-client.js`, inlined, no dependencies): `+`, `-`, `fit`,
191
+ `1:1` buttons and Ctrl/Cmd+wheel zoom with a percentage readout; drag empty
192
+ background or hold Space and drag to pan. The script never listens for clicks
193
+ on cards or text (the annotation layer owns those) and only calls
194
+ `preventDefault` for Ctrl/Cmd+wheel and the Space key over a board. Arrow paths
195
+ are measured in the browser, so without JS the board simply scrolls inside its
196
+ own container and arrows are not drawn (a hidden list keeps the relations
197
+ readable). Below 900px the side navigation is hidden when the page is
198
+ board-only.
199
+
200
+ **Demos** (`aos-plan-canvas open lib/plan-builder/examples/<name> --mode bdb-plan-builder`):
201
+
202
+ - `demo-plan`: 4 screens, 3 transitions, 2 numbered sections, one Mermaid diagram.
203
+ - `signup-storyboard`: 6 absolutely positioned kit artboards, 2 sections, 5 transitions, 2 annotations.
204
+ - `recap-demo`: `kind: recap` header, Before/After, implementation map, table, code tabs.
205
+
206
+ All demo content is invented.
207
+
208
+ ## Archify
209
+
210
+ `<Archify src="00_architecture.html" label="System" height={560} />` embeds the
211
+ standalone HTML produced by the `archify` skill. `src` is resolved against the
212
+ plan folder and must stay inside it: `..`, absolute paths, URLs and symlink
213
+ escapes show an error card plus a warning, as does a missing file ("file not
214
+ found"), a non-`.html` file, or a file over 5 MB. The page runs in
215
+ `sandbox="allow-scripts"` only (no same-origin, forms, popups or top
216
+ navigation) and a link row opens the file standalone.
217
+
218
+ ## Prototype hint
219
+
220
+ If any `Artboard` or `Screen` has `surface` `web` or `desktop`, or the `plan.mdx`
221
+ frontmatter says `prototype: suggest`, the document ends with one "Suggested next
222
+ step" callout naming those screens and saying a throwaway prototype can be built
223
+ with the `prototype` skill. It is plain escaped text and starts nothing.
224
+ `prototype: skip`, or a plan with no such screens, omits it.
225
+
226
+ ## Trail export
227
+
228
+ `aos-plan-canvas trail <plan-dir|plan.mdx> [--out <file>] [--force]` writes an
229
+ agenttrail plan file (`trail.js`) so `aos-trail . --plan <file> --no-open` shows
230
+ the live map after approval. Components: headings tagged `{#id}` and `<Section
231
+ id title>`, in document order across `plan.mdx`, `canvas.mdx`, `prototype.mdx`.
232
+ `needs`: `<Section needs=[...]>` or frontmatter `needs-<id>: a, b`. `files`:
233
+ `<ImplementationMap>` entries. Tasks: `<Checklist>` items (`{#id}` kept, else
234
+ generated). The default output is `production_artifacts/00_execution_plan.md`; it must resolve
235
+ inside the workspace root (cwd), is never overwritten without `--force`, and no
236
+ components means exit 2. Prints `{out, components, tasks, next_step}`.
237
+
238
+ ## Templates
239
+
240
+ Starter plans live in `templates/<id>/` (`meta.json`, `plan.mdx`, optional
241
+ `canvas.mdx`, `standard.md`).
242
+
243
+ ```bash
244
+ aos-plan-canvas templates # JSON: id, label, description, useWhen, hasBoard (= has a design canvas)
245
+ aos-plan-canvas new <template-id> <target-dir> # --mode bdb-plan-builder (default) | standard
246
+ ```
247
+
248
+ Recap templates: `recap` (minimal), `recap-review` (document-style recap for a PR or merge decision: summary, changed areas with FileTree and ImplementationMap, decisions with the rejected alternative, a before/after `Compare`, a commands table plus an explicit Verified / Not verified split, risks) and `recap-board` (design canvas with BEFORE and AFTER artboards, a labelled `change` connector and numbered `Annotation` markers on AFTER, plus a short `plan.mdx` whose numbered change list matches the markers).
249
+
250
+ Builder mode copies `plan.mdx` (and `canvas.mdx` when the template has a board,
251
+ never `meta.json`); standard mode writes `plan.md` from `standard.md`. A
252
+ non-empty target exits **2** and nothing is overwritten; an unknown id exits
253
+ **2** and lists the valid ids. A "board" template includes a design canvas (screens + arrows). `new` never starts the canvas server. The home page (`GET /`) lists templates, open reviews and installed visual skills read-only. A template
254
+ folder without a readable `meta.json` is skipped by `templates`.
255
+
256
+ ## Section navigation
257
+
258
+ The left section list has a "Hide sections" toggle in the top bar; the choice is kept in `localStorage` (page works without it). On phone width the list stays visible.
259
+
260
+ ## Security
261
+
262
+ - Everything from the plan is escaped (`escapeHtml`, and `renderMarkdown` escapes
263
+ before any inline rule runs).
264
+ - `Archify` is the one sandbox that allows scripts (`allow-scripts`, no same-origin), for a file read from inside the plan folder.
265
+ - Raw HTML — `custom-html`, `HtmlBlock`, and the `html` of wireframe/diagram blocks — only
266
+ reaches `<iframe sandbox srcdoc=...>` **without `allow-scripts`**.
267
+ - Attribute values are JSON-parsed, never evaluated. Template-literal
268
+ interpolation is refused with a warning.
269
+
270
+ ## Files
271
+
272
+ | File | Role |
273
+ |---|---|
274
+ | `index.js` | `renderPlanFolder` / `renderPlanSource` / re-export `parseMdx` |
275
+ | `trail.js` | agenttrail plan file from a plan folder |
276
+ | `mdx.js` | frontmatter, headings, prose, JSX-like tags; never throws |
277
+ | `render.js` | block → HTML, flow and absolute boards, recap header, Mermaid loader, page shell |
278
+ | `kit.js` | wireframe kit tags → markup |
279
+ | `board-client.js` | pan/zoom + arrow routing, inlined only when a board exists |
280
+ | `theme.css` | BDB CI theme, inlined into the output |
281
+ | `examples/demo-plan/` | richer demo: plan.mdx + canvas.mdx |
282
+ | `examples/signup-storyboard/`, `examples/recap-demo/` | kit storyboard and recap demos |
283
+
284
+ Adding this directory is also what flips `bdb-plan-builder` to **available** in
285
+ `aos-plan-canvas modes`.
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * <AgentTrail /> — static, embeddable view of the plan's multi-agent workflow.
5
+ * Components, needs and tasks come from derive() in trail.js (same plan folder);
6
+ * nothing is parsed twice. The live `aos-trail` app stays the standard; `live`
7
+ * only links to it, and `embed` additionally frames it.
8
+ */
9
+
10
+ const { escapeHtml: esc } = require('../plan-canvas/markdown');
11
+
12
+ const COL_W = 210;
13
+ const COL_GAP = 60;
14
+ const ROW_H = 80;
15
+ const ROW_GAP = 16;
16
+ const PAD = 8;
17
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1']);
18
+
19
+ function localUrl(raw) {
20
+ try {
21
+ const u = new URL(String(raw));
22
+ if (!/^https?:$/.test(u.protocol) || !LOCAL_HOSTS.has(u.hostname) || u.username || u.password) return null;
23
+ return u.href;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ // Level = longest chain of stated needs; a cycle edge is ignored rather than looping.
30
+ function levels(components) {
31
+ const byId = new Map(components.map((c) => [c.id, c]));
32
+ const memo = new Map();
33
+ const walking = new Set();
34
+ const level = (c) => {
35
+ if (memo.has(c.id)) return memo.get(c.id);
36
+ walking.add(c.id);
37
+ let n = 0;
38
+ for (const need of c.needs) {
39
+ const dep = byId.get(need);
40
+ if (dep && !walking.has(dep.id)) n = Math.max(n, level(dep) + 1);
41
+ }
42
+ walking.delete(c.id);
43
+ memo.set(c.id, n);
44
+ return n;
45
+ };
46
+ components.forEach(level);
47
+ return memo;
48
+ }
49
+
50
+ const pct4 = (f) => Number((f * 100).toFixed(3));
51
+
52
+ function note(message) {
53
+ return `<div class="card trail-empty"><div class="label">AgentTrail</div><div>${esc(message)}</div></div>`;
54
+ }
55
+
56
+ function renderAgentTrail(block, ctx) {
57
+ const props = block.props || {};
58
+ let model = [];
59
+ try {
60
+ if (!ctx.dir) throw new Error('needs a plan folder on disk');
61
+ model = require('./trail').derive(ctx.dir).graph;
62
+ } catch (error) {
63
+ ctx.warnings.push(`<AgentTrail> could not read the plan folder: ${error.message}`);
64
+ }
65
+ if (!model.length) {
66
+ ctx.warnings.push('<AgentTrail> no components found; add {#id} headings and needs: lines');
67
+ return note('AgentTrail: no components found - add {#id} headings and needs: lines');
68
+ }
69
+
70
+ const level = levels(model);
71
+ const rowOf = new Map();
72
+ const pos = new Map();
73
+ for (const c of model) {
74
+ const col = level.get(c.id);
75
+ const row = rowOf.get(col) || 0;
76
+ rowOf.set(col, row + 1);
77
+ pos.set(c.id, { x: PAD + col * (COL_W + COL_GAP), y: PAD + row * (ROW_H + ROW_GAP) });
78
+ }
79
+ const width = PAD * 2 + (Math.max(...level.values()) + 1) * COL_W + Math.max(...level.values()) * COL_GAP;
80
+ const height = PAD * 2 + Math.max(...rowOf.values()) * ROW_H + (Math.max(...rowOf.values()) - 1) * ROW_GAP;
81
+
82
+ const edges = [];
83
+ for (const c of model) {
84
+ for (const need of c.needs) {
85
+ const a = pos.get(need);
86
+ const b = pos.get(c.id);
87
+ if (!a || !b) continue;
88
+ const x1 = a.x + COL_W;
89
+ const y1 = a.y + ROW_H / 2;
90
+ const x2 = b.x;
91
+ const y2 = b.y + ROW_H / 2;
92
+ const mid = (x1 + x2) / 2;
93
+ edges.push(`<path class="trail-edge" data-from="${esc(need)}" data-to="${esc(c.id)}" d="M${x1},${y1} C${mid},${y1} ${mid},${y2} ${x2},${y2}"/>`);
94
+ }
95
+ }
96
+
97
+ const cards = model.map((c) => {
98
+ const done = c.tasks.filter((t) => t.checked).length;
99
+ const total = c.tasks.length;
100
+ const pct = total ? Math.round((done / total) * 100) : 0;
101
+ const tasks = total
102
+ ? `<ul class="trail-tasks">${c.tasks.map((t) => `<li class="${t.checked ? 'done' : ''}"><span class="mark" aria-hidden="true">${t.checked ? '&#10003;' : '&#9675;'}</span>${esc(t.label)}</li>`).join('')}</ul>`
103
+ : '<div class="trail-tasks note">no tasks</div>';
104
+ const p = pos.get(c.id);
105
+ return `<details class="trail-node" data-id="${esc(c.id)}" style="left:${pct4(p.x / width)}%;top:${p.y}px;width:${pct4(COL_W / width)}%;min-height:${ROW_H}px">` +
106
+ `<summary><span class="trail-id">${esc(c.id)}</span><span class="trail-title" title="${esc(c.title)}">${esc(c.title)}</span>` +
107
+ `<span class="trail-count">${done} of ${total} tasks</span>` +
108
+ `<span class="trail-bar"><span style="width:${pct}%"></span></span></summary>${tasks}</details>`;
109
+ }).join('');
110
+
111
+ let live = '';
112
+ if (props.live !== undefined && props.live !== true) {
113
+ const href = localUrl(props.live);
114
+ if (!href) {
115
+ ctx.warnings.push(`<AgentTrail live="${String(props.live)}"> must be an http(s) URL on localhost or 127.0.0.1; link and iframe omitted`);
116
+ } else {
117
+ live = `<div class="trail-live"><a href="${esc(href)}" target="_blank" rel="noopener">Open live agent trail</a></div>`;
118
+ if (props.embed !== undefined && props.embed !== false) {
119
+ live += `<iframe class="trail-frame" sandbox="allow-scripts allow-same-origin" loading="lazy" title="Live agent trail" src="${esc(href)}"></iframe>`;
120
+ }
121
+ }
122
+ }
123
+
124
+ return '<div class="card trail"><div class="label">agent trail</div>' +
125
+ `<div class="trail-scroll"><div class="trail-stage" style="height:${height}px">` +
126
+ `<svg class="trail-edges" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" aria-hidden="true">${edges.join('')}</svg>${cards}</div></div>${live}</div>`;
127
+ }
128
+
129
+ module.exports = { renderAgentTrail, localUrl };
@@ -0,0 +1,124 @@
1
+ (function () {
2
+ var MIN = 0.2, MAX = 2.5;
3
+ document.documentElement.classList.add('js');
4
+ document.querySelectorAll('.board').forEach(function (board) {
5
+ var vp = board.querySelector('.board-viewport');
6
+ var stage = board.querySelector('.board-stage');
7
+ var canvas = board.querySelector('.board-canvas');
8
+ var svg = board.querySelector('.board-edges');
9
+ var readout = board.querySelector('.zoom-readout');
10
+ var scale = 1, space = false, hover = false, drag = null;
11
+
12
+ function route(a, b) {
13
+ var A = { l: a.offsetLeft, t: a.offsetTop, w: a.offsetWidth, h: a.offsetHeight };
14
+ var B = { l: b.offsetLeft, t: b.offsetTop, w: b.offsetWidth, h: b.offsetHeight };
15
+ var x1, y1, x2, y2, mx, my;
16
+ if (B.l >= A.l + A.w || A.l >= B.l + B.w) {
17
+ var right = B.l >= A.l + A.w;
18
+ x1 = right ? A.l + A.w : A.l; y1 = A.t + A.h / 2;
19
+ x2 = right ? B.l : B.l + B.w; y2 = B.t + B.h / 2;
20
+ mx = (x1 + x2) / 2;
21
+ return { d: 'M' + x1 + ' ' + y1 + 'H' + mx + 'V' + y2 + 'H' + x2, x: mx, y: (y1 + y2) / 2 };
22
+ }
23
+ var down = B.t >= A.t + A.h;
24
+ x1 = A.l + A.w / 2; y1 = down ? A.t + A.h : A.t;
25
+ x2 = B.l + B.w / 2; y2 = down ? B.t : B.t + B.h;
26
+ my = (y1 + y2) / 2;
27
+ return { d: 'M' + x1 + ' ' + y1 + 'V' + my + 'H' + x2 + 'V' + y2, x: (x1 + x2) / 2, y: my };
28
+ }
29
+
30
+ function layout() {
31
+ stage.style.width = canvas.offsetWidth * scale + 'px';
32
+ stage.style.height = canvas.offsetHeight * scale + 'px';
33
+ if (!svg) return;
34
+ svg.setAttribute('width', canvas.offsetWidth);
35
+ svg.setAttribute('height', canvas.offsetHeight);
36
+ svg.querySelectorAll('.edge').forEach(function (g) {
37
+ var a = document.getElementById(g.getAttribute('data-from'));
38
+ var b = document.getElementById(g.getAttribute('data-to'));
39
+ if (!a || !b) return;
40
+ var r = route(a, b);
41
+ g.querySelector('path').setAttribute('d', r.d);
42
+ var label = g.querySelector('text');
43
+ label.setAttribute('x', r.x);
44
+ label.setAttribute('y', r.y - 6);
45
+ });
46
+ }
47
+
48
+ function zoomTo(next, cx, cy) {
49
+ next = Math.min(MAX, Math.max(MIN, next));
50
+ var k = next / scale, sx = vp.scrollLeft + cx, sy = vp.scrollTop + cy;
51
+ scale = next;
52
+ canvas.style.transform = 'scale(' + next + ')';
53
+ layout();
54
+ vp.scrollLeft = sx * k - cx;
55
+ vp.scrollTop = sy * k - cy;
56
+ readout.textContent = Math.round(next * 100) + '%';
57
+ }
58
+
59
+ function fit() {
60
+ // Fit the width; a tall board scrolls vertically instead of shrinking to an unreadable scale.
61
+ var byWidth = Math.min(1, vp.clientWidth / canvas.offsetWidth);
62
+ var byHeight = vp.clientHeight / canvas.offsetHeight;
63
+ zoomTo(byHeight < byWidth ? Math.max(byHeight, Math.min(byWidth, 0.45)) : byWidth, 0, 0);
64
+ vp.scrollLeft = 0;
65
+ vp.scrollTop = 0;
66
+ }
67
+
68
+ board.querySelectorAll('[data-zoom]').forEach(function (btn) {
69
+ btn.addEventListener('click', function () {
70
+ var mode = btn.getAttribute('data-zoom');
71
+ var cx = vp.clientWidth / 2, cy = vp.clientHeight / 2;
72
+ if (mode === 'in') zoomTo(scale * 1.25, cx, cy);
73
+ else if (mode === 'out') zoomTo(scale / 1.25, cx, cy);
74
+ else if (mode === 'fit') fit();
75
+ else { zoomTo(1, 0, 0); vp.scrollLeft = 0; vp.scrollTop = 0; }
76
+ });
77
+ });
78
+
79
+ vp.addEventListener('wheel', function (e) {
80
+ if (!(e.ctrlKey || e.metaKey)) return;
81
+ e.preventDefault();
82
+ var r = vp.getBoundingClientRect();
83
+ zoomTo(scale * Math.exp(-e.deltaY * 0.0015), e.clientX - r.left, e.clientY - r.top);
84
+ }, { passive: false });
85
+
86
+ vp.addEventListener('pointerdown', function (e) {
87
+ if (e.button !== 0 || e.offsetX > vp.clientWidth || e.offsetY > vp.clientHeight) return;
88
+ if (!space && e.target.closest('.bcard')) return;
89
+ drag = { x: e.clientX, y: e.clientY, l: vp.scrollLeft, t: vp.scrollTop };
90
+ vp.classList.add('panning');
91
+ vp.setPointerCapture(e.pointerId);
92
+ });
93
+ vp.addEventListener('pointermove', function (e) {
94
+ if (!drag) return;
95
+ vp.scrollLeft = drag.l - (e.clientX - drag.x);
96
+ vp.scrollTop = drag.t - (e.clientY - drag.y);
97
+ });
98
+ function endDrag() { drag = null; vp.classList.remove('panning'); }
99
+ vp.addEventListener('pointerup', endDrag);
100
+ vp.addEventListener('pointercancel', endDrag);
101
+
102
+ board.addEventListener('mouseenter', function () { hover = true; });
103
+ board.addEventListener('mouseleave', function () { hover = false; });
104
+ document.addEventListener('keydown', function (e) {
105
+ if (e.code !== 'Space' || !hover || e.target.isContentEditable || /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(e.target.tagName)) return;
106
+ space = true;
107
+ vp.classList.add('space');
108
+ e.preventDefault();
109
+ });
110
+ document.addEventListener('keyup', function (e) {
111
+ if (e.code !== 'Space') return;
112
+ space = false;
113
+ vp.classList.remove('space');
114
+ });
115
+
116
+ if (window.ResizeObserver) {
117
+ var ro = new ResizeObserver(layout);
118
+ ro.observe(canvas);
119
+ board.querySelectorAll('.bcard').forEach(function (c) { ro.observe(c); });
120
+ }
121
+ window.addEventListener('resize', layout);
122
+ fit();
123
+ });
124
+ })();
@@ -0,0 +1,19 @@
1
+ <DesignBoard transitions={[{"from":"entry","to":"check-email","label":"submit email"},{"from":"check-email","to":"home","label":"open link"},{"from":"check-email","to":"expired","label":"link too old"}]}>
2
+ <Section title="Auth Entry">
3
+ <Artboard id="entry" title="Sign in">
4
+ <Screen surface="mobile" html={'<div class="wf-card"><h3>Welcome back</h3><p class="wf-muted">Enter your email</p><p>name@example.com</p><button class="primary">Send link</button></div>'} />
5
+ </Artboard>
6
+ <Artboard id="check-email" title="Check your email">
7
+ <Screen surface="mobile" html={'<div class="wf-card"><h3>Link sent</h3><p class="wf-muted">Open the link on this device.</p></div>'} />
8
+ </Artboard>
9
+ </Section>
10
+ <Section title="Outcome">
11
+ <Artboard id="home" title="Home">
12
+ <Screen surface="mobile" html={'<div class="wf-card"><h3>Signed in</h3><p class="wf-muted">Your projects</p></div>'} />
13
+ </Artboard>
14
+ <Artboard id="expired" title="Link expired">
15
+ <Screen surface="mobile" html={'<div class="wf-card"><h3>This link expired</h3><button class="primary">Send a new link</button></div>'} />
16
+ </Artboard>
17
+ <Annotation>Expired links never reveal whether the email has an account.</Annotation>
18
+ </Section>
19
+ </DesignBoard>
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Demo Plan - Sign-in Flow
3
+ status: draft
4
+ ---
5
+
6
+ # Sign-in flow
7
+
8
+ ## Goal
9
+
10
+ Let a returning user sign in with an email link, and show a clear error when the link has expired. All data here is invented for the demo.
11
+
12
+ ## Sequence
13
+
14
+ <Mermaid label="Sign-in sequence" source={"sequenceDiagram\n participant U as User\n participant A as App\n participant M as Mailer\n U->>A: request link\n A->>M: send link\n M-->>U: email\n U->>A: open link\n A-->>U: signed in"} />
15
+
16
+ ## Open questions
17
+
18
+ <QuestionForm title="Open Questions" questions={[{"title":"How long is a link valid?","mode":"single","options":[{"label":"15 minutes","recommended":true},{"label":"1 hour"}]}]} />
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Offline drafts for the notes editor
3
+ subtitle: Drafts now survive a lost connection and sync when the network returns. Invented example.
4
+ kind: recap
5
+ pr: "#214"
6
+ branch: feat/offline-drafts
7
+ base: main
8
+ commit: 4f2a9c1
9
+ files: 4
10
+ additions: 186
11
+ deletions: 41
12
+ author: sample-author
13
+ date: 2026-01-15
14
+ ---
15
+
16
+ ## What changed
17
+
18
+ Typing in the editor no longer stops when the connection drops. Each edit is written to a local queue first and replayed in order once the network is back.
19
+
20
+ ## Before and after
21
+
22
+ <Compare>
23
+ <Before>
24
+ <Screen surface="mobile" caption="Connection lost">
25
+ <FrameScreen>
26
+ <Col full>
27
+ <Title text="Untitled note" />
28
+ <Lines n={3} widths={[90, 70, 40]} />
29
+ <Box dashed><Text value="Could not save. Retry" tone="muted" /></Box>
30
+ </Col>
31
+ </FrameScreen>
32
+ </Screen>
33
+ </Before>
34
+ <After>
35
+ <Screen surface="mobile" caption="Connection lost">
36
+ <FrameScreen>
37
+ <Col full>
38
+ <Title text="Untitled note" />
39
+ <Lines n={3} widths={[90, 70, 40]} />
40
+ <Row><IconSquare active /><Text value="Saved on this device" tone="muted" /></Row>
41
+ </Col>
42
+ </FrameScreen>
43
+ </Screen>
44
+ </After>
45
+ </Compare>
46
+
47
+ ## Files
48
+
49
+ <ImplementationMap files={[
50
+ { path: "editor/draft-queue.ts", change: "added", note: "Ordered local queue with replay" },
51
+ { path: "editor/save.ts", change: "modified", note: "Write to the queue before the network call" },
52
+ { path: "editor/status-pill.tsx", change: "modified", note: "New 'Saved on this device' state" },
53
+ { path: "editor/legacy-retry.ts", change: "removed", note: "Replaced by the queue" },
54
+ ]} />
55
+
56
+ ## Behaviour
57
+
58
+ <Table columns={["State", "Before", "After"]} rows={[
59
+ ["Offline typing", "Edits lost on reload", "Edits kept and replayed"],
60
+ ["Reconnect", "Manual retry", "Automatic replay in order"],
61
+ ]} />
62
+
63
+ <CodeTabs tabs={[
64
+ { label: "draft-queue.ts", language: "ts", code: "export function enqueue(edit: Edit) {\n queue.push(edit);\n persist(queue);\n}" },
65
+ { label: "save.ts", language: "ts", code: "await enqueue(edit);\nif (online) await flush();" },
66
+ ]} />
67
+
68
+ <Callout tone="note" title="Review focus">
69
+
70
+ Check the replay order when two tabs edit the same note.
71
+
72
+ </Callout>