jackherd 1.8.1 → 1.10.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 (261) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/INSTALL.md +3 -1
  3. package/README.md +75 -64
  4. package/dist/{chunk-OU2KALXP.js → chunk-5MQBCUQ5.js} +24 -4
  5. package/dist/config.d.ts +45 -4
  6. package/dist/config.js +84 -27
  7. package/dist/plan-format.d.ts +63 -7
  8. package/dist/plan-format.js +66 -12
  9. package/docs/workflow-dark.svg +67 -63
  10. package/docs/workflow.svg +67 -63
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/install.sh +1 -1
  13. package/kit/hooks/ask-rules.json +15 -0
  14. package/kit/hooks/hooks/approval-gate/index.ts +22 -4
  15. package/kit/hooks/hooks/approval-gate/run.ts +29 -6
  16. package/kit/hooks/hooks/approval-gate/subagent.ts +18 -0
  17. package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
  18. package/kit/hooks/hooks/ask-hint/index.ts +15 -19
  19. package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
  20. package/kit/hooks/hooks/git-guard/decision.ts +50 -102
  21. package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
  22. package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
  23. package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
  24. package/kit/hooks/hooks/merge-guard/decision.ts +127 -140
  25. package/kit/hooks/hooks/merge-guard/index.ts +6 -3
  26. package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
  27. package/kit/hooks/hooks/merge-guard/run.ts +59 -54
  28. package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
  29. package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
  30. package/kit/hooks/hooks/open-question/index.ts +26 -0
  31. package/kit/hooks/hooks/open-question/record.ts +135 -0
  32. package/kit/hooks/hooks/open-question/run.ts +52 -0
  33. package/kit/hooks/hooks/open-question/store.ts +39 -0
  34. package/kit/hooks/hooks/sent-file/index.ts +19 -0
  35. package/kit/hooks/hooks/sent-file/record.ts +128 -0
  36. package/kit/hooks/hooks/sent-file/run.ts +48 -0
  37. package/kit/hooks/hooks/sent-file/store.ts +9 -0
  38. package/kit/hooks/hooks/shared/board-lanes.ts +15 -2
  39. package/kit/hooks/hooks/shared/config.ts +7 -0
  40. package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
  41. package/kit/hooks/hooks/shared/files.ts +7 -0
  42. package/kit/hooks/hooks/shared/lane.ts +1 -1
  43. package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
  44. package/kit/hooks/hooks/shared/private-directory.ts +56 -9
  45. package/kit/hooks/hooks/shared/program-position.ts +2 -75
  46. package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
  47. package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
  48. package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
  49. package/kit/hooks/hooks/task-hint/decision.ts +49 -0
  50. package/kit/hooks/hooks/task-hint/index.ts +79 -0
  51. package/kit/hooks/hooks/task-hint/run.ts +62 -0
  52. package/kit/hooks/hooks/task-hint/store.ts +51 -0
  53. package/kit/hooks/hooks.json +21 -4
  54. package/kit/hooks/statusline.sh +1 -1
  55. package/kit/review/agents/skill-review.md +34 -0
  56. package/kit/review/skills/pre-commit/SKILL.md +1 -1
  57. package/kit/workflow/skills/follow/SKILL.md +1 -8
  58. package/kit/workflow/skills/implement/SKILL.md +10 -6
  59. package/kit/workflow/skills/orchestrate/SKILL.md +213 -412
  60. package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
  61. package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
  62. package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
  63. package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
  64. package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
  65. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
  66. package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
  67. package/kit/workflow/skills/orchestrate/references/events.md +86 -0
  68. package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
  69. package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
  70. package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
  71. package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
  72. package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
  73. package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
  74. package/kit/workflow/skills/plan/SKILL.md +6 -5
  75. package/kit/workflow/skills/plan/references/template-English.md +4 -4
  76. package/kit/workflow/skills/plan/references/template-German.md +4 -4
  77. package/package.json +4 -1
  78. package/recipes/README.md +2 -1
  79. package/recipes/sources.md +1 -1
  80. package/schemas/jackherd.schema.json +85 -7
  81. package/src/check/agent-text-reviewers.ts +23 -0
  82. package/src/check/command.ts +2 -0
  83. package/src/check/plan-structure-check.ts +49 -2
  84. package/src/check/plan.ts +8 -1
  85. package/src/check/schema.ts +9 -0
  86. package/src/check/skill-budget-defaults.ts +55 -0
  87. package/src/check/skill-budgets.ts +185 -0
  88. package/src/cli.ts +4 -0
  89. package/src/config/auto-window.ts +8 -0
  90. package/src/config/link-kinds.ts +7 -1
  91. package/src/config/schema.ts +80 -26
  92. package/src/context/fill.ts +12 -0
  93. package/src/context/session-log.ts +43 -1
  94. package/src/lane/close-board.ts +106 -0
  95. package/src/lane/close-check.ts +183 -0
  96. package/src/lane/close-command-line.ts +11 -0
  97. package/src/lane/close-command.ts +217 -0
  98. package/src/lane/close-options.ts +43 -0
  99. package/src/lane/close-survey.ts +83 -0
  100. package/src/lane/close.ts +141 -0
  101. package/src/lane/command.ts +13 -0
  102. package/src/lane-prompt/build.ts +74 -0
  103. package/src/lane-prompt/command.ts +87 -0
  104. package/src/lane-prompt/input.ts +116 -0
  105. package/src/lane-prompt/options.ts +68 -0
  106. package/src/lane-prompt/texts.ts +101 -0
  107. package/src/plan-format/clock.ts +12 -0
  108. package/src/plan-format/dependencies.ts +9 -9
  109. package/src/plan-format/index.ts +1 -0
  110. package/src/plan-format/lanes.ts +30 -5
  111. package/src/plan-format/short-names.ts +71 -0
  112. package/src/plan-format/terminal-text.ts +4 -0
  113. package/src/plan-format/waiting.ts +53 -0
  114. package/src/plans/active-tasks.ts +60 -0
  115. package/src/plans/appointment-rows.ts +59 -0
  116. package/src/plans/ask-coordinator.ts +77 -0
  117. package/src/plans/ask-target.ts +25 -0
  118. package/src/plans/child-stop.ts +51 -0
  119. package/src/plans/closable-records.ts +34 -0
  120. package/src/plans/command.ts +3 -2
  121. package/src/plans/current-phase.ts +17 -7
  122. package/src/plans/current-task.ts +63 -5
  123. package/src/plans/fetch.ts +26 -3
  124. package/src/plans/git.ts +19 -5
  125. package/src/plans/herd-claim.ts +173 -0
  126. package/src/plans/herd-memory.ts +36 -25
  127. package/src/plans/lane-plans.ts +44 -13
  128. package/src/plans/lane-standing.ts +9 -4
  129. package/src/plans/lane-summary.ts +367 -92
  130. package/src/plans/lane-tickets.ts +15 -0
  131. package/src/plans/layout-phase.ts +80 -19
  132. package/src/plans/layout-summary.ts +163 -74
  133. package/src/plans/layout.ts +472 -114
  134. package/src/plans/link-chips.ts +1 -0
  135. package/src/plans/live.ts +119 -10
  136. package/src/plans/phase-ids.ts +8 -0
  137. package/src/plans/phase-models.ts +1 -1
  138. package/src/plans/phase-ranges.ts +45 -0
  139. package/src/plans/phase-source.ts +15 -0
  140. package/src/plans/plan-cache.ts +28 -0
  141. package/src/plans/plan-heads.ts +54 -0
  142. package/src/plans/pull-requests.ts +23 -2
  143. package/src/plans/question-records.ts +48 -0
  144. package/src/plans/questions-read.ts +13 -0
  145. package/src/plans/read-cache.ts +8 -0
  146. package/src/plans/record-id.ts +4 -0
  147. package/src/plans/restart-decision.ts +13 -4
  148. package/src/plans/selection.ts +117 -34
  149. package/src/plans/sent-files-read.ts +13 -0
  150. package/src/plans/sent-files.ts +114 -0
  151. package/src/plans/settings.ts +2 -1
  152. package/src/plans/signalled-phase.ts +42 -0
  153. package/src/plans/signals-read.ts +62 -0
  154. package/src/plans/snapshot.ts +279 -60
  155. package/src/plans/sprint-export.ts +11 -11
  156. package/src/plans/sprint-page.ts +5 -5
  157. package/src/plans/subagent-phases.ts +95 -0
  158. package/src/plans/subagent-tasks.ts +51 -0
  159. package/src/plans/subagents-read.ts +131 -0
  160. package/src/plans/subagents.ts +190 -0
  161. package/src/plans/supervisor.ts +14 -4
  162. package/src/plans/symbols.ts +3 -0
  163. package/src/plans/task-name.ts +34 -0
  164. package/src/plans/texts.ts +36 -14
  165. package/src/plans/unlisted-lanes.ts +10 -5
  166. package/src/plans/view.ts +20 -12
  167. package/src/plans/waiting-click.ts +231 -0
  168. package/src/plans/waiting-records.ts +39 -0
  169. package/src/plans/waiting-window.ts +8 -0
  170. package/src/preview/command.ts +105 -0
  171. package/src/preview/embed.ts +209 -0
  172. package/src/preview/images.ts +89 -0
  173. package/src/preview/open.ts +12 -0
  174. package/src/preview/options.ts +32 -0
  175. package/src/preview/page.ts +89 -0
  176. package/src/preview/render.ts +41 -0
  177. package/src/settled/board-row.ts +1 -2
  178. package/src/shared/is-record.ts +2 -0
  179. package/src/shared/private-location.ts +89 -0
  180. package/src/shared/private-records.ts +48 -0
  181. package/src/shared/stat-cache.ts +48 -0
  182. package/src/sync/kit-source.ts +29 -10
  183. package/src/sync/kit.ts +2 -0
  184. package/src/sync/manifest.ts +2 -0
  185. package/src/sync/plan-ask-rules.ts +94 -0
  186. package/src/sync/plan-hooks.ts +2 -0
  187. package/src/sync/render.ts +3 -3
  188. package/src/sync/settings-hooks.ts +4 -1
  189. package/src/sync/sync-plan.ts +1 -0
  190. package/src/sync/sync.ts +24 -3
  191. package/src/task-signals/agent.ts +15 -0
  192. package/src/task-signals/claude-read.ts +89 -0
  193. package/src/task-signals/claude-signals.ts +112 -0
  194. package/src/task-signals/command.ts +71 -0
  195. package/src/task-signals/fixtures/session.jsonl +7 -0
  196. package/src/task-signals/matcher.ts +96 -0
  197. package/src/task-signals/record-store.ts +40 -0
  198. package/src/task-signals/record.ts +68 -0
  199. package/src/task-signals/text.ts +13 -0
  200. package/src/task-signals/types.ts +14 -0
  201. package/src/terminal/open.ts +13 -6
  202. package/src/terminal/text.ts +16 -4
  203. package/src/terminal/width.ts +2 -1
  204. package/src/version/skill-diff.ts +46 -0
  205. package/src/vocabulary/index.ts +44 -6
  206. package/src/waiting/command.ts +50 -0
  207. package/src/watch/appointments.ts +2 -4
  208. package/src/watch/approvals.ts +2 -2
  209. package/src/watch/asking-lanes.ts +40 -0
  210. package/src/watch/base-tip.ts +11 -0
  211. package/src/watch/command.ts +401 -90
  212. package/src/watch/failed-tests.ts +56 -10
  213. package/src/watch/failure-read.ts +18 -6
  214. package/src/watch/failures.ts +29 -5
  215. package/src/watch/feedback.ts +1 -1
  216. package/src/watch/gh-process.ts +9 -3
  217. package/src/watch/heartbeat-progress.ts +6 -18
  218. package/src/watch/json-guards.ts +0 -3
  219. package/src/watch/kit-update.ts +28 -9
  220. package/src/watch/lane-fill.ts +1 -1
  221. package/src/watch/lane-model.ts +1 -1
  222. package/src/watch/lane-name.ts +6 -0
  223. package/src/watch/lane-parallel.ts +1 -1
  224. package/src/watch/lane-progress.ts +58 -14
  225. package/src/watch/lane-report-file.ts +20 -5
  226. package/src/watch/lane-report.ts +37 -5
  227. package/src/watch/lane-rest.ts +28 -0
  228. package/src/watch/lane-sources.ts +31 -3
  229. package/src/watch/main-checkout.ts +34 -0
  230. package/src/watch/plan-badges-read.ts +13 -7
  231. package/src/watch/plan-badges.ts +9 -2
  232. package/src/watch/previews.ts +1 -1
  233. package/src/watch/pull-requests.ts +8 -3
  234. package/src/watch/reference-paths.ts +87 -0
  235. package/src/watch/running-subagents.ts +43 -0
  236. package/src/watch/skill-change.ts +124 -32
  237. package/src/watch/source-change.ts +48 -0
  238. package/src/watch/source-path.ts +1 -1
  239. package/src/watch/sources.ts +7 -4
  240. package/src/watch/stream-output.ts +14 -8
  241. package/src/watch/task-parallel-auto.ts +208 -0
  242. package/src/watch/task-parallel-data-paths.ts +46 -0
  243. package/src/watch/task-parallel-input.ts +108 -0
  244. package/src/watch/task-parallel-read.ts +151 -0
  245. package/src/watch/task-parallel-touched.ts +39 -0
  246. package/src/watch/task-parallel.ts +235 -0
  247. package/src/watch/task-signal-missing.ts +69 -0
  248. package/src/watch/task-signal-read.ts +29 -0
  249. package/src/watch/texts.ts +10 -0
  250. package/src/watch/update-available.ts +6 -3
  251. package/src/watch/waiting-line.ts +19 -0
  252. package/src/watch/watch-events.ts +36 -1
  253. package/src/watch/watcher-lock.ts +25 -29
  254. package/src/watch/workflow-runs.ts +2 -1
  255. package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
  256. package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
  257. package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
  258. package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
  259. package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
  260. package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
  261. package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
@@ -0,0 +1,141 @@
1
+ import { twoDigits } from '../plan-format/clock'
2
+ import { terminalSafe } from '../plan-format/terminal-text'
3
+ import { errorText } from '../plans/error-text'
4
+ import { PLAN_LANGUAGE, type PlanLanguage } from '../vocabulary'
5
+ import { closedMark, historyDate, withHistoryRow, withLaneClosed } from './close-board'
6
+ import { BOARD_LANE, type ClosableLane, type CloseFacts, closeVerdict } from './close-check'
7
+ import { LANE_CLOSE_COMMAND } from './close-command-line'
8
+
9
+ /** The facts the check reads, and the herdr workspace the worktree is open in, null when none is. */
10
+ export type CloseSurvey = { facts: CloseFacts; workspace: string | null }
11
+
12
+ /** What `jh lane close` was asked for; `worktree` comes from the settings and the lane's name, never from a reply. */
13
+ export type CloseRequest = {
14
+ lane: string
15
+ worktree: string
16
+ /** The board as the output names it, relative to the main checkout. */
17
+ boardPath: string
18
+ dryRun: boolean
19
+ now: Date
20
+ language: PlanLanguage
21
+ }
22
+
23
+ /** The I/O `closeLane` drives; every port but `survey` and the printers throws when it fails. */
24
+ export type ClosePorts = {
25
+ survey: () => Promise<CloseSurvey>
26
+ /** Ends the agent in this pane and returns once the terminal no longer lists its session. */
27
+ exitAgent: (pane: string) => Promise<void>
28
+ /** Removes the worktree without force, then closes the terminal workspace `workspace` names, if any. */
29
+ removeWorktree: (worktree: string, workspace: string | null) => void
30
+ /** Deletes the branch only while it still points to `head`, the commit the check compared. */
31
+ deleteBranch: (branch: string, head: string) => void
32
+ readBoard: () => string
33
+ writeBoard: (text: string) => void
34
+ print: (line: string) => void
35
+ printError: (line: string) => void
36
+ }
37
+
38
+ type ClosedLane = { lane: string } & ClosableLane
39
+
40
+ type Step = { label: string; run: () => Promise<void> | void }
41
+
42
+ /** A merge commit as the history row cites it. */
43
+ const SHORT_COMMIT_LENGTH = 7
44
+
45
+ const HISTORY_TEXT: Record<PlanLanguage, (closed: ClosedLane, mergeCommit: string) => string> = {
46
+ [PLAN_LANGUAGE.de]: ({ lane, branch, pullRequest }, mergeCommit) =>
47
+ `Spur \`${lane}\` abgeräumt mit \`${LANE_CLOSE_COMMAND}\`: PR #${pullRequest.number} gemergt (\`${mergeCommit}\`), Baum gleich Merge, Worktree entfernt, lokaler Branch \`${branch}\` gelöscht`,
48
+ [PLAN_LANGUAGE.en]: ({ lane, branch, pullRequest }, mergeCommit) =>
49
+ `Lane \`${lane}\` cleaned up with \`${LANE_CLOSE_COMMAND}\`: PR #${pullRequest.number} merged (\`${mergeCommit}\`), tree equal to the merge, worktree removed, local branch \`${branch}\` deleted`,
50
+ }
51
+
52
+ const clockOf = (time: Date): string => `${twoDigits(time.getHours())}:${twoDigits(time.getMinutes())}`
53
+
54
+ /** The history row's text: the time, then what was closed and proven, in the board's language. */
55
+ export const historyText = (closed: ClosedLane, request: Pick<CloseRequest, 'now' | 'language'>): string =>
56
+ `${clockOf(request.now)}: ${HISTORY_TEXT[request.language](closed, closed.pullRequest.mergeCommit.slice(0, SHORT_COMMIT_LENGTH))}`
57
+
58
+ /** The board with the lane's rows marked and the history row on top. */
59
+ const closedBoard = (board: string, closed: ClosedLane, request: CloseRequest): string =>
60
+ withHistoryRow(
61
+ withLaneClosed(board, closed.lane, closedMark(request.now, request.language), request.language),
62
+ historyDate(request.now),
63
+ historyText(closed, request),
64
+ request.language,
65
+ )
66
+
67
+ const boardLabel = (facts: CloseFacts, request: CloseRequest): string =>
68
+ facts.boardLane === BOARD_LANE.closed
69
+ ? `add a history row in ${request.boardPath}, whose rows mark ${request.lane} already`
70
+ : `mark the rows of ${request.lane} "${closedMark(request.now, request.language)}" and add a history row in ${request.boardPath}`
71
+
72
+ /** The removal steps after the agent is gone, in the order that never leaves the board claiming more than was done. */
73
+ const removalSteps = (closed: ClosedLane, survey: CloseSurvey, request: CloseRequest, ports: ClosePorts): Step[] => {
74
+ const { workspace, facts } = survey
75
+ const place = workspace === null ? '' : ` and close workspace ${workspace}`
76
+ return [
77
+ {
78
+ label: `remove worktree ${request.worktree}${place}`,
79
+ run: () => ports.removeWorktree(request.worktree, workspace),
80
+ },
81
+ { label: `delete branch ${closed.branch}`, run: () => ports.deleteBranch(closed.branch, closed.head) },
82
+ { label: boardLabel(facts, request), run: () => ports.writeBoard(closedBoard(ports.readBoard(), closed, request)) },
83
+ ]
84
+ }
85
+
86
+ const exitSteps = ({ facts }: CloseSurvey, request: CloseRequest, ports: ClosePorts): Step[] => {
87
+ const { agent } = facts
88
+ if (agent === null) return []
89
+ return [
90
+ { label: `exit the agent of ${request.lane} and wait until it is gone`, run: () => ports.exitAgent(agent.pane) },
91
+ ]
92
+ }
93
+
94
+ /** The lane closable from `survey`, or null after printing every reason against it under `heading`. */
95
+ const checked = (survey: CloseSurvey, heading: string, request: CloseRequest, ports: ClosePorts): ClosedLane | null => {
96
+ const verdict = closeVerdict(survey.facts)
97
+ if (verdict.kind === 'closable') return { lane: request.lane, ...verdict }
98
+ ports.printError(heading)
99
+ for (const refusal of verdict.refusals) ports.printError(` ${terminalSafe(refusal)}`)
100
+ return null
101
+ }
102
+
103
+ /** Runs the steps in order, one line each; at the first failure, names it and every step left undone. */
104
+ const runSteps = async (steps: readonly Step[], ports: ClosePorts): Promise<boolean> => {
105
+ for (const [index, step] of steps.entries()) {
106
+ try {
107
+ await step.run()
108
+ ports.print(`done: ${step.label}`)
109
+ } catch (error) {
110
+ ports.printError(`failed: ${step.label}: ${terminalSafe(errorText(error))}`)
111
+ for (const left of steps.slice(index + 1)) ports.printError(`not done: ${left.label}`)
112
+ return false
113
+ }
114
+ }
115
+ return true
116
+ }
117
+
118
+ /**
119
+ * Closes a lane in one run, or refuses with every reason and changes nothing. Checks first (`closeVerdict`); a dry run
120
+ * stops there and prints the steps. Otherwise it ends the lane's agent, checks again, since the agent may have written
121
+ * until it exited, then removes the worktree and its workspace, deletes the local branch, and marks the board last, so
122
+ * the board never records a removal that failed. Exit 0 closed or dry run, 1 refused or failed.
123
+ */
124
+ export const closeLane = async (request: CloseRequest, ports: ClosePorts): Promise<number> => {
125
+ const first = await ports.survey()
126
+ const closed = checked(first, `not closing ${request.lane}:`, request, ports)
127
+ if (closed === null) return 1
128
+ const exit = exitSteps(first, request, ports)
129
+ if (request.dryRun) {
130
+ ports.print(`dry run, nothing changed; closing ${request.lane} would:`)
131
+ for (const step of [...exit, ...removalSteps(closed, first, request, ports)]) ports.print(` ${step.label}`)
132
+ return 0
133
+ }
134
+ if (!(await runSteps(exit, ports))) return 1
135
+ const settled = exit.length === 0 ? first : await ports.survey()
136
+ const late = checked(settled, `not closing ${request.lane} after its agent exited:`, request, ports)
137
+ if (late === null) return 1
138
+ if (!(await runSteps(removalSteps(late, settled, request, ports), ports))) return 1
139
+ ports.print(`closed ${request.lane}`)
140
+ return 0
141
+ }
@@ -0,0 +1,13 @@
1
+ import { run as runPrompt } from '../lane-prompt/command'
2
+
3
+ const CLOSE = 'close'
4
+
5
+ /**
6
+ * `jh lane close …` closes a lane, everything else is `jh lane prompt …`. `close` must come first, so the ask rule
7
+ * `jh lane close` always sees it; `jh lane --board b.md close x` reaches the prompt parser and is refused there.
8
+ */
9
+ export const run = async (args: string[], cwd: string = process.cwd()): Promise<number> => {
10
+ if (args[0] !== CLOSE) return runPrompt(args, cwd)
11
+ const { run: runClose } = await import('./close-command')
12
+ return runClose(args.slice(1), cwd)
13
+ }
@@ -0,0 +1,74 @@
1
+ import { phaseLabel } from '../plan-format/lanes'
2
+ import { LANE_PROMPT_OPENING, PLAN_VOCABULARY, type PlanLanguage } from '../vocabulary'
3
+ import { laneReportFile } from '../watch/lane-report-file'
4
+ import { LANE_PROMPT_TEXTS, type LanePromptTexts } from './texts'
5
+
6
+ /** A phase of the lane with the paths its row of the dependency table writes (`''` is the whole repository); null without a row. */
7
+ export type LanePhase = { id: string; writes: string[] | null }
8
+
9
+ /** A plan the lane works on, by its path from the repository root, with the lane's phases in it. */
10
+ export type LanePlan = { path: string; phases: LanePhase[] }
11
+
12
+ /**
13
+ * Everything a lane prompt is made of. `port` is null when the board gives the lane none; `handover` names the
14
+ * hand-over file a restarted lane reads first (`/orchestrate` § 8), null for a fresh start.
15
+ */
16
+ export type LanePromptInput = {
17
+ language: PlanLanguage
18
+ lane: string
19
+ plans: LanePlan[]
20
+ port: number | null
21
+ handover: string | null
22
+ }
23
+
24
+ const writesText = (language: PlanLanguage, texts: LanePromptTexts, writes: string[] | null): string => {
25
+ if (writes === null) return texts.noWrites
26
+ const paths = writes.map((path) => (path === '' ? texts.wholeRepository : `\`${path}\``)).join(', ')
27
+ return `${PLAN_VOCABULARY[language].columns.writes}: ${paths}`
28
+ }
29
+
30
+ const phaseText = (language: PlanLanguage, texts: LanePromptTexts, phase: LanePhase): string =>
31
+ `\`${PLAN_VOCABULARY[language].phase} ${phaseLabel(phase.id)}\` (${writesText(language, texts, phase.writes)})`
32
+
33
+ const planLines = (language: PlanLanguage, texts: LanePromptTexts, plan: LanePlan): string[] => [
34
+ `- \`${plan.path}\``,
35
+ ...plan.phases.map((phase) => ` - ${phaseText(language, texts, phase)}`),
36
+ ]
37
+
38
+ const ruleLines = (input: LanePromptInput, texts: LanePromptTexts): string[] => [
39
+ input.port === null ? texts.noPort : texts.port(input.port),
40
+ texts.blocking,
41
+ texts.mustNot,
42
+ texts.commit,
43
+ texts.report(laneReportFile(input.lane)),
44
+ texts.textBlock,
45
+ texts.secret,
46
+ texts.askRule,
47
+ texts.approvalGate,
48
+ texts.helper(`${input.lane}-${texts.helperPurpose}`),
49
+ ]
50
+
51
+ /**
52
+ * The prompt that starts a lane, in the plan language: a first line that names the lane (the words the ask hint hook
53
+ * and the project's lane exception recognise), the plans with their phases as `Phase P<n>` and each phase's write
54
+ * set, the rules of `/orchestrate` § 1 that `/implement` does not carry already, and the start: `/implement`, or the
55
+ * hand-over file after a restart.
56
+ */
57
+ export const buildLanePrompt = (input: LanePromptInput): string => {
58
+ const texts = LANE_PROMPT_TEXTS[input.language]
59
+ return [
60
+ `${LANE_PROMPT_OPENING[input.language]} ${input.lane}. ${texts.answersTo}`,
61
+ '',
62
+ texts.phasesIntro,
63
+ '',
64
+ ...input.plans.flatMap((plan) => planLines(input.language, texts, plan)),
65
+ '',
66
+ texts.editInside,
67
+ texts.writeTools,
68
+ '',
69
+ ...ruleLines(input, texts).map((line) => `- ${line}`),
70
+ '',
71
+ input.handover === null ? texts.start : texts.restart(input.handover),
72
+ '',
73
+ ].join('\n')
74
+ }
@@ -0,0 +1,87 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, relative, resolve } from 'node:path'
3
+ import { claudePath, HANDOVER_DIRECTORY } from '../claude/paths'
4
+ import { boardFor } from '../plans/board-pick'
5
+ import { mainCheckout, worktreeRoot } from '../plans/git'
6
+ import { boardRepoPathOf, readSettings, worktreeOf } from '../plans/settings'
7
+ import { asksForHelp } from '../preview/options'
8
+ import { buildLanePrompt } from './build'
9
+ import { type BoardSource, type LanePromptRequest, lanePromptInputOf, type ReadPlan } from './input'
10
+ import { LANE_PROMPT_USAGE, type LanePromptOptions, parseLanePromptOptions } from './options'
11
+
12
+ /**
13
+ * What `lanePromptInputOf` reads, located from the command line, and the main checkout the default file goes to;
14
+ * `readLanePlan` reads from the lane's worktree, null for every plan while it has none.
15
+ */
16
+ type PromptSources = {
17
+ request: LanePromptRequest
18
+ board: BoardSource
19
+ readPlan: ReadPlan
20
+ readLanePlan: ReadPlan
21
+ root: string
22
+ }
23
+
24
+ /** Where the prompt goes without `--out`: the hand-over directory of the main checkout, which git ignores. */
25
+ export const defaultPromptFile = (root: string, lane: string): string =>
26
+ join(root, claudePath(HANDOVER_DIRECTORY), `${lane}-prompt.md`)
27
+
28
+ /** The first of `roots` that holds `path` as a regular file, read; null when none does. */
29
+ const readFirstIn =
30
+ (roots: readonly string[]) =>
31
+ (path: string): string | null => {
32
+ const file = roots
33
+ .map((root) => join(root, path))
34
+ .find((candidate) => existsSync(candidate) && statSync(candidate).isFile())
35
+ return file === undefined ? null : readFileSync(file, 'utf8')
36
+ }
37
+
38
+ /** The lane's request, its board and the plan reader, from the checkouts of `cwd`; prints the board pick's notice. */
39
+ const promptSourcesOf = (options: LanePromptOptions, cwd: string): PromptSources => {
40
+ const checkout = worktreeRoot(cwd)
41
+ const root = mainCheckout(cwd)
42
+ const settings = readSettings([checkout, root])
43
+ const boardPath = options.boardPath === null ? null : resolve(cwd, options.boardPath)
44
+ const board = boardFor(boardPath, [root, checkout], settings, () => [options.lane])
45
+ if (board.notice) console.error(board.notice)
46
+ const directory = dirname(boardRepoPathOf(root, settings, board.file))
47
+ return {
48
+ request: { lane: options.lane, handover: options.handover, language: settings.planLanguage },
49
+ board: { text: readFileSync(board.file, 'utf8'), directory, path: relative(cwd, board.file) },
50
+ readPlan: readFirstIn([root, checkout]),
51
+ readLanePlan: readFirstIn([worktreeOf(root, settings, options.lane)]),
52
+ root,
53
+ }
54
+ }
55
+
56
+ const writePrompt = (out: string, prompt: string): void => {
57
+ mkdirSync(dirname(out), { recursive: true })
58
+ writeFileSync(out, prompt)
59
+ }
60
+
61
+ /**
62
+ * `jh lane prompt <lane> [--board <board.md>] [--out <file>] [--handover <file>]`: writes the lane's prompt, built
63
+ * from its board row and its plan in the plan language, and prints the file's path, so the coordinator sends it with
64
+ * `herdr agent prompt <lane> "$(cat <file>)"`. `--help` prints the usage. Exit 0 written, 1 refused or failed.
65
+ */
66
+ export const run = async (args: string[], cwd: string = process.cwd()): Promise<number> => {
67
+ if (asksForHelp(args)) {
68
+ console.log(LANE_PROMPT_USAGE)
69
+ return 0
70
+ }
71
+ try {
72
+ const options = parseLanePromptOptions(args)
73
+ const { request, board, readPlan, readLanePlan, root } = promptSourcesOf(options, cwd)
74
+ const result = lanePromptInputOf(request, board, readPlan, readLanePlan)
75
+ if (result.kind === 'refused') {
76
+ console.error(result.reason)
77
+ return 1
78
+ }
79
+ const out = options.out === null ? defaultPromptFile(root, options.lane) : resolve(cwd, options.out)
80
+ writePrompt(out, buildLanePrompt(result.input))
81
+ console.log(out)
82
+ return 0
83
+ } catch (error) {
84
+ console.error(error instanceof Error ? error.message : String(error))
85
+ return 1
86
+ }
87
+ }
@@ -0,0 +1,116 @@
1
+ import { type PhaseDependency, parseDependencyTable, writeSetOf } from '../plan-format/dependencies'
2
+ import { parseSprintLanes, phaseLabel, resolvePlanPath, type SprintLane } from '../plan-format/lanes'
3
+ import { parsePlanPhases } from '../plan-format/phases'
4
+ import { isPhaseFinished } from '../plan-format/progress'
5
+ import type { PlanLanguage } from '../vocabulary'
6
+ import type { LanePhase, LanePlan, LanePromptInput } from './build'
7
+
8
+ /** The lane to prompt, the hand-over file of a restart (null for a fresh start) and the plan language. */
9
+ export type LanePromptRequest = { lane: string; handover: string | null; language: PlanLanguage }
10
+
11
+ /**
12
+ * The board's text, its directory from the repository root, which its plan links are relative to, and the path a
13
+ * refusal names it by.
14
+ */
15
+ export type BoardSource = { text: string; directory: string; path: string }
16
+
17
+ /** A plan's text by its path from the repository root; null when it cannot be read. */
18
+ export type ReadPlan = (path: string) => string | null
19
+
20
+ export type LanePromptResult = { kind: 'prompt'; input: LanePromptInput } | { kind: 'refused'; reason: string }
21
+
22
+ type PlanSteps = { path: string; ids: string[] }
23
+
24
+ type Refusal = { reason: string }
25
+
26
+ type PlanOutcome = { plan: LanePlan } | Refusal
27
+
28
+ type PlansOutcome = { plans: LanePlan[] } | Refusal
29
+
30
+ const refused = (reason: string): LanePromptResult => ({ kind: 'refused', reason })
31
+
32
+ /** The lane's plans by path in board order, each with the phase ids its rows name, rows of one plan merged. */
33
+ const planStepsOf = (lane: SprintLane, directory: string): PlanSteps[] => {
34
+ const byPath = new Map<string, string[]>()
35
+ for (const step of lane.steps) {
36
+ if (step.planHref === null || step.phaseIds.length === 0) continue
37
+ const path = resolvePlanPath(directory, step.planHref)
38
+ byPath.set(path, [...new Set([...(byPath.get(path) ?? []), ...step.phaseIds])])
39
+ }
40
+ return [...byPath].map(([path, ids]) => ({ path, ids }))
41
+ }
42
+
43
+ /** The write set of the phase `id`; null without a row, for an unknown target or a cycle. */
44
+ const phaseWriteSet = (rows: readonly PhaseDependency[], id: string): string[] | null => {
45
+ const row = rows.find((candidate) => candidate.id === id)
46
+ return row ? writeSetOf(row, new Map(rows.map((candidate) => [candidate.id, candidate]))) : null
47
+ }
48
+
49
+ /** A plan's text on the lane branch (null without a worktree or a copy there) and in the main checkout. */
50
+ type PlanTexts = { lane: string | null; main: string | null }
51
+
52
+ const finishedIdsOf = (text: string | null, language: PlanLanguage): Set<string> =>
53
+ new Set(
54
+ text === null ? [] : parsePlanPhases(text, language).flatMap((phase) => (isPhaseFinished(phase) ? [phase.id] : [])),
55
+ )
56
+
57
+ /**
58
+ * The plan's open named phases, read from the lane branch when it has a copy, which holds the phases the lane wrote
59
+ * and the ticks it set; a phase finished in either copy is left out, so a lane branch behind the main checkout does not
60
+ * bring back what another lane finished.
61
+ */
62
+ const lanePlanOf = (steps: PlanSteps, texts: PlanTexts, text: string, language: PlanLanguage): PlanOutcome => {
63
+ const phases = parsePlanPhases(text, language)
64
+ const named = steps.ids.map((id) => ({ id, phase: phases.find((phase) => phase.id === id) }))
65
+ const missing = named.find(({ phase }) => phase === undefined)
66
+ if (missing) return { reason: `${steps.path} has no phase ${phaseLabel(missing.id)}` }
67
+ const rows = parseDependencyTable(text, language) ?? []
68
+ const finishedInMain = finishedIdsOf(texts.main, language)
69
+ const open = named.filter(
70
+ ({ id, phase }) => phase !== undefined && !isPhaseFinished(phase) && !finishedInMain.has(id),
71
+ )
72
+ const lanePhases: LanePhase[] = open.map(({ id }) => ({ id, writes: phaseWriteSet(rows, id) }))
73
+ return { plan: { path: steps.path, phases: lanePhases } }
74
+ }
75
+
76
+ const plansOf = (
77
+ steps: readonly PlanSteps[],
78
+ readers: { main: ReadPlan; lane: ReadPlan },
79
+ language: PlanLanguage,
80
+ ): PlansOutcome => {
81
+ const plans: LanePlan[] = []
82
+ for (const step of steps) {
83
+ const texts = { lane: readers.lane(step.path), main: readers.main(step.path) }
84
+ const text = texts.lane ?? texts.main
85
+ if (text === null) return { reason: `cannot read the plan ${step.path}` }
86
+ const outcome = lanePlanOf(step, texts, text, language)
87
+ if ('reason' in outcome) return outcome
88
+ if (outcome.plan.phases.length > 0) plans.push(outcome.plan)
89
+ }
90
+ return { plans }
91
+ }
92
+
93
+ /**
94
+ * What the lane's prompt is built from: its open row of the board's lanes table (port, plans, phases), each named
95
+ * phase that is not finished, and the write set of each from its plan's dependency table. A plan is read from the lane
96
+ * branch (`readLanePlan`) when it has a copy, else from the main checkout (`readPlan`). Refused, with the reason,
97
+ * for a lane without a row, a closed lane, rows that name no phase, a plan that cannot be read or lacks a phase the
98
+ * row names, and a lane whose phases are all finished.
99
+ */
100
+ export const lanePromptInputOf = (
101
+ request: LanePromptRequest,
102
+ board: BoardSource,
103
+ readPlan: ReadPlan,
104
+ readLanePlan: ReadPlan = () => null,
105
+ ): LanePromptResult => {
106
+ const sprintLane = parseSprintLanes(board.text, request.language).find((candidate) => candidate.name === request.lane)
107
+ if (!sprintLane) return refused(`no row for lane ${request.lane} in ${board.path}`)
108
+ if (sprintLane.closed) return refused(`lane ${request.lane} is closed in ${board.path}`)
109
+ const steps = planStepsOf(sprintLane, board.directory)
110
+ if (steps.length === 0) return refused(`the row of lane ${request.lane} in ${board.path} links no plan with a phase`)
111
+ const found = plansOf(steps, { main: readPlan, lane: readLanePlan }, request.language)
112
+ if ('reason' in found) return refused(found.reason)
113
+ if (found.plans.length === 0) return refused(`every phase of lane ${request.lane} is finished`)
114
+ const { lane, handover, language } = request
115
+ return { kind: 'prompt', input: { language, lane, plans: found.plans, port: sprintLane.port, handover } }
116
+ }
@@ -0,0 +1,68 @@
1
+ import { CLI_NAME } from '../version/release'
2
+ import { SAFE_LANE_NAME } from '../watch/lane-name'
3
+
4
+ /** What `jh lane prompt` was asked for; null where an option was left out. */
5
+ export type LanePromptOptions = { lane: string; boardPath: string | null; out: string | null; handover: string | null }
6
+
7
+ const VALUE_OPTIONS = ['--board', '--out', '--handover'] as const
8
+
9
+ type ValueOption = (typeof VALUE_OPTIONS)[number]
10
+
11
+ const SUBCOMMAND = 'prompt'
12
+
13
+ export const LANE_PROMPT_USAGE = `usage: ${CLI_NAME} lane prompt <lane> [--board <board.md>] [--out <file>] [--handover <file>]
14
+
15
+ Writes the prompt that starts the lane, from its row in the board and its plan, and prints the file's path.
16
+ --board <board.md> the board to read; without it the board is picked as for ${CLI_NAME} herd
17
+ --out <file> where to write the prompt; default .claude/handover/<lane>-prompt.md in the main checkout
18
+ --handover <file> a restart after a hand-over: the lane reads this file first and continues with its next step`
19
+
20
+ const usageError = (message: string): Error => new Error(`${message}\n${LANE_PROMPT_USAGE}`)
21
+
22
+ const isValueOption = (arg: string): arg is ValueOption => (VALUE_OPTIONS as readonly string[]).includes(arg)
23
+
24
+ const splitArguments = (args: readonly string[]): { values: Map<ValueOption, string>; words: string[] } => {
25
+ const values = new Map<ValueOption, string>()
26
+ const words: string[] = []
27
+ for (let i = 0; i < args.length; i += 1) {
28
+ const arg = args[i]
29
+ if (!arg.startsWith('--')) {
30
+ words.push(arg)
31
+ continue
32
+ }
33
+ if (!isValueOption(arg)) throw usageError(`unknown option ${arg}`)
34
+ const value = args[i + 1]
35
+ if (value === undefined || value.startsWith('--')) throw usageError(`${arg} needs a value`)
36
+ values.set(arg, value)
37
+ i += 1
38
+ }
39
+ return { values, words }
40
+ }
41
+
42
+ const laneOf = (words: readonly string[]): string => {
43
+ const [subcommand, ...lanes] = words
44
+ if (subcommand !== SUBCOMMAND)
45
+ throw usageError(subcommand ? `unknown subcommand ${subcommand}` : 'the subcommand is required')
46
+ if (lanes.length === 0) throw usageError('the lane is required')
47
+ if (lanes.length > 1) throw usageError(`one lane at most, got ${lanes.join(' ')}`)
48
+ if (!SAFE_LANE_NAME.test(lanes[0])) throw usageError(`${lanes[0]} is no lane name (letters, digits, . _ -)`)
49
+ return lanes[0]
50
+ }
51
+
52
+ /** The hand-over path as the prompt quotes it in backticks: one line without a backtick. */
53
+ const handoverOf = (value: string | undefined): string | null => {
54
+ if (value === undefined) return null
55
+ if (/[`\r\n]/.test(value)) throw usageError('--handover needs a path without backticks or line breaks')
56
+ return value
57
+ }
58
+
59
+ /** Reads `prompt <lane> [--board …] [--out …] [--handover …]` in any order; throws on a wrong command line. */
60
+ export const parseLanePromptOptions = (args: readonly string[]): LanePromptOptions => {
61
+ const { values, words } = splitArguments(args)
62
+ return {
63
+ lane: laneOf(words),
64
+ boardPath: values.get('--board') ?? null,
65
+ out: values.get('--out') ?? null,
66
+ handover: handoverOf(values.get('--handover')),
67
+ }
68
+ }
@@ -0,0 +1,101 @@
1
+ import { PLAN_LANGUAGE, type PlanLanguage } from '../vocabulary'
2
+
3
+ /**
4
+ * The sentences of a lane prompt in one language, after `/orchestrate` § 1. The opening words and the column name of
5
+ * a write set come from the vocabulary; the report file keeps its `## Questions` heading in every language, since the
6
+ * watcher finds the section by that word.
7
+ */
8
+ export type LanePromptTexts = {
9
+ answersTo: string
10
+ phasesIntro: string
11
+ noWrites: string
12
+ wholeRepository: string
13
+ editInside: string
14
+ writeTools: string
15
+ port: (port: number) => string
16
+ noPort: string
17
+ blocking: string
18
+ mustNot: string
19
+ commit: string
20
+ report: (reportPath: string) => string
21
+ textBlock: string
22
+ secret: string
23
+ askRule: string
24
+ approvalGate: string
25
+ helper: (helperName: string) => string
26
+ helperPurpose: string
27
+ start: string
28
+ restart: (handoverPath: string) => string
29
+ }
30
+
31
+ export const LANE_PROMPT_TEXTS: Record<PlanLanguage, LanePromptTexts> = {
32
+ [PLAN_LANGUAGE.en]: {
33
+ answersTo: 'You answer to the coordinator, not to the user.',
34
+ phasesIntro: 'Your plan and phases:',
35
+ noWrites: 'no Writes set in the plan',
36
+ wholeRepository: 'the whole repository',
37
+ editInside:
38
+ 'Nothing outside the Writes set of your phases is edited, except the task lines of your phases in the plan: tick a finished one right away, by an Edit of exactly that line.',
39
+ writeTools:
40
+ 'Write code and tests with Write and Edit, never through a here-document or a script in Bash: such a call shows the reviewer no diff, and an unquoted or unreadable one can stop at a guard.',
41
+ port: (port) =>
42
+ `Port ${port}: the coordinator runs your dev server in its own pane; look at your visible changes there (\`/implement\` Step 4b) and never start a server in your own shell.`,
43
+ noPort:
44
+ 'No port: this lane has no dev server of its own. Never start a server in your own shell; ask the coordinator when you need one.',
45
+ blocking:
46
+ 'Mark every question of yours `blocking` or `not blocking`. A blocking one stops the step it decides; for a not blocking one leave a `TODO` at its spot and work on, building neither your recommendation nor another option as decided.',
47
+ mustNot:
48
+ 'Without the user’s approval you push no database migration and write nothing against a shared environment (stage, production); you never run `git reset` or `git stash` on shared refs.',
49
+ commit: 'Ask before every commit (`/pre-commit`); no push, PR or merge without approval.',
50
+ report: (reportPath) =>
51
+ `The report at the end of each step goes to \`${reportPath}\` in the worktree, in the short form: one line of diff stat, one line per check with its numbers, open points, questions as a text block under the heading \`## Questions\` (whatever the report’s language: the watcher finds the section by that word and shows it whole); everything else is in the plan, by reference.`,
52
+ textBlock:
53
+ 'Every question to the coordinator is a text block at the end of your turn, never a dialog (`AskUserQuestion`), also where a skill or a project rule asks for one, the commit confirmation of `/pre-commit` included: the question with the situation, the problem and what each option leads to, in everyday words (the user may not know your terms), the options, your recommendation, the evidence at `file:line`.',
54
+ secret:
55
+ 'A value only the user knows (a secret, password, token or one-time code) never passes through you, your arguments, your output or a file you write: ask the coordinator for the step as a question.',
56
+ askRule:
57
+ 'A tool that asks for approval on every call (an ask rule, such as a database query tool or a project CLI) gets as few calls as possible: bundle the steps into one call (one script, one query) instead of one call per step.',
58
+ approvalGate:
59
+ 'A call that would open a permission dialog waits for the coordinator: the approval gate holds it and the coordinator asks the user. Denied with “approval pending”, retry the same call once the coordinator says it is approved; denied by the user, do not retry it.',
60
+ helper: (helperName) =>
61
+ `A helper agent in a pane of your own (a measurement, a probe session) is announced to the coordinator before it starts, as a question at the end of your turn: what it does, how long, and which temporary settings it uses. Once approved, name it \`${helperName}\` (\`herdr agent rename\`), so the watcher watches it with your lane, and close it yourself when it is done: \`/exit\`, then \`herdr pane close\`, and remove every temporary file it needed.`,
62
+ helperPurpose: '<purpose>',
63
+ start: 'Start: run `/implement` for your phases, in the order above.',
64
+ restart: (handoverPath) => `Start: read \`${handoverPath}\` first and continue with the next step it names.`,
65
+ },
66
+ [PLAN_LANGUAGE.de]: {
67
+ answersTo: 'Du antwortest dem Koordinator, nicht dem User.',
68
+ phasesIntro: 'Dein Plan und deine Phasen:',
69
+ noWrites: 'keine Schreibt-Menge im Plan',
70
+ wholeRepository: 'das ganze Repository',
71
+ editInside:
72
+ 'Außerhalb der Schreibt-Menge deiner Phasen änderst du nichts, außer den Aufgabenzeilen deiner Phasen im Plan: eine erledigte hakst du sofort ab, per Edit auf genau diese Zeile.',
73
+ writeTools:
74
+ 'Code und Tests schreibst du mit Write und Edit, nie per Heredoc oder Skript in Bash: ein solcher Aufruf zeigt dem Prüfer keinen Diff, und ein ungequoteter oder unlesbarer kann an einem Wächter hängen bleiben.',
75
+ port: (port) =>
76
+ `Port ${port}: Der Koordinator betreibt deinen Dev-Server in seinem eigenen Pane; deine sichtbaren Änderungen siehst du dort an (\`/implement\` Step 4b) und startest nie einen Server in deiner eigenen Shell.`,
77
+ noPort:
78
+ 'Kein Port: Diese Spur hat keinen eigenen Dev-Server. Starte nie einen Server in deiner eigenen Shell; brauchst du einen, frag den Koordinator.',
79
+ blocking:
80
+ 'Markiere jede deiner Fragen mit `blocking` oder `not blocking`. Eine blockierende hält den Schritt an, den sie entscheidet; bei einer nicht blockierenden lässt du an ihrer Stelle ein `TODO` und arbeitest weiter, ohne deine Empfehlung oder eine andere Option als entschieden zu bauen.',
81
+ mustNot:
82
+ 'Ohne Freigabe des Users pushst du keine Datenbank-Migration und schreibst nichts gegen eine geteilte Umgebung (Stage, Produktion); `git reset` und `git stash` auf geteilten Refs führst du nie aus.',
83
+ commit: 'Vor jedem Commit fragst du (`/pre-commit`); kein Push, kein PR und kein Merge ohne Freigabe.',
84
+ report: (reportPath) =>
85
+ `Den Bericht am Ende jedes Schritts schreibst du nach \`${reportPath}\` im Worktree, in der Kurzform: eine Zeile Diff-Stat, eine Zeile je Check mit seinen Zahlen, offene Punkte, Fragen als Textblock unter der Überschrift \`## Questions\` (in jeder Berichtssprache: der Watcher findet den Abschnitt an diesem Wort und zeigt ihn ganz); alles andere steht im Plan, per Verweis.`,
86
+ textBlock:
87
+ 'Jede Frage an den Koordinator ist ein Textblock am Ende deines Turns, nie ein Dialog (`AskUserQuestion`), auch wo ein Skill oder eine Projektregel einen verlangt, die Commit-Bestätigung von `/pre-commit` eingeschlossen: die Frage mit der Lage, dem Problem und wohin jede Option führt, in Alltagsworten (der User kennt deine Begriffe vielleicht nicht), die Optionen, deine Empfehlung, der Beleg als `datei:zeile`.',
88
+ secret:
89
+ 'Ein Wert, den nur der User kennt (ein Geheimnis, Passwort, Token oder Einmalcode), läuft nie über dich, deine Argumente, deine Ausgabe oder eine Datei, die du schreibst: bitte den Koordinator mit einer Frage um diesen Schritt.',
90
+ askRule:
91
+ 'Ein Werkzeug, das bei jedem Aufruf um Freigabe fragt (eine Ask-Regel, etwa ein Datenbank-Abfragewerkzeug oder eine Projekt-CLI), bekommt so wenige Aufrufe wie möglich: bündle die Schritte in einen Aufruf (ein Skript, eine Abfrage) statt eines Aufrufs je Schritt.',
92
+ approvalGate:
93
+ 'Ein Aufruf, der einen Erlaubnis-Dialog öffnen würde, wartet auf den Koordinator: Das Approval-Gate hält ihn an, und der Koordinator fragt den User. Abgelehnt mit „approval pending“: denselben Aufruf wiederholen, sobald der Koordinator ihn als freigegeben meldet; vom User abgelehnt: nicht wiederholen.',
94
+ helper: (helperName) =>
95
+ `Einen Helfer-Agenten in einem eigenen Pane (eine Messung, eine Probesitzung) meldest du dem Koordinator an, bevor er startet, als Frage am Ende deines Turns: was er tut, wie lange, und welche temporären Einstellungen er nutzt. Nach der Freigabe benennst du ihn \`${helperName}\` (\`herdr agent rename\`), damit der Watcher ihn mit deiner Spur beobachtet, und schließt ihn selbst, wenn er fertig ist: \`/exit\`, dann \`herdr pane close\`, und entferne jede temporäre Datei, die er brauchte.`,
96
+ helperPurpose: '<zweck>',
97
+ start: 'Start: Führe `/implement` für deine Phasen aus, in der Reihenfolge oben.',
98
+ restart: (handoverPath) =>
99
+ `Start: Lies zuerst \`${handoverPath}\` und mach mit dem nächsten Schritt weiter, den sie nennt.`,
100
+ },
101
+ }
@@ -0,0 +1,12 @@
1
+ export const MS_PER_MINUTE = 60_000
2
+
3
+ export const DAY_MS = 86_400_000
4
+
5
+ /** A day, month, hour or minute with a leading zero: `7` → `07`. */
6
+ export const twoDigits = (value: number): string => String(value).padStart(2, '0')
7
+
8
+ /** A time as the for-you rows show it: `DD.MM. HH:MM`. */
9
+ export const clockText = (at: number): string => {
10
+ const date = new Date(at)
11
+ return `${twoDigits(date.getDate())}.${twoDigits(date.getMonth() + 1)}. ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`
12
+ }