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.
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +389 -2
- package/THIRD_PARTY_NOTICES +14 -0
- package/TROUBLESHOOTING.md +225 -0
- package/agents/codex/frontend-verifier.toml +352 -0
- package/agents/codex/task-checker.toml +345 -0
- package/agents/codex/task-executor.toml +157 -0
- package/agents/codex/test-writer.toml +149 -0
- package/agents/frontend-verifier.md +351 -0
- package/agents/task-checker.md +344 -0
- package/agents/task-executor.md +156 -0
- package/agents/test-writer.md +148 -0
- package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
- package/assets/playwriter-bridge/doctor.ps1 +450 -0
- package/assets/playwriter-bridge/launcher-src.cs +131 -0
- package/assets/playwriter-bridge/opener.js +330 -0
- package/assets/playwriter-bridge/opener.ps1 +63 -0
- package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
- package/assets/playwriter-bridge/relay.ps1 +50 -0
- package/assets/playwriter-bridge/stop.ps1 +125 -0
- package/assets/playwriter-bridge/sync.ps1 +271 -0
- package/assets/playwriter-bridge/tunnel.ps1 +325 -0
- package/assets/playwriter-bridge/vm-free-port.sh +166 -0
- package/assets/terminal-workspace/client-keybindings.md +87 -0
- package/assets/terminal-workspace/config.example.yaml +329 -0
- package/assets/terminal-workspace/setup.sh +4317 -0
- package/cli/README.md +47 -0
- package/cli/bin/acstack.js +3 -0
- package/cli/dist/index.js +25984 -0
- package/cli/dist/index.js.map +7 -0
- package/cli/package.json +59 -0
- package/docs/adr/0001-ste-writing-standard.md +16 -0
- package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
- package/docs/adr/0003-handoff-ack-command.md +19 -0
- package/docs/adr/0004-dev-setup-per-host.md +13 -0
- package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
- package/docs/adr/0006-document-opener-service.md +89 -0
- package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
- package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
- package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
- package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
- package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
- package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
- package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
- package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
- package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
- package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
- package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
- package/docs/adr/index.md +17 -0
- package/docs/config-reference.md +572 -0
- package/docs/dev-setups.md +164 -0
- package/docs/migrate-0.8.md +28 -0
- package/docs/playwriter-bridge-windows.md +15 -0
- package/docs/terminal-workspace-keys.md +137 -0
- package/docs/terminal-workspace.md +1774 -0
- package/docs/workflows.md +309 -0
- package/hooks/ac-writing-style-reminder.sh +86 -0
- package/hooks/context-pressure.sh +583 -0
- package/hooks/hooks.json +50 -0
- package/hooks/session-start-handoff.sh +165 -0
- package/hooks/session-start.sh +214 -0
- package/orchestrator/AGENTS.md +532 -0
- package/output-styles/ac-writing-style.md +286 -0
- package/package.json +50 -4
- package/pi/agents/frontend-verifier.md +357 -0
- package/pi/agents/task-checker.md +348 -0
- package/pi/agents/task-executor.md +160 -0
- package/pi/agents/test-writer.md +154 -0
- package/pi/extension.ts +723 -0
- package/profiles/fullstack-python.yaml +29 -0
- package/profiles/node-typescript.yaml +23 -0
- package/profiles/python.yaml +24 -0
- package/skills/archive/SKILL.md +160 -0
- package/skills/bootstrap/SKILL.md +109 -0
- package/skills/bridge/HANDOFF.md +93 -0
- package/skills/bridge/SKILL-VM.md +292 -0
- package/skills/bridge/SKILL-WINDOWS.md +526 -0
- package/skills/bridge/SKILL.md +121 -0
- package/skills/browser-verify/SKILL.md +229 -0
- package/skills/code-review/SKILL.md +44 -0
- package/skills/commit/SKILL.md +106 -0
- package/skills/compact-and-continue/SKILL.md +241 -0
- package/skills/explore/SKILL.md +149 -0
- package/skills/grilling/SKILL.md +134 -0
- package/skills/handoff/SKILL.md +250 -0
- package/skills/handoff/launch-herdr.md +197 -0
- package/skills/handoff/launch-without-herdr.md +30 -0
- package/skills/handoff/lead-check.md +59 -0
- package/skills/handoff/outcomes.md +162 -0
- package/skills/handoff/running-agent.md +50 -0
- package/skills/handoff/watch.md +115 -0
- package/skills/host/SKILL.md +247 -0
- package/skills/host/detect-host.sh +297 -0
- package/skills/host/herdr.md +79 -0
- package/skills/host/install-herdr-block.sh +467 -0
- package/skills/host/install-orca.sh +512 -0
- package/skills/host/notes.md +97 -0
- package/skills/host/orca.md +160 -0
- package/skills/host/questions.md +163 -0
- package/skills/host/report.md +42 -0
- package/skills/implement/SKILL.md +230 -0
- package/skills/implement/decision-log.md +62 -0
- package/skills/implement/design-pass.md +32 -0
- package/skills/implement/relaxed-tdd.md +28 -0
- package/skills/map/SKILL.md +250 -0
- package/skills/map/frontend.md +227 -0
- package/skills/map/trackers/files.md +74 -0
- package/skills/map/trackers/github.md +180 -0
- package/skills/map/visuals.md +387 -0
- package/skills/new-worktree/SKILL.md +155 -0
- package/skills/new-worktree/detect-orchestrator.sh +328 -0
- package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
- package/skills/new-worktree/herdr.md +84 -0
- package/skills/new-worktree/orca.md +101 -0
- package/skills/pre-commit-review/SKILL.md +242 -0
- package/skills/pre-commit-review/auto-fix.md +19 -0
- package/skills/pre-commit-review/interactive.md +35 -0
- package/skills/quick-fix/SKILL.md +125 -0
- package/skills/record-context/SKILL.md +106 -0
- package/skills/setup/SKILL.md +250 -0
- package/skills/setup/browser.md +123 -0
- package/skills/setup/design-review.md +97 -0
- package/skills/setup/first-run.md +114 -0
- package/skills/setup/report.md +68 -0
- package/skills/setup/row-fixes.md +196 -0
- package/skills/setup/terminal-workspace.md +153 -0
- package/skills/sync/SKILL.md +171 -0
- package/skills/teardown-worktree/SKILL.md +245 -0
- package/skills/teardown-worktree/detector.md +39 -0
- package/skills/teardown-worktree/herdr.md +113 -0
- package/skills/teardown-worktree/orca.md +61 -0
- package/skills/terminal-workspace/SKILL.md +249 -0
- package/skills/terminal-workspace/components.md +36 -0
- package/skills/terminal-workspace/config-choices.md +69 -0
- package/skills/terminal-workspace/emulator-keys.md +48 -0
- package/skills/terminal-workspace/headless-clipboard.md +175 -0
- package/skills/terminal-workspace/turn-off-and-update.md +104 -0
- package/skills/test-conventions/SKILL.md +59 -0
- package/skills/visual-docs/SKILL.md +246 -0
- package/skills/writing/SKILL.md +231 -0
- package/skills/writing/examples.md +153 -0
- 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 `&` first**, or the entities the others produce get escaped twice
|
|
246
|
+
- **Then `<`, `>`, and `"`**
|
|
247
|
+
- **Never `'` 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
|
+
- **`'` 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.
|