@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.
- package/README.md +11 -9
- package/dist/builtins/autodoc.workflow.d.ts +191 -4
- package/dist/builtins/autodoc.workflow.js +156 -30
- package/dist/builtins/autodoc.workflow.js.map +1 -1
- package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
- package/dist/builtins/autoimplement-command-batches.js +29 -31
- package/dist/builtins/autoimplement-command-batches.js.map +1 -1
- package/dist/builtins/autoimplement.workflow.d.ts +1259 -29
- package/dist/builtins/autoimplement.workflow.js +416 -153
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +6 -0
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +110 -0
- package/dist/builtins/change-verification.workflow.js +860 -0
- package/dist/builtins/change-verification.workflow.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -3
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +361 -6
- package/dist/builtins/plan-change.workflow.js +26 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
- package/dist/builtins/workspace-preparation.workflow.js +498 -0
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
- package/dist/controllers/sqlite.js +16 -51
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +45 -7
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.js +1 -4
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +2 -15
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/state/schema.js +22 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.js.map +1 -1
- package/dist/workflows/engine.js +4 -3
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/store.d.ts +6 -1
- package/dist/workflows/store.js +286 -33
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +1 -1
- package/docs/SQLITE_STATE.md +20 -14
- package/docs/WORKFLOW_COMPOSITION.md +8 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +1 -1
- package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- package/docs/session-event-journal.md +2 -3
- package/docs/workflows.md +43 -21
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autodoc/SKILL.md +7 -0
- package/skills/autoimplement/SKILL.md +4 -0
- package/src/builtins/autodoc.workflow.ts +184 -33
- package/src/builtins/autoimplement-command-batches.ts +39 -33
- package/src/builtins/autoimplement.workflow.ts +483 -175
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +5 -5
- package/src/builtins/change-verification.workflow.ts +1143 -0
- package/src/builtins/monitor.workflow.ts +6 -3
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/plan-change.workflow.ts +35 -0
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/builtins/workspace-preparation.workflow.ts +668 -0
- package/src/controllers/sqlite.ts +19 -53
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/executor.ts +1 -4
- package/src/extension/index.ts +2 -15
- package/src/extension/recorder.ts +10 -8
- package/src/state/schema.ts +22 -3
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/engine.ts +4 -3
- package/src/workflows/store.ts +397 -43
- package/src/workflows/types.ts +0 -1
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
package/skills/autodoc/SKILL.md
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
`
|
|
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: {
|
|
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
|
|
439
|
+
evidence: verificationResult(context),
|
|
294
440
|
},
|
|
295
|
-
verification: context
|
|
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: "
|
|
461
|
+
cases: { current: "finalize", update: "workspaceGuard", blocked: "blocked" },
|
|
315
462
|
},
|
|
316
463
|
},
|
|
317
|
-
{ from: "updateDocumentation", to: "verifyDocumentation" },
|
|
318
464
|
{
|
|
319
|
-
from: "
|
|
320
|
-
switch: { on: "$.
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 =
|
|
311
|
+
const joined = args.join(" ").toLowerCase();
|
|
314
312
|
if (/\b(publish|release|deploy|push|merge)\b/.test(joined)) {
|
|
315
|
-
throw new Error(
|
|
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,
|