@wichayutdew/pi-workflows 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -84
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +3 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -7,7 +7,7 @@ import type { WorkflowCatalog } from '../config/types.ts';
|
|
|
7
7
|
import type { WorkflowRun } from '../engine/state.ts';
|
|
8
8
|
import type { MainStepRuntimeController } from '../runtime/main-step-runtime.ts';
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
formatWorkflowProgressStatus,
|
|
11
11
|
type WorkflowStatusExecution,
|
|
12
12
|
type WorkflowStatusSnapshot,
|
|
13
13
|
} from '../workflow-status.ts';
|
|
@@ -45,7 +45,6 @@ export type StatusActions = {
|
|
|
45
45
|
refreshStatusWhileRunning: (this: StatusContext) => void;
|
|
46
46
|
stopStatusRefresh: (this: StatusContext) => void;
|
|
47
47
|
registerWorkflowStatusShortcut: (this: StatusContext) => void;
|
|
48
|
-
openWorkflowStatus: (this: StatusContext, context: ExtensionContext) => void;
|
|
49
48
|
showWorkflowStatus: (
|
|
50
49
|
this: StatusContext,
|
|
51
50
|
context: ExtensionContext,
|
|
@@ -88,13 +87,17 @@ function updateStatus(this: StatusContext): void {
|
|
|
88
87
|
return;
|
|
89
88
|
}
|
|
90
89
|
const snapshot = this.workflowStatusSnapshot();
|
|
91
|
-
if (this.run.status !== 'running') {
|
|
90
|
+
if (this.run.status !== 'running' && this.run.status !== 'awaiting-gate') {
|
|
91
|
+
this.latestContext.ui.setStatus(STATUS_KEY, undefined);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!snapshot) {
|
|
92
95
|
this.latestContext.ui.setStatus(STATUS_KEY, undefined);
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
this.latestContext.ui.setStatus(
|
|
96
99
|
STATUS_KEY,
|
|
97
|
-
|
|
100
|
+
formatWorkflowProgressStatus(snapshot, this.statusShortcutLabel),
|
|
98
101
|
);
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -135,18 +138,6 @@ function registerWorkflowStatusShortcut(this: StatusContext): void {
|
|
|
135
138
|
});
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
function openWorkflowStatus(
|
|
139
|
-
this: StatusContext,
|
|
140
|
-
context: ExtensionContext,
|
|
141
|
-
): void {
|
|
142
|
-
void this.showWorkflowStatus(context).catch((error: unknown) => {
|
|
143
|
-
context.ui.notify(
|
|
144
|
-
`Cannot open workflow status: ${error instanceof Error ? error.message : String(error)}`,
|
|
145
|
-
'error',
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
141
|
async function showWorkflowStatus(
|
|
151
142
|
this: StatusContext,
|
|
152
143
|
context: ExtensionContext,
|
|
@@ -185,7 +176,6 @@ export function createStatusActions(): StatusActions {
|
|
|
185
176
|
refreshStatusWhileRunning,
|
|
186
177
|
stopStatusRefresh,
|
|
187
178
|
registerWorkflowStatusShortcut,
|
|
188
|
-
openWorkflowStatus,
|
|
189
179
|
showWorkflowStatus,
|
|
190
180
|
};
|
|
191
181
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { WorkflowStep } from '../config/types.ts';
|
|
2
|
+
import type { WorkflowRun } from '../engine/state.ts';
|
|
3
|
+
import type { RunStepEffects } from '../engine/run-advance.ts';
|
|
4
|
+
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
5
|
+
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
6
|
+
|
|
7
|
+
type StepEffectDependencies = Pick<
|
|
8
|
+
WorkflowHarnessDependencies,
|
|
9
|
+
'resolveWorkspaceDirectory'
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
const boundWorkspaceCwd = (run: WorkflowRun): string | undefined => {
|
|
13
|
+
for (let index = run.history.length - 1; index >= 0; index -= 1) {
|
|
14
|
+
const cwd = run.history[index]?.workspaceCwd;
|
|
15
|
+
if (cwd) return cwd;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates declarative effects carried by a structured step result.
|
|
22
|
+
*
|
|
23
|
+
* No prompt text, summary, artifact, command, or repository convention is
|
|
24
|
+
* interpreted here.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveStepEffects(
|
|
27
|
+
run: WorkflowRun,
|
|
28
|
+
step: WorkflowStep,
|
|
29
|
+
result: WorkflowStepResult,
|
|
30
|
+
dependencies: StepEffectDependencies,
|
|
31
|
+
): RunStepEffects {
|
|
32
|
+
const shouldBind = step.workspace?.bindOn.includes(result.outcome) ?? false;
|
|
33
|
+
if (shouldBind && !result.workspace) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`step outcome "${result.outcome}" requires a workspace result`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!shouldBind && result.workspace) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`step outcome "${result.outcome}" is not allowed to bind a workspace`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!result.workspace || !step.workspace) return {};
|
|
44
|
+
const startCwd = run.startCwd ?? run.cwd;
|
|
45
|
+
if (!startCwd) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'workflow run has no starting directory for workspace validation',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const workspaceCwd = dependencies.resolveWorkspaceDirectory({
|
|
52
|
+
candidateCwd: result.workspace.cwd,
|
|
53
|
+
startCwd,
|
|
54
|
+
allowedRoots: step.workspace.allowedRoots,
|
|
55
|
+
});
|
|
56
|
+
const existingCwd = boundWorkspaceCwd(run);
|
|
57
|
+
if (existingCwd && existingCwd !== workspaceCwd) {
|
|
58
|
+
throw new Error(`workflow workspace is already bound to "${existingCwd}"`);
|
|
59
|
+
}
|
|
60
|
+
return { workspaceCwd };
|
|
61
|
+
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
|
|
3
3
|
import type { WorkflowRun } from '../engine/state.ts';
|
|
4
|
+
import {
|
|
5
|
+
appendMainStepLog,
|
|
6
|
+
beginMainStepAttempt,
|
|
7
|
+
beginSubagentStepAttempt,
|
|
8
|
+
recordCurrentStepResult,
|
|
9
|
+
} from '../engine/step-trace.ts';
|
|
4
10
|
import { advanceRun, allowedOutcomes } from '../engine/transitions.ts';
|
|
5
11
|
import { digest } from '../digest.ts';
|
|
6
12
|
import type { SubagentDelegationResponse } from '../integrations/subagents/protocol.ts';
|
|
7
13
|
import { buildMainStepTask } from '../prompt.ts';
|
|
8
|
-
import { narrowApprovedBashCommands } from '../policy/approved-commands.ts';
|
|
9
14
|
import type { MainStepExecution } from '../runtime/main-step-runtime.ts';
|
|
10
15
|
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
11
16
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
12
17
|
import { createDelegationPlan } from './delegation-plan.ts';
|
|
13
18
|
import type { DelegationRecovery, MainStepIdentity } from './types.ts';
|
|
19
|
+
import { resolveStepEffects } from './step-effects.ts';
|
|
14
20
|
|
|
15
21
|
type HarnessActionContext = Pick<
|
|
16
22
|
FullHarnessActionContext,
|
|
@@ -25,10 +31,13 @@ type HarnessActionContext = Pick<
|
|
|
25
31
|
| 'mainSteps'
|
|
26
32
|
| 'mutationQueue'
|
|
27
33
|
| 'pauseForExecutionFailure'
|
|
34
|
+
| 'persist'
|
|
28
35
|
| 'pi'
|
|
29
36
|
| 'queueDelegationFailure'
|
|
30
37
|
| 'queueDelegationResponse'
|
|
38
|
+
| 'queueMainStepLog'
|
|
31
39
|
| 'queueMainStepResult'
|
|
40
|
+
| 'recordMainStepLog'
|
|
32
41
|
| 'run'
|
|
33
42
|
| 'sessionEpoch'
|
|
34
43
|
| 'settleAfterTransition'
|
|
@@ -49,6 +58,18 @@ export type StepExecutionActions = {
|
|
|
49
58
|
run: WorkflowRun,
|
|
50
59
|
step: WorkflowStep,
|
|
51
60
|
) => void;
|
|
61
|
+
queueMainStepLog: (
|
|
62
|
+
this: HarnessActionContext,
|
|
63
|
+
identity: MainStepIdentity,
|
|
64
|
+
lines: ReadonlyArray<string>,
|
|
65
|
+
context: ExtensionContext,
|
|
66
|
+
) => Promise<void>;
|
|
67
|
+
recordMainStepLog: (
|
|
68
|
+
this: HarnessActionContext,
|
|
69
|
+
identity: MainStepIdentity,
|
|
70
|
+
lines: ReadonlyArray<string>,
|
|
71
|
+
context: ExtensionContext,
|
|
72
|
+
) => Promise<void>;
|
|
52
73
|
queueMainStepResult: (
|
|
53
74
|
this: HarnessActionContext,
|
|
54
75
|
identity: MainStepIdentity,
|
|
@@ -107,6 +128,18 @@ function launchCurrentStep(
|
|
|
107
128
|
}
|
|
108
129
|
const { active, request } = plan;
|
|
109
130
|
|
|
131
|
+
try {
|
|
132
|
+
this.run = beginSubagentStepAttempt(
|
|
133
|
+
run,
|
|
134
|
+
active.requestId,
|
|
135
|
+
active.agent,
|
|
136
|
+
active.transcriptTask,
|
|
137
|
+
this.dependencies.now(),
|
|
138
|
+
);
|
|
139
|
+
this.persist();
|
|
140
|
+
} catch {
|
|
141
|
+
// Status evidence is best-effort and must never block step execution.
|
|
142
|
+
}
|
|
110
143
|
this.activeDelegation = active;
|
|
111
144
|
this.updateStatus();
|
|
112
145
|
this.latestContext?.ui.notify(
|
|
@@ -149,17 +182,38 @@ function launchMainStep(
|
|
|
149
182
|
run: WorkflowRun,
|
|
150
183
|
step: WorkflowStep,
|
|
151
184
|
): void {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
const actualCwd = this.latestContext?.cwd;
|
|
186
|
+
if (!run.cwd || !actualCwd) {
|
|
187
|
+
this.pauseForExecutionFailure(
|
|
188
|
+
'Main-agent step',
|
|
189
|
+
'Workflow working directory is unavailable; abort it and start a new run',
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const expectedCanonicalCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
195
|
+
candidateCwd: run.cwd,
|
|
196
|
+
startCwd: run.cwd,
|
|
197
|
+
allowedRoots: ['.'],
|
|
198
|
+
});
|
|
199
|
+
const actualCanonicalCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
200
|
+
candidateCwd: actualCwd,
|
|
201
|
+
startCwd: actualCwd,
|
|
202
|
+
allowedRoots: ['.'],
|
|
203
|
+
});
|
|
204
|
+
if (actualCanonicalCwd !== expectedCanonicalCwd) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`current session cwd "${actualCwd}" does not match captured workflow cwd "${run.cwd}"`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.pauseForExecutionFailure(
|
|
211
|
+
'Main-agent step',
|
|
212
|
+
error instanceof Error ? error.message : String(error),
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
163
217
|
const policyDigest = digest({
|
|
164
218
|
version: 1,
|
|
165
219
|
execution: 'main',
|
|
@@ -168,32 +222,52 @@ function launchMainStep(
|
|
|
168
222
|
stepId: run.currentStepId,
|
|
169
223
|
stepDigest: run.currentStepDigest,
|
|
170
224
|
permissions: step.permissions,
|
|
171
|
-
approvedBashCommands,
|
|
172
225
|
nonce: this.dependencies.createRequestId(),
|
|
173
226
|
});
|
|
227
|
+
const task = buildMainStepTask(workflow, run);
|
|
228
|
+
const identity: MainStepIdentity = {
|
|
229
|
+
requestId: policyDigest,
|
|
230
|
+
runId: run.runId,
|
|
231
|
+
stepId: run.currentStepId,
|
|
232
|
+
stepDigest: run.currentStepDigest,
|
|
233
|
+
sessionEpoch: this.sessionEpoch,
|
|
234
|
+
};
|
|
174
235
|
const execution: MainStepExecution = {
|
|
175
236
|
workflowId: workflow.definition.id,
|
|
176
237
|
runId: run.runId,
|
|
177
238
|
stepId: run.currentStepId,
|
|
178
239
|
stepDigest: run.currentStepDigest,
|
|
179
240
|
policyDigest,
|
|
241
|
+
task,
|
|
180
242
|
step: structuredClone(step),
|
|
181
|
-
approvedBashCommands,
|
|
182
243
|
outcomes: allowedOutcomes(workflow, run),
|
|
183
244
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
184
245
|
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
246
|
+
onTrace: (lines, context) =>
|
|
247
|
+
this.queueMainStepLog(identity, lines, context),
|
|
185
248
|
onSettled: (result, context) =>
|
|
186
249
|
this.queueMainStepResult(identity, result, context),
|
|
187
250
|
};
|
|
188
251
|
|
|
189
252
|
try {
|
|
253
|
+
try {
|
|
254
|
+
this.run = beginMainStepAttempt(
|
|
255
|
+
run,
|
|
256
|
+
policyDigest,
|
|
257
|
+
task,
|
|
258
|
+
this.dependencies.now(),
|
|
259
|
+
);
|
|
260
|
+
this.persist();
|
|
261
|
+
} catch {
|
|
262
|
+
// Status evidence is best-effort and must never block step execution.
|
|
263
|
+
}
|
|
190
264
|
this.mainSteps.activate(execution);
|
|
191
265
|
this.updateStatus();
|
|
192
266
|
this.latestContext?.ui.notify(
|
|
193
267
|
`Started "${run.currentStepId}" in the main agent`,
|
|
194
268
|
'info',
|
|
195
269
|
);
|
|
196
|
-
this.pi.sendUserMessage(
|
|
270
|
+
this.pi.sendUserMessage(task, {
|
|
197
271
|
deliverAs: 'followUp',
|
|
198
272
|
});
|
|
199
273
|
} catch (error) {
|
|
@@ -205,6 +279,73 @@ function launchMainStep(
|
|
|
205
279
|
}
|
|
206
280
|
}
|
|
207
281
|
|
|
282
|
+
function hasCurrentMainStepIdentity(
|
|
283
|
+
context: HarnessActionContext,
|
|
284
|
+
identity: MainStepIdentity,
|
|
285
|
+
): context is HarnessActionContext & { run: WorkflowRun } {
|
|
286
|
+
return (
|
|
287
|
+
context.isSessionActive &&
|
|
288
|
+
context.sessionEpoch === identity.sessionEpoch &&
|
|
289
|
+
context.run?.status === 'running' &&
|
|
290
|
+
context.run.runId === identity.runId &&
|
|
291
|
+
context.run.currentStepId === identity.stepId &&
|
|
292
|
+
context.run.currentStepDigest === identity.stepDigest
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function matchesLatestMainStepAttempt(
|
|
297
|
+
run: WorkflowRun,
|
|
298
|
+
identity: MainStepIdentity,
|
|
299
|
+
): boolean {
|
|
300
|
+
const attempt = run.currentStepAttempts?.at(-1);
|
|
301
|
+
return (
|
|
302
|
+
attempt === undefined ||
|
|
303
|
+
(attempt.kind === 'main' && attempt.requestId === identity.requestId)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function queueMainStepLog(
|
|
308
|
+
this: HarnessActionContext,
|
|
309
|
+
identity: MainStepIdentity,
|
|
310
|
+
lines: ReadonlyArray<string>,
|
|
311
|
+
context: ExtensionContext,
|
|
312
|
+
): Promise<void> {
|
|
313
|
+
return this.mutationQueue
|
|
314
|
+
.run(() => this.recordMainStepLog(identity, lines, context))
|
|
315
|
+
.catch(() => {
|
|
316
|
+
// Status evidence is best-effort and must never pause step execution.
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function recordMainStepLog(
|
|
321
|
+
this: HarnessActionContext,
|
|
322
|
+
identity: MainStepIdentity,
|
|
323
|
+
lines: ReadonlyArray<string>,
|
|
324
|
+
context: ExtensionContext,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
if (
|
|
327
|
+
!hasCurrentMainStepIdentity(this, identity) ||
|
|
328
|
+
!matchesLatestMainStepAttempt(this.run, identity)
|
|
329
|
+
) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const attempt = this.run.currentStepAttempts?.at(-1);
|
|
333
|
+
if (attempt?.kind !== 'main' || attempt.requestId !== identity.requestId) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.latestContext = context;
|
|
337
|
+
const traced = appendMainStepLog(
|
|
338
|
+
this.run,
|
|
339
|
+
identity.requestId,
|
|
340
|
+
lines,
|
|
341
|
+
this.dependencies.now(),
|
|
342
|
+
);
|
|
343
|
+
if (traced === this.run) return;
|
|
344
|
+
this.run = traced;
|
|
345
|
+
this.persist();
|
|
346
|
+
this.updateStatus();
|
|
347
|
+
}
|
|
348
|
+
|
|
208
349
|
function queueMainStepResult(
|
|
209
350
|
this: HarnessActionContext,
|
|
210
351
|
identity: MainStepIdentity,
|
|
@@ -227,18 +368,13 @@ async function finishMainStep(
|
|
|
227
368
|
result: WorkflowStepResult | undefined,
|
|
228
369
|
context: ExtensionContext,
|
|
229
370
|
): Promise<void> {
|
|
230
|
-
this.latestContext = context;
|
|
231
371
|
if (
|
|
232
|
-
!this
|
|
233
|
-
this.
|
|
234
|
-
!this.run ||
|
|
235
|
-
this.run.status !== 'running' ||
|
|
236
|
-
this.run.runId !== identity.runId ||
|
|
237
|
-
this.run.currentStepId !== identity.stepId ||
|
|
238
|
-
this.run.currentStepDigest !== identity.stepDigest
|
|
372
|
+
!hasCurrentMainStepIdentity(this, identity) ||
|
|
373
|
+
!matchesLatestMainStepAttempt(this.run, identity)
|
|
239
374
|
) {
|
|
240
375
|
return;
|
|
241
376
|
}
|
|
377
|
+
this.latestContext = context;
|
|
242
378
|
if (!result) {
|
|
243
379
|
throw new Error(
|
|
244
380
|
'agent settled without calling workflow_complete_step exactly once',
|
|
@@ -251,23 +387,41 @@ async function finishMainStep(
|
|
|
251
387
|
throw new Error('Active workflow configuration is unavailable');
|
|
252
388
|
}
|
|
253
389
|
if (step.gate?.submitOutcome === result.outcome) {
|
|
390
|
+
this.run = recordCurrentStepResult(
|
|
391
|
+
this.run,
|
|
392
|
+
result,
|
|
393
|
+
this.dependencies.now(),
|
|
394
|
+
);
|
|
254
395
|
await this.submitGate(
|
|
255
396
|
workflow,
|
|
256
397
|
this.run,
|
|
257
398
|
result.outcome,
|
|
399
|
+
result.summary,
|
|
258
400
|
result.artifact ?? '',
|
|
259
401
|
);
|
|
260
402
|
return;
|
|
261
403
|
}
|
|
262
404
|
|
|
405
|
+
const effects = resolveStepEffects(this.run, step, result, this.dependencies);
|
|
406
|
+
const tracedRun = recordCurrentStepResult(
|
|
407
|
+
this.run,
|
|
408
|
+
result,
|
|
409
|
+
this.dependencies.now(),
|
|
410
|
+
effects.workspaceCwd,
|
|
411
|
+
);
|
|
263
412
|
this.run = advanceRun(
|
|
264
413
|
workflow,
|
|
265
|
-
|
|
414
|
+
tracedRun,
|
|
266
415
|
result.outcome,
|
|
267
416
|
result.summary,
|
|
268
417
|
this.dependencies.now(),
|
|
418
|
+
effects,
|
|
269
419
|
);
|
|
270
|
-
this.settleAfterTransition(workflow
|
|
420
|
+
this.settleAfterTransition(workflow, {
|
|
421
|
+
stepId: identity.stepId,
|
|
422
|
+
outcome: result.outcome,
|
|
423
|
+
summary: result.summary,
|
|
424
|
+
});
|
|
271
425
|
}
|
|
272
426
|
|
|
273
427
|
/**
|
|
@@ -277,6 +431,8 @@ export function createStepExecutionActions(): StepExecutionActions {
|
|
|
277
431
|
return {
|
|
278
432
|
launchCurrentStep,
|
|
279
433
|
launchMainStep,
|
|
434
|
+
queueMainStepLog,
|
|
435
|
+
recordMainStepLog,
|
|
280
436
|
queueMainStepResult,
|
|
281
437
|
finishMainStep,
|
|
282
438
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { LoadedWorkflow } from '../config/types.ts';
|
|
3
|
+
import type { WorkflowRun } from '../engine/state.ts';
|
|
4
|
+
import { redactStepDetailText } from '../step-log.ts';
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_STEP_SUMMARY_MESSAGE_TYPE = 'workflow-step-summary';
|
|
7
|
+
export const MAX_POSTED_STEP_SUMMARY_CHARS = 4_000;
|
|
8
|
+
export const MAX_POSTED_STEP_FAILURE_CHARS = 1_000;
|
|
9
|
+
|
|
10
|
+
export type SettledStepReport = {
|
|
11
|
+
readonly stepId: string;
|
|
12
|
+
readonly outcome: string;
|
|
13
|
+
readonly summary: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type StepSummaryDetails = {
|
|
17
|
+
readonly workflowId: string;
|
|
18
|
+
readonly runId: string;
|
|
19
|
+
readonly stepId: string;
|
|
20
|
+
readonly stepTitle: string;
|
|
21
|
+
readonly status: 'completed' | 'paused' | 'failed';
|
|
22
|
+
readonly outcome?: string;
|
|
23
|
+
readonly workflowCompleted: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function boundedPostedText(value: string, maxChars: number): string {
|
|
27
|
+
const normalized = redactStepDetailText(value).trim();
|
|
28
|
+
if (normalized.length <= maxChars) return normalized;
|
|
29
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Produces the only failure text suitable for compact chat/toast surfaces. */
|
|
33
|
+
export function conciseStepFailureSummary(reason: string): string {
|
|
34
|
+
return (
|
|
35
|
+
boundedPostedText(reason, MAX_POSTED_STEP_FAILURE_CHARS) ||
|
|
36
|
+
'The step failed without a diagnostic summary.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Produces a compact pause reason while full state remains in the checkpoint. */
|
|
41
|
+
export function conciseStepPauseSummary(reason: string): string {
|
|
42
|
+
return (
|
|
43
|
+
boundedPostedText(reason, MAX_POSTED_STEP_FAILURE_CHARS) ||
|
|
44
|
+
'Manual action is required before this step can continue.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stepTitle(
|
|
49
|
+
workflow: LoadedWorkflow | undefined,
|
|
50
|
+
stepId: string,
|
|
51
|
+
): string {
|
|
52
|
+
const title = workflow?.definition.steps[stepId]?.title ?? stepId;
|
|
53
|
+
return boundedPostedText(title, 200) || stepId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function postStepMessage(
|
|
57
|
+
pi: ExtensionAPI,
|
|
58
|
+
content: string,
|
|
59
|
+
details: StepSummaryDetails,
|
|
60
|
+
): void {
|
|
61
|
+
try {
|
|
62
|
+
pi.sendMessage(
|
|
63
|
+
{
|
|
64
|
+
customType: WORKFLOW_STEP_SUMMARY_MESSAGE_TYPE,
|
|
65
|
+
content,
|
|
66
|
+
display: true,
|
|
67
|
+
details,
|
|
68
|
+
},
|
|
69
|
+
{ triggerTurn: false },
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
// Reporting is best-effort and must never block a durable transition.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Posts only the structured handoff supplied by a successfully settled step.
|
|
78
|
+
*
|
|
79
|
+
* Execution transcripts and artifacts are deliberately unavailable to this
|
|
80
|
+
* function, so they cannot leak into the concise chat summary.
|
|
81
|
+
*/
|
|
82
|
+
export function reportSettledStep(
|
|
83
|
+
pi: ExtensionAPI,
|
|
84
|
+
workflow: LoadedWorkflow,
|
|
85
|
+
run: WorkflowRun,
|
|
86
|
+
report: SettledStepReport,
|
|
87
|
+
): void {
|
|
88
|
+
const workflowCompleted = run.status === 'completed';
|
|
89
|
+
const paused =
|
|
90
|
+
run.status === 'paused' &&
|
|
91
|
+
run.failedStepId === undefined &&
|
|
92
|
+
run.currentStepId === report.stepId;
|
|
93
|
+
const status = paused ? 'paused' : 'completed';
|
|
94
|
+
const title = stepTitle(workflow, report.stepId);
|
|
95
|
+
const summary =
|
|
96
|
+
boundedPostedText(report.summary, MAX_POSTED_STEP_SUMMARY_CHARS) ||
|
|
97
|
+
'No summary was provided.';
|
|
98
|
+
const completion = workflowCompleted
|
|
99
|
+
? `\n\nWorkflow \`${run.workflowId}\` completed.`
|
|
100
|
+
: '';
|
|
101
|
+
postStepMessage(
|
|
102
|
+
pi,
|
|
103
|
+
`### Step ${status}: ${title} (\`${report.stepId}\`)\n\n` +
|
|
104
|
+
`Outcome: \`${report.outcome}\`\n\n${summary}${completion}`,
|
|
105
|
+
{
|
|
106
|
+
workflowId: run.workflowId,
|
|
107
|
+
runId: run.runId,
|
|
108
|
+
stepId: report.stepId,
|
|
109
|
+
stepTitle: title,
|
|
110
|
+
status,
|
|
111
|
+
outcome: report.outcome,
|
|
112
|
+
workflowCompleted,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Posts a concise failure reason for the current step.
|
|
119
|
+
*
|
|
120
|
+
* The full diagnostic remains in the workflow checkpoint/status explorer.
|
|
121
|
+
* Only a redacted prefix is displayed in chat.
|
|
122
|
+
*/
|
|
123
|
+
export function reportFailedStep(
|
|
124
|
+
pi: ExtensionAPI,
|
|
125
|
+
workflow: LoadedWorkflow | undefined,
|
|
126
|
+
run: WorkflowRun,
|
|
127
|
+
reason: string,
|
|
128
|
+
): void {
|
|
129
|
+
const title = stepTitle(workflow, run.currentStepId);
|
|
130
|
+
const summary = conciseStepFailureSummary(reason);
|
|
131
|
+
postStepMessage(
|
|
132
|
+
pi,
|
|
133
|
+
`### Step failed: ${title} (\`${run.currentStepId}\`)\n\n${summary}\n\n` +
|
|
134
|
+
`Workflow \`${run.workflowId}\` is paused.`,
|
|
135
|
+
{
|
|
136
|
+
workflowId: run.workflowId,
|
|
137
|
+
runId: run.runId,
|
|
138
|
+
stepId: run.currentStepId,
|
|
139
|
+
stepTitle: title,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
workflowCompleted: false,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Posts the concise reason for a non-failure workflow pause. */
|
|
147
|
+
export function reportPausedStep(
|
|
148
|
+
pi: ExtensionAPI,
|
|
149
|
+
workflow: LoadedWorkflow | undefined,
|
|
150
|
+
run: WorkflowRun,
|
|
151
|
+
reason: string,
|
|
152
|
+
): void {
|
|
153
|
+
const title = stepTitle(workflow, run.currentStepId);
|
|
154
|
+
const summary = conciseStepPauseSummary(reason);
|
|
155
|
+
postStepMessage(
|
|
156
|
+
pi,
|
|
157
|
+
`### Step paused: ${title} (\`${run.currentStepId}\`)\n\n${summary}\n\n` +
|
|
158
|
+
`Workflow \`${run.workflowId}\` is paused.`,
|
|
159
|
+
{
|
|
160
|
+
workflowId: run.workflowId,
|
|
161
|
+
runId: run.runId,
|
|
162
|
+
stepId: run.currentStepId,
|
|
163
|
+
stepTitle: title,
|
|
164
|
+
status: 'paused',
|
|
165
|
+
workflowCompleted: false,
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
package/src/harness/types.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep, win32 } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type ResolveWorkspaceDirectoryOptions = {
|
|
5
|
+
readonly candidateCwd: string;
|
|
6
|
+
readonly startCwd: string;
|
|
7
|
+
readonly allowedRoots: ReadonlyArray<string>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const isWithin = (root: string, candidate: string): boolean => {
|
|
11
|
+
const pathFromRoot = relative(root, candidate);
|
|
12
|
+
return (
|
|
13
|
+
pathFromRoot === '' ||
|
|
14
|
+
(pathFromRoot !== '..' &&
|
|
15
|
+
!pathFromRoot.startsWith(`..${sep}`) &&
|
|
16
|
+
!isAbsolute(pathFromRoot))
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Canonicalizes and validates one YAML-authorized execution directory.
|
|
22
|
+
*
|
|
23
|
+
* This boundary intentionally knows only filesystem paths. The workflow prompt
|
|
24
|
+
* owns how the directory was selected or created.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveWorkspaceDirectory({
|
|
27
|
+
candidateCwd,
|
|
28
|
+
startCwd,
|
|
29
|
+
allowedRoots,
|
|
30
|
+
}: ResolveWorkspaceDirectoryOptions): string {
|
|
31
|
+
if (
|
|
32
|
+
!candidateCwd ||
|
|
33
|
+
!isAbsolute(candidateCwd) ||
|
|
34
|
+
candidateCwd.includes('\0')
|
|
35
|
+
) {
|
|
36
|
+
throw new Error('workspace cwd must be a non-empty absolute path');
|
|
37
|
+
}
|
|
38
|
+
if (!startCwd || !isAbsolute(startCwd) || startCwd.includes('\0')) {
|
|
39
|
+
throw new Error('workflow start cwd must be a non-empty absolute path');
|
|
40
|
+
}
|
|
41
|
+
if (allowedRoots.length === 0) {
|
|
42
|
+
throw new Error('workspace binding must declare an allowed root');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const canonicalStart = realpathSync(startCwd);
|
|
46
|
+
const canonicalRoots = allowedRoots.map((allowedRoot) => {
|
|
47
|
+
if (
|
|
48
|
+
!allowedRoot ||
|
|
49
|
+
isAbsolute(allowedRoot) ||
|
|
50
|
+
win32.parse(allowedRoot).root !== '' ||
|
|
51
|
+
allowedRoot.includes('\0')
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'workspace allowed roots must be non-empty relative paths',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return realpathSync(resolve(canonicalStart, allowedRoot));
|
|
58
|
+
});
|
|
59
|
+
const canonicalCwd = realpathSync(candidateCwd);
|
|
60
|
+
if (!statSync(canonicalCwd).isDirectory()) {
|
|
61
|
+
throw new Error('workspace cwd must resolve to a directory');
|
|
62
|
+
}
|
|
63
|
+
if (!canonicalRoots.some((root) => isWithin(root, canonicalCwd))) {
|
|
64
|
+
throw new Error('workspace cwd is outside the YAML-authorized roots');
|
|
65
|
+
}
|
|
66
|
+
return canonicalCwd;
|
|
67
|
+
}
|