jackherd 1.10.0 → 1.11.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 (64) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +17 -2
  3. package/dist/{chunk-5MQBCUQ5.js → chunk-RMMXD4G5.js} +9 -2
  4. package/dist/config.d.ts +60 -0
  5. package/dist/config.js +39 -4
  6. package/dist/plan-format.d.ts +7 -1
  7. package/dist/plan-format.js +4 -2
  8. package/herdr-plugin/herdr-plugin.toml +1 -1
  9. package/install.sh +1 -1
  10. package/kit/hooks/hooks/open-question/run.ts +8 -3
  11. package/kit/rules/rules/kit/pull-requests.md +2 -0
  12. package/kit/workflow/skills/follow/SKILL.md +3 -3
  13. package/kit/workflow/skills/orchestrate/SKILL.md +10 -2
  14. package/package.json +1 -1
  15. package/recipes/sources.md +4 -0
  16. package/schemas/jackherd.schema.json +80 -0
  17. package/src/cli.ts +1 -0
  18. package/src/config/schema.ts +24 -1
  19. package/src/config/tones.ts +27 -0
  20. package/src/lane-prompt/additions.ts +14 -0
  21. package/src/lane-prompt/command.ts +8 -2
  22. package/src/lane-prompt/input.ts +44 -3
  23. package/src/lane-prompt/options.ts +2 -0
  24. package/src/mermaid/command.ts +36 -0
  25. package/src/mermaid/problems.ts +48 -0
  26. package/src/plan-format/dependencies.ts +2 -2
  27. package/src/plan-format/waiting.ts +15 -2
  28. package/src/plans/appointment-rows.ts +34 -10
  29. package/src/plans/ask-coordinator.ts +46 -12
  30. package/src/plans/ask-target.ts +13 -2
  31. package/src/plans/board-close.ts +85 -0
  32. package/src/plans/frame.ts +5 -12
  33. package/src/plans/git.ts +35 -0
  34. package/src/plans/groomed.ts +27 -0
  35. package/src/plans/layout-phase.ts +7 -4
  36. package/src/plans/layout.ts +258 -48
  37. package/src/plans/link-chips.ts +22 -4
  38. package/src/plans/live.ts +73 -5
  39. package/src/plans/next-rows.ts +44 -0
  40. package/src/plans/planned.ts +22 -0
  41. package/src/plans/row-actions.ts +52 -0
  42. package/src/plans/row-focus.ts +148 -0
  43. package/src/plans/row-targets.ts +7 -0
  44. package/src/plans/selection.ts +14 -1
  45. package/src/plans/settings.ts +10 -0
  46. package/src/plans/snapshot.ts +79 -21
  47. package/src/plans/source-asked.ts +19 -0
  48. package/src/plans/source-prefix.ts +17 -0
  49. package/src/plans/source-rows.ts +188 -0
  50. package/src/plans/texts.ts +28 -3
  51. package/src/plans/tones.ts +30 -0
  52. package/src/plans/view.ts +8 -2
  53. package/src/plans/waiting-click.ts +129 -24
  54. package/src/terminal/text.ts +8 -0
  55. package/src/terminal/types.ts +4 -0
  56. package/src/vocabulary/index.ts +10 -0
  57. package/src/watch/plan-badges-read.ts +9 -2
  58. package/src/watch/plan-badges.ts +10 -2
  59. package/src/watch/report-questions.ts +4 -3
  60. package/src/watch/source-entries.ts +55 -0
  61. package/src/watch/source-fields.ts +1 -0
  62. package/src/watch/source-statuses.ts +60 -0
  63. package/src/watch/source-tickets.ts +21 -1
  64. package/src/watch/sources.ts +41 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  What changed for people who use jackherd, newest first.
4
4
 
5
+ ## 1.11.0
6
+
7
+ - **The herd lists your sources.** A `watch.sources` entry that is no ticket gets a row in a `Sources` block: when it last ran, since when it fails (red mark), its newest report (amber mark while it is under a day old); a click opens the report's URL. `/jh-follow` and `recipes/sources.md` say how.
8
+ - **The colours of the herd are configurable.** `plans.tones` names the tone of each role (`waiting`, `new`, `running`, `resting`) from amber, teal, grey, red, green, blue and magenta; a source may carry its own `tone` for its new reports. The defaults are the colours it had.
9
+ - **A row from a source says which one.** `watch.sources[].label` (at most 40 characters, one line; without it the source's name) is put before a "Waiting for you" row whose `Origin` cell names the source, as `<label>: <row>`.
10
+ - **"Waiting for you" rows have `[done]`, the appointments have `[done]`, `[run]` and `[move]`.** `[done]` closes the row (the board's status cell, a sent file's record), `[run]` asks the coordinator to run the appointment now, `[move]` to ask you for a new time.
11
+ - **New blocks and cells in the herd.** `Next` shows the board's next section; a resting lane says `waits on: …` or `after: …`; `groomed 3d ago` beside the clock (amber from eight days) from the board-grooming appointment; `planned` shows the next phase per plan, with `stale?` once its plan file has not changed on the base branch for 14 days.
12
+ - **`jh mermaid <file>` checks the diagrams of a pull request text.** An edge label between dashes (`-- text -->`) or a pipe label without quotes makes GitHub show `Lexical error` instead of the diagram; the command exits 1 with the line. The pull request rule now says to write `-->|"label"|` and to run it before `gh pr create` or `gh pr edit`.
13
+ - **A report that says "Keine offenen." asks nothing.** The watcher no longer counts it as a question, and a bold or listed "None." does not either.
14
+ - **A note in the write set no longer widens it to the whole repository.** A backticked config key such as `watch.sources[].label` in the `Writes` cell of a plan's dependency table is no path, so `jh lane prompt` lists only the real folders.
15
+ - **One colour rule for the buttons.** The buttons no longer wear the colour of the row's mark: amber and bold in "Waiting for you", the normal foreground colour in the other blocks, dim only where nothing is left to do (an entry that says `↑ asked`).
16
+ - **A source with several entries opens on a click.** The `Sources` block shows one folded row per source (amber only for a new entry that no row of "Waiting for you" asks about); opened, it lists up to five entries with title, count (`map.count`, new) and time, then `+<n> more`. A click on an entry opens its `url`, `[ask]` asks the coordinator to look at exactly that entry, and an entry that "Waiting for you" already asks about is dim and says `↑ asked`. The watcher keeps the last 20 entries of such a source.
17
+ - **A question put off twice stays one open question.** The `open-question` hook writes no second record while one with the same text is still open, so asking it again with `[ask]` and answering "later" again leaves a single row.
18
+ - **A click folds the ticked tasks again.** The listed ticked tasks of a phase get a line `▾ <n> ticked · click or a folds them` above them; before, only `a` closed the list that a click had opened. The key hint and the legend name both.
19
+ - **`jh lane prompt` keeps your additions and names no contested phase.** What you write below the marker line at the end of the prompt file stays through every new writing, `--handover` included. A phase that two open rows of the board name comes into neither prompt; the command prints it and a lane left with nothing is refused.
20
+ - **The row under the mouse or the arrows is marked, everywhere.** Any clickable row of the herd (lane, phase, ticked line, `waiting`, `planned`, and the rows of `Waiting for you`, `Next`, `Appointments` and `Sources`) gets a light background and a `❯` in front, and its buttons turn bold; the background joins the text and its buttons across the full width. The herd now asks the terminal for mouse movement and redraws only when the row under the mouse changes. `↑↓` also walks the rows of the blocks on top; enter on one asks, opens or expands it, and never presses `[done]` or `[run]`. The legend names it.
21
+ - **Links are bracket buttons, and the mouse bolds only the one it is on.** The links under a lane (`[↗ #<n>] [📄 Plan] [✦ title]`) lose the grey background and read like `[ask]`: a square bracket means a click does something. `… checks` stays plain text, amber and dark red badges stay badges. An artifact link shows its title (the link text, the bold label before it, else the text of its line) instead of "Artifact". With the mouse over a row only the bracket under it turns bold; with the arrows the whole row is the unit. A line of links is marked by the mouse, not walked by the arrows.
22
+ - **The README follows a source from the command to your answer.** A short section with a diagram: source, watcher, coordinator, "Waiting for you", you.
23
+
5
24
  ## 1.10.0
6
25
 
7
26
  - **`jh sync` writes the ask rules the merge guard left behind.** A project sync adds `permissions.ask` rules for `gh run rerun` and the `gh api` writes that merge, write a file, dispatch or rerun a workflow, or move a ref; it records them in the lock, removes one only when the kit drops it and only if a kit shipped it, and leaves your own rules alone. Claude Code applies a matching ask rule even when a hook answers `allow`.
package/README.md CHANGED
@@ -71,6 +71,21 @@ Install with one command, run `/jh-start` in a repository, and it offers your fi
71
71
 
72
72
  By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)
73
73
 
74
+ ## Follow anything outside GitHub
75
+
76
+ `/jh-follow` turns a ticket, an error log or a status page into something Jack acts on. It writes the source to `watch.sources` after a dry run and your yes; from then on a change takes this path:
77
+
78
+ ```mermaid
79
+ flowchart LR
80
+ source["Source<br/>ticket, error log, status page"] -->|"new or changed entry"| watcher["jh watch"]
81
+ watcher -->|"=== source … report"| coordinator["Jack"]
82
+ coordinator -->|"board row, origin = source"| waiting["Waiting for you<br/>Error log · Checkout: …"]
83
+ waiting -->|"[ask]"| coordinator
84
+ coordinator -->|"asks you, then a lane works it"| you["You"]
85
+ ```
86
+
87
+ The herd lists each source that is no ticket in a `Sources` block (when it ran, since when it fails, its newest report, a click opens it), and a row that came from a source starts with its `label`. Details and every field: [recipes](https://unpkg.com/jackherd/recipes/sources.md).
88
+
74
89
  ## In detail
75
90
 
76
91
  <details>
@@ -175,7 +190,7 @@ On top, **Waiting for you** lists what needs your answer: a file or Artifact sen
175
190
  ────────────────────────────────────────────────────────────────────────────────
176
191
  · no agent (1) docs-site 1/3
177
192
  ────────────────────────────────────────────────────────────────────────────────
178
- ↑↓ select →← open a ticked tasks s html q quit ? help
193
+ ↑↓ select →← open a/click ticked tasks s html q quit ? help
179
194
  ```
180
195
 
181
196
  </details>
@@ -191,7 +206,7 @@ On top, **Waiting for you** lists what needs your answer: a file or Artifact sen
191
206
  | `jh check` | types, lint and tests of what you touched, plus the plan and board checks (`--skip-slow`, `--parallel <plan>`) |
192
207
  | `jh herd` | the live view (`--once`, `--lane <name>`) |
193
208
  | `jh watch` | the watcher Jack runs |
194
- | `jh lane prompt <lane>` | writes a lane's first prompt from its board row and its plan (`--out`, `--handover`) |
209
+ | `jh lane prompt <lane>` | writes a lane's first prompt from its board row and its plan (`--out`, `--handover`); your additions below the marker line at its end stay |
195
210
  | `jh lane close <lane>` | removes a finished lane: agent, worktree, herdr workspace and branch, and marks the board; first checks that nothing is lost, and refuses with every reason; `--dry-run` only checks. Claude Code asks before each call |
196
211
  | `jh preview <file>` | renders a Markdown file through the GitHub API into a preview in GitHub and npm style |
197
212
  | `jh waiting` | the files and Artifacts sent to you that you have not answered yet (`close <id>…` or `close --all` once you have) |
@@ -40,15 +40,21 @@ var APPOINTMENT_VOCABULARY = {
40
40
  heading: "Termine",
41
41
  columns: { when: "Wann", action: "Aktion", origin: "Herkunft", status: "Stand" },
42
42
  statusAlso: ["Status"],
43
- open: ["offen"]
43
+ open: ["offen"],
44
+ grooming: "board-pflege"
44
45
  },
45
46
  [PLAN_LANGUAGE.en]: {
46
47
  heading: "Appointments",
47
48
  columns: { when: "When", action: "Action", origin: "Origin", status: "Status" },
48
49
  statusAlso: [],
49
- open: ["open"]
50
+ open: ["open"],
51
+ grooming: "board grooming"
50
52
  }
51
53
  };
54
+ var NEXT_VOCABULARY = {
55
+ [PLAN_LANGUAGE.de]: { heading: "Als N\xE4chstes" },
56
+ [PLAN_LANGUAGE.en]: { heading: "Next" }
57
+ };
52
58
  var WAITING_VOCABULARY = {
53
59
  [PLAN_LANGUAGE.de]: {
54
60
  heading: "Wartet auf dich",
@@ -164,6 +170,7 @@ export {
164
170
  LANE_PROMPT_OPENING,
165
171
  APPROVAL_TEXTS,
166
172
  APPOINTMENT_VOCABULARY,
173
+ NEXT_VOCABULARY,
167
174
  WAITING_VOCABULARY,
168
175
  isPlanLanguage,
169
176
  PLAN_STATUS,
package/dist/config.d.ts CHANGED
@@ -34,11 +34,22 @@ declare const sourceSchema: z.ZodObject<{
34
34
  body: z.ZodOptional<z.ZodString>;
35
35
  author: z.ZodOptional<z.ZodString>;
36
36
  status: z.ZodOptional<z.ZodString>;
37
+ count: z.ZodOptional<z.ZodString>;
37
38
  }, z.core.$catchall<z.ZodString>>;
38
39
  ignoreAuthors: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
40
  reportLimit: z.ZodDefault<z.ZodNumber>;
41
+ label: z.ZodOptional<z.ZodString>;
40
42
  lane: z.ZodOptional<z.ZodString>;
41
43
  ticket: z.ZodOptional<z.ZodBoolean>;
44
+ tone: z.ZodOptional<z.ZodEnum<{
45
+ amber: "amber";
46
+ teal: "teal";
47
+ grey: "grey";
48
+ red: "red";
49
+ green: "green";
50
+ blue: "blue";
51
+ magenta: "magenta";
52
+ }>>;
42
53
  reportOn: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
54
  }, z.core.$strict>;
44
55
  declare const jackherdConfigSchema: z.ZodObject<{
@@ -110,6 +121,44 @@ declare const jackherdConfigSchema: z.ZodObject<{
110
121
  alertTokens: z.ZodDefault<z.ZodNumber>;
111
122
  }, z.core.$strict>>;
112
123
  }, z.core.$strict>>;
124
+ tones: z.ZodPrefault<z.ZodObject<{
125
+ waiting: z.ZodDefault<z.ZodEnum<{
126
+ amber: "amber";
127
+ teal: "teal";
128
+ grey: "grey";
129
+ red: "red";
130
+ green: "green";
131
+ blue: "blue";
132
+ magenta: "magenta";
133
+ }>>;
134
+ new: z.ZodDefault<z.ZodEnum<{
135
+ amber: "amber";
136
+ teal: "teal";
137
+ grey: "grey";
138
+ red: "red";
139
+ green: "green";
140
+ blue: "blue";
141
+ magenta: "magenta";
142
+ }>>;
143
+ running: z.ZodDefault<z.ZodEnum<{
144
+ amber: "amber";
145
+ teal: "teal";
146
+ grey: "grey";
147
+ red: "red";
148
+ green: "green";
149
+ blue: "blue";
150
+ magenta: "magenta";
151
+ }>>;
152
+ resting: z.ZodDefault<z.ZodEnum<{
153
+ amber: "amber";
154
+ teal: "teal";
155
+ grey: "grey";
156
+ red: "red";
157
+ green: "green";
158
+ blue: "blue";
159
+ magenta: "magenta";
160
+ }>>;
161
+ }, z.core.$strict>>;
113
162
  }, z.core.$strict>>;
114
163
  watch: z.ZodPrefault<z.ZodObject<{
115
164
  deploy: z.ZodOptional<z.ZodObject<{
@@ -149,11 +198,22 @@ declare const jackherdConfigSchema: z.ZodObject<{
149
198
  body: z.ZodOptional<z.ZodString>;
150
199
  author: z.ZodOptional<z.ZodString>;
151
200
  status: z.ZodOptional<z.ZodString>;
201
+ count: z.ZodOptional<z.ZodString>;
152
202
  }, z.core.$catchall<z.ZodString>>;
153
203
  ignoreAuthors: z.ZodDefault<z.ZodArray<z.ZodString>>;
154
204
  reportLimit: z.ZodDefault<z.ZodNumber>;
205
+ label: z.ZodOptional<z.ZodString>;
155
206
  lane: z.ZodOptional<z.ZodString>;
156
207
  ticket: z.ZodOptional<z.ZodBoolean>;
208
+ tone: z.ZodOptional<z.ZodEnum<{
209
+ amber: "amber";
210
+ teal: "teal";
211
+ grey: "grey";
212
+ red: "red";
213
+ green: "green";
214
+ blue: "blue";
215
+ magenta: "magenta";
216
+ }>>;
157
217
  reportOn: z.ZodOptional<z.ZodArray<z.ZodString>>;
158
218
  }, z.core.$strict>>>;
159
219
  }, z.core.$strict>>;
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PLAN_LANGUAGE,
3
3
  RESPONSE_ADDRESS
4
- } from "./chunk-5MQBCUQ5.js";
4
+ } from "./chunk-RMMXD4G5.js";
5
5
 
6
6
  // src/config/json-schema.ts
7
7
  import { z as z3 } from "zod";
@@ -164,7 +164,8 @@ var SOURCE_FIELD = {
164
164
  url: "url",
165
165
  body: "body",
166
166
  author: "author",
167
- status: "status"
167
+ status: "status",
168
+ count: "count"
168
169
  };
169
170
  var CONTENT_FIELDS = [SOURCE_FIELD.title, SOURCE_FIELD.author, SOURCE_FIELD.body, SOURCE_FIELD.url];
170
171
 
@@ -274,6 +275,25 @@ import { isAbsolute } from "path";
274
275
  var segmentsOf = (path) => path.split(/[\\/]/).filter((segment) => segment !== "" && segment !== ".");
275
276
  var isRepositoryDirectory = (value) => !isAbsolute(value) && segmentsOf(value).length > 0 && !segmentsOf(value).includes("..");
276
277
 
278
+ // src/config/tones.ts
279
+ var TONE = {
280
+ amber: "amber",
281
+ teal: "teal",
282
+ grey: "grey",
283
+ red: "red",
284
+ green: "green",
285
+ blue: "blue",
286
+ magenta: "magenta"
287
+ };
288
+ var TONE_NAMES = Object.values(TONE);
289
+ var TONE_ROLE = { waiting: "waiting", new: "new", running: "running", resting: "resting" };
290
+ var DEFAULT_TONES = {
291
+ [TONE_ROLE.waiting]: TONE.amber,
292
+ [TONE_ROLE.new]: TONE.amber,
293
+ [TONE_ROLE.running]: TONE.teal,
294
+ [TONE_ROLE.resting]: TONE.grey
295
+ };
296
+
277
297
  // src/config/schema.ts
278
298
  var TERMINAL_ADAPTER = { none: "none", herdr: "herdr" };
279
299
  var MODEL_ROLE = {
@@ -387,11 +407,14 @@ var sourceMapSchema = z2.object({
387
407
  [SOURCE_FIELD.body]: sourcePath.optional(),
388
408
  [SOURCE_FIELD.author]: sourcePath.optional(),
389
409
  /** A ticket source's status, shown on its badge in the herd; also a field `reportOn` can name. */
390
- [SOURCE_FIELD.status]: sourcePath.optional()
410
+ [SOURCE_FIELD.status]: sourcePath.optional(),
411
+ /** How often the entry happened (an error count), shown as `113×` beside the entry in the herd's `Sources` block. */
412
+ [SOURCE_FIELD.count]: sourcePath.optional()
391
413
  }).catchall(sourcePath).refine((map) => Object.keys(map).every((field) => SOURCE_FIELD_NAME.test(field)), {
392
414
  message: "a map field is a letter, then letters, digits and `_`"
393
415
  });
394
416
  var canChange = ({ map, reportOn }) => map.updatedAt !== void 0 || CONTENT_FIELDS.some((field) => map[field] !== void 0) || reportOn !== void 0;
417
+ var toneName = z2.enum(TONE_NAMES);
395
418
  var sourceSchema = z2.strictObject({
396
419
  /** Printed in every report of the source and the key of its saved state. */
397
420
  name: z2.string().regex(/^[A-Za-z0-9][\w.-]*$/, "letters, digits, `_`, `.` and `-`"),
@@ -406,6 +429,8 @@ var sourceSchema = z2.strictObject({
406
429
  ignoreAuthors: z2.array(z2.string().min(1)).default([]),
407
430
  /** The most new or changed entries one report shows of this source; the rest is one line `+<n> more`. */
408
431
  reportLimit: z2.number().int().min(1).default(5),
432
+ /** Shown before a row of "Waiting for you" that came from this source (`Error log · Checkout: …`); without it the source's name. */
433
+ label: z2.string().min(1).max(40).regex(/^[^\r\n]+$/, "one line").optional(),
409
434
  /** The lane the source's reports name. */
410
435
  lane: z2.string().min(1).optional(),
411
436
  /**
@@ -413,6 +438,8 @@ var sourceSchema = z2.strictObject({
413
438
  * the lane above or the lane whose board row or plan names its id.
414
439
  */
415
440
  ticket: z2.boolean().optional(),
441
+ /** The tone of the source's mark in the herd's `Sources` block while its newest report is new; without it the tone of the role `new`. */
442
+ tone: toneName.optional(),
416
443
  /**
417
444
  * The mapped fields whose change reports, with the old and the new value, in place of the content of `title`,
418
445
  * `author`, `body` and `url`; without it a change of that content reports.
@@ -512,6 +539,12 @@ var hintsSchema = z2.strictObject({
512
539
  taskSignal: z2.boolean().default(true)
513
540
  });
514
541
  var DEFAULT_TASK_SIGNAL_MINUTES = 10;
542
+ var tonesSchema = z2.strictObject({
543
+ waiting: toneName.default(DEFAULT_TONES.waiting),
544
+ new: toneName.default(DEFAULT_TONES.new),
545
+ running: toneName.default(DEFAULT_TONES.running),
546
+ resting: toneName.default(DEFAULT_TONES.resting)
547
+ });
515
548
  var contextSchema = z2.strictObject({
516
549
  /** Tokens in a lane's context from which its fill shows yellow and `/implement` offers a new chat. */
517
550
  warnTokens: tokenCount.default(25e4),
@@ -581,7 +614,9 @@ var jackherdConfigSchema = z2.strictObject({
581
614
  icon: z2.string().min(1)
582
615
  })
583
616
  ).default(DEFAULT_LINK_KINDS.map((kind) => ({ ...kind }))),
584
- context: contextSchema.prefault({})
617
+ context: contextSchema.prefault({}),
618
+ /** The colour of each role in the herd, by name: what waits for you, what is new, what runs, what rests. */
619
+ tones: tonesSchema.prefault({})
585
620
  }).prefault({}),
586
621
  /**
587
622
  * Read by `jh watch`; without `deploy` it reports no deploy runs. `feedback` is on unless set to `false`:
@@ -64,6 +64,12 @@ declare const APPOINTMENT_VOCABULARY: Record<PlanLanguage, {
64
64
  /** Further header names that count as the status column. */
65
65
  statusAlso: readonly string[];
66
66
  open: readonly string[];
67
+ /** The lower-case word an action starts with when it is the weekly board grooming, which the herd's header counts from. */
68
+ grooming: string;
69
+ }>;
70
+ /** The heading of the board section of what the coordinator does next, a numbered list of at most three lines. */
71
+ declare const NEXT_VOCABULARY: Record<PlanLanguage, {
72
+ heading: string;
67
73
  }>;
68
74
  /**
69
75
  * The board section of what waits on the user's answer (a sent preview, an open question, a report to accept): its
@@ -568,4 +574,4 @@ declare const readSprintDocument: (markdown: string, language: PlanLanguage) =>
568
574
  */
569
575
  declare const readPhaseDependencies: (planMarkdown: string, language: PlanLanguage) => Record<string, string>;
570
576
 
571
- export { ADDRESS_FORM, APPOINTMENT_VOCABULARY, APPROVAL_TEXTS, type ChecklistItem, type ColumnModel, type DeferredItem, type DriftFinding, LANE_PROMPT_OPENING, LANGUAGE_NAME, type LaneProgress, type LongPhaseTitle, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, PHASE_CELL_LIMIT, PHASE_ID_PREFIX, PHASE_TITLE_LIMIT, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, PROSE_REASON, type PhaseDependency, type PhaseHeading, type PhaseProgress, type PhaseSchedule, 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, SHORT_NAME_LIMIT, SHORT_NAME_PROBLEM, STANDING_APPROVAL_TEXTS, type ShortNameFinding, type ShortNameProblem, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, WAITING_VOCABULARY, activeItem, boardRowsWithProse, boardRowsWithoutPhase, classifyStatus, columnModels, countDecisions, criticalPath, currentPhase, currentPhaseOf, deferralTarget, differsFromColumn, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isDocumentationGateItem, isOpenCheckboxLine, isPhaseFinished, isPhaseOpen, isPlanLanguage, isReviewGateLine, isTickedUnmerged, laneNameOf, laneProgress, longPhaseTitles, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, onlyDocumentationOpen, parallelGroups, parallelGroupsOfPlan, parseDependencyTable, parsePlanPhases, parseSprintLanes, phaseHeading, phaseHeadingPattern, phaseHeadings, phaseIdsOfCell, phaseIdsOfPlan, phaseLabel, phaseLabels, phaseProgress, phaseWaves, phasesText, phasesWithoutDependencyRow, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortNameFindings, shortTitle, sprintTitleOf, sumPhases, tallyPhases, wavesOfPlan, withoutRepeats };
577
+ export { ADDRESS_FORM, APPOINTMENT_VOCABULARY, APPROVAL_TEXTS, type ChecklistItem, type ColumnModel, type DeferredItem, type DriftFinding, LANE_PROMPT_OPENING, LANGUAGE_NAME, type LaneProgress, type LongPhaseTitle, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, NEXT_VOCABULARY, PHASE_CELL_LIMIT, PHASE_ID_PREFIX, PHASE_TITLE_LIMIT, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, PROSE_REASON, type PhaseDependency, type PhaseHeading, type PhaseProgress, type PhaseSchedule, 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, SHORT_NAME_LIMIT, SHORT_NAME_PROBLEM, STANDING_APPROVAL_TEXTS, type ShortNameFinding, type ShortNameProblem, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, WAITING_VOCABULARY, activeItem, boardRowsWithProse, boardRowsWithoutPhase, classifyStatus, columnModels, countDecisions, criticalPath, currentPhase, currentPhaseOf, deferralTarget, differsFromColumn, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isDocumentationGateItem, isOpenCheckboxLine, isPhaseFinished, isPhaseOpen, isPlanLanguage, isReviewGateLine, isTickedUnmerged, laneNameOf, laneProgress, longPhaseTitles, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, onlyDocumentationOpen, parallelGroups, parallelGroupsOfPlan, parseDependencyTable, parsePlanPhases, parseSprintLanes, phaseHeading, phaseHeadingPattern, phaseHeadings, phaseIdsOfCell, phaseIdsOfPlan, phaseLabel, phaseLabels, phaseProgress, phaseWaves, phasesText, phasesWithoutDependencyRow, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortNameFindings, shortTitle, sprintTitleOf, sumPhases, tallyPhases, wavesOfPlan, withoutRepeats };
@@ -4,6 +4,7 @@ import {
4
4
  APPROVAL_TEXTS,
5
5
  LANE_PROMPT_OPENING,
6
6
  LANGUAGE_NAME,
7
+ NEXT_VOCABULARY,
7
8
  PLAN_LANGUAGE,
8
9
  PLAN_STATUS,
9
10
  PLAN_VOCABULARY,
@@ -15,7 +16,7 @@ import {
15
16
  isPlanLanguage,
16
17
  phaseHeading,
17
18
  sectionHeading
18
- } from "./chunk-5MQBCUQ5.js";
19
+ } from "./chunk-RMMXD4G5.js";
19
20
 
20
21
  // src/plan-format/lanes.ts
21
22
  import { posix } from "path";
@@ -499,7 +500,7 @@ var dependsOf = (cell, language) => {
499
500
  return { after, external };
500
501
  };
501
502
  var pathOf = (entry) => {
502
- if (/\s/.test(entry)) return null;
503
+ if (/\s/.test(entry) || entry.includes("[]")) return null;
503
504
  const glob = entry.search(GLOB_CHARACTER);
504
505
  const literal = glob === -1 ? entry : entry.slice(0, Math.max(entry.lastIndexOf("/", glob), 0));
505
506
  const path = literal.replace(/^\.\//, "").replace(/\/+$/, "");
@@ -902,6 +903,7 @@ export {
902
903
  APPROVAL_TEXTS,
903
904
  LANE_PROMPT_OPENING,
904
905
  LANGUAGE_NAME,
906
+ NEXT_VOCABULARY,
905
907
  PHASE_CELL_LIMIT,
906
908
  PHASE_ID_PREFIX,
907
909
  PHASE_TITLE_LIMIT,
@@ -1,6 +1,6 @@
1
1
  id = "crossdelta.jackherd"
2
2
  name = "jackherd"
3
- version = "1.10.0"
3
+ version = "1.11.0"
4
4
  min_herdr_version = "0.8.2"
5
5
  description = "Show the herd, the state of a board's lanes, in a herdr pane"
6
6
  platforms = ["linux", "macos"]
package/install.sh CHANGED
@@ -9,7 +9,7 @@ set -eu
9
9
 
10
10
  # The default is the version this file ships with. The release bump changes it together with
11
11
  # package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
12
- JACKHERD_VERSION="${JACKHERD_VERSION:-1.10.0}"
12
+ JACKHERD_VERSION="${JACKHERD_VERSION:-1.11.0}"
13
13
 
14
14
  JACKHERD_PACKAGE='jackherd'
15
15
  # Test seam: the terminal the wizard asks its questions on.
@@ -28,7 +28,8 @@ export const relevantLines = (text: string): string => {
28
28
  * Records the questions of the session's dialogs that ended without an answer, and closes the open records of every
29
29
  * session that a dialog in this transcript has answered since. The whole transcript is read, not the last turn: a
30
30
  * rejected dialog ends its turn without a Stop hook, so it is found at the end of the next turn. Claude Code's hook
31
- * input: `cwd`, `session_id`, `transcript_path`. Writes nothing on any failure.
31
+ * input: `cwd`, `session_id`, `transcript_path`. A question whose text has an open record already adds none, so asking
32
+ * it again and putting it off again leaves one. Writes nothing on any failure.
32
33
  */
33
34
  export const runOpenQuestion = (input: unknown, dependencies: OpenQuestionDependencies): void => {
34
35
  try {
@@ -44,8 +45,12 @@ export const runOpenQuestion = (input: unknown, dependencies: OpenQuestionDepend
44
45
  if (answer) dependencies.writeRecord(main, { ...record, closedAt: answer.answeredAt })
45
46
  }
46
47
  const known = new Set(stored.map(({ id }) => id))
47
- for (const record of openQuestionsOf(entries, { cwd, session, answered, idOf: dependencies.idOf }))
48
- if (!known.has(record.id)) dependencies.writeRecord(main, record)
48
+ const openTexts = new Set(stored.filter(({ closedAt }) => closedAt === null).map(({ question }) => question))
49
+ for (const record of openQuestionsOf(entries, { cwd, session, answered, idOf: dependencies.idOf })) {
50
+ if (known.has(record.id) || openTexts.has(record.question)) continue
51
+ openTexts.add(record.question)
52
+ dependencies.writeRecord(main, record)
53
+ }
49
54
  } catch {
50
55
  // A failed record is no record; the herd shows no question.
51
56
  }
@@ -42,6 +42,8 @@ Add a fenced ` ```mermaid ` block **when it removes reader effort**:
42
42
 
43
43
  Keep each diagram to one idea; two small diagrams beat one dense one. Small or single-file PRs need none.
44
44
 
45
+ Write edge labels as `-->|"label"|` (`-- text -->` breaks on a `--` in it); `jh mermaid <body file>` must exit 0 before `gh pr create` or `gh pr edit`.
46
+
45
47
  ## Review order (large PRs)
46
48
 
47
49
  When a PR spans many files, add a short **suggested review order** — **pure/domain logic first, adapters next, UI and wiring last**. A `| # | Area | Start here | Why first |` table works well.
@@ -15,8 +15,8 @@ You add one entry to `watch.sources`, so that `jh watch` reports a new or change
15
15
  - **Service:** the services the recipe index lists, and "another command that prints JSON".
16
16
  - **What to follow:** the item the argument names (its comments, recommended) or a query over several items.
17
17
  - **Lane:** the lane the reports name, from the board's lanes table, or none.
18
- - **Ticket:** whether the entries are tickets (`ticket: true`, recommended for an issue tracker or a project board), which the herd shows as a badge beside the plan of their lane: the lane above, or the lane whose name, board row or plan names the ticket's id. A source of comments or messages is no ticket. The path of the status you find yourself in step 3.
19
- - **What reports:** a change of what the user reads (title, body, the latest comment; recommended), or a change of named fields such as a status (`reportOn` with those fields, mapped in `map`).
18
+ - **Ticket:** whether the entries are tickets (`ticket: true`, recommended for an issue tracker or a project board), which the herd shows as a badge beside the plan of their lane: the lane above, or the lane whose name, board row or plan names the ticket's id. A source of comments or messages is no ticket; it shows in the herd's `Sources` block instead (a click opens the entry at `map.url`). The path of the status you find yourself in step 3.
19
+ - **Tone:** for a non-ticket source, the colour of its `Sources` mark while its report is new (`tone`: amber, teal, grey, red, green, blue, magenta); recommended: no `tone`.- **What reports:** a change of what the user reads (title, body, the latest comment; recommended), or a change of named fields such as a status (`reportOn` with those fields, mapped in `map`).
20
20
  - **Own changes:** ignore the user's own account on that service (`ignoreAuthors`, needs an author in the map), or report everything.
21
21
 
22
22
  ## 2. Check, read only
@@ -30,7 +30,7 @@ Anything missing goes to the user as the command to run themselves (`! <command>
30
30
 
31
31
  Before filling, read the help of the installed CLI version for every command the recipe calls (`acli jira workitem search --help` and `workitem view --help`, `gh issue list --help`) and confirm that the fields and options the entry needs exist there. Read the provider's web documentation only when the help leaves it open or no recipe fits. A recipe is a starting point; the installed version decides.
32
32
 
33
- Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors` and `ticket` where chosen. For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it, from the recipe's script under `recipes/sources/`, to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]`. Script and direct command follow the index's **Rules every recipe follows**.
33
+ Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors`, `ticket` and `tone` where chosen, and a short `label` (e.g. `Error log · Checkout`; it prefixes the source's `Waiting for you` rows). For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it, from the recipe's script under `recipes/sources/`, to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]`. Script and direct command follow the index's **Rules every recipe follows**.
34
34
 
35
35
  Write the entry as one JSON object to a draft file in the scratchpad and run, from the main checkout:
36
36
 
@@ -67,7 +67,7 @@ Each file under `references/` is read when its trigger fires: the step or the ev
67
67
  `jh lane prompt <lane> [--board <board>]` writes the lane prompt from the lane's board row and its plan, in
68
68
  the plan's language, to `.claude/handover/<lane>-prompt.md` and prints the path. It opens with a first line
69
69
  that names the lane, which the hooks and the project's rules read: keep that line as it is. Before you send
70
- it, add to the file with `Edit` the open questions that touch the lane and what the project
70
+ it, add to the file with `Edit`, below the marker line at its end (it stays through every new writing), the open questions that touch the lane and what the project
71
71
  supplement adds to the lane prompt. Never paste the rules of this skill into a lane prompt. The lane asks you in text blocks at the end of its turn; answer them with
72
72
  `herdr agent prompt` (§ 2 step 8).
73
73
 
@@ -290,10 +290,18 @@ watcher reports an open row once when it falls due (`due`, above); an empty stat
290
290
  and a reworded or moved row is a new appointment and reports again. Close a reported row through its
291
291
  status cell.
292
292
 
293
+ **Board grooming** is an appointment of its own, a row in that section whose action starts with
294
+ `Board-Pflege` (`Board grooming`); a board without one gets it, due a week from now. When it reports:
295
+ check that the open lane rows still serve the board's goal (a mismatch joins the bundled question), rewrite the
296
+ board's `Als Nächstes` (`Next`) section (three lines at most, each with a date), clear finished lane
297
+ rows, ask for outdated plans in one bundled question, then set its `When` a week after now and leave its status
298
+ empty: this is the one appointment that is moved, not closed. The herd
299
+ counts `groomed <n>d ago` from it and turns it amber at eight days.
300
+
293
301
  **Anything that waits on the user's answer** (a preview or a file sent to them, a question put to them, a
294
302
  report handed over for acceptance) goes into the board in the same turn you send it, in the section
295
303
  `## Wartet auf dich` (German plans) or `## Waiting for you` (English), one row `Since | Item | Origin | Status`
296
- (`Seit | Punkt | Herkunft | Stand`) with the time as in **An agreed time**. It survives a hand-over, which a
304
+ (`Seit | Punkt | Herkunft | Stand`) with the time as in **An agreed time**; for a source's report the origin is the source's name, and the herd prefixes the row with its label. It survives a hand-over, which a
297
305
  message in this chat does not. Close a row in its status cell in the turn the user's answer arrives (also a
298
306
  "no"), or when the thing is withdrawn; an empty status counts as open. A file sent with `SendUserFile` and an
299
307
  Artifact you publish need no row (the kit hook `sent-file` records each): close them after the answer with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jackherd",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "keywords": [
5
5
  "claude-code",
6
6
  "coding-agents",
@@ -60,6 +60,10 @@ A run that ends with an exit code other than 0, runs past 30 seconds (then its w
60
60
 
61
61
  **Show a ticket beside its plan.** `"ticket": true` marks a source's entries as tickets. The herd then shows each one as a badge in the 📎 line of its lane's plan, right after `📄 Plan`: `📡 T-123 · In Progress`, linked to the ticket. A ticket belongs to the source's `lane`, and also to any lane whose board row, name or plan names its id as a whole word (`T-12` is not found in `T-123`); so one source listing all your open tickets works without `lane`. `map.status` is the status the badge shows; without it the badge shows only the key. A ticket whose last reported change is less than a day old is amber, and a folded lane then shows `📡 <n>` in amber before its bar, linked to the first new ticket. A lane shows at most three tickets, new ones first, then `+<n>`; a link chip of the plan with the same URL gives way to the badge. For this the saved state keeps, of a marked source only, each entry of its last successful run with its URL, its status (stored like a `reportOn` value) and the time its last change was reported. The badges come from the watcher's state like the pull request badges: without a running watcher or with a state older than three minutes there are none, and the header says `watcher off` or `state 4 min old`, also without a pull request. The [GitHub Projects recipe](#user-content-recipes) is a ticket source with a status.
62
62
 
63
+ **Show a source in the herd.** A source without `ticket` appears in the herd's `Sources` block, one row each: its name, when it last ran (`ran 12 min ago`), since when it fails (`failing since 3 h ago`, mark red) and its newest report (mark amber while that report is under a day old). A source with several entries is one folded row; a click opens its entries (up to five, then `+<n> more`), each with its title, `map.count` as `113×` and the time of its last change. A click on an entry opens its `url` (the watcher keeps it only as a checked `https:` URL of at most 200 characters, so map a short link), `[ask]` asks the coordinator to look at that entry. An entry that an open row of "Waiting for you" already asks about (the row's `Origin` names the source and its text the title) is dim and says `↑ asked`; the row is amber only for a new entry nobody asked about. The watcher keeps the last 20 entries of such a source. `tone` (amber, teal, grey, red, green, blue or magenta) sets the mark's colour while the report is new, instead of the tone of the role `new`; the roles `waiting`, `new`, `running` and `resting` take their tones from `plans.tones` in the config (defaults: amber, amber, teal, grey).
64
+
65
+ **Name a source in "Waiting for you".** A row of the board's waiting section whose `Origin` cell is the name of a source (case ignored) shows `<label>: <row>` in the herd; `label` (one line, at most 40 characters) is optional, without it the source's name stands before the row. `/jh-follow` proposes a short label when it writes the entry.
66
+
63
67
  **How you hear about it.** The report goes to the coordinator, as a `=== source …` event of its watcher. It passes you one line with the entry, its gist and the link. A decision inside it, such as a product owner's answer, it takes over only after you confirmed it, since the text comes from outside. You hear of it only while the coordinator's session and its watcher run; what happened in between is reported at the next start. There is no desktop notification of its own: you get the one herdr shows for any lane that needs you. What a source reports is a change in the mapped content fields (`title`, `author`, `body`, `url`), or with `reportOn` a change of the fields it names, such as a status.
64
68
 
65
69
  **Try a source before the watcher runs it.** `jh watch source <name> --dry-run` runs a configured source once; a draft not yet in the config comes as a file (`jh watch source draft.json --dry-run`, one source object) or on stdin (`-`). It lists the entries (`<id> · <author> · <title>`), says that the watcher's first run of the source is a baseline and reports nothing, and shows one entry as a later report would print it; it neither reads nor changes the watcher's state. A field of `map` that is empty in every entry is named with its path (`map.updatedAt: in 23 of 23 entries empty (.fields.updated)`), read from the raw output, so a run that failed on a missing `id` or `updatedAt` names it too; the dry run then ends with exit code 1, as for a failed run, and `/jh-follow` does not write the source. Without entries it says `0 entries: map not checked`. `--raw` adds the command's output, for finding the paths of `items` and `map`:
@@ -297,6 +297,65 @@
297
297
  }
298
298
  },
299
299
  "additionalProperties": false
300
+ },
301
+ "tones": {
302
+ "default": {},
303
+ "type": "object",
304
+ "properties": {
305
+ "waiting": {
306
+ "default": "amber",
307
+ "type": "string",
308
+ "enum": [
309
+ "amber",
310
+ "teal",
311
+ "grey",
312
+ "red",
313
+ "green",
314
+ "blue",
315
+ "magenta"
316
+ ]
317
+ },
318
+ "new": {
319
+ "default": "amber",
320
+ "type": "string",
321
+ "enum": [
322
+ "amber",
323
+ "teal",
324
+ "grey",
325
+ "red",
326
+ "green",
327
+ "blue",
328
+ "magenta"
329
+ ]
330
+ },
331
+ "running": {
332
+ "default": "teal",
333
+ "type": "string",
334
+ "enum": [
335
+ "amber",
336
+ "teal",
337
+ "grey",
338
+ "red",
339
+ "green",
340
+ "blue",
341
+ "magenta"
342
+ ]
343
+ },
344
+ "resting": {
345
+ "default": "grey",
346
+ "type": "string",
347
+ "enum": [
348
+ "amber",
349
+ "teal",
350
+ "grey",
351
+ "red",
352
+ "green",
353
+ "blue",
354
+ "magenta"
355
+ ]
356
+ }
357
+ },
358
+ "additionalProperties": false
300
359
  }
301
360
  },
302
361
  "additionalProperties": false
@@ -483,6 +542,9 @@
483
542
  },
484
543
  "status": {
485
544
  "type": "string"
545
+ },
546
+ "count": {
547
+ "type": "string"
486
548
  }
487
549
  },
488
550
  "required": [
@@ -506,6 +568,12 @@
506
568
  "minimum": 1,
507
569
  "maximum": 9007199254740991
508
570
  },
571
+ "label": {
572
+ "type": "string",
573
+ "minLength": 1,
574
+ "maxLength": 40,
575
+ "pattern": "^[^\\r\\n]+$"
576
+ },
509
577
  "lane": {
510
578
  "type": "string",
511
579
  "minLength": 1
@@ -513,6 +581,18 @@
513
581
  "ticket": {
514
582
  "type": "boolean"
515
583
  },
584
+ "tone": {
585
+ "type": "string",
586
+ "enum": [
587
+ "amber",
588
+ "teal",
589
+ "grey",
590
+ "red",
591
+ "green",
592
+ "blue",
593
+ "magenta"
594
+ ]
595
+ },
516
596
  "reportOn": {
517
597
  "minItems": 1,
518
598
  "maxItems": 10,
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ export const COMMANDS: CommandTable = {
25
25
  init: () => import('./sync/init-command'),
26
26
  [HERD_COMMAND.alias]: () => import('./plans/command'),
27
27
  lane: () => import('./lane/command'),
28
+ mermaid: () => import('./mermaid/command'),
28
29
  preview: () => import('./preview/command'),
29
30
  settled: () => import('./settled/command'),
30
31
  sync: () => import('./sync/command'),