@wichayutdew/pi-workflows 2.0.1 → 2.2.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 +19 -955
- package/dist/index.js +841 -535
- package/examples/mr-comments.workflow.yaml +1 -1
- package/examples/prompts/mr-comments/plan.md +8 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +1 -1
- package/examples/starter-kit/mr-review.workflow.yaml +4 -2
- package/examples/starter-kit/steps/mr-comment/plan.md +9 -2
- package/examples/starter-kit/steps/mr-review/publish.md +9 -2
- package/examples/starter-kit/steps/mr-review/review.md +9 -2
- package/examples/starter-kit/steps/mr-review/verify.md +16 -5
- package/examples/starter-kit/steps/shared/prepare-workspace.md +42 -4
- package/examples/starter-kit/steps/ticket/plan.md +31 -2
- package/examples/starter-kit/steps/work/plan.md +31 -2
- package/examples/starter-kit/ticket.workflow.yaml +14 -2
- package/examples/starter-kit/work.workflow.yaml +14 -2
- package/package.json +1 -1
- package/schemas/workflow.schema.json +1 -0
- package/src/command-names.ts +1 -0
- package/src/commands.ts +14 -0
- package/src/config/types.ts +1 -1
- package/src/config/validation/prompt.ts +3 -0
- package/src/engine/create-run.ts +4 -0
- package/src/engine/gate-transitions.ts +80 -33
- package/src/engine/run-advance.ts +36 -3
- package/src/engine/run-lifecycle.ts +69 -0
- package/src/engine/run-reconciliation.ts +2 -0
- package/src/engine/run-validation.ts +24 -1
- package/src/engine/state-types.ts +10 -0
- package/src/engine/state.ts +1 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +6 -0
- package/src/harness/core-actions.ts +2 -0
- package/src/harness/dependencies.ts +4 -0
- package/src/harness/lifecycle-actions.ts +14 -1
- package/src/harness/session-persistence.ts +66 -0
- package/src/harness/start-actions.ts +188 -1
- package/src/harness.ts +20 -0
- package/src/prompt/step-sections.ts +14 -0
- package/src/prompt/step-task.ts +3 -0
- package/src/prompt/template.ts +6 -3
- package/src/workflow-doctor.ts +1 -1
- package/src/workflow-status/render-summary.ts +3 -0
- package/src/workflow-status/view.ts +29 -3
package/dist/index.js
CHANGED
|
@@ -133,14 +133,17 @@ var DEFAULT_SETTINGS = {
|
|
|
133
133
|
var PROMPT_VARIABLES = new Set([
|
|
134
134
|
"workflow.input",
|
|
135
135
|
"workflow.id",
|
|
136
|
+
"workflow.iteration",
|
|
136
137
|
"run.id",
|
|
137
138
|
"step.id",
|
|
138
139
|
"step.title",
|
|
139
140
|
"last.summary",
|
|
140
141
|
"reviewed.artifact",
|
|
141
142
|
"reviewed.feedback",
|
|
143
|
+
"gate.artifact",
|
|
142
144
|
"gate.feedback",
|
|
143
|
-
"resume.input"
|
|
145
|
+
"resume.input",
|
|
146
|
+
"restart.workspace"
|
|
144
147
|
]);
|
|
145
148
|
function validatePromptText(text, path) {
|
|
146
149
|
return [...text.matchAll(/\{\{([^{}]+)\}\}/g)].flatMap((match) => {
|
|
@@ -660,6 +663,7 @@ var HARNESS_COMMAND_NAMES = [
|
|
|
660
663
|
"workflow-list",
|
|
661
664
|
"workflow-pause",
|
|
662
665
|
"workflow-reload",
|
|
666
|
+
"workflow-restart",
|
|
663
667
|
"workflow-resume",
|
|
664
668
|
"workflow-start"
|
|
665
669
|
];
|
|
@@ -1342,6 +1346,13 @@ function createHarnessCommands(controller) {
|
|
|
1342
1346
|
}
|
|
1343
1347
|
},
|
|
1344
1348
|
createStartCommand(controller),
|
|
1349
|
+
{
|
|
1350
|
+
name: "workflow-restart",
|
|
1351
|
+
options: {
|
|
1352
|
+
description: "Restart the completed workflow in its worktree: /workflow-restart [input]",
|
|
1353
|
+
handler: async (input, context) => controller.restart(input.trim(), context)
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1345
1356
|
{
|
|
1346
1357
|
name: "workflow-pause",
|
|
1347
1358
|
options: {
|
|
@@ -2946,7 +2957,7 @@ function parseAvailableSkills(systemPrompt) {
|
|
|
2946
2957
|
|
|
2947
2958
|
// src/harness/dependencies.ts
|
|
2948
2959
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2949
|
-
import { constants as constants3, mkdtempSync, writeFileSync } from "node:fs";
|
|
2960
|
+
import { constants as constants3, mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2950
2961
|
import { lstat as lstat3, open as open3, rm } from "node:fs/promises";
|
|
2951
2962
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
2952
2963
|
import { join as join5 } from "node:path";
|
|
@@ -3634,6 +3645,7 @@ function registerMainStepLifecycle({
|
|
|
3634
3645
|
|
|
3635
3646
|
// src/engine/state-types.ts
|
|
3636
3647
|
var RUN_STATE_VERSION = 1;
|
|
3648
|
+
var MAX_GATE_FEEDBACK_CHARS = 50000;
|
|
3637
3649
|
var MAX_RESUME_INPUT_CHARS = 16000;
|
|
3638
3650
|
var MAX_STEP_TRACE_TASK_CHARS = 64000;
|
|
3639
3651
|
var MAX_STEP_TRACE_ATTEMPTS = 16;
|
|
@@ -4208,6 +4220,7 @@ function renderSummaryLines(theme, snapshot, width) {
|
|
|
4208
4220
|
...keyValueLines(theme, "about", workflow.definition.description, width)
|
|
4209
4221
|
] : [],
|
|
4210
4222
|
...keyValueLines(theme, "run", run.runId, width),
|
|
4223
|
+
...run.iteration && run.iteration > 1 ? [...keyValueLines(theme, "iteration", String(run.iteration), width)] : [],
|
|
4211
4224
|
...keyValueLines(theme, "status", statusLabel(run.status), width, statusColor(run.status)),
|
|
4212
4225
|
...keyValueLines(theme, "current", formatStepName(stepTitle(workflow, run.currentStepId), run.currentStepId), width),
|
|
4213
4226
|
...keyValueLines(theme, "visit", String(Math.max(1, run.visits[run.currentStepId] ?? 1)), width),
|
|
@@ -4472,10 +4485,11 @@ import { lstat as lstat2, open as open2, realpath as realpath3 } from "node:fs/p
|
|
|
4472
4485
|
import { isAbsolute as isAbsolute9, relative as relative5, resolve as resolve9, sep as sep4 } from "node:path";
|
|
4473
4486
|
|
|
4474
4487
|
// src/engine/create-run.ts
|
|
4475
|
-
var createRun = (workflow, input, baselineTools, runId, now, cwd) => {
|
|
4488
|
+
var createRun = (workflow, input, baselineTools, runId, now, cwd, iteration = 1) => {
|
|
4476
4489
|
const startStepId = workflow.definition.start;
|
|
4477
4490
|
return {
|
|
4478
4491
|
stateVersion: RUN_STATE_VERSION,
|
|
4492
|
+
iteration,
|
|
4479
4493
|
runId,
|
|
4480
4494
|
workflowId: workflow.definition.id,
|
|
4481
4495
|
workflowDigest: workflow.digest,
|
|
@@ -4493,6 +4507,7 @@ var createRun = (workflow, input, baselineTools, runId, now, cwd) => {
|
|
|
4493
4507
|
...cwd ? { startCwd: cwd, cwd } : {},
|
|
4494
4508
|
stepHandoff: "",
|
|
4495
4509
|
lastSummary: "",
|
|
4510
|
+
gateArtifact: "",
|
|
4496
4511
|
gateFeedback: ""
|
|
4497
4512
|
};
|
|
4498
4513
|
};
|
|
@@ -4815,7 +4830,7 @@ function recordCurrentGateDecision(run, decision, now) {
|
|
|
4815
4830
|
// src/engine/run-validation.ts
|
|
4816
4831
|
var isRecord8 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4817
4832
|
var isAbsoluteCwd = (value) => typeof value === "string" && value.length > 0 && isAbsolute8(value) && !value.includes("\x00");
|
|
4818
|
-
var isGateApproval = (value) => isRecord8(value) && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.artifact === "string" && value.artifact.trim().length > 0 && typeof value.feedback === "string" && typeof value.stepStructuralDigest === "string" && value.stepStructuralDigest.length > 0;
|
|
4833
|
+
var isGateApproval = (value) => isRecord8(value) && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.artifact === "string" && value.artifact.trim().length > 0 && typeof value.feedback === "string" && value.feedback.length <= MAX_GATE_FEEDBACK_CHARS && typeof value.stepStructuralDigest === "string" && value.stepStructuralDigest.length > 0;
|
|
4819
4834
|
var isStepAttemptResult = (value) => isRecord8(value) && typeof value.outcome === "string" && typeof value.summary === "string" && value.summary.length <= MAX_STEP_TRACE_SUMMARY_CHARS && (value.summaryTruncated === undefined || value.summaryTruncated === true) && (value.artifact === undefined || typeof value.artifact === "string") && (typeof value.artifact !== "string" || value.artifact.length <= MAX_STEP_TRACE_ARTIFACT_CHARS) && (value.artifactTruncated === undefined || value.artifactTruncated === true) && !(value.artifactTruncated === true && value.artifact === undefined) && (value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd));
|
|
4820
4835
|
var isStepGateDecision = (value) => isRecord8(value) && (value.provider === "prompt" || value.provider === "plannotator") && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.approved === "boolean" && typeof value.feedback === "string" && value.feedback.length <= MAX_STEP_TRACE_SUMMARY_CHARS && (value.feedbackTruncated === undefined || value.feedbackTruncated === true) && typeof value.resolvedAt === "number" && (value.reviewId === undefined || typeof value.reviewId === "string");
|
|
4821
4836
|
var isSafeTraceIdentityField = (value) => typeof value === "string" && value.length > 0 && !value.includes("\x00") && !value.includes("/") && !value.includes("\\") && value !== "." && value !== "..";
|
|
@@ -4862,11 +4877,12 @@ var isStepExecutionAttempts = (value) => Array.isArray(value) && value.length <=
|
|
|
4862
4877
|
return value.slice(0, index).every((earlier) => earlier.ordinal === undefined || earlier.ordinal < ordinal);
|
|
4863
4878
|
});
|
|
4864
4879
|
var isStepHistoryEntry = (value) => isRecord8(value) && typeof value.stepId === "string" && typeof value.stepDigest === "string" && typeof value.outcome === "string" && typeof value.summary === "string" && (value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd)) && (value.artifact === undefined || typeof value.artifact === "string") && (value.approval === undefined || isGateApproval(value.approval) && value.artifact === value.approval.artifact) && (value.attempts === undefined || isStepExecutionAttempts(value.attempts)) && (value.omittedAttempts === undefined || Number.isSafeInteger(value.omittedAttempts) && value.omittedAttempts > 0) && typeof value.completedAt === "number";
|
|
4865
|
-
var isGateResolution = (value) => isRecord8(value) && typeof value.approved === "boolean" && typeof value.feedback === "string" && typeof value.resolvedAt === "number";
|
|
4880
|
+
var isGateResolution = (value) => isRecord8(value) && typeof value.approved === "boolean" && typeof value.feedback === "string" && value.feedback.length <= MAX_GATE_FEEDBACK_CHARS && typeof value.resolvedAt === "number";
|
|
4866
4881
|
var isPendingGate = (value) => isRecord8(value) && (value.provider === "prompt" || value.provider === "plannotator") && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.stepId === "string" && typeof value.artifact === "string" && (value.summary === undefined || typeof value.summary === "string") && typeof value.submittedOutcome === "string" && typeof value.requestedAt === "number" && (value.reviewId === undefined || typeof value.reviewId === "string") && (value.resolution === undefined || isGateResolution(value.resolution));
|
|
4867
4882
|
var isVisitCounts = (value) => isRecord8(value) && Object.values(value).every((count) => typeof count === "number" && Number.isInteger(count) && count >= 0);
|
|
4868
4883
|
var isOptionalString = (value) => value === undefined || typeof value === "string";
|
|
4869
4884
|
var isOptionalResumeInput = (value) => value === undefined || typeof value === "string" && value.length <= MAX_RESUME_INPUT_CHARS;
|
|
4885
|
+
var isOptionalIteration = (value) => value === undefined || Number.isSafeInteger(value) && value >= 1;
|
|
4870
4886
|
var isWorkflowRunStatus = (value) => value === "running" || value === "paused" || value === "awaiting-gate" || value === "completed" || value === "aborted";
|
|
4871
4887
|
var hasValidPauseState = (run) => run.status === "paused" ? run.pausedFrom === "running" || run.pausedFrom === "awaiting-gate" : run.pausedFrom === undefined;
|
|
4872
4888
|
var hasValidFailureState = (run) => run.failedStepId === undefined || run.status === "paused" && run.failedStepId === run.currentStepId;
|
|
@@ -4890,15 +4906,18 @@ var hasValidWorkspaceState = (run, history) => {
|
|
|
4890
4906
|
var isWorkflowRun = (value) => {
|
|
4891
4907
|
if (!isRecord8(value))
|
|
4892
4908
|
return false;
|
|
4893
|
-
const hasValidRequiredFields = value.stateVersion === RUN_STATE_VERSION && typeof value.runId === "string" && typeof value.workflowId === "string" && typeof value.workflowDigest === "string" && typeof value.input === "string" && isWorkflowRunStatus(value.status) && typeof value.currentStepId === "string" && typeof value.currentStepDigest === "string" && Array.isArray(value.baselineTools) && value.baselineTools.every((tool) => typeof tool === "string") && Array.isArray(value.history) && value.history.every(isStepHistoryEntry) && (value.currentStepAttempts === undefined || isStepExecutionAttempts(value.currentStepAttempts)) && (value.currentStepOmittedAttempts === undefined || Number.isSafeInteger(value.currentStepOmittedAttempts) && value.currentStepOmittedAttempts > 0) && isVisitCounts(value.visits) && typeof value.startedAt === "number" && typeof value.updatedAt === "number" && typeof value.lastSummary === "string" && typeof value.gateFeedback === "string";
|
|
4909
|
+
const hasValidRequiredFields = value.stateVersion === RUN_STATE_VERSION && typeof value.runId === "string" && typeof value.workflowId === "string" && typeof value.workflowDigest === "string" && typeof value.input === "string" && isWorkflowRunStatus(value.status) && typeof value.currentStepId === "string" && typeof value.currentStepDigest === "string" && Array.isArray(value.baselineTools) && value.baselineTools.every((tool) => typeof tool === "string") && Array.isArray(value.history) && value.history.every(isStepHistoryEntry) && (value.currentStepAttempts === undefined || isStepExecutionAttempts(value.currentStepAttempts)) && (value.currentStepOmittedAttempts === undefined || Number.isSafeInteger(value.currentStepOmittedAttempts) && value.currentStepOmittedAttempts > 0) && isVisitCounts(value.visits) && typeof value.startedAt === "number" && typeof value.updatedAt === "number" && typeof value.lastSummary === "string" && typeof value.gateFeedback === "string" && value.gateFeedback.length <= MAX_GATE_FEEDBACK_CHARS;
|
|
4894
4910
|
if (!hasValidRequiredFields)
|
|
4895
4911
|
return false;
|
|
4896
|
-
const hasValidOptionalFields = isOptionalString(value.reviewedArtifact) && isOptionalString(value.reviewedFeedback) && isOptionalString(value.stepHandoff) && isOptionalResumeInput(value.resumeInput) && isOptionalString(value.pauseReason) && isOptionalString(value.failedStepId) && (value.pausedFrom === undefined || value.pausedFrom === "running" || value.pausedFrom === "awaiting-gate");
|
|
4912
|
+
const hasValidOptionalFields = isOptionalIteration(value.iteration) && isOptionalString(value.reviewedArtifact) && isOptionalString(value.reviewedFeedback) && (typeof value.reviewedFeedback !== "string" || value.reviewedFeedback.length <= MAX_GATE_FEEDBACK_CHARS) && isOptionalString(value.stepHandoff) && isOptionalString(value.gateArtifact) && (value.restartWorkspaceCwd === undefined || isAbsoluteCwd(value.restartWorkspaceCwd)) && isOptionalResumeInput(value.resumeInput) && isOptionalString(value.pauseReason) && isOptionalString(value.failedStepId) && (value.pausedFrom === undefined || value.pausedFrom === "running" || value.pausedFrom === "awaiting-gate");
|
|
4897
4913
|
if (!hasValidOptionalFields)
|
|
4898
4914
|
return false;
|
|
4899
4915
|
const pendingGate = value.pendingGate;
|
|
4900
4916
|
if (pendingGate !== undefined && !isPendingGate(pendingGate))
|
|
4901
4917
|
return false;
|
|
4918
|
+
if (value.restartWorkspaceCwd !== undefined && value.history.some((entry) => entry.workspaceCwd !== undefined)) {
|
|
4919
|
+
return false;
|
|
4920
|
+
}
|
|
4902
4921
|
return workflowTraceChars(value) <= MAX_WORKFLOW_TRACE_CHARS && hasValidWorkspaceState(value, value.history) && hasValidPauseState(value) && hasValidFailureState(value) && hasValidGateState(value, pendingGate);
|
|
4903
4922
|
};
|
|
4904
4923
|
// src/workflow-status/transcript-reader.ts
|
|
@@ -5081,6 +5100,7 @@ class WorkflowStatusView {
|
|
|
5081
5100
|
statusShortcut;
|
|
5082
5101
|
timer;
|
|
5083
5102
|
state = initialViewportState();
|
|
5103
|
+
pendingDetailTopKey = false;
|
|
5084
5104
|
statusShortcutLabel;
|
|
5085
5105
|
dependencies;
|
|
5086
5106
|
transcriptCache = new Map;
|
|
@@ -5106,7 +5126,9 @@ class WorkflowStatusView {
|
|
|
5106
5126
|
}
|
|
5107
5127
|
invalidate() {}
|
|
5108
5128
|
handleInput(data) {
|
|
5109
|
-
|
|
5129
|
+
const isDetailHalfPageDown = this.state.mode === "detail" && matchesKey(data, Key.ctrl("d"));
|
|
5130
|
+
const isDetailHalfPageUp = this.state.mode === "detail" && matchesKey(data, Key.ctrl("u"));
|
|
5131
|
+
if (data === "q" || data === "Q" || matchesKey(data, "ctrl+c") || this.state.mode !== "detail" && matchesKey(data, "ctrl+d") || matchesKey(data, this.statusShortcut)) {
|
|
5110
5132
|
this.close();
|
|
5111
5133
|
return;
|
|
5112
5134
|
}
|
|
@@ -5119,13 +5141,31 @@ class WorkflowStatusView {
|
|
|
5119
5141
|
return;
|
|
5120
5142
|
}
|
|
5121
5143
|
const pageSize = Math.max(1, this.state.viewportRows - 2);
|
|
5144
|
+
const contentHeight = Math.max(1, this.state.viewportRows - 1);
|
|
5145
|
+
const halfPageSize = Math.max(1, Math.floor(contentHeight / 2));
|
|
5122
5146
|
if (this.state.mode === "detail") {
|
|
5123
|
-
if (
|
|
5147
|
+
if (data === "gg" || data === "g" && this.pendingDetailTopKey) {
|
|
5148
|
+
this.pendingDetailTopKey = false;
|
|
5149
|
+
this.setScrollOffset(0);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5152
|
+
if (data === "g") {
|
|
5153
|
+
this.pendingDetailTopKey = true;
|
|
5154
|
+
return;
|
|
5155
|
+
}
|
|
5156
|
+
this.pendingDetailTopKey = false;
|
|
5157
|
+
if (data === "G") {
|
|
5158
|
+
this.setScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
5159
|
+
} else if (matchesKey(data, Key.left) || data === "h") {
|
|
5124
5160
|
this.showBoard();
|
|
5125
5161
|
} else if (matchesKey(data, Key.down) || data === "j") {
|
|
5126
5162
|
this.setScrollOffset(this.state.scrollOffset + 1);
|
|
5127
5163
|
} else if (matchesKey(data, Key.up) || data === "k") {
|
|
5128
5164
|
this.setScrollOffset(this.state.scrollOffset - 1);
|
|
5165
|
+
} else if (isDetailHalfPageDown) {
|
|
5166
|
+
this.setScrollOffset(this.state.scrollOffset + halfPageSize);
|
|
5167
|
+
} else if (isDetailHalfPageUp) {
|
|
5168
|
+
this.setScrollOffset(this.state.scrollOffset - halfPageSize);
|
|
5129
5169
|
} else if (matchesKey(data, Key.pageDown)) {
|
|
5130
5170
|
this.setScrollOffset(this.state.scrollOffset + pageSize);
|
|
5131
5171
|
} else if (matchesKey(data, Key.pageUp)) {
|
|
@@ -5137,6 +5177,7 @@ class WorkflowStatusView {
|
|
|
5137
5177
|
}
|
|
5138
5178
|
return;
|
|
5139
5179
|
}
|
|
5180
|
+
this.pendingDetailTopKey = false;
|
|
5140
5181
|
if (matchesKey(data, Key.down) || data === "j") {
|
|
5141
5182
|
this.moveSelection(1);
|
|
5142
5183
|
} else if (matchesKey(data, Key.up) || data === "k") {
|
|
@@ -5168,7 +5209,7 @@ class WorkflowStatusView {
|
|
|
5168
5209
|
this.ensureSelectedTranscripts(snapshot);
|
|
5169
5210
|
}
|
|
5170
5211
|
const rendered = lines.map((line) => padAnsi(truncateToWidth4(line, contentWidth, "…"), viewportWidth));
|
|
5171
|
-
const page = paginateBoard(this.state, rendered, viewportWidth, this.tui.terminal?.rows, this.statusShortcutLabel, this.theme, this.state.mode === "detail" ? "
|
|
5212
|
+
const page = paginateBoard(this.state, rendered, viewportWidth, this.tui.terminal?.rows, this.statusShortcutLabel, this.theme, this.state.mode === "detail" ? "↑↓/jk · Ctrl+D/U half-page · gg/G top/bottom · PgUp/PgDn · ←/h/Esc" : "↑/↓ or j/k select · Enter/→/l inspect · PgUp/PgDn");
|
|
5172
5213
|
this.state = page.state;
|
|
5173
5214
|
return page.lines;
|
|
5174
5215
|
}
|
|
@@ -5210,6 +5251,7 @@ class WorkflowStatusView {
|
|
|
5210
5251
|
this.normalizeSelection(snapshot);
|
|
5211
5252
|
if (!selectedStepDetail(snapshot, this.state.selectedIndex))
|
|
5212
5253
|
return;
|
|
5254
|
+
this.pendingDetailTopKey = false;
|
|
5213
5255
|
this.state = { ...this.state, mode: "detail", scrollOffset: 0 };
|
|
5214
5256
|
this.ensureSelectedTranscripts(snapshot);
|
|
5215
5257
|
this.tui.requestRender(true);
|
|
@@ -5217,6 +5259,7 @@ class WorkflowStatusView {
|
|
|
5217
5259
|
showBoard() {
|
|
5218
5260
|
if (this.state.mode === "board")
|
|
5219
5261
|
return;
|
|
5262
|
+
this.pendingDetailTopKey = false;
|
|
5220
5263
|
this.state = { ...this.state, mode: "board", scrollOffset: 0 };
|
|
5221
5264
|
this.tui.requestRender(true);
|
|
5222
5265
|
}
|
|
@@ -5309,6 +5352,42 @@ function resolveWorkspaceDirectory({
|
|
|
5309
5352
|
return canonicalCwd;
|
|
5310
5353
|
}
|
|
5311
5354
|
|
|
5355
|
+
// src/harness/session-persistence.ts
|
|
5356
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
5357
|
+
var isAlreadyPersisted = (error) => error instanceof Error && ("code" in error) && error.code === "EEXIST";
|
|
5358
|
+
function getAdoptableSession(session) {
|
|
5359
|
+
const adoptable = session;
|
|
5360
|
+
if (typeof adoptable.setSessionFile !== "function") {
|
|
5361
|
+
throw new Error("This Pi runtime cannot adopt a materialized workflow session file");
|
|
5362
|
+
}
|
|
5363
|
+
return adoptable;
|
|
5364
|
+
}
|
|
5365
|
+
function flushUnwrittenSession(session) {
|
|
5366
|
+
const sessionFile = session.getSessionFile();
|
|
5367
|
+
const header = session.getHeader();
|
|
5368
|
+
if (!sessionFile || !header)
|
|
5369
|
+
return false;
|
|
5370
|
+
if (existsSync(sessionFile))
|
|
5371
|
+
return false;
|
|
5372
|
+
const adoptable = getAdoptableSession(session);
|
|
5373
|
+
const serialized = [header, ...session.getEntries()].map((entry) => JSON.stringify(entry)).join(`
|
|
5374
|
+
`);
|
|
5375
|
+
try {
|
|
5376
|
+
writeFileSync(sessionFile, `${serialized}
|
|
5377
|
+
`, {
|
|
5378
|
+
encoding: "utf8",
|
|
5379
|
+
flag: "wx",
|
|
5380
|
+
mode: 384
|
|
5381
|
+
});
|
|
5382
|
+
adoptable.setSessionFile(sessionFile);
|
|
5383
|
+
return true;
|
|
5384
|
+
} catch (error) {
|
|
5385
|
+
if (isAlreadyPersisted(error))
|
|
5386
|
+
return false;
|
|
5387
|
+
throw error;
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5312
5391
|
// src/harness/dependencies.ts
|
|
5313
5392
|
var MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
5314
5393
|
function createDelegationWorkspace() {
|
|
@@ -5316,7 +5395,7 @@ function createDelegationWorkspace() {
|
|
|
5316
5395
|
const capabilityPath = join5(resultDirectory, "capability");
|
|
5317
5396
|
const capabilityToken = randomBytes(32).toString("hex");
|
|
5318
5397
|
const resultPath = join5(resultDirectory, "result.json");
|
|
5319
|
-
|
|
5398
|
+
writeFileSync2(capabilityPath, capabilityToken, {
|
|
5320
5399
|
encoding: "utf8",
|
|
5321
5400
|
flag: "wx",
|
|
5322
5401
|
mode: 384
|
|
@@ -5373,6 +5452,7 @@ var DEFAULT_DEPENDENCIES3 = {
|
|
|
5373
5452
|
createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
|
|
5374
5453
|
createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
|
|
5375
5454
|
createMutationQueue: createSerialTaskQueue,
|
|
5455
|
+
flushUnwrittenSession,
|
|
5376
5456
|
scheduleInterval: (operation, intervalMs) => setInterval(operation, intervalMs),
|
|
5377
5457
|
cancelInterval: (timer) => {
|
|
5378
5458
|
clearInterval(timer);
|
|
@@ -5489,397 +5569,134 @@ function createStatusActions() {
|
|
|
5489
5569
|
};
|
|
5490
5570
|
}
|
|
5491
5571
|
|
|
5492
|
-
// src/
|
|
5493
|
-
var
|
|
5494
|
-
var
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
var
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5572
|
+
// src/engine/transition-helpers.ts
|
|
5573
|
+
var currentStep = (workflow, run) => workflow.definition.steps[run.currentStepId];
|
|
5574
|
+
var withRunUpdate = (run, changes, now) => ({ ...run, ...changes, updatedAt: now });
|
|
5575
|
+
|
|
5576
|
+
// src/engine/run-advance.ts
|
|
5577
|
+
var completedStep = (run, outcome, summary, now, effects) => ({
|
|
5578
|
+
stepId: run.currentStepId,
|
|
5579
|
+
stepDigest: run.currentStepDigest,
|
|
5580
|
+
outcome,
|
|
5581
|
+
summary,
|
|
5582
|
+
...effects.workspaceCwd ? { workspaceCwd: effects.workspaceCwd } : {},
|
|
5583
|
+
...run.currentStepAttempts?.length ? { attempts: run.currentStepAttempts } : {},
|
|
5584
|
+
...run.currentStepOmittedAttempts ? { omittedAttempts: run.currentStepOmittedAttempts } : {},
|
|
5585
|
+
completedAt: now
|
|
5586
|
+
});
|
|
5587
|
+
var advanceRun = (workflow, run, outcome, summary, now, effects = {}, options = {}) => {
|
|
5588
|
+
if (run.status !== "running") {
|
|
5589
|
+
throw new Error(`workflow is ${run.status}; only a running workflow can advance`);
|
|
5509
5590
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
const canComplete = new Set(Object.entries(definition.steps).filter(([, step]) => Object.values(step.transitions).includes("$done")).map(([stepId]) => stepId));
|
|
5514
|
-
const reverse = new Map;
|
|
5515
|
-
for (const [source, targets] of Object.entries(adjacency)) {
|
|
5516
|
-
for (const target of targets) {
|
|
5517
|
-
reverse.set(target, [...reverse.get(target) ?? [], source]);
|
|
5518
|
-
}
|
|
5591
|
+
const step = currentStep(workflow, run);
|
|
5592
|
+
if (!step) {
|
|
5593
|
+
throw new Error(`current step "${run.currentStepId}" no longer exists`);
|
|
5519
5594
|
}
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
const stepId = pending.pop();
|
|
5523
|
-
if (!stepId)
|
|
5524
|
-
continue;
|
|
5525
|
-
for (const predecessor of (reverse.get(stepId) ?? []).sort(lexical)) {
|
|
5526
|
-
if (canComplete.has(predecessor))
|
|
5527
|
-
continue;
|
|
5528
|
-
canComplete.add(predecessor);
|
|
5529
|
-
pending.push(predecessor);
|
|
5530
|
-
}
|
|
5595
|
+
if (step.gate?.submitOutcome === outcome) {
|
|
5596
|
+
throw new Error(`outcome "${outcome}" must be submitted through the configured gate`);
|
|
5531
5597
|
}
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
const
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5598
|
+
const target = step.transitions[outcome];
|
|
5599
|
+
if (!target) {
|
|
5600
|
+
throw new Error(`outcome "${outcome}" is not valid for step "${run.currentStepId}"`);
|
|
5601
|
+
}
|
|
5602
|
+
const shouldBindWorkspace = step.workspace?.bindOn.includes(outcome) ?? false;
|
|
5603
|
+
if (shouldBindWorkspace !== Boolean(effects.workspaceCwd)) {
|
|
5604
|
+
throw new Error(shouldBindWorkspace ? `outcome "${outcome}" requires a validated workspace binding` : `outcome "${outcome}" cannot bind a workspace`);
|
|
5605
|
+
}
|
|
5606
|
+
if (shouldBindWorkspace && run.restartWorkspaceCwd !== undefined && effects.workspaceCwd !== run.restartWorkspaceCwd) {
|
|
5607
|
+
throw new Error(`restarted workflow must rebind workspace "${run.restartWorkspaceCwd}"`);
|
|
5608
|
+
}
|
|
5609
|
+
if (target === "$pause") {
|
|
5610
|
+
return withRunUpdate(run, {
|
|
5611
|
+
status: "paused",
|
|
5612
|
+
pausedFrom: "running",
|
|
5613
|
+
pauseReason: summary || `Step "${run.currentStepId}" requested a pause`,
|
|
5614
|
+
lastSummary: summary,
|
|
5615
|
+
resumeInput: undefined
|
|
5616
|
+
}, now);
|
|
5617
|
+
}
|
|
5618
|
+
const completed = completedStep(run, outcome, summary, now, effects);
|
|
5619
|
+
const cwd = effects.workspaceCwd ?? run.cwd;
|
|
5620
|
+
if (target === "$done") {
|
|
5621
|
+
if (run.restartWorkspaceCwd !== undefined) {
|
|
5622
|
+
throw new Error(`restarted workflow completed before rebinding workspace "${run.restartWorkspaceCwd}"`);
|
|
5554
5623
|
}
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
visit(stepId);
|
|
5624
|
+
return withRunUpdate(run, {
|
|
5625
|
+
status: "completed",
|
|
5626
|
+
history: [...run.history, completed],
|
|
5627
|
+
currentStepAttempts: undefined,
|
|
5628
|
+
currentStepOmittedAttempts: undefined,
|
|
5629
|
+
...cwd ? { cwd } : {},
|
|
5630
|
+
...effects.workspaceCwd ? { restartWorkspaceCwd: undefined } : {},
|
|
5631
|
+
stepHandoff: summary,
|
|
5632
|
+
lastSummary: summary,
|
|
5633
|
+
gateArtifact: "",
|
|
5634
|
+
gateFeedback: "",
|
|
5635
|
+
pausedFrom: undefined,
|
|
5636
|
+
pendingGate: undefined,
|
|
5637
|
+
resumeInput: undefined
|
|
5638
|
+
}, now);
|
|
5571
5639
|
}
|
|
5572
|
-
|
|
5640
|
+
if (!workflow.definition.steps[target]) {
|
|
5641
|
+
throw new Error(`transition target "${target}" does not exist`);
|
|
5642
|
+
}
|
|
5643
|
+
const preservesGateRevisionContext = target === run.currentStepId && (options.sameStepHumanGateRevision || Boolean(run.gateArtifact));
|
|
5644
|
+
const nextVisitCount = (run.visits[target] ?? 0) + 1;
|
|
5645
|
+
const isOverVisitLimit = !options.sameStepHumanGateRevision && nextVisitCount > workflow.definition.maxStepVisits;
|
|
5646
|
+
const visitLimitChanges = isOverVisitLimit ? {
|
|
5647
|
+
status: "paused",
|
|
5648
|
+
pausedFrom: "running",
|
|
5649
|
+
pauseReason: `Step "${target}" exceeded maxStepVisits (${workflow.definition.maxStepVisits})`,
|
|
5650
|
+
failedStepId: target
|
|
5651
|
+
} : {
|
|
5652
|
+
status: "running",
|
|
5653
|
+
pausedFrom: undefined,
|
|
5654
|
+
pauseReason: undefined,
|
|
5655
|
+
failedStepId: undefined
|
|
5656
|
+
};
|
|
5657
|
+
return withRunUpdate(run, {
|
|
5658
|
+
...visitLimitChanges,
|
|
5659
|
+
currentStepId: target,
|
|
5660
|
+
currentStepDigest: workflow.stepDigests[target] ?? "",
|
|
5661
|
+
visits: { ...run.visits, [target]: nextVisitCount },
|
|
5662
|
+
history: [...run.history, completed],
|
|
5663
|
+
currentStepAttempts: undefined,
|
|
5664
|
+
currentStepOmittedAttempts: undefined,
|
|
5665
|
+
...cwd ? { cwd } : {},
|
|
5666
|
+
...effects.workspaceCwd ? { restartWorkspaceCwd: undefined } : {},
|
|
5667
|
+
stepHandoff: preservesGateRevisionContext ? run.stepHandoff : summary,
|
|
5668
|
+
lastSummary: summary,
|
|
5669
|
+
gateArtifact: preservesGateRevisionContext ? run.gateArtifact : "",
|
|
5670
|
+
gateFeedback: preservesGateRevisionContext ? run.gateFeedback : "",
|
|
5671
|
+
resumeInput: undefined
|
|
5672
|
+
}, now);
|
|
5573
5673
|
};
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5674
|
+
|
|
5675
|
+
// src/engine/gate-transitions.ts
|
|
5676
|
+
var GATE_FEEDBACK_TRUNCATION_SUFFIX = `
|
|
5677
|
+
… [gate feedback truncated by Pi Workflows]`;
|
|
5678
|
+
var MAX_GATE_REJECTION_SUMMARY_CHARS = 500;
|
|
5679
|
+
var boundedGateFeedback = (feedback) => feedback.length <= MAX_GATE_FEEDBACK_CHARS ? feedback : `${feedback.slice(0, MAX_GATE_FEEDBACK_CHARS - GATE_FEEDBACK_TRUNCATION_SUFFIX.length)}${GATE_FEEDBACK_TRUNCATION_SUFFIX}`;
|
|
5680
|
+
var gateRejectionSummary = (feedback) => {
|
|
5681
|
+
const compact = feedback.trim().replace(/\s+/g, " ");
|
|
5682
|
+
if (!compact)
|
|
5683
|
+
return "Gate rejected";
|
|
5684
|
+
const summary = `Gate rejected: ${compact}`;
|
|
5685
|
+
return summary.length <= MAX_GATE_REJECTION_SUMMARY_CHARS ? summary : `${summary.slice(0, MAX_GATE_REJECTION_SUMMARY_CHARS - 1)}…`;
|
|
5579
5686
|
};
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
const canComplete = stepsThatCanComplete(definition, adjacency);
|
|
5584
|
-
const issues = [];
|
|
5585
|
-
const stranded = [...reachable].filter((stepId) => !canComplete.has(stepId)).sort(lexical);
|
|
5586
|
-
if (!canComplete.has(definition.start)) {
|
|
5587
|
-
issues.push({
|
|
5588
|
-
level: "error",
|
|
5589
|
-
code: "no-completion-path",
|
|
5590
|
-
steps: [definition.start],
|
|
5591
|
-
message: `start step ${definition.start} cannot reach $done`
|
|
5592
|
-
});
|
|
5687
|
+
var beginGate = (workflow, run, outcome, artifact, requestId, now, summary) => {
|
|
5688
|
+
if (run.status !== "running") {
|
|
5689
|
+
throw new Error(`workflow is ${run.status}; gate submission requires a running workflow`);
|
|
5593
5690
|
}
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
code: "reachable-step-cannot-reach-done",
|
|
5598
|
-
steps: stranded,
|
|
5599
|
-
message: `reachable step${stranded.length === 1 ? "" : "s"} ${stranded.join(", ")} cannot reach $done`
|
|
5600
|
-
});
|
|
5691
|
+
const step = currentStep(workflow, run);
|
|
5692
|
+
if (!step?.gate) {
|
|
5693
|
+
throw new Error(`step "${run.currentStepId}" has no gate`);
|
|
5601
5694
|
}
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
issues.push({
|
|
5605
|
-
level: "warning",
|
|
5606
|
-
code: "unreachable-steps",
|
|
5607
|
-
steps: unreachable,
|
|
5608
|
-
message: `unreachable step${unreachable.length === 1 ? "" : "s"}: ${unreachable.join(", ")}`
|
|
5609
|
-
});
|
|
5695
|
+
if (outcome !== step.gate.submitOutcome) {
|
|
5696
|
+
throw new Error(`gate expects outcome "${step.gate.submitOutcome}"`);
|
|
5610
5697
|
}
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
continue;
|
|
5614
|
-
const componentIsReachable = component.some((stepId) => reachable.has(stepId));
|
|
5615
|
-
const componentCanReachDone = component.some((stepId) => canComplete.has(stepId));
|
|
5616
|
-
issues.push({
|
|
5617
|
-
level: "warning",
|
|
5618
|
-
code: "cycle",
|
|
5619
|
-
steps: component,
|
|
5620
|
-
reachable: componentIsReachable,
|
|
5621
|
-
canReachDone: componentCanReachDone,
|
|
5622
|
-
message: `${componentIsReachable ? "reachable" : "unreachable"} cyclic component: ${component.join(", ")}; ${componentCanReachDone ? "an exit can reach $done" : "no member can reach $done"}; maxStepVisits=${definition.maxStepVisits} bounds uninterrupted graph cycling`
|
|
5623
|
-
});
|
|
5624
|
-
}
|
|
5625
|
-
return {
|
|
5626
|
-
workflowId: definition.id,
|
|
5627
|
-
maxStepVisits: definition.maxStepVisits,
|
|
5628
|
-
reachableSteps: [...reachable].sort(lexical),
|
|
5629
|
-
issues
|
|
5630
|
-
};
|
|
5631
|
-
}
|
|
5632
|
-
var escapeMarkdown = (value) => value.replaceAll("\\", "\\\\").replaceAll("|", "\\|");
|
|
5633
|
-
function formatWorkflowDoctor(reports) {
|
|
5634
|
-
const lines = ["# Workflow doctor", ""];
|
|
5635
|
-
for (const report of reports) {
|
|
5636
|
-
const errors = report.issues.filter((issue) => issue.level === "error");
|
|
5637
|
-
const warnings = report.issues.filter((issue) => issue.level === "warning");
|
|
5638
|
-
lines.push(`## ${report.workflowId}`, "", `Result: ${errors.length > 0 ? "ERROR" : warnings.length > 0 ? "WARNING" : "PASS"}`, "", `Runtime loop guard: each step executes at most ${report.maxStepVisits} time${report.maxStepVisits === 1 ? "" : "s"} before the next attempted entry pauses an uninterrupted run. This bounds graph cycling; it does not guarantee $done or bound time spent inside a step or gate.`, "");
|
|
5639
|
-
if (report.issues.length === 0) {
|
|
5640
|
-
lines.push("- No liveness issues found.", "");
|
|
5641
|
-
continue;
|
|
5642
|
-
}
|
|
5643
|
-
lines.push(...report.issues.map((issue) => `- ${issue.level.toUpperCase()} \`${issue.code}\`: ${escapeMarkdown(issue.message)}`), "");
|
|
5644
|
-
}
|
|
5645
|
-
return lines.join(`
|
|
5646
|
-
`).trimEnd();
|
|
5647
|
-
}
|
|
5648
|
-
|
|
5649
|
-
// src/workflow-list.ts
|
|
5650
|
-
function escapeMarkdownTableCell(value) {
|
|
5651
|
-
return value.replaceAll("\\", "\\\\").replaceAll("|", "\\|").replace(/\r\n|\r|\n/g, " ");
|
|
5652
|
-
}
|
|
5653
|
-
function formatWorkflowList(workflows) {
|
|
5654
|
-
return [
|
|
5655
|
-
"| Workflow | Command | Description |",
|
|
5656
|
-
"| --- | --- | --- |",
|
|
5657
|
-
...workflows.map((workflow) => `| \`${workflow.id}\` | \`/${workflow.command}\` | ${escapeMarkdownTableCell(workflow.description)} |`)
|
|
5658
|
-
].join(`
|
|
5659
|
-
`);
|
|
5660
|
-
}
|
|
5661
|
-
|
|
5662
|
-
// src/harness/start-actions.ts
|
|
5663
|
-
function isCurrentSession(session, sessionEpoch) {
|
|
5664
|
-
return session.isSessionActive && session.sessionEpoch === sessionEpoch;
|
|
5665
|
-
}
|
|
5666
|
-
async function listWorkflows(context) {
|
|
5667
|
-
const workflows = [...this.catalog.workflows.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
|
|
5668
|
-
if (workflows.length === 0) {
|
|
5669
|
-
context.ui.notify(`No workflows loaded from ${this.catalog.userDirectory}`, this.catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
5670
|
-
return;
|
|
5671
|
-
}
|
|
5672
|
-
this.pi.sendMessage({
|
|
5673
|
-
customType: "workflow-list",
|
|
5674
|
-
content: formatWorkflowList(workflows.map((workflow) => workflow.definition)),
|
|
5675
|
-
display: true
|
|
5676
|
-
});
|
|
5677
|
-
}
|
|
5678
|
-
async function doctorWorkflows(workflowId, context) {
|
|
5679
|
-
const catalog = await this.dependencies.loadCatalog({
|
|
5680
|
-
cwd: context.cwd,
|
|
5681
|
-
projectTrusted: context.isProjectTrusted()
|
|
5682
|
-
});
|
|
5683
|
-
if (catalog.diagnostics.some((diagnostic) => diagnostic.level === "error")) {
|
|
5684
|
-
context.ui.notify(`Workflow configuration errors:
|
|
5685
|
-
${formatCatalogDiagnostics(catalog)}`, "warning");
|
|
5686
|
-
}
|
|
5687
|
-
const selected = workflowId ? [catalog.workflows.get(workflowId)].filter((workflow) => workflow !== undefined) : [...catalog.workflows.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
|
|
5688
|
-
if (workflowId && selected.length === 0) {
|
|
5689
|
-
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
5690
|
-
return;
|
|
5691
|
-
}
|
|
5692
|
-
if (selected.length === 0) {
|
|
5693
|
-
context.ui.notify(`No workflows loaded from ${catalog.userDirectory}`, catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
5694
|
-
return;
|
|
5695
|
-
}
|
|
5696
|
-
this.pi.sendMessage({
|
|
5697
|
-
customType: "workflow-doctor",
|
|
5698
|
-
content: formatWorkflowDoctor(selected.map((workflow) => analyzeWorkflow(workflow.definition))),
|
|
5699
|
-
display: true
|
|
5700
|
-
});
|
|
5701
|
-
}
|
|
5702
|
-
async function startNow(workflowId, input, startContext, sessionEpoch) {
|
|
5703
|
-
const { context } = startContext;
|
|
5704
|
-
if (this.activeDelegation) {
|
|
5705
|
-
context.ui.notify(`Cannot start a workflow while subagent "${this.activeDelegation.agent}" is still cancelling`, "warning");
|
|
5706
|
-
return;
|
|
5707
|
-
}
|
|
5708
|
-
if (this.run && this.run.status !== "completed" && this.run.status !== "aborted") {
|
|
5709
|
-
context.ui.notify(`Workflow "${this.run.workflowId}" is ${this.run.status}; resume or abort it first`, "warning");
|
|
5710
|
-
return;
|
|
5711
|
-
}
|
|
5712
|
-
if (!context.isIdle()) {
|
|
5713
|
-
context.abort();
|
|
5714
|
-
await startContext.waitForIdle();
|
|
5715
|
-
}
|
|
5716
|
-
if (!isCurrentSession(this, sessionEpoch)) {
|
|
5717
|
-
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
5718
|
-
return;
|
|
5719
|
-
}
|
|
5720
|
-
this.captureSkills(startContext.skills());
|
|
5721
|
-
if (!await this.reloadCatalog(context, false)) {
|
|
5722
|
-
context.ui.notify("Workflow start was superseded by a newer configuration load", "warning");
|
|
5723
|
-
return;
|
|
5724
|
-
}
|
|
5725
|
-
if (!isCurrentSession(this, sessionEpoch)) {
|
|
5726
|
-
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
5727
|
-
return;
|
|
5728
|
-
}
|
|
5729
|
-
const workflow = this.catalog.workflows.get(workflowId);
|
|
5730
|
-
if (!workflow) {
|
|
5731
|
-
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
5732
|
-
return;
|
|
5733
|
-
}
|
|
5734
|
-
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter((issue) => issue.level === "error");
|
|
5735
|
-
if (livenessErrors.length > 0) {
|
|
5736
|
-
context.ui.notify(`Cannot start workflow; run /workflow-doctor ${workflowId}:
|
|
5737
|
-
${livenessErrors.map((issue) => issue.message).join(`
|
|
5738
|
-
`)}`, "error");
|
|
5739
|
-
return;
|
|
5740
|
-
}
|
|
5741
|
-
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
5742
|
-
if (preflightErrors.length > 0) {
|
|
5743
|
-
context.ui.notify(`Cannot start workflow:
|
|
5744
|
-
${preflightErrors.join(`
|
|
5745
|
-
`)}`, "error");
|
|
5746
|
-
return;
|
|
5747
|
-
}
|
|
5748
|
-
let canonicalStartCwd;
|
|
5749
|
-
try {
|
|
5750
|
-
canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
5751
|
-
candidateCwd: context.cwd,
|
|
5752
|
-
startCwd: context.cwd,
|
|
5753
|
-
allowedRoots: ["."]
|
|
5754
|
-
});
|
|
5755
|
-
} catch (error) {
|
|
5756
|
-
context.ui.notify(`Cannot capture workflow working directory: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
5757
|
-
return;
|
|
5758
|
-
}
|
|
5759
|
-
this.run = createRun(workflow, input.trim(), this.pi.getActiveTools(), this.dependencies.createRequestId(), this.dependencies.now(), canonicalStartCwd);
|
|
5760
|
-
this.persist();
|
|
5761
|
-
this.isolateMainSessionTools();
|
|
5762
|
-
this.updateStatus();
|
|
5763
|
-
this.launchCurrentStep(workflow);
|
|
5764
|
-
}
|
|
5765
|
-
async function reloadNow(context) {
|
|
5766
|
-
if (this.run && (this.run.status === "running" || this.run.status === "awaiting-gate")) {
|
|
5767
|
-
context.ui.notify("Pause the workflow before reloading its configuration", "warning");
|
|
5768
|
-
return;
|
|
5769
|
-
}
|
|
5770
|
-
this.captureSkills(context.getSystemPromptOptions().skills);
|
|
5771
|
-
await this.reloadCatalog(context, true);
|
|
5772
|
-
}
|
|
5773
|
-
function createStartActions() {
|
|
5774
|
-
return { listWorkflows, doctorWorkflows, startNow, reloadNow };
|
|
5775
|
-
}
|
|
5776
|
-
|
|
5777
|
-
// src/engine/transition-helpers.ts
|
|
5778
|
-
var currentStep = (workflow, run) => workflow.definition.steps[run.currentStepId];
|
|
5779
|
-
var withRunUpdate = (run, changes, now) => ({ ...run, ...changes, updatedAt: now });
|
|
5780
|
-
|
|
5781
|
-
// src/engine/run-advance.ts
|
|
5782
|
-
var completedStep = (run, outcome, summary, now, effects) => ({
|
|
5783
|
-
stepId: run.currentStepId,
|
|
5784
|
-
stepDigest: run.currentStepDigest,
|
|
5785
|
-
outcome,
|
|
5786
|
-
summary,
|
|
5787
|
-
...effects.workspaceCwd ? { workspaceCwd: effects.workspaceCwd } : {},
|
|
5788
|
-
...run.currentStepAttempts?.length ? { attempts: run.currentStepAttempts } : {},
|
|
5789
|
-
...run.currentStepOmittedAttempts ? { omittedAttempts: run.currentStepOmittedAttempts } : {},
|
|
5790
|
-
completedAt: now
|
|
5791
|
-
});
|
|
5792
|
-
var advanceRun = (workflow, run, outcome, summary, now, effects = {}) => {
|
|
5793
|
-
if (run.status !== "running") {
|
|
5794
|
-
throw new Error(`workflow is ${run.status}; only a running workflow can advance`);
|
|
5795
|
-
}
|
|
5796
|
-
const step = currentStep(workflow, run);
|
|
5797
|
-
if (!step) {
|
|
5798
|
-
throw new Error(`current step "${run.currentStepId}" no longer exists`);
|
|
5799
|
-
}
|
|
5800
|
-
if (step.gate?.submitOutcome === outcome) {
|
|
5801
|
-
throw new Error(`outcome "${outcome}" must be submitted through the configured gate`);
|
|
5802
|
-
}
|
|
5803
|
-
const target = step.transitions[outcome];
|
|
5804
|
-
if (!target) {
|
|
5805
|
-
throw new Error(`outcome "${outcome}" is not valid for step "${run.currentStepId}"`);
|
|
5806
|
-
}
|
|
5807
|
-
const shouldBindWorkspace = step.workspace?.bindOn.includes(outcome) ?? false;
|
|
5808
|
-
if (shouldBindWorkspace !== Boolean(effects.workspaceCwd)) {
|
|
5809
|
-
throw new Error(shouldBindWorkspace ? `outcome "${outcome}" requires a validated workspace binding` : `outcome "${outcome}" cannot bind a workspace`);
|
|
5810
|
-
}
|
|
5811
|
-
if (target === "$pause") {
|
|
5812
|
-
return withRunUpdate(run, {
|
|
5813
|
-
status: "paused",
|
|
5814
|
-
pausedFrom: "running",
|
|
5815
|
-
pauseReason: summary || `Step "${run.currentStepId}" requested a pause`,
|
|
5816
|
-
lastSummary: summary,
|
|
5817
|
-
resumeInput: undefined
|
|
5818
|
-
}, now);
|
|
5819
|
-
}
|
|
5820
|
-
const completed = completedStep(run, outcome, summary, now, effects);
|
|
5821
|
-
const cwd = effects.workspaceCwd ?? run.cwd;
|
|
5822
|
-
if (target === "$done") {
|
|
5823
|
-
return withRunUpdate(run, {
|
|
5824
|
-
status: "completed",
|
|
5825
|
-
history: [...run.history, completed],
|
|
5826
|
-
currentStepAttempts: undefined,
|
|
5827
|
-
currentStepOmittedAttempts: undefined,
|
|
5828
|
-
...cwd ? { cwd } : {},
|
|
5829
|
-
stepHandoff: summary,
|
|
5830
|
-
lastSummary: summary,
|
|
5831
|
-
gateFeedback: "",
|
|
5832
|
-
pausedFrom: undefined,
|
|
5833
|
-
pendingGate: undefined,
|
|
5834
|
-
resumeInput: undefined
|
|
5835
|
-
}, now);
|
|
5836
|
-
}
|
|
5837
|
-
if (!workflow.definition.steps[target]) {
|
|
5838
|
-
throw new Error(`transition target "${target}" does not exist`);
|
|
5839
|
-
}
|
|
5840
|
-
const nextVisitCount = (run.visits[target] ?? 0) + 1;
|
|
5841
|
-
const isOverVisitLimit = nextVisitCount > workflow.definition.maxStepVisits;
|
|
5842
|
-
const visitLimitChanges = isOverVisitLimit ? {
|
|
5843
|
-
status: "paused",
|
|
5844
|
-
pausedFrom: "running",
|
|
5845
|
-
pauseReason: `Step "${target}" exceeded maxStepVisits (${workflow.definition.maxStepVisits})`,
|
|
5846
|
-
failedStepId: target
|
|
5847
|
-
} : {
|
|
5848
|
-
status: "running",
|
|
5849
|
-
pausedFrom: undefined,
|
|
5850
|
-
pauseReason: undefined,
|
|
5851
|
-
failedStepId: undefined
|
|
5852
|
-
};
|
|
5853
|
-
return withRunUpdate(run, {
|
|
5854
|
-
...visitLimitChanges,
|
|
5855
|
-
currentStepId: target,
|
|
5856
|
-
currentStepDigest: workflow.stepDigests[target] ?? "",
|
|
5857
|
-
visits: { ...run.visits, [target]: nextVisitCount },
|
|
5858
|
-
history: [...run.history, completed],
|
|
5859
|
-
currentStepAttempts: undefined,
|
|
5860
|
-
currentStepOmittedAttempts: undefined,
|
|
5861
|
-
...cwd ? { cwd } : {},
|
|
5862
|
-
stepHandoff: summary,
|
|
5863
|
-
lastSummary: summary,
|
|
5864
|
-
gateFeedback: "",
|
|
5865
|
-
resumeInput: undefined
|
|
5866
|
-
}, now);
|
|
5867
|
-
};
|
|
5868
|
-
|
|
5869
|
-
// src/engine/gate-transitions.ts
|
|
5870
|
-
var beginGate = (workflow, run, outcome, artifact, requestId, now, summary) => {
|
|
5871
|
-
if (run.status !== "running") {
|
|
5872
|
-
throw new Error(`workflow is ${run.status}; gate submission requires a running workflow`);
|
|
5873
|
-
}
|
|
5874
|
-
const step = currentStep(workflow, run);
|
|
5875
|
-
if (!step?.gate) {
|
|
5876
|
-
throw new Error(`step "${run.currentStepId}" has no gate`);
|
|
5877
|
-
}
|
|
5878
|
-
if (outcome !== step.gate.submitOutcome) {
|
|
5879
|
-
throw new Error(`gate expects outcome "${step.gate.submitOutcome}"`);
|
|
5880
|
-
}
|
|
5881
|
-
if (!artifact.trim()) {
|
|
5882
|
-
throw new Error("gate submission requires a non-empty artifact");
|
|
5698
|
+
if (!artifact.trim()) {
|
|
5699
|
+
throw new Error("gate submission requires a non-empty artifact");
|
|
5883
5700
|
}
|
|
5884
5701
|
if (!summary.trim()) {
|
|
5885
5702
|
throw new Error("gate submission requires a non-empty summary");
|
|
@@ -5907,37 +5724,55 @@ var attachGateReviewId = (run, reviewId, now) => {
|
|
|
5907
5724
|
}
|
|
5908
5725
|
return withRunUpdate(run, { pendingGate: { ...run.pendingGate, reviewId } }, now);
|
|
5909
5726
|
};
|
|
5910
|
-
var failGate = (run, reason, now) =>
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
if (!
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
}
|
|
5935
|
-
|
|
5936
|
-
|
|
5727
|
+
var failGate = (run, reason, now) => {
|
|
5728
|
+
if (!run.pendingGate)
|
|
5729
|
+
return run;
|
|
5730
|
+
return withRunUpdate(run, {
|
|
5731
|
+
status: "running",
|
|
5732
|
+
pendingGate: undefined,
|
|
5733
|
+
gateArtifact: run.pendingGate.artifact,
|
|
5734
|
+
gateFeedback: boundedGateFeedback(reason),
|
|
5735
|
+
pausedFrom: undefined,
|
|
5736
|
+
pauseReason: undefined,
|
|
5737
|
+
failedStepId: undefined
|
|
5738
|
+
}, now);
|
|
5739
|
+
};
|
|
5740
|
+
var storeGateResolution = (run, resolution, now) => {
|
|
5741
|
+
if (!run.pendingGate)
|
|
5742
|
+
return run;
|
|
5743
|
+
return withRunUpdate(run, {
|
|
5744
|
+
pendingGate: {
|
|
5745
|
+
...run.pendingGate,
|
|
5746
|
+
resolution: {
|
|
5747
|
+
...resolution,
|
|
5748
|
+
feedback: boundedGateFeedback(resolution.feedback)
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
}, now);
|
|
5752
|
+
};
|
|
5753
|
+
var resolveGate = (workflow, run, resolution, now) => {
|
|
5754
|
+
const pendingGate = run.pendingGate;
|
|
5755
|
+
if (!pendingGate)
|
|
5756
|
+
throw new Error("workflow has no pending gate");
|
|
5757
|
+
const step = workflow.definition.steps[pendingGate.stepId];
|
|
5758
|
+
if (!step?.gate) {
|
|
5759
|
+
throw new Error(`gated step "${pendingGate.stepId}" no longer exists`);
|
|
5760
|
+
}
|
|
5761
|
+
if (run.currentStepId !== pendingGate.stepId) {
|
|
5762
|
+
throw new Error("gate result does not match the current step");
|
|
5763
|
+
}
|
|
5764
|
+
const outcome = resolution.approved ? step.gate.approvedOutcome : step.gate.rejectedOutcome;
|
|
5765
|
+
const feedback = boundedGateFeedback(resolution.feedback);
|
|
5766
|
+
const stepStructuralDigest = workflow.stepStructuralDigests[pendingGate.stepId] ?? "";
|
|
5767
|
+
if (resolution.approved && !stepStructuralDigest) {
|
|
5768
|
+
throw new Error(`gated step "${pendingGate.stepId}" has no structural digest`);
|
|
5769
|
+
}
|
|
5770
|
+
const summary = resolution.approved ? pendingGate.summary ?? "" : gateRejectionSummary(feedback);
|
|
5771
|
+
const decidedRun = recordCurrentGateDecision(run, {
|
|
5937
5772
|
provider: pendingGate.provider,
|
|
5938
5773
|
requestId: pendingGate.requestId,
|
|
5939
5774
|
approved: resolution.approved,
|
|
5940
|
-
feedback
|
|
5775
|
+
feedback,
|
|
5941
5776
|
resolvedAt: resolution.resolvedAt,
|
|
5942
5777
|
...pendingGate.reviewId ? { reviewId: pendingGate.reviewId } : {}
|
|
5943
5778
|
}, now);
|
|
@@ -5946,9 +5781,13 @@ var resolveGate = (workflow, run, resolution, now) => {
|
|
|
5946
5781
|
pendingGate: undefined,
|
|
5947
5782
|
pausedFrom: undefined,
|
|
5948
5783
|
pauseReason: undefined,
|
|
5949
|
-
|
|
5784
|
+
gateArtifact: resolution.approved ? "" : pendingGate.artifact,
|
|
5785
|
+
gateFeedback: resolution.approved ? "" : feedback
|
|
5950
5786
|
}, now);
|
|
5951
|
-
const
|
|
5787
|
+
const isSameStepHumanRevision = !resolution.approved && step.transitions[outcome] === pendingGate.stepId;
|
|
5788
|
+
const advanced = advanceRun(workflow, runnableRun, outcome, summary, now, {}, {
|
|
5789
|
+
sameStepHumanGateRevision: isSameStepHumanRevision
|
|
5790
|
+
});
|
|
5952
5791
|
const completedApprovedGate = resolution.approved && advanced.history.length > runnableRun.history.length;
|
|
5953
5792
|
const history = completedApprovedGate ? advanced.history.map((entry, index) => index === advanced.history.length - 1 ? {
|
|
5954
5793
|
...entry,
|
|
@@ -5956,7 +5795,7 @@ var resolveGate = (workflow, run, resolution, now) => {
|
|
|
5956
5795
|
approval: {
|
|
5957
5796
|
requestId: pendingGate.requestId,
|
|
5958
5797
|
artifact: pendingGate.artifact,
|
|
5959
|
-
feedback
|
|
5798
|
+
feedback,
|
|
5960
5799
|
stepStructuralDigest
|
|
5961
5800
|
}
|
|
5962
5801
|
} : entry) : advanced.history;
|
|
@@ -5965,12 +5804,21 @@ var resolveGate = (workflow, run, resolution, now) => {
|
|
|
5965
5804
|
history,
|
|
5966
5805
|
...completedApprovedGate ? {
|
|
5967
5806
|
reviewedArtifact: pendingGate.artifact,
|
|
5968
|
-
reviewedFeedback:
|
|
5807
|
+
reviewedFeedback: feedback
|
|
5969
5808
|
} : {},
|
|
5970
|
-
|
|
5809
|
+
gateArtifact: resolution.approved ? "" : pendingGate.artifact,
|
|
5810
|
+
gateFeedback: resolution.approved ? "" : feedback
|
|
5971
5811
|
};
|
|
5972
5812
|
};
|
|
5973
5813
|
// src/engine/run-lifecycle.ts
|
|
5814
|
+
var completedWorkspaceCwd = (run) => {
|
|
5815
|
+
for (let index = run.history.length - 1;index >= 0; index -= 1) {
|
|
5816
|
+
const workspaceCwd = run.history[index]?.workspaceCwd;
|
|
5817
|
+
if (workspaceCwd)
|
|
5818
|
+
return workspaceCwd;
|
|
5819
|
+
}
|
|
5820
|
+
return;
|
|
5821
|
+
};
|
|
5974
5822
|
var allowedOutcomes = (workflow, run) => {
|
|
5975
5823
|
const step = currentStep(workflow, run);
|
|
5976
5824
|
if (!step)
|
|
@@ -6015,6 +5863,32 @@ var abortRun = (run, reason, now) => withRunUpdate(run, {
|
|
|
6015
5863
|
pendingGate: undefined,
|
|
6016
5864
|
resumeInput: undefined
|
|
6017
5865
|
}, now);
|
|
5866
|
+
var restartRun = (workflow, run, input, baselineTools, now) => {
|
|
5867
|
+
if (run.status !== "completed") {
|
|
5868
|
+
throw new Error("only a completed workflow can be restarted");
|
|
5869
|
+
}
|
|
5870
|
+
if (!run.startCwd) {
|
|
5871
|
+
throw new Error("the completed workflow has no captured start directory; start a new workflow instead");
|
|
5872
|
+
}
|
|
5873
|
+
const previousIteration = run.iteration ?? 1;
|
|
5874
|
+
if (!Number.isSafeInteger(previousIteration) || previousIteration < 1) {
|
|
5875
|
+
throw new Error("the completed workflow has an invalid iteration number");
|
|
5876
|
+
}
|
|
5877
|
+
if (previousIteration >= Number.MAX_SAFE_INTEGER) {
|
|
5878
|
+
throw new Error("the workflow iteration limit has been reached");
|
|
5879
|
+
}
|
|
5880
|
+
const workspaceCwd = completedWorkspaceCwd(run);
|
|
5881
|
+
if (workspaceCwd && run.cwd !== workspaceCwd) {
|
|
5882
|
+
throw new Error("the completed workflow workspace does not match its recorded binding");
|
|
5883
|
+
}
|
|
5884
|
+
const restarted = createRun(workflow, input, baselineTools, run.runId, now, run.startCwd, previousIteration + 1);
|
|
5885
|
+
return {
|
|
5886
|
+
...restarted,
|
|
5887
|
+
stepHandoff: run.lastSummary,
|
|
5888
|
+
lastSummary: run.lastSummary,
|
|
5889
|
+
...workspaceCwd ? { restartWorkspaceCwd: workspaceCwd } : {}
|
|
5890
|
+
};
|
|
5891
|
+
};
|
|
6018
5892
|
// src/engine/reconciliation-history.ts
|
|
6019
5893
|
var isApprovedGateEntry = (workflow, entry) => {
|
|
6020
5894
|
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
@@ -6155,126 +6029,523 @@ function validateRunWorkflowSemantics(run, workflow) {
|
|
|
6155
6029
|
expectedStepId = target;
|
|
6156
6030
|
expectedVisits[target] = (expectedVisits[target] ?? 0) + 1;
|
|
6157
6031
|
}
|
|
6158
|
-
if (run.currentStepId !== expectedStepId) {
|
|
6159
|
-
return `current step "${run.currentStepId}" does not match reachable step "${expectedStepId}"`;
|
|
6032
|
+
if (run.currentStepId !== expectedStepId) {
|
|
6033
|
+
return `current step "${run.currentStepId}" does not match reachable step "${expectedStepId}"`;
|
|
6034
|
+
}
|
|
6035
|
+
if (reachedDone !== (run.status === "completed")) {
|
|
6036
|
+
return reachedDone ? "a workflow that reached $done must be completed" : "a completed workflow has no $done transition in its history";
|
|
6037
|
+
}
|
|
6038
|
+
if (run.status === "completed" && run.currentStepDigest !== run.history.at(-1)?.stepDigest) {
|
|
6039
|
+
return "completed workflow current-step digest does not match its terminal history";
|
|
6040
|
+
}
|
|
6041
|
+
if (!sameVisitCounts(run.visits, expectedVisits)) {
|
|
6042
|
+
return "workflow visit counts do not match its execution history";
|
|
6043
|
+
}
|
|
6044
|
+
const reviewedArtifact = run.reviewedArtifact ?? "";
|
|
6045
|
+
const reviewedFeedback = run.reviewedFeedback ?? "";
|
|
6046
|
+
if (reviewedArtifact !== (latestApproval?.artifact ?? "") || reviewedFeedback !== (latestApproval?.feedback ?? "")) {
|
|
6047
|
+
return "reviewed artifact and feedback do not match authoritative approval history";
|
|
6048
|
+
}
|
|
6049
|
+
const currentStep2 = workflow.definition.steps[run.currentStepId];
|
|
6050
|
+
if (!currentStep2) {
|
|
6051
|
+
return `current step "${run.currentStepId}" is missing from the workflow`;
|
|
6052
|
+
}
|
|
6053
|
+
const currentStepChanged = run.currentStepDigest !== workflow.stepDigests[run.currentStepId];
|
|
6054
|
+
if (sameWorkflowDigest && currentStepChanged) {
|
|
6055
|
+
return `current step "${run.currentStepId}" does not match the active workflow digest`;
|
|
6056
|
+
}
|
|
6057
|
+
if (boundWorkspaceCwd && !currentStep2.subagent) {
|
|
6058
|
+
return `bound workflow current step "${run.currentStepId}" must use a subagent`;
|
|
6059
|
+
}
|
|
6060
|
+
if (currentStepChanged)
|
|
6061
|
+
return;
|
|
6062
|
+
return validatePendingGate(run, currentStep2);
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
// src/engine/run-reconciliation.ts
|
|
6066
|
+
var reconcileRun = (run, workflow, now) => {
|
|
6067
|
+
if (run.workflowId !== workflow.definition.id) {
|
|
6068
|
+
return {
|
|
6069
|
+
changed: false,
|
|
6070
|
+
error: `run belongs to "${run.workflowId}", not "${workflow.definition.id}"`
|
|
6071
|
+
};
|
|
6072
|
+
}
|
|
6073
|
+
const reconciledRun = run.workflowDigest === workflow.digest ? run : refreshApprovedGateHistory(run, workflow);
|
|
6074
|
+
const changedHistoryIndex = run.workflowDigest === workflow.digest ? -1 : reconciledRun.history.findIndex((entry) => workflow.stepDigests[entry.stepId] !== entry.stepDigest);
|
|
6075
|
+
const changedHistoryEntry = changedHistoryIndex >= 0 ? reconciledRun.history[changedHistoryIndex] : undefined;
|
|
6076
|
+
if (changedHistoryEntry && !workflow.definition.steps[changedHistoryEntry.stepId]) {
|
|
6077
|
+
return {
|
|
6078
|
+
changed: true,
|
|
6079
|
+
error: "a completed step was removed; abort or restore the configuration"
|
|
6080
|
+
};
|
|
6081
|
+
}
|
|
6082
|
+
if (changedHistoryIndex < 0 && !workflow.definition.steps[run.currentStepId]) {
|
|
6083
|
+
return {
|
|
6084
|
+
changed: true,
|
|
6085
|
+
error: `current step "${run.currentStepId}" was removed; abort or restore the configuration`
|
|
6086
|
+
};
|
|
6087
|
+
}
|
|
6088
|
+
const semanticError = validateRunWorkflowSemantics(reconciledRun, workflow);
|
|
6089
|
+
if (semanticError) {
|
|
6090
|
+
return {
|
|
6091
|
+
changed: run.workflowDigest !== workflow.digest,
|
|
6092
|
+
error: `workflow checkpoint is inconsistent: ${semanticError}`
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
6095
|
+
if (run.workflowDigest === workflow.digest) {
|
|
6096
|
+
return { run, changed: false };
|
|
6097
|
+
}
|
|
6098
|
+
if (changedHistoryIndex >= 0) {
|
|
6099
|
+
const changedEntry = changedHistoryEntry;
|
|
6100
|
+
if (!changedEntry) {
|
|
6101
|
+
return { changed: true, error: "changed history entry is unavailable" };
|
|
6102
|
+
}
|
|
6103
|
+
const retainedHistory = reconciledRun.history.slice(0, changedHistoryIndex);
|
|
6104
|
+
const restartedStep = changedEntry.stepId;
|
|
6105
|
+
const stepHandoff = retainedHistory.at(-1)?.summary ?? "";
|
|
6106
|
+
const reviewedApproval = retainedReviewedApproval(workflow, retainedHistory);
|
|
6107
|
+
return {
|
|
6108
|
+
changed: true,
|
|
6109
|
+
restartedStep,
|
|
6110
|
+
run: withRunUpdate(reconciledRun, {
|
|
6111
|
+
workflowDigest: workflow.digest,
|
|
6112
|
+
status: "paused",
|
|
6113
|
+
currentStepId: restartedStep,
|
|
6114
|
+
currentStepDigest: workflow.stepDigests[restartedStep] ?? "",
|
|
6115
|
+
history: retainedHistory,
|
|
6116
|
+
currentStepAttempts: changedEntry.attempts,
|
|
6117
|
+
currentStepOmittedAttempts: changedEntry.omittedAttempts,
|
|
6118
|
+
visits: rebuildVisits(retainedHistory, restartedStep),
|
|
6119
|
+
cwd: retainedWorkspaceCwd(reconciledRun, retainedHistory),
|
|
6120
|
+
reviewedArtifact: reviewedApproval?.artifact ?? "",
|
|
6121
|
+
reviewedFeedback: reviewedApproval?.feedback ?? "",
|
|
6122
|
+
stepHandoff,
|
|
6123
|
+
lastSummary: stepHandoff,
|
|
6124
|
+
pendingGate: undefined,
|
|
6125
|
+
pausedFrom: "running",
|
|
6126
|
+
failedStepId: undefined,
|
|
6127
|
+
pauseReason: `Configuration changed; restarted step "${restartedStep}"`,
|
|
6128
|
+
gateArtifact: "",
|
|
6129
|
+
gateFeedback: ""
|
|
6130
|
+
}, now)
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
6133
|
+
const currentDigest = workflow.stepDigests[run.currentStepId] ?? "";
|
|
6134
|
+
const hasCurrentStepChanged = currentDigest !== reconciledRun.currentStepDigest;
|
|
6135
|
+
const restartChanges = hasCurrentStepChanged ? {
|
|
6136
|
+
status: "paused",
|
|
6137
|
+
pendingGate: undefined,
|
|
6138
|
+
pausedFrom: "running",
|
|
6139
|
+
failedStepId: undefined,
|
|
6140
|
+
pauseReason: `Configuration changed; restarted step "${run.currentStepId}"`,
|
|
6141
|
+
gateArtifact: "",
|
|
6142
|
+
gateFeedback: ""
|
|
6143
|
+
} : {};
|
|
6144
|
+
return {
|
|
6145
|
+
changed: true,
|
|
6146
|
+
...hasCurrentStepChanged ? { restartedStep: run.currentStepId } : {},
|
|
6147
|
+
run: withRunUpdate(reconciledRun, {
|
|
6148
|
+
...restartChanges,
|
|
6149
|
+
workflowDigest: workflow.digest,
|
|
6150
|
+
currentStepDigest: currentDigest
|
|
6151
|
+
}, now)
|
|
6152
|
+
};
|
|
6153
|
+
};
|
|
6154
|
+
// src/workflow-doctor.ts
|
|
6155
|
+
var lexical = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
6156
|
+
var internalTargets = (step) => [
|
|
6157
|
+
...new Set(Object.values(step.transitions).filter((target) => target !== "$done" && target !== "$pause"))
|
|
6158
|
+
].sort(lexical);
|
|
6159
|
+
var adjacencyFor = (definition) => Object.fromEntries(Object.entries(definition.steps).sort(([left], [right]) => lexical(left, right)).map(([stepId, step]) => [stepId, internalTargets(step)]));
|
|
6160
|
+
var reachableSteps = (definition, adjacency) => {
|
|
6161
|
+
const reachable = new Set;
|
|
6162
|
+
const pending = [definition.start];
|
|
6163
|
+
while (pending.length > 0) {
|
|
6164
|
+
const stepId = pending.pop();
|
|
6165
|
+
if (!stepId || reachable.has(stepId))
|
|
6166
|
+
continue;
|
|
6167
|
+
if (!definition.steps[stepId])
|
|
6168
|
+
continue;
|
|
6169
|
+
reachable.add(stepId);
|
|
6170
|
+
pending.push(...adjacency[stepId] ?? []);
|
|
6171
|
+
}
|
|
6172
|
+
return reachable;
|
|
6173
|
+
};
|
|
6174
|
+
var stepsThatCanComplete = (definition, adjacency) => {
|
|
6175
|
+
const canComplete = new Set(Object.entries(definition.steps).filter(([, step]) => Object.values(step.transitions).includes("$done")).map(([stepId]) => stepId));
|
|
6176
|
+
const reverse = new Map;
|
|
6177
|
+
for (const [source, targets] of Object.entries(adjacency)) {
|
|
6178
|
+
for (const target of targets) {
|
|
6179
|
+
reverse.set(target, [...reverse.get(target) ?? [], source]);
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6182
|
+
const pending = [...canComplete].sort(lexical);
|
|
6183
|
+
while (pending.length > 0) {
|
|
6184
|
+
const stepId = pending.pop();
|
|
6185
|
+
if (!stepId)
|
|
6186
|
+
continue;
|
|
6187
|
+
for (const predecessor of (reverse.get(stepId) ?? []).sort(lexical)) {
|
|
6188
|
+
if (canComplete.has(predecessor))
|
|
6189
|
+
continue;
|
|
6190
|
+
canComplete.add(predecessor);
|
|
6191
|
+
pending.push(predecessor);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
return canComplete;
|
|
6195
|
+
};
|
|
6196
|
+
var stronglyConnectedComponents = (definition, adjacency) => {
|
|
6197
|
+
let nextIndex = 0;
|
|
6198
|
+
const indexes = new Map;
|
|
6199
|
+
const lowLinks = new Map;
|
|
6200
|
+
const stack = [];
|
|
6201
|
+
const onStack = new Set;
|
|
6202
|
+
const components = [];
|
|
6203
|
+
const visit = (stepId) => {
|
|
6204
|
+
indexes.set(stepId, nextIndex);
|
|
6205
|
+
lowLinks.set(stepId, nextIndex);
|
|
6206
|
+
nextIndex += 1;
|
|
6207
|
+
stack.push(stepId);
|
|
6208
|
+
onStack.add(stepId);
|
|
6209
|
+
for (const target of adjacency[stepId] ?? []) {
|
|
6210
|
+
if (!indexes.has(target)) {
|
|
6211
|
+
visit(target);
|
|
6212
|
+
lowLinks.set(stepId, Math.min(lowLinks.get(stepId) ?? 0, lowLinks.get(target) ?? 0));
|
|
6213
|
+
} else if (onStack.has(target)) {
|
|
6214
|
+
lowLinks.set(stepId, Math.min(lowLinks.get(stepId) ?? 0, indexes.get(target) ?? 0));
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
if (lowLinks.get(stepId) !== indexes.get(stepId))
|
|
6218
|
+
return;
|
|
6219
|
+
const component = [];
|
|
6220
|
+
let member;
|
|
6221
|
+
do {
|
|
6222
|
+
member = stack.pop();
|
|
6223
|
+
if (!member)
|
|
6224
|
+
break;
|
|
6225
|
+
onStack.delete(member);
|
|
6226
|
+
component.push(member);
|
|
6227
|
+
} while (member !== stepId);
|
|
6228
|
+
components.push(component.sort(lexical));
|
|
6229
|
+
};
|
|
6230
|
+
for (const stepId of Object.keys(definition.steps).sort(lexical)) {
|
|
6231
|
+
if (!indexes.has(stepId))
|
|
6232
|
+
visit(stepId);
|
|
6233
|
+
}
|
|
6234
|
+
return components.sort((left, right) => lexical(left.join("\x00"), right.join("\x00")));
|
|
6235
|
+
};
|
|
6236
|
+
var isCycle = (adjacency, component) => {
|
|
6237
|
+
if (component.length > 1)
|
|
6238
|
+
return true;
|
|
6239
|
+
const [stepId] = component;
|
|
6240
|
+
return Boolean(stepId && adjacency[stepId]?.includes(stepId));
|
|
6241
|
+
};
|
|
6242
|
+
function analyzeWorkflow(definition) {
|
|
6243
|
+
const adjacency = adjacencyFor(definition);
|
|
6244
|
+
const reachable = reachableSteps(definition, adjacency);
|
|
6245
|
+
const canComplete = stepsThatCanComplete(definition, adjacency);
|
|
6246
|
+
const issues = [];
|
|
6247
|
+
const stranded = [...reachable].filter((stepId) => !canComplete.has(stepId)).sort(lexical);
|
|
6248
|
+
if (!canComplete.has(definition.start)) {
|
|
6249
|
+
issues.push({
|
|
6250
|
+
level: "error",
|
|
6251
|
+
code: "no-completion-path",
|
|
6252
|
+
steps: [definition.start],
|
|
6253
|
+
message: `start step ${definition.start} cannot reach $done`
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
if (stranded.length > 0) {
|
|
6257
|
+
issues.push({
|
|
6258
|
+
level: "error",
|
|
6259
|
+
code: "reachable-step-cannot-reach-done",
|
|
6260
|
+
steps: stranded,
|
|
6261
|
+
message: `reachable step${stranded.length === 1 ? "" : "s"} ${stranded.join(", ")} cannot reach $done`
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
const unreachable = Object.keys(definition.steps).filter((stepId) => !reachable.has(stepId)).sort(lexical);
|
|
6265
|
+
if (unreachable.length > 0) {
|
|
6266
|
+
issues.push({
|
|
6267
|
+
level: "warning",
|
|
6268
|
+
code: "unreachable-steps",
|
|
6269
|
+
steps: unreachable,
|
|
6270
|
+
message: `unreachable step${unreachable.length === 1 ? "" : "s"}: ${unreachable.join(", ")}`
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
for (const component of stronglyConnectedComponents(definition, adjacency)) {
|
|
6274
|
+
if (!isCycle(adjacency, component))
|
|
6275
|
+
continue;
|
|
6276
|
+
const componentIsReachable = component.some((stepId) => reachable.has(stepId));
|
|
6277
|
+
const componentCanReachDone = component.some((stepId) => canComplete.has(stepId));
|
|
6278
|
+
issues.push({
|
|
6279
|
+
level: "warning",
|
|
6280
|
+
code: "cycle",
|
|
6281
|
+
steps: component,
|
|
6282
|
+
reachable: componentIsReachable,
|
|
6283
|
+
canReachDone: componentCanReachDone,
|
|
6284
|
+
message: `${componentIsReachable ? "reachable" : "unreachable"} cyclic component: ${component.join(", ")}; ${componentCanReachDone ? "an exit can reach $done" : "no member can reach $done"}; maxStepVisits=${definition.maxStepVisits} bounds uninterrupted graph cycling`
|
|
6285
|
+
});
|
|
6286
|
+
}
|
|
6287
|
+
return {
|
|
6288
|
+
workflowId: definition.id,
|
|
6289
|
+
maxStepVisits: definition.maxStepVisits,
|
|
6290
|
+
reachableSteps: [...reachable].sort(lexical),
|
|
6291
|
+
issues
|
|
6292
|
+
};
|
|
6293
|
+
}
|
|
6294
|
+
var escapeMarkdown = (value) => value.replaceAll("\\", "\\\\").replaceAll("|", "\\|");
|
|
6295
|
+
function formatWorkflowDoctor(reports) {
|
|
6296
|
+
const lines = ["# Workflow doctor", ""];
|
|
6297
|
+
for (const report of reports) {
|
|
6298
|
+
const errors = report.issues.filter((issue) => issue.level === "error");
|
|
6299
|
+
const warnings = report.issues.filter((issue) => issue.level === "warning");
|
|
6300
|
+
lines.push(`## ${report.workflowId}`, "", `Result: ${errors.length > 0 ? "ERROR" : warnings.length > 0 ? "WARNING" : "PASS"}`, "", `Runtime loop guard: automatic graph advancement enters each step at most ${report.maxStepVisits} time${report.maxStepVisits === 1 ? "" : "s"} before the next attempted entry pauses the run. An explicit human rejection back to the same gated step bypasses that check for its transition because every revision awaits another decision; the visit is still recorded. This bounds unattended cycling; it does not guarantee $done or bound time spent inside a step or gate.`, "");
|
|
6301
|
+
if (report.issues.length === 0) {
|
|
6302
|
+
lines.push("- No liveness issues found.", "");
|
|
6303
|
+
continue;
|
|
6304
|
+
}
|
|
6305
|
+
lines.push(...report.issues.map((issue) => `- ${issue.level.toUpperCase()} \`${issue.code}\`: ${escapeMarkdown(issue.message)}`), "");
|
|
6306
|
+
}
|
|
6307
|
+
return lines.join(`
|
|
6308
|
+
`).trimEnd();
|
|
6309
|
+
}
|
|
6310
|
+
|
|
6311
|
+
// src/workflow-list.ts
|
|
6312
|
+
function escapeMarkdownTableCell(value) {
|
|
6313
|
+
return value.replaceAll("\\", "\\\\").replaceAll("|", "\\|").replace(/\r\n|\r|\n/g, " ");
|
|
6314
|
+
}
|
|
6315
|
+
function formatWorkflowList(workflows) {
|
|
6316
|
+
return [
|
|
6317
|
+
"| Workflow | Command | Description |",
|
|
6318
|
+
"| --- | --- | --- |",
|
|
6319
|
+
...workflows.map((workflow) => `| \`${workflow.id}\` | \`/${workflow.command}\` | ${escapeMarkdownTableCell(workflow.description)} |`)
|
|
6320
|
+
].join(`
|
|
6321
|
+
`);
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
// src/harness/start-actions.ts
|
|
6325
|
+
function isCurrentSession(session, sessionEpoch) {
|
|
6326
|
+
return session.isSessionActive && session.sessionEpoch === sessionEpoch;
|
|
6327
|
+
}
|
|
6328
|
+
function completedWorkspaceBinding(run, workflow) {
|
|
6329
|
+
for (let index = run.history.length - 1;index >= 0; index -= 1) {
|
|
6330
|
+
const entry = run.history[index];
|
|
6331
|
+
if (!entry?.workspaceCwd)
|
|
6332
|
+
continue;
|
|
6333
|
+
const step = workflow.definition.steps[entry.stepId];
|
|
6334
|
+
if (!step?.workspace || !step.workspace.bindOn.includes(entry.outcome)) {
|
|
6335
|
+
throw new Error(`workspace-binding step "${entry.stepId}" no longer matches the completed iteration`);
|
|
6336
|
+
}
|
|
6337
|
+
return {
|
|
6338
|
+
cwd: entry.workspaceCwd,
|
|
6339
|
+
allowedRoots: step.workspace.allowedRoots
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
async function listWorkflows(context) {
|
|
6345
|
+
const workflows = [...this.catalog.workflows.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
|
|
6346
|
+
if (workflows.length === 0) {
|
|
6347
|
+
context.ui.notify(`No workflows loaded from ${this.catalog.userDirectory}`, this.catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
6350
|
+
this.pi.sendMessage({
|
|
6351
|
+
customType: "workflow-list",
|
|
6352
|
+
content: formatWorkflowList(workflows.map((workflow) => workflow.definition)),
|
|
6353
|
+
display: true
|
|
6354
|
+
});
|
|
6355
|
+
}
|
|
6356
|
+
async function doctorWorkflows(workflowId, context) {
|
|
6357
|
+
const catalog = await this.dependencies.loadCatalog({
|
|
6358
|
+
cwd: context.cwd,
|
|
6359
|
+
projectTrusted: context.isProjectTrusted()
|
|
6360
|
+
});
|
|
6361
|
+
if (catalog.diagnostics.some((diagnostic) => diagnostic.level === "error")) {
|
|
6362
|
+
context.ui.notify(`Workflow configuration errors:
|
|
6363
|
+
${formatCatalogDiagnostics(catalog)}`, "warning");
|
|
6364
|
+
}
|
|
6365
|
+
const selected = workflowId ? [catalog.workflows.get(workflowId)].filter((workflow) => workflow !== undefined) : [...catalog.workflows.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
|
|
6366
|
+
if (workflowId && selected.length === 0) {
|
|
6367
|
+
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
6368
|
+
return;
|
|
6160
6369
|
}
|
|
6161
|
-
if (
|
|
6162
|
-
|
|
6370
|
+
if (selected.length === 0) {
|
|
6371
|
+
context.ui.notify(`No workflows loaded from ${catalog.userDirectory}`, catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
6372
|
+
return;
|
|
6163
6373
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6374
|
+
this.pi.sendMessage({
|
|
6375
|
+
customType: "workflow-doctor",
|
|
6376
|
+
content: formatWorkflowDoctor(selected.map((workflow) => analyzeWorkflow(workflow.definition))),
|
|
6377
|
+
display: true
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6380
|
+
async function startNow(workflowId, input, startContext, sessionEpoch) {
|
|
6381
|
+
const { context } = startContext;
|
|
6382
|
+
if (this.activeDelegation) {
|
|
6383
|
+
context.ui.notify(`Cannot start a workflow while subagent "${this.activeDelegation.agent}" is still cancelling`, "warning");
|
|
6384
|
+
return;
|
|
6166
6385
|
}
|
|
6167
|
-
if (
|
|
6168
|
-
|
|
6386
|
+
if (this.run && this.run.status !== "completed" && this.run.status !== "aborted") {
|
|
6387
|
+
context.ui.notify(`Workflow "${this.run.workflowId}" is ${this.run.status}; resume or abort it first`, "warning");
|
|
6388
|
+
return;
|
|
6169
6389
|
}
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
return "reviewed artifact and feedback do not match authoritative approval history";
|
|
6390
|
+
if (!context.isIdle()) {
|
|
6391
|
+
context.abort();
|
|
6392
|
+
await startContext.waitForIdle();
|
|
6174
6393
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
return
|
|
6394
|
+
if (!isCurrentSession(this, sessionEpoch)) {
|
|
6395
|
+
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
6396
|
+
return;
|
|
6178
6397
|
}
|
|
6179
|
-
|
|
6180
|
-
if (
|
|
6181
|
-
|
|
6398
|
+
this.captureSkills(startContext.skills());
|
|
6399
|
+
if (!await this.reloadCatalog(context, false)) {
|
|
6400
|
+
context.ui.notify("Workflow start was superseded by a newer configuration load", "warning");
|
|
6401
|
+
return;
|
|
6182
6402
|
}
|
|
6183
|
-
if (
|
|
6184
|
-
|
|
6403
|
+
if (!isCurrentSession(this, sessionEpoch)) {
|
|
6404
|
+
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
6405
|
+
return;
|
|
6185
6406
|
}
|
|
6186
|
-
|
|
6407
|
+
const workflow = this.catalog.workflows.get(workflowId);
|
|
6408
|
+
if (!workflow) {
|
|
6409
|
+
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
6187
6410
|
return;
|
|
6188
|
-
|
|
6411
|
+
}
|
|
6412
|
+
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter((issue) => issue.level === "error");
|
|
6413
|
+
if (livenessErrors.length > 0) {
|
|
6414
|
+
context.ui.notify(`Cannot start workflow; run /workflow-doctor ${workflowId}:
|
|
6415
|
+
${livenessErrors.map((issue) => issue.message).join(`
|
|
6416
|
+
`)}`, "error");
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
6420
|
+
if (preflightErrors.length > 0) {
|
|
6421
|
+
context.ui.notify(`Cannot start workflow:
|
|
6422
|
+
${preflightErrors.join(`
|
|
6423
|
+
`)}`, "error");
|
|
6424
|
+
return;
|
|
6425
|
+
}
|
|
6426
|
+
let canonicalStartCwd;
|
|
6427
|
+
try {
|
|
6428
|
+
canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
6429
|
+
candidateCwd: context.cwd,
|
|
6430
|
+
startCwd: context.cwd,
|
|
6431
|
+
allowedRoots: ["."]
|
|
6432
|
+
});
|
|
6433
|
+
} catch (error) {
|
|
6434
|
+
context.ui.notify(`Cannot capture workflow working directory: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
6435
|
+
return;
|
|
6436
|
+
}
|
|
6437
|
+
this.run = createRun(workflow, input.trim(), this.pi.getActiveTools(), this.dependencies.createRequestId(), this.dependencies.now(), canonicalStartCwd);
|
|
6438
|
+
this.persist();
|
|
6439
|
+
this.isolateMainSessionTools();
|
|
6440
|
+
this.updateStatus();
|
|
6441
|
+
this.launchCurrentStep(workflow);
|
|
6189
6442
|
}
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
if (
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
error: `run belongs to "${run.workflowId}", not "${workflow.definition.id}"`
|
|
6197
|
-
};
|
|
6443
|
+
async function restartNow(input, startContext, sessionEpoch) {
|
|
6444
|
+
const { context } = startContext;
|
|
6445
|
+
const completedRun = this.run;
|
|
6446
|
+
if (!completedRun || completedRun.status !== "completed") {
|
|
6447
|
+
context.ui.notify("Only a completed workflow can be restarted", "warning");
|
|
6448
|
+
return;
|
|
6198
6449
|
}
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
if (changedHistoryEntry && !workflow.definition.steps[changedHistoryEntry.stepId]) {
|
|
6203
|
-
return {
|
|
6204
|
-
changed: true,
|
|
6205
|
-
error: "a completed step was removed; abort or restore the configuration"
|
|
6206
|
-
};
|
|
6450
|
+
if (this.activeDelegation) {
|
|
6451
|
+
context.ui.notify(`Cannot restart while subagent "${this.activeDelegation.agent}" is still cancelling`, "warning");
|
|
6452
|
+
return;
|
|
6207
6453
|
}
|
|
6208
|
-
if (
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
error: `current step "${run.currentStepId}" was removed; abort or restore the configuration`
|
|
6212
|
-
};
|
|
6454
|
+
if (!completedRun.startCwd) {
|
|
6455
|
+
context.ui.notify("Cannot restart this workflow on the same worktree because its original start directory was not captured; start a new workflow instead", "error");
|
|
6456
|
+
return;
|
|
6213
6457
|
}
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
changed: run.workflowDigest !== workflow.digest,
|
|
6218
|
-
error: `workflow checkpoint is inconsistent: ${semanticError}`
|
|
6219
|
-
};
|
|
6458
|
+
if (!context.isIdle()) {
|
|
6459
|
+
context.abort();
|
|
6460
|
+
await startContext.waitForIdle();
|
|
6220
6461
|
}
|
|
6221
|
-
if (run
|
|
6222
|
-
|
|
6462
|
+
if (!isCurrentSession(this, sessionEpoch) || this.run !== completedRun) {
|
|
6463
|
+
context.ui.notify("Workflow restart was superseded by a session or workflow change", "warning");
|
|
6464
|
+
return;
|
|
6223
6465
|
}
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6466
|
+
this.captureSkills(startContext.skills());
|
|
6467
|
+
if (!await this.reloadCatalog(context, false)) {
|
|
6468
|
+
context.ui.notify("Workflow restart was superseded by a newer configuration load", "warning");
|
|
6469
|
+
return;
|
|
6470
|
+
}
|
|
6471
|
+
if (!isCurrentSession(this, sessionEpoch) || this.run !== completedRun) {
|
|
6472
|
+
context.ui.notify("Workflow restart was superseded by a session or workflow change", "warning");
|
|
6473
|
+
return;
|
|
6474
|
+
}
|
|
6475
|
+
const workflow = this.catalog.workflows.get(completedRun.workflowId);
|
|
6476
|
+
if (!workflow) {
|
|
6477
|
+
context.ui.notify(`Workflow "${completedRun.workflowId}" is no longer loaded`, "error");
|
|
6478
|
+
return;
|
|
6479
|
+
}
|
|
6480
|
+
const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter((issue) => issue.level === "error");
|
|
6481
|
+
if (livenessErrors.length > 0) {
|
|
6482
|
+
context.ui.notify(`Cannot restart workflow; run /workflow-doctor ${workflow.definition.id}:
|
|
6483
|
+
${livenessErrors.map((issue) => issue.message).join(`
|
|
6484
|
+
`)}`, "error");
|
|
6485
|
+
return;
|
|
6486
|
+
}
|
|
6487
|
+
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
6488
|
+
if (preflightErrors.length > 0) {
|
|
6489
|
+
context.ui.notify(`Cannot restart workflow:
|
|
6490
|
+
${preflightErrors.join(`
|
|
6491
|
+
`)}`, "error");
|
|
6492
|
+
return;
|
|
6493
|
+
}
|
|
6494
|
+
let canonicalStartCwd;
|
|
6495
|
+
let canonicalSessionCwd;
|
|
6496
|
+
try {
|
|
6497
|
+
canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
6498
|
+
candidateCwd: completedRun.startCwd,
|
|
6499
|
+
startCwd: completedRun.startCwd,
|
|
6500
|
+
allowedRoots: ["."]
|
|
6501
|
+
});
|
|
6502
|
+
canonicalSessionCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
6503
|
+
candidateCwd: context.cwd,
|
|
6504
|
+
startCwd: context.cwd,
|
|
6505
|
+
allowedRoots: ["."]
|
|
6506
|
+
});
|
|
6507
|
+
} catch (error) {
|
|
6508
|
+
context.ui.notify(`Cannot restart workflow on its captured worktree: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
6509
|
+
return;
|
|
6510
|
+
}
|
|
6511
|
+
if (canonicalStartCwd !== completedRun.startCwd || canonicalSessionCwd !== canonicalStartCwd) {
|
|
6512
|
+
context.ui.notify("Current session cwd does not match the captured workflow start directory", "error");
|
|
6513
|
+
return;
|
|
6514
|
+
}
|
|
6515
|
+
try {
|
|
6516
|
+
const binding = completedWorkspaceBinding(completedRun, workflow);
|
|
6517
|
+
if (binding) {
|
|
6518
|
+
const canonicalWorkspaceCwd = this.dependencies.resolveWorkspaceDirectory({
|
|
6519
|
+
candidateCwd: binding.cwd,
|
|
6520
|
+
startCwd: canonicalStartCwd,
|
|
6521
|
+
allowedRoots: binding.allowedRoots
|
|
6522
|
+
});
|
|
6523
|
+
if (canonicalWorkspaceCwd !== binding.cwd) {
|
|
6524
|
+
throw new Error("previous workspace no longer resolves to its captured canonical directory");
|
|
6525
|
+
}
|
|
6228
6526
|
}
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
return {
|
|
6234
|
-
changed: true,
|
|
6235
|
-
restartedStep,
|
|
6236
|
-
run: withRunUpdate(reconciledRun, {
|
|
6237
|
-
workflowDigest: workflow.digest,
|
|
6238
|
-
status: "paused",
|
|
6239
|
-
currentStepId: restartedStep,
|
|
6240
|
-
currentStepDigest: workflow.stepDigests[restartedStep] ?? "",
|
|
6241
|
-
history: retainedHistory,
|
|
6242
|
-
currentStepAttempts: changedEntry.attempts,
|
|
6243
|
-
currentStepOmittedAttempts: changedEntry.omittedAttempts,
|
|
6244
|
-
visits: rebuildVisits(retainedHistory, restartedStep),
|
|
6245
|
-
cwd: retainedWorkspaceCwd(reconciledRun, retainedHistory),
|
|
6246
|
-
reviewedArtifact: reviewedApproval?.artifact ?? "",
|
|
6247
|
-
reviewedFeedback: reviewedApproval?.feedback ?? "",
|
|
6248
|
-
stepHandoff,
|
|
6249
|
-
lastSummary: stepHandoff,
|
|
6250
|
-
pendingGate: undefined,
|
|
6251
|
-
pausedFrom: "running",
|
|
6252
|
-
failedStepId: undefined,
|
|
6253
|
-
pauseReason: `Configuration changed; restarted step "${restartedStep}"`,
|
|
6254
|
-
gateFeedback: ""
|
|
6255
|
-
}, now)
|
|
6256
|
-
};
|
|
6527
|
+
this.run = restartRun(workflow, completedRun, input.trim() || completedRun.input, this.pi.getActiveTools(), this.dependencies.now());
|
|
6528
|
+
} catch (error) {
|
|
6529
|
+
context.ui.notify(`Cannot restart workflow on the same worktree: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
6530
|
+
return;
|
|
6257
6531
|
}
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
}
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
}, now)
|
|
6276
|
-
};
|
|
6277
|
-
};
|
|
6532
|
+
this.persist();
|
|
6533
|
+
this.isolateMainSessionTools();
|
|
6534
|
+
this.updateStatus();
|
|
6535
|
+
this.launchCurrentStep(workflow);
|
|
6536
|
+
}
|
|
6537
|
+
async function reloadNow(context) {
|
|
6538
|
+
if (this.run && (this.run.status === "running" || this.run.status === "awaiting-gate")) {
|
|
6539
|
+
context.ui.notify("Pause the workflow before reloading its configuration", "warning");
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6542
|
+
this.captureSkills(context.getSystemPromptOptions().skills);
|
|
6543
|
+
await this.reloadCatalog(context, true);
|
|
6544
|
+
}
|
|
6545
|
+
function createStartActions() {
|
|
6546
|
+
return { listWorkflows, doctorWorkflows, startNow, restartNow, reloadNow };
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6278
6549
|
// src/harness/step-reporting.ts
|
|
6279
6550
|
var WORKFLOW_STEP_SUMMARY_MESSAGE_TYPE = "workflow-step-summary";
|
|
6280
6551
|
var MAX_POSTED_STEP_SUMMARY_CHARS = 4000;
|
|
@@ -6711,6 +6982,17 @@ function buildDelegatedHandoffSection(handoff) {
|
|
|
6711
6982
|
""
|
|
6712
6983
|
];
|
|
6713
6984
|
}
|
|
6985
|
+
function buildRestartWorkspaceSection(workspaceCwd) {
|
|
6986
|
+
if (!workspaceCwd)
|
|
6987
|
+
return [];
|
|
6988
|
+
return [
|
|
6989
|
+
"## Restart workspace constraint",
|
|
6990
|
+
"",
|
|
6991
|
+
`This iteration must reuse and rebind exactly this existing workspace: ${workspaceCwd}`,
|
|
6992
|
+
"Do not create or substitute another workspace. If it cannot be safely reused, complete with a configured non-binding outcome that pauses the workflow.",
|
|
6993
|
+
""
|
|
6994
|
+
];
|
|
6995
|
+
}
|
|
6714
6996
|
function buildDelegatedCompletionInstructions() {
|
|
6715
6997
|
return [
|
|
6716
6998
|
"This child is non-interactive. Never call `contact_supervisor`, `subagent_supervisor`, or `intercom`.",
|
|
@@ -6732,7 +7014,7 @@ function currentStepHandoff(run) {
|
|
|
6732
7014
|
"Incoming previous-step handoff:",
|
|
6733
7015
|
incomingHandoff,
|
|
6734
7016
|
"",
|
|
6735
|
-
"Latest
|
|
7017
|
+
"Latest current-step summary:",
|
|
6736
7018
|
run.lastSummary
|
|
6737
7019
|
].join(`
|
|
6738
7020
|
`);
|
|
@@ -6751,14 +7033,17 @@ function createTemplateValues({
|
|
|
6751
7033
|
return {
|
|
6752
7034
|
"workflow.input": run.input,
|
|
6753
7035
|
"workflow.id": workflow.definition.id,
|
|
7036
|
+
"workflow.iteration": String(run.iteration ?? 1),
|
|
6754
7037
|
"run.id": run.runId,
|
|
6755
7038
|
"step.id": run.currentStepId,
|
|
6756
7039
|
"step.title": step.title,
|
|
6757
7040
|
"last.summary": currentStepHandoff(run),
|
|
6758
7041
|
"reviewed.artifact": run.reviewedArtifact ?? "",
|
|
6759
7042
|
"reviewed.feedback": run.reviewedFeedback ?? "",
|
|
7043
|
+
"gate.artifact": run.gateArtifact ?? "",
|
|
6760
7044
|
"gate.feedback": run.gateFeedback,
|
|
6761
|
-
"resume.input": run.resumeInput ?? ""
|
|
7045
|
+
"resume.input": run.resumeInput ?? "",
|
|
7046
|
+
"restart.workspace": run.restartWorkspaceCwd ?? ""
|
|
6762
7047
|
};
|
|
6763
7048
|
}
|
|
6764
7049
|
|
|
@@ -6822,6 +7107,7 @@ function buildStepTask(options) {
|
|
|
6822
7107
|
"",
|
|
6823
7108
|
`Workflow: ${workflow.definition.id}`,
|
|
6824
7109
|
`Run: ${run.runId}`,
|
|
7110
|
+
`Iteration: ${run.iteration ?? 1}`,
|
|
6825
7111
|
`Step: ${run.currentStepId} (${step.title})`,
|
|
6826
7112
|
...isDelegated ? [
|
|
6827
7113
|
`Agent profile: ${step.subagent?.agent ?? "generalist"}`,
|
|
@@ -6833,6 +7119,7 @@ function buildStepTask(options) {
|
|
|
6833
7119
|
prompt,
|
|
6834
7120
|
"",
|
|
6835
7121
|
...isDelegated ? buildDelegatedHandoffSection(handoff) : [],
|
|
7122
|
+
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
6836
7123
|
...buildResumeInputSection(run, RESUME_INPUT_PLACEHOLDER.test(promptTemplate)),
|
|
6837
7124
|
...buildResourceSection({ execution, step }),
|
|
6838
7125
|
"## Completion contract",
|
|
@@ -6931,9 +7218,17 @@ function registerLifecycle() {
|
|
|
6931
7218
|
this.restoreFromSession(context);
|
|
6932
7219
|
this.isSessionActive = true;
|
|
6933
7220
|
});
|
|
6934
|
-
this.pi.on("session_shutdown", async () => {
|
|
7221
|
+
this.pi.on("session_shutdown", async (_event, context) => {
|
|
6935
7222
|
this.sessionEpoch += 1;
|
|
6936
7223
|
this.isSessionActive = false;
|
|
7224
|
+
if (this.run) {
|
|
7225
|
+
this.latestContext = context;
|
|
7226
|
+
try {
|
|
7227
|
+
this.persist();
|
|
7228
|
+
} catch (error) {
|
|
7229
|
+
context.ui.notify(`Workflow checkpoint could not be saved before shutdown: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
6937
7232
|
this.cancelPromptReview();
|
|
6938
7233
|
this.mainSteps.deactivate();
|
|
6939
7234
|
await this.cancelActiveDelegation("Pi session shut down");
|
|
@@ -8030,6 +8325,9 @@ function enqueueMutation(context, operation) {
|
|
|
8030
8325
|
function persist() {
|
|
8031
8326
|
if (this.run) {
|
|
8032
8327
|
this.pi.appendEntry(STATE_ENTRY_TYPE, structuredClone(this.run));
|
|
8328
|
+
const session = this.latestContext?.sessionManager;
|
|
8329
|
+
if (session)
|
|
8330
|
+
this.dependencies.flushUnwrittenSession(session);
|
|
8033
8331
|
}
|
|
8034
8332
|
}
|
|
8035
8333
|
function restoreFromSession(context) {
|
|
@@ -8164,6 +8462,7 @@ class WorkflowHarness {
|
|
|
8164
8462
|
listWorkflows = START_ACTIONS.listWorkflows;
|
|
8165
8463
|
doctorWorkflows = START_ACTIONS.doctorWorkflows;
|
|
8166
8464
|
startNow = START_ACTIONS.startNow;
|
|
8465
|
+
restartNow = START_ACTIONS.restartNow;
|
|
8167
8466
|
reloadNow = START_ACTIONS.reloadNow;
|
|
8168
8467
|
pauseNow = PAUSE_ACTIONS.pauseNow;
|
|
8169
8468
|
abortNow = PAUSE_ACTIONS.abortNow;
|
|
@@ -8238,6 +8537,13 @@ class WorkflowHarness {
|
|
|
8238
8537
|
waitForIdle: () => context.waitForIdle()
|
|
8239
8538
|
}, sessionEpoch));
|
|
8240
8539
|
}
|
|
8540
|
+
restart(input, context) {
|
|
8541
|
+
return this.enqueueMutation(context, (sessionEpoch) => this.restartNow(input, {
|
|
8542
|
+
context,
|
|
8543
|
+
skills: () => context.getSystemPromptOptions().skills,
|
|
8544
|
+
waitForIdle: () => context.waitForIdle()
|
|
8545
|
+
}, sessionEpoch));
|
|
8546
|
+
}
|
|
8241
8547
|
pause(reason, context) {
|
|
8242
8548
|
return this.enqueueMutation(context, () => this.pauseNow(reason, context));
|
|
8243
8549
|
}
|
|
@@ -8293,14 +8599,14 @@ var parseChildStructuredResult = ({
|
|
|
8293
8599
|
// src/integrations/subagents/child-runtime-dependencies.ts
|
|
8294
8600
|
import { randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
8295
8601
|
import {
|
|
8296
|
-
existsSync,
|
|
8602
|
+
existsSync as existsSync2,
|
|
8297
8603
|
lstatSync,
|
|
8298
8604
|
readFileSync,
|
|
8299
8605
|
realpathSync as realpathSync2,
|
|
8300
8606
|
renameSync,
|
|
8301
8607
|
statSync as statSync2,
|
|
8302
8608
|
unlinkSync,
|
|
8303
|
-
writeFileSync as
|
|
8609
|
+
writeFileSync as writeFileSync3
|
|
8304
8610
|
} from "node:fs";
|
|
8305
8611
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8306
8612
|
var tokensAreEqual = (actual, expected) => {
|
|
@@ -8310,7 +8616,7 @@ var tokensAreEqual = (actual, expected) => {
|
|
|
8310
8616
|
};
|
|
8311
8617
|
var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
8312
8618
|
fileSystem: {
|
|
8313
|
-
exists:
|
|
8619
|
+
exists: existsSync2,
|
|
8314
8620
|
inspect: lstatSync,
|
|
8315
8621
|
readText: (path) => readFileSync(path, "utf8"),
|
|
8316
8622
|
realPath: realpathSync2,
|
|
@@ -8318,7 +8624,7 @@ var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
8318
8624
|
stat: statSync2,
|
|
8319
8625
|
unlink: unlinkSync,
|
|
8320
8626
|
writeExclusive: (path, content) => {
|
|
8321
|
-
|
|
8627
|
+
writeFileSync3(path, content, {
|
|
8322
8628
|
encoding: "utf8",
|
|
8323
8629
|
flag: "wx",
|
|
8324
8630
|
mode: 384
|