jackherd 1.6.1 → 1.7.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 (152) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/INSTALL.md +176 -0
  3. package/README.md +226 -310
  4. package/assets/branding/README.md +7 -3
  5. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  6. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  7. package/assets/branding/jackherd-logo-dark.svg +1 -1
  8. package/assets/icons/bark-dark.svg +1 -0
  9. package/assets/icons/bark.svg +1 -0
  10. package/assets/icons/bowl-dark.svg +1 -0
  11. package/assets/icons/bowl.svg +1 -0
  12. package/assets/icons/decide-dark.svg +1 -0
  13. package/assets/icons/decide.svg +1 -0
  14. package/assets/icons/ear-dark.svg +1 -0
  15. package/assets/icons/ear.svg +1 -0
  16. package/assets/icons/lanes-dark.svg +1 -0
  17. package/assets/icons/lanes.svg +1 -0
  18. package/assets/icons/plan-dark.svg +1 -0
  19. package/assets/icons/plan.svg +1 -0
  20. package/assets/icons/space.svg +1 -0
  21. package/dist/{chunk-QJSXHHQ3.js → chunk-OBUWTQ6Z.js} +25 -0
  22. package/dist/config.d.ts +14 -3
  23. package/dist/config.js +111 -10
  24. package/dist/plan-format.d.ts +180 -77
  25. package/dist/plan-format.js +292 -93
  26. package/docs/lanes-dark.svg +50 -0
  27. package/docs/lanes.svg +50 -1
  28. package/docs/skills-dark.svg +65 -0
  29. package/docs/skills.svg +65 -1
  30. package/docs/workflow-dark.svg +138 -0
  31. package/docs/workflow.svg +135 -169
  32. package/herdr-plugin/herdr-plugin.toml +6 -6
  33. package/herdr-plugin/open-plans.ts +1 -1
  34. package/herdr-plugin/restore-plans.ts +7 -7
  35. package/install.sh +1 -1
  36. package/kit/hooks/hooks/approval-gate/decision.ts +61 -0
  37. package/kit/hooks/hooks/approval-gate/herdr-panes.ts +24 -0
  38. package/kit/hooks/hooks/approval-gate/index.ts +69 -0
  39. package/kit/hooks/hooks/approval-gate/lane.ts +14 -0
  40. package/kit/hooks/hooks/approval-gate/request.ts +111 -0
  41. package/kit/hooks/hooks/approval-gate/run.ts +94 -0
  42. package/kit/hooks/hooks/approval-gate/settings.ts +26 -0
  43. package/kit/hooks/hooks/approval-gate/store.ts +70 -0
  44. package/kit/hooks/hooks/approval-gate/transcripts.ts +40 -0
  45. package/kit/hooks/hooks/merge-guard/approval.ts +11 -74
  46. package/kit/hooks/hooks/merge-guard/run.ts +2 -1
  47. package/kit/hooks/hooks/model-gate/agent-definition.ts +13 -0
  48. package/kit/hooks/hooks/model-gate/index.ts +58 -0
  49. package/kit/hooks/hooks/model-gate/run.ts +63 -0
  50. package/kit/hooks/hooks/model-log/index.ts +60 -0
  51. package/kit/hooks/hooks/model-log/run.ts +57 -0
  52. package/kit/hooks/hooks/model-log/store.ts +40 -0
  53. package/kit/hooks/hooks/model-log/transcript.ts +37 -0
  54. package/kit/hooks/hooks/shared/checkout.ts +22 -0
  55. package/kit/hooks/hooks/shared/command.ts +6 -0
  56. package/kit/hooks/hooks/shared/config.ts +21 -0
  57. package/kit/hooks/hooks/shared/dialog-answer.ts +5 -0
  58. package/kit/hooks/hooks/shared/dialog-answers.ts +74 -0
  59. package/kit/hooks/hooks/shared/files.ts +19 -0
  60. package/kit/hooks/hooks/shared/fresh.ts +4 -0
  61. package/kit/hooks/hooks/shared/json.ts +4 -0
  62. package/kit/hooks/hooks/shared/model-family.ts +48 -0
  63. package/kit/hooks/hooks/shared/plan-model-table.ts +41 -0
  64. package/kit/hooks/hooks/shared/private-directory.ts +29 -0
  65. package/kit/hooks/hooks/shared/repository-directory.ts +14 -0
  66. package/kit/hooks/hooks/shared/subagent-phase.ts +99 -0
  67. package/kit/hooks/hooks.json +25 -0
  68. package/kit/workflow/skills/follow/SKILL.md +4 -3
  69. package/kit/workflow/skills/implement/SKILL.md +6 -6
  70. package/kit/workflow/skills/orchestrate/SKILL.md +109 -27
  71. package/kit/workflow/skills/plan/SKILL.md +5 -5
  72. package/kit/workflow/skills/start/SKILL.md +2 -2
  73. package/package.json +4 -1
  74. package/schemas/jackherd.schema.json +43 -5
  75. package/src/check/command.ts +31 -4
  76. package/src/check/deferral-arrows.ts +1 -1
  77. package/src/check/parallel-report.ts +23 -0
  78. package/src/check/plan-structure-check.ts +120 -53
  79. package/src/cli.ts +8 -2
  80. package/src/config/nested-quantifier.ts +73 -0
  81. package/src/config/repository-directory.ts +11 -0
  82. package/src/config/schema.ts +63 -8
  83. package/src/plan-format/board-prose.ts +36 -0
  84. package/src/plan-format/dependencies.ts +227 -0
  85. package/src/plan-format/documentation-gate.ts +16 -0
  86. package/src/plan-format/index.ts +23 -1
  87. package/src/plan-format/lanes.ts +35 -1
  88. package/src/plan-format/model-column.ts +52 -0
  89. package/src/plan-format/phases.ts +16 -1
  90. package/src/plan-format/plan-structure.ts +10 -28
  91. package/src/plan-format/progress.ts +7 -29
  92. package/src/plans/command.ts +3 -3
  93. package/src/plans/current-phase.ts +71 -0
  94. package/src/plans/ensure-panes.ts +2 -2
  95. package/src/plans/herd-command.ts +4 -0
  96. package/src/plans/lane-discovery.ts +14 -2
  97. package/src/plans/lane-tickets.ts +69 -0
  98. package/src/plans/layout.ts +182 -88
  99. package/src/plans/live.ts +5 -1
  100. package/src/plans/phase-models-read.ts +37 -0
  101. package/src/plans/phase-models.ts +70 -0
  102. package/src/plans/plan-panes.ts +8 -7
  103. package/src/plans/pull-requests.ts +78 -4
  104. package/src/plans/selection.ts +1 -40
  105. package/src/plans/settings.ts +9 -3
  106. package/src/plans/shown-with-more.ts +5 -0
  107. package/src/plans/snapshot.ts +82 -8
  108. package/src/plans/sprint-html.ts +2 -2
  109. package/src/plans/supervisor.ts +5 -4
  110. package/src/plans/symbols.ts +9 -6
  111. package/src/plans/texts.ts +30 -13
  112. package/src/plans/view.ts +1 -0
  113. package/src/sync/herdr-plugin.ts +1 -1
  114. package/src/sync/next-steps.ts +1 -1
  115. package/src/sync/render.ts +2 -1
  116. package/src/sync/setup-questions.ts +2 -2
  117. package/src/terminal/text.ts +4 -0
  118. package/src/terminal/types.ts +1 -1
  119. package/src/vocabulary/index.ts +39 -0
  120. package/src/watch/approvals.ts +96 -0
  121. package/src/watch/command.ts +154 -50
  122. package/src/watch/failed-tests.ts +52 -0
  123. package/src/watch/failure-read.ts +56 -0
  124. package/src/watch/failures.ts +107 -0
  125. package/src/watch/feedback-item.ts +21 -0
  126. package/src/watch/feedback-read.ts +2 -1
  127. package/src/watch/feedback.ts +37 -28
  128. package/src/watch/gh-process.ts +53 -0
  129. package/src/watch/gh.ts +10 -20
  130. package/src/watch/heartbeat-progress.ts +1 -1
  131. package/src/watch/lane-model.ts +54 -0
  132. package/src/watch/lane-parallel.ts +46 -0
  133. package/src/watch/lane-progress.ts +28 -4
  134. package/src/watch/lane-report.ts +62 -9
  135. package/src/watch/lane-sources.ts +1 -1
  136. package/src/watch/no-finding.ts +47 -0
  137. package/src/watch/plan-badges-read.ts +19 -7
  138. package/src/watch/plan-badges.ts +59 -7
  139. package/src/watch/plan-views.ts +5 -5
  140. package/src/watch/previews.ts +3 -9
  141. package/src/watch/pull-requests.ts +12 -2
  142. package/src/watch/recorded-reads.ts +25 -0
  143. package/src/watch/source-fields.ts +4 -0
  144. package/src/watch/source-tickets.ts +99 -0
  145. package/src/watch/sources.ts +62 -15
  146. package/src/watch/texts.ts +21 -1
  147. package/src/watch/watch-defaults.ts +8 -0
  148. package/src/watch/watch-events.ts +64 -4
  149. package/src/watch/watcher-lock.ts +1 -1
  150. package/src/watch/workflow-runs.ts +5 -7
  151. package/docs/lanes.mmd +0 -9
  152. package/docs/skills.mmd +0 -10
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PLAN_LANGUAGE,
3
3
  RESPONSE_ADDRESS
4
- } from "./chunk-QJSXHHQ3.js";
4
+ } from "./chunk-OBUWTQ6Z.js";
5
5
 
6
6
  // src/config/json-schema.ts
7
7
  import { z as z3 } from "zod";
@@ -157,8 +157,10 @@ var SOURCE_FIELD = {
157
157
  title: "title",
158
158
  url: "url",
159
159
  body: "body",
160
- author: "author"
160
+ author: "author",
161
+ status: "status"
161
162
  };
163
+ var CONTENT_FIELDS = [SOURCE_FIELD.title, SOURCE_FIELD.author, SOURCE_FIELD.body, SOURCE_FIELD.url];
162
164
 
163
165
  // src/watch/source-path.ts
164
166
  var ROOT_PATH = ".";
@@ -176,6 +178,10 @@ var pathSteps = (path) => {
176
178
  };
177
179
  var isSourcePath = (path) => pathSteps(path) !== null;
178
180
 
181
+ // src/watch/watch-defaults.ts
182
+ var CLEAN_REVIEW_TEXT = "## Code review\n\nNo issues found. Checked for bugs and CLAUDE.md compliance.";
183
+ var BUN_FAILED_TEST_PATTERN = "^\\(fail\\) (.+?)(?: \\[[\\d.]+m?s\\])?$";
184
+
179
185
  // src/config/link-kinds.ts
180
186
  var DEFAULT_LINK_KINDS = [
181
187
  { pattern: "^https://github\\.com/[^/]+/[^/]+/(pull|issues)/\\d+", label: "GitHub", icon: "\u2442" },
@@ -191,6 +197,65 @@ var isRegularExpression = (source) => {
191
197
  }
192
198
  };
193
199
 
200
+ // src/config/nested-quantifier.ts
201
+ var GROUP_PREFIX = /^\?(?::|=|!|<=|<!|<[^>]+>)/;
202
+ var BRACES = /^\{(\d+)(?:(,)(\d*))?\}/;
203
+ var repeatingLengthAt = (pattern, index) => {
204
+ if (pattern[index] === "*" || pattern[index] === "+") return 1;
205
+ const braces = pattern.slice(index).match(BRACES);
206
+ const [text, , comma, max] = braces ?? [];
207
+ return text !== void 0 && comma && (max === "" || Number(max) > 1) ? text.length : 0;
208
+ };
209
+ var classEnd = (pattern, start) => {
210
+ let index = start + 1;
211
+ while (index < pattern.length && pattern[index] !== "]") index += pattern[index] === "\\" ? 2 : 1;
212
+ return index;
213
+ };
214
+ var overlapsAnother = (branches) => branches.some(
215
+ (branch, index) => branches.some((other, otherIndex) => index !== otherIndex && other.startsWith(branch))
216
+ );
217
+ var NESTED = -1;
218
+ var appendTo = (group, text) => {
219
+ group.branches[group.branches.length - 1] += text;
220
+ };
221
+ var prefixLengthAt = (pattern, index) => pattern.slice(index + 1).match(GROUP_PREFIX)?.[0].length ?? 0;
222
+ var closeGroup = (pattern, index, groups) => {
223
+ const closed = groups.pop();
224
+ const repeated = repeatingLengthAt(pattern, index + 1) > 0;
225
+ if (repeated && (closed.repeats || overlapsAnother(closed.branches))) return NESTED;
226
+ groups[groups.length - 1].repeats ||= closed.repeats;
227
+ return index + 1;
228
+ };
229
+ var stepAt = (pattern, index, groups) => {
230
+ const top = groups[groups.length - 1];
231
+ const char = pattern[index];
232
+ const literalEnd = char === "\\" ? index + 2 : classEnd(pattern, index) + 1;
233
+ const quantifierLength = repeatingLengthAt(pattern, index);
234
+ if (char === "\\" || char === "[") appendTo(top, pattern.slice(index, literalEnd));
235
+ else if (char === "(") groups.push({ repeats: false, branches: [""] });
236
+ else if (char === ")" && groups.length > 1) return closeGroup(pattern, index, groups);
237
+ else if (char === "|") top.branches.push("");
238
+ else if (quantifierLength > 0) top.repeats = true;
239
+ else appendTo(top, char);
240
+ if (char === "\\" || char === "[") return literalEnd;
241
+ if (char === "(") return index + 1 + prefixLengthAt(pattern, index);
242
+ return index + Math.max(quantifierLength, 1);
243
+ };
244
+ var hasNestedQuantifier = (pattern) => {
245
+ const groups = [{ repeats: false, branches: [""] }];
246
+ let index = 0;
247
+ while (index < pattern.length) {
248
+ index = stepAt(pattern, index, groups);
249
+ if (index === NESTED) return true;
250
+ }
251
+ return false;
252
+ };
253
+
254
+ // src/config/repository-directory.ts
255
+ import { isAbsolute } from "path";
256
+ var segmentsOf = (path) => path.split(/[\\/]/).filter((segment) => segment !== "" && segment !== ".");
257
+ var isRepositoryDirectory = (value) => !isAbsolute(value) && segmentsOf(value).length > 0 && !segmentsOf(value).includes("..");
258
+
194
259
  // src/config/schema.ts
195
260
  var TERMINAL_ADAPTER = { none: "none", herdr: "herdr" };
196
261
  var MODEL_ROLE = {
@@ -228,7 +293,7 @@ var boardSchema = z2.strictObject({
228
293
  pattern: boardPattern.default("board-*"),
229
294
  /** The board document inside each of those directories. */
230
295
  file: boardFile.default("board.md"),
231
- /** What the project calls a board: the plans view, its page and the watcher print it as written. */
296
+ /** What the project calls a board: the herd, its page and the watcher print it as written. */
232
297
  term: z2.string().min(1).default("board")
233
298
  });
234
299
  var deploySchema = z2.strictObject({
@@ -239,7 +304,23 @@ var deploySchema = z2.strictObject({
239
304
  });
240
305
  var feedbackSchema = z2.strictObject({
241
306
  /** GitHub logins whose feedback does not wake the coordinator, such as a status bot; case is ignored. */
242
- ignore: z2.array(z2.string().min(1)).default([])
307
+ ignore: z2.array(z2.string().min(1)).default([]),
308
+ /**
309
+ * Texts of a bot's pull request comment that report no finding: such a comment (login ending `[bot]`, no inline
310
+ * comment by the same author within five minutes of it) is no feedback. Compared with whitespace collapsed; the
311
+ * default is the CI code review's comment for a clean run. An empty list counts every bot comment.
312
+ */
313
+ noFindingTemplates: z2.array(z2.string().min(1)).default([CLEAN_REVIEW_TEXT])
314
+ });
315
+ var checksSchema = z2.strictObject({
316
+ /**
317
+ * Finds a failed test in a line of a red run's log (`gh run view --log-failed`) after its job, step and timestamp
318
+ * prefix: the first group is the name, else the match. The default is Bun's `(fail) <name> [<duration>]`. The
319
+ * watcher runs it on every line of a log it does not control, so a pattern that repeats a group which repeats inside
320
+ * itself (`(a+)+`, `(\w*)*`, and with alternation `(a|aa)+`) is refused: a best-effort scan of the source against
321
+ * catastrophic backtracking, not a proof.
322
+ */
323
+ failedTestPattern: z2.string().min(1).refine(isRegularExpression, "not a valid regular expression").refine((pattern) => !hasNestedQuantifier(pattern), "a quantified group that holds a quantifier backtracks badly").default(BUN_FAILED_TEST_PATTERN)
243
324
  });
244
325
  var previewsSchema = z2.strictObject({
245
326
  /** Reads the GitHub deployments of each head commit; a successful one outside production and `deploy.environments` names its `environment_url`. */
@@ -256,17 +337,23 @@ var sourcePath = z2.string().refine(isSourcePath, "not a source path: `.`, or `.
256
337
  var SOURCE_FIELD_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
257
338
  var sourceMapSchema = z2.object({
258
339
  [SOURCE_FIELD.id]: sourcePath,
259
- /** Changes whenever the entry changes; compared as text, so any stable form of a time or version works. */
260
- [SOURCE_FIELD.updatedAt]: sourcePath,
340
+ /**
341
+ * Changes whenever the entry changes; compared as text, so any stable form of a time or version works. Needed
342
+ * only where no content field and no `reportOn` can tell a change.
343
+ */
344
+ [SOURCE_FIELD.updatedAt]: sourcePath.optional(),
261
345
  [SOURCE_FIELD.title]: sourcePath.optional(),
262
346
  /** Shown only as an `https:` URL without user name or password. */
263
347
  [SOURCE_FIELD.url]: sourcePath.optional(),
264
348
  /** The first line with text is shown. */
265
349
  [SOURCE_FIELD.body]: sourcePath.optional(),
266
- [SOURCE_FIELD.author]: sourcePath.optional()
350
+ [SOURCE_FIELD.author]: sourcePath.optional(),
351
+ /** A ticket source's status, shown on its badge in the herd; also a field `reportOn` can name. */
352
+ [SOURCE_FIELD.status]: sourcePath.optional()
267
353
  }).catchall(sourcePath).refine((map) => Object.keys(map).every((field) => SOURCE_FIELD_NAME.test(field)), {
268
354
  message: "a map field is a letter, then letters, digits and `_`"
269
355
  });
356
+ var canChange = ({ map, reportOn }) => map.updatedAt !== void 0 || CONTENT_FIELDS.some((field) => map[field] !== void 0) || reportOn !== void 0;
270
357
  var sourceSchema = z2.strictObject({
271
358
  /** Printed in every report of the source and the key of its saved state. */
272
359
  name: z2.string().regex(/^[A-Za-z0-9][\w.-]*$/, "letters, digits, `_`, `.` and `-`"),
@@ -283,11 +370,19 @@ var sourceSchema = z2.strictObject({
283
370
  reportLimit: z2.number().int().min(1).default(5),
284
371
  /** The lane the source's reports name. */
285
372
  lane: z2.string().min(1).optional(),
373
+ /**
374
+ * Marks the entries as tickets: the herd shows each as a badge beside the plan of the lane it belongs to,
375
+ * the lane above or the lane whose board row or plan names its id.
376
+ */
377
+ ticket: z2.boolean().optional(),
286
378
  /**
287
379
  * The mapped fields whose change reports, with the old and the new value, in place of the content of `title`,
288
380
  * `author`, `body` and `url`; without it a change of that content reports.
289
381
  */
290
382
  reportOn: z2.array(z2.string().min(1)).min(1).max(10).optional()
383
+ }).refine(canChange, {
384
+ message: "map needs updatedAt, a content field (title, author, body, url) or reportOn",
385
+ path: ["map"]
291
386
  }).refine((source) => source.ignoreAuthors.length === 0 || source.map.author !== void 0, {
292
387
  message: "ignoreAuthors needs map.author",
293
388
  path: ["ignoreAuthors"]
@@ -331,6 +426,10 @@ var guardSchema = z2.strictObject({
331
426
  offer: z2.boolean().default(true),
332
427
  /** How long an answer to that question lets `gh pr merge <number>` pass without asking, counted from the answer. */
333
428
  approvalMinutes: z2.number().int().min(1).max(1440).default(180)
429
+ }).prefault({}),
430
+ approvals: z2.strictObject({
431
+ /** How long the approval gate hook holds a lane's call for the coordinator's dialog before it denies it; below the hook's 10 minutes. */
432
+ waitMinutes: z2.number().int().min(1).max(9).default(8)
334
433
  }).prefault({})
335
434
  });
336
435
  var hintsSchema = z2.strictObject({
@@ -360,7 +459,7 @@ var jackherdConfigSchema = z2.strictObject({
360
459
  board: boardSchema.prefault({}),
361
460
  /** Replaced by `board.pattern` in 0.13.0; named here only so an old config is refused with that hint. */
362
461
  sprintPattern: z2.never({ error: "sprintPattern was replaced by board.pattern (with board.file and board.term)" }).optional(),
363
- worktreeDirectory: z2.string().min(1).default(claudePath(WORKTREES_DIRECTORY)),
462
+ worktreeDirectory: z2.string().min(1).refine(isRepositoryDirectory, "must lie below the repository root: relative, not `.`, without `..`").default(claudePath(WORKTREES_DIRECTORY)),
364
463
  baseBranch: branchName.default(DEFAULT_BASE_BRANCH),
365
464
  /**
366
465
  * Put before every skill and agent name. `jh sync --global` refuses to run without one: a personal skill
@@ -388,7 +487,7 @@ var jackherdConfigSchema = z2.strictObject({
388
487
  models: z2.record(z2.enum(Object.values(MODEL_ROLE)), modelId),
389
488
  terminal: z2.strictObject({ adapter: z2.enum(Object.values(TERMINAL_ADAPTER)).default(TERMINAL_ADAPTER.none) }).prefault({}),
390
489
  plans: z2.strictObject({
391
- /** Minutes between background fetches of the base branch while `jh plans` runs; 0 turns them off. */
490
+ /** Minutes between background fetches of the base branch while `jh herd` runs; 0 turns them off. */
392
491
  fetchIntervalMinutes: z2.number().min(0).default(5),
393
492
  /** Links in a plan that the sprint page shows as chips; the list replaces the built-in kinds. */
394
493
  linkKinds: z2.array(
@@ -403,12 +502,14 @@ var jackherdConfigSchema = z2.strictObject({
403
502
  /**
404
503
  * Read by `jh watch`; without `deploy` it reports no deploy runs. `feedback` is on unless set to `false`:
405
504
  * it reports new feedback on the lanes' pull requests except from the account the watcher runs as and `ignore`.
505
+ * `checks` reads the failed tests of a red lane pull request (`failedTestPattern`), also with `feedback` off.
406
506
  * Without `previews` it reads no deploy preview; with it, one `preview` per new head commit. Each of `sources` is a
407
507
  * command whose JSON output reports `source` for a new or changed entry.
408
508
  */
409
509
  watch: z2.strictObject({
410
510
  deploy: deploySchema.optional(),
411
- feedback: z2.union([z2.literal(false), feedbackSchema]).default({ ignore: [] }),
511
+ feedback: z2.union([z2.literal(false), feedbackSchema]).prefault({}),
512
+ checks: checksSchema.prefault({}),
412
513
  previews: previewsSchema.optional(),
413
514
  sources: z2.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").default([])
414
515
  }).prefault({}),
@@ -22,6 +22,16 @@ declare const ADDRESS_FORM: Record<PlanLanguage, Record<ResponseAddress, string>
22
22
  * a config value.
23
23
  */
24
24
  declare const SELF_MERGE_LABEL: Record<PlanLanguage, string>;
25
+ /**
26
+ * The words of an approval: the two option labels of the coordinator's question about a lane's call, which the
27
+ * approval gate hook matches exactly, and what the hook tells the lane when it denies. Fixed per language, not config.
28
+ */
29
+ declare const APPROVAL_TEXTS: Record<PlanLanguage, {
30
+ allow: string;
31
+ deny: string;
32
+ pending: string;
33
+ denied: string;
34
+ }>;
25
35
  declare const isPlanLanguage: (value: string) => value is PlanLanguage;
26
36
  declare const PLAN_STATUS: {
27
37
  readonly done: "done";
@@ -68,8 +78,18 @@ type PlanVocabulary = {
68
78
  laneClosed: string;
69
79
  /** Words a starts-after cell begins with when the lane may start at once. */
70
80
  startsNow: readonly string[];
81
+ /** Opens the parenthesis by which a board row's phases cell records a user's side order in prose. */
82
+ sideOrderMarker: string;
71
83
  /** Words that may stand between two phases of one entry (`P1 Rule und P2 Monitor`). */
72
84
  phaseConnectors: readonly string[];
85
+ /** Words that open a writes cell naming another phase's set instead of listing it (`wie 15`). */
86
+ writesLike: readonly string[];
87
+ /** Words that join several phases after a `writesLike` word (`wie 15 und 16`); a comma joins them in every language. */
88
+ writesLikeJoiners: readonly string[];
89
+ /** Words a `Depends on` cell says it depends on nothing with, besides a dash. */
90
+ noDependency: readonly string[];
91
+ /** The word that opens the serial marker of a phase that ships in a release (`Release 1.7.0`); the release files stay a judgement. */
92
+ releaseMarker: string;
73
93
  statusWords: Record<PlanStatus, StatusWord>;
74
94
  };
75
95
  declare const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary>;
@@ -79,6 +99,139 @@ declare const classifyStatus: (language: PlanLanguage, statusText: string) => Pl
79
99
  declare const phaseHeading: (language: PlanLanguage, id: string, title: string) => string;
80
100
  declare const sectionHeading: (language: PlanLanguage, section: keyof PlanVocabulary["sections"]) => string;
81
101
 
102
+ /**
103
+ * The longest phases cell, in characters, that still reads as a phase and its state. Longer cells in the open
104
+ * lanes of the real boards carry prose, which belongs in the plan.
105
+ */
106
+ declare const PHASE_CELL_LIMIT = 250;
107
+ declare const PROSE_REASON: {
108
+ readonly long: "long";
109
+ readonly sideOrder: "side-order";
110
+ };
111
+ type ProseReason = (typeof PROSE_REASON)[keyof typeof PROSE_REASON];
112
+ /** A changed row of an open lane (1-based line) whose phases cell is `length` characters and carries prose. */
113
+ type RowWithProse = {
114
+ line: number;
115
+ lane: string;
116
+ reason: ProseReason;
117
+ length: number;
118
+ };
119
+ /**
120
+ * The changed rows of open lanes whose phases cell is longer than `PHASE_CELL_LIMIT` or records a side order of the
121
+ * user in prose (`dazu (User …`): the board row names the phase, the side order is a task in the plan's phase.
122
+ * A cell that is both counts once, as a side order.
123
+ */
124
+ declare const boardRowsWithProse: (markdown: string, language: PlanLanguage, previousLines: ReadonlySet<string>) => RowWithProse[];
125
+
126
+ /** A checkbox of a plan. `gate` is set on the ones listed under the phase's review gate, and only on those. */
127
+ type ChecklistItem = {
128
+ done: boolean;
129
+ text: string;
130
+ gate?: true;
131
+ };
132
+ /** An open checkbox that ends in `→ P<n>`: moved on purpose to phase `target`, its text without the marker. */
133
+ type DeferredItem = {
134
+ text: string;
135
+ target: string;
136
+ };
137
+ /** An open checkbox whose phase arrow is not at the end of its line (1-based), with the form the parser reads. */
138
+ type MisplacedDeferral = {
139
+ line: number;
140
+ target: string;
141
+ corrected: string;
142
+ };
143
+ /**
144
+ * `touched` is set by the phase source when the worktree's ticks differ from the base branch's. `deferred` holds
145
+ * the checkboxes moved to another phase; they are not among `items`, so they count neither as open nor as done.
146
+ */
147
+ type PlanPhase = {
148
+ id: string;
149
+ title: string;
150
+ items: ChecklistItem[];
151
+ deferred?: DeferredItem[];
152
+ touched?: true;
153
+ };
154
+ /** A phase heading with its 1-based line, the lower-case id and the title. */
155
+ type PhaseHeading = {
156
+ line: number;
157
+ id: string;
158
+ title: string;
159
+ };
160
+ /** The line that opens a phase's review gate: `**Review-Gate:**`, `**Review gate** (…)`. */
161
+ declare const isReviewGateLine: (line: string) => boolean;
162
+ /** A heading of level three or higher, which ends the section of a phase. */
163
+ declare const endsPhase: (line: string) => boolean;
164
+ declare const isCheckboxLine: (line: string) => boolean;
165
+ declare const isOpenCheckboxLine: (line: string) => boolean;
166
+ declare const phaseHeadingPattern: (language: PlanLanguage) => RegExp;
167
+ /**
168
+ * The phase an open checkbox line is moved to with `→ P<n>` at its end; null for any other line, a ticked box,
169
+ * a ✅/⬜ table row, and an arrow to the box's own phase `phaseId`. The one rule the parser and the tally share.
170
+ */
171
+ declare const deferralTarget: (line: string, phaseId: string) => string | null;
172
+ /**
173
+ * Open checkboxes that carry `→ P<n>` somewhere other than at the end of the line. `deferralTarget` reads the arrow
174
+ * only there, so such a box counts as open in its own phase without a word; `jh check` refuses it. In an
175
+ * open checkbox the phase arrow stands only at the end, also where it would be meant as prose (`→ P2 prüfen`).
176
+ */
177
+ declare const misplacedDeferrals: (markdown: string) => MisplacedDeferral[];
178
+ /** Every phase heading of a plan, `### Phase <id>` and `## P<id> — title` alike, with its 1-based line. */
179
+ declare const phaseHeadings: (markdown: string, language: PlanLanguage) => PhaseHeading[];
180
+ /**
181
+ * Every `### Phase <id>` section, and every `## <id> — title` section, with its checkboxes. A
182
+ * section runs to the next heading of level three or higher, so a review-gate list or a `####`
183
+ * sub-heading inside the phase still counts. A phase without checkboxes of its own that points at
184
+ * a section (``Siehe `## Dokumentation` ``) counts that section's items, so it is done when the
185
+ * section is. An open checkbox ending in `→ P<n>` is kept under `deferred` and listed in phase `<n>` as open.
186
+ */
187
+ declare const parsePlanPhases: (markdown: string, language: PlanLanguage) => PlanPhase[];
188
+
189
+ /**
190
+ * A row of the dependency table. `after` holds the phases it waits for, `external` the entries that name no phase
191
+ * (`User: Zuruf`) or a range that cannot be expanded; `writes` are the paths of the writes cell (`''` is the whole
192
+ * repository), `writesLike` the phases whose sets the cell takes over (`wie 15 und 16`); `serialMarkers` are the
193
+ * entries of the marker cell that keep two phases apart.
194
+ */
195
+ type PhaseDependency = {
196
+ id: string;
197
+ after: string[];
198
+ external: string[];
199
+ writes: string[];
200
+ writesLike: string[];
201
+ serialMarkers: string[];
202
+ };
203
+ /** A phase heading (1-based line) the dependency table has no row for. */
204
+ type PhaseWithoutRow = PhaseHeading;
205
+ /**
206
+ * The rows of the plan's dependency table (`### Abhängigkeiten und Parallelisierung`); null when the plan has no
207
+ * such table or the table lacks one of its three columns, so a plan that never had one is not asked for rows.
208
+ */
209
+ declare const parseDependencyTable: (markdown: string, language: PlanLanguage) => PhaseDependency[] | null;
210
+ /** The phase headings of a plan with a dependency table that the table has no row for; null for a plan without one. */
211
+ declare const phasesWithoutDependencyRow: (markdown: string, language: PlanLanguage) => PhaseWithoutRow[] | null;
212
+ /**
213
+ * The phases that can run side by side, as groups in table order: the first group can start now, a later one holds
214
+ * the ready phases that collide with an earlier group's members. A phase is offered when it is not in `doneIds`,
215
+ * every phase in its `Depends on` cell is, and the cell names no external gate: nothing in the plan says such a gate
216
+ * holds, so the phase stays out and the caller asks. Two phases share a group only when no path of one lies at or
217
+ * below a path of the other (a root path lies below nothing and above everything) and they share no serial marker,
218
+ * compared without case (a release marker does not count). A `wie N` writes cell takes the set of phase N; a phase
219
+ * whose set is empty, whose `wie` target is unknown, or whose `wie` chain is a cycle, cannot be shown disjoint and
220
+ * stays out. Whether two phases collide in content, or in the release files, is not decided here.
221
+ */
222
+ declare const parallelGroups: (rows: readonly PhaseDependency[], doneIds: ReadonlySet<string>) => string[][];
223
+ /** The phases as a report names them, side by side: `P19 ∥ P20`. */
224
+ declare const phasesText: (phases: readonly string[]) => string;
225
+ /** `parallelGroups` of the plan's own dependency table and ticks (a phase is done when every box is ticked); null for a plan without a table. */
226
+ declare const parallelGroupsOfPlan: (markdown: string, language: PlanLanguage) => string[][] | null;
227
+
228
+ /** The review-gate box `Dokumentation aktualisiert …` (`Documentation updated …`): a gate item starting with the documentation word. */
229
+ declare const isDocumentationGateItem: ({ gate, text }: ChecklistItem) => boolean;
230
+ /** The documentation gate box is the only unticked box of the phase: everything else is done. */
231
+ declare const onlyDocumentationOpen: ({ items }: {
232
+ items: readonly ChecklistItem[];
233
+ }) => boolean;
234
+
82
235
  type DriftFinding = {
83
236
  file: string;
84
237
  reason: string;
@@ -121,6 +274,12 @@ type SprintLane = {
121
274
  steps: PlanStep[];
122
275
  closed: boolean;
123
276
  };
277
+ /** A row of the lanes table (1-based line) of an open lane with the text of its phases cell. */
278
+ type OpenLaneRow = {
279
+ line: number;
280
+ lane: string;
281
+ phases: string;
282
+ };
124
283
  /** Sprint tables name a phase `P4`; the sidebar title prints it the same way. */
125
284
  declare const PHASE_ID_PREFIX = "P";
126
285
  /** The phase ids a phases cell of the lanes table names, read as `parseSprintLanes` reads them. */
@@ -138,6 +297,22 @@ declare const sprintTitleOf: (markdown: string, fallback: string) => string;
138
297
  /** The repository path of a plan link, relative to the sprint document's directory. */
139
298
  declare const resolvePlanPath: (sprintDirectory: string, href: string) => string;
140
299
 
300
+ /**
301
+ * A model as a board's model column names it: its words (`Opus 5.5` names `opus`; `claude-sonnet-5-5` names
302
+ * `claude` and `sonnet`, one of which is the family) and its version (`['5', '5']`).
303
+ */
304
+ type ColumnModel = {
305
+ names: string[];
306
+ version: string[];
307
+ };
308
+ /** The models a model column names, in its order; a note in brackets (`(Plan)`) and a dash name none. */
309
+ declare const columnModels: (column: string) => ColumnModel[];
310
+ /**
311
+ * Whether a running model's id fits none of the models the column names, by family and version (`Opus 5.5` fits
312
+ * `claude-opus-5-5`, also with a date or window suffix). Knows no vendor. A column that names no model never differs.
313
+ */
314
+ declare const differsFromColumn: (column: string, modelId: string) => boolean;
315
+
141
316
  /** Per heading that carries checkboxes or table status cells: how many are done and open. */
142
317
  type PhaseTally = {
143
318
  title: string;
@@ -153,73 +328,10 @@ type PhaseTally = {
153
328
  */
154
329
  declare const tallyPhases: (markdown: string, language: PlanLanguage) => PhaseTally[];
155
330
 
156
- /** A checkbox of a plan. `gate` is set on the ones listed under the phase's review gate, and only on those. */
157
- type ChecklistItem = {
158
- done: boolean;
159
- text: string;
160
- gate?: true;
161
- };
162
- /** An open checkbox that ends in `→ P<n>`: moved on purpose to phase `target`, its text without the marker. */
163
- type DeferredItem = {
164
- text: string;
165
- target: string;
166
- };
167
- /** An open checkbox whose phase arrow is not at the end of its line (1-based), with the form the parser reads. */
168
- type MisplacedDeferral = {
169
- line: number;
170
- target: string;
171
- corrected: string;
172
- };
173
- /**
174
- * `touched` is set by the phase source when the worktree's ticks differ from the base branch's. `deferred` holds
175
- * the checkboxes moved to another phase; they are not among `items`, so they count neither as open nor as done.
176
- */
177
- type PlanPhase = {
178
- id: string;
179
- title: string;
180
- items: ChecklistItem[];
181
- deferred?: DeferredItem[];
182
- touched?: true;
183
- };
184
- /** The line that opens a phase's review gate: `**Review-Gate:**`, `**Review gate** (…)`. */
185
- declare const isReviewGateLine: (line: string) => boolean;
186
- /** A heading of level three or higher, which ends the section of a phase. */
187
- declare const endsPhase: (line: string) => boolean;
188
- declare const isCheckboxLine: (line: string) => boolean;
189
- declare const isOpenCheckboxLine: (line: string) => boolean;
190
- declare const phaseHeadingPattern: (language: PlanLanguage) => RegExp;
191
- /**
192
- * The phase an open checkbox line is moved to with `→ P<n>` at its end; null for any other line, a ticked box,
193
- * a ✅/⬜ table row, and an arrow to the box's own phase `phaseId`. The one rule the parser and the tally share.
194
- */
195
- declare const deferralTarget: (line: string, phaseId: string) => string | null;
196
- /**
197
- * Open checkboxes that carry `→ P<n>` somewhere other than at the end of the line. `deferralTarget` reads the arrow
198
- * only there, so such a box counts as open in its own phase without a word; `jh check` refuses it. In an
199
- * open checkbox the phase arrow stands only at the end, also where it would be meant as prose (`→ P2 prüfen`).
200
- */
201
- declare const misplacedDeferrals: (markdown: string) => MisplacedDeferral[];
202
- /**
203
- * Every `### Phase <id>` section, and every `## <id> — title` section, with its checkboxes. A
204
- * section runs to the next heading of level three or higher, so a review-gate list or a `####`
205
- * sub-heading inside the phase still counts. A phase without checkboxes of its own that points at
206
- * a section (``Siehe `## Dokumentation` ``) counts that section's items, so it is done when the
207
- * section is. An open checkbox ending in `→ P<n>` is kept under `deferred` and listed in phase `<n>` as open.
208
- */
209
- declare const parsePlanPhases: (markdown: string, language: PlanLanguage) => PlanPhase[];
210
-
211
331
  /** A `### Phase` heading (1-based line) whose section has tasks and neither a review gate nor the line saying why not. */
212
- type PhaseWithoutGate = {
213
- line: number;
214
- id: string;
215
- title: string;
216
- };
332
+ type PhaseWithoutGate = PhaseHeading;
217
333
  /** A row of the lanes table (1-based line) whose phases cell names no phase id. */
218
- type RowWithoutPhase = {
219
- line: number;
220
- lane: string;
221
- phases: string;
222
- };
334
+ type RowWithoutPhase = OpenLaneRow;
223
335
  /**
224
336
  * A `### Phase` heading (1-based line) whose tasks are all ticked on the base branch while its own section still
225
337
  * has open checkboxes not moved on with `→ P<n>` (`openLines`, 1-based).
@@ -246,10 +358,8 @@ declare const hasLanesTable: (markdown: string, language: PlanLanguage) => boole
246
358
  */
247
359
  declare const mergedPhasesWithOpenBoxes: (markdown: string, base: string, language: PlanLanguage) => MergedPhaseWithOpenBoxes[];
248
360
  /**
249
- * The rows of the lanes table (`## Spuren`) that are not in `previousLines` word for word and whose phases cell
250
- * names no phase id (`P10c`), so neither the view nor the watcher can tie that step to a phase. The table is read
251
- * as `parseSprintLanes` reads it (its first table, a lane closed when any of its rows says so); rows of a closed
252
- * lane are history and stay out.
361
+ * The changed rows of open lanes whose phases cell names no phase id (`P10c`), so neither the view nor the watcher
362
+ * can tie that step to a phase.
253
363
  */
254
364
  declare const boardRowsWithoutPhase: (markdown: string, language: PlanLanguage, previousLines: ReadonlySet<string>) => RowWithoutPhase[];
255
365
 
@@ -286,13 +396,6 @@ declare const sumPhases: (phases: readonly PhaseProgress[]) => {
286
396
  done: number;
287
397
  total: number;
288
398
  };
289
- /**
290
- * The ids of a step that show a phase of their own. An id the plan lacks (`5b`) falls back to its
291
- * base phase (`5`); when the step already names that phase by its own id, or an earlier id fell
292
- * back to it, the fallback would list the same tasks twice, so it is dropped. A lone fallback
293
- * stays: the sprint line `P4a` finds phase 4 in the plans that have no `4a`.
294
- */
295
- declare const keptPhaseIds: (phases: PlanPhase[], ids: readonly string[]) => string[];
296
399
  /**
297
400
  * A phase the lane ticked to the end in its worktree while the base branch does not carry those ticks yet: still
298
401
  * the lane's work until it is merged, not a finished phase.
@@ -341,4 +444,4 @@ declare const readSprintDocument: (markdown: string, language: PlanLanguage) =>
341
444
  */
342
445
  declare const readPhaseDependencies: (planMarkdown: string, language: PlanLanguage) => Record<string, string>;
343
446
 
344
- export { ADDRESS_FORM, type ChecklistItem, type DeferredItem, type DriftFinding, LANGUAGE_NAME, type LaneProgress, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, PHASE_ID_PREFIX, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, type PhaseProgress, type PhaseTally, type PhaseWithoutGate, type PlanHeader, type PlanLanguage, type PlanPhase, type PlanStatus, type PlanStep, type PlanVocabulary, type PlansByHref, RESPONSE_ADDRESS, type ResponseAddress, type RowWithoutPhase, SELF_MERGE_LABEL, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, activeItem, boardRowsWithoutPhase, classifyStatus, countDecisions, currentPhase, currentPhaseOf, deferralTarget, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isOpenCheckboxLine, isPlanLanguage, isReviewGateLine, isTickedUnmerged, keptPhaseIds, laneNameOf, laneProgress, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, parsePlanPhases, parseSprintLanes, phaseHeading, phaseHeadingPattern, phaseIdsOfCell, phaseIdsOfPlan, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortTitle, sprintTitleOf, sumPhases, tallyPhases, withoutRepeats };
447
+ export { ADDRESS_FORM, APPROVAL_TEXTS, type ChecklistItem, type ColumnModel, type DeferredItem, type DriftFinding, LANGUAGE_NAME, type LaneProgress, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, PHASE_CELL_LIMIT, PHASE_ID_PREFIX, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, PROSE_REASON, type PhaseDependency, type PhaseHeading, type PhaseProgress, type PhaseTally, type PhaseWithoutGate, type PhaseWithoutRow, type PlanHeader, type PlanLanguage, type PlanPhase, type PlanStatus, type PlanStep, type PlanVocabulary, type PlansByHref, type ProseReason, RESPONSE_ADDRESS, type ResponseAddress, type RowWithProse, type RowWithoutPhase, SELF_MERGE_LABEL, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, activeItem, boardRowsWithProse, boardRowsWithoutPhase, classifyStatus, columnModels, countDecisions, currentPhase, currentPhaseOf, deferralTarget, differsFromColumn, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isDocumentationGateItem, isOpenCheckboxLine, isPlanLanguage, isReviewGateLine, isTickedUnmerged, laneNameOf, laneProgress, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, onlyDocumentationOpen, parallelGroups, parallelGroupsOfPlan, parseDependencyTable, parsePlanPhases, parseSprintLanes, phaseHeading, phaseHeadingPattern, phaseHeadings, phaseIdsOfCell, phaseIdsOfPlan, phasesText, phasesWithoutDependencyRow, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortTitle, sprintTitleOf, sumPhases, tallyPhases, withoutRepeats };