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
package/cli/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acstack-cli",
|
|
3
|
+
"version": "0.47.0",
|
|
4
|
+
"description": "CLI for the ACStack agentic coding stack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "ac8318740"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ac8318740/spechub.git",
|
|
13
|
+
"directory": "cli"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"acstack": "./bin/acstack.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/index.js",
|
|
20
|
+
"bin/",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "npm run sync-version && node build.mjs",
|
|
29
|
+
"dev": "node build.mjs --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint src/",
|
|
32
|
+
"test": "vitest run --pool=vmThreads",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
34
|
+
"sync-version": "node scripts/sync-version.mjs"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^9.39.5",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"chalk": "^5.4.0",
|
|
40
|
+
"commander": "^14.0.0",
|
|
41
|
+
"esbuild": "^0.24.0",
|
|
42
|
+
"eslint": "^9.39.5",
|
|
43
|
+
"fast-glob": "^3.3.0",
|
|
44
|
+
"typescript": "^5.7.0",
|
|
45
|
+
"typescript-eslint": "^8.67.0",
|
|
46
|
+
"vitest": "^3.0.0",
|
|
47
|
+
"yaml": "^2.7.0",
|
|
48
|
+
"zod": "^3.24.0"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/ac8318740/spechub/tree/main/cli",
|
|
51
|
+
"keywords": [
|
|
52
|
+
"agentic-coding",
|
|
53
|
+
"acstack",
|
|
54
|
+
"planning",
|
|
55
|
+
"agent",
|
|
56
|
+
"claude",
|
|
57
|
+
"cli"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# One plain-language writing standard based on ASD-STE100
|
|
2
|
+
|
|
3
|
+
Every durable artifact an agent writes follows one standard. The standard lives in the `writing` skill and follows ASD-STE100 – Simplified Technical English, an aerospace standard for controlled technical writing. We adopt its rules: short sentences with a numeric cap, active voice, one instruction per sentence, one term for one meaning.
|
|
4
|
+
|
|
5
|
+
We reject the "add voice" advice common in anti-AI-writing guides. Someone with no context reads the artifacts weeks later. Uniformity serves that reader better than personality.
|
|
6
|
+
|
|
7
|
+
## Considered options
|
|
8
|
+
|
|
9
|
+
- Adopt the Cursor `unslop` skill as is – a removal checklist plus "add soul" (first person, varied rhythm, deliberate imperfection). Rejected: removal framing tells an agent what not to do, and personality is noise in specs, ADRs and map nodes.
|
|
10
|
+
- Keep the rules where they are today – restated in eight files in slightly different words. Rejected: they had already drifted, and no one could point at one file.
|
|
11
|
+
|
|
12
|
+
## Consequences
|
|
13
|
+
|
|
14
|
+
- The standard lives in one skill; every other skill points at it. Plugin prose must itself comply, so a sweep follows.
|
|
15
|
+
- A deterministic lint, `acstack lint-prose`, checks what a grep can check. It warns and never blocks, because sentence-length heuristics misfire in tables and code.
|
|
16
|
+
- The ASD-STE100 dictionary requires a license, so the plugin does not reproduce it; it ships its own short vocabulary table instead.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# A PR gate enforces the version bump; CI never bumps it
|
|
2
|
+
|
|
3
|
+
The Claude Code plugin cache is keyed by the version in
|
|
4
|
+
`.claude-plugin/plugin.json`, so a merge to `main` that does not change the
|
|
5
|
+
version is invisible to every installed copy. A pull request gate
|
|
6
|
+
(`scripts/version-gate.sh`, run by the `version-gate` workflow) fails the check
|
|
7
|
+
when a shipped path changed without a higher version, rather than CI bumping
|
|
8
|
+
the version itself. The author decides the semver level, because deriving it
|
|
9
|
+
from commit message prefixes is brittle and gets the level wrong exactly when
|
|
10
|
+
it matters.
|
|
11
|
+
|
|
12
|
+
## Considered options
|
|
13
|
+
|
|
14
|
+
- **Auto-bump on push to `main`.** CI raises the patch version whenever shipped
|
|
15
|
+
files land. Removes the failure, but a feature or a breaking change ships as
|
|
16
|
+
a patch, and CI has to write to `main`.
|
|
17
|
+
|
|
18
|
+
- **release-please.** Derives the level from Conventional Commit prefixes and
|
|
19
|
+
opens a release pull request. Correct only when every commit is labelled
|
|
20
|
+
correctly, which squashed merges and hand-written subjects do not guarantee.
|
|
21
|
+
|
|
22
|
+
- **A pull request gate.** CI refuses the merge; a human sets the version.
|
|
23
|
+
Chosen.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
- `main` needs branch protection with `version-gate` as a required status
|
|
28
|
+
check. Without it the gate is advisory.
|
|
29
|
+
|
|
30
|
+
- A shipped change that genuinely should not roll out needs the `no-bump` label
|
|
31
|
+
on the pull request. A version that goes down fails even with the label.
|
|
32
|
+
|
|
33
|
+
- Documentation-only paths are exempt, listed as `INERT_PATHS` in the script
|
|
34
|
+
and duplicated in CONTRIBUTING.md. The two lists have to be kept in sync by
|
|
35
|
+
hand.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Handoff acknowledgement is a CLI command, not a reply
|
|
2
|
+
|
|
3
|
+
/acstack:handoff hands work to another agent and asks it to reply ACCEPT or DECLINE. The sender's watcher, `acstack handoff watch` (`cli/src/lib/ackwatch.ts`), grepped the receiver's transcript for that keyword, a convention an agent can drift from.
|
|
4
|
+
|
|
5
|
+
The receiver now acknowledges by running `acstack handoff ack accept|decline --file <handoff-file> "<reason>"`. That command writes a sidecar `<handoff-file>.ack` file, which the watcher polls instead of the transcript.
|
|
6
|
+
|
|
7
|
+
## Considered options
|
|
8
|
+
|
|
9
|
+
- Wording only – tighten the ACCEPT/DECLINE prompt further. Rejected: it is still a convention, and the model can still drift from it.
|
|
10
|
+
- Lenient transcript matching – match more patterns in the transcript. Rejected: it keeps the same drift risk.
|
|
11
|
+
- A PreToolUse hook that blocks work until the sidecar exists – the only hard guarantee. Rejected for now: scoping the hook to handoff-launched sessions is separate work. Revisit if this failure recurs.
|
|
12
|
+
|
|
13
|
+
## Consequences
|
|
14
|
+
|
|
15
|
+
- The receiver can still read the handoff file first. It must run the ack command before any other tool use.
|
|
16
|
+
- The watcher gains an **engaged** outcome. Engaged means no acknowledgement, but a tool call after the anchor read the handoff file or used Agent, Edit, Write, or Bash.
|
|
17
|
+
- The sender sends one nudge after `ack_turns` turns, then restarts the watcher.
|
|
18
|
+
- A final engaged outcome is reported as "proceeding, unacknowledged." The sender never relaunches the work elsewhere. Two agents on the same files is the one failure file ownership exists to prevent.
|
|
19
|
+
- The transcript keyword match stays as a fallback, labelled `via: text`. It never writes the sidecar.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Dev setup is declared per host, not per project
|
|
2
|
+
|
|
3
|
+
A dev setup – the machine-level tools an ACStack session runs inside, such as which agent orchestrator hosts the terminal panes and git worktrees and which browser-verification modes work – is recorded in the ACStack CLI's global config (`~/.config/acstack/config.json`, keys under `host.*`), not in the project's `acstack/project.yaml`. The same repository is opened on several machines with different setups, so a per-project value would have to be wrong on all but one of them.
|
|
4
|
+
|
|
5
|
+
## Considered options
|
|
6
|
+
|
|
7
|
+
- `acstack/project.yaml`, like `frontend.browser.mode` – shareable and checked in, but forces one answer across every machine that opens the repo.
|
|
8
|
+
- Host-level global config, with project.yaml keeping only a preference where one makes sense – chosen.
|
|
9
|
+
- Both layers, with the project able to constrain the host – deferred; add only if a real conflict appears.
|
|
10
|
+
|
|
11
|
+
## Consequences
|
|
12
|
+
|
|
13
|
+
Skills that change behaviour on the dev setup read the global config first and then check the runtime environment to see what is actually active. The project's `frontend.browser.mode` becomes a preference resolved against what the host declares available. Host setup cannot be committed with the repo, so a fresh machine needs `/acstack:host` before worktree and browser skills behave fully.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Two orchestrators coexist on one host, and a checkout's path names its owner
|
|
2
|
+
|
|
3
|
+
An orchestrator is a tool that hosts agent terminal panes and git worktrees. Both herdr and Orca are orchestrators, and one host – a single developer machine – may run both at once.
|
|
4
|
+
|
|
5
|
+
The host config declares each one as installed or not, at `host.orchestrators.herdr` and `host.orchestrators.orca`. A developer with both installed should not have to pick one, and the session environment already says which tool hosts a session.
|
|
6
|
+
|
|
7
|
+
A session lives in the host that launched it and never moves. To continue work from the other host, open a new Claude session in the same checkout – one git worktree directory – from that host. ACStack's `/acstack:handoff` stays what it is, a handoff of a full context window.
|
|
8
|
+
|
|
9
|
+
A checkout's path root names its owner. Orca owns `~/orca/workspaces/<repo>/`. Everything under herdr's worktree root, `~/.herdr/worktrees/<repo>/` by default, is herdr's.
|
|
10
|
+
|
|
11
|
+
Any other path belongs to plain git. The session's host creates a checkout, and the owner removes it. The worktree skills check `orca worktree ps --json` and `herdr worktree list` before every removal, because neither tool sees the other's sessions.
|
|
12
|
+
|
|
13
|
+
Orca lists herdr checkouts, and the paired phone lists them, once the repo's "show in worktree list" switch is on in the desktop app. No CLI sets it, so `/acstack:host` tells the user to.
|
|
14
|
+
|
|
15
|
+
## Considered options
|
|
16
|
+
|
|
17
|
+
- One declared orchestrator per host, the earlier `host.orchestrator` enum. Rejected: it forces a choice the developer does not want to make.
|
|
18
|
+
- A feature that moves a running session to the other host. Rejected: neither tool offers one, and a new session in the same checkout reaches the same place.
|
|
19
|
+
- Either tool removes any checkout. Rejected on pilot evidence from 2026-08-23: Orca's `rm` refuses a path outside its own root, and herdr's `remove` deletes any directory and keeps the branch.
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
|
|
23
|
+
The `teardown-worktree` skill picks the removal command from the checkout's path root, not from the session's host. It deletes the branch itself for a herdr checkout.
|
|
24
|
+
|
|
25
|
+
The `new-worktree` skill never passes a path that crosses roots. A config that still sets the old `host.orchestrator` key fails `config check`, and `/acstack:host` re-declares both booleans.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# A document opener service on the laptop, separate from the Playwriter bridge
|
|
2
|
+
|
|
3
|
+
`acstack-open` sends a rendered page to the opener, a small HTTP service on
|
|
4
|
+
the user's laptop. The opener stores the page and serves it back on an
|
|
5
|
+
address only the laptop can reach (the loopback interface). It then hands
|
|
6
|
+
that address to the user's default browser.
|
|
7
|
+
|
|
8
|
+
The Playwriter bridge stays as it is, and it no longer shows a document to a
|
|
9
|
+
person.
|
|
10
|
+
|
|
11
|
+
A page renders on a development virtual machine, which has no browser of its
|
|
12
|
+
own. The virtual machine already reaches the laptop through the bridge.
|
|
13
|
+
|
|
14
|
+
The bridge is a Chrome extension plus a relay, meaning a small process on the
|
|
15
|
+
laptop that the extension connects to. It lets an agent drive a real browser
|
|
16
|
+
over the Chrome DevTools Protocol (CDP). Reusing it to show a page to a person
|
|
17
|
+
looked like the cheaper answer.
|
|
18
|
+
|
|
19
|
+
The two do not collapse into one. The bridge attaches one tab at a time, and
|
|
20
|
+
only after someone clicks the extension icon. That click **arms** the tab.
|
|
21
|
+
|
|
22
|
+
The bridge also drives a dedicated Chrome profile rather than the user's
|
|
23
|
+
default browser. So it cannot satisfy "open this in my normal browser" at all,
|
|
24
|
+
and every document through it costs an arming click first.
|
|
25
|
+
|
|
26
|
+
The bridge exists so an agent can *drive* a browser. The opener exists so a
|
|
27
|
+
person can *see* a page.
|
|
28
|
+
|
|
29
|
+
## Considered options
|
|
30
|
+
|
|
31
|
+
**Drive the existing bridge**. Rejected. Arming the bridge takes one click
|
|
32
|
+
per tab, inside a separate Chrome profile.
|
|
33
|
+
|
|
34
|
+
Rewriting a live page over CDP also detaches the extension outright. The
|
|
35
|
+
bridge then stays down until the user arms it again by hand.
|
|
36
|
+
|
|
37
|
+
**Forward a port and serve from the virtual machine**. Rejected. The user's
|
|
38
|
+
terminal would open an address that a forwarded port carries back to a server
|
|
39
|
+
on the virtual machine.
|
|
40
|
+
|
|
41
|
+
The reverse tunnel the bridge already maintains runs the wrong way, so this
|
|
42
|
+
needs a second forward. That forward comes from a `LocalForward` line in the
|
|
43
|
+
user's own SSH host block, and nothing in setup writes it. The page also dies
|
|
44
|
+
when the server on the virtual machine stops, which is the common case when a
|
|
45
|
+
session ends.
|
|
46
|
+
|
|
47
|
+
**Print a clickable hyperlink in the terminal**. Kept as the fallback, not as
|
|
48
|
+
the answer.
|
|
49
|
+
|
|
50
|
+
The terminal already prints one today. It costs a deliberate click for every
|
|
51
|
+
single document. The requirement was to read one document after another
|
|
52
|
+
without ceremony.
|
|
53
|
+
|
|
54
|
+
**Open the document in the editor instead of a browser**. Rejected.
|
|
55
|
+
|
|
56
|
+
The user is moving away from an editor-hosted terminal toward a plain one.
|
|
57
|
+
The user asked specifically for the default browser.
|
|
58
|
+
|
|
59
|
+
## Consequences
|
|
60
|
+
|
|
61
|
+
A second always-on service now runs on the laptop, and every virtual machine
|
|
62
|
+
gets one more tunnel task to carry it. This is deliberate.
|
|
63
|
+
|
|
64
|
+
Setup already registers the relay and its tunnels as self-healing scheduled
|
|
65
|
+
tasks. The opener inherits that machinery, its deployment, and its
|
|
66
|
+
restart-on-change behaviour rather than inventing any.
|
|
67
|
+
|
|
68
|
+
The opener gets its own tunnel rather than a second forward on the bridge's
|
|
69
|
+
connection. `tunnel.ps1` passes `ExitOnForwardFailure=yes`, so one wedged
|
|
70
|
+
port fails the whole `ssh` session. A stuck opener port would take the bridge
|
|
71
|
+
down with it.
|
|
72
|
+
|
|
73
|
+
The virtual machine can now ask the opener to restart the relay and to
|
|
74
|
+
restart the tunnels. Those are two recovery actions the virtual machine could
|
|
75
|
+
not perform before, and a human had to paste both into a shell. Arming stays
|
|
76
|
+
manual, because it is behaviour of a third-party extension and nothing on
|
|
77
|
+
either side can click it.
|
|
78
|
+
|
|
79
|
+
The opener stores rendered documents on the laptop. They outlive the session
|
|
80
|
+
that produced them. That is what makes a page still work after the virtual
|
|
81
|
+
machine goes away.
|
|
82
|
+
|
|
83
|
+
The opener prunes the store after a week.
|
|
84
|
+
|
|
85
|
+
The service accepts a document and puts it on the user's screen, so a shared
|
|
86
|
+
secret called the opener token guards it. Setup generates that token and
|
|
87
|
+
copies it to the virtual machine. Loopback binding alone is not enough,
|
|
88
|
+
because the reverse tunnel makes the port reachable by anything running on
|
|
89
|
+
the virtual machine.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# A session's own transcript decides lead or child, not the environment
|
|
2
|
+
|
|
3
|
+
The `handoff` and `compact-and-continue` skills run only in a **lead session**,
|
|
4
|
+
because both write state the lead alone owns. An agent decides which it is by
|
|
5
|
+
planting a random mark in its own transcript and then asking which transcript
|
|
6
|
+
holds it.
|
|
7
|
+
|
|
8
|
+
A hit under `~/.claude/projects/<project>/<session>/subagents/` means the agent
|
|
9
|
+
is a subagent or a teammate. No environment variable takes part in that
|
|
10
|
+
decision.
|
|
11
|
+
|
|
12
|
+
The first version read `CLAUDE_CODE_CHILD_SESSION` instead. Claude Code sets
|
|
13
|
+
that variable to `1` in the environment of every Bash tool subprocess, in every
|
|
14
|
+
session. So the check was true unconditionally, and both skills refused to run
|
|
15
|
+
everywhere (#146).
|
|
16
|
+
|
|
17
|
+
The variable marks "spawned by Claude Code", and nothing finer.
|
|
18
|
+
|
|
19
|
+
## Considered options
|
|
20
|
+
|
|
21
|
+
- **A different environment variable.** Measured on Claude Code 2.1.241, an
|
|
22
|
+
in-process subagent and an in-process teammate both report the lead's exact
|
|
23
|
+
`CLAUDE_CODE_SESSION_ID`, `CLAUDE_PID` and `CLAUDE_CODE_ENTRYPOINT`. A child
|
|
24
|
+
runs inside the lead's own process, so no variable separates them. Ruled out
|
|
25
|
+
by measurement, not by preference.
|
|
26
|
+
|
|
27
|
+
- **Drop the check and let a child run the skills.** Simplest, and wrong. A
|
|
28
|
+
child shares the lead's `CLAUDE_CODE_SESSION_ID`, so a child's quiet marker
|
|
29
|
+
silences a context-pressure nudge the lead still needs, and a child's
|
|
30
|
+
`acstack/HANDOFF.md` overwrites the lead's anchor.
|
|
31
|
+
|
|
32
|
+
- **Read `taskKind` from the agent's `.meta.json`.** The host does write one
|
|
33
|
+
beside each agent transcript, naming `in_process_teammate` for a teammate and
|
|
34
|
+
omitting `taskKind` for a plain subagent. An agent cannot tell which of those
|
|
35
|
+
files is its own, so the file answers a question nobody can ask.
|
|
36
|
+
|
|
37
|
+
- **Plant a mark, then find it.** Chosen. The mark is unique per agent, so the
|
|
38
|
+
answer is the agent's own and never a sibling's.
|
|
39
|
+
|
|
40
|
+
## Consequences
|
|
41
|
+
|
|
42
|
+
- The command polls for up to ten seconds rather than grepping once. The host
|
|
43
|
+
writes the record carrying a command while that command still runs, roughly
|
|
44
|
+
four seconds in.
|
|
45
|
+
|
|
46
|
+
A single grep therefore runs too early, finds nothing and answers
|
|
47
|
+
`lead` for everyone. A lead usually exits in about four seconds, as
|
|
48
|
+
soon as its own record lands.
|
|
49
|
+
|
|
50
|
+
- The check reads a Claude Code internal layout that no public contract
|
|
51
|
+
describes. A host that renames the `subagents/` directory, or files agent
|
|
52
|
+
transcripts elsewhere, turns every child into a `lead` verdict.
|
|
53
|
+
|
|
54
|
+
- Absence of evidence reads as `lead`. A host that persists no transcript
|
|
55
|
+
leaves the command empty-handed, and the skill proceeds. Failing closed is
|
|
56
|
+
what produced #146, so the failure now falls on the side that keeps the
|
|
57
|
+
skills runnable.
|
|
58
|
+
|
|
59
|
+
- The agent transcripts alone decide. An agent's own mark reaches
|
|
60
|
+
`<session>.jsonl` whether it leads or not. So the loop reads that file only
|
|
61
|
+
as an early exit for a lead, never as evidence of a child.
|
|
62
|
+
|
|
63
|
+
- The nonce placeholder stays angle-bracketed. `n=acstack-whoami-<nonce>` is a
|
|
64
|
+
bash syntax error, so an agent that pastes the block without substituting
|
|
65
|
+
fails loudly. A bare token would run instead. It would match every transcript
|
|
66
|
+
in the session that had already read the skill file, so the command would
|
|
67
|
+
answer `child` to a real lead.
|
|
68
|
+
|
|
69
|
+
- The agent still chooses the nonce, and a language model samples randomness
|
|
70
|
+
badly. Two agents in one session that pick the same eight characters read
|
|
71
|
+
each other's transcripts. Generating the nonce at runtime cannot fix that.
|
|
72
|
+
|
|
73
|
+
The mark has to sit in the command text for the command to find it. A
|
|
74
|
+
value computed at runtime reaches the transcript only after the command
|
|
75
|
+
ends.
|
|
76
|
+
|
|
77
|
+
- Both skills carry their own copy of the block, rather than calling a shared
|
|
78
|
+
script. `skills/new-worktree/detect-orchestrator.sh` shows the repo has that
|
|
79
|
+
pattern. No home exists for a script two skills share, though.
|
|
80
|
+
|
|
81
|
+
The repo also already repeats the context-pressure path expression in
|
|
82
|
+
three places. `tests/test-skill-gates.sh` asserts the two copies match
|
|
83
|
+
byte for byte. That assertion is what makes the duplication safe to keep.
|
|
84
|
+
|
|
85
|
+
- `tests/test-skill-gates.sh` extracts the command from each `SKILL.md` and
|
|
86
|
+
runs it against fixture transcript layouts. The fixtures encode the layout
|
|
87
|
+
above, so they need revisiting whenever the host changes it.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# The CLI audits, the skill interviews
|
|
2
|
+
|
|
3
|
+
`acstack config check` is the only health check in ACStack. It reports what is
|
|
4
|
+
wrong with a machine and a project. It changes nothing.
|
|
5
|
+
|
|
6
|
+
`/acstack:setup` reads that report, then asks the user what to do about each
|
|
7
|
+
failure. The skill writes the answers. Neither one does the other's job.
|
|
8
|
+
|
|
9
|
+
The check reads `~/.claude/settings.json` and the two project settings files,
|
|
10
|
+
even though Claude Code owns them and ACStack does not. A health check with a
|
|
11
|
+
row it cannot see is a health check nobody can trust.
|
|
12
|
+
|
|
13
|
+
## Considered options
|
|
14
|
+
|
|
15
|
+
ACStack had two commands named `check`.
|
|
16
|
+
|
|
17
|
+
The CLI's audited the host axes. The config skill's audited the project's
|
|
18
|
+
infrastructure in prose. Neither called the other, and a user could not tell
|
|
19
|
+
them apart by name.
|
|
20
|
+
|
|
21
|
+
Leaving the split in place keeps ACStack's CLI reading only ACStack's own
|
|
22
|
+
files. It also lets the two audits drift apart. It leaves the project half
|
|
23
|
+
untestable, because continuous integration cannot run prose in a skill body.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
The check grows a `--json` output, so the skill maps a failing row to the fix
|
|
28
|
+
it offers. That output is an interface. Renaming a row identifier breaks the
|
|
29
|
+
skill silently, so the row identifiers need tests.
|
|
30
|
+
|
|
31
|
+
Every audit rule now lives in TypeScript and gets tested. No skill re-probes
|
|
32
|
+
the machine with its own shell commands.
|
|
33
|
+
|
|
34
|
+
A user who wants one setting changed runs `acstack config set <key> <value>`
|
|
35
|
+
and answers no questions. The full key reference lives in
|
|
36
|
+
`docs/config-reference.md`, because seventeen keys carry more nuance than a
|
|
37
|
+
help string holds.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# The config writer verifies its result instead of predicting it
|
|
2
|
+
|
|
3
|
+
`acstack config set` writes a key into `acstack/project.yaml` by splicing the
|
|
4
|
+
new value over the old value's bytes, so a hand-edited file keeps its comments
|
|
5
|
+
and its spacing. Before writing, it parses what it built and compares that data
|
|
6
|
+
against what the YAML document interface would have produced. The two must
|
|
7
|
+
agree, or the write falls back to the document interface.
|
|
8
|
+
|
|
9
|
+
The writer verifies that fallback the same way. It parses the emitted text and
|
|
10
|
+
compares it against the same data. A write neither path can verify refuses with
|
|
11
|
+
exit 1 and leaves the file byte for byte as it was.
|
|
12
|
+
|
|
13
|
+
The comparison covers the whole file, not the key that changed.
|
|
14
|
+
|
|
15
|
+
## Considered options
|
|
16
|
+
|
|
17
|
+
The first version predicted safety with guards on the node it was replacing.
|
|
18
|
+
Three rounds of that shipped four ways to corrupt a user's file, each behind a
|
|
19
|
+
green success message and exit code 0.
|
|
20
|
+
|
|
21
|
+
A folded or literal block scalar's byte range runs past its trailing newline,
|
|
22
|
+
so the splice ate the line break and destroyed the following key. An existing
|
|
23
|
+
empty value has a zero-width range, so the write produced `test:npm test`.
|
|
24
|
+
|
|
25
|
+
A new value spanning lines landed at column zero. A scalar inside a flow
|
|
26
|
+
collection passed every guard, and a value holding a comma truncated the key
|
|
27
|
+
and invented a second one beside it.
|
|
28
|
+
|
|
29
|
+
Each guard was correct. Each author read the node and not the context around
|
|
30
|
+
it. A user would have found the fifth shape.
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
The writer costs one extra parse per write, which nobody running a command by
|
|
35
|
+
hand will notice.
|
|
36
|
+
|
|
37
|
+
A key-only comparison passes every test in the suite and every input a 2020
|
|
38
|
+
case sweep could reach. The writer compares the whole file anyway. Narrowing it
|
|
39
|
+
means arguing about shapes nobody has constructed yet, and that argument has
|
|
40
|
+
already been wrong four times.
|
|
41
|
+
|
|
42
|
+
Do not narrow it to the changed key on the grounds that no test covers the
|
|
43
|
+
difference. No test can.
|
|
44
|
+
|
|
45
|
+
The document interface was the trusted baseline until a fifth shape broke it. A
|
|
46
|
+
whitespace-only value over a block scalar with a sibling key after it emitted a
|
|
47
|
+
content line no parser reads back. The value returned as the empty string,
|
|
48
|
+
behind exit 0.
|
|
49
|
+
|
|
50
|
+
The splice refused that write correctly and handed it to a fallback nobody
|
|
51
|
+
checked. The writer checks both paths now.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# A labelled workflow fast-forwards main to dev, and nobody merges
|
|
2
|
+
|
|
3
|
+
ACStack promotes `dev` to `main` by fast-forward. Open a pull request with base
|
|
4
|
+
`main` and head `dev`. Review it, then add the `promote-to-main` label.
|
|
5
|
+
`.github/workflows/promote.yml` checks six preconditions and moves
|
|
6
|
+
`refs/heads/main` to the commit the pull request carries.
|
|
7
|
+
|
|
8
|
+
GitHub then closes the pull request as merged on its own. `main` reaches every
|
|
9
|
+
commit on `dev`, which is what GitHub reads as merged. The workflow pushes with
|
|
10
|
+
`GITHUB_TOKEN`, so no credential sits at rest in this repository.
|
|
11
|
+
|
|
12
|
+
GitHub offers no fast-forward merge. Its three options are a merge commit, a
|
|
13
|
+
squash, and a rebase. Each one writes a commit to `main` that `dev` can never
|
|
14
|
+
contain, and that commit forces a back-merge afterwards.
|
|
15
|
+
|
|
16
|
+
Commits `ee41411` and `04602c7` are the last two the old flow cost. The open
|
|
17
|
+
question was how to stop a person merging into `main`. `GITHUB_TOKEN` bypasses
|
|
18
|
+
nothing, which rules out the usual answer.
|
|
19
|
+
|
|
20
|
+
## Considered options
|
|
21
|
+
|
|
22
|
+
A GitHub App can hold a bypass on a ruleset, so an App blocks the Merge button
|
|
23
|
+
outright. We rejected it for two reasons. Its private key would sit in a
|
|
24
|
+
repository secret and need rotating by hand, and this repository has one person
|
|
25
|
+
in it.
|
|
26
|
+
|
|
27
|
+
We rejected a fine-grained personal access token for two other reasons. It
|
|
28
|
+
expires silently. Its bypass would also come from the owner's admin role, which
|
|
29
|
+
leaves the Merge button open to the very person the rule binds.
|
|
30
|
+
|
|
31
|
+
We chose `GITHUB_TOKEN`. Nothing sits in a secret, the token expires when the
|
|
32
|
+
job ends, and GitHub scopes it to this repository.
|
|
33
|
+
|
|
34
|
+
That choice rules out a bypass, so the block comes from somewhere else. It comes
|
|
35
|
+
from a required check that no ordinary pull request ever gets: `promotion-gate`.
|
|
36
|
+
Only `promote.yml` creates it.
|
|
37
|
+
|
|
38
|
+
A pull request into `main` without the label is missing that check and cannot
|
|
39
|
+
merge. `enforce_admins` extends that to admins. The workflow posts the check
|
|
40
|
+
green immediately before it moves the ref, then flips it to failed if the push
|
|
41
|
+
does not land.
|
|
42
|
+
|
|
43
|
+
A personal repository could not have taken the App route anyway. The rulesets
|
|
44
|
+
API rejects the GitHub Actions integration as a bypass actor. Classic branch
|
|
45
|
+
protection rejects a bypass list outright.
|
|
46
|
+
|
|
47
|
+
Both errors came from this repository, and `promote.yml` quotes both messages.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
`main` stays an ancestor of `dev` by construction. The next promotion therefore
|
|
52
|
+
fast-forwards too, and nothing is ever stranded on `main`.
|
|
53
|
+
|
|
54
|
+
Three things follow as rules rather than preferences. No merge commit, squash,
|
|
55
|
+
or rebase onto `main`. No cherry-pick onto `main`, since a new SHA makes the two
|
|
56
|
+
graphs diverge for good.
|
|
57
|
+
|
|
58
|
+
No back-merge of `main` into `dev` either. A fast-forward strands nothing there.
|
|
59
|
+
|
|
60
|
+
The Merge button works for about a second per promotion. That is the gap between
|
|
61
|
+
`promotion-gate` going green and the ref moving, inside a run an admin
|
|
62
|
+
triggered. Pressing it there writes a merge commit to `main`, and the
|
|
63
|
+
fast-forward then refuses and says so.
|
|
64
|
+
|
|
65
|
+
The gate step therefore sits as late in the job as it can. Every new
|
|
66
|
+
precondition belongs above it.
|
|
67
|
+
|
|
68
|
+
A push made with `GITHUB_TOKEN` starts no workflow runs. `tag-release.yml` would
|
|
69
|
+
never fire, and a promotion would tag nothing. `promote.yml` dispatches it
|
|
70
|
+
explicitly through `workflow_dispatch`. GitHub documents that event as always
|
|
71
|
+
creating a run.
|
|
72
|
+
|
|
73
|
+
Nothing enforces that coupling. Add any future workflow that triggers on a push
|
|
74
|
+
to `main` to the dispatch step by hand.
|
|
75
|
+
|
|
76
|
+
`tag-release.yml` no longer compares `HEAD` against `HEAD~1`. A fast-forward
|
|
77
|
+
carries every commit `dev` collected, so `HEAD~1` is the second-newest `dev`
|
|
78
|
+
commit rather than `main`'s previous tip.
|
|
79
|
+
|
|
80
|
+
Most of those commits leave the version alone, so the old comparison would read
|
|
81
|
+
"unchanged" and skip the tag. The workflow now tags whatever version `main`
|
|
82
|
+
holds, unless that tag already exists.
|
|
83
|
+
|
|
84
|
+
The label that starts a promotion used to sabotage it. `version-gate.yml`
|
|
85
|
+
triggers on `labeled` and `unlabeled`, so adding `promote-to-main` started a
|
|
86
|
+
fresh `version-gate` run on the same commit.
|
|
87
|
+
|
|
88
|
+
That run is pending while the promotion reads the checks and pushes, and branch
|
|
89
|
+
protection refuses a push whose required check is pending. Removing and re-adding
|
|
90
|
+
the label fired the gate twice more, so the advice for a stuck promotion made it
|
|
91
|
+
permanent.
|
|
92
|
+
|
|
93
|
+
`version-gate.yml` now skips every label event except `no-bump`. That is the one
|
|
94
|
+
label which changes its verdict. A skipped job reports as passing, so the earlier
|
|
95
|
+
green run still stands.
|
|
96
|
+
|
|
97
|
+
`promote.yml` also waits up to ten minutes on a pending or absent check, rather
|
|
98
|
+
than refusing it.
|
|
99
|
+
|
|
100
|
+
The close step looks the gate up by name on the commit, not by the id the open
|
|
101
|
+
step captured. A `POST` can land while `gh` still exits non-zero on a dropped
|
|
102
|
+
response read. Trusting that id would leave the gate green and the pull request
|
|
103
|
+
mergeable by hand.
|
|
104
|
+
|
|
105
|
+
One hole stays open. A runner killed between the gate opening and the ref moving
|
|
106
|
+
leaves `promotion-gate` green with no job left to close it. Close the pull
|
|
107
|
+
request, or set that check run to `failure` by hand.
|
|
108
|
+
|
|
109
|
+
An admin can still turn branch protection off in the repository settings. No
|
|
110
|
+
configuration prevents that on any plan. This decision makes merging into `main`
|
|
111
|
+
impossible, not repository administration.
|
|
112
|
+
|
|
113
|
+
`promote.yml` names five CI checks by their `name:` values in `ci.yml`,
|
|
114
|
+
`tests.yml`, and `version-gate.yml`. Rename a job in one place without the
|
|
115
|
+
other, and every promotion blocks. The workflow says so when it happens.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# The node body header is the authoritative edge encoding on GitHub
|
|
2
|
+
|
|
3
|
+
Every map node on the GitHub tracker opens its body with one header line. It names the node's map, its root, its `answers` parent, its `blocked-by` ids, and its short `label`. Every composed query reads its edges from that line.
|
|
4
|
+
|
|
5
|
+
The tracker still writes GitHub's native sub-issue and dependency links. Those links drive GitHub's own interface, and no query reads them.
|
|
6
|
+
|
|
7
|
+
The header costs one `gh issue list` call to read the whole graph, because `body` is one of the fields that command returns.
|
|
8
|
+
|
|
9
|
+
## Considered options
|
|
10
|
+
|
|
11
|
+
- Read the edges from GitHub's native links. Rejected: the parent needs a GraphQL query and the blockers need a REST call, both per issue. A forty-node map would spend eighty calls on every frontier recompute.
|
|
12
|
+
- Drop the native links and keep only the header. Rejected: the links render a map inside GitHub's own issue interface. That is half the reason to pick this tracker.
|
|
13
|
+
- Give the files backend the same header. Rejected: its frontmatter already holds `answers` and `blocked-by`, and its directory name is the map. A header there would copy data the CLI owns, free to drift from it.
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- One edge lives in two places on GitHub. The tracker doc states the dual write as a single step, for adding an edge and for removing one. A write that updates one copy alone leaves the two out of step.
|
|
18
|
+
- A degraded remote, meaning one where GitHub's link features are off, now loses only the link interface. Every query keeps working. The tracker needs no second encoding and no per-map choice between them.
|
|
19
|
+
- A reader landing on an issue from a notification sees which map it belongs to and what sits above it. The issue title alone never told them.
|
|
20
|
+
- The two backends no longer share one body shape. A header line on GitHub, frontmatter on files.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# A map node's kind is a closed set of five
|
|
2
|
+
|
|
3
|
+
A map node carries one `kind`. It is one of `destination`, `notes`, `decision`, `research`, or `work`.
|
|
4
|
+
|
|
5
|
+
The CLI enforces the set with a zod enum. It rejects anything else at create, at update, and on reading a stored file. Before this, `kind` was free text that the CLI called advisory only.
|
|
6
|
+
|
|
7
|
+
The five split in two. `destination` and `notes` are records rather than questions. `decision`, `research`, and `work` are things still to settle.
|
|
8
|
+
|
|
9
|
+
The two records resolve differently. `notes` resolves the moment someone writes it. `destination` is the root, and it carries no status of its own.
|
|
10
|
+
|
|
11
|
+
The root's state follows its subtree. It stays open while any node below it still needs work. `archive` resolves it when the map clears.
|
|
12
|
+
|
|
13
|
+
Each has one test.
|
|
14
|
+
|
|
15
|
+
- `destination` is the root, one per map
|
|
16
|
+
- `notes` holds no question, and it binds every other node
|
|
17
|
+
- `decision` has two or more defensible answers
|
|
18
|
+
- `research` has one findable answer
|
|
19
|
+
- `work` ends in a diff
|
|
20
|
+
|
|
21
|
+
## Considered options
|
|
22
|
+
|
|
23
|
+
- Keep `kind` free text and let the generated legend list only the kinds a map happens to contain. Rejected: `kind` now picks a node's shape in the map diagram, and an unknown value has no drawing.
|
|
24
|
+
- Fix a set of six, to leave one slot spare. Rejected: every candidate sixth landed inside an existing kind. A bug is `work`, a spike is `research`, and a glossary term is a `decision` about what a word means.
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
- Five kinds map to five distinct mermaid shapes, so shape carries kind and the other channels are free for status, mode, and the frontier.
|
|
29
|
+
- Every node must name a kind, and nothing can clear it. A node with no kind has no shape, so the field cannot be optional.
|
|
30
|
+
- The enum rejects any map still carrying an older kind such as `grilling` or `task`. That is acceptable, because nodes are transient working state a map throws away at archive.
|
|
31
|
+
- `kind` and `mode` will agree most of the time, and the disagreements are the point. A `decision` node marked `afk` draws as a dashed rhombus, which is an agent about to settle something that was the human's.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The diagram renderer takes nodes in and never calls gh
|
|
2
|
+
|
|
3
|
+
`acstack node diagram` reads nodes and writes mermaid. It reads them from disk for a named map, or as JSON on stdin. It makes no network call and needs no authentication.
|
|
4
|
+
|
|
5
|
+
On the GitHub backend the agent runs the pipe itself:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gh issue list --label "map:<name>" --state all --limit 500 \
|
|
9
|
+
--json number,title,body,state,stateReason,labels,url \
|
|
10
|
+
| acstack node diagram --stdin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Considered options
|
|
14
|
+
|
|
15
|
+
- Have the CLI run `gh issue list` itself behind a `--tracker github` flag. Rejected: the invocation would then live in both `cli/src/` and `skills/map/trackers/github.md`, and this repository's own rule is that one fact gets one home. It is also nicer for the agent, which is the whole case for it.
|
|
16
|
+
- Give the CLI a real GitHub backend that speaks to the API. Rejected: far larger than this effort needs, and it would put authentication inside a renderer.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
- The renderer's tests are JSON fixtures. No subprocess, no network, no token, and 59 tests run in the same suite as everything else.
|
|
21
|
+
- Both backends reach one rendering path. One line branches on the backend, and the specification demands it: a node id wraps in an anchor when the node carries a URL.
|
|
22
|
+
- The agent runs two commands instead of one. `skills/map/trackers/github.md` states the pipe verbatim, so nobody composes it from memory.
|
|
23
|
+
- The CLI gained `readStdin`, because `readFileSync(0, 'utf-8')` fails with EAGAIN when a parent process hands over a non-blocking fd 0. An agent harness does exactly that.
|