faberun 0.3.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/bin/faberun.mjs +25 -0
  4. package/integrations/claude-code/statusline-bench.sh +42 -0
  5. package/integrations/claude-code/statusline.sh +80 -0
  6. package/package.json +33 -0
  7. package/skills/faberun/SKILL.md +24 -0
  8. package/skills/faberun/references/contract.md +380 -0
  9. package/skills/faberun/references/engineering.md +29 -0
  10. package/skills/faberun/references/handoffs.md +26 -0
  11. package/skills/faberun/references/operations.md +184 -0
  12. package/skills/faberun/references/rules.md +35 -0
  13. package/skills/faberun/references/workflow.md +23 -0
  14. package/skills/init-agentkit/SKILL.md +108 -0
  15. package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
  16. package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
  17. package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
  18. package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
  19. package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
  20. package/skills/init-agentkit/templates/AGENTS.md +110 -0
  21. package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
  22. package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
  23. package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
  24. package/skills/init-agentkit/templates/docs/VISION.md +33 -0
  25. package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
  26. package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
  27. package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
  28. package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
  29. package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
  30. package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
  31. package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
  32. package/src/campaign/brief.mjs +394 -0
  33. package/src/campaign/chain.mjs +555 -0
  34. package/src/campaign/handoff.mjs +516 -0
  35. package/src/campaign/index.mjs +300 -0
  36. package/src/campaign/journal.mjs +347 -0
  37. package/src/campaign/layout.mjs +51 -0
  38. package/src/campaign/metrics-evals.mjs +25 -0
  39. package/src/campaign/metrics.mjs +517 -0
  40. package/src/campaign/projection.mjs +250 -0
  41. package/src/campaign/record.mjs +102 -0
  42. package/src/campaign/unpark.mjs +56 -0
  43. package/src/cli/brand.mjs +205 -0
  44. package/src/cli/campaign.mjs +730 -0
  45. package/src/cli/contract.mjs +67 -0
  46. package/src/cli/init.mjs +170 -0
  47. package/src/cli/launch.mjs +239 -0
  48. package/src/cli/seat.mjs +139 -0
  49. package/src/cli/setup.mjs +294 -0
  50. package/src/cli/skills.mjs +105 -0
  51. package/src/cli/update.mjs +216 -0
  52. package/src/cli.mjs +525 -0
  53. package/src/contract/articles.mjs +12 -0
  54. package/src/contract/assert.mjs +162 -0
  55. package/src/contract/definition-of-done.mjs +97 -0
  56. package/src/contract/final-verification.mjs +96 -0
  57. package/src/contract/index.mjs +641 -0
  58. package/src/contract/judge-envelope.mjs +25 -0
  59. package/src/contract/review-modes.mjs +151 -0
  60. package/src/contract/runtime.mjs +204 -0
  61. package/src/contract/schema-version.mjs +25 -0
  62. package/src/contract/scope-findings.mjs +77 -0
  63. package/src/contract/snapshot.mjs +639 -0
  64. package/src/contract/task-packet.mjs +495 -0
  65. package/src/contract/untrusted.mjs +75 -0
  66. package/src/contract/verification.mjs +185 -0
  67. package/src/contract/worker-result.mjs +138 -0
  68. package/src/engine/assignment.mjs +63 -0
  69. package/src/engine/backoff.mjs +492 -0
  70. package/src/engine/bulk-read.mjs +361 -0
  71. package/src/engine/cancel.mjs +177 -0
  72. package/src/engine/detach.mjs +101 -0
  73. package/src/engine/dispatch.mjs +752 -0
  74. package/src/engine/failover.mjs +192 -0
  75. package/src/engine/gate.mjs +183 -0
  76. package/src/engine/judge-gate.mjs +517 -0
  77. package/src/engine/lifecycle.mjs +772 -0
  78. package/src/engine/live-preflight.mjs +299 -0
  79. package/src/engine/mutation.mjs +146 -0
  80. package/src/engine/notify-queue.mjs +327 -0
  81. package/src/engine/process-identity.mjs +72 -0
  82. package/src/engine/process.mjs +774 -0
  83. package/src/engine/prompts.mjs +289 -0
  84. package/src/engine/recover.mjs +300 -0
  85. package/src/engine/result-file.mjs +222 -0
  86. package/src/engine/resume.mjs +635 -0
  87. package/src/engine/retry.mjs +334 -0
  88. package/src/engine/review.mjs +228 -0
  89. package/src/engine/run-command.mjs +287 -0
  90. package/src/engine/run-identity.mjs +411 -0
  91. package/src/engine/runtime-discovery.mjs +235 -0
  92. package/src/engine/scheduler.mjs +526 -0
  93. package/src/engine/scope.mjs +378 -0
  94. package/src/engine/settle.mjs +207 -0
  95. package/src/engine/state.mjs +148 -0
  96. package/src/engine/supervise.mjs +713 -0
  97. package/src/engine/verify.mjs +167 -0
  98. package/src/harnesses/agy/index.mjs +62 -0
  99. package/src/harnesses/catalogue.mjs +509 -0
  100. package/src/harnesses/claude/index.mjs +90 -0
  101. package/src/harnesses/codex/index.mjs +87 -0
  102. package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
  103. package/src/harnesses/dsh/index.mjs +210 -0
  104. package/src/harnesses/dsh/runner.mjs +259 -0
  105. package/src/harnesses/exec-jsonl/index.mjs +788 -0
  106. package/src/harnesses/index.mjs +508 -0
  107. package/src/harnesses/protocol.mjs +531 -0
  108. package/src/harnesses/replay/bin.mjs +386 -0
  109. package/src/harnesses/replay/index.mjs +238 -0
  110. package/src/harnesses/zcode/index.mjs +276 -0
  111. package/src/host/config.mjs +87 -0
  112. package/src/host/home.mjs +149 -0
  113. package/src/host/package.mjs +23 -0
  114. package/src/host/preflight.mjs +520 -0
  115. package/src/host/tool-policy-decisions.mjs +341 -0
  116. package/src/host/tool-policy-hook.mjs +270 -0
  117. package/src/notify/index.mjs +359 -0
  118. package/src/notify/os-macos.mjs +81 -0
  119. package/src/repo/declared-paths.mjs +220 -0
  120. package/src/repo/integrate.mjs +546 -0
  121. package/src/repo/scope-closure.mjs +665 -0
  122. package/src/repo/signal-block.mjs +16 -0
  123. package/src/repo/signal.mjs +222 -0
  124. package/src/repo/source-identity.mjs +295 -0
  125. package/src/repo/workspace.mjs +557 -0
  126. package/src/repo/worktree.mjs +352 -0
  127. package/src/report/final.mjs +200 -0
  128. package/src/report/metrics-report.mjs +99 -0
  129. package/src/report/next.mjs +383 -0
  130. package/src/report/render.mjs +716 -0
  131. package/src/run/disk-gc.mjs +251 -0
  132. package/src/run/lock.mjs +329 -0
  133. package/src/run/node-store.mjs +62 -0
  134. package/src/run/operations.mjs +286 -0
  135. package/src/run/store.mjs +187 -0
  136. package/src/run/usage.mjs +337 -0
  137. package/src/seat/harnesses.mjs +83 -0
  138. package/src/seat/index.mjs +239 -0
  139. package/src/seat/tmux.mjs +208 -0
  140. package/src/util.mjs +0 -0
  141. package/src/web/api.mjs +371 -0
  142. package/src/web/boundary.mjs +88 -0
  143. package/src/web/index.html +299 -0
  144. package/src/web/server.mjs +552 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The primitive assertions every validator in this tree needs, in one place.
3
+ *
4
+ * They were copied instead: `requireString` stood four times, `rejectUnknown`
5
+ * four, `requireId` three, `requireTimestamp` three, and the copies had drifted
6
+ * into six behaviours. Each definition below is the superset of what its copies
7
+ * did, and the message wording is the majority's, because that is what the
8
+ * tests match on (`has unexpected field X`); the tests that cover the others
9
+ * match the label, not the wording, so no assertion changed meaning.
10
+ *
11
+ * Deliberately not here: `requirePacketHash` (a contract-specific format),
12
+ * `requireRuntime` (needs the runtime table), and `repo/integrate.mjs`'s
13
+ * `requireText`, which despite the name reads a file and is a different
14
+ * function entirely.
15
+ */
16
+
17
+ /**
18
+ * @param {Record<string, unknown>} value
19
+ * @param {Set<string>} allowed
20
+ * @param {string} label
21
+ */
22
+ export function rejectUnknown(value, allowed, label) {
23
+ for (const key of Object.keys(value)) {
24
+ if (!allowed.has(key)) throw new TypeError(`${label} has unexpected field ${key}`);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {unknown} value
30
+ * @param {string} label
31
+ * @returns {asserts value is Record<string, unknown>}
32
+ */
33
+ export function assertObject(value, label) {
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new TypeError(`${label} must be an object`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * A path-safe identifier: letters, numbers, dot, underscore, dash, and never
41
+ * `.` or `..` on their own. The exclusion is load-bearing -- these ids become
42
+ * directory names under `.runs/`.
43
+ *
44
+ * @param {unknown} value
45
+ * @param {string} label
46
+ * @returns {asserts value is string}
47
+ */
48
+ export function requireId(value, label) {
49
+ if (typeof value !== "string" || !/^[A-Za-z0-9._-]+$/u.test(value)) {
50
+ throw new TypeError(`${label} must contain only letters, numbers, dot, underscore, or dash`);
51
+ }
52
+ if (value === "." || value === "..") throw new TypeError(`${label} must not be "." or ".."`);
53
+ }
54
+
55
+ /**
56
+ * @param {unknown} value
57
+ * @param {string} label
58
+ * @returns {asserts value is string}
59
+ */
60
+ export function requireString(value, label) {
61
+ if (typeof value !== "string" || !value.trim()) throw new TypeError(`${label} must be a non-empty string`);
62
+ }
63
+
64
+ /**
65
+ * @param {unknown} value
66
+ * @param {string} label
67
+ * @param {number} [maxBytes]
68
+ * @returns {asserts value is string}
69
+ */
70
+ export function requireText(value, label, maxBytes) {
71
+ requireString(value, label);
72
+ if (maxBytes !== undefined && Buffer.byteLength(/** @type {string} */ (value), "utf8") > maxBytes) {
73
+ throw new TypeError(`${label} exceeds ${maxBytes} bytes`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {unknown} value
79
+ * @param {string} label
80
+ * @param {number} maxBytes
81
+ */
82
+ export function boundedString(value, label, maxBytes) {
83
+ requireText(value, label, maxBytes);
84
+ }
85
+
86
+ /**
87
+ * @param {unknown} value
88
+ * @param {string} label
89
+ * @param {boolean} [nonEmpty]
90
+ */
91
+ export function requireStringArray(value, label, nonEmpty = false) {
92
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
93
+ if (nonEmpty && !value.length) throw new TypeError(`${label} must not be empty`);
94
+ for (const [index, item] of value.entries()) requireString(item, `${label}[${index}]`);
95
+ }
96
+
97
+ /**
98
+ * @param {unknown} value
99
+ * @param {string} label
100
+ * @returns {asserts value is string}
101
+ */
102
+ export function requireTimestamp(value, label) {
103
+ if (typeof value !== "string" || !value.trim() || Number.isNaN(Date.parse(value))) {
104
+ throw new TypeError(`${label} must be a valid timestamp`);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {unknown} value
110
+ * @param {string} label
111
+ */
112
+ export function requireInteger(value, label) {
113
+ if (!Number.isInteger(value)) throw new TypeError(`${label} must be an integer`);
114
+ }
115
+
116
+ /**
117
+ * @param {unknown} value
118
+ * @param {string} label
119
+ * @returns {number}
120
+ */
121
+ export function positiveInteger(value, label) {
122
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new TypeError(`${label} must be a positive integer`);
123
+ return value;
124
+ }
125
+
126
+ /**
127
+ * @param {unknown} value
128
+ * @param {string} label
129
+ * @returns {number}
130
+ */
131
+ export function nonNegativeInteger(value, label) {
132
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new TypeError(`${label} must be a non-negative integer`);
133
+ return value;
134
+ }
135
+
136
+ /**
137
+ * @param {unknown} value
138
+ * @param {string} label
139
+ * @returns {number}
140
+ */
141
+ export function positiveNumber(value, label) {
142
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new TypeError(`${label} must be a positive number`);
143
+ return value;
144
+ }
145
+
146
+ /**
147
+ * @param {unknown} value
148
+ * @param {string} label
149
+ * @returns {number}
150
+ */
151
+ export function nonNegativeNumber(value, label) {
152
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new TypeError(`${label} must be a non-negative number`);
153
+ return value;
154
+ }
155
+
156
+ /**
157
+ * @param {unknown} value
158
+ * @param {string} label
159
+ */
160
+ export function requirePacketHash(value, label) {
161
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) throw new TypeError(`${label} must be a SHA-256 hash`);
162
+ }
@@ -0,0 +1,97 @@
1
+ import { rejectUnknown, requireId, requireString } from "./assert.mjs";
2
+ /**
3
+ * Schema 2 Definition of Done items. Each item is an object that names an
4
+ * observable outcome and declares how it is proven: mechanically through a
5
+ * `proof` (a verification `command`, a workspace `path`, or a `verification`
6
+ * entry reused by reference) or by judge `judgment`. The schema-1 string item
7
+ * is rejected; there is no converter and no dual acceptance.
8
+ */
9
+
10
+ /** @typedef {{kind: "command"|"path"|"verification", ref: string}} DefinitionOfDoneProof */
11
+
12
+ /** @typedef {{id: string, text: string, proof?: DefinitionOfDoneProof, judgment?: true}} DefinitionOfDoneItem */
13
+
14
+ /**
15
+ * @param {unknown} value
16
+ * @param {string} label
17
+ * @param {{verificationCount?: number, recordableCount?: number}} [options]
18
+ * @returns {DefinitionOfDoneItem[]}
19
+ */
20
+ export function validateDefinitionOfDone(value, label, options = {}) {
21
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array of Definition of Done objects`);
22
+ return value.map((item, index) => {
23
+ const itemLabel = `${label}[${index}]`;
24
+ if (typeof item === "string") {
25
+ throw new TypeError(`${itemLabel} must be an object, not a schema-1 string item`);
26
+ }
27
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
28
+ throw new TypeError(`${itemLabel} must be an object with id, text, and proof or judgment: true`);
29
+ }
30
+ const record = /** @type {Record<string, unknown>} */ (item);
31
+ rejectUnknown(record, new Set(["id", "text", "proof", "judgment"]), itemLabel);
32
+ requireId(record.id, `${itemLabel}.id`);
33
+ requireString(record.text, `${itemLabel}.text`);
34
+ if (record.judgment !== undefined && record.judgment !== true) {
35
+ throw new TypeError(`${itemLabel}.judgment must be true when present`);
36
+ }
37
+ const proof = record.proof === undefined
38
+ ? undefined
39
+ : validateProof(record.proof, `${itemLabel}.proof`, options);
40
+ if (proof === undefined && record.judgment !== true) {
41
+ throw new TypeError(`${itemLabel} must declare proof or judgment: true`);
42
+ }
43
+ return {
44
+ id: /** @type {string} */ (record.id),
45
+ text: /** @type {string} */ (record.text),
46
+ ...(proof === undefined ? {} : { proof }),
47
+ ...(record.judgment === true ? { judgment: true } : {}),
48
+ };
49
+ });
50
+ }
51
+
52
+ /**
53
+ * @param {unknown} value
54
+ * @param {string} label
55
+ * @param {{verificationCount?: number, recordableCount?: number}} options
56
+ * @returns {DefinitionOfDoneProof}
57
+ */
58
+ function validateProof(value, label, options) {
59
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
60
+ throw new TypeError(`${label} must be an object with kind and ref`);
61
+ }
62
+ const record = /** @type {Record<string, unknown>} */ (value);
63
+ rejectUnknown(record, new Set(["kind", "ref"]), label);
64
+ const kind = record.kind;
65
+ if (kind !== "command" && kind !== "path" && kind !== "verification") {
66
+ throw new TypeError(`${label}.kind must be "command", "path", or "verification"`);
67
+ }
68
+ if (kind === "verification") return { kind, ref: validateVerificationRef(record.ref, label, options) };
69
+ requireString(record.ref, `${label}.ref`);
70
+ return { kind, ref: /** @type {string} */ (record.ref) };
71
+ }
72
+
73
+ /**
74
+ * A `verification` proof reuses a recorded controller verification result by
75
+ * position, never by comparing command strings: a joined argv loses argument
76
+ * boundaries and shell semantics, so the reference is the only sound name.
77
+ *
78
+ * @param {unknown} value
79
+ * @param {string} label
80
+ * @param {{verificationCount?: number, recordableCount?: number}} options
81
+ * @returns {string}
82
+ */
83
+ function validateVerificationRef(value, label, options) {
84
+ const text = typeof value === "number" ? String(value) : value;
85
+ if (typeof text !== "string" || !/^\d+$/u.test(text.trim())) {
86
+ throw new TypeError(`${label}.ref must be the zero-based index of a verification command`);
87
+ }
88
+ const index = Number.parseInt(text, 10);
89
+ if (typeof options.verificationCount === "number" && index >= options.verificationCount) {
90
+ throw new TypeError(`${label}.ref ${index} names no verification command: the packet declares ${options.verificationCount}`);
91
+ }
92
+ if (typeof options.recordableCount === "number" && index >= options.recordableCount) {
93
+ throw new TypeError(`${label}.ref ${index} cannot be reused at gate time: only the first ${options.recordableCount} verification commands are recorded on the node`);
94
+ }
95
+ return String(index);
96
+ }
97
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Verification schema that is not part of a task packet (TECH-SPEC section 6.2,
3
+ * rule 12).
4
+ *
5
+ * `contract.finalVerification` is the contract-wide proof that the phase as a
6
+ * whole closes: the controller runs it before the judge on the phase-terminal
7
+ * node (the node no other node depends on), so no final checkpoint is ever
8
+ * approved on partial verification. The persisted node-snapshot shape for
9
+ * verification evidence lives here too, next to the schema it records.
10
+ */
11
+
12
+ import { Buffer } from "node:buffer";
13
+ import { validateVerificationCommands } from "./verification.mjs";
14
+ import { assertObject, nonNegativeInteger, positiveInteger, rejectUnknown, requireInteger, requireString, requireTimestamp } from "./assert.mjs";
15
+
16
+ /** @typedef {Record<string, unknown>} JsonObject */
17
+ /** @typedef {import("./verification.mjs").VerificationCommand} VerificationCommand */
18
+
19
+ /**
20
+ * Validate the optional contract-level `finalVerification` field. It carries
21
+ * the full verification-command schema and nothing else.
22
+ *
23
+ * @param {unknown} value
24
+ * @param {string} label
25
+ * @returns {VerificationCommand[]|undefined}
26
+ */
27
+ export function validateFinalVerification(value, label = "contract.finalVerification") {
28
+ if (value === undefined) return undefined;
29
+ return validateVerificationCommands(value, label);
30
+ }
31
+
32
+ /**
33
+ * The contract's `finalVerification` commands when this node is the one that
34
+ * closes the phase, otherwise none. A node is phase-terminal when no other
35
+ * node in the contract depends on it.
36
+ *
37
+ * @param {{finalVerification?: VerificationCommand[], nodes: {id: string, dependsOn: string[]}[]}} contract
38
+ * @param {{id: string}} node
39
+ * @returns {VerificationCommand[]}
40
+ */
41
+ export function finalVerificationCommands(contract, node) {
42
+ const commands = contract.finalVerification ?? [];
43
+ if (commands.length === 0) return [];
44
+ const hasDependant = contract.nodes.some((candidate) => candidate.dependsOn.includes(node.id));
45
+ return hasDependant ? [] : commands;
46
+ }
47
+
48
+ /**
49
+ * @param {unknown} value
50
+ */
51
+ export function validateVerificationSnapshot(value) {
52
+ assertObject(value, "node snapshot.verification");
53
+ if (typeof value.passed !== "boolean" || !Array.isArray(value.commands) || value.commands.length > 32) {
54
+ throw new TypeError("node snapshot.verification is invalid");
55
+ }
56
+ if (value.completed !== undefined && typeof value.completed !== "boolean") throw new TypeError("node snapshot.verification.completed is invalid");
57
+ if (value.attempts !== undefined) {
58
+ if (!Array.isArray(value.attempts) || value.attempts.length > 16) throw new TypeError("node snapshot.verification.attempts is invalid");
59
+ const attempts = /** @type {unknown[]} */ (value.attempts);
60
+ for (const [index, attempt] of attempts.entries()) validateVerificationAttempt(attempt, `node snapshot.verification.attempts[${index}]`);
61
+ }
62
+ if (value.error !== undefined && (typeof value.error !== "string" || Buffer.byteLength(value.error, "utf8") > 4096)) {
63
+ throw new TypeError("node snapshot.verification.error is invalid");
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {unknown} value
69
+ * @param {string} label
70
+ */
71
+ function validateVerificationAttempt(value, label) {
72
+ assertObject(value, label);
73
+ rejectUnknown(value, new Set([
74
+ "invocationId", "commandIndex", "attempt", "pid", "processStartToken", "processGroupId",
75
+ "startedAt", "deadlineAt", "status", "completedAt", "result",
76
+ ]), label);
77
+ requireString(value.invocationId, `${label}.invocationId`);
78
+ nonNegativeInteger(value.commandIndex, `${label}.commandIndex`);
79
+ positiveInteger(value.attempt, `${label}.attempt`);
80
+ if (value.pid !== null) requireInteger(value.pid, `${label}.pid`);
81
+ if (value.processGroupId !== null) requireInteger(value.processGroupId, `${label}.processGroupId`);
82
+ if (value.processStartToken !== null) requireString(value.processStartToken, `${label}.processStartToken`);
83
+ requireTimestamp(value.startedAt, `${label}.startedAt`);
84
+ requireTimestamp(value.deadlineAt, `${label}.deadlineAt`);
85
+ if (!["active", "closed", "failed", "crashed", "canceled"].includes(/** @type {string} */ (value.status))) throw new TypeError(`${label}.status is invalid`);
86
+ if (value.completedAt !== null) requireTimestamp(value.completedAt, `${label}.completedAt`);
87
+ if (value.result !== null) {
88
+ assertObject(value.result, `${label}.result`);
89
+ const result = /** @type {JsonObject} */ (value.result);
90
+ for (const key of ["stdout", "stderr", "error"]) {
91
+ if (result[key] !== null && result[key] !== undefined && (typeof result[key] !== "string" || Buffer.byteLength(result[key], "utf8") > 2048)) throw new TypeError(`${label}.result.${key} is invalid`);
92
+ }
93
+ if (typeof result.passed !== "boolean") throw new TypeError(`${label}.result.passed is invalid`);
94
+ }
95
+ }
96
+