@osolmaz/pi-workflows 0.11.2 → 0.12.1

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 (177) hide show
  1. package/README.md +15 -10
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +509 -124
  6. package/dist/builtins/autoimplement.workflow.js +884 -327
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +5 -3
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +4 -1
  11. package/dist/builtins/index.js +3 -1
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/monitor.workflow.d.ts +2 -4
  14. package/dist/builtins/monitor.workflow.js +26 -128
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/builtins/pi-agent-group.d.ts +72 -0
  17. package/dist/builtins/pi-agent-group.js +1087 -0
  18. package/dist/builtins/pi-agent-group.js.map +1 -0
  19. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  20. package/dist/builtins/plan-approval.workflow.js +92 -14
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  22. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  23. package/dist/builtins/plan-change.workflow.js +256 -0
  24. package/dist/builtins/plan-change.workflow.js.map +1 -0
  25. package/dist/builtins/plan-presentation.js +2 -2
  26. package/dist/builtins/plan-presentation.js.map +1 -1
  27. package/dist/builtins/sanity-check.workflow.d.ts +85 -0
  28. package/dist/builtins/sanity-check.workflow.js +482 -0
  29. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  30. package/dist/controllers/sqlite.d.ts +90 -2
  31. package/dist/controllers/sqlite.js +380 -5
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.d.ts +2 -2
  34. package/dist/extension/decision-channels.js +29 -35
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  37. package/dist/extension/deferred-turn-coordinator.js +143 -0
  38. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  39. package/dist/extension/deferred-turn.d.ts +44 -0
  40. package/dist/extension/deferred-turn.js +110 -0
  41. package/dist/extension/deferred-turn.js.map +1 -0
  42. package/dist/extension/index.js +348 -80
  43. package/dist/extension/index.js.map +1 -1
  44. package/dist/extension/session-events.d.ts +2 -2
  45. package/dist/extension/step-message.d.ts +1 -0
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.js +23 -3
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/herdr/setup.js +4 -4
  50. package/dist/render/graph-render.js +1 -2
  51. package/dist/render/graph-render.js.map +1 -1
  52. package/dist/viewer/render.js +7 -6
  53. package/dist/viewer/render.js.map +1 -1
  54. package/dist/workflows/catalog.js +7 -2
  55. package/dist/workflows/catalog.js.map +1 -1
  56. package/dist/workflows/command-batch.d.ts +38 -0
  57. package/dist/workflows/command-batch.js +176 -0
  58. package/dist/workflows/command-batch.js.map +1 -0
  59. package/dist/workflows/composition.js +8 -0
  60. package/dist/workflows/composition.js.map +1 -1
  61. package/dist/workflows/decision-presentation.d.ts +1 -1
  62. package/dist/workflows/decision-presentation.js +51 -38
  63. package/dist/workflows/decision-presentation.js.map +1 -1
  64. package/dist/workflows/engine.d.ts +2 -2
  65. package/dist/workflows/engine.js +22 -17
  66. package/dist/workflows/engine.js.map +1 -1
  67. package/dist/workflows/errors.d.ts +13 -0
  68. package/dist/workflows/errors.js +15 -0
  69. package/dist/workflows/errors.js.map +1 -1
  70. package/dist/workflows/human-decision.d.ts +16 -4
  71. package/dist/workflows/human-decision.js +175 -72
  72. package/dist/workflows/human-decision.js.map +1 -1
  73. package/dist/workflows/index.d.ts +3 -2
  74. package/dist/workflows/index.js +2 -1
  75. package/dist/workflows/index.js.map +1 -1
  76. package/dist/workflows/progress.d.ts +1 -0
  77. package/dist/workflows/progress.js +15 -3
  78. package/dist/workflows/progress.js.map +1 -1
  79. package/dist/workflows/schema.js +12 -1
  80. package/dist/workflows/schema.js.map +1 -1
  81. package/dist/workflows/shell.d.ts +4 -0
  82. package/dist/workflows/shell.js +6 -0
  83. package/dist/workflows/shell.js.map +1 -1
  84. package/dist/workflows/store.js +8 -1
  85. package/dist/workflows/store.js.map +1 -1
  86. package/dist/workflows/types.d.ts +37 -48
  87. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  88. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  89. package/docs/CONTROLLERS.md +2 -2
  90. package/docs/DEFERRED_TURNS.md +298 -0
  91. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  92. package/docs/HUMAN_DECISIONS.md +30 -40
  93. package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
  94. package/docs/MONITOR.md +7 -13
  95. package/docs/WORKFLOW_COMPOSITION.md +10 -9
  96. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  97. package/docs/WORKFLOW_UPDATES.md +28 -5
  98. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  99. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  100. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  101. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  102. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  103. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  104. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  105. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  106. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  107. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  108. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  109. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  110. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  111. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  112. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  113. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  114. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  115. package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
  116. package/docs/run-bundles.md +8 -6
  117. package/docs/workflows.md +85 -22
  118. package/examples/workflows/approved-plan.workflow.ts +19 -46
  119. package/examples/workflows/command-batch.workflow.ts +50 -0
  120. package/examples/workflows/sanity-check.workflow.ts +1 -0
  121. package/herdr-plugin.toml +3 -3
  122. package/package.json +7 -7
  123. package/schemas/decision-presentation-v1.schema.json +1 -1
  124. package/schemas/human-decision-accepted-v1.schema.json +16 -4
  125. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  126. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  127. package/schemas/human-decision-continuation-v1.schema.json +11 -2
  128. package/schemas/human-decision-delivery-v1.schema.json +9 -1
  129. package/schemas/human-decision-receipt-v1.schema.json +9 -1
  130. package/schemas/human-decision-request-v1.schema.json +25 -5
  131. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  132. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  133. package/skills/autodoc/SKILL.md +33 -2
  134. package/skills/autoimplement/SKILL.md +93 -12
  135. package/skills/autoplan/SKILL.md +26 -2
  136. package/skills/monitor/SKILL.md +66 -24
  137. package/skills/pi-workflows/SKILL.md +21 -6
  138. package/skills/sanity-check/SKILL.md +44 -0
  139. package/src/builtins/autoimplement-command-batches.ts +364 -0
  140. package/src/builtins/autoimplement.workflow.ts +1075 -370
  141. package/src/builtins/catalog.ts +5 -3
  142. package/src/builtins/index.ts +24 -0
  143. package/src/builtins/monitor.workflow.ts +27 -150
  144. package/src/builtins/pi-agent-group.ts +1407 -0
  145. package/src/builtins/plan-approval.workflow.ts +157 -24
  146. package/src/builtins/plan-change.workflow.ts +321 -0
  147. package/src/builtins/plan-presentation.ts +2 -2
  148. package/src/builtins/sanity-check.workflow.ts +769 -0
  149. package/src/controllers/sqlite.ts +580 -7
  150. package/src/extension/decision-channels.ts +36 -66
  151. package/src/extension/deferred-turn-coordinator.ts +171 -0
  152. package/src/extension/deferred-turn.ts +166 -0
  153. package/src/extension/index.ts +438 -90
  154. package/src/extension/session-events.ts +2 -2
  155. package/src/extension/step-message.ts +1 -0
  156. package/src/extension/widget.ts +24 -5
  157. package/src/herdr/setup.ts +4 -4
  158. package/src/render/graph-render.ts +1 -2
  159. package/src/viewer/render.ts +7 -6
  160. package/src/workflows/catalog.ts +7 -2
  161. package/src/workflows/command-batch.ts +254 -0
  162. package/src/workflows/composition.ts +9 -0
  163. package/src/workflows/decision-presentation.ts +56 -43
  164. package/src/workflows/engine.ts +25 -20
  165. package/src/workflows/errors.ts +24 -0
  166. package/src/workflows/human-decision.ts +218 -101
  167. package/src/workflows/index.ts +20 -11
  168. package/src/workflows/progress.ts +18 -3
  169. package/src/workflows/schema.ts +19 -1
  170. package/src/workflows/shell.ts +10 -0
  171. package/src/workflows/store.ts +8 -1
  172. package/src/workflows/types.ts +43 -59
  173. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  174. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  175. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  176. package/schemas/human-decision-request-v2.schema.json +0 -69
  177. package/schemas/human-decision-resolution-v2.schema.json +0 -27
@@ -0,0 +1,364 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import {
4
+ MAX_COMMAND_BATCH_ITEMS,
5
+ validateCommandBatchRequest,
6
+ type CommandBatchItem,
7
+ } from "../workflows/command-batch.js";
8
+
9
+ export const REVIEW_TIMEOUT_MS = 10 * 60_000;
10
+ export const CI_WATCH_TIMEOUT_MS = 5 * 60_000;
11
+ export const VERIFICATION_TIMEOUT_MS = 45 * 60_000;
12
+ export const AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS = 1_000_000;
13
+ export const AUTOIMPLEMENT_MAX_CONCURRENCY = 8;
14
+
15
+ export type AutoimplementConcurrency = {
16
+ reviewer: number;
17
+ ciWatch: number;
18
+ verification: number;
19
+ };
20
+
21
+ export type PublishedRepository = {
22
+ id: string;
23
+ repository: string;
24
+ branch: string;
25
+ baseBranch: string;
26
+ headRevision: string;
27
+ pr: string;
28
+ dependencyFingerprint?: string;
29
+ };
30
+
31
+ export type PublishedRepositories = {
32
+ repositories: PublishedRepository[];
33
+ };
34
+
35
+ export type VerificationCommandPlan = {
36
+ commands: CommandBatchItem[];
37
+ untested: string[];
38
+ };
39
+
40
+ export type CiTargetInspection = {
41
+ id: string;
42
+ repository: string;
43
+ headRevision: string;
44
+ pr: string;
45
+ route: "green" | "failed" | "pending" | "unavailable";
46
+ reason: string;
47
+ relatedFailures: string[];
48
+ unrelatedFailures: string[];
49
+ trackingCommand?: CommandBatchItem;
50
+ };
51
+
52
+ export type CiInspectionBatch = {
53
+ route: "green" | "failed" | "pending" | "unavailable";
54
+ reason: string;
55
+ relatedFailures: string[];
56
+ unrelatedFailures: string[];
57
+ targets: CiTargetInspection[];
58
+ };
59
+
60
+ export function parseAutoimplementConcurrency(value: unknown): AutoimplementConcurrency {
61
+ if (value === undefined) return { reviewer: 4, ciWatch: 4, verification: 2 };
62
+ const input = requireRecord(value, "autoimplement concurrency");
63
+ for (const key of Object.keys(input)) {
64
+ if (key !== "reviewer" && key !== "ciWatch" && key !== "verification") {
65
+ throw new Error(`autoimplement concurrency.${key} is not supported`);
66
+ }
67
+ }
68
+ return {
69
+ reviewer: concurrencyValue(input.reviewer ?? 4, "reviewer"),
70
+ ciWatch: concurrencyValue(input.ciWatch ?? 4, "ciWatch"),
71
+ verification: concurrencyValue(input.verification ?? 2, "verification"),
72
+ };
73
+ }
74
+
75
+ export function parsePublishedRepositories(value: unknown): PublishedRepositories {
76
+ const result = requireRecord(value, "publication result");
77
+ if (!Array.isArray(result.repositories) || result.repositories.length === 0) {
78
+ throw new Error("publication result repositories must be a non-empty array");
79
+ }
80
+ if (result.repositories.length > MAX_COMMAND_BATCH_ITEMS) {
81
+ throw new Error(
82
+ `publication result repositories must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`,
83
+ );
84
+ }
85
+ const ids = new Set<string>();
86
+ const repositories = result.repositories.map((entry, index) => {
87
+ const raw = requireRecord(entry, `publication repositories[${index}]`);
88
+ const repository = requireAbsolutePath(
89
+ raw.repository,
90
+ `publication repositories[${index}].repository`,
91
+ );
92
+ const id = repositoryId(repository);
93
+ if (ids.has(id)) throw new Error(`publication repository is duplicated: ${repository}`);
94
+ ids.add(id);
95
+ const dependencyFingerprint = optionalString(
96
+ raw.dependencyFingerprint,
97
+ `publication repositories[${index}].dependencyFingerprint`,
98
+ );
99
+ if (raw.pushed !== true) {
100
+ throw new Error(`publication repositories[${index}].pushed must be true`);
101
+ }
102
+ return {
103
+ id,
104
+ repository,
105
+ branch: requireString(raw.branch, `publication repositories[${index}].branch`),
106
+ baseBranch: requireString(raw.baseBranch, `publication repositories[${index}].baseBranch`),
107
+ headRevision: requireString(
108
+ raw.headRevision,
109
+ `publication repositories[${index}].headRevision`,
110
+ ),
111
+ pr: requireString(raw.pr, `publication repositories[${index}].pr`),
112
+ ...(dependencyFingerprint !== undefined ? { dependencyFingerprint } : {}),
113
+ };
114
+ });
115
+ return { repositories };
116
+ }
117
+
118
+ export function reviewerCommand(repository: PublishedRepository): CommandBatchItem {
119
+ return {
120
+ id: repository.id,
121
+ command: "pi-reviewer",
122
+ args: ["--base", repository.baseBranch],
123
+ cwd: repository.repository,
124
+ timeoutMs: REVIEW_TIMEOUT_MS,
125
+ maxOutputChars: AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
126
+ };
127
+ }
128
+
129
+ export function parseVerificationCommandPlan(value: unknown): VerificationCommandPlan {
130
+ const result = requireRecord(value, "verification command plan");
131
+ if (!Array.isArray(result.commands) || result.commands.length === 0) {
132
+ throw new Error("verification commands must be a non-empty array");
133
+ }
134
+ const directories = new Set<string>();
135
+ const commands = result.commands.map((entry, index) => {
136
+ const command = parseCommandItem(entry, `verification commands[${index}]`, {
137
+ maxTimeoutMs: VERIFICATION_TIMEOUT_MS,
138
+ });
139
+ if (directories.has(command.cwd)) {
140
+ throw new Error(
141
+ `verification commands must use distinct working directories: ${command.cwd}`,
142
+ );
143
+ }
144
+ directories.add(command.cwd);
145
+ validateVerificationCommand(command, index);
146
+ return command;
147
+ });
148
+ const validated = validateCommandBatchRequest({ items: commands, maxConcurrency: 1 });
149
+ return {
150
+ commands: validated.items,
151
+ untested: stringArray(result.untested ?? [], "verification untested"),
152
+ };
153
+ }
154
+
155
+ export function parseCiInspectionBatch(value: unknown): CiInspectionBatch {
156
+ const result = requireRecord(value, "CI inspection");
157
+ if (!Array.isArray(result.targets) || result.targets.length === 0) {
158
+ throw new Error("CI inspection targets must be a non-empty array");
159
+ }
160
+ if (result.targets.length > MAX_COMMAND_BATCH_ITEMS) {
161
+ throw new Error(
162
+ `CI inspection targets must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`,
163
+ );
164
+ }
165
+ const ids = new Set<string>();
166
+ const targets = result.targets.map((entry, index) => {
167
+ const raw = requireRecord(entry, `CI targets[${index}]`);
168
+ const repository = requireAbsolutePath(raw.repository, `CI targets[${index}].repository`);
169
+ const id = repositoryId(repository);
170
+ if (ids.has(id)) throw new Error(`CI target is duplicated: ${repository}`);
171
+ ids.add(id);
172
+ if (
173
+ raw.route !== "green" &&
174
+ raw.route !== "failed" &&
175
+ raw.route !== "pending" &&
176
+ raw.route !== "unavailable"
177
+ ) {
178
+ throw new Error(`CI targets[${index}].route is invalid`);
179
+ }
180
+ const pr = requireString(raw.pr, `CI targets[${index}].pr`);
181
+ const target: CiTargetInspection = {
182
+ id,
183
+ repository,
184
+ headRevision: requireString(raw.headRevision, `CI targets[${index}].headRevision`),
185
+ pr,
186
+ route: raw.route,
187
+ reason: requireString(raw.reason, `CI targets[${index}].reason`),
188
+ relatedFailures: stringArray(
189
+ raw.relatedFailures ?? [],
190
+ `CI targets[${index}].relatedFailures`,
191
+ ),
192
+ unrelatedFailures: stringArray(
193
+ raw.unrelatedFailures ?? [],
194
+ `CI targets[${index}].unrelatedFailures`,
195
+ ),
196
+ };
197
+ if (raw.route === "pending") {
198
+ target.trackingCommand = parseCiCommand(raw.trackingCommand, id, repository, pr);
199
+ }
200
+ return target;
201
+ });
202
+ const route = targets.some((target) => target.route === "failed")
203
+ ? "failed"
204
+ : targets.some((target) => target.route === "pending")
205
+ ? "pending"
206
+ : targets.some((target) => target.route === "unavailable")
207
+ ? "unavailable"
208
+ : "green";
209
+ return {
210
+ route,
211
+ reason: targets.map((target) => `${target.id}: ${target.reason}`).join("; "),
212
+ relatedFailures: targets.flatMap((target) => target.relatedFailures),
213
+ unrelatedFailures: targets.flatMap((target) => target.unrelatedFailures),
214
+ targets,
215
+ };
216
+ }
217
+
218
+ export function parseCiCommand(
219
+ value: unknown,
220
+ id: string,
221
+ repository: string,
222
+ pr: string,
223
+ ): CommandBatchItem {
224
+ const raw = requireRecord(value, "CI tracking command");
225
+ if (raw.id !== id) throw new Error("CI tracking command id must match the target repository");
226
+ const command = parseCommandItem(raw, "CI tracking command", {
227
+ id,
228
+ command: "gh",
229
+ cwd: repository,
230
+ maxTimeoutMs: CI_WATCH_TIMEOUT_MS,
231
+ });
232
+ const args = command.args;
233
+ const prWatch =
234
+ (args.length === 3 && args[0] === "pr" && args[1] === "checks" && args[2] === "--watch") ||
235
+ (args.length === 4 &&
236
+ args[0] === "pr" &&
237
+ args[1] === "checks" &&
238
+ args[2] === pr &&
239
+ args[3] === "--watch");
240
+ const runWatch =
241
+ args.length === 3 &&
242
+ args[0] === "run" &&
243
+ args[1] === "watch" &&
244
+ /^[1-9]\d*$/.test(args[2] ?? "");
245
+ if (!prWatch && !runWatch) {
246
+ throw new Error("CI tracking command args are not allowed for the target PR");
247
+ }
248
+ return { ...command, args: ["pr", "checks", pr, "--watch"] };
249
+ }
250
+
251
+ export function repositoryId(repository: string): string {
252
+ const canonical = path.resolve(repository);
253
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
254
+ }
255
+
256
+ function parseCommandItem(
257
+ value: unknown,
258
+ label: string,
259
+ options: {
260
+ id?: string;
261
+ command?: string;
262
+ cwd?: string;
263
+ maxTimeoutMs: number;
264
+ },
265
+ ): CommandBatchItem {
266
+ const raw = requireRecord(value, label);
267
+ const id = options.id ?? requireString(raw.id, `${label}.id`);
268
+ const command = options.command ?? requireString(raw.command, `${label}.command`);
269
+ if (options.command !== undefined && raw.command !== options.command) {
270
+ throw new Error(`${label}.command must be ${options.command}`);
271
+ }
272
+ if (!Array.isArray(raw.args) || raw.args.some((arg) => typeof arg !== "string")) {
273
+ throw new Error(`${label}.args must be an array of strings`);
274
+ }
275
+ const cwd = options.cwd ?? requireAbsolutePath(raw.cwd, `${label}.cwd`);
276
+ if (options.cwd !== undefined && path.resolve(String(raw.cwd)) !== options.cwd) {
277
+ throw new Error(`${label}.cwd must match the target repository`);
278
+ }
279
+ const timeoutMs = positiveInteger(raw.timeoutMs, `${label}.timeoutMs`, options.maxTimeoutMs);
280
+ const maxOutputChars = positiveInteger(
281
+ raw.maxOutputChars ?? AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
282
+ `${label}.maxOutputChars`,
283
+ AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
284
+ );
285
+ return { id, command, args: [...raw.args] as string[], cwd, timeoutMs, maxOutputChars };
286
+ }
287
+
288
+ function validateVerificationCommand(command: CommandBatchItem, index: number): void {
289
+ const forbiddenCommands = new Set([
290
+ "ash",
291
+ "bash",
292
+ "cmd",
293
+ "cmd.exe",
294
+ "csh",
295
+ "dash",
296
+ "fish",
297
+ "gh",
298
+ "git",
299
+ "ksh",
300
+ "powershell",
301
+ "powershell.exe",
302
+ "pwsh",
303
+ "pwsh.exe",
304
+ "rm",
305
+ "sh",
306
+ "tcsh",
307
+ "zsh",
308
+ ]);
309
+ const executable = path.win32.basename(path.basename(command.command)).toLowerCase();
310
+ if (forbiddenCommands.has(executable)) {
311
+ throw new Error(`verification commands[${index}].command is not allowed`);
312
+ }
313
+ const joined = command.args.join(" ").toLowerCase();
314
+ if (/\b(publish|release|deploy|push|merge)\b/.test(joined)) {
315
+ throw new Error(`verification commands[${index}] contains a mutation or publication action`);
316
+ }
317
+ }
318
+
319
+ function concurrencyValue(value: unknown, field: string): number {
320
+ return positiveInteger(
321
+ value,
322
+ `autoimplement concurrency.${field}`,
323
+ AUTOIMPLEMENT_MAX_CONCURRENCY,
324
+ );
325
+ }
326
+
327
+ function positiveInteger(value: unknown, label: string, maximum: number): number {
328
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
329
+ throw new Error(`${label} must be an integer from 1 through ${maximum}`);
330
+ }
331
+ return value as number;
332
+ }
333
+
334
+ function stringArray(value: unknown, label: string): string[] {
335
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
336
+ throw new Error(`${label} must be an array of strings`);
337
+ }
338
+ return [...value] as string[];
339
+ }
340
+
341
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
342
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
343
+ throw new Error(`${label} must be an object`);
344
+ }
345
+ return value as Record<string, unknown>;
346
+ }
347
+
348
+ function requireString(value: unknown, label: string): string {
349
+ if (typeof value !== "string" || value.trim().length === 0) {
350
+ throw new Error(`${label} must be a non-empty string`);
351
+ }
352
+ return value;
353
+ }
354
+
355
+ function optionalString(value: unknown, label: string): string | undefined {
356
+ if (value === undefined) return undefined;
357
+ return requireString(value, label);
358
+ }
359
+
360
+ function requireAbsolutePath(value: unknown, label: string): string {
361
+ const resolved = requireString(value, label);
362
+ if (!path.isAbsolute(resolved)) throw new Error(`${label} must be absolute`);
363
+ return path.resolve(resolved);
364
+ }