@osolmaz/pi-workflows 0.13.0 → 0.13.2

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 (84) hide show
  1. package/README.md +11 -9
  2. package/dist/builtins/autodoc.workflow.d.ts +191 -4
  3. package/dist/builtins/autodoc.workflow.js +156 -30
  4. package/dist/builtins/autodoc.workflow.js.map +1 -1
  5. package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
  6. package/dist/builtins/autoimplement-command-batches.js +29 -31
  7. package/dist/builtins/autoimplement-command-batches.js.map +1 -1
  8. package/dist/builtins/autoimplement.workflow.d.ts +1259 -29
  9. package/dist/builtins/autoimplement.workflow.js +416 -153
  10. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  11. package/dist/builtins/autoplan.workflow.d.ts +6 -0
  12. package/dist/builtins/autoplan.workflow.js +68 -32
  13. package/dist/builtins/autoplan.workflow.js.map +1 -1
  14. package/dist/builtins/catalog.js +5 -5
  15. package/dist/builtins/catalog.js.map +1 -1
  16. package/dist/builtins/change-verification.workflow.d.ts +110 -0
  17. package/dist/builtins/change-verification.workflow.js +860 -0
  18. package/dist/builtins/change-verification.workflow.js.map +1 -0
  19. package/dist/builtins/monitor.workflow.js +4 -3
  20. package/dist/builtins/monitor.workflow.js.map +1 -1
  21. package/dist/builtins/plain-summary.workflow.js +35 -24
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  23. package/dist/builtins/plan-change.workflow.d.ts +361 -6
  24. package/dist/builtins/plan-change.workflow.js +26 -0
  25. package/dist/builtins/plan-change.workflow.js.map +1 -1
  26. package/dist/builtins/sanity-check.workflow.js +19 -22
  27. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  28. package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
  29. package/dist/builtins/workspace-preparation.workflow.js +498 -0
  30. package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
  31. package/dist/controllers/sqlite.js +16 -51
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.js +45 -7
  34. package/dist/extension/decision-channels.js.map +1 -1
  35. package/dist/extension/executor.js +1 -4
  36. package/dist/extension/executor.js.map +1 -1
  37. package/dist/extension/index.js +2 -15
  38. package/dist/extension/index.js.map +1 -1
  39. package/dist/extension/recorder.d.ts +1 -1
  40. package/dist/extension/recorder.js +8 -8
  41. package/dist/extension/recorder.js.map +1 -1
  42. package/dist/state/schema.js +22 -3
  43. package/dist/state/schema.js.map +1 -1
  44. package/dist/viewer/session-reducer.d.ts +0 -1
  45. package/dist/viewer/session-reducer.js +2 -24
  46. package/dist/viewer/session-reducer.js.map +1 -1
  47. package/dist/workflows/engine.js +4 -3
  48. package/dist/workflows/engine.js.map +1 -1
  49. package/dist/workflows/store.d.ts +6 -1
  50. package/dist/workflows/store.js +286 -33
  51. package/dist/workflows/store.js.map +1 -1
  52. package/dist/workflows/types.d.ts +1 -1
  53. package/docs/SQLITE_STATE.md +20 -14
  54. package/docs/WORKFLOW_COMPOSITION.md +8 -0
  55. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +1 -1
  56. package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
  57. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  58. package/docs/session-event-journal.md +2 -3
  59. package/docs/workflows.md +43 -21
  60. package/herdr-plugin.toml +1 -1
  61. package/package.json +1 -1
  62. package/skills/autodoc/SKILL.md +7 -0
  63. package/skills/autoimplement/SKILL.md +4 -0
  64. package/src/builtins/autodoc.workflow.ts +184 -33
  65. package/src/builtins/autoimplement-command-batches.ts +39 -33
  66. package/src/builtins/autoimplement.workflow.ts +483 -175
  67. package/src/builtins/autoplan.workflow.ts +80 -32
  68. package/src/builtins/catalog.ts +5 -5
  69. package/src/builtins/change-verification.workflow.ts +1143 -0
  70. package/src/builtins/monitor.workflow.ts +6 -3
  71. package/src/builtins/plain-summary.workflow.ts +38 -40
  72. package/src/builtins/plan-change.workflow.ts +35 -0
  73. package/src/builtins/sanity-check.workflow.ts +19 -22
  74. package/src/builtins/workspace-preparation.workflow.ts +668 -0
  75. package/src/controllers/sqlite.ts +19 -53
  76. package/src/extension/decision-channels.ts +47 -7
  77. package/src/extension/executor.ts +1 -4
  78. package/src/extension/index.ts +2 -15
  79. package/src/extension/recorder.ts +10 -8
  80. package/src/state/schema.ts +22 -3
  81. package/src/viewer/session-reducer.ts +2 -29
  82. package/src/workflows/engine.ts +4 -3
  83. package/src/workflows/store.ts +397 -43
  84. package/src/workflows/types.ts +0 -1
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.13.0"
3
+ version = "0.13.2"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -15,6 +15,10 @@ Build the input as follows:
15
15
  - `task`: State what selected plan must be recorded and that this run is documentation-only.
16
16
  - `plan`: Pass the complete selected plan. Autodoc does not devise or improve it.
17
17
  - `repository`: Use the absolute path of the repository that owns the canonical documentation.
18
+ - `baseBranch`: Use the requested base or the repository default branch.
19
+ - `scope`: Name the allowed repository and documentation edits. Exclude implementation and unauthorized remote actions.
20
+ - `workspaceMode`: Use `auto` unless the conversation requires `branch`, `worktree`, or explicitly authorized `defaultBranch` work. Autodoc prepares the workspace only when documentation needs an update.
21
+ - `preparedWorkspace`: Include a previously confirmed `pi-workflows.prepared-workspace.v1` result when a parent workflow already prepared the workspace. Omit it otherwise.
18
22
  - `documents`: Include every known canonical specification or plan candidate. Use an empty array when none is known.
19
23
  - `evidence`: Include implementation evidence or current-document evidence when it affects whether documentation is current.
20
24
 
@@ -31,6 +35,9 @@ Replace the example values below with facts from the conversation, then make one
31
35
  "requirements": ["Keep cancellation terminal."]
32
36
  },
33
37
  "repository": "/absolute/path/to/repository",
38
+ "baseBranch": "main",
39
+ "scope": "Only /absolute/path/to/repository. May update canonical documentation and run documentation checks. Must not implement, push, merge, release, or deploy.",
40
+ "workspaceMode": "auto",
34
41
  "documents": ["docs/plans/timeout-fallback-plan.md"],
35
42
  "evidence": {
36
43
  "currentBehavior": "A timeout ends the run."
@@ -18,6 +18,9 @@ Build the input as follows:
18
18
  - `scope`: Always include a concrete authority statement. Name every allowed repository and the allowed edit, test, commit, push, pull-request, merge, and release actions. Carry forward exclusions from the conversation. A repository path alone is not a scope.
19
19
  - `constraints`: Include all applicable user and repository constraints. Use an empty array when none apply.
20
20
  - `baseBranch`: Use the requested base or the repository default branch.
21
+ - `workspaceMode`: Use `auto` unless the user or repository requires `branch`, `worktree`, or `defaultBranch`. `auto` keeps a correct task branch, creates a task branch from a clean default branch, and isolates a dirty default checkout in a standard sibling worktree. Use `defaultBranch` only with explicit direct-work authority.
22
+ - `preparedWorkspace`: Include a previously confirmed `pi-workflows.prepared-workspace.v1` result when the workspace was prepared before the run. Omit it otherwise.
23
+ - `directDefaultBranchAuthorized`: Set `true` only when direct work on the actual default branch is explicit. It does not grant commit, push, merge, or release authority.
21
24
  - `merge`: Set `true` only when the user explicitly requested merge or an applicable standing instruction authorizes it. Otherwise set `false`.
22
25
  - `documents`: Include known canonical plan or specification paths. Use an empty array when none are known.
23
26
  - `approval`: Omit it for the default behavior: ask on each new plan and continue after 10 minutes without an answer. Use `{ "mode": "required" }` when the user says to block on plan changes. Use `{ "mode": "skip" }` when the user says to continue without asking about plan changes.
@@ -46,6 +49,7 @@ Replace the example values below with facts from the conversation, then make one
46
49
  "scope": "Only /absolute/path/to/repository. May edit and test task-related files, create commits, push the task branch, and open or update its pull request. Must not modify other repositories, merge, release, deploy, change credentials, or change repository policy.",
47
50
  "constraints": ["Preserve immediate cancellation.", "Keep deferred-turn work separate."],
48
51
  "baseBranch": "main",
52
+ "workspaceMode": "auto",
49
53
  "merge": false,
50
54
  "documents": ["docs/plans/timeout-fallback-plan.md"]
51
55
  }
@@ -1,11 +1,35 @@
1
- import { agent, compute, defineWorkflow } from "../workflows/definition.js";
1
+ import path from "node:path";
2
+ import {
3
+ agent,
4
+ compute,
5
+ defineWorkflow,
6
+ includeWorkflow,
7
+ includedResult,
8
+ } from "../workflows/definition.js";
2
9
  import { digest } from "../workflows/human-decision.js";
3
10
  import type { WorkflowNodeContext } from "../workflows/types.js";
11
+ import changeVerificationWorkflow, {
12
+ type ChangeVerificationInput,
13
+ type ChangeVerificationResult,
14
+ type VerificationCheck,
15
+ } from "./change-verification.workflow.js";
16
+ import workspacePreparationWorkflow, {
17
+ parsePreparedWorkspace,
18
+ type PreparedWorkspace,
19
+ type WorkspaceMode,
20
+ type WorkspacePreparationInput,
21
+ } from "./workspace-preparation.workflow.js";
4
22
 
5
23
  export type AutodocInput = {
6
24
  task: string;
7
25
  plan?: unknown;
8
26
  repository?: string;
27
+ baseBranch?: string;
28
+ scope?: string;
29
+ workspaceMode?: WorkspaceMode;
30
+ directDefaultBranchAuthorized?: boolean;
31
+ preparedWorkspace?: PreparedWorkspace;
32
+ verificationChecks?: VerificationCheck[];
9
33
  documents?: string[];
10
34
  evidence?: unknown;
11
35
  };
@@ -21,7 +45,8 @@ export type DocumentedPlan = {
21
45
  digests: Record<string, string>;
22
46
  evidence: unknown;
23
47
  };
24
- verification: unknown;
48
+ verification: ChangeVerificationResult | { route: "ready"; reason: string };
49
+ workspace?: PreparedWorkspace;
25
50
  };
26
51
 
27
52
  export type AutodocBlocked = {
@@ -29,6 +54,7 @@ export type AutodocBlocked = {
29
54
  task: string;
30
55
  reason: string;
31
56
  evidence: unknown;
57
+ sourceNode?: string;
32
58
  };
33
59
 
34
60
  type LocatedPlan = {
@@ -47,6 +73,10 @@ type DocumentationAssessment = {
47
73
  evidence: unknown;
48
74
  };
49
75
 
76
+ type WorkspaceGuard =
77
+ | { route: "ready"; repository: string }
78
+ | { route: "blocked"; reason: string; evidence: string[] };
79
+
50
80
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
51
81
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
52
82
  throw new Error(`${label} must be an object`);
@@ -61,6 +91,12 @@ function requireString(value: unknown, label: string): string {
61
91
  return value.trim();
62
92
  }
63
93
 
94
+ function requireAbsolutePath(value: unknown, label: string): string {
95
+ const result = requireString(value, label);
96
+ if (!path.isAbsolute(result)) throw new Error(`${label} must be absolute`);
97
+ return path.resolve(result);
98
+ }
99
+
64
100
  function stringArray(value: unknown, label: string): string[] {
65
101
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
66
102
  throw new Error(`${label} must be an array of strings`);
@@ -77,15 +113,38 @@ function stringRecord(value: unknown, label: string): Record<string, string> {
77
113
  return record as Record<string, string>;
78
114
  }
79
115
 
116
+ function parseWorkspaceMode(value: unknown): WorkspaceMode | undefined {
117
+ if (value === undefined) return undefined;
118
+ if (value !== "auto" && value !== "branch" && value !== "worktree" && value !== "defaultBranch") {
119
+ throw new Error("autodoc workspaceMode must be auto, branch, worktree, or defaultBranch");
120
+ }
121
+ return value;
122
+ }
123
+
80
124
  function parseInput(value: unknown): AutodocInput {
81
125
  const input = requireRecord(value, "autodoc input");
82
126
  const documents = input.documents;
127
+ const workspaceMode = parseWorkspaceMode(input.workspaceMode);
83
128
  return {
84
129
  task: requireString(input.task, "autodoc task"),
85
130
  ...(input.plan !== undefined ? { plan: input.plan } : {}),
86
131
  ...(input.repository !== undefined
87
- ? { repository: requireString(input.repository, "autodoc repository") }
132
+ ? { repository: requireAbsolutePath(input.repository, "autodoc repository") }
133
+ : {}),
134
+ ...(input.baseBranch !== undefined
135
+ ? { baseBranch: requireString(input.baseBranch, "autodoc baseBranch") }
88
136
  : {}),
137
+ ...(input.scope !== undefined ? { scope: requireString(input.scope, "autodoc scope") } : {}),
138
+ ...(workspaceMode === undefined ? {} : { workspaceMode }),
139
+ ...(input.directDefaultBranchAuthorized === undefined
140
+ ? {}
141
+ : { directDefaultBranchAuthorized: input.directDefaultBranchAuthorized === true }),
142
+ ...(input.preparedWorkspace === undefined
143
+ ? {}
144
+ : { preparedWorkspace: parsePreparedWorkspace(input.preparedWorkspace) }),
145
+ ...(input.verificationChecks === undefined
146
+ ? {}
147
+ : { verificationChecks: input.verificationChecks as VerificationCheck[] }),
89
148
  ...(documents !== undefined ? { documents: stringArray(documents, "autodoc documents") } : {}),
90
149
  ...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
91
150
  };
@@ -132,10 +191,61 @@ function currentPlan(context: WorkflowNodeContext): unknown {
132
191
  throw new Error("autodoc does not have a selected plan");
133
192
  }
134
193
 
194
+ function mutationRepository(request: AutodocInput): string {
195
+ if (request.repository !== undefined) return request.repository;
196
+ if (request.preparedWorkspace !== undefined) return request.preparedWorkspace.repository;
197
+ throw new Error("autodoc mutation requires an explicit repository");
198
+ }
199
+
200
+ function preparedWorkspace(context: WorkflowNodeContext): PreparedWorkspace {
201
+ const request = context.input as AutodocInput;
202
+ if (request.preparedWorkspace !== undefined) return request.preparedWorkspace;
203
+ const result = includedResult(workspacePreparationWorkflow, context.outputs.workspace);
204
+ if (result.exit !== "ready") throw new Error("autodoc workspace is not ready");
205
+ return result.output;
206
+ }
207
+
208
+ function verificationResult(context: WorkflowNodeContext): ChangeVerificationResult {
209
+ const result = includedResult(changeVerificationWorkflow, context.outputs.verification);
210
+ return result.output;
211
+ }
212
+
135
213
  function blockedReason(context: WorkflowNodeContext): AutodocBlocked {
136
214
  const request = context.input as AutodocInput;
137
215
  const assessment = context.outputs.inspectDocumentation as DocumentationAssessment | undefined;
138
216
  const located = context.outputs.locatePlan as LocatedPlan | undefined;
217
+ if (context.outputs.verification !== undefined) {
218
+ const result = includedResult(changeVerificationWorkflow, context.outputs.verification);
219
+ return {
220
+ status: "blocked",
221
+ task: request.task,
222
+ reason: result.output.reason,
223
+ evidence: result.output,
224
+ sourceNode: result.output.qualifiedNode,
225
+ };
226
+ }
227
+ const guard = context.outputs.workspaceGuard as WorkspaceGuard | undefined;
228
+ if (guard?.route === "blocked") {
229
+ return {
230
+ status: "blocked",
231
+ task: request.task,
232
+ reason: guard.reason,
233
+ evidence: guard.evidence,
234
+ sourceNode: "autodoc/workspaceGuard",
235
+ };
236
+ }
237
+ if (context.outputs.workspace !== undefined) {
238
+ const result = includedResult(workspacePreparationWorkflow, context.outputs.workspace);
239
+ if (result.exit === "blocked") {
240
+ return {
241
+ status: "blocked",
242
+ task: request.task,
243
+ reason: result.output.reason,
244
+ evidence: result.output,
245
+ sourceNode: "autodoc/workspace",
246
+ };
247
+ }
248
+ }
139
249
  return {
140
250
  status: "blocked",
141
251
  task: request.task,
@@ -146,6 +256,8 @@ function blockedReason(context: WorkflowNodeContext): AutodocBlocked {
146
256
  ? located.reason
147
257
  : "Autodoc could not identify a clear selected plan or canonical document target.",
148
258
  evidence: assessment?.evidence ?? located?.evidence ?? null,
259
+ sourceNode:
260
+ assessment?.route === "blocked" ? "autodoc/inspectDocumentation" : "autodoc/locatePlan",
149
261
  };
150
262
  }
151
263
 
@@ -156,7 +268,42 @@ export const autodocWorkflow = defineWorkflow({
156
268
  input: parseInput,
157
269
  title: ({ input }) => `autodoc: ${input.task.slice(0, 60)}`,
158
270
  startAt: "prepare",
159
- maxSteps: 24,
271
+ maxSteps: 80,
272
+ includes: {
273
+ workspace: includeWorkflow(workspacePreparationWorkflow, {
274
+ input: (context): WorkspacePreparationInput => {
275
+ const request = context.input as AutodocInput;
276
+ return {
277
+ repository: mutationRepository(request),
278
+ ...(request.baseBranch === undefined ? {} : { baseBranch: request.baseBranch }),
279
+ ...(request.scope === undefined ? {} : { scope: request.scope }),
280
+ ...(request.workspaceMode === undefined ? {} : { workspaceMode: request.workspaceMode }),
281
+ ...(request.directDefaultBranchAuthorized === undefined
282
+ ? {}
283
+ : { directDefaultBranchAuthorized: request.directDefaultBranchAuthorized }),
284
+ ...(request.preparedWorkspace === undefined
285
+ ? {}
286
+ : { preparedWorkspace: request.preparedWorkspace }),
287
+ };
288
+ },
289
+ }),
290
+ verification: includeWorkflow(changeVerificationWorkflow, {
291
+ input: (context): ChangeVerificationInput => {
292
+ const request = context.input as AutodocInput;
293
+ const update = requireRecord(context.outputs.updateDocumentation, "documentation update");
294
+ return {
295
+ originatingWorkflow: "autodoc",
296
+ qualifiedNode: "autodoc/verification",
297
+ workspace: preparedWorkspace(context),
298
+ ...(request.verificationChecks === undefined
299
+ ? {}
300
+ : { checks: request.verificationChecks }),
301
+ changedFiles: stringArray(update.files, "updated documentation files"),
302
+ plan: currentPlan(context),
303
+ };
304
+ },
305
+ }),
306
+ },
160
307
  exits: {
161
308
  ready: {
162
309
  from: "finalize",
@@ -184,14 +331,26 @@ export const autodocWorkflow = defineWorkflow({
184
331
  "Return blocked when no single clear selected plan exists.",
185
332
  `Task: ${request.task}`,
186
333
  `Repository: ${request.repository ?? "current repository"}`,
187
- `Referenced documents: ${JSON.stringify(request.documents ?? [])}`,
188
- `Evidence: ${JSON.stringify(request.evidence ?? null)}`,
334
+ `Preferred documents: ${JSON.stringify(request.documents ?? [])}`,
189
335
  ].join("\n");
190
336
  },
191
337
  expectedOutput:
192
338
  '{ "route": "found" | "blocked", "plan": {} (required when found), "sources": ["source"], "reason": "reason", "evidence": "evidence" }',
193
339
  validate: parseLocatedPlan,
194
340
  }),
341
+ workspaceGuard: compute({
342
+ run: ({ input }): WorkspaceGuard => {
343
+ const request = input as AutodocInput;
344
+ if (request.repository !== undefined || request.preparedWorkspace !== undefined) {
345
+ return { route: "ready", repository: mutationRepository(request) };
346
+ }
347
+ return {
348
+ route: "blocked",
349
+ reason: "Updating canonical documentation requires an explicit repository path.",
350
+ evidence: ["repository was not supplied", "preparedWorkspace was not supplied"],
351
+ };
352
+ },
353
+ }),
195
354
  inspectDocumentation: agent({
196
355
  statusDetail: "checking canonical documentation",
197
356
  prompt: (context) => {
@@ -221,6 +380,7 @@ export const autodocWorkflow = defineWorkflow({
221
380
  "Update the canonical specification and implementation plan to preserve the selected plan exactly.",
222
381
  "Use the repository documentation rules and keep the text plain and complete.",
223
382
  "Do not redesign the solution and do not implement code.",
383
+ `Prepared workspace: ${JSON.stringify(preparedWorkspace(context))}`,
224
384
  `Task: ${request.task}`,
225
385
  `Selected plan: ${JSON.stringify(currentPlan(context))}`,
226
386
  `Canonical files: ${JSON.stringify(assessment.files)}`,
@@ -239,26 +399,6 @@ export const autodocWorkflow = defineWorkflow({
239
399
  return result;
240
400
  },
241
401
  }),
242
- verifyDocumentation: agent({
243
- timeoutMs: 20 * 60_000,
244
- statusDetail: "verifying documentation",
245
- prompt: ({ outputs }) =>
246
- [
247
- "Verify the changed canonical documentation.",
248
- "Run the repository documentation, formatting, link, and privacy checks that apply.",
249
- "Do not implement code.",
250
- `Documentation update: ${JSON.stringify(outputs.updateDocumentation)}`,
251
- ].join("\n"),
252
- expectedOutput:
253
- '{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"] }',
254
- validate: (value) => {
255
- const result = requireRecord(value, "documentation verification");
256
- if (typeof result.passed !== "boolean") {
257
- throw new Error("documentation verification passed must be boolean");
258
- }
259
- return result;
260
- },
261
- }),
262
402
  finalize: compute({
263
403
  run: (context) => {
264
404
  const request = context.input as AutodocInput;
@@ -277,7 +417,13 @@ export const autodocWorkflow = defineWorkflow({
277
417
  digests: assessment.digests,
278
418
  evidence: assessment.evidence,
279
419
  },
280
- verification: { passed: true, reason: "Canonical documentation was already current." },
420
+ verification: {
421
+ route: "ready",
422
+ reason: "Canonical documentation was already current.",
423
+ },
424
+ ...(request.preparedWorkspace === undefined
425
+ ? {}
426
+ : { workspace: request.preparedWorkspace }),
281
427
  } satisfies DocumentedPlan;
282
428
  }
283
429
  const updateRecord = requireRecord(updated, "documentation update");
@@ -290,9 +436,10 @@ export const autodocWorkflow = defineWorkflow({
290
436
  state: "updated",
291
437
  files: stringArray(updateRecord.files, "updated documentation files"),
292
438
  digests: stringRecord(updateRecord.digests, "updated documentation digests"),
293
- evidence: context.outputs.verifyDocumentation,
439
+ evidence: verificationResult(context),
294
440
  },
295
- verification: context.outputs.verifyDocumentation,
441
+ verification: verificationResult(context),
442
+ workspace: preparedWorkspace(context),
296
443
  } satisfies DocumentedPlan;
297
444
  },
298
445
  }),
@@ -311,14 +458,18 @@ export const autodocWorkflow = defineWorkflow({
311
458
  from: "inspectDocumentation",
312
459
  switch: {
313
460
  on: "$.route",
314
- cases: { current: "finalize", update: "updateDocumentation", blocked: "blocked" },
461
+ cases: { current: "finalize", update: "workspaceGuard", blocked: "blocked" },
315
462
  },
316
463
  },
317
- { from: "updateDocumentation", to: "verifyDocumentation" },
318
464
  {
319
- from: "verifyDocumentation",
320
- switch: { on: "$.passed", cases: { true: "finalize", false: "blocked" } },
465
+ from: "workspaceGuard",
466
+ switch: { on: "$.route", cases: { ready: "workspace", blocked: "blocked" } },
321
467
  },
468
+ { from: "workspace.ready", to: "updateDocumentation" },
469
+ { from: "workspace.blocked", to: "blocked" },
470
+ { from: "updateDocumentation", to: "verification" },
471
+ { from: "verification.ready", to: "finalize" },
472
+ { from: "verification.blocked", to: "blocked" },
322
473
  ],
323
474
  });
324
475
 
@@ -131,17 +131,10 @@ export function parseVerificationCommandPlan(value: unknown): VerificationComman
131
131
  if (!Array.isArray(result.commands) || result.commands.length === 0) {
132
132
  throw new Error("verification commands must be a non-empty array");
133
133
  }
134
- const directories = new Set<string>();
135
134
  const commands = result.commands.map((entry, index) => {
136
135
  const command = parseCommandItem(entry, `verification commands[${index}]`, {
137
136
  maxTimeoutMs: VERIFICATION_TIMEOUT_MS,
138
137
  });
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
138
  validateVerificationCommand(command, index);
146
139
  return command;
147
140
  });
@@ -285,37 +278,50 @@ function parseCommandItem(
285
278
  return { id, command, args: [...raw.args] as string[], cwd, timeoutMs, maxOutputChars };
286
279
  }
287
280
 
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`);
281
+ const FORBIDDEN_VERIFICATION_EXECUTABLES = new Set([
282
+ "ash",
283
+ "bash",
284
+ "cmd",
285
+ "cmd.exe",
286
+ "csh",
287
+ "dash",
288
+ "fish",
289
+ "gh",
290
+ "git",
291
+ "ksh",
292
+ "powershell",
293
+ "powershell.exe",
294
+ "pwsh",
295
+ "pwsh.exe",
296
+ "rm",
297
+ "sh",
298
+ "tcsh",
299
+ "zsh",
300
+ ]);
301
+
302
+ export function validateVerificationCommandSafety(
303
+ command: string,
304
+ args: readonly string[],
305
+ label: string,
306
+ ): void {
307
+ const executable = path.win32.basename(path.basename(command)).toLowerCase();
308
+ if (FORBIDDEN_VERIFICATION_EXECUTABLES.has(executable)) {
309
+ throw new Error(`${label}.command is not allowed`);
312
310
  }
313
- const joined = command.args.join(" ").toLowerCase();
311
+ const joined = args.join(" ").toLowerCase();
314
312
  if (/\b(publish|release|deploy|push|merge)\b/.test(joined)) {
315
- throw new Error(`verification commands[${index}] contains a mutation or publication action`);
313
+ throw new Error(`${label} contains a mutation or publication action`);
316
314
  }
317
315
  }
318
316
 
317
+ function validateVerificationCommand(command: CommandBatchItem, index: number): void {
318
+ validateVerificationCommandSafety(
319
+ command.command,
320
+ command.args,
321
+ `verification commands[${index}]`,
322
+ );
323
+ }
324
+
319
325
  function concurrencyValue(value: unknown, field: string): number {
320
326
  return positiveInteger(
321
327
  value,