jackherd 1.23.3 → 1.25.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 (90) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/INSTALL.md +16 -0
  3. package/README.md +23 -22
  4. package/dist/{chunk-JDO6O3UO.js → chunk-O365KHHP.js} +139 -3
  5. package/dist/config.d.ts +13 -1
  6. package/dist/config.js +178 -132
  7. package/dist/plan-format.d.ts +121 -3
  8. package/dist/plan-format.js +176 -35
  9. package/docs/plan-contract.md +52 -5
  10. package/herdr-plugin/herdr-plugin.toml +1 -1
  11. package/install.sh +1 -1
  12. package/kit/hooks/hooks/approval-gate/index.ts +2 -2
  13. package/kit/hooks/hooks/merge-guard/lane-plan.ts +1 -1
  14. package/kit/hooks/hooks/shared/board-lanes.ts +33 -12
  15. package/kit/hooks/hooks/shared/boards.ts +22 -6
  16. package/kit/workflow/skills/orchestrate/SKILL.md +2 -1
  17. package/kit/workflow/skills/orchestrate/references/backend.md +30 -0
  18. package/kit/workflow/skills/orchestrate/references/close-lane.md +1 -0
  19. package/kit/workflow/skills/orchestrate/references/herd.md +3 -3
  20. package/kit/workflow/skills/orchestrate/references/start-lane.md +2 -2
  21. package/kit/workflow/skills/plan/SKILL.md +3 -1
  22. package/kit/workflow/skills/plan/references/template-English.md +2 -0
  23. package/kit/workflow/skills/plan/references/template-German.md +2 -0
  24. package/package.json +1 -1
  25. package/schemas/jackherd.schema.json +49 -0
  26. package/src/backend/command-line.ts +116 -0
  27. package/src/backend/command.ts +14 -0
  28. package/src/backend/control.ts +194 -0
  29. package/src/backend/handover.ts +28 -0
  30. package/src/backend/herdr.ts +147 -0
  31. package/src/backend/lock.ts +84 -0
  32. package/src/backend/process-cwd.ts +32 -0
  33. package/src/backend/schema.ts +47 -0
  34. package/src/backend/state.ts +57 -0
  35. package/src/backend/step.ts +93 -0
  36. package/src/backend/system.ts +150 -0
  37. package/src/backend/watch.ts +75 -0
  38. package/src/brief/command.ts +1 -1
  39. package/src/brief/lane-facts.ts +2 -2
  40. package/src/check/command.ts +2 -1
  41. package/src/check/plan-structure-check.ts +68 -14
  42. package/src/check/settings.ts +6 -1
  43. package/src/check/skill-budget-defaults.ts +6 -3
  44. package/src/cli.ts +1 -0
  45. package/src/config/schema.ts +6 -0
  46. package/src/lane/close-board.ts +4 -4
  47. package/src/lane/close-command.ts +2 -2
  48. package/src/lane/dev-command.ts +1 -1
  49. package/src/lane/dev.ts +2 -2
  50. package/src/lane/port-command.ts +1 -1
  51. package/src/plan-contract/command.ts +2 -1
  52. package/src/plan-contract/contract.ts +30 -9
  53. package/src/plan-format/bare-codes.ts +4 -4
  54. package/src/plan-format/dependencies.ts +5 -5
  55. package/src/plan-format/lanes.ts +3 -4
  56. package/src/plan-format/markdown.ts +125 -10
  57. package/src/plan-format/open-status.ts +15 -0
  58. package/src/plan-format/phases.ts +45 -4
  59. package/src/plan-format/plan-head.ts +31 -9
  60. package/src/plan-format/plan-structure.ts +43 -7
  61. package/src/plan-format/progress.ts +3 -0
  62. package/src/plan-format/sprint-document.ts +4 -4
  63. package/src/plan-format/unrecognized-sections.ts +21 -0
  64. package/src/plan-format/waiting.ts +13 -13
  65. package/src/plans/board-close.ts +25 -16
  66. package/src/plans/board-pick.ts +1 -1
  67. package/src/plans/ensure-panes.ts +2 -2
  68. package/src/plans/layout-summary.ts +8 -1
  69. package/src/plans/layout.ts +14 -5
  70. package/src/plans/live.ts +1 -1
  71. package/src/plans/plan-panes.ts +1 -1
  72. package/src/plans/settings.ts +6 -0
  73. package/src/plans/snapshot.ts +9 -5
  74. package/src/plans/sprint-export.ts +1 -1
  75. package/src/plans/texts.ts +3 -0
  76. package/src/plans/view.ts +1 -0
  77. package/src/resume/coordinator-read.ts +2 -2
  78. package/src/settled/command.ts +1 -1
  79. package/src/settled/settle.ts +5 -5
  80. package/src/shared/shell-word.ts +2 -0
  81. package/src/sync/render.ts +20 -5
  82. package/src/sync/sync-plan.ts +6 -2
  83. package/src/sync/sync.ts +3 -2
  84. package/src/vocabulary/index.ts +192 -4
  85. package/src/watch/appointments.ts +13 -20
  86. package/src/watch/command.ts +32 -5
  87. package/src/watch/lane-progress.ts +1 -1
  88. package/src/watch/merge-overlap-read.ts +2 -2
  89. package/src/watch/plan-missing-read.ts +3 -1
  90. package/src/watch/reference-paths.ts +4 -0
package/dist/config.js CHANGED
@@ -1,17 +1,61 @@
1
1
  import {
2
2
  PLAN_LANGUAGE,
3
3
  RESPONSE_ADDRESS
4
- } from "./chunk-JDO6O3UO.js";
4
+ } from "./chunk-O365KHHP.js";
5
5
 
6
6
  // src/config/json-schema.ts
7
- import { z as z3 } from "zod";
7
+ import { z as z4 } from "zod";
8
8
 
9
9
  // src/config/schema.ts
10
- import { z as z2 } from "zod";
10
+ import { z as z3 } from "zod";
11
11
 
12
- // src/check/schema.ts
12
+ // src/backend/schema.ts
13
13
  import { z } from "zod";
14
14
 
15
+ // src/version/release.ts
16
+ var CLI_NAME = "jh";
17
+
18
+ // src/lane/port.ts
19
+ var DEFAULT_BASE_PORT = 3100;
20
+ var MAX_PORT = 65535;
21
+ var BOARD = "--board";
22
+ var LANE_PORT_USAGE = `usage: ${CLI_NAME} lane port [${BOARD} <board.md>]
23
+
24
+ Prints the dev-server port for a new lane: the lowest port from lanes.basePort of the config (default ${DEFAULT_BASE_PORT})
25
+ that nothing listens on and no lane still running holds in the port column of the board. Rows of cleaned-up lanes
26
+ reserve nothing. Exit 1 and one line when no port up to ${MAX_PORT} is free.`;
27
+
28
+ // src/shared/shell-word.ts
29
+ var SHELL_WORD = /^(?!-)[\w@./:-]+$/;
30
+
31
+ // src/backend/schema.ts
32
+ var SERVICES_PLACEHOLDER = "{services}";
33
+ var DEFAULT_BACKEND_LABEL = "backend";
34
+ var BACKEND_LABEL = /^(?!-)[\w.-]+$/;
35
+ var duplicateIn = (values) => new Set(values).size !== values.length;
36
+ var serviceSchema = z.strictObject({
37
+ /** The name the command's tool takes, e.g. a package name. */
38
+ name: z.string().regex(SHELL_WORD, "must be one shell word of letters, digits and @ . / : _ -, not starting with -"),
39
+ /** The port the service listens on; checked before a start and named when the service is handed to a lane. */
40
+ port: z.number().int().min(1).max(MAX_PORT)
41
+ });
42
+ var backendSchema = z.strictObject({
43
+ /** The command that runs the services in the main checkout; `{services}` becomes the names of the services not handed to a lane. */
44
+ command: z.string().min(1).refine((command3) => command3.includes(SERVICES_PLACEHOLDER), `must contain ${SERVICES_PLACEHOLDER}`),
45
+ services: z.array(serviceSchema).min(1),
46
+ /** The label of the backend pane; `backend` when left out. */
47
+ label: z.string().regex(BACKEND_LABEL, "letters, digits and . _ -, not starting with -").default(DEFAULT_BACKEND_LABEL)
48
+ }).refine(({ services }) => !duplicateIn(services.map(({ name }) => name)), {
49
+ message: "service names must be unique",
50
+ path: ["services"]
51
+ }).refine(({ services }) => !duplicateIn(services.map(({ port }) => port)), {
52
+ message: "service ports must be unique",
53
+ path: ["services"]
54
+ });
55
+
56
+ // src/check/schema.ts
57
+ import { z as z2 } from "zod";
58
+
15
59
  // src/check/command-line.ts
16
60
  var TOKEN = /(?:"[^"]*"|'[^']*'|\S)+/g;
17
61
  var QUOTED = /"([^"]*)"|'([^']*)'/g;
@@ -24,7 +68,7 @@ var tokenize = (command3) => (command3.match(TOKEN) ?? []).map((token) => {
24
68
  var CHECK_KIND = { lint: "lint", project: "project", types: "types", tests: "tests" };
25
69
 
26
70
  // src/check/schema.ts
27
- var nonEmpty = z.string().min(1);
71
+ var nonEmpty = z2.string().min(1);
28
72
  var isTokenizable = (command3) => {
29
73
  try {
30
74
  tokenize(command3);
@@ -35,7 +79,7 @@ var isTokenizable = (command3) => {
35
79
  };
36
80
  var command = nonEmpty.refine(isTokenizable, "Unterminated quote in command");
37
81
  var glob = nonEmpty.refine((value) => !/[{}[\]]/.test(value), "Braces and brackets are not supported in globs");
38
- var globs = z.array(glob).min(1);
82
+ var globs = z2.array(glob).min(1);
39
83
  var directory = nonEmpty.transform((value) => value.replace(/^(\.\/)+/, "").replace(/\/+$/, "") || ".");
40
84
  var DEFAULT_BUILD_ARTIFACT_GLOBS = [
41
85
  "**/tsup.config.bundled_*",
@@ -44,12 +88,12 @@ var DEFAULT_BUILD_ARTIFACT_GLOBS = [
44
88
  "**/node_modules/**",
45
89
  "**/*.tsbuildinfo"
46
90
  ];
47
- var reviewerRoute = z.strictObject({
91
+ var reviewerRoute = z2.strictObject({
48
92
  reviewer: nonEmpty,
49
93
  globs,
50
94
  why: nonEmpty
51
95
  });
52
- var projectCheck = z.strictObject({
96
+ var projectCheck = z2.strictObject({
53
97
  name: nonEmpty,
54
98
  command,
55
99
  /** Without globs the check runs on every change; with them, only when a changed path matches. */
@@ -58,41 +102,41 @@ var projectCheck = z.strictObject({
58
102
  * `tests` for a check that runs the project's tests, `types` for its type check; without one, a plain project check.
59
103
  * A test run counts towards "no green without a test", and both run with the slow checks.
60
104
  */
61
- kind: z.enum([CHECK_KIND.tests, CHECK_KIND.types]).optional()
105
+ kind: z2.enum([CHECK_KIND.tests, CHECK_KIND.types]).optional()
62
106
  });
63
- var generatorOwned = z.strictObject({
107
+ var generatorOwned = z2.strictObject({
64
108
  glob,
65
109
  generator: nonEmpty
66
110
  });
67
- var publicContent = z.strictObject({
111
+ var publicContent = z2.strictObject({
68
112
  /** The published files: a change here gets the reviewer and the scan for names of neighbouring projects. */
69
113
  globs,
70
114
  /** Files among them that never ship, such as tests. */
71
- exclude: z.array(glob).default([]),
115
+ exclude: z2.array(glob).default([]),
72
116
  /** The agent that judges such a change; a global install names it with its prefix. */
73
117
  reviewer: nonEmpty.default("public-content")
74
118
  });
75
- var skillBudgets = z.record(glob, z.number().int().positive());
119
+ var skillBudgets = z2.record(glob, z2.number().int().positive());
76
120
  var OPEN_BOXES_LEVEL = { fail: "fail", warn: "warn", off: "off" };
77
121
  var LINT_POINTERS_LEVEL = { warn: "warn", fail: "fail", off: "off" };
78
- var lintPointers = z.strictObject({
122
+ var lintPointers = z2.strictObject({
79
123
  /** `warn` reports a block and lets the commit through; `fail` stops it; `off` skips the check. */
80
- level: z.enum([LINT_POINTERS_LEVEL.warn, LINT_POINTERS_LEVEL.fail, LINT_POINTERS_LEVEL.off]).default(LINT_POINTERS_LEVEL.warn),
124
+ level: z2.enum([LINT_POINTERS_LEVEL.warn, LINT_POINTERS_LEVEL.fail, LINT_POINTERS_LEVEL.off]).default(LINT_POINTERS_LEVEL.warn),
81
125
  /** Rule files left unread, as globs from the project root; the kit copies in the lock file are always left out. */
82
- exclude: z.array(glob).default([])
126
+ exclude: z2.array(glob).default([])
83
127
  });
84
128
  var DEFAULT_LINT_POINTERS = { level: LINT_POINTERS_LEVEL.warn, exclude: [] };
85
- var checkConfigSchema = z.strictObject({
129
+ var checkConfigSchema = z2.strictObject({
86
130
  /**
87
131
  * Run once over the changed files, which are appended as arguments. Separate from `commands.lint`,
88
132
  * which lints the whole project. Detected from the repository when left out; without one, no lint runs.
89
133
  */
90
134
  lintFilesCommand: command.optional(),
91
135
  /** Project checks, run alongside lint; add repository-specific scripts here. */
92
- checks: z.array(projectCheck).default([]),
93
- generatorOwned: z.array(generatorOwned).default([]),
94
- buildArtifacts: z.array(glob).default([...DEFAULT_BUILD_ARTIFACT_GLOBS]),
95
- reviewerRoutes: z.array(reviewerRoute).default([]),
136
+ checks: z2.array(projectCheck).default([]),
137
+ generatorOwned: z2.array(generatorOwned).default([]),
138
+ buildArtifacts: z2.array(glob).default([...DEFAULT_BUILD_ARTIFACT_GLOBS]),
139
+ reviewerRoutes: z2.array(reviewerRoute).default([]),
96
140
  publicContent: publicContent.optional(),
97
141
  /**
98
142
  * Word budgets for the files an agent loads again and again. An entry replaces the kit's budget for the same
@@ -104,19 +148,19 @@ var checkConfigSchema = z.strictObject({
104
148
  * default) stops the commit, `warn` reports, `off` skips. A row carrying the `partialPhase` word (`(Teil)` in German) builds part of the phase and is
105
149
  * left alone; a branch whose directory is no lane's is not checked.
106
150
  */
107
- openBoxes: z.enum([OPEN_BOXES_LEVEL.fail, OPEN_BOXES_LEVEL.warn, OPEN_BOXES_LEVEL.off]).default(OPEN_BOXES_LEVEL.fail),
151
+ openBoxes: z2.enum([OPEN_BOXES_LEVEL.fail, OPEN_BOXES_LEVEL.warn, OPEN_BOXES_LEVEL.off]).default(OPEN_BOXES_LEVEL.fail),
108
152
  /**
109
153
  * Whether a new or changed block of a rule file (or `CLAUDE.md`, `AGENTS.md`) that a linter could check, such
110
154
  * as a forbidden import path or call, must name its lint rule with `` `lint:<rule id>` `` (or `` `lint:none` `` and a reason).
111
155
  */
112
156
  lintPointers: lintPointers.optional(),
113
157
  /** Also run tests in directories that hold test files but belong to no package. */
114
- looseTests: z.boolean().default(false),
158
+ looseTests: z2.boolean().default(false),
115
159
  /**
116
160
  * Package roots, as directories from the repository root, read only where no stack is detected: a changed file
117
161
  * belongs to the nearest one above it, else to the repository root `.`.
118
162
  */
119
- roots: z.array(directory).min(1).optional(),
163
+ roots: z2.array(directory).min(1).optional(),
120
164
  /** The files a test covers; replaces the stack's pattern, or the rule of thumb where no stack is detected. */
121
165
  sourceFiles: globs.optional(),
122
166
  /** The test files; replaces the stack's pattern, or the rule of thumb where no stack is detected. */
@@ -132,9 +176,6 @@ var SKILL_PATH = new RegExp(`^${SKILLS_DIRECTORY}/([^/]+)/(.+)$`);
132
176
  var AGENT_PATH = new RegExp(`^${AGENTS_DIRECTORY}/([^/]+)\\.md$`);
133
177
  var claudePath = (...segments) => [CLAUDE_DIRECTORY, ...segments].join("/");
134
178
 
135
- // src/version/release.ts
136
- var CLI_NAME = "jh";
137
-
138
179
  // src/secrets/options.ts
139
180
  var SECRET_NAME = /^[A-Z][A-Z0-9_]*$/;
140
181
  var SECRET_USAGE = `Usage: ${CLI_NAME} secret set <NAME> store a value in the keychain, typed hidden in a terminal
@@ -348,14 +389,14 @@ var DEFAULT_SKILL_PREFIX = "jh-";
348
389
  var KIT_CORE = ["orchestrate", "start"];
349
390
  var KIT_ENTRY_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
350
391
  var FULL_MODEL_ID = /^claude-[a-z0-9][a-z0-9.-]*$/;
351
- var language = z2.enum(Object.values(PLAN_LANGUAGE));
352
- var modelId = z2.string().regex(FULL_MODEL_ID, "Use a full model ID such as claude-opus-5-5, not an alias like opus or sonnet");
353
- var command2 = z2.string().min(1);
354
- var excludedKitEntry = z2.string().regex(KIT_ENTRY_NAME, "a kit skill or agent name as the kit names it, e.g. devops").refine((name) => !KIT_CORE.includes(name), {
392
+ var language = z3.enum(Object.values(PLAN_LANGUAGE));
393
+ var modelId = z3.string().regex(FULL_MODEL_ID, "Use a full model ID such as claude-opus-5-5, not an alias like opus or sonnet");
394
+ var command2 = z3.string().min(1);
395
+ var excludedKitEntry = z3.string().regex(KIT_ENTRY_NAME, "a kit skill or agent name as the kit names it, e.g. devops").refine((name) => !KIT_CORE.includes(name), {
355
396
  error: (issue) => `${issue.input} is the kit's core and cannot be excluded`
356
397
  });
357
- var tokenCount = z2.number().int().positive();
358
- var commandsSchema = z2.strictObject({
398
+ var tokenCount = z3.number().int().positive();
399
+ var commandsSchema = z3.strictObject({
359
400
  test: command2.optional(),
360
401
  typecheck: command2.optional(),
361
402
  lint: command2.optional(),
@@ -367,65 +408,65 @@ var commandsSchema = z2.strictObject({
367
408
  });
368
409
  var COMMAND_KEYS = Object.keys(commandsSchema.shape);
369
410
  var DEFAULT_CONTEXT_WINDOW = 2e5;
370
- var laneCopyPath = z2.string().regex(
411
+ var laneCopyPath = z3.string().regex(
371
412
  // biome-ignore lint/suspicious/noControlCharactersInRegex: the class exists to refuse control characters
372
413
  /^(?![/-])(?!(?:.*\/)?\.\.(?:\/|$))[^'\x00-\x1f\x7f]+$/,
373
414
  "a relative path inside the checkout: no leading / or -, no .. segment, no quote or control character"
374
415
  );
375
416
  var warnBelowAlert = ({ warnTokens, alertTokens }) => warnTokens < alertTokens;
376
417
  var WARN_BELOW_ALERT = { message: "warnTokens must be below alertTokens", path: ["warnTokens"] };
377
- var lockFilePath = z2.string().regex(/^(?![/-])(?!(?:.*\/)?\.\.(?:\/|$))[A-Za-z0-9._/-]+$/, "a plain relative file name such as yarn.lock");
378
- var branchName = z2.string().regex(
418
+ var lockFilePath = z3.string().regex(/^(?![/-])(?!(?:.*\/)?\.\.(?:\/|$))[A-Za-z0-9._/-]+$/, "a plain relative file name such as yarn.lock");
419
+ var branchName = z3.string().regex(
379
420
  /^(?!HEAD$)(?![-/.])(?!.*(?:\.\.|\/\/|@\{|\.lock(?:\/|$)))[A-Za-z0-9._/-]+(?<![./])$/,
380
421
  "a branch name such as main: not HEAD, no leading - / or ., no .. and no spaces"
381
422
  );
382
- var workflowFile = z2.string().regex(/^(?![-.])[A-Za-z0-9._-]+\.ya?ml$/, "a workflow file name such as deploy.yml");
383
- var boardPattern = z2.string().regex(/^(?![-.])[A-Za-z0-9._*?-]+$/, "one directory name such as board-*: no / and no leading - or .");
384
- var boardFile = z2.string().regex(/^(?![-.])[A-Za-z0-9._-]+\.md$/, "one markdown file name such as board.md: no / and no leading - or .");
385
- var boardSchema = z2.strictObject({
423
+ var workflowFile = z3.string().regex(/^(?![-.])[A-Za-z0-9._-]+\.ya?ml$/, "a workflow file name such as deploy.yml");
424
+ var boardPattern = z3.string().regex(/^(?![-.])[A-Za-z0-9._*?-]+$/, "one directory name such as board-*: no / and no leading - or .");
425
+ var boardFile = z3.string().regex(/^(?![-.])[A-Za-z0-9._-]+\.md$/, "one markdown file name such as board.md: no / and no leading - or .");
426
+ var boardSchema = z3.strictObject({
386
427
  /** The directories under `planDirectory` that hold a board; the newest board is the default of `plans` and `watch`. */
387
428
  pattern: boardPattern.default("board-*"),
388
429
  /** The board document inside each of those directories. */
389
430
  file: boardFile.default("board.md"),
390
431
  /** What the project calls a board: the herd, its page and the watcher print it as written. */
391
- term: z2.string().min(1).default("board")
432
+ term: z3.string().min(1).default("board")
392
433
  });
393
- var deploySchema = z2.strictObject({
434
+ var deploySchema = z3.strictObject({
394
435
  /** The workflow whose runs on the base branch the watcher reports as `deploy`. */
395
436
  workflow: workflowFile,
396
437
  /** The deployment environments the watcher names as a run's target; any other reads `unknown`. */
397
- environments: z2.array(z2.string().min(1)).min(1)
438
+ environments: z3.array(z3.string().min(1)).min(1)
398
439
  });
399
- var feedbackSchema = z2.strictObject({
440
+ var feedbackSchema = z3.strictObject({
400
441
  /** GitHub logins whose feedback does not wake the coordinator, such as a status bot; case is ignored. */
401
- ignore: z2.array(z2.string().min(1)).default([]),
442
+ ignore: z3.array(z3.string().min(1)).default([]),
402
443
  /**
403
444
  * Texts of a bot's pull request comment that report no finding: such a comment (login ending `[bot]`, no inline
404
445
  * comment by the same author within five minutes of it) is no feedback. Compared with whitespace collapsed; the
405
446
  * default is the CI code review's comment for a clean run. An empty list counts every bot comment.
406
447
  */
407
- noFindingTemplates: z2.array(z2.string().min(1)).default([CLEAN_REVIEW_TEXT])
448
+ noFindingTemplates: z3.array(z3.string().min(1)).default([CLEAN_REVIEW_TEXT])
408
449
  });
409
450
  var DEFAULT_PARALLEL_TASKS_MAX_TOKENS = 1e5;
410
- var parallelTasksSchema = z2.strictObject({
451
+ var parallelTasksSchema = z3.strictObject({
411
452
  /**
412
453
  * The most the extra agents of a parallel proposal may cost before they do anything: the token baseline of a fresh
413
454
  * agent times the agents to add. Above it the watcher does not propose parallel tasks. The baseline is the context
414
455
  * the first answer of a lane's session carried.
415
456
  */
416
- maxTokens: z2.number().int().positive().default(DEFAULT_PARALLEL_TASKS_MAX_TOKENS),
457
+ maxTokens: z3.number().int().positive().default(DEFAULT_PARALLEL_TASKS_MAX_TOKENS),
417
458
  /**
418
459
  * The days, both included, on which a proposal that keeps every limit is marked `auto` and the coordinator starts it
419
460
  * without asking: subagents beside running work in a lane, at most two extra agents, no path that always asks.
420
461
  * `since` is the day it was switched on, `until` at most seven days later; outside the window it is off. Unset by
421
462
  * default, so every proposal asks.
422
463
  */
423
- auto: z2.strictObject({ since: z2.iso.date(), until: z2.iso.date() }).refine(({ since, until }) => until >= since, "until must not be before since").refine(
464
+ auto: z3.strictObject({ since: z3.iso.date(), until: z3.iso.date() }).refine(({ since, until }) => until >= since, "until must not be before since").refine(
424
465
  ({ since, until }) => windowDays(since, until) <= MAX_AUTO_WINDOW_DAYS,
425
466
  `until must be at most ${MAX_AUTO_WINDOW_DAYS} days after since`
426
467
  ).optional()
427
468
  });
428
- var checksSchema = z2.strictObject({
469
+ var checksSchema = z3.strictObject({
429
470
  /**
430
471
  * Finds a failed test in a line of a red run's log (`gh run view --log-failed`) after its job, step and timestamp
431
472
  * prefix: the first group is the name, else the match. The default is Bun's `(fail) <name> [<duration>]`. The
@@ -433,22 +474,22 @@ var checksSchema = z2.strictObject({
433
474
  * itself (`(a+)+`, `(\w*)*`, and with alternation `(a|aa)+`) is refused: a best-effort scan of the source against
434
475
  * catastrophic backtracking, not a proof.
435
476
  */
436
- 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)
477
+ failedTestPattern: z3.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)
437
478
  });
438
- var previewsSchema = z2.strictObject({
479
+ var previewsSchema = z3.strictObject({
439
480
  /** Reads the GitHub deployments of each head commit; a successful one outside production and `deploy.environments` names its `environment_url`. */
440
- deployments: z2.boolean().default(false),
481
+ deployments: z3.boolean().default(false),
441
482
  /**
442
483
  * GitHub logins whose pull request comments link a preview; nothing they write counts as feedback, so name a bot that
443
484
  * only posts previews, not one that also reviews. Case is ignored.
444
485
  */
445
- authors: z2.array(z2.string().min(1)).default([]),
486
+ authors: z3.array(z3.string().min(1)).default([]),
446
487
  /** Finds the URL in such a comment: its first group, else its match; without it the first https link. */
447
- urlPattern: z2.string().min(1).refine(isRegularExpression, "not a valid regular expression").optional()
488
+ urlPattern: z3.string().min(1).refine(isRegularExpression, "not a valid regular expression").optional()
448
489
  });
449
- var sourcePath = z2.string().refine(isSourcePath, "not a source path: `.`, or `.key`, `[n]` and `[-n]` in a chain");
490
+ var sourcePath = z3.string().refine(isSourcePath, "not a source path: `.`, or `.key`, `[n]` and `[-n]` in a chain");
450
491
  var SOURCE_FIELD_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
451
- var sourceMapSchema = z2.object({
492
+ var sourceMapSchema = z3.object({
452
493
  [SOURCE_FIELD.id]: sourcePath,
453
494
  /**
454
495
  * Changes whenever the entry changes; compared as text, so any stable form of a time or version works. Needed
@@ -469,49 +510,49 @@ var sourceMapSchema = z2.object({
469
510
  message: "a map field is a letter, then letters, digits and `_`"
470
511
  });
471
512
  var canChange = ({ map, reportOn }) => map.updatedAt !== void 0 || CONTENT_FIELDS.some((field) => map[field] !== void 0) || reportOn !== void 0;
472
- var toneName = z2.enum(TONE_NAMES);
473
- var sourceSchema = z2.strictObject({
513
+ var toneName = z3.enum(TONE_NAMES);
514
+ var sourceSchema = z3.strictObject({
474
515
  /** Printed in every report of the source and the key of its saved state. */
475
- name: z2.string().regex(/^[A-Za-z0-9][\w.-]*$/, "letters, digits, `_`, `.` and `-`"),
516
+ name: z3.string().regex(/^[A-Za-z0-9][\w.-]*$/, "letters, digits, `_`, `.` and `-`"),
476
517
  /** Program and arguments, run without a shell in the main checkout; it prints JSON on stdout and only reads. */
477
- command: z2.array(z2.string().min(1)).min(1),
518
+ command: z3.array(z3.string().min(1)).min(1),
478
519
  /** Minutes between two runs, at least 1. */
479
- every: z2.number().int().min(1).default(5),
520
+ every: z3.number().int().min(1).default(5),
480
521
  /** The path of the list in the output; without it the output is the list. */
481
522
  items: sourcePath.optional(),
482
523
  map: sourceMapSchema,
483
524
  /** Values of `map.author` whose changes wake nobody, such as the account the command runs as; case is ignored. */
484
- ignoreAuthors: z2.array(z2.string().min(1)).default([]),
525
+ ignoreAuthors: z3.array(z3.string().min(1)).default([]),
485
526
  /** The most new or changed entries one report shows of this source; the rest is one line `+<n> more`. */
486
- reportLimit: z2.number().int().min(1).default(5),
527
+ reportLimit: z3.number().int().min(1).default(5),
487
528
  /** Shown before a row of "Your turn" that came from this source (`Error log · Checkout: …`); without it the source's name. */
488
- label: z2.string().min(1).max(40).regex(/^[^\r\n]+$/, "one line").optional(),
529
+ label: z3.string().min(1).max(40).regex(/^[^\r\n]+$/, "one line").optional(),
489
530
  /** The lane the source's reports name. */
490
- lane: z2.string().min(1).optional(),
531
+ lane: z3.string().min(1).optional(),
491
532
  /**
492
533
  * Marks the entries as tickets: the herd shows each as a badge beside the plan of the lane it belongs to,
493
534
  * the lane above or the lane whose board row or plan names its id.
494
535
  */
495
- ticket: z2.boolean().optional(),
536
+ ticket: z3.boolean().optional(),
496
537
  /** 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`. */
497
538
  tone: toneName.optional(),
498
539
  /**
499
540
  * The mapped fields whose change reports, with the old and the new value, in place of the content of `title`,
500
541
  * `author`, `body` and `url`; without it a change of that content reports.
501
542
  */
502
- reportOn: z2.array(z2.string().min(1)).min(1).max(10).optional(),
543
+ reportOn: z3.array(z3.string().min(1)).min(1).max(10).optional(),
503
544
  /**
504
545
  * Names of secrets the command needs, handed to it as environment variables of the same name; each is looked up
505
546
  * in the keychain, then in the watcher's environment.
506
547
  */
507
- secrets: z2.array(z2.string().regex(SECRET_NAME, "capital letters, digits and `_`, starting with a letter")).max(10).refine((names) => new Set(names).size === names.length, "secret names must be unique").optional(),
548
+ secrets: z3.array(z3.string().regex(SECRET_NAME, "capital letters, digits and `_`, starting with a letter")).max(10).refine((names) => new Set(names).size === names.length, "secret names must be unique").optional(),
508
549
  /** `entries` shows the source's entries open, in a herd block of its own; not for a ticket source, whose entries are badges. */
509
- show: z2.literal(SOURCE_SHOW.entries).optional(),
550
+ show: z3.literal(SOURCE_SHOW.entries).optional(),
510
551
  /**
511
552
  * `owner/name` of the repositories a personal source runs in (matched without regard to case); without it every
512
553
  * repository. Only in the personal sources file: the repository's own config is for that repository already.
513
554
  */
514
- repos: z2.array(z2.string().regex(REPOSITORY_NAME, "owner/name, such as acme/tool")).min(1).max(20).refine((repos) => new Set(repos.map((repo) => repo.toLowerCase())).size === repos.length, "repos must be unique").optional()
555
+ repos: z3.array(z3.string().regex(REPOSITORY_NAME, "owner/name, such as acme/tool")).min(1).max(20).refine((repos) => new Set(repos.map((repo) => repo.toLowerCase())).size === repos.length, "repos must be unique").optional()
515
556
  }).refine(canChange, {
516
557
  message: "map needs updatedAt, a content field (title, author, body, url) or reportOn",
517
558
  path: ["map"]
@@ -529,7 +570,7 @@ var sourceSchema = z2.strictObject({
529
570
  path: ["reportOn"]
530
571
  });
531
572
  var hasUniqueNames = (sources) => new Set(sources.map(({ name }) => name)).size === sources.length;
532
- var repositorySources = z2.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").superRefine((sources, context) => {
573
+ var repositorySources = z3.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").superRefine((sources, context) => {
533
574
  sources.forEach((source, index) => {
534
575
  if (source.repos === void 0) return;
535
576
  context.addIssue({
@@ -540,7 +581,7 @@ var repositorySources = z2.array(sourceSchema).refine(hasUniqueNames, "source na
540
581
  });
541
582
  });
542
583
  var DEFAULT_CLEAR_TOKENS = 25e4;
543
- var coordinatorSchema = z2.strictObject({
584
+ var coordinatorSchema = z3.strictObject({
544
585
  /** Tokens in the coordinator's context from which the watcher reports `coordinator warn`. */
545
586
  warnTokens: tokenCount.default(35e4),
546
587
  /** Tokens in the coordinator's context from which the watcher reports `coordinator alert`. */
@@ -562,62 +603,62 @@ var coordinatorSchema = z2.strictObject({
562
603
  ...thresholds,
563
604
  clearTokens: clearTokens ?? Math.min(DEFAULT_CLEAR_TOKENS, thresholds.warnTokens - 1)
564
605
  }));
565
- var standingTool = z2.string().regex(
606
+ var standingTool = z3.string().regex(
566
607
  /^mcp__[A-Za-z0-9_-]+__execute_sql$/,
567
608
  "a full MCP tool name ending in execute_sql, such as mcp__<server>__execute_sql"
568
609
  ).refine((name) => !/prod/i.test(name), "a tool whose name says prod never runs under a standing approval");
569
- var guardedCommand = z2.string().regex(/^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*){1,3}$/, "a program and one to three plain words, such as gh pr merge");
570
- var checkFreePattern = z2.string().regex(/^[A-Za-z0-9_.*/-]+$/, "path words with * and ** only, such as docs/**/*.md").refine(
610
+ var guardedCommand = z3.string().regex(/^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*){1,3}$/, "a program and one to three plain words, such as gh pr merge");
611
+ var checkFreePattern = z3.string().regex(/^[A-Za-z0-9_.*/-]+$/, "path words with * and ** only, such as docs/**/*.md").refine(
571
612
  (pattern) => !pattern.startsWith("/") && !pattern.split("/").includes(".."),
572
613
  "a relative path that stays inside the repository"
573
614
  ).refine(
574
615
  (pattern) => pattern.split("/").some((segment) => /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment)),
575
616
  "a pattern with a whole path segment that is no wildcard, such as docs/**/*.md"
576
617
  );
577
- var alwaysAskPattern = z2.string().regex(/^[A-Za-z0-9_.*/-]+$/, "path words with * and ** only, such as .github/** or apps/*/src/**").refine(
618
+ var alwaysAskPattern = z3.string().regex(/^[A-Za-z0-9_.*/-]+$/, "path words with * and ** only, such as .github/** or apps/*/src/**").refine(
578
619
  (pattern) => pattern.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."),
579
620
  "a relative path that stays inside the repository, without an empty or . segment"
580
621
  );
581
- var checkName = z2.string().max(200).refine(
622
+ var checkName = z3.string().max(200).refine(
582
623
  (name) => name.trim() !== "" && ![...name].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127),
583
624
  "a check name as GitHub lists it, such as test"
584
625
  );
585
- var mergeSchema = z2.strictObject({
626
+ var mergeSchema = z3.strictObject({
586
627
  /**
587
628
  * Paths of pull requests that are merged without waiting for checks: when every file of the pull request matches one
588
629
  * of them, `/orchestrate` merges at once and the merge guard does not ask, for a merge bound to the head commit with
589
630
  * `--match-head-commit`. Empty means every merge waits for checks.
590
631
  */
591
- withoutChecks: z2.array(checkFreePattern).default([]),
632
+ withoutChecks: z3.array(checkFreePattern).default([]),
592
633
  /**
593
634
  * Whether a lane opens every pull request as a draft (`gh pr create --draft`), so pushes while it is worked on start
594
635
  * no workflow that skips drafts. `/orchestrate` runs `gh pr ready` before the merge and waits for the checks that
595
636
  * start then; the merge guard never lets a draft through. Off opens pull requests ready, as before.
596
637
  */
597
- openAsDraft: z2.boolean().default(false)
638
+ openAsDraft: z3.boolean().default(false)
598
639
  });
599
- var guardFields = z2.strictObject({
640
+ var guardFields = z3.strictObject({
600
641
  /**
601
642
  * The commands the merge guard hook asks the user about before they run, where they stand at a program position, also
602
643
  * behind options before the verb, a chain, a wrapper or `sh -c`. `gh api`, when listed, asks only for a write to a
603
644
  * merge, a file, a dispatch, a rerun or a ref. An empty list switches the guard off.
604
645
  */
605
- commands: z2.array(guardedCommand).default(["gh pr merge", "gh workflow run"]),
606
- selfMerge: z2.strictObject({
646
+ commands: z3.array(guardedCommand).default(["gh pr merge", "gh workflow run"]),
647
+ selfMerge: z3.strictObject({
607
648
  /** Whether `/orchestrate` offers "merge it yourself after green checks" in the pull request question; off also stops the hook honouring such an answer. */
608
- offer: z2.boolean().default(true),
649
+ offer: z3.boolean().default(true),
609
650
  /** How long an answer to that question lets `gh pr merge <number>` pass without asking, counted from the answer. */
610
- approvalMinutes: z2.number().int().min(1).max(1440).default(180)
651
+ approvalMinutes: z3.number().int().min(1).max(1440).default(180)
611
652
  }).prefault({}),
612
- approvals: z2.strictObject({
653
+ approvals: z3.strictObject({
613
654
  /** 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. */
614
- waitMinutes: z2.number().int().min(1).max(9).default(8),
655
+ waitMinutes: z3.number().int().min(1).max(9).default(8),
615
656
  /**
616
657
  * The calls a standing approval of the user may let through once the coordinator marked them checked: each tool
617
658
  * by its full name and the environment it writes to, only `stage`. A tool whose name says `prod` is refused, as the
618
659
  * hook refuses it under a broken config; which project a server reaches is the project's start script's to say.
619
660
  */
620
- standing: z2.strictObject({ tools: z2.record(standingTool, z2.enum(Object.values(STANDING_ENVIRONMENT))) }).optional()
661
+ standing: z3.strictObject({ tools: z3.record(standingTool, z3.enum(Object.values(STANDING_ENVIRONMENT))) }).optional()
621
662
  }).prefault({}),
622
663
  /**
623
664
  * Paths that always ask, on top of the ones jackherd brings (`.github/**`, `**\/migrations/**`, `**\/.env*`,
@@ -626,15 +667,15 @@ var guardFields = z2.strictObject({
626
667
  * `merge.withoutChecks` pattern never opens them, and the watcher never starts parallel work on them by itself. They
627
668
  * add to that list, never replace it. A standing approval covers them; only workflows, migrations and env files ask.
628
669
  */
629
- alwaysAsk: z2.array(alwaysAskPattern).default([]),
670
+ alwaysAsk: z3.array(alwaysAskPattern).default([]),
630
671
  /**
631
672
  * Checks that must be among the head commit's and green before a standing approval lets a merge pass, by their names
632
673
  * on GitHub. Unset, the merge needs at least one check and every one green; set, also each of these. Either way
633
674
  * GitHub must be able to merge it.
634
675
  */
635
- requiredChecks: z2.array(checkName).optional(),
676
+ requiredChecks: z3.array(checkName).optional(),
636
677
  /** Read until 2.0.0 as `alwaysAsk` (`exceptPaths`) and `requiredChecks`; the plan approval it was for is gone. */
637
- planApproval: z2.strictObject({ exceptPaths: z2.array(alwaysAskPattern).optional(), requiredChecks: z2.array(checkName).optional() }).optional().meta({ deprecated: true })
678
+ planApproval: z3.strictObject({ exceptPaths: z3.array(alwaysAskPattern).optional(), requiredChecks: z3.array(checkName).optional() }).optional().meta({ deprecated: true })
638
679
  });
639
680
  var guardWithoutPlanApproval = ({ planApproval, ...guard }) => {
640
681
  const requiredChecks = guard.requiredChecks ?? planApproval?.requiredChecks;
@@ -645,21 +686,21 @@ var guardWithoutPlanApproval = ({ planApproval, ...guard }) => {
645
686
  };
646
687
  };
647
688
  var guardSchema = guardFields.transform(guardWithoutPlanApproval);
648
- var hintsSchema = z2.strictObject({
689
+ var hintsSchema = z3.strictObject({
649
690
  /** Whether the `ask-hint` hook points out a last message that ends in a question while the turn used no `AskUserQuestion`; a lane session is never hinted. */
650
- askUserQuestion: z2.boolean().default(true),
691
+ askUserQuestion: z3.boolean().default(true),
651
692
  /** Whether the `task-hint` hook points a lane that edits a file without an open task signal to `jh task start`, once per task. */
652
- taskSignal: z2.boolean().default(true)
693
+ taskSignal: z3.boolean().default(true)
653
694
  });
654
695
  var DEFAULT_TASK_SIGNAL_MINUTES = 10;
655
- var tonesSchema = z2.strictObject({
696
+ var tonesSchema = z3.strictObject({
656
697
  waiting: toneName.default(DEFAULT_TONES.waiting),
657
698
  planned: toneName.default(DEFAULT_TONES.planned),
658
699
  new: toneName.default(DEFAULT_TONES.new),
659
700
  running: toneName.default(DEFAULT_TONES.running),
660
701
  resting: toneName.default(DEFAULT_TONES.resting)
661
702
  });
662
- var contextSchema = z2.strictObject({
703
+ var contextSchema = z3.strictObject({
663
704
  /** Tokens in a lane's context from which its fill shows yellow and `/implement` offers a new chat. */
664
705
  warnTokens: tokenCount.default(25e4),
665
706
  /** Tokens in a lane's context from which its fill shows red and the lane hands over at the next phase boundary. */
@@ -668,70 +709,75 @@ var contextSchema = z2.strictObject({
668
709
  * Context window in tokens by full model ID, only the upper bound of the share a view shows (the thresholds are
669
710
  * tokens); the log does not say which window a session ran with.
670
711
  */
671
- windows: z2.record(modelId, tokenCount).default({}),
712
+ windows: z3.record(modelId, tokenCount).default({}),
672
713
  /** The window assumed for a model not listed in `windows`. */
673
714
  defaultWindow: tokenCount.default(DEFAULT_CONTEXT_WINDOW),
674
715
  /** The coordinator's own thresholds, used by `jh context --coordinator`; the lanes keep `warnTokens` and `alertTokens`. */
675
716
  coordinator: coordinatorSchema.prefault({})
676
717
  }).refine(warnBelowAlert, WARN_BELOW_ALERT);
677
- var jackherdConfigSchema = z2.strictObject({
678
- $schema: z2.string().optional(),
718
+ var jackherdConfigSchema = z3.strictObject({
719
+ $schema: z3.string().optional(),
679
720
  responseLanguage: language.default(PLAN_LANGUAGE.en),
680
721
  /** Whether replies address the user with the familiar or the polite form where the language has both (German: du or Sie). */
681
- responseAddress: z2.enum(Object.values(RESPONSE_ADDRESS)).default(RESPONSE_ADDRESS.informal),
722
+ responseAddress: z3.enum(Object.values(RESPONSE_ADDRESS)).default(RESPONSE_ADDRESS.informal),
723
+ /** The language new plans and the plan templates are written in. Boards and plans are read in every known language whatever this says. */
682
724
  planLanguage: language.default(PLAN_LANGUAGE.en),
683
- planDirectory: z2.string().min(1).default("docs/plans"),
725
+ /** The language jh writes the board in (history lines, lane rows, waiting rows); `planLanguage` when left out. */
726
+ boardLanguage: language.optional(),
727
+ planDirectory: z3.string().min(1).default("docs/plans"),
684
728
  /** Where the boards live and what the project calls them. */
685
729
  board: boardSchema.prefault({}),
686
730
  /** Replaced by `board.pattern` in 0.13.0; named here only so an old config is refused with that hint. */
687
- sprintPattern: z2.never({ error: "sprintPattern was replaced by board.pattern (with board.file and board.term)" }).optional(),
688
- worktreeDirectory: z2.string().min(1).refine(isRepositoryDirectory, "must lie below the repository root: relative, not `.`, without `..`").default(claudePath(WORKTREES_DIRECTORY)),
731
+ sprintPattern: z3.never({ error: "sprintPattern was replaced by board.pattern (with board.file and board.term)" }).optional(),
732
+ worktreeDirectory: z3.string().min(1).refine(isRepositoryDirectory, "must lie below the repository root: relative, not `.`, without `..`").default(claudePath(WORKTREES_DIRECTORY)),
689
733
  baseBranch: branchName.default(DEFAULT_BASE_BRANCH),
690
734
  /**
691
735
  * Put before every skill and agent name. `jh sync --global` refuses to run without one: a personal skill
692
736
  * shadows a project skill of the same name. In a project it is optional and keeps the project's own skills of a
693
737
  * kit name (`implement`, `plan`, …) apart from the kit's.
694
738
  */
695
- skillPrefix: z2.union([z2.literal(""), z2.string().regex(SKILL_PREFIX, "lower-case letters and digits ending in a dash, e.g. kit-")]).default(""),
739
+ skillPrefix: z3.union([z3.literal(""), z3.string().regex(SKILL_PREFIX, "lower-case letters and digits ending in a dash, e.g. kit-")]).default(""),
696
740
  commands: commandsSchema.prefault({}),
697
741
  /**
698
742
  * Kit skills and agents `jh sync` leaves out, by the kit's name without `skillPrefix`; a name takes its skill and
699
743
  * its agent alike. An unchanged copy already installed is removed, a changed one stays untracked.
700
744
  */
701
- kit: z2.strictObject({ exclude: z2.array(excludedKitEntry).default([]) }).prefault({}),
745
+ kit: z3.strictObject({ exclude: z3.array(excludedKitEntry).default([]) }).prefault({}),
702
746
  /** How `/orchestrate` sets up a lane's worktree. */
703
- lanes: z2.strictObject({
747
+ lanes: z3.strictObject({
704
748
  /** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
705
- copy: z2.array(laneCopyPath).default([]),
749
+ copy: z3.array(laneCopyPath).default([]),
706
750
  /** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand; detected from the repository when left out. */
707
751
  lockFile: lockFilePath.optional(),
708
752
  /** Free gigabytes the disk must hold before `jh lane space` lets a new lane install its dependencies; 5 when left out. */
709
- minFreeGb: z2.number().positive().optional(),
753
+ minFreeGb: z3.number().positive().optional(),
710
754
  /** The port `jh lane port` starts counting from when it picks a lane's dev-server port; 3100 when left out. */
711
- basePort: z2.number().int().min(1).max(65535).optional(),
755
+ basePort: z3.number().int().min(1).max(65535).optional(),
712
756
  /** The command `jh lane dev` runs as `PORT=<port> <command>` in a pane beside the lane's agent; `{port}` and `{filter}` (`--filter <package>`) are filled in. Without it `jh lane dev` refuses. */
713
- devCommand: z2.string().min(1).optional()
757
+ devCommand: z3.string().min(1).optional(),
758
+ /** The services every lane's app talks to, run in one pane of the main checkout that the watcher starts and keeps; without it `jh backend` has nothing to run. */
759
+ backend: backendSchema.optional()
714
760
  }).prefault({}),
715
- models: z2.record(z2.enum(Object.values(MODEL_ROLE)), modelId).default(DEFAULT_MODELS),
716
- terminal: z2.strictObject({ adapter: z2.enum(Object.values(TERMINAL_ADAPTER)).default(TERMINAL_ADAPTER.none) }).prefault({}),
761
+ models: z3.record(z3.enum(Object.values(MODEL_ROLE)), modelId).default(DEFAULT_MODELS),
762
+ terminal: z3.strictObject({ adapter: z3.enum(Object.values(TERMINAL_ADAPTER)).default(TERMINAL_ADAPTER.none) }).prefault({}),
717
763
  /** How the coordinator shows a draft for review (`orchestrate` § 4). */
718
- preview: z2.strictObject({
764
+ preview: z3.strictObject({
719
765
  /** Publish the draft as a private Artifact when the session has the tool; `false` sends a file card or `jh preview --open` instead. */
720
- publish: z2.boolean().default(true)
766
+ publish: z3.boolean().default(true)
721
767
  }).prefault({}),
722
- plans: z2.strictObject({
768
+ plans: z3.strictObject({
723
769
  /** Minutes between background fetches of the base branch while `jh herd` runs; 0 turns them off. */
724
- fetchIntervalMinutes: z2.number().min(0).default(5),
770
+ fetchIntervalMinutes: z3.number().min(0).default(5),
725
771
  /** The running task of a folded lane shimmers in `jh herd`; `false` keeps it still. `NO_COLOR` keeps it still too. */
726
- shimmer: z2.boolean().default(true),
772
+ shimmer: z3.boolean().default(true),
727
773
  /** Minutes without a prompt after which the `return-brief` hook asks the coordinator to show `jh brief` first; 0 turns it off. */
728
- briefAfterMinutes: z2.number().min(0).default(60),
774
+ briefAfterMinutes: z3.number().min(0).default(60),
729
775
  /** Links in a plan that the sprint page shows as chips; the list replaces the built-in kinds. */
730
- linkKinds: z2.array(
731
- z2.strictObject({
732
- pattern: z2.string().min(1).refine(isRegularExpression, "not a valid regular expression"),
733
- label: z2.string().min(1),
734
- icon: z2.string().min(1)
776
+ linkKinds: z3.array(
777
+ z3.strictObject({
778
+ pattern: z3.string().min(1).refine(isRegularExpression, "not a valid regular expression"),
779
+ label: z3.string().min(1),
780
+ icon: z3.string().min(1)
735
781
  })
736
782
  ).default(DEFAULT_LINK_KINDS.map((kind) => ({ ...kind }))),
737
783
  context: contextSchema.prefault({}),
@@ -747,12 +793,12 @@ var jackherdConfigSchema = z2.strictObject({
747
793
  * Without `previews` it reads no deploy preview; with it, one `preview` per new head commit. Each of `sources` is a
748
794
  * command whose JSON output reports `source` for a new or changed entry.
749
795
  */
750
- watch: z2.strictObject({
796
+ watch: z3.strictObject({
751
797
  deploy: deploySchema.optional(),
752
- feedback: z2.union([z2.literal(false), feedbackSchema]).prefault({}),
798
+ feedback: z3.union([z3.literal(false), feedbackSchema]).prefault({}),
753
799
  checks: checksSchema.prefault({}),
754
800
  parallelTasks: parallelTasksSchema.prefault({}),
755
- taskSignalMinutes: z2.number().int().positive().default(DEFAULT_TASK_SIGNAL_MINUTES),
801
+ taskSignalMinutes: z3.number().int().positive().default(DEFAULT_TASK_SIGNAL_MINUTES),
756
802
  previews: previewsSchema.optional(),
757
803
  sources: repositorySources.default([])
758
804
  }).prefault({}),
@@ -769,19 +815,19 @@ var parseJackherdConfig = (raw) => jackherdConfigSchema.parse(raw);
769
815
  var parseLabelled = (schema, raw, label) => {
770
816
  const parsed = schema.safeParse(raw);
771
817
  if (!parsed.success) throw new Error(`${label} is invalid:
772
- ${z2.prettifyError(parsed.error)}`);
818
+ ${z3.prettifyError(parsed.error)}`);
773
819
  return parsed.data;
774
820
  };
775
821
  var parseConfigFile = (raw, label) => parseLabelled(jackherdConfigSchema, raw, label);
776
- var personalSourcesSchema = z2.strictObject({
777
- $schema: z2.string().optional(),
778
- sources: z2.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").default([])
822
+ var personalSourcesSchema = z3.strictObject({
823
+ $schema: z3.string().optional(),
824
+ sources: z3.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").default([])
779
825
  });
780
826
  var parsePersonalSourcesFile = (raw, label) => parseLabelled(personalSourcesSchema, raw, label);
781
827
  var parseSourceDraft = (raw, label) => parseLabelled(sourceSchema, raw, label);
782
828
 
783
829
  // src/config/json-schema.ts
784
- var renderJsonSchema = (schema) => `${JSON.stringify(z3.toJSONSchema(schema, { io: "input" }), null, 2)}
830
+ var renderJsonSchema = (schema) => `${JSON.stringify(z4.toJSONSchema(schema, { io: "input" }), null, 2)}
785
831
  `;
786
832
  var renderConfigJsonSchema = () => renderJsonSchema(jackherdConfigSchema);
787
833
  var renderSourcesJsonSchema = () => renderJsonSchema(personalSourcesSchema);