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,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.