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.
- package/CHANGELOG.md +9 -0
- package/INSTALL.md +16 -0
- package/README.md +23 -22
- package/dist/{chunk-JDO6O3UO.js → chunk-O365KHHP.js} +139 -3
- package/dist/config.d.ts +13 -1
- package/dist/config.js +178 -132
- package/dist/plan-format.d.ts +121 -3
- package/dist/plan-format.js +176 -35
- package/docs/plan-contract.md +52 -5
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/index.ts +2 -2
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +1 -1
- package/kit/hooks/hooks/shared/board-lanes.ts +33 -12
- package/kit/hooks/hooks/shared/boards.ts +22 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +2 -1
- package/kit/workflow/skills/orchestrate/references/backend.md +30 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +1 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +3 -3
- package/kit/workflow/skills/orchestrate/references/start-lane.md +2 -2
- package/kit/workflow/skills/plan/SKILL.md +3 -1
- package/kit/workflow/skills/plan/references/template-English.md +2 -0
- package/kit/workflow/skills/plan/references/template-German.md +2 -0
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +49 -0
- package/src/backend/command-line.ts +116 -0
- package/src/backend/command.ts +14 -0
- package/src/backend/control.ts +194 -0
- package/src/backend/handover.ts +28 -0
- package/src/backend/herdr.ts +147 -0
- package/src/backend/lock.ts +84 -0
- package/src/backend/process-cwd.ts +32 -0
- package/src/backend/schema.ts +47 -0
- package/src/backend/state.ts +57 -0
- package/src/backend/step.ts +93 -0
- package/src/backend/system.ts +150 -0
- package/src/backend/watch.ts +75 -0
- package/src/brief/command.ts +1 -1
- package/src/brief/lane-facts.ts +2 -2
- package/src/check/command.ts +2 -1
- package/src/check/plan-structure-check.ts +68 -14
- package/src/check/settings.ts +6 -1
- package/src/check/skill-budget-defaults.ts +6 -3
- package/src/cli.ts +1 -0
- package/src/config/schema.ts +6 -0
- package/src/lane/close-board.ts +4 -4
- package/src/lane/close-command.ts +2 -2
- package/src/lane/dev-command.ts +1 -1
- package/src/lane/dev.ts +2 -2
- package/src/lane/port-command.ts +1 -1
- package/src/plan-contract/command.ts +2 -1
- package/src/plan-contract/contract.ts +30 -9
- package/src/plan-format/bare-codes.ts +4 -4
- package/src/plan-format/dependencies.ts +5 -5
- package/src/plan-format/lanes.ts +3 -4
- package/src/plan-format/markdown.ts +125 -10
- package/src/plan-format/open-status.ts +15 -0
- package/src/plan-format/phases.ts +45 -4
- package/src/plan-format/plan-head.ts +31 -9
- package/src/plan-format/plan-structure.ts +43 -7
- package/src/plan-format/progress.ts +3 -0
- package/src/plan-format/sprint-document.ts +4 -4
- package/src/plan-format/unrecognized-sections.ts +21 -0
- package/src/plan-format/waiting.ts +13 -13
- package/src/plans/board-close.ts +25 -16
- package/src/plans/board-pick.ts +1 -1
- package/src/plans/ensure-panes.ts +2 -2
- package/src/plans/layout-summary.ts +8 -1
- package/src/plans/layout.ts +14 -5
- package/src/plans/live.ts +1 -1
- package/src/plans/plan-panes.ts +1 -1
- package/src/plans/settings.ts +6 -0
- package/src/plans/snapshot.ts +9 -5
- package/src/plans/sprint-export.ts +1 -1
- package/src/plans/texts.ts +3 -0
- package/src/plans/view.ts +1 -0
- package/src/resume/coordinator-read.ts +2 -2
- package/src/settled/command.ts +1 -1
- package/src/settled/settle.ts +5 -5
- package/src/shared/shell-word.ts +2 -0
- package/src/sync/render.ts +20 -5
- package/src/sync/sync-plan.ts +6 -2
- package/src/sync/sync.ts +3 -2
- package/src/vocabulary/index.ts +192 -4
- package/src/watch/appointments.ts +13 -20
- package/src/watch/command.ts +32 -5
- package/src/watch/lane-progress.ts +1 -1
- package/src/watch/merge-overlap-read.ts +2 -2
- package/src/watch/plan-missing-read.ts +3 -1
- 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-
|
|
4
|
+
} from "./chunk-O365KHHP.js";
|
|
5
5
|
|
|
6
6
|
// src/config/json-schema.ts
|
|
7
|
-
import { z as
|
|
7
|
+
import { z as z4 } from "zod";
|
|
8
8
|
|
|
9
9
|
// src/config/schema.ts
|
|
10
|
-
import { z as
|
|
10
|
+
import { z as z3 } from "zod";
|
|
11
11
|
|
|
12
|
-
// src/
|
|
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 =
|
|
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 =
|
|
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 =
|
|
91
|
+
var reviewerRoute = z2.strictObject({
|
|
48
92
|
reviewer: nonEmpty,
|
|
49
93
|
globs,
|
|
50
94
|
why: nonEmpty
|
|
51
95
|
});
|
|
52
|
-
var projectCheck =
|
|
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:
|
|
105
|
+
kind: z2.enum([CHECK_KIND.tests, CHECK_KIND.types]).optional()
|
|
62
106
|
});
|
|
63
|
-
var generatorOwned =
|
|
107
|
+
var generatorOwned = z2.strictObject({
|
|
64
108
|
glob,
|
|
65
109
|
generator: nonEmpty
|
|
66
110
|
});
|
|
67
|
-
var publicContent =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
126
|
+
exclude: z2.array(glob).default([])
|
|
83
127
|
});
|
|
84
128
|
var DEFAULT_LINT_POINTERS = { level: LINT_POINTERS_LEVEL.warn, exclude: [] };
|
|
85
|
-
var checkConfigSchema =
|
|
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:
|
|
93
|
-
generatorOwned:
|
|
94
|
-
buildArtifacts:
|
|
95
|
-
reviewerRoutes:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
352
|
-
var modelId =
|
|
353
|
-
var command2 =
|
|
354
|
-
var excludedKitEntry =
|
|
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 =
|
|
358
|
-
var commandsSchema =
|
|
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 =
|
|
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 =
|
|
378
|
-
var branchName =
|
|
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 =
|
|
383
|
-
var boardPattern =
|
|
384
|
-
var boardFile =
|
|
385
|
-
var boardSchema =
|
|
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:
|
|
432
|
+
term: z3.string().min(1).default("board")
|
|
392
433
|
});
|
|
393
|
-
var deploySchema =
|
|
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:
|
|
438
|
+
environments: z3.array(z3.string().min(1)).min(1)
|
|
398
439
|
});
|
|
399
|
-
var feedbackSchema =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
488
|
+
urlPattern: z3.string().min(1).refine(isRegularExpression, "not a valid regular expression").optional()
|
|
448
489
|
});
|
|
449
|
-
var sourcePath =
|
|
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 =
|
|
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 =
|
|
473
|
-
var sourceSchema =
|
|
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:
|
|
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:
|
|
518
|
+
command: z3.array(z3.string().min(1)).min(1),
|
|
478
519
|
/** Minutes between two runs, at least 1. */
|
|
479
|
-
every:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
570
|
-
var checkFreePattern =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
638
|
+
openAsDraft: z3.boolean().default(false)
|
|
598
639
|
});
|
|
599
|
-
var guardFields =
|
|
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:
|
|
606
|
-
selfMerge:
|
|
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:
|
|
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:
|
|
651
|
+
approvalMinutes: z3.number().int().min(1).max(1440).default(180)
|
|
611
652
|
}).prefault({}),
|
|
612
|
-
approvals:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
693
|
+
taskSignal: z3.boolean().default(true)
|
|
653
694
|
});
|
|
654
695
|
var DEFAULT_TASK_SIGNAL_MINUTES = 10;
|
|
655
|
-
var tonesSchema =
|
|
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 =
|
|
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:
|
|
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 =
|
|
678
|
-
$schema:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
688
|
-
worktreeDirectory:
|
|
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:
|
|
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:
|
|
745
|
+
kit: z3.strictObject({ exclude: z3.array(excludedKitEntry).default([]) }).prefault({}),
|
|
702
746
|
/** How `/orchestrate` sets up a lane's worktree. */
|
|
703
|
-
lanes:
|
|
747
|
+
lanes: z3.strictObject({
|
|
704
748
|
/** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
|
|
705
|
-
copy:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
716
|
-
terminal:
|
|
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:
|
|
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:
|
|
766
|
+
publish: z3.boolean().default(true)
|
|
721
767
|
}).prefault({}),
|
|
722
|
-
plans:
|
|
768
|
+
plans: z3.strictObject({
|
|
723
769
|
/** Minutes between background fetches of the base branch while `jh herd` runs; 0 turns them off. */
|
|
724
|
-
fetchIntervalMinutes:
|
|
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:
|
|
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:
|
|
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:
|
|
731
|
-
|
|
732
|
-
pattern:
|
|
733
|
-
label:
|
|
734
|
-
icon:
|
|
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:
|
|
796
|
+
watch: z3.strictObject({
|
|
751
797
|
deploy: deploySchema.optional(),
|
|
752
|
-
feedback:
|
|
798
|
+
feedback: z3.union([z3.literal(false), feedbackSchema]).prefault({}),
|
|
753
799
|
checks: checksSchema.prefault({}),
|
|
754
800
|
parallelTasks: parallelTasksSchema.prefault({}),
|
|
755
|
-
taskSignalMinutes:
|
|
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
|
-
${
|
|
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 =
|
|
777
|
-
$schema:
|
|
778
|
-
sources:
|
|
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(
|
|
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);
|