jackherd 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +38 -0
  4. package/README.md +1122 -3
  5. package/assets/branding/README.md +62 -0
  6. package/assets/branding/jackherd-icon-256.png +0 -0
  7. package/assets/branding/jackherd-icon-512.png +0 -0
  8. package/assets/branding/jackherd-icon-64.png +0 -0
  9. package/assets/branding/jackherd-icon-small-32.png +0 -0
  10. package/assets/branding/jackherd-icon-small-64.png +0 -0
  11. package/assets/branding/jackherd-icon-small.svg +1 -0
  12. package/assets/branding/jackherd-icon.svg +1 -0
  13. package/assets/branding/jackherd-logo-1024.png +0 -0
  14. package/assets/branding/jackherd-logo-512.png +0 -0
  15. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  16. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  17. package/assets/branding/jackherd-logo-dark.svg +1 -0
  18. package/assets/branding/jackherd-logo-mark-1024.png +0 -0
  19. package/assets/branding/jackherd-logo-mark-512.png +0 -0
  20. package/assets/branding/jackherd-logo-mark.svg +1 -0
  21. package/assets/branding/jackherd-logo.svg +1 -0
  22. package/dist/chunk-R6MGHCD7.js +93 -0
  23. package/dist/config.d.ts +167 -0
  24. package/dist/config.js +401 -0
  25. package/dist/plan-format.d.ts +325 -0
  26. package/dist/plan-format.js +636 -0
  27. package/docs/lanes.mmd +9 -0
  28. package/docs/lanes.svg +1 -0
  29. package/docs/skills.mmd +10 -0
  30. package/docs/skills.svg +1 -0
  31. package/docs/workflow.svg +172 -0
  32. package/herdr-plugin/herdr-plugin.toml +22 -0
  33. package/herdr-plugin/open-plans.ts +43 -0
  34. package/herdr-plugin/restore-plans.ts +107 -0
  35. package/install.sh +157 -0
  36. package/kit/hooks/hooks/git-guard/decision.ts +438 -0
  37. package/kit/hooks/hooks/git-guard/index.ts +38 -0
  38. package/kit/hooks/hooks/git-guard/shell.ts +407 -0
  39. package/kit/hooks/hooks/watch-guard/background.ts +36 -0
  40. package/kit/hooks/hooks/watch-guard/decision.ts +53 -0
  41. package/kit/hooks/hooks/watch-guard/index.ts +11 -0
  42. package/kit/hooks/hooks/watch-guard/shell-input.ts +32 -0
  43. package/kit/hooks/hooks/watch-guard/starts.ts +154 -0
  44. package/kit/hooks/hooks/watch-guard/text-fallback.ts +40 -0
  45. package/kit/hooks/hooks.json +30 -0
  46. package/kit/hooks/statusline.sh +72 -0
  47. package/kit/review/agents/devops.md +11 -0
  48. package/kit/review/agents/public-content.md +39 -0
  49. package/kit/review/agents/security-audit.md +11 -0
  50. package/kit/review/agents/uncle-bob.md +11 -0
  51. package/kit/review/skills/devops/SKILL.md +50 -0
  52. package/kit/review/skills/diagnose/SKILL.md +140 -0
  53. package/kit/review/skills/diagnose/references/feedback-loops.md +59 -0
  54. package/kit/review/skills/diagnose/scripts/hitl-loop.template.sh +71 -0
  55. package/kit/review/skills/pre-commit/SKILL.md +100 -0
  56. package/kit/review/skills/review/SKILL.md +43 -0
  57. package/kit/review/skills/security-audit/SKILL.md +65 -0
  58. package/kit/review/skills/uncle-bob/SKILL.md +43 -0
  59. package/kit/rules/rules/kit/arc42.md +89 -0
  60. package/kit/rules/rules/kit/architecture-files.md +87 -0
  61. package/kit/rules/rules/kit/code-style.md +159 -0
  62. package/kit/rules/rules/kit/pull-requests.md +59 -0
  63. package/kit/rules/rules/kit/testing.md +91 -0
  64. package/kit/rules/rules/kit/writing-for-agents.md +110 -0
  65. package/kit/workflow/agents/explore.md +25 -0
  66. package/kit/workflow/agents/implement.md +16 -0
  67. package/kit/workflow/agents/plan.md +13 -0
  68. package/kit/workflow/agents/spec-review.md +96 -0
  69. package/kit/workflow/skills/follow/SKILL.md +48 -0
  70. package/kit/workflow/skills/grill/SKILL.md +60 -0
  71. package/kit/workflow/skills/implement/SKILL.md +107 -0
  72. package/kit/workflow/skills/orchestrate/SKILL.md +565 -0
  73. package/kit/workflow/skills/orchestrate/handover/decision.ts +247 -0
  74. package/kit/workflow/skills/orchestrate/handover/index.ts +171 -0
  75. package/kit/workflow/skills/plan/SKILL.md +118 -0
  76. package/kit/workflow/skills/plan/references/template-English.md +92 -0
  77. package/kit/workflow/skills/plan/references/template-German.md +92 -0
  78. package/kit/workflow/skills/start/SKILL.md +47 -0
  79. package/package.json +63 -5
  80. package/schemas/jackherd.schema.json +601 -0
  81. package/src/check/changes.ts +45 -0
  82. package/src/check/command-line.ts +12 -0
  83. package/src/check/command.ts +136 -0
  84. package/src/check/deferral-arrows.ts +41 -0
  85. package/src/check/execute.ts +29 -0
  86. package/src/check/glob.ts +15 -0
  87. package/src/check/plan-structure-check.ts +135 -0
  88. package/src/check/plan.ts +106 -0
  89. package/src/check/public-content-check.ts +32 -0
  90. package/src/check/public-content-facts.ts +177 -0
  91. package/src/check/public-content.ts +216 -0
  92. package/src/check/report.ts +55 -0
  93. package/src/check/result.ts +42 -0
  94. package/src/check/roots.ts +36 -0
  95. package/src/check/routing.ts +22 -0
  96. package/src/check/schema.ts +83 -0
  97. package/src/check/settings.ts +44 -0
  98. package/src/claude/paths.ts +14 -0
  99. package/src/cli.ts +46 -0
  100. package/src/config/command.ts +45 -0
  101. package/src/config/detect.ts +196 -0
  102. package/src/config/index.ts +2 -0
  103. package/src/config/json-schema.ts +5 -0
  104. package/src/config/legacy-names.ts +56 -0
  105. package/src/config/link-kinds.ts +25 -0
  106. package/src/config/location.ts +46 -0
  107. package/src/config/read-config.ts +42 -0
  108. package/src/config/repository-facts.ts +19 -0
  109. package/src/config/repository-values.ts +46 -0
  110. package/src/config/schema.ts +302 -0
  111. package/src/context/command.ts +39 -0
  112. package/src/context/coordinator-name.ts +16 -0
  113. package/src/context/fill.ts +69 -0
  114. package/src/context/index.ts +10 -0
  115. package/src/context/level.ts +22 -0
  116. package/src/context/options.ts +29 -0
  117. package/src/context/report.ts +42 -0
  118. package/src/context/session-log.ts +91 -0
  119. package/src/plan-format/drift.ts +69 -0
  120. package/src/plan-format/header.ts +27 -0
  121. package/src/plan-format/index.ts +9 -0
  122. package/src/plan-format/lanes.ts +128 -0
  123. package/src/plan-format/markdown.ts +98 -0
  124. package/src/plan-format/phase-tally.ts +55 -0
  125. package/src/plan-format/phases.ts +224 -0
  126. package/src/plan-format/plan-structure.ts +150 -0
  127. package/src/plan-format/progress.ts +202 -0
  128. package/src/plan-format/sprint-document.ts +69 -0
  129. package/src/plan-format/terminal-text.ts +28 -0
  130. package/src/plans/board-choice.ts +60 -0
  131. package/src/plans/board-pick.ts +72 -0
  132. package/src/plans/board-rows.ts +62 -0
  133. package/src/plans/command.ts +60 -0
  134. package/src/plans/ensure-panes.ts +71 -0
  135. package/src/plans/error-text.ts +5 -0
  136. package/src/plans/fetch.ts +96 -0
  137. package/src/plans/git.ts +145 -0
  138. package/src/plans/github.ts +35 -0
  139. package/src/plans/lane-discovery.ts +34 -0
  140. package/src/plans/lane-plans.ts +80 -0
  141. package/src/plans/layout.ts +881 -0
  142. package/src/plans/link-chips.ts +74 -0
  143. package/src/plans/live.ts +336 -0
  144. package/src/plans/markdown-html.ts +140 -0
  145. package/src/plans/options.ts +15 -0
  146. package/src/plans/phase-source.ts +71 -0
  147. package/src/plans/plan-chips.ts +75 -0
  148. package/src/plans/plan-panes.ts +206 -0
  149. package/src/plans/planned.ts +117 -0
  150. package/src/plans/pull-requests.ts +107 -0
  151. package/src/plans/restart-decision.ts +42 -0
  152. package/src/plans/selection.ts +352 -0
  153. package/src/plans/settings.ts +68 -0
  154. package/src/plans/single-flight.ts +25 -0
  155. package/src/plans/snapshot.ts +300 -0
  156. package/src/plans/sprint-export.ts +103 -0
  157. package/src/plans/sprint-html.ts +301 -0
  158. package/src/plans/sprint-page.ts +287 -0
  159. package/src/plans/supervisor.ts +124 -0
  160. package/src/plans/symbols.ts +54 -0
  161. package/src/plans/texts.ts +103 -0
  162. package/src/plans/unlisted-lanes.ts +45 -0
  163. package/src/plans/view.ts +68 -0
  164. package/src/plans/worked-phases.ts +53 -0
  165. package/src/sync/checksum.ts +13 -0
  166. package/src/sync/choice.ts +14 -0
  167. package/src/sync/command-context.ts +113 -0
  168. package/src/sync/command.ts +94 -0
  169. package/src/sync/error-message.ts +1 -0
  170. package/src/sync/gitignore.ts +45 -0
  171. package/src/sync/global-kit.ts +176 -0
  172. package/src/sync/herdr-plugin.ts +175 -0
  173. package/src/sync/if-changed.ts +109 -0
  174. package/src/sync/init-command.ts +239 -0
  175. package/src/sync/init.ts +136 -0
  176. package/src/sync/kit-hooks.ts +18 -0
  177. package/src/sync/kit-path.ts +77 -0
  178. package/src/sync/kit-source.ts +169 -0
  179. package/src/sync/kit.ts +9 -0
  180. package/src/sync/legacy-files.ts +116 -0
  181. package/src/sync/manifest.ts +44 -0
  182. package/src/sync/next-steps.ts +42 -0
  183. package/src/sync/plan-file.ts +154 -0
  184. package/src/sync/plan-hooks.ts +165 -0
  185. package/src/sync/project-fs.ts +132 -0
  186. package/src/sync/render.ts +60 -0
  187. package/src/sync/report.ts +96 -0
  188. package/src/sync/settings-hooks.ts +90 -0
  189. package/src/sync/settings-migration.ts +94 -0
  190. package/src/sync/settings-offer.ts +168 -0
  191. package/src/sync/setup-questions.ts +70 -0
  192. package/src/sync/setup.ts +52 -0
  193. package/src/sync/sync-plan.ts +120 -0
  194. package/src/sync/sync-result.ts +9 -0
  195. package/src/sync/sync.ts +307 -0
  196. package/src/sync/tools.ts +52 -0
  197. package/src/terminal/herdr.ts +91 -0
  198. package/src/terminal/index.ts +13 -0
  199. package/src/terminal/none.ts +7 -0
  200. package/src/terminal/open.ts +39 -0
  201. package/src/terminal/text.ts +159 -0
  202. package/src/terminal/types.ts +30 -0
  203. package/src/terminal/width.ts +90 -0
  204. package/src/update/command.ts +65 -0
  205. package/src/update/package-manager.ts +56 -0
  206. package/src/update/update.ts +105 -0
  207. package/src/version/installed-version.ts +32 -0
  208. package/src/version/release.ts +22 -0
  209. package/src/version/skill-diff.ts +63 -0
  210. package/src/version/started-kit.ts +14 -0
  211. package/src/vocabulary/index.ts +132 -0
  212. package/src/watch/base-tip.ts +18 -0
  213. package/src/watch/command.ts +919 -0
  214. package/src/watch/coordinator-fill.ts +76 -0
  215. package/src/watch/feedback-read.ts +73 -0
  216. package/src/watch/feedback.ts +308 -0
  217. package/src/watch/gh.ts +22 -0
  218. package/src/watch/github-read.ts +175 -0
  219. package/src/watch/herdr.ts +11 -0
  220. package/src/watch/json-guards.ts +9 -0
  221. package/src/watch/kit-update.ts +18 -0
  222. package/src/watch/lane-fill.ts +36 -0
  223. package/src/watch/lane-message.ts +99 -0
  224. package/src/watch/lane-progress.ts +44 -0
  225. package/src/watch/lane-report-file.ts +53 -0
  226. package/src/watch/lane-report.ts +270 -0
  227. package/src/watch/lane-search.ts +41 -0
  228. package/src/watch/lane-sources.ts +46 -0
  229. package/src/watch/lane-watch.ts +121 -0
  230. package/src/watch/monitor-tool.ts +19 -0
  231. package/src/watch/options.ts +79 -0
  232. package/src/watch/plan-badges-read.ts +30 -0
  233. package/src/watch/plan-badges.ts +66 -0
  234. package/src/watch/plan-views.ts +59 -0
  235. package/src/watch/preview-read.ts +60 -0
  236. package/src/watch/previews.ts +234 -0
  237. package/src/watch/pull-requests.ts +117 -0
  238. package/src/watch/single-report.ts +15 -0
  239. package/src/watch/skill-change.ts +61 -0
  240. package/src/watch/source-dry-run-command.ts +35 -0
  241. package/src/watch/source-dry-run.ts +124 -0
  242. package/src/watch/source-fields.ts +9 -0
  243. package/src/watch/source-path.ts +40 -0
  244. package/src/watch/source-run.ts +90 -0
  245. package/src/watch/sources.ts +344 -0
  246. package/src/watch/stream-output.ts +14 -0
  247. package/src/watch/texts.ts +76 -0
  248. package/src/watch/watch-events.ts +399 -0
  249. package/src/watch/watcher-lock.ts +145 -0
  250. package/src/watch/workflow-runs.ts +96 -0
@@ -0,0 +1,247 @@
1
+ /** The herdr agent in the coordinator's pane, as one `herdr agent list` entry shows it. */
2
+ export type PaneAgent = { status: string; sessionId: string | null; name: string | null; seq: number }
3
+
4
+ /** One look at the pane: its agent, no agent at all, or a listing that could not be read. */
5
+ export type PaneReading =
6
+ | { kind: 'agent'; agent: PaneAgent }
7
+ | { kind: 'none' }
8
+ | { kind: 'unreadable'; reason: string }
9
+
10
+ /** The herdr agent states the hand-over acts on; herdr knows more (`unknown`), which count as not ready. */
11
+ const AGENT_STATUS = { idle: 'idle', done: 'done', working: 'working', blocked: 'blocked' } as const
12
+
13
+ export const HANDOVER_PHASE = {
14
+ waitIdle: 'waitIdle',
15
+ waitSession: 'waitSession',
16
+ waitWork: 'waitWork',
17
+ finished: 'finished',
18
+ aborted: 'aborted',
19
+ } as const
20
+
21
+ /** Since when the agent has been ready without a new `state_change_seq`. */
22
+ type Quiet = { since: number; seq: number }
23
+
24
+ type WaitIdle = {
25
+ phase: typeof HANDOVER_PHASE.waitIdle
26
+ since: number
27
+ sessionId: string
28
+ name: string | null
29
+ quiet: Quiet | null
30
+ }
31
+ type WaitSession = { phase: typeof HANDOVER_PHASE.waitSession; since: number; sessionId: string; name: string | null }
32
+ type WaitWork = { phase: typeof HANDOVER_PHASE.waitWork; since: number; sessionId: string }
33
+ type Waiting = WaitIdle | WaitSession | WaitWork
34
+
35
+ export type HandoverState =
36
+ | Waiting
37
+ | { phase: typeof HANDOVER_PHASE.finished }
38
+ | { phase: typeof HANDOVER_PHASE.aborted; reason: string }
39
+
40
+ /** `send` restores the herdr name first when `rename` carries one. */
41
+ type HandoverAction =
42
+ | { kind: 'wait' }
43
+ | { kind: 'clear' }
44
+ | { kind: 'send'; rename: string | null }
45
+ | { kind: 'finish' }
46
+ | { kind: 'abort'; reason: string }
47
+
48
+ type HandoverStep = { state: HandoverState; action: HandoverAction }
49
+
50
+ export type HandoverOptions = { pane: string; send: string; log: string; foreground: boolean }
51
+
52
+ /**
53
+ * `settleMs`: how long the agent stays ready with the same `state_change_seq` before `/clear`, so
54
+ * the turn that started the script has really ended. The timeouts bound each wait in milliseconds.
55
+ */
56
+ export const HANDOVER_TIMING = {
57
+ pollMs: 1000,
58
+ settleMs: 3000,
59
+ idleTimeoutMs: 15 * 60_000,
60
+ clearTimeoutMs: 60_000,
61
+ workTimeoutMs: 60_000,
62
+ } as const
63
+
64
+ const DEFAULT_HANDOVER_LOG = '.claude/handover/coordinator.log'
65
+
66
+ const READY_STATUS = new Set<string>([AGENT_STATUS.idle, AGENT_STATUS.done])
67
+ const STARTED_STATUS = new Set<string>([AGENT_STATUS.working, AGENT_STATUS.blocked])
68
+
69
+ const PHASE_TIMEOUT_MS: Record<Waiting['phase'], number> = {
70
+ waitIdle: HANDOVER_TIMING.idleTimeoutMs,
71
+ waitSession: HANDOVER_TIMING.clearTimeoutMs,
72
+ waitWork: HANDOVER_TIMING.workTimeoutMs,
73
+ }
74
+ const NO_AGENT = 'no agent in the pane'
75
+ const UNEXPECTED_LISTING = 'unexpected herdr agent list'
76
+
77
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
78
+
79
+ const stringOrNull = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null)
80
+
81
+ const toPaneAgent = (entry: Record<string, unknown>): PaneAgent | null => {
82
+ const { agent_status: status, state_change_seq: seq, agent_session: session, name } = entry
83
+ if (typeof status !== 'string' || typeof seq !== 'number') return null
84
+ return { status, sessionId: isRecord(session) ? stringOrNull(session.value) : null, name: stringOrNull(name), seq }
85
+ }
86
+
87
+ /** Finds the agent of one pane in the JSON of `herdr agent list`. */
88
+ export const paneAgentOf = (listing: unknown, pane: string): PaneReading => {
89
+ const agents = isRecord(listing) && isRecord(listing.result) ? listing.result.agents : undefined
90
+ if (!Array.isArray(agents)) return { kind: 'unreadable', reason: UNEXPECTED_LISTING }
91
+ const entry = agents.find((candidate) => isRecord(candidate) && candidate.pane_id === pane)
92
+ if (entry === undefined) return { kind: 'none' }
93
+ const agent = toPaneAgent(entry)
94
+ return agent ? { kind: 'agent', agent } : { kind: 'unreadable', reason: UNEXPECTED_LISTING }
95
+ }
96
+
97
+ export const startHandover = (
98
+ reading: PaneReading,
99
+ now: number,
100
+ ): { ok: true; state: HandoverState } | { ok: false; reason: string } => {
101
+ if (reading.kind === 'none') return { ok: false, reason: NO_AGENT }
102
+ if (reading.kind === 'unreadable') return { ok: false, reason: reading.reason }
103
+ const { sessionId, name } = reading.agent
104
+ if (sessionId === null) {
105
+ return {
106
+ ok: false,
107
+ reason: 'the agent reports no session id, so a cleared chat could not be told from the old one',
108
+ }
109
+ }
110
+ return { ok: true, state: { phase: HANDOVER_PHASE.waitIdle, since: now, sessionId, name, quiet: null } }
111
+ }
112
+
113
+ const stay = (state: HandoverState): HandoverStep => ({ state, action: { kind: 'wait' } })
114
+
115
+ const abort = (reason: string): HandoverStep => ({
116
+ state: { phase: HANDOVER_PHASE.aborted, reason },
117
+ action: { kind: 'abort', reason },
118
+ })
119
+
120
+ const minutes = (milliseconds: number): number => milliseconds / 60_000
121
+ const seconds = (milliseconds: number): number => milliseconds / 1000
122
+
123
+ /** The one place that decides whether a wait has run out, whatever the pane showed meanwhile. */
124
+ const isPastTimeout = (state: Waiting, now: number): boolean => now - state.since > PHASE_TIMEOUT_MS[state.phase]
125
+
126
+ const stepWaitIdle = (state: WaitIdle, agent: PaneAgent, now: number): HandoverStep => {
127
+ if (agent.sessionId !== null && agent.sessionId !== state.sessionId) {
128
+ return abort(`the session changed before /clear was sent (${state.sessionId} → ${agent.sessionId})`)
129
+ }
130
+ if (isPastTimeout(state, now)) {
131
+ return abort(`the agent was not idle within ${minutes(HANDOVER_TIMING.idleTimeoutMs)} min`)
132
+ }
133
+ if (agent.sessionId === null) return stay(state)
134
+ if (!READY_STATUS.has(agent.status)) return stay({ ...state, quiet: null })
135
+ if (state.quiet === null || state.quiet.seq !== agent.seq)
136
+ return stay({ ...state, quiet: { since: now, seq: agent.seq } })
137
+ if (now - state.quiet.since < HANDOVER_TIMING.settleMs) return stay(state)
138
+ return {
139
+ state: { phase: HANDOVER_PHASE.waitSession, since: now, sessionId: state.sessionId, name: state.name },
140
+ action: { kind: 'clear' },
141
+ }
142
+ }
143
+
144
+ const sessionTimeoutReason = (state: WaitSession, sessionId: string | null): string => {
145
+ const limit = seconds(HANDOVER_TIMING.clearTimeoutMs)
146
+ if (sessionId === null) return `the agent reported no session ${limit} s after /clear`
147
+ if (sessionId === state.sessionId)
148
+ return `the session is still ${state.sessionId} ${limit} s after /clear: the chat was not cleared`
149
+ return `the new session ${sessionId} was not ready within ${limit} s`
150
+ }
151
+
152
+ const stepWaitSession = (state: WaitSession, agent: PaneAgent, now: number): HandoverStep => {
153
+ const { sessionId } = agent
154
+ if (sessionId !== null && sessionId !== state.sessionId && READY_STATUS.has(agent.status)) {
155
+ const rename = state.name !== null && agent.name !== state.name ? state.name : null
156
+ return { state: { phase: HANDOVER_PHASE.waitWork, since: now, sessionId }, action: { kind: 'send', rename } }
157
+ }
158
+ return isPastTimeout(state, now) ? abort(sessionTimeoutReason(state, sessionId)) : stay(state)
159
+ }
160
+
161
+ const stepWaitWork = (state: WaitWork, agent: PaneAgent, now: number): HandoverStep => {
162
+ if (agent.sessionId !== null && agent.sessionId !== state.sessionId) {
163
+ return abort(`the session changed again after the command (${state.sessionId} → ${agent.sessionId})`)
164
+ }
165
+ if (agent.sessionId !== null && STARTED_STATUS.has(agent.status)) {
166
+ return { state: { phase: HANDOVER_PHASE.finished }, action: { kind: 'finish' } }
167
+ }
168
+ if (isPastTimeout(state, now)) {
169
+ return abort(`the agent did not start working within ${seconds(HANDOVER_TIMING.workTimeoutMs)} s of the command`)
170
+ }
171
+ return stay(state)
172
+ }
173
+
174
+ /**
175
+ * One poll of the hand-over: wait until the agent settled, clear, wait for a new session, send the
176
+ * command, wait for its turn. An unreadable listing waits, but never past the phase's timeout.
177
+ */
178
+ export const handoverStep = (state: HandoverState, reading: PaneReading, now: number): HandoverStep => {
179
+ if (state.phase === HANDOVER_PHASE.finished || state.phase === HANDOVER_PHASE.aborted) return stay(state)
180
+ if (reading.kind === 'none') return abort(NO_AGENT)
181
+ if (reading.kind === 'unreadable') {
182
+ return isPastTimeout(state, now)
183
+ ? abort(`herdr could not be read (${reading.reason}) until the ${state.phase} timeout ran out`)
184
+ : stay(state)
185
+ }
186
+ if (state.phase === HANDOVER_PHASE.waitIdle) return stepWaitIdle(state, reading.agent, now)
187
+ if (state.phase === HANDOVER_PHASE.waitSession) return stepWaitSession(state, reading.agent, now)
188
+ return stepWaitWork(state, reading.agent, now)
189
+ }
190
+
191
+ /** One line for the log: what the pane showed at a step. */
192
+ export const describeReading = (reading: PaneReading): string => {
193
+ switch (reading.kind) {
194
+ case 'agent': {
195
+ const { status, sessionId, name } = reading.agent
196
+ return `status ${status}, session ${sessionId ?? '(none)'}, name ${name ?? '(none)'}`
197
+ }
198
+ case 'none':
199
+ return 'no agent'
200
+ case 'unreadable':
201
+ return `unreadable (${reading.reason})`
202
+ }
203
+ }
204
+
205
+ export const manualPath = (pane: string, line: string): string =>
206
+ `by hand: type /clear in pane ${pane}, then type: ${line}`
207
+
208
+ const VALUE_OPTIONS = new Set(['--send', '--pane', '--log'])
209
+
210
+ const readArguments = (argv: string[]): { values: Map<string, string>; foreground: boolean } | { error: string } => {
211
+ const values = new Map<string, string>()
212
+ let foreground = false
213
+ for (let index = 0; index < argv.length; index++) {
214
+ const option = argv[index]
215
+ if (option === '--foreground') {
216
+ foreground = true
217
+ continue
218
+ }
219
+ if (!VALUE_OPTIONS.has(option)) return { error: `unknown option ${option}` }
220
+ const value = argv[index + 1]
221
+ if (value === undefined) return { error: `${option} needs a value` }
222
+ values.set(option, value)
223
+ index++
224
+ }
225
+ return { values, foreground }
226
+ }
227
+
228
+ /**
229
+ * `--send <line>` is the one input typed into the cleared chat. `--foreground` runs the hand-over
230
+ * in this process; without it the script detaches a copy of itself and returns.
231
+ */
232
+ export const parseHandoverOptions = (
233
+ argv: string[],
234
+ env: Record<string, string | undefined>,
235
+ ): { ok: true; options: HandoverOptions } | { ok: false; error: string } => {
236
+ const parsed = readArguments(argv)
237
+ if ('error' in parsed) return { ok: false, error: parsed.error }
238
+ const send = parsed.values.get('--send')?.trim() ?? ''
239
+ if (send === '') return { ok: false, error: '--send <line> is required' }
240
+ if (/[\r\n]/.test(send)) {
241
+ return { ok: false, error: '--send must be one line: a line break would submit the rest as a second input' }
242
+ }
243
+ const pane = parsed.values.get('--pane') ?? env.HERDR_PANE_ID
244
+ if (!pane) return { ok: false, error: 'no pane: run inside herdr (HERDR_PANE_ID) or pass --pane <id>' }
245
+ const log = parsed.values.get('--log') ?? DEFAULT_HANDOVER_LOG
246
+ return { ok: true, options: { pane, send, log, foreground: parsed.foreground } }
247
+ }
@@ -0,0 +1,171 @@
1
+ // Hands the coordinator over to a fresh chat, shipped by jackherd. Run from the
2
+ // coordinator's own Bash call: it detaches a copy of itself and returns, the copy waits until the
3
+ // coordinator's pane is idle, sends /clear, checks that a new session runs and then types the
4
+ // command. Every step goes to the log; an abort also names the way by hand and shows a herdr notice.
5
+ import { spawn } from 'node:child_process'
6
+ import { appendFile, mkdir } from 'node:fs/promises'
7
+ import { dirname, resolve } from 'node:path'
8
+ import {
9
+ describeReading,
10
+ HANDOVER_TIMING,
11
+ type HandoverOptions,
12
+ type HandoverState,
13
+ handoverStep,
14
+ manualPath,
15
+ type PaneReading,
16
+ paneAgentOf,
17
+ parseHandoverOptions,
18
+ startHandover,
19
+ } from './decision'
20
+
21
+ type HerdrResult = { ok: true; stdout: string } | { ok: false; error: string }
22
+
23
+ const EXIT = { done: 0, aborted: 1, usage: 2 } as const
24
+
25
+ const ABORT_TITLE = 'Hand-over aborted'
26
+
27
+ const herdr = async (args: string[]): Promise<HerdrResult> => {
28
+ try {
29
+ const child = Bun.spawn(['herdr', ...args], { stdout: 'pipe', stderr: 'pipe' })
30
+ const [stdout, stderr, code] = await Promise.all([
31
+ new Response(child.stdout).text(),
32
+ new Response(child.stderr).text(),
33
+ child.exited,
34
+ ])
35
+ return code === 0 ? { ok: true, stdout } : { ok: false, error: (stderr || stdout).trim() || `exit ${code}` }
36
+ } catch (error) {
37
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
38
+ }
39
+ }
40
+
41
+ const readPane = async (pane: string): Promise<PaneReading> => {
42
+ const listed = await herdr(['agent', 'list'])
43
+ if (!listed.ok) return { kind: 'unreadable', reason: `herdr agent list: ${listed.error}` }
44
+ try {
45
+ return paneAgentOf(JSON.parse(listed.stdout), pane)
46
+ } catch {
47
+ return { kind: 'unreadable', reason: 'herdr agent list answered no JSON' }
48
+ }
49
+ }
50
+
51
+ const writeLog = async (path: string, line: string): Promise<void> => {
52
+ const time = new Date().toTimeString().slice(0, 8)
53
+ await appendFile(path, `${time} ${line}\n`)
54
+ }
55
+
56
+ const sleep = (milliseconds: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, milliseconds))
57
+
58
+ type Log = (line: string) => Promise<void>
59
+
60
+ /** Returns the reason of a failed herdr call, or null once `/clear` went out. */
61
+ const sendClear = async (options: HandoverOptions, log: Log): Promise<string | null> => {
62
+ const cleared = await herdr(['agent', 'prompt', options.pane, '/clear'])
63
+ if (!cleared.ok) return `herdr agent prompt /clear failed: ${cleared.error}`
64
+ await log('sent /clear')
65
+ return null
66
+ }
67
+
68
+ /** A failed rename is only logged: the name is a convenience, the line is the hand-over. */
69
+ const sendCommand = async (rename: string | null, options: HandoverOptions, log: Log): Promise<string | null> => {
70
+ if (rename !== null) {
71
+ const renamed = await herdr(['agent', 'rename', options.pane, rename])
72
+ await log(renamed.ok ? `renamed the agent to ${rename}` : `rename to ${rename} failed: ${renamed.error}`)
73
+ }
74
+ const sent = await herdr(['agent', 'prompt', options.pane, options.send])
75
+ if (!sent.ok) return `herdr agent prompt failed: ${sent.error}`
76
+ await log(`sent: ${options.send}`)
77
+ return null
78
+ }
79
+
80
+ const showAbortNotice = (reason: string, options: HandoverOptions): Promise<HerdrResult> =>
81
+ herdr([
82
+ 'notification',
83
+ 'show',
84
+ ABORT_TITLE,
85
+ '--body',
86
+ `${reason}. ${manualPath(options.pane, options.send)}`,
87
+ '--sound',
88
+ 'request',
89
+ ])
90
+
91
+ /** Logs the abort and tells the user through a herdr notice: the coordinator's turn has ended, nobody else would. */
92
+ const finishAborted = async (reason: string, options: HandoverOptions, log: Log): Promise<number> => {
93
+ await log(`aborted: ${reason}`)
94
+ await log(manualPath(options.pane, options.send))
95
+ const notice = await showAbortNotice(reason, options)
96
+ if (!notice.ok) await log(`notice failed: ${notice.error}`)
97
+ return EXIT.aborted
98
+ }
99
+
100
+ const runHandover = async (options: HandoverOptions): Promise<number> => {
101
+ await mkdir(dirname(options.log), { recursive: true })
102
+ const log: Log = (line) => writeLog(options.log, line)
103
+ const initialReading = await readPane(options.pane)
104
+ const started = startHandover(initialReading, Date.now())
105
+ if (!started.ok) return finishAborted(started.reason, options, log)
106
+ await log(`pid ${process.pid}: waiting for pane ${options.pane} to settle (${describeReading(initialReading)})`)
107
+ let state: HandoverState = started.state
108
+ while (true) {
109
+ await sleep(HANDOVER_TIMING.pollMs)
110
+ const reading = await readPane(options.pane)
111
+ const { state: next, action } = handoverStep(state, reading, Date.now())
112
+ if (next.phase !== state.phase) await log(`${state.phase} → ${next.phase}: ${describeReading(reading)}`)
113
+ state = next
114
+ let failure: string | null = null
115
+ switch (action.kind) {
116
+ case 'finish':
117
+ await log('delivered: the new session is working on the command')
118
+ return EXIT.done
119
+ case 'abort':
120
+ return finishAborted(action.reason, options, log)
121
+ case 'clear':
122
+ failure = await sendClear(options, log)
123
+ break
124
+ case 'send':
125
+ failure = await sendCommand(action.rename, options, log)
126
+ break
127
+ }
128
+ if (failure !== null) return finishAborted(failure, options, log)
129
+ }
130
+ }
131
+
132
+ /** The detached copy has no terminal: a crash (a log it cannot write) must still reach the user. */
133
+ const runVisibly = async (options: HandoverOptions): Promise<number> => {
134
+ try {
135
+ return await runHandover(options)
136
+ } catch (error) {
137
+ await showAbortNotice(
138
+ `the hand-over script failed: ${error instanceof Error ? error.message : String(error)}`,
139
+ options,
140
+ )
141
+ return EXIT.aborted
142
+ }
143
+ }
144
+
145
+ /** Starts this script again in a session of its own, so it outlives the Bash call and the /clear. */
146
+ const detach = async (argv: string[], options: HandoverOptions): Promise<number> => {
147
+ const started = startHandover(await readPane(options.pane), Date.now())
148
+ if (!started.ok) {
149
+ console.error(`hand-over not started: ${started.reason}`)
150
+ return EXIT.aborted
151
+ }
152
+ // node:child_process, not Bun.spawn: only it offers `detached`, a session of the child's own.
153
+ const child = spawn(process.execPath, [import.meta.path, ...argv, '--foreground'], {
154
+ detached: true,
155
+ stdio: 'ignore',
156
+ cwd: process.cwd(),
157
+ env: process.env,
158
+ })
159
+ child.unref()
160
+ console.log(`hand-over started: pid ${child.pid}, log ${options.log}`)
161
+ return EXIT.done
162
+ }
163
+
164
+ const argv = process.argv.slice(2)
165
+ const parsed = parseHandoverOptions(argv, process.env)
166
+ if (!parsed.ok) {
167
+ console.error(parsed.error)
168
+ process.exit(EXIT.usage)
169
+ }
170
+ const options = { ...parsed.options, log: resolve(parsed.options.log) }
171
+ process.exit(options.foreground ? await runVisibly(options) : await detach(argv, options))
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: plan
3
+ description: 'Create structured concept/plan documents for {{kit.planDirectory}}/. Use when planning new features or architectural changes.'
4
+ ---
5
+
6
+ If `.claude/project/plan.md` exists, read it first. It extends this skill for this project (where to look in the pre-scan, which documents carry product context, extra review-gate items) and wins where the two disagree.
7
+
8
+ You create a structured concept/plan document in `{{kit.planDirectory}}/`.
9
+
10
+ > **Output language: {{kit.planLanguage}}.** These instructions are English; the plan document is
11
+ > written in {{kit.planLanguage}}. Keep the section headings of the template **verbatim** — the plan
12
+ > parser, `/implement` and `spec-review` find `## {{kit.heading.decisions}}`,
13
+ > `## {{kit.heading.implementation}}` and `## {{kit.heading.documentation}}` by those exact names.
14
+
15
+ ## Your Workflow
16
+
17
+ ### 0. Are the decisions actually made?
18
+
19
+ A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would end up holding your guesses rather than the user's answers, **stop and recommend `/grill`** — name the open questions you would otherwise have to invent answers for, and let the user decide whether to run it. Entering clarification mode is the user's call, so you recommend it and leave the invocation to them. It works the questions in rounds and hands back rows in this table's format.
20
+
21
+ Skip this when the request already carries its decisions (a plan for a bug fix with one obvious approach, a change the user has already specified).
22
+
23
+ ### 1. Pre-Scan (before a single word of the plan is written)
24
+
25
+ - [ ] Read the relevant files in their **current** state, from disk
26
+ - [ ] Search the project's shared components and utilities for what covers 80%+ of the need. 80%+ match → the plan specifies reuse or an override; 60–80% → the plan extends the shared piece; below 60% → new code, with the rationale under the rejected alternatives
27
+ - [ ] Read related plans in `{{kit.planDirectory}}/` — check dependencies and open TODOs
28
+ - [ ] Read the architecture documentation the change touches — understand what is already documented before you plan
29
+
30
+ ### 2. Create the Document
31
+
32
+ **Filename:** `{{kit.planDirectory}}/YYYY-MM-DD-<kebab-case-title>.md`
33
+
34
+ **Structure:** copy the template in [`references/template-{{kit.planLanguage}}.md`](references/template-{{kit.planLanguage}}.md) and fill it in. Every section stays, in its order, with its heading verbatim.
35
+
36
+ The closing documentation section reads `## N — …` with rows `N.1` to `N.5`: replace `N` with the number of that phase (`## 10 — …`, `10.1`). A literal `N` is no phase to the plans view, which shows it as "not in the plan".
37
+
38
+ A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The plans view counts it neither as open nor as done in its own phase and lists it as open in phase 9, so a phase whose only open box was moved on reads as finished. The arrow counts only at the end of the line; `jh check` refuses an open box in a changed plan that carries it anywhere else (`- [ ] → P9 Update …`). Once a phase's tasks are ticked on the base branch and the change does not tick it further, every box still open in it, gate boxes included, is ticked or moved on this way: `jh check` warns on a changed plan that leaves one.
39
+
40
+ What the template's tables mean:
41
+
42
+ - **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. The project's default model for implementation (`{{kit.model.implement}}`) is the default of every row. A phase moves up to the planning model (`{{kit.model.plan}}`) when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
43
+ - **Dependencies and parallelization** (from three phases) — `/implement` reads it and starts every phase whose dependencies are done and that shares no written path with another, at the same time.
44
+ - **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`). A phase is ready when everything here is done.
45
+ - **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible. Shared files many phases touch (barrels, translation files, `package.json`, generated types, architecture docs) are listed explicitly, so the overlap is visible.
46
+ - **{{kit.heading.serialMarker}}:** `Migration`, `<collection> schema bump`, `Event <type>`, a generator run. Two phases with a migration never run at the same time; neither do two that bump the same schema.
47
+
48
+ ### 3. Architecture Review
49
+
50
+ Apply `/uncle-bob` to the created plan. Additionally check:
51
+
52
+ - **Interfaces:** Are the boundaries between modules/services clear? Who consumes what?
53
+ - **Blast radius:** Which existing consumers are affected? Are tests needed in all consuming packages?
54
+
55
+ ### 4. Anchor the Review Gate in the Plan
56
+
57
+ Every phase gets the review gate block from the template at its end. Add the reviewers the project supplement names for its areas (a UI review for UI phases, for instance). A phase with tasks that genuinely needs no gate (documentation only, a draft) says so and why in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
58
+
59
+ ### 5. Quality Checks
60
+
61
+ - **Focus:** Does the plan solve a concrete problem? No "would be nice to have".
62
+ - **Scope:** Is the scope clearly bounded? No feature-creep risk?
63
+ - **Feasibility:** Can the phases be split into individual PRs/commits?
64
+ - **References:** Are related plans in `{{kit.planDirectory}}/` linked?
65
+ - **Current state is fact-based:** Read code, count files, verify architecture; every claim has a source.
66
+ - **Shared code checked:** For every planned component it is documented why it is built new instead of reused or extended.
67
+ - **Tests defined:** The tests section is filled in or carries an explicit rationale why none are needed (decision tree in `.claude/rules/kit/testing.md`).
68
+ - **Model recommendations defined:** `## {{kit.heading.implementation}}` starts with the model table, one row per phase, justified by error cost × complexity.
69
+ - **Parallelism defined (3+ phases):** the dependencies table follows it. Check it against the phase bodies: every migration, schema bump and new event in a phase appears as a serial marker; every shared file appears under {{kit.heading.writes}}. A dependency that exists only in your head lets `/implement` start two phases that collide.
70
+ - **arc42 check (after the plan exists):** apply the filter in `.claude/rules/kit/arc42.md` to the finished plan and **ask the user concretely** which sections should be extended. On "yes", add a concrete task to `## {{kit.heading.documentation}}`.
71
+ - **Architecture check (after the plan exists):** apply the filter in `.claude/rules/kit/architecture-files.md` — new ownership, a new invariant or an accepted risk each trigger a concrete question about the unit's `ARCHITECTURE.md`.
72
+ - **arc42 closing phase (3+ phases or a new service/package):** end the plan with the architecture documentation phase from the template. Add arc42 sections beyond it only when they deliver real value.
73
+ - **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated after completion.
74
+
75
+ ### 6. Propose the board rows
76
+
77
+ A **board** gathers the lanes one coordinator runs with `/orchestrate`: `{{kit.board.file}}` in a directory
78
+ matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). The plan
79
+ already holds what a lane row needs, so end by proposing the rows; `/orchestrate` then starts the lanes from them.
80
+ Skip this step for a plan of one phase that nobody runs as a lane, and say so.
81
+
82
+ Derive the rows from the dependencies table and the model table, nothing else:
83
+
84
+ - **Own lane:** a phase that can run beside the others: no path under {{kit.heading.writes}} in common with a phase
85
+ of another lane, and no serial marker that another lane carries at the same time.
86
+ - **Continuation:** a phase that shares a written path or a serial marker with an earlier phase, or depends on it,
87
+ is a `↳` row under that phase's lane, in the same worktree.
88
+ - **Starts after:** from the phase's {{kit.heading.dependsOn}} cell, as the board reads it (`P2 merged`, an external
89
+ gate such as `User: mockups`); a phase without one starts at once.
90
+ - **Model:** the full model ID from the model table (`{{kit.model.implement}}`, `{{kit.model.plan}}`), never an alias.
91
+ - **Plan:** a Markdown link to the plan, its path relative to the board's directory (`../<file>.md` for a plan
92
+ directly under `{{kit.planDirectory}}`).
93
+ Plans stay where they are; the row only links them.
94
+ - **Lane name:** short kebab case, unique on the board. `/orchestrate` names the worktree directory and the herdr
95
+ agent after it, so the worktree directory and the herdr agent carry the same name; a name that differs leaves the
96
+ watcher reporting the lane as `no agent`.
97
+ - **Port:** `–`; the coordinator sets it when the lane starts, unless the project supplement assigns ports here.
98
+
99
+ Show the rows as the table they become: `## {{kit.heading.lanes}}` with the columns lane, port, plan, phases, starts
100
+ after, model, in this order and headed in {{kit.planLanguage}} (the plans view reads them by position). Then ask
101
+ where they go:
102
+
103
+ - **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`. The
104
+ rows are appended to its `## {{kit.heading.lanes}}` table.
105
+ - **A new board.** Ask for its name and propose one: the year and month or the topic, in the pattern's shape. Check
106
+ that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again.
107
+ Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the `## {{kit.heading.lanes}}`
108
+ table holding the proposed rows.
109
+ - **Not now:** the rows stay in your answer, for `/orchestrate` to write when it starts the lanes.
110
+
111
+ Nothing is written without that answer. The board is a file like the plan: it goes into the same commit, under the
112
+ same approval.
113
+
114
+ After writing the rows, name the next step in one line: `/orchestrate <board path>`, from a Claude Code session
115
+ inside herdr. A plan or board that is not on `origin/{{kit.baseBranch}}` yet needs no merge first: `/orchestrate`
116
+ asks to commit it on a documentation branch and starts the lanes from there.
117
+
118
+ Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
@@ -0,0 +1,92 @@
1
+ # Title
2
+
3
+ **{{kit.heading.date}}:** YYYY-MM-DD
4
+ **{{kit.heading.status}}:** Draft / In Progress / Done / Abandoned
5
+ **{{kit.heading.scope}}:** `affected/paths`
6
+
7
+ ## Problem / Context
8
+
9
+ What is the trigger? Why now? One paragraph.
10
+
11
+ ## Current State
12
+
13
+ Fact-based: code analysis, file counts, current architecture. Tables preferred.
14
+
15
+ ## Target State
16
+
17
+ What changes concretely? Files, modules, flows.
18
+
19
+ ## {{kit.heading.decisions}}
20
+
21
+ | Decision | Rationale |
22
+ | -------- | --------- |
23
+
24
+ ## {{kit.heading.implementation}}
25
+
26
+ ### {{kit.heading.modelRecommendations}}
27
+
28
+ | {{kit.heading.phase}} | Model | Rationale |
29
+ | ----- | ----- | --------- |
30
+ | 1 — ... | {{kit.model.implement}} | Error cost × complexity |
31
+
32
+ > **Applying the models:** `/implement` applies this table on its own (step 0, model gate): it runs every phase whose recommendation differs from the running model through a subagent with the recommended model, in both directions. Whoever implements **without** `/implement` sets the model themselves at the start.
33
+
34
+ ### {{kit.heading.dependencies}}
35
+
36
+ | {{kit.heading.phase}} | {{kit.heading.dependsOn}} | {{kit.heading.writes}} | {{kit.heading.serialMarker}} |
37
+ | ----- | ------------ | -------- | -------------- |
38
+ | 1 — ... | — | `path/to/module/` | Migration |
39
+ | 2 — ... | 1 | `other/module/` | — |
40
+
41
+ > **Applying the parallelism:** `/implement` starts ready phases without a shared written path and without colliding serial markers at the same time (step 0b), each with its model from the table above. Whoever implements without `/implement` reads here which sessions may run in parallel.
42
+
43
+ ### {{kit.heading.phase}} 1: ...
44
+
45
+ - [ ] Step 1
46
+ - [ ] Step 2
47
+
48
+ **Review gate {{kit.heading.phase}} 1:**
49
+
50
+ - [ ] Pre-scan: [the files and modules to read before starting]
51
+ - [ ] Implementation complete and committable
52
+ - [ ] Tests written (or the reason none are needed)
53
+ - [ ] `/uncle-bob` — Clean Architecture review
54
+ - [ ] Documentation updated or created
55
+ - [ ] Plan and roadmap in sync
56
+
57
+ ## Tests
58
+
59
+ Which tests are needed? Decide with the decision tree in `.claude/rules/kit/testing.md`.
60
+
61
+ | File | Kind | Why |
62
+ | ---- | ---- | --- |
63
+
64
+ > No tests needed → give the reason explicitly (e.g. "layout only, no logic branch").
65
+
66
+ ## {{kit.heading.documentation}}
67
+
68
+ What gets documented after completion?
69
+
70
+ - [ ] ...
71
+
72
+ ## Rejected Alternatives
73
+
74
+ | Idea | Reason |
75
+ | ---- | ------ |
76
+
77
+ ## Risks
78
+
79
+ | Risk | Severity | Mitigation |
80
+ | ---- | -------- | ---------- |
81
+
82
+ ## N — Architecture documentation (arc42 light)
83
+
84
+ > After the implementation: derive the architecture docs from the finished code.
85
+
86
+ | # | Task | Status |
87
+ | --- | ---- | ------ |
88
+ | N.1 | Context: external systems and interfaces | ⬜ |
89
+ | N.2 | Building blocks: services, packages, apps | ⬜ |
90
+ | N.3 | Runtime: significant flows as sequence diagrams | ⬜ |
91
+ | N.4 | Decisions: ADRs from the decisions table | ⬜ |
92
+ | N.5 | `ARCHITECTURE.md` of the touched units: new invariants and risks | ⬜ |