@wichayutdew/pi-workflows 2.1.0 → 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 -983
- package/dist/index.js +995 -756
- package/examples/starter-kit/mr-review.workflow.yaml +3 -1
- package/examples/starter-kit/steps/mr-review/publish.md +9 -2
- package/examples/starter-kit/steps/mr-review/verify.md +16 -5
- 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/validation/prompt.ts +2 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/run-advance.ts +16 -0
- package/src/engine/run-lifecycle.ts +69 -0
- package/src/engine/run-validation.ts +16 -0
- package/src/engine/state-types.ts +7 -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 +2 -0
- package/src/workflow-status/render-summary.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -133,6 +133,7 @@ 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",
|
|
@@ -141,7 +142,8 @@ var PROMPT_VARIABLES = new Set([
|
|
|
141
142
|
"reviewed.feedback",
|
|
142
143
|
"gate.artifact",
|
|
143
144
|
"gate.feedback",
|
|
144
|
-
"resume.input"
|
|
145
|
+
"resume.input",
|
|
146
|
+
"restart.workspace"
|
|
145
147
|
]);
|
|
146
148
|
function validatePromptText(text, path) {
|
|
147
149
|
return [...text.matchAll(/\{\{([^{}]+)\}\}/g)].flatMap((match) => {
|
|
@@ -661,6 +663,7 @@ var HARNESS_COMMAND_NAMES = [
|
|
|
661
663
|
"workflow-list",
|
|
662
664
|
"workflow-pause",
|
|
663
665
|
"workflow-reload",
|
|
666
|
+
"workflow-restart",
|
|
664
667
|
"workflow-resume",
|
|
665
668
|
"workflow-start"
|
|
666
669
|
];
|
|
@@ -1343,6 +1346,13 @@ function createHarnessCommands(controller) {
|
|
|
1343
1346
|
}
|
|
1344
1347
|
},
|
|
1345
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
|
+
},
|
|
1346
1356
|
{
|
|
1347
1357
|
name: "workflow-pause",
|
|
1348
1358
|
options: {
|
|
@@ -2947,7 +2957,7 @@ function parseAvailableSkills(systemPrompt) {
|
|
|
2947
2957
|
|
|
2948
2958
|
// src/harness/dependencies.ts
|
|
2949
2959
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2950
|
-
import { constants as constants3, mkdtempSync, writeFileSync } from "node:fs";
|
|
2960
|
+
import { constants as constants3, mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2951
2961
|
import { lstat as lstat3, open as open3, rm } from "node:fs/promises";
|
|
2952
2962
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
2953
2963
|
import { join as join5 } from "node:path";
|
|
@@ -4210,6 +4220,7 @@ function renderSummaryLines(theme, snapshot, width) {
|
|
|
4210
4220
|
...keyValueLines(theme, "about", workflow.definition.description, width)
|
|
4211
4221
|
] : [],
|
|
4212
4222
|
...keyValueLines(theme, "run", run.runId, width),
|
|
4223
|
+
...run.iteration && run.iteration > 1 ? [...keyValueLines(theme, "iteration", String(run.iteration), width)] : [],
|
|
4213
4224
|
...keyValueLines(theme, "status", statusLabel(run.status), width, statusColor(run.status)),
|
|
4214
4225
|
...keyValueLines(theme, "current", formatStepName(stepTitle(workflow, run.currentStepId), run.currentStepId), width),
|
|
4215
4226
|
...keyValueLines(theme, "visit", String(Math.max(1, run.visits[run.currentStepId] ?? 1)), width),
|
|
@@ -4474,10 +4485,11 @@ import { lstat as lstat2, open as open2, realpath as realpath3 } from "node:fs/p
|
|
|
4474
4485
|
import { isAbsolute as isAbsolute9, relative as relative5, resolve as resolve9, sep as sep4 } from "node:path";
|
|
4475
4486
|
|
|
4476
4487
|
// src/engine/create-run.ts
|
|
4477
|
-
var createRun = (workflow, input, baselineTools, runId, now, cwd) => {
|
|
4488
|
+
var createRun = (workflow, input, baselineTools, runId, now, cwd, iteration = 1) => {
|
|
4478
4489
|
const startStepId = workflow.definition.start;
|
|
4479
4490
|
return {
|
|
4480
4491
|
stateVersion: RUN_STATE_VERSION,
|
|
4492
|
+
iteration,
|
|
4481
4493
|
runId,
|
|
4482
4494
|
workflowId: workflow.definition.id,
|
|
4483
4495
|
workflowDigest: workflow.digest,
|
|
@@ -4870,6 +4882,7 @@ var isPendingGate = (value) => isRecord8(value) && (value.provider === "prompt"
|
|
|
4870
4882
|
var isVisitCounts = (value) => isRecord8(value) && Object.values(value).every((count) => typeof count === "number" && Number.isInteger(count) && count >= 0);
|
|
4871
4883
|
var isOptionalString = (value) => value === undefined || typeof value === "string";
|
|
4872
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;
|
|
4873
4886
|
var isWorkflowRunStatus = (value) => value === "running" || value === "paused" || value === "awaiting-gate" || value === "completed" || value === "aborted";
|
|
4874
4887
|
var hasValidPauseState = (run) => run.status === "paused" ? run.pausedFrom === "running" || run.pausedFrom === "awaiting-gate" : run.pausedFrom === undefined;
|
|
4875
4888
|
var hasValidFailureState = (run) => run.failedStepId === undefined || run.status === "paused" && run.failedStepId === run.currentStepId;
|
|
@@ -4896,12 +4909,15 @@ var isWorkflowRun = (value) => {
|
|
|
4896
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;
|
|
4897
4910
|
if (!hasValidRequiredFields)
|
|
4898
4911
|
return false;
|
|
4899
|
-
const hasValidOptionalFields = isOptionalString(value.reviewedArtifact) && isOptionalString(value.reviewedFeedback) && (typeof value.reviewedFeedback !== "string" || value.reviewedFeedback.length <= MAX_GATE_FEEDBACK_CHARS) && isOptionalString(value.stepHandoff) && isOptionalString(value.gateArtifact) && 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");
|
|
4900
4913
|
if (!hasValidOptionalFields)
|
|
4901
4914
|
return false;
|
|
4902
4915
|
const pendingGate = value.pendingGate;
|
|
4903
4916
|
if (pendingGate !== undefined && !isPendingGate(pendingGate))
|
|
4904
4917
|
return false;
|
|
4918
|
+
if (value.restartWorkspaceCwd !== undefined && value.history.some((entry) => entry.workspaceCwd !== undefined)) {
|
|
4919
|
+
return false;
|
|
4920
|
+
}
|
|
4905
4921
|
return workflowTraceChars(value) <= MAX_WORKFLOW_TRACE_CHARS && hasValidWorkspaceState(value, value.history) && hasValidPauseState(value) && hasValidFailureState(value) && hasValidGateState(value, pendingGate);
|
|
4906
4922
|
};
|
|
4907
4923
|
// src/workflow-status/transcript-reader.ts
|
|
@@ -5336,6 +5352,42 @@ function resolveWorkspaceDirectory({
|
|
|
5336
5352
|
return canonicalCwd;
|
|
5337
5353
|
}
|
|
5338
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
|
+
|
|
5339
5391
|
// src/harness/dependencies.ts
|
|
5340
5392
|
var MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
5341
5393
|
function createDelegationWorkspace() {
|
|
@@ -5343,7 +5395,7 @@ function createDelegationWorkspace() {
|
|
|
5343
5395
|
const capabilityPath = join5(resultDirectory, "capability");
|
|
5344
5396
|
const capabilityToken = randomBytes(32).toString("hex");
|
|
5345
5397
|
const resultPath = join5(resultDirectory, "result.json");
|
|
5346
|
-
|
|
5398
|
+
writeFileSync2(capabilityPath, capabilityToken, {
|
|
5347
5399
|
encoding: "utf8",
|
|
5348
5400
|
flag: "wx",
|
|
5349
5401
|
mode: 384
|
|
@@ -5400,6 +5452,7 @@ var DEFAULT_DEPENDENCIES3 = {
|
|
|
5400
5452
|
createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
|
|
5401
5453
|
createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
|
|
5402
5454
|
createMutationQueue: createSerialTaskQueue,
|
|
5455
|
+
flushUnwrittenSession,
|
|
5403
5456
|
scheduleInterval: (operation, intervalMs) => setInterval(operation, intervalMs),
|
|
5404
5457
|
cancelInterval: (timer) => {
|
|
5405
5458
|
clearInterval(timer);
|
|
@@ -5516,831 +5569,983 @@ function createStatusActions() {
|
|
|
5516
5569
|
};
|
|
5517
5570
|
}
|
|
5518
5571
|
|
|
5519
|
-
// src/
|
|
5520
|
-
var
|
|
5521
|
-
var
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
var
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
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`);
|
|
5536
5590
|
}
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
const canComplete = new Set(Object.entries(definition.steps).filter(([, step]) => Object.values(step.transitions).includes("$done")).map(([stepId]) => stepId));
|
|
5541
|
-
const reverse = new Map;
|
|
5542
|
-
for (const [source, targets] of Object.entries(adjacency)) {
|
|
5543
|
-
for (const target of targets) {
|
|
5544
|
-
reverse.set(target, [...reverse.get(target) ?? [], source]);
|
|
5545
|
-
}
|
|
5591
|
+
const step = currentStep(workflow, run);
|
|
5592
|
+
if (!step) {
|
|
5593
|
+
throw new Error(`current step "${run.currentStepId}" no longer exists`);
|
|
5546
5594
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
const stepId = pending.pop();
|
|
5550
|
-
if (!stepId)
|
|
5551
|
-
continue;
|
|
5552
|
-
for (const predecessor of (reverse.get(stepId) ?? []).sort(lexical)) {
|
|
5553
|
-
if (canComplete.has(predecessor))
|
|
5554
|
-
continue;
|
|
5555
|
-
canComplete.add(predecessor);
|
|
5556
|
-
pending.push(predecessor);
|
|
5557
|
-
}
|
|
5595
|
+
if (step.gate?.submitOutcome === outcome) {
|
|
5596
|
+
throw new Error(`outcome "${outcome}" must be submitted through the configured gate`);
|
|
5558
5597
|
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
const
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
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}"`);
|
|
5581
5623
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
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);
|
|
5598
5639
|
}
|
|
5599
|
-
|
|
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);
|
|
5600
5673
|
};
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
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)}…`;
|
|
5606
5686
|
};
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
const canComplete = stepsThatCanComplete(definition, adjacency);
|
|
5611
|
-
const issues = [];
|
|
5612
|
-
const stranded = [...reachable].filter((stepId) => !canComplete.has(stepId)).sort(lexical);
|
|
5613
|
-
if (!canComplete.has(definition.start)) {
|
|
5614
|
-
issues.push({
|
|
5615
|
-
level: "error",
|
|
5616
|
-
code: "no-completion-path",
|
|
5617
|
-
steps: [definition.start],
|
|
5618
|
-
message: `start step ${definition.start} cannot reach $done`
|
|
5619
|
-
});
|
|
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`);
|
|
5620
5690
|
}
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
code: "reachable-step-cannot-reach-done",
|
|
5625
|
-
steps: stranded,
|
|
5626
|
-
message: `reachable step${stranded.length === 1 ? "" : "s"} ${stranded.join(", ")} cannot reach $done`
|
|
5627
|
-
});
|
|
5691
|
+
const step = currentStep(workflow, run);
|
|
5692
|
+
if (!step?.gate) {
|
|
5693
|
+
throw new Error(`step "${run.currentStepId}" has no gate`);
|
|
5628
5694
|
}
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
issues.push({
|
|
5632
|
-
level: "warning",
|
|
5633
|
-
code: "unreachable-steps",
|
|
5634
|
-
steps: unreachable,
|
|
5635
|
-
message: `unreachable step${unreachable.length === 1 ? "" : "s"}: ${unreachable.join(", ")}`
|
|
5636
|
-
});
|
|
5695
|
+
if (outcome !== step.gate.submitOutcome) {
|
|
5696
|
+
throw new Error(`gate expects outcome "${step.gate.submitOutcome}"`);
|
|
5637
5697
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
continue;
|
|
5641
|
-
const componentIsReachable = component.some((stepId) => reachable.has(stepId));
|
|
5642
|
-
const componentCanReachDone = component.some((stepId) => canComplete.has(stepId));
|
|
5643
|
-
issues.push({
|
|
5644
|
-
level: "warning",
|
|
5645
|
-
code: "cycle",
|
|
5646
|
-
steps: component,
|
|
5647
|
-
reachable: componentIsReachable,
|
|
5648
|
-
canReachDone: componentCanReachDone,
|
|
5649
|
-
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`
|
|
5650
|
-
});
|
|
5698
|
+
if (!artifact.trim()) {
|
|
5699
|
+
throw new Error("gate submission requires a non-empty artifact");
|
|
5651
5700
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
maxStepVisits: definition.maxStepVisits,
|
|
5655
|
-
reachableSteps: [...reachable].sort(lexical),
|
|
5656
|
-
issues
|
|
5657
|
-
};
|
|
5658
|
-
}
|
|
5659
|
-
var escapeMarkdown = (value) => value.replaceAll("\\", "\\\\").replaceAll("|", "\\|");
|
|
5660
|
-
function formatWorkflowDoctor(reports) {
|
|
5661
|
-
const lines = ["# Workflow doctor", ""];
|
|
5662
|
-
for (const report of reports) {
|
|
5663
|
-
const errors = report.issues.filter((issue) => issue.level === "error");
|
|
5664
|
-
const warnings = report.issues.filter((issue) => issue.level === "warning");
|
|
5665
|
-
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.`, "");
|
|
5666
|
-
if (report.issues.length === 0) {
|
|
5667
|
-
lines.push("- No liveness issues found.", "");
|
|
5668
|
-
continue;
|
|
5669
|
-
}
|
|
5670
|
-
lines.push(...report.issues.map((issue) => `- ${issue.level.toUpperCase()} \`${issue.code}\`: ${escapeMarkdown(issue.message)}`), "");
|
|
5701
|
+
if (!summary.trim()) {
|
|
5702
|
+
throw new Error("gate submission requires a non-empty summary");
|
|
5671
5703
|
}
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
}
|
|
5693
|
-
async function listWorkflows(context) {
|
|
5694
|
-
const workflows = [...this.catalog.workflows.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
|
|
5695
|
-
if (workflows.length === 0) {
|
|
5696
|
-
context.ui.notify(`No workflows loaded from ${this.catalog.userDirectory}`, this.catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
5697
|
-
return;
|
|
5704
|
+
if (!requestId)
|
|
5705
|
+
throw new Error("gate submission requires a request id");
|
|
5706
|
+
return withRunUpdate(run, {
|
|
5707
|
+
status: "awaiting-gate",
|
|
5708
|
+
pendingGate: {
|
|
5709
|
+
provider: step.gate.provider,
|
|
5710
|
+
requestId,
|
|
5711
|
+
stepId: run.currentStepId,
|
|
5712
|
+
artifact,
|
|
5713
|
+
summary,
|
|
5714
|
+
submittedOutcome: outcome,
|
|
5715
|
+
requestedAt: now
|
|
5716
|
+
}
|
|
5717
|
+
}, now);
|
|
5718
|
+
};
|
|
5719
|
+
var attachGateReviewId = (run, reviewId, now) => {
|
|
5720
|
+
if (!run.pendingGate)
|
|
5721
|
+
throw new Error("workflow has no pending gate");
|
|
5722
|
+
if (run.pendingGate.provider !== "plannotator") {
|
|
5723
|
+
throw new Error("only a Plannotator gate can have a review id");
|
|
5698
5724
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5725
|
+
return withRunUpdate(run, { pendingGate: { ...run.pendingGate, reviewId } }, now);
|
|
5726
|
+
};
|
|
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`);
|
|
5713
5760
|
}
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
5717
|
-
return;
|
|
5761
|
+
if (run.currentStepId !== pendingGate.stepId) {
|
|
5762
|
+
throw new Error("gate result does not match the current step");
|
|
5718
5763
|
}
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
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`);
|
|
5722
5769
|
}
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5770
|
+
const summary = resolution.approved ? pendingGate.summary ?? "" : gateRejectionSummary(feedback);
|
|
5771
|
+
const decidedRun = recordCurrentGateDecision(run, {
|
|
5772
|
+
provider: pendingGate.provider,
|
|
5773
|
+
requestId: pendingGate.requestId,
|
|
5774
|
+
approved: resolution.approved,
|
|
5775
|
+
feedback,
|
|
5776
|
+
resolvedAt: resolution.resolvedAt,
|
|
5777
|
+
...pendingGate.reviewId ? { reviewId: pendingGate.reviewId } : {}
|
|
5778
|
+
}, now);
|
|
5779
|
+
const runnableRun = withRunUpdate(decidedRun, {
|
|
5780
|
+
status: "running",
|
|
5781
|
+
pendingGate: undefined,
|
|
5782
|
+
pausedFrom: undefined,
|
|
5783
|
+
pauseReason: undefined,
|
|
5784
|
+
gateArtifact: resolution.approved ? "" : pendingGate.artifact,
|
|
5785
|
+
gateFeedback: resolution.approved ? "" : feedback
|
|
5786
|
+
}, now);
|
|
5787
|
+
const isSameStepHumanRevision = !resolution.approved && step.transitions[outcome] === pendingGate.stepId;
|
|
5788
|
+
const advanced = advanceRun(workflow, runnableRun, outcome, summary, now, {}, {
|
|
5789
|
+
sameStepHumanGateRevision: isSameStepHumanRevision
|
|
5727
5790
|
});
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5791
|
+
const completedApprovedGate = resolution.approved && advanced.history.length > runnableRun.history.length;
|
|
5792
|
+
const history = completedApprovedGate ? advanced.history.map((entry, index) => index === advanced.history.length - 1 ? {
|
|
5793
|
+
...entry,
|
|
5794
|
+
artifact: pendingGate.artifact,
|
|
5795
|
+
approval: {
|
|
5796
|
+
requestId: pendingGate.requestId,
|
|
5797
|
+
artifact: pendingGate.artifact,
|
|
5798
|
+
feedback,
|
|
5799
|
+
stepStructuralDigest
|
|
5800
|
+
}
|
|
5801
|
+
} : entry) : advanced.history;
|
|
5802
|
+
return {
|
|
5803
|
+
...advanced,
|
|
5804
|
+
history,
|
|
5805
|
+
...completedApprovedGate ? {
|
|
5806
|
+
reviewedArtifact: pendingGate.artifact,
|
|
5807
|
+
reviewedFeedback: feedback
|
|
5808
|
+
} : {},
|
|
5809
|
+
gateArtifact: resolution.approved ? "" : pendingGate.artifact,
|
|
5810
|
+
gateFeedback: resolution.approved ? "" : feedback
|
|
5811
|
+
};
|
|
5812
|
+
};
|
|
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;
|
|
5738
5819
|
}
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5820
|
+
return;
|
|
5821
|
+
};
|
|
5822
|
+
var allowedOutcomes = (workflow, run) => {
|
|
5823
|
+
const step = currentStep(workflow, run);
|
|
5824
|
+
if (!step)
|
|
5825
|
+
return [];
|
|
5826
|
+
const gateResolutionOutcomes = step.gate ? new Set([step.gate.approvedOutcome, step.gate.rejectedOutcome]) : undefined;
|
|
5827
|
+
return [
|
|
5828
|
+
...Object.keys(step.transitions).filter((outcome) => !gateResolutionOutcomes?.has(outcome)),
|
|
5829
|
+
...step.gate ? [step.gate.submitOutcome] : []
|
|
5830
|
+
];
|
|
5831
|
+
};
|
|
5832
|
+
var pauseRun = (run, reason, now) => {
|
|
5833
|
+
if (run.status !== "running" && run.status !== "awaiting-gate") {
|
|
5834
|
+
return withRunUpdate(run, { pauseReason: reason || run.pauseReason }, now);
|
|
5742
5835
|
}
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5836
|
+
return withRunUpdate(run, {
|
|
5837
|
+
status: "paused",
|
|
5838
|
+
pausedFrom: run.status,
|
|
5839
|
+
pauseReason: reason || `Paused during step "${run.currentStepId}"`,
|
|
5840
|
+
failedStepId: undefined
|
|
5841
|
+
}, now);
|
|
5842
|
+
};
|
|
5843
|
+
var failRun = (run, reason, now) => {
|
|
5844
|
+
const pausedRun = pauseRun(run, reason, now);
|
|
5845
|
+
return pausedRun.status === "paused" ? { ...pausedRun, failedStepId: pausedRun.currentStepId } : pausedRun;
|
|
5846
|
+
};
|
|
5847
|
+
var resumeRun = (run, now) => {
|
|
5848
|
+
if (run.status !== "paused")
|
|
5849
|
+
return run;
|
|
5850
|
+
return withRunUpdate(run, {
|
|
5851
|
+
status: run.pausedFrom ?? (run.pendingGate ? "awaiting-gate" : "running"),
|
|
5852
|
+
pauseReason: undefined,
|
|
5853
|
+
pausedFrom: undefined,
|
|
5854
|
+
failedStepId: undefined
|
|
5855
|
+
}, now);
|
|
5856
|
+
};
|
|
5857
|
+
var setResumeInput = (run, input, now) => withRunUpdate(run, { resumeInput: input.trim() || undefined }, now);
|
|
5858
|
+
var abortRun = (run, reason, now) => withRunUpdate(run, {
|
|
5859
|
+
status: "aborted",
|
|
5860
|
+
pauseReason: reason || "Aborted by user",
|
|
5861
|
+
pausedFrom: undefined,
|
|
5862
|
+
failedStepId: undefined,
|
|
5863
|
+
pendingGate: undefined,
|
|
5864
|
+
resumeInput: undefined
|
|
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");
|
|
5746
5869
|
}
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
context.ui.notify("Workflow start was superseded by a newer configuration load", "warning");
|
|
5750
|
-
return;
|
|
5870
|
+
if (!run.startCwd) {
|
|
5871
|
+
throw new Error("the completed workflow has no captured start directory; start a new workflow instead");
|
|
5751
5872
|
}
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
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");
|
|
5755
5876
|
}
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
5759
|
-
return;
|
|
5877
|
+
if (previousIteration >= Number.MAX_SAFE_INTEGER) {
|
|
5878
|
+
throw new Error("the workflow iteration limit has been reached");
|
|
5760
5879
|
}
|
|
5761
|
-
const
|
|
5762
|
-
if (
|
|
5763
|
-
|
|
5764
|
-
${livenessErrors.map((issue) => issue.message).join(`
|
|
5765
|
-
`)}`, "error");
|
|
5766
|
-
return;
|
|
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");
|
|
5767
5883
|
}
|
|
5768
|
-
const
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
}
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
this.run = createRun(workflow, input.trim(), this.pi.getActiveTools(), this.dependencies.createRequestId(), this.dependencies.now(), canonicalStartCwd);
|
|
5787
|
-
this.persist();
|
|
5788
|
-
this.isolateMainSessionTools();
|
|
5789
|
-
this.updateStatus();
|
|
5790
|
-
this.launchCurrentStep(workflow);
|
|
5791
|
-
}
|
|
5792
|
-
async function reloadNow(context) {
|
|
5793
|
-
if (this.run && (this.run.status === "running" || this.run.status === "awaiting-gate")) {
|
|
5794
|
-
context.ui.notify("Pause the workflow before reloading its configuration", "warning");
|
|
5795
|
-
return;
|
|
5796
|
-
}
|
|
5797
|
-
this.captureSkills(context.getSystemPromptOptions().skills);
|
|
5798
|
-
await this.reloadCatalog(context, true);
|
|
5799
|
-
}
|
|
5800
|
-
function createStartActions() {
|
|
5801
|
-
return { listWorkflows, doctorWorkflows, startNow, reloadNow };
|
|
5802
|
-
}
|
|
5803
|
-
|
|
5804
|
-
// src/engine/transition-helpers.ts
|
|
5805
|
-
var currentStep = (workflow, run) => workflow.definition.steps[run.currentStepId];
|
|
5806
|
-
var withRunUpdate = (run, changes, now) => ({ ...run, ...changes, updatedAt: now });
|
|
5807
|
-
|
|
5808
|
-
// src/engine/run-advance.ts
|
|
5809
|
-
var completedStep = (run, outcome, summary, now, effects) => ({
|
|
5810
|
-
stepId: run.currentStepId,
|
|
5811
|
-
stepDigest: run.currentStepDigest,
|
|
5812
|
-
outcome,
|
|
5813
|
-
summary,
|
|
5814
|
-
...effects.workspaceCwd ? { workspaceCwd: effects.workspaceCwd } : {},
|
|
5815
|
-
...run.currentStepAttempts?.length ? { attempts: run.currentStepAttempts } : {},
|
|
5816
|
-
...run.currentStepOmittedAttempts ? { omittedAttempts: run.currentStepOmittedAttempts } : {},
|
|
5817
|
-
completedAt: now
|
|
5818
|
-
});
|
|
5819
|
-
var advanceRun = (workflow, run, outcome, summary, now, effects = {}, options = {}) => {
|
|
5820
|
-
if (run.status !== "running") {
|
|
5821
|
-
throw new Error(`workflow is ${run.status}; only a running workflow can advance`);
|
|
5822
|
-
}
|
|
5823
|
-
const step = currentStep(workflow, run);
|
|
5824
|
-
if (!step) {
|
|
5825
|
-
throw new Error(`current step "${run.currentStepId}" no longer exists`);
|
|
5826
|
-
}
|
|
5827
|
-
if (step.gate?.submitOutcome === outcome) {
|
|
5828
|
-
throw new Error(`outcome "${outcome}" must be submitted through the configured gate`);
|
|
5829
|
-
}
|
|
5830
|
-
const target = step.transitions[outcome];
|
|
5831
|
-
if (!target) {
|
|
5832
|
-
throw new Error(`outcome "${outcome}" is not valid for step "${run.currentStepId}"`);
|
|
5833
|
-
}
|
|
5834
|
-
const shouldBindWorkspace = step.workspace?.bindOn.includes(outcome) ?? false;
|
|
5835
|
-
if (shouldBindWorkspace !== Boolean(effects.workspaceCwd)) {
|
|
5836
|
-
throw new Error(shouldBindWorkspace ? `outcome "${outcome}" requires a validated workspace binding` : `outcome "${outcome}" cannot bind a workspace`);
|
|
5837
|
-
}
|
|
5838
|
-
if (target === "$pause") {
|
|
5839
|
-
return withRunUpdate(run, {
|
|
5840
|
-
status: "paused",
|
|
5841
|
-
pausedFrom: "running",
|
|
5842
|
-
pauseReason: summary || `Step "${run.currentStepId}" requested a pause`,
|
|
5843
|
-
lastSummary: summary,
|
|
5844
|
-
resumeInput: undefined
|
|
5845
|
-
}, now);
|
|
5846
|
-
}
|
|
5847
|
-
const completed = completedStep(run, outcome, summary, now, effects);
|
|
5848
|
-
const cwd = effects.workspaceCwd ?? run.cwd;
|
|
5849
|
-
if (target === "$done") {
|
|
5850
|
-
return withRunUpdate(run, {
|
|
5851
|
-
status: "completed",
|
|
5852
|
-
history: [...run.history, completed],
|
|
5853
|
-
currentStepAttempts: undefined,
|
|
5854
|
-
currentStepOmittedAttempts: undefined,
|
|
5855
|
-
...cwd ? { cwd } : {},
|
|
5856
|
-
stepHandoff: summary,
|
|
5857
|
-
lastSummary: summary,
|
|
5858
|
-
gateArtifact: "",
|
|
5859
|
-
gateFeedback: "",
|
|
5860
|
-
pausedFrom: undefined,
|
|
5861
|
-
pendingGate: undefined,
|
|
5862
|
-
resumeInput: undefined
|
|
5863
|
-
}, now);
|
|
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
|
+
};
|
|
5892
|
+
// src/engine/reconciliation-history.ts
|
|
5893
|
+
var isApprovedGateEntry = (workflow, entry) => {
|
|
5894
|
+
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
5895
|
+
return gate !== undefined && entry.outcome === gate.approvedOutcome;
|
|
5896
|
+
};
|
|
5897
|
+
var latestApprovedGateEntry = (workflow, history) => {
|
|
5898
|
+
for (let index = history.length - 1;index >= 0; index -= 1) {
|
|
5899
|
+
const entry = history[index];
|
|
5900
|
+
if (entry && isApprovedGateEntry(workflow, entry))
|
|
5901
|
+
return { entry, index };
|
|
5864
5902
|
}
|
|
5865
|
-
|
|
5866
|
-
|
|
5903
|
+
return;
|
|
5904
|
+
};
|
|
5905
|
+
var rebuildVisits = (history, currentStepId) => {
|
|
5906
|
+
const visitedStepIds = [
|
|
5907
|
+
...history.map((entry) => entry.stepId),
|
|
5908
|
+
currentStepId
|
|
5909
|
+
];
|
|
5910
|
+
return visitedStepIds.reduce((visits, stepId) => ({
|
|
5911
|
+
...visits,
|
|
5912
|
+
[stepId]: (visits[stepId] ?? 0) + 1
|
|
5913
|
+
}), {});
|
|
5914
|
+
};
|
|
5915
|
+
var retainedWorkspaceCwd = (run, history) => {
|
|
5916
|
+
for (let index = history.length - 1;index >= 0; index -= 1) {
|
|
5917
|
+
const cwd = history[index]?.workspaceCwd;
|
|
5918
|
+
if (cwd)
|
|
5919
|
+
return cwd;
|
|
5867
5920
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
const
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
currentStepDigest: workflow.stepDigests[target] ?? "",
|
|
5886
|
-
visits: { ...run.visits, [target]: nextVisitCount },
|
|
5887
|
-
history: [...run.history, completed],
|
|
5888
|
-
currentStepAttempts: undefined,
|
|
5889
|
-
currentStepOmittedAttempts: undefined,
|
|
5890
|
-
...cwd ? { cwd } : {},
|
|
5891
|
-
stepHandoff: preservesGateRevisionContext ? run.stepHandoff : summary,
|
|
5892
|
-
lastSummary: summary,
|
|
5893
|
-
gateArtifact: preservesGateRevisionContext ? run.gateArtifact : "",
|
|
5894
|
-
gateFeedback: preservesGateRevisionContext ? run.gateFeedback : "",
|
|
5895
|
-
resumeInput: undefined
|
|
5896
|
-
}, now);
|
|
5921
|
+
return run.startCwd ?? run.cwd;
|
|
5922
|
+
};
|
|
5923
|
+
var retainedReviewedApproval = (workflow, history) => {
|
|
5924
|
+
const retainedApproval = latestApprovedGateEntry(workflow, history);
|
|
5925
|
+
const approval = retainedApproval?.entry.approval;
|
|
5926
|
+
return approval ? { artifact: approval.artifact, feedback: approval.feedback } : undefined;
|
|
5927
|
+
};
|
|
5928
|
+
var refreshApprovedGateHistory = (run, workflow) => {
|
|
5929
|
+
const history = run.history.map((entry) => {
|
|
5930
|
+
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
5931
|
+
const currentDigest = workflow.stepDigests[entry.stepId];
|
|
5932
|
+
const currentStructuralDigest = workflow.stepStructuralDigests[entry.stepId];
|
|
5933
|
+
const shouldRefresh = gate !== undefined && typeof currentDigest === "string" && currentDigest.length > 0 && typeof currentStructuralDigest === "string" && currentStructuralDigest.length > 0 && entry.outcome === gate.approvedOutcome && entry.approval?.stepStructuralDigest === currentStructuralDigest && entry.stepDigest !== currentDigest;
|
|
5934
|
+
return shouldRefresh ? { ...entry, stepDigest: currentDigest } : entry;
|
|
5935
|
+
});
|
|
5936
|
+
const hasChanged = history.some((entry, index) => entry !== run.history[index]);
|
|
5937
|
+
return hasChanged ? { ...run, history } : run;
|
|
5897
5938
|
};
|
|
5898
5939
|
|
|
5899
|
-
// src/engine/
|
|
5900
|
-
var
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
var gateRejectionSummary = (feedback) => {
|
|
5905
|
-
const compact = feedback.trim().replace(/\s+/g, " ");
|
|
5906
|
-
if (!compact)
|
|
5907
|
-
return "Gate rejected";
|
|
5908
|
-
const summary = `Gate rejected: ${compact}`;
|
|
5909
|
-
return summary.length <= MAX_GATE_REJECTION_SUMMARY_CHARS ? summary : `${summary.slice(0, MAX_GATE_REJECTION_SUMMARY_CHARS - 1)}…`;
|
|
5940
|
+
// src/engine/run-workflow-validation.ts
|
|
5941
|
+
var sameVisitCounts = (actual, expected) => {
|
|
5942
|
+
const actualEntries = Object.entries(actual).sort(([left], [right]) => left.localeCompare(right));
|
|
5943
|
+
const expectedEntries = Object.entries(expected).sort(([left], [right]) => left.localeCompare(right));
|
|
5944
|
+
return actualEntries.length === expectedEntries.length && actualEntries.every(([stepId, count], index) => expectedEntries[index]?.[0] === stepId && expectedEntries[index][1] === count);
|
|
5910
5945
|
};
|
|
5911
|
-
var
|
|
5912
|
-
|
|
5913
|
-
|
|
5946
|
+
var validateHistoryMetadata = (workflow, step, entry) => {
|
|
5947
|
+
const shouldBind = step.workspace?.bindOn.includes(entry.outcome) === true;
|
|
5948
|
+
if (shouldBind !== (entry.workspaceCwd !== undefined)) {
|
|
5949
|
+
return shouldBind ? `history step "${entry.stepId}" is missing its workspace binding` : `history step "${entry.stepId}" has an unauthorized workspace binding`;
|
|
5914
5950
|
}
|
|
5915
|
-
const
|
|
5916
|
-
if (!
|
|
5917
|
-
|
|
5951
|
+
const isApprovedGate = step.gate !== undefined && entry.outcome === step.gate.approvedOutcome;
|
|
5952
|
+
if (isApprovedGate && !entry.approval) {
|
|
5953
|
+
return `history step "${entry.stepId}" is missing authoritative gate approval`;
|
|
5918
5954
|
}
|
|
5919
|
-
if (
|
|
5920
|
-
|
|
5955
|
+
if (isApprovedGate && entry.artifact !== entry.approval?.artifact) {
|
|
5956
|
+
return `history step "${entry.stepId}" approval artifact is inconsistent`;
|
|
5921
5957
|
}
|
|
5922
|
-
if (!artifact
|
|
5923
|
-
|
|
5958
|
+
if (!isApprovedGate && (entry.approval || entry.artifact !== undefined)) {
|
|
5959
|
+
return `history step "${entry.stepId}" has approval data for a non-approved outcome`;
|
|
5924
5960
|
}
|
|
5925
|
-
if (
|
|
5926
|
-
|
|
5961
|
+
if (isApprovedGate && entry.approval?.stepStructuralDigest !== workflow.stepStructuralDigests[entry.stepId]) {
|
|
5962
|
+
return `history step "${entry.stepId}" approval does not match its configured structure`;
|
|
5927
5963
|
}
|
|
5928
|
-
|
|
5929
|
-
throw new Error("gate submission requires a request id");
|
|
5930
|
-
return withRunUpdate(run, {
|
|
5931
|
-
status: "awaiting-gate",
|
|
5932
|
-
pendingGate: {
|
|
5933
|
-
provider: step.gate.provider,
|
|
5934
|
-
requestId,
|
|
5935
|
-
stepId: run.currentStepId,
|
|
5936
|
-
artifact,
|
|
5937
|
-
summary,
|
|
5938
|
-
submittedOutcome: outcome,
|
|
5939
|
-
requestedAt: now
|
|
5940
|
-
}
|
|
5941
|
-
}, now);
|
|
5964
|
+
return;
|
|
5942
5965
|
};
|
|
5943
|
-
var
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5966
|
+
var validatePendingGate = (run, step) => {
|
|
5967
|
+
const pending = run.pendingGate;
|
|
5968
|
+
if (!pending)
|
|
5969
|
+
return;
|
|
5970
|
+
if (!step.gate || pending.stepId !== run.currentStepId || pending.provider !== step.gate.provider || pending.submittedOutcome !== step.gate.submitOutcome) {
|
|
5971
|
+
return "pending gate does not match the current workflow step";
|
|
5948
5972
|
}
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
return run;
|
|
5954
|
-
return withRunUpdate(run, {
|
|
5955
|
-
status: "running",
|
|
5956
|
-
pendingGate: undefined,
|
|
5957
|
-
gateArtifact: run.pendingGate.artifact,
|
|
5958
|
-
gateFeedback: boundedGateFeedback(reason),
|
|
5959
|
-
pausedFrom: undefined,
|
|
5960
|
-
pauseReason: undefined,
|
|
5961
|
-
failedStepId: undefined
|
|
5962
|
-
}, now);
|
|
5973
|
+
if (pending.provider === "prompt" && pending.reviewId !== undefined) {
|
|
5974
|
+
return "built-in prompt gate cannot carry a Plannotator review id";
|
|
5975
|
+
}
|
|
5976
|
+
return;
|
|
5963
5977
|
};
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5978
|
+
function validateRunWorkflowSemantics(run, workflow) {
|
|
5979
|
+
const sameWorkflowDigest = run.workflowDigest === workflow.digest;
|
|
5980
|
+
let expectedStepId = workflow.definition.start;
|
|
5981
|
+
let reachedDone = false;
|
|
5982
|
+
let boundWorkspaceCwd;
|
|
5983
|
+
let latestApproval;
|
|
5984
|
+
const expectedVisits = { [expectedStepId]: 1 };
|
|
5985
|
+
for (let index = 0;index < run.history.length; index += 1) {
|
|
5986
|
+
const entry = run.history[index];
|
|
5987
|
+
if (!entry)
|
|
5988
|
+
continue;
|
|
5989
|
+
if (reachedDone) {
|
|
5990
|
+
return "workflow history continues after a $done transition";
|
|
5991
|
+
}
|
|
5992
|
+
if (entry.stepId !== expectedStepId) {
|
|
5993
|
+
return `history step "${entry.stepId}" is not reachable after "${expectedStepId}"`;
|
|
5994
|
+
}
|
|
5995
|
+
const step = workflow.definition.steps[entry.stepId];
|
|
5996
|
+
const configuredDigest = workflow.stepDigests[entry.stepId];
|
|
5997
|
+
if (!step || entry.stepDigest !== configuredDigest) {
|
|
5998
|
+
return sameWorkflowDigest ? `history step "${entry.stepId}" does not match the active workflow digest` : undefined;
|
|
5999
|
+
}
|
|
6000
|
+
const metadataError = validateHistoryMetadata(workflow, step, entry);
|
|
6001
|
+
if (metadataError)
|
|
6002
|
+
return metadataError;
|
|
6003
|
+
if (entry.workspaceCwd) {
|
|
6004
|
+
if (boundWorkspaceCwd !== undefined && boundWorkspaceCwd !== entry.workspaceCwd) {
|
|
6005
|
+
return "workflow history attempts to replace an existing workspace binding";
|
|
5973
6006
|
}
|
|
6007
|
+
boundWorkspaceCwd = entry.workspaceCwd;
|
|
5974
6008
|
}
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6009
|
+
if (entry.approval) {
|
|
6010
|
+
latestApproval = {
|
|
6011
|
+
artifact: entry.approval.artifact,
|
|
6012
|
+
feedback: entry.approval.feedback
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
6015
|
+
const target = step.transitions[entry.outcome];
|
|
6016
|
+
if (!target) {
|
|
6017
|
+
return `history outcome "${entry.outcome}" is not configured for step "${entry.stepId}"`;
|
|
6018
|
+
}
|
|
6019
|
+
if (target === "$pause") {
|
|
6020
|
+
return `history step "${entry.stepId}" records a non-completing $pause transition`;
|
|
6021
|
+
}
|
|
6022
|
+
if (target === "$done") {
|
|
6023
|
+
if (index !== run.history.length - 1) {
|
|
6024
|
+
return "workflow history continues after a $done transition";
|
|
6025
|
+
}
|
|
6026
|
+
reachedDone = true;
|
|
6027
|
+
continue;
|
|
6028
|
+
}
|
|
6029
|
+
expectedStepId = target;
|
|
6030
|
+
expectedVisits[target] = (expectedVisits[target] ?? 0) + 1;
|
|
5984
6031
|
}
|
|
5985
|
-
if (run.currentStepId !==
|
|
5986
|
-
|
|
6032
|
+
if (run.currentStepId !== expectedStepId) {
|
|
6033
|
+
return `current step "${run.currentStepId}" does not match reachable step "${expectedStepId}"`;
|
|
5987
6034
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
const stepStructuralDigest = workflow.stepStructuralDigests[pendingGate.stepId] ?? "";
|
|
5991
|
-
if (resolution.approved && !stepStructuralDigest) {
|
|
5992
|
-
throw new Error(`gated step "${pendingGate.stepId}" has no structural digest`);
|
|
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";
|
|
5993
6037
|
}
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
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" };
|
|
6024
6102
|
}
|
|
6025
|
-
|
|
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
|
+
} : {};
|
|
6026
6144
|
return {
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
gateFeedback: resolution.approved ? "" : feedback
|
|
6145
|
+
changed: true,
|
|
6146
|
+
...hasCurrentStepChanged ? { restartedStep: run.currentStepId } : {},
|
|
6147
|
+
run: withRunUpdate(reconciledRun, {
|
|
6148
|
+
...restartChanges,
|
|
6149
|
+
workflowDigest: workflow.digest,
|
|
6150
|
+
currentStepDigest: currentDigest
|
|
6151
|
+
}, now)
|
|
6035
6152
|
};
|
|
6036
6153
|
};
|
|
6037
|
-
// src/
|
|
6038
|
-
var
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
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] ?? []);
|
|
6051
6171
|
}
|
|
6052
|
-
return
|
|
6053
|
-
status: "paused",
|
|
6054
|
-
pausedFrom: run.status,
|
|
6055
|
-
pauseReason: reason || `Paused during step "${run.currentStepId}"`,
|
|
6056
|
-
failedStepId: undefined
|
|
6057
|
-
}, now);
|
|
6058
|
-
};
|
|
6059
|
-
var failRun = (run, reason, now) => {
|
|
6060
|
-
const pausedRun = pauseRun(run, reason, now);
|
|
6061
|
-
return pausedRun.status === "paused" ? { ...pausedRun, failedStepId: pausedRun.currentStepId } : pausedRun;
|
|
6062
|
-
};
|
|
6063
|
-
var resumeRun = (run, now) => {
|
|
6064
|
-
if (run.status !== "paused")
|
|
6065
|
-
return run;
|
|
6066
|
-
return withRunUpdate(run, {
|
|
6067
|
-
status: run.pausedFrom ?? (run.pendingGate ? "awaiting-gate" : "running"),
|
|
6068
|
-
pauseReason: undefined,
|
|
6069
|
-
pausedFrom: undefined,
|
|
6070
|
-
failedStepId: undefined
|
|
6071
|
-
}, now);
|
|
6072
|
-
};
|
|
6073
|
-
var setResumeInput = (run, input, now) => withRunUpdate(run, { resumeInput: input.trim() || undefined }, now);
|
|
6074
|
-
var abortRun = (run, reason, now) => withRunUpdate(run, {
|
|
6075
|
-
status: "aborted",
|
|
6076
|
-
pauseReason: reason || "Aborted by user",
|
|
6077
|
-
pausedFrom: undefined,
|
|
6078
|
-
failedStepId: undefined,
|
|
6079
|
-
pendingGate: undefined,
|
|
6080
|
-
resumeInput: undefined
|
|
6081
|
-
}, now);
|
|
6082
|
-
// src/engine/reconciliation-history.ts
|
|
6083
|
-
var isApprovedGateEntry = (workflow, entry) => {
|
|
6084
|
-
const gate = workflow.definition.steps[entry.stepId]?.gate;
|
|
6085
|
-
return gate !== undefined && entry.outcome === gate.approvedOutcome;
|
|
6172
|
+
return reachable;
|
|
6086
6173
|
};
|
|
6087
|
-
var
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
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
|
+
}
|
|
6092
6181
|
}
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
};
|
|
6105
|
-
var retainedWorkspaceCwd = (run, history) => {
|
|
6106
|
-
for (let index = history.length - 1;index >= 0; index -= 1) {
|
|
6107
|
-
const cwd = history[index]?.workspaceCwd;
|
|
6108
|
-
if (cwd)
|
|
6109
|
-
return cwd;
|
|
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
|
+
}
|
|
6110
6193
|
}
|
|
6111
|
-
return
|
|
6112
|
-
};
|
|
6113
|
-
var retainedReviewedApproval = (workflow, history) => {
|
|
6114
|
-
const retainedApproval = latestApprovedGateEntry(workflow, history);
|
|
6115
|
-
const approval = retainedApproval?.entry.approval;
|
|
6116
|
-
return approval ? { artifact: approval.artifact, feedback: approval.feedback } : undefined;
|
|
6194
|
+
return canComplete;
|
|
6117
6195
|
};
|
|
6118
|
-
var
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
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")));
|
|
6128
6235
|
};
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
const
|
|
6133
|
-
|
|
6134
|
-
return actualEntries.length === expectedEntries.length && actualEntries.every(([stepId, count], index) => expectedEntries[index]?.[0] === stepId && expectedEntries[index][1] === count);
|
|
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));
|
|
6135
6241
|
};
|
|
6136
|
-
|
|
6137
|
-
const
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
const
|
|
6142
|
-
if (
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
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
|
+
});
|
|
6147
6255
|
}
|
|
6148
|
-
if (
|
|
6149
|
-
|
|
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
|
+
});
|
|
6150
6263
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
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
|
+
});
|
|
6153
6272
|
}
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
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
|
+
});
|
|
6162
6286
|
}
|
|
6163
|
-
|
|
6164
|
-
|
|
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)}`), "");
|
|
6165
6306
|
}
|
|
6166
|
-
return
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
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) {
|
|
6176
6330
|
const entry = run.history[index];
|
|
6177
|
-
if (!entry)
|
|
6331
|
+
if (!entry?.workspaceCwd)
|
|
6178
6332
|
continue;
|
|
6179
|
-
if (reachedDone) {
|
|
6180
|
-
return "workflow history continues after a $done transition";
|
|
6181
|
-
}
|
|
6182
|
-
if (entry.stepId !== expectedStepId) {
|
|
6183
|
-
return `history step "${entry.stepId}" is not reachable after "${expectedStepId}"`;
|
|
6184
|
-
}
|
|
6185
6333
|
const step = workflow.definition.steps[entry.stepId];
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
return sameWorkflowDigest ? `history step "${entry.stepId}" does not match the active workflow digest` : undefined;
|
|
6189
|
-
}
|
|
6190
|
-
const metadataError = validateHistoryMetadata(workflow, step, entry);
|
|
6191
|
-
if (metadataError)
|
|
6192
|
-
return metadataError;
|
|
6193
|
-
if (entry.workspaceCwd) {
|
|
6194
|
-
if (boundWorkspaceCwd !== undefined && boundWorkspaceCwd !== entry.workspaceCwd) {
|
|
6195
|
-
return "workflow history attempts to replace an existing workspace binding";
|
|
6196
|
-
}
|
|
6197
|
-
boundWorkspaceCwd = entry.workspaceCwd;
|
|
6198
|
-
}
|
|
6199
|
-
if (entry.approval) {
|
|
6200
|
-
latestApproval = {
|
|
6201
|
-
artifact: entry.approval.artifact,
|
|
6202
|
-
feedback: entry.approval.feedback
|
|
6203
|
-
};
|
|
6204
|
-
}
|
|
6205
|
-
const target = step.transitions[entry.outcome];
|
|
6206
|
-
if (!target) {
|
|
6207
|
-
return `history outcome "${entry.outcome}" is not configured for step "${entry.stepId}"`;
|
|
6208
|
-
}
|
|
6209
|
-
if (target === "$pause") {
|
|
6210
|
-
return `history step "${entry.stepId}" records a non-completing $pause transition`;
|
|
6211
|
-
}
|
|
6212
|
-
if (target === "$done") {
|
|
6213
|
-
if (index !== run.history.length - 1) {
|
|
6214
|
-
return "workflow history continues after a $done transition";
|
|
6215
|
-
}
|
|
6216
|
-
reachedDone = true;
|
|
6217
|
-
continue;
|
|
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`);
|
|
6218
6336
|
}
|
|
6219
|
-
|
|
6220
|
-
|
|
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");
|
|
6221
6364
|
}
|
|
6222
|
-
|
|
6223
|
-
|
|
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;
|
|
6224
6369
|
}
|
|
6225
|
-
if (
|
|
6226
|
-
|
|
6370
|
+
if (selected.length === 0) {
|
|
6371
|
+
context.ui.notify(`No workflows loaded from ${catalog.userDirectory}`, catalog.diagnostics.length > 0 ? "warning" : "info");
|
|
6372
|
+
return;
|
|
6227
6373
|
}
|
|
6228
|
-
|
|
6229
|
-
|
|
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;
|
|
6230
6385
|
}
|
|
6231
|
-
if (
|
|
6232
|
-
|
|
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;
|
|
6233
6389
|
}
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
return "reviewed artifact and feedback do not match authoritative approval history";
|
|
6390
|
+
if (!context.isIdle()) {
|
|
6391
|
+
context.abort();
|
|
6392
|
+
await startContext.waitForIdle();
|
|
6238
6393
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
return
|
|
6394
|
+
if (!isCurrentSession(this, sessionEpoch)) {
|
|
6395
|
+
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
6396
|
+
return;
|
|
6242
6397
|
}
|
|
6243
|
-
|
|
6244
|
-
if (
|
|
6245
|
-
|
|
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;
|
|
6246
6402
|
}
|
|
6247
|
-
if (
|
|
6248
|
-
|
|
6403
|
+
if (!isCurrentSession(this, sessionEpoch)) {
|
|
6404
|
+
context.ui.notify("Workflow start was superseded by a session change", "warning");
|
|
6405
|
+
return;
|
|
6249
6406
|
}
|
|
6250
|
-
|
|
6407
|
+
const workflow = this.catalog.workflows.get(workflowId);
|
|
6408
|
+
if (!workflow) {
|
|
6409
|
+
context.ui.notify(`Workflow "${workflowId}" is not loaded`, "error");
|
|
6251
6410
|
return;
|
|
6252
|
-
|
|
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);
|
|
6253
6442
|
}
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
if (
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
error: `run belongs to "${run.workflowId}", not "${workflow.definition.id}"`
|
|
6261
|
-
};
|
|
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;
|
|
6262
6449
|
}
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
if (changedHistoryEntry && !workflow.definition.steps[changedHistoryEntry.stepId]) {
|
|
6267
|
-
return {
|
|
6268
|
-
changed: true,
|
|
6269
|
-
error: "a completed step was removed; abort or restore the configuration"
|
|
6270
|
-
};
|
|
6450
|
+
if (this.activeDelegation) {
|
|
6451
|
+
context.ui.notify(`Cannot restart while subagent "${this.activeDelegation.agent}" is still cancelling`, "warning");
|
|
6452
|
+
return;
|
|
6271
6453
|
}
|
|
6272
|
-
if (
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
error: `current step "${run.currentStepId}" was removed; abort or restore the configuration`
|
|
6276
|
-
};
|
|
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;
|
|
6277
6457
|
}
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
changed: run.workflowDigest !== workflow.digest,
|
|
6282
|
-
error: `workflow checkpoint is inconsistent: ${semanticError}`
|
|
6283
|
-
};
|
|
6458
|
+
if (!context.isIdle()) {
|
|
6459
|
+
context.abort();
|
|
6460
|
+
await startContext.waitForIdle();
|
|
6284
6461
|
}
|
|
6285
|
-
if (run
|
|
6286
|
-
|
|
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;
|
|
6287
6465
|
}
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
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
|
+
}
|
|
6292
6526
|
}
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
return {
|
|
6298
|
-
changed: true,
|
|
6299
|
-
restartedStep,
|
|
6300
|
-
run: withRunUpdate(reconciledRun, {
|
|
6301
|
-
workflowDigest: workflow.digest,
|
|
6302
|
-
status: "paused",
|
|
6303
|
-
currentStepId: restartedStep,
|
|
6304
|
-
currentStepDigest: workflow.stepDigests[restartedStep] ?? "",
|
|
6305
|
-
history: retainedHistory,
|
|
6306
|
-
currentStepAttempts: changedEntry.attempts,
|
|
6307
|
-
currentStepOmittedAttempts: changedEntry.omittedAttempts,
|
|
6308
|
-
visits: rebuildVisits(retainedHistory, restartedStep),
|
|
6309
|
-
cwd: retainedWorkspaceCwd(reconciledRun, retainedHistory),
|
|
6310
|
-
reviewedArtifact: reviewedApproval?.artifact ?? "",
|
|
6311
|
-
reviewedFeedback: reviewedApproval?.feedback ?? "",
|
|
6312
|
-
stepHandoff,
|
|
6313
|
-
lastSummary: stepHandoff,
|
|
6314
|
-
pendingGate: undefined,
|
|
6315
|
-
pausedFrom: "running",
|
|
6316
|
-
failedStepId: undefined,
|
|
6317
|
-
pauseReason: `Configuration changed; restarted step "${restartedStep}"`,
|
|
6318
|
-
gateArtifact: "",
|
|
6319
|
-
gateFeedback: ""
|
|
6320
|
-
}, now)
|
|
6321
|
-
};
|
|
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;
|
|
6322
6531
|
}
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
currentStepDigest: currentDigest
|
|
6341
|
-
}, now)
|
|
6342
|
-
};
|
|
6343
|
-
};
|
|
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
|
+
|
|
6344
6549
|
// src/harness/step-reporting.ts
|
|
6345
6550
|
var WORKFLOW_STEP_SUMMARY_MESSAGE_TYPE = "workflow-step-summary";
|
|
6346
6551
|
var MAX_POSTED_STEP_SUMMARY_CHARS = 4000;
|
|
@@ -6777,6 +6982,17 @@ function buildDelegatedHandoffSection(handoff) {
|
|
|
6777
6982
|
""
|
|
6778
6983
|
];
|
|
6779
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
|
+
}
|
|
6780
6996
|
function buildDelegatedCompletionInstructions() {
|
|
6781
6997
|
return [
|
|
6782
6998
|
"This child is non-interactive. Never call `contact_supervisor`, `subagent_supervisor`, or `intercom`.",
|
|
@@ -6817,6 +7033,7 @@ function createTemplateValues({
|
|
|
6817
7033
|
return {
|
|
6818
7034
|
"workflow.input": run.input,
|
|
6819
7035
|
"workflow.id": workflow.definition.id,
|
|
7036
|
+
"workflow.iteration": String(run.iteration ?? 1),
|
|
6820
7037
|
"run.id": run.runId,
|
|
6821
7038
|
"step.id": run.currentStepId,
|
|
6822
7039
|
"step.title": step.title,
|
|
@@ -6825,7 +7042,8 @@ function createTemplateValues({
|
|
|
6825
7042
|
"reviewed.feedback": run.reviewedFeedback ?? "",
|
|
6826
7043
|
"gate.artifact": run.gateArtifact ?? "",
|
|
6827
7044
|
"gate.feedback": run.gateFeedback,
|
|
6828
|
-
"resume.input": run.resumeInput ?? ""
|
|
7045
|
+
"resume.input": run.resumeInput ?? "",
|
|
7046
|
+
"restart.workspace": run.restartWorkspaceCwd ?? ""
|
|
6829
7047
|
};
|
|
6830
7048
|
}
|
|
6831
7049
|
|
|
@@ -6889,6 +7107,7 @@ function buildStepTask(options) {
|
|
|
6889
7107
|
"",
|
|
6890
7108
|
`Workflow: ${workflow.definition.id}`,
|
|
6891
7109
|
`Run: ${run.runId}`,
|
|
7110
|
+
`Iteration: ${run.iteration ?? 1}`,
|
|
6892
7111
|
`Step: ${run.currentStepId} (${step.title})`,
|
|
6893
7112
|
...isDelegated ? [
|
|
6894
7113
|
`Agent profile: ${step.subagent?.agent ?? "generalist"}`,
|
|
@@ -6900,6 +7119,7 @@ function buildStepTask(options) {
|
|
|
6900
7119
|
prompt,
|
|
6901
7120
|
"",
|
|
6902
7121
|
...isDelegated ? buildDelegatedHandoffSection(handoff) : [],
|
|
7122
|
+
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
6903
7123
|
...buildResumeInputSection(run, RESUME_INPUT_PLACEHOLDER.test(promptTemplate)),
|
|
6904
7124
|
...buildResourceSection({ execution, step }),
|
|
6905
7125
|
"## Completion contract",
|
|
@@ -6998,9 +7218,17 @@ function registerLifecycle() {
|
|
|
6998
7218
|
this.restoreFromSession(context);
|
|
6999
7219
|
this.isSessionActive = true;
|
|
7000
7220
|
});
|
|
7001
|
-
this.pi.on("session_shutdown", async () => {
|
|
7221
|
+
this.pi.on("session_shutdown", async (_event, context) => {
|
|
7002
7222
|
this.sessionEpoch += 1;
|
|
7003
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
|
+
}
|
|
7004
7232
|
this.cancelPromptReview();
|
|
7005
7233
|
this.mainSteps.deactivate();
|
|
7006
7234
|
await this.cancelActiveDelegation("Pi session shut down");
|
|
@@ -8097,6 +8325,9 @@ function enqueueMutation(context, operation) {
|
|
|
8097
8325
|
function persist() {
|
|
8098
8326
|
if (this.run) {
|
|
8099
8327
|
this.pi.appendEntry(STATE_ENTRY_TYPE, structuredClone(this.run));
|
|
8328
|
+
const session = this.latestContext?.sessionManager;
|
|
8329
|
+
if (session)
|
|
8330
|
+
this.dependencies.flushUnwrittenSession(session);
|
|
8100
8331
|
}
|
|
8101
8332
|
}
|
|
8102
8333
|
function restoreFromSession(context) {
|
|
@@ -8231,6 +8462,7 @@ class WorkflowHarness {
|
|
|
8231
8462
|
listWorkflows = START_ACTIONS.listWorkflows;
|
|
8232
8463
|
doctorWorkflows = START_ACTIONS.doctorWorkflows;
|
|
8233
8464
|
startNow = START_ACTIONS.startNow;
|
|
8465
|
+
restartNow = START_ACTIONS.restartNow;
|
|
8234
8466
|
reloadNow = START_ACTIONS.reloadNow;
|
|
8235
8467
|
pauseNow = PAUSE_ACTIONS.pauseNow;
|
|
8236
8468
|
abortNow = PAUSE_ACTIONS.abortNow;
|
|
@@ -8305,6 +8537,13 @@ class WorkflowHarness {
|
|
|
8305
8537
|
waitForIdle: () => context.waitForIdle()
|
|
8306
8538
|
}, sessionEpoch));
|
|
8307
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
|
+
}
|
|
8308
8547
|
pause(reason, context) {
|
|
8309
8548
|
return this.enqueueMutation(context, () => this.pauseNow(reason, context));
|
|
8310
8549
|
}
|
|
@@ -8360,14 +8599,14 @@ var parseChildStructuredResult = ({
|
|
|
8360
8599
|
// src/integrations/subagents/child-runtime-dependencies.ts
|
|
8361
8600
|
import { randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
8362
8601
|
import {
|
|
8363
|
-
existsSync,
|
|
8602
|
+
existsSync as existsSync2,
|
|
8364
8603
|
lstatSync,
|
|
8365
8604
|
readFileSync,
|
|
8366
8605
|
realpathSync as realpathSync2,
|
|
8367
8606
|
renameSync,
|
|
8368
8607
|
statSync as statSync2,
|
|
8369
8608
|
unlinkSync,
|
|
8370
|
-
writeFileSync as
|
|
8609
|
+
writeFileSync as writeFileSync3
|
|
8371
8610
|
} from "node:fs";
|
|
8372
8611
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8373
8612
|
var tokensAreEqual = (actual, expected) => {
|
|
@@ -8377,7 +8616,7 @@ var tokensAreEqual = (actual, expected) => {
|
|
|
8377
8616
|
};
|
|
8378
8617
|
var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
8379
8618
|
fileSystem: {
|
|
8380
|
-
exists:
|
|
8619
|
+
exists: existsSync2,
|
|
8381
8620
|
inspect: lstatSync,
|
|
8382
8621
|
readText: (path) => readFileSync(path, "utf8"),
|
|
8383
8622
|
realPath: realpathSync2,
|
|
@@ -8385,7 +8624,7 @@ var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
8385
8624
|
stat: statSync2,
|
|
8386
8625
|
unlink: unlinkSync,
|
|
8387
8626
|
writeExclusive: (path, content) => {
|
|
8388
|
-
|
|
8627
|
+
writeFileSync3(path, content, {
|
|
8389
8628
|
encoding: "utf8",
|
|
8390
8629
|
flag: "wx",
|
|
8391
8630
|
mode: 384
|