acstack 0.0.0-stage → 0.47.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.
Files changed (143) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -2
  4. package/THIRD_PARTY_NOTICES +14 -0
  5. package/TROUBLESHOOTING.md +225 -0
  6. package/agents/codex/frontend-verifier.toml +352 -0
  7. package/agents/codex/task-checker.toml +345 -0
  8. package/agents/codex/task-executor.toml +157 -0
  9. package/agents/codex/test-writer.toml +149 -0
  10. package/agents/frontend-verifier.md +351 -0
  11. package/agents/task-checker.md +344 -0
  12. package/agents/task-executor.md +156 -0
  13. package/agents/test-writer.md +148 -0
  14. package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
  15. package/assets/playwriter-bridge/doctor.ps1 +450 -0
  16. package/assets/playwriter-bridge/launcher-src.cs +131 -0
  17. package/assets/playwriter-bridge/opener.js +330 -0
  18. package/assets/playwriter-bridge/opener.ps1 +63 -0
  19. package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
  20. package/assets/playwriter-bridge/relay.ps1 +50 -0
  21. package/assets/playwriter-bridge/stop.ps1 +125 -0
  22. package/assets/playwriter-bridge/sync.ps1 +271 -0
  23. package/assets/playwriter-bridge/tunnel.ps1 +325 -0
  24. package/assets/playwriter-bridge/vm-free-port.sh +166 -0
  25. package/assets/terminal-workspace/client-keybindings.md +87 -0
  26. package/assets/terminal-workspace/config.example.yaml +329 -0
  27. package/assets/terminal-workspace/setup.sh +4317 -0
  28. package/cli/README.md +47 -0
  29. package/cli/bin/acstack.js +3 -0
  30. package/cli/dist/index.js +25984 -0
  31. package/cli/dist/index.js.map +7 -0
  32. package/cli/package.json +59 -0
  33. package/docs/adr/0001-ste-writing-standard.md +16 -0
  34. package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
  35. package/docs/adr/0003-handoff-ack-command.md +19 -0
  36. package/docs/adr/0004-dev-setup-per-host.md +13 -0
  37. package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
  38. package/docs/adr/0006-document-opener-service.md +89 -0
  39. package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
  40. package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
  41. package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
  42. package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
  43. package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
  44. package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
  45. package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
  46. package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
  47. package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
  48. package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
  49. package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
  50. package/docs/adr/index.md +17 -0
  51. package/docs/config-reference.md +572 -0
  52. package/docs/dev-setups.md +164 -0
  53. package/docs/migrate-0.8.md +28 -0
  54. package/docs/playwriter-bridge-windows.md +15 -0
  55. package/docs/terminal-workspace-keys.md +137 -0
  56. package/docs/terminal-workspace.md +1774 -0
  57. package/docs/workflows.md +309 -0
  58. package/hooks/ac-writing-style-reminder.sh +86 -0
  59. package/hooks/context-pressure.sh +583 -0
  60. package/hooks/hooks.json +50 -0
  61. package/hooks/session-start-handoff.sh +165 -0
  62. package/hooks/session-start.sh +214 -0
  63. package/orchestrator/AGENTS.md +532 -0
  64. package/output-styles/ac-writing-style.md +286 -0
  65. package/package.json +50 -4
  66. package/pi/agents/frontend-verifier.md +357 -0
  67. package/pi/agents/task-checker.md +348 -0
  68. package/pi/agents/task-executor.md +160 -0
  69. package/pi/agents/test-writer.md +154 -0
  70. package/pi/extension.ts +723 -0
  71. package/profiles/fullstack-python.yaml +29 -0
  72. package/profiles/node-typescript.yaml +23 -0
  73. package/profiles/python.yaml +24 -0
  74. package/skills/archive/SKILL.md +160 -0
  75. package/skills/bootstrap/SKILL.md +109 -0
  76. package/skills/bridge/HANDOFF.md +93 -0
  77. package/skills/bridge/SKILL-VM.md +292 -0
  78. package/skills/bridge/SKILL-WINDOWS.md +526 -0
  79. package/skills/bridge/SKILL.md +121 -0
  80. package/skills/browser-verify/SKILL.md +229 -0
  81. package/skills/code-review/SKILL.md +44 -0
  82. package/skills/commit/SKILL.md +106 -0
  83. package/skills/compact-and-continue/SKILL.md +241 -0
  84. package/skills/explore/SKILL.md +149 -0
  85. package/skills/grilling/SKILL.md +134 -0
  86. package/skills/handoff/SKILL.md +250 -0
  87. package/skills/handoff/launch-herdr.md +197 -0
  88. package/skills/handoff/launch-without-herdr.md +30 -0
  89. package/skills/handoff/lead-check.md +59 -0
  90. package/skills/handoff/outcomes.md +162 -0
  91. package/skills/handoff/running-agent.md +50 -0
  92. package/skills/handoff/watch.md +115 -0
  93. package/skills/host/SKILL.md +247 -0
  94. package/skills/host/detect-host.sh +297 -0
  95. package/skills/host/herdr.md +79 -0
  96. package/skills/host/install-herdr-block.sh +467 -0
  97. package/skills/host/install-orca.sh +512 -0
  98. package/skills/host/notes.md +97 -0
  99. package/skills/host/orca.md +160 -0
  100. package/skills/host/questions.md +163 -0
  101. package/skills/host/report.md +42 -0
  102. package/skills/implement/SKILL.md +230 -0
  103. package/skills/implement/decision-log.md +62 -0
  104. package/skills/implement/design-pass.md +32 -0
  105. package/skills/implement/relaxed-tdd.md +28 -0
  106. package/skills/map/SKILL.md +250 -0
  107. package/skills/map/frontend.md +227 -0
  108. package/skills/map/trackers/files.md +74 -0
  109. package/skills/map/trackers/github.md +180 -0
  110. package/skills/map/visuals.md +387 -0
  111. package/skills/new-worktree/SKILL.md +155 -0
  112. package/skills/new-worktree/detect-orchestrator.sh +328 -0
  113. package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
  114. package/skills/new-worktree/herdr.md +84 -0
  115. package/skills/new-worktree/orca.md +101 -0
  116. package/skills/pre-commit-review/SKILL.md +242 -0
  117. package/skills/pre-commit-review/auto-fix.md +19 -0
  118. package/skills/pre-commit-review/interactive.md +35 -0
  119. package/skills/quick-fix/SKILL.md +125 -0
  120. package/skills/record-context/SKILL.md +106 -0
  121. package/skills/setup/SKILL.md +250 -0
  122. package/skills/setup/browser.md +123 -0
  123. package/skills/setup/design-review.md +97 -0
  124. package/skills/setup/first-run.md +114 -0
  125. package/skills/setup/report.md +68 -0
  126. package/skills/setup/row-fixes.md +196 -0
  127. package/skills/setup/terminal-workspace.md +153 -0
  128. package/skills/sync/SKILL.md +171 -0
  129. package/skills/teardown-worktree/SKILL.md +245 -0
  130. package/skills/teardown-worktree/detector.md +39 -0
  131. package/skills/teardown-worktree/herdr.md +113 -0
  132. package/skills/teardown-worktree/orca.md +61 -0
  133. package/skills/terminal-workspace/SKILL.md +249 -0
  134. package/skills/terminal-workspace/components.md +36 -0
  135. package/skills/terminal-workspace/config-choices.md +69 -0
  136. package/skills/terminal-workspace/emulator-keys.md +48 -0
  137. package/skills/terminal-workspace/headless-clipboard.md +175 -0
  138. package/skills/terminal-workspace/turn-off-and-update.md +104 -0
  139. package/skills/test-conventions/SKILL.md +59 -0
  140. package/skills/visual-docs/SKILL.md +246 -0
  141. package/skills/writing/SKILL.md +231 -0
  142. package/skills/writing/examples.md +153 -0
  143. package/skills/writing/vocabulary.md +96 -0
@@ -0,0 +1,180 @@
1
+ # GitHub tracker
2
+
3
+ First-class backend.
4
+
5
+ A node is an issue. The map is visible to the whole team, survives every
6
+ device, and needs no sync. It requires `gh` authenticated against the repo's
7
+ GitHub remote.
8
+
9
+ ## Mapping
10
+
11
+ | Node concept | GitHub |
12
+ | ------------ | ------ |
13
+ | node id | the issue number |
14
+ | map | issue label `map:<name>` on every node |
15
+ | title, body | issue title and body |
16
+ | `answers` | the `answers:` field of the body header, mirrored as a native sub-issue link |
17
+ | `blocked-by` | the `blocked-by:` field of the body header, mirrored as a native issue dependency |
18
+ | `fog` | open issue, issue label `fog` |
19
+ | `open` | open issue, no `fog` or `claimed` issue label |
20
+ | `claimed` | open issue, issue label `claimed` |
21
+ | `resolved` | closed as completed |
22
+ | `out-of-scope` | closed as not planned |
23
+ | `mode: afk` | issue label `afk` (its absence means `hitl`) |
24
+ | `kind` | issue label `kind:<value>` |
25
+ | `pinned` | issue label `pinned` |
26
+ | the root | issue label `root-node` |
27
+ | node `label` | the `label:` field of the body header |
28
+
29
+ Create every label at materialisation, in one pass:
30
+
31
+ ```bash
32
+ kinds=$(~/.claude/acstack/bin/acstack node kinds) &&
33
+ for l in "map:<name>" fog claimed afk pinned root-node \
34
+ $(echo "$kinds" | sed 's/^/kind:/'); do
35
+ gh label create "$l" 2>/dev/null || true
36
+ done
37
+ ```
38
+
39
+ The CLI owns the set of kinds. The loop reads that set from `acstack node
40
+ kinds`, so a kind added to the code reaches this loop with no edit here. A
41
+ second copy written out by hand would go stale in silence, since `|| true`
42
+ swallows every failure the loop meets.
43
+
44
+ The loop captures `kinds` first, and the `&&` holds it back until that read
45
+ succeeds. An installed CLI too old to know the subcommand prints its own error
46
+ and creates no label at all. Reading the kinds inline instead would substitute
47
+ an empty list and create the six fixed labels alone, silently.
48
+
49
+ The `root-node` label is how a reader and a query find the entry point.
50
+ Without it, finding the root means walking `parent` links until one is
51
+ missing, at one GraphQL call per hop.
52
+
53
+ To enumerate a repo's maps: `gh label list --search "map:" --json name`.
54
+
55
+ ## The four operations
56
+
57
+ **create**
58
+
59
+ ```bash
60
+ gh issue create --title "<title>" --body "<markdown>" --label "map:<name>[,fog][,claimed][,afk][,pinned][,root-node][,kind:<k>]"
61
+ ```
62
+
63
+ **Every body opens with the header line**, which is the first line of the
64
+ markdown and carries the node's whole place in the map:
65
+
66
+ ```
67
+ map: <name> · root: #12 · answers: #19 · blocked-by: #14, #17 · label: Nodes in git?
68
+ ```
69
+
70
+ - The root writes `root: this` and omits `answers`
71
+ - A node with no blockers omits `blocked-by`
72
+ - `label` is the node's short name, at most four words and thirty characters,
73
+ never the full title
74
+
75
+ The header does two jobs. A reader landing on the issue from a notification
76
+ sees which map it belongs to and what sits above it. Every composed query
77
+ below reads its edges from the header, in one `list` call.
78
+
79
+ Then mirror the edges as native links. Nothing in this workflow reads them
80
+ back, because every query below reads the header. They exist so GitHub's own
81
+ sub-issue and dependency interface renders the map.
82
+
83
+ First the provenance parent, which every node except the root has:
84
+
85
+ ```bash
86
+ child_id=$(gh api repos/{owner}/{repo}/issues/<child> --jq .id)
87
+ gh api -X POST repos/{owner}/{repo}/issues/<parent>/sub_issues -F sub_issue_id=$child_id
88
+ ```
89
+
90
+ And each blocking edge, in the same step that wrote the header. One write
91
+ that skips the other leaves the two copies out of step.
92
+
93
+ ```bash
94
+ blocker_id=$(gh api repos/{owner}/{repo}/issues/<blocker> --jq .id)
95
+ gh api -X POST repos/{owner}/{repo}/issues/<n>/dependencies/blocked_by -F issue_id=$blocker_id
96
+ ```
97
+
98
+ **read**
99
+
100
+ ```bash
101
+ gh issue view <n> --json number,title,body,state,stateReason,labels,url
102
+ ```
103
+
104
+ One call. The edges come from the body header, so the tracker calls neither
105
+ the dependencies API nor a GraphQL parent query.
106
+
107
+ **update**
108
+
109
+ - Fields: `gh issue edit <n> --add-label / --remove-label / --title / --body`
110
+ - Claim: `gh issue edit <n> --add-label claimed`. Release removes it.
111
+ - Resolve: append the answer to the body. Then run `gh issue close <n> --reason completed`
112
+ - Out of scope: `gh issue close <n> --reason "not planned"`
113
+ - Root: only `archive` closes it, once the map clears
114
+ - Graduate fog: `gh issue edit <n> --remove-label fog`
115
+ - Change an edge: rewrite the header line **and** change the matching native
116
+ link in the same step. Editing one copy alone leaves the two out of step.
117
+
118
+ - Remove a blocker: drop it from the header, then
119
+ `gh api -X DELETE repos/{owner}/{repo}/issues/<n>/dependencies/blocked_by/<blocker_id>`
120
+
121
+ **list**
122
+
123
+ ```bash
124
+ gh issue list --label "map:<name>" --state all --limit 500 \
125
+ --json number,title,body,state,stateReason,labels,url
126
+ ```
127
+
128
+ `body` carries the edges and the node's `label`. `url` is the address a node
129
+ id links to in a generated diagram, and `../visuals.md` gives the anchor syntax.
130
+ Neither field is optional.
131
+
132
+ `--limit` caps the result. Closed issues count toward it, so history alone
133
+ can hit the ceiling. If the result count equals the limit, re-query with a
134
+ higher one – a truncated list silently corrupts every composed query.
135
+
136
+ ## Composed queries
137
+
138
+ Read every edge from the body header, never from the native links. One
139
+ `list` call returns the whole graph, because `body` is one of its fields.
140
+ Reading the native links instead costs a GraphQL query for the parent and a
141
+ REST call for the blockers, per issue, on every round.
142
+
143
+ - **Frontier**: from `list`, keep open issues without `fog` or `claimed`
144
+ labels. Drop any whose header `blocked-by` names an open issue, and drop
145
+ the issue labelled `root-node`.
146
+ Order by provenance depth (follow header `answers` up to the root), issue
147
+ number as the final tiebreak.
148
+
149
+ The root is never work. `archive` closes it when the map clears.
150
+
151
+ - **Walk**: start at the issue labelled `root-node`, then recurse over the
152
+ nodes whose header `answers` names it, in number order. Read the root and
153
+ every `pinned` node in full, and take the title alone as the gist for the
154
+ rest.
155
+
156
+ - The tracker derives depth from the header chain and never stores it.
157
+
158
+ The CLI runs both queries from one `list` pipe:
159
+
160
+ ```bash
161
+ issues=$(gh issue list --label "map:<name>" --state all --limit 500 \
162
+ --json number,title,body,state,stateReason,labels,url)
163
+ echo "$issues" | ~/.claude/acstack/bin/acstack node frontier --stdin [--mode hitl|afk] [--json]
164
+ echo "$issues" | ~/.claude/acstack/bin/acstack node walk --stdin [--full] [--json] [--visuals]
165
+ ```
166
+
167
+ `--map` refuses on this backend. The map is not on disk, so a `--map` read
168
+ would answer out of an empty directory.
169
+
170
+ ## Degraded remotes
171
+
172
+ A degraded remote is one where GitHub's native link features are off or not
173
+ permitted. Sub-issues and dependencies need a repo with those features
174
+ enabled, and either API can return 404 or 403.
175
+
176
+ Nothing breaks. The body header already carries every edge and every query
177
+ already reads it, so a degraded remote loses only GitHub's own link interface.
178
+
179
+ Skip the failing API call and carry on. Note on the root node that the map
180
+ has no native links, so a later session does not retry them on every create.
@@ -0,0 +1,387 @@
1
+ # Visuals
2
+
3
+ Every map node explains itself with a diagram, two at most. A childless
4
+ `notes` node is the one exception.
5
+
6
+ Read this file when you materialise a map, when a `fog` node graduates to
7
+ `open`, at every resolution, and at every frontier recompute. `SKILL.md` owns
8
+ the map. This file owns what sits in a node body below the node's statement,
9
+ and how you regenerate a parent's diagram.
10
+
11
+ ## Which visual a node gets
12
+
13
+ ```mermaid
14
+ flowchart TD
15
+ C{"Does the node have children?"} -->|yes| G["Generated: its subtree<br/>acstack node diagram --map name --from id"]
16
+ C -->|no| K{"Its kind"}
17
+ K -->|notes| X["No visual required"]
18
+ K -->|decision| D["The option space"]
19
+ K -->|research| R["The region of the system it touches"]
20
+ K -->|work| W["The flow it changes, before and after"]
21
+ G --> V["Visual 2: free in type and subject, or none"]
22
+ X --> V
23
+ D --> V
24
+ R --> V
25
+ W --> V
26
+ ```
27
+
28
+ Visual 1 is the node's own explanation of itself. Read the diagram top down to
29
+ find it.
30
+
31
+ | kind | Visual 1 | Author |
32
+ | --- | --- | --- |
33
+ | `destination` | the whole map, resolved subtrees collapsed | generated |
34
+ | `notes` | exempt | none |
35
+ | `decision` | the option space, often a `quadrantChart` | you |
36
+ | `research` | the region of the system the question touches | you |
37
+ | `work` | the flow the change alters, before and after | you |
38
+
39
+ Four rules sit on top of that table:
40
+
41
+ - **A node with children leads with its subtree**, whatever its kind, so
42
+ `acstack node diagram --map <name> --from <id>` writes visual 1 and your own
43
+ diagram moves to slot 2
44
+ - The github backend pipes `gh issue list` in and passes `--stdin` instead
45
+ - `SKILL.md` holds both commands in full
46
+ - **Two visuals is the cap**, so a node that gains a child can lose its second
47
+ visual
48
+ - **A `notes` node is exempt**, since a standing preference such as "en dashes,
49
+ never em dashes" has no shape
50
+ - **Draw one on a `notes` node anyway** where it genuinely helps, in any diagram
51
+ type
52
+
53
+ A `fog` node is not exempt. It is the node a reader can least read, so its
54
+ diagram carries the most: draw the region you cannot yet state a question about.
55
+
56
+ ## Where visuals sit in the body
57
+
58
+ A node body has five parts, in this order:
59
+
60
+ 1. The header line, on GitHub only
61
+ 2. The node's statement, in prose
62
+ 3. The generated block, wrapped in its two markers
63
+ 4. Your hand-drawn visual
64
+ 5. `## Answer`, appended when the node resolves
65
+
66
+ ```
67
+ <!-- acstack:diagram -->
68
+ ...two mermaid fences...
69
+ <!-- /acstack:diagram -->
70
+ ```
71
+
72
+ - **The markers hold generated content only**, so a regeneration never touches a
73
+ word a human wrote
74
+ - **The generated block sits above everything a resolution writes**, because
75
+ `--append-body` writes `## Answer` to the end and can then never collide with
76
+ it
77
+ - **The files backend body starts at part 2**, since the CLI owns the
78
+ frontmatter and the title heading
79
+ - `--body` and `--append-body` never reach either one
80
+ - Writing the fields into the body duplicates what every query reads
81
+ - **A node with no generated diagram carries no markers**, since an empty marker
82
+ block is noise
83
+ - **A marker counts only when it passes three tests**, since a naive
84
+ search-and-replace corrupts any body that talks about markers
85
+ - It sits alone on its line, so a sentence that mentions one is prose
86
+ - It sits outside a code fence, so a fenced example is prose
87
+ - An end marker pairs with the last start marker before it, so an earlier
88
+ start was prose too
89
+ - **Insert a fresh pair directly below the statement** when no marker in the
90
+ body passes all three
91
+
92
+ ## The four channels a diagram draws with
93
+
94
+ | Channel | Carries | Values |
95
+ | --- | --- | --- |
96
+ | shape | `kind` | `{{destination}}`, `[[notes]]`, `{decision}`, `([research])`, `[work]` |
97
+ | fill | `status` | fog `#f6f8fa`, open `#ddf4ff`, claimed `#fff8c5`, resolved `#dafbe1`, out-of-scope `#ffebe9` |
98
+ | stroke | the frontier and `hitl` | see the precedence below |
99
+ | label text | id, kind, status, and the node's `label` | `#14 decision - open<br/>Cache the token?` |
100
+
101
+ The label repeats the id, the kind, and the status as plain text. A renderer
102
+ that drops the styling still leaves every fact readable.
103
+
104
+ `cli/src/lib/diagram.ts` implements this table. Read it when the two disagree.
105
+
106
+ ### The root derives its state from its subtree
107
+
108
+ The root is the map's one node with no `answers`. It carries no status of its
109
+ own. Every node below it decides what it draws.
110
+
111
+ - **Its label carries a count where another node carries a status**, in the
112
+ form `#229 destination - 3 of 12 open<br/>Ship the editor`
113
+ - **The count reads unresolved nodes out of every node below the root**
114
+ - **Its fill turns resolved green once every other node settles**, and open
115
+ blue until then
116
+ - **It draws no mode cue** (see Mark the exception below)
117
+
118
+ ### The stroke is a precedence, not a conflict
119
+
120
+ Two cues want the same stroke, so they rank:
121
+
122
+ | Node | Stroke |
123
+ | --- | --- |
124
+ | on the frontier | `stroke:#bf3989` at `stroke-width:5px` |
125
+ | unresolved `hitl`, not on the frontier | `stroke:#1f2328` at `stroke-width:2px`, dashed `6 4` |
126
+ | everything else | its own fill, so no border shows |
127
+
128
+ A node that is both on the frontier and unresolved `hitl` draws magenta, 5px,
129
+ **and** dashed. The frontier wins `stroke` and `stroke-width`, and the dash
130
+ survives because nothing else claims it.
131
+
132
+ Keep that combination visible. A node ready to work that also needs a human is
133
+ the most important node on the map.
134
+
135
+ ### Mark the exception, never the common case
136
+
137
+ A cue that lands on nine nodes out of eleven carries no information.
138
+
139
+ - **The border marks `hitl`, never `afk`**, since a human answering is the
140
+ scarce case
141
+ - **A resolved or out-of-scope node draws no mode cue**, because mode says who
142
+ will settle a node and nobody will settle a settled one
143
+ - **A map with no unresolved `hitl` node draws no mode cue and no mode legend
144
+ item**
145
+ - **The root draws no mode cue**, because nobody answers a destination
146
+
147
+ ### Edges point forward in time
148
+
149
+ | Edge | Means | Direction |
150
+ | --- | --- | --- |
151
+ | solid arrow | `answers` | from the node that surfaced the question, to the question |
152
+ | dotted arrow, `stroke-dasharray:10 8` | `blocked-by` | from the blocker, to the node that waits |
153
+
154
+ Both arrows read the same way: the source comes first in time, the target comes
155
+ second. Nothing on a map diagram ever points backwards.
156
+
157
+ Mermaid's own `-.->` dash reads as a solid line at a glance, so a `linkStyle`
158
+ line widens every blocking edge.
159
+
160
+ ## What the generated diagram leaves out
161
+
162
+ - **A resolved subtree collapses to one node** carrying a `+N more` count, since
163
+ a diagram past roughly nine nodes stops being readable
164
+ - **A subtree counts as resolved only when the node and every descendant carry
165
+ `resolved`**, so one open leaf keeps its whole chain drawn
166
+ - **The start node never collapses**, however settled it is
167
+ - **An `out-of-scope` node draws in full**, with its own fill and never
168
+ collapsed, because the map has to keep pointing at what it ruled out
169
+ - **`--from <id>` draws that node and its descendants only**, which is how a
170
+ parent gets its subtree instead of the whole map
171
+ - **The waiting node's label names any blocker outside the drawn subtree**,
172
+ since there is no box to draw the arrow against
173
+
174
+ ## Every unexplained cue earns a legend
175
+
176
+ A diagram carries a legend fence when it uses a cue whose meaning the diagram
177
+ does not spell out.
178
+
179
+ - **A generated diagram always carries one**, because shape, fill, and stroke
180
+ each mean something a reader cannot guess
181
+ - **A `quadrantChart` carries none**, since it labels its own axes
182
+ - **A hand-drawn flowchart that fills its nodes by status carries one**
183
+
184
+ The legend is a second mermaid fence directly below the diagram it explains, and
185
+ never a subgraph inside that diagram.
186
+
187
+ - **Its own fence**, so it is a separate layout and can collide with nothing
188
+ - **`flowchart LR`**, with the whole legend wrapped in `subgraph Legend["Legend"]`
189
+ and `direction LR`
190
+ - **Items chained by invisible `~~~` links**, which is what forces one row
191
+ - **Six boxes per row at most, and three rows at most**, counting boxes rather
192
+ than items, since an edge item draws two
193
+ - **Rows balanced**, so the largest row holds at most one box more than the
194
+ smallest, and no row is ever left orphaned
195
+ - **An edge item never splits across a row break**
196
+ - **Only the cues that diagram actually uses**, so a map with no `research` node
197
+ draws no `research` shape
198
+ - The two mode items are the exception, and draw as a pair
199
+ - A diagram with any unresolved `hitl` node gets both `afk` and `hitl`
200
+ - **Every cue drawn, never described**, which is the whole reason it is a diagram
201
+ - **No hyperlink anywhere in the legend**, since its items point at nothing
202
+
203
+ Three terms get a plain-words gloss in parentheses, because the term alone
204
+ explains nothing:
205
+
206
+ - `afk (agent can work alone)`
207
+ - `hitl (a human must answer)`
208
+ - `frontier (ready to work on)`
209
+
210
+ Shape and fill items need no gloss, since `work` and `resolved` say what they
211
+ are.
212
+
213
+ A legend edge carries its meaning on the edge, never in its boxes:
214
+
215
+ ```
216
+ lg7["A"] -->|"A surfaced B"| lg8["B"]
217
+ lg9["C"] -.->|"C must finish before D"| lg10["D"]
218
+ ```
219
+
220
+ A reader takes the left box as the subject. Boxes reading `blocked-by` and
221
+ `waits on` therefore reverse the sentence, putting the waiting node first.
222
+
223
+ ## Rules a hand-drawn diagram must follow
224
+
225
+ - **Mermaid only**, in a fence marked `mermaid`, so GitHub renders it in place
226
+ - **Not every diagram is a flowchart**, and `quadrantChart`, `stateDiagram-v2`,
227
+ `erDiagram`, `sequenceDiagram`, `timeline`, and `mindmap` all render on GitHub
228
+ - **Render any type outside that list once before you rely on it**
229
+ - **Cap it at roughly nine nodes**, the same cap the generated diagram works to
230
+ - **Anchor every map node id you name**, exactly the way the renderer does
231
+ - **Write the anchor around the id alone**, never the whole label
232
+ - **Single-quote the `href`**, because the mermaid label is already
233
+ double-quoted
234
+
235
+ ```
236
+ n14{"<a href='https://github.com/o/r/issues/14'>#14</a> decision - open<br/>Cache the token?"}
237
+ ```
238
+
239
+ The mermaid `click` directive does not work here. GitHub renders at
240
+ `securityLevel: strict` and its Content Security Policy blocks the `frame-src`
241
+ that directive needs.
242
+
243
+ Escape a label with HTML entities, and never with a numeric or hash-bearing one:
244
+
245
+ - **Write `&amp;` first**, or the entities the others produce get escaped twice
246
+ - **Then `&lt;`, `&gt;`, and `&quot;`**
247
+ - **Never `&#39;` or `#quot;`**, since one of them kills the whole fence
248
+ - Mermaid rewrites anything matching `/#\w+;/` into a private sentinel above
249
+ codepoint 255
250
+ - It then calls `btoa` on that sentinel, which throws
251
+ - **`&apos;` is safe**
252
+
253
+ The files backend emits plain ids, since a node on disk has no URL.
254
+
255
+ ## When a diagram is redrawn
256
+
257
+ | Moment | What changes |
258
+ | --- | --- |
259
+ | a node gains its first child | the generated subtree takes slot 1, your visual moves to slot 2 |
260
+ | a `fog` node graduates | one update rewrites the title, the body, and the diagram together |
261
+ | a node resolves | you redraw the hand-drawn visual to mark the path you chose |
262
+ | any descendant changes status | every ancestor's generated subtree is stale |
263
+
264
+ - **A graduating fog node rewrites its diagram**, since the old one drew the
265
+ region you could not yet state a question about
266
+ - **Mark a chosen path by renaming the item**, since every diagram type carries
267
+ labels and only some carry classes
268
+ - **Mark the hand-drawn visual, never the generated block**
269
+ - The renderer knows nothing about a chosen path
270
+ - Its next run overwrites any mark you leave inside the markers
271
+ - **Resolving a `decision` node is two writes**: the hand-drawn diagram, then
272
+ the answer appended after it
273
+ - **Skip the write when the rendered block matches the body byte for byte**, so
274
+ a quiet round costs no API call
275
+
276
+ ## Regenerating a parent's diagram
277
+
278
+ A parent's diagram draws its subtree, so any status change below it leaves
279
+ that diagram wrong. A parent is any node another node's `answers` names, and
280
+ the root is always one.
281
+
282
+ Regenerate at two moments: on the resolved node's parent after every
283
+ resolution, and on every parent at each frontier recompute. The second sweep
284
+ catches a claim or a release, which changes a node's fill without resolving
285
+ anything.
286
+
287
+ ```bash
288
+ # files backend
289
+ ~/.claude/acstack/bin/acstack node diagram --map <name> --from <parent id>
290
+
291
+ # github backend – one list call feeds every parent's render
292
+ gh issue list --label "map:<name>" --state all --limit 500 \
293
+ --json number,title,body,state,stateReason,labels,url > /tmp/<name>.json
294
+ ~/.claude/acstack/bin/acstack node diagram --stdin --from <parent id> < /tmp/<name>.json
295
+ ```
296
+
297
+ The renderer prints the two markers itself, so its output is the whole block.
298
+ Replace the parent's existing block, markers included, and leave every word
299
+ around it untouched. Skip any marker inside a fenced code block – that pair is
300
+ an example, and Where visuals sit in the body gives the rule.
301
+
302
+ - **Skip the write when the rendered block matches the body's block byte for
303
+ byte**, so a quiet round costs no write at all
304
+ - **Never write the output between the existing markers**, since that nests one
305
+ pair inside another and leaves two top-level pairs behind
306
+ - **Render every parent from one `list` call**, since the renderer reads
307
+ nothing but the JSON you hand it
308
+ - The rest of this file holds the markers, the body template, and the cue
309
+ vocabulary every diagram draws with
310
+
311
+ ## A worked body
312
+
313
+ A `decision` node that has already surfaced one child, so the generated subtree
314
+ holds slot 1 and the hand-drawn option space holds slot 2.
315
+
316
+ ````markdown
317
+ map: demo · root: #12 · answers: #12 · label: Cache the token?
318
+
319
+ Should the client hold the auth token between requests, or fetch a new one each
320
+ time?
321
+
322
+ <!-- acstack:diagram -->
323
+
324
+ ```mermaid
325
+ flowchart TD
326
+ n14{"<a href='https://github.com/o/r/issues/14'>#14</a> decision - open<br/>Cache the token?"}
327
+ n17["<a href='https://github.com/o/r/issues/17'>#17</a> work - open<br/>Token store"]
328
+ n14 --> n17
329
+ n14 -.-> n17
330
+ linkStyle 1 stroke-dasharray:10 8
331
+ classDef open fill:#ddf4ff,color:#1f2328,stroke:#ddf4ff
332
+ classDef openFrontHitl fill:#ddf4ff,color:#1f2328,stroke:#bf3989,stroke-width:5px,stroke-dasharray:6 4
333
+ class n17 open
334
+ class n14 openFrontHitl
335
+ ```
336
+
337
+ ```mermaid
338
+ flowchart LR
339
+ subgraph Legend["Legend"]
340
+ direction LR
341
+ lg1{"decision"}
342
+ lg2["work"]
343
+ lg3["open"]
344
+ lg4["afk (agent can work alone)"]
345
+ lg5["hitl (a human must answer)"]
346
+ lg1 ~~~ lg2 ~~~ lg3 ~~~ lg4 ~~~ lg5
347
+ lg6["frontier (ready to work on)"]
348
+ lg7["A"] -->|"A surfaced B"| lg8["B"]
349
+ lg9["C"] -.->|"C must finish before D"| lg10["D"]
350
+ lg6 ~~~ lg7
351
+ lg8 ~~~ lg9
352
+ end
353
+ linkStyle 5 stroke-dasharray:10 8
354
+ classDef legendShape fill:#ffffff,color:#1f2328,stroke:#d0d7de
355
+ classDef open fill:#ddf4ff,color:#1f2328,stroke:#ddf4ff
356
+ classDef legendHitl fill:#ffffff,color:#1f2328,stroke:#1f2328,stroke-width:2px,stroke-dasharray:6 4
357
+ classDef legendFront fill:#ffffff,color:#1f2328,stroke:#bf3989,stroke-width:5px
358
+ class lg1,lg2,lg4,lg7,lg8,lg9,lg10 legendShape
359
+ class lg3 open
360
+ class lg5 legendHitl
361
+ class lg6 legendFront
362
+ ```
363
+
364
+ <!-- /acstack:diagram -->
365
+
366
+ ```mermaid
367
+ quadrantChart
368
+ title The option space
369
+ x-axis "Slow to build" --> "Fast to build"
370
+ y-axis "Weak on revocation" --> "Strong on revocation"
371
+ quadrant-1 "Take it"
372
+ quadrant-2 "Worth the cost"
373
+ quadrant-3 "Reject"
374
+ quadrant-4 "Tempting, and wrong"
375
+ "Fetch every request": [0.85, 0.90]
376
+ "Cache to expiry": [0.75, 0.25]
377
+ "Cache with a revocation check": [0.30, 0.85]
378
+ ```
379
+
380
+ ## Answer
381
+
382
+ ...
383
+ ````
384
+
385
+ The generated block came out of the renderer, so its shape is exact. Node #14
386
+ sits on the frontier and is `hitl`, which is why its outline is magenta at 5px
387
+ **and** dashed.