@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.
Files changed (43) hide show
  1. package/README.md +19 -955
  2. package/dist/index.js +841 -535
  3. package/examples/mr-comments.workflow.yaml +1 -1
  4. package/examples/prompts/mr-comments/plan.md +8 -1
  5. package/examples/starter-kit/mr-comment.workflow.yaml +1 -1
  6. package/examples/starter-kit/mr-review.workflow.yaml +4 -2
  7. package/examples/starter-kit/steps/mr-comment/plan.md +9 -2
  8. package/examples/starter-kit/steps/mr-review/publish.md +9 -2
  9. package/examples/starter-kit/steps/mr-review/review.md +9 -2
  10. package/examples/starter-kit/steps/mr-review/verify.md +16 -5
  11. package/examples/starter-kit/steps/shared/prepare-workspace.md +42 -4
  12. package/examples/starter-kit/steps/ticket/plan.md +31 -2
  13. package/examples/starter-kit/steps/work/plan.md +31 -2
  14. package/examples/starter-kit/ticket.workflow.yaml +14 -2
  15. package/examples/starter-kit/work.workflow.yaml +14 -2
  16. package/package.json +1 -1
  17. package/schemas/workflow.schema.json +1 -0
  18. package/src/command-names.ts +1 -0
  19. package/src/commands.ts +14 -0
  20. package/src/config/types.ts +1 -1
  21. package/src/config/validation/prompt.ts +3 -0
  22. package/src/engine/create-run.ts +4 -0
  23. package/src/engine/gate-transitions.ts +80 -33
  24. package/src/engine/run-advance.ts +36 -3
  25. package/src/engine/run-lifecycle.ts +69 -0
  26. package/src/engine/run-reconciliation.ts +2 -0
  27. package/src/engine/run-validation.ts +24 -1
  28. package/src/engine/state-types.ts +10 -0
  29. package/src/engine/state.ts +1 -0
  30. package/src/engine/transitions.ts +1 -0
  31. package/src/harness/action-context.ts +6 -0
  32. package/src/harness/core-actions.ts +2 -0
  33. package/src/harness/dependencies.ts +4 -0
  34. package/src/harness/lifecycle-actions.ts +14 -1
  35. package/src/harness/session-persistence.ts +66 -0
  36. package/src/harness/start-actions.ts +188 -1
  37. package/src/harness.ts +20 -0
  38. package/src/prompt/step-sections.ts +14 -0
  39. package/src/prompt/step-task.ts +3 -0
  40. package/src/prompt/template.ts +6 -3
  41. package/src/workflow-doctor.ts +1 -1
  42. package/src/workflow-status/render-summary.ts +3 -0
  43. 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
- if (data === "q" || data === "Q" || matchesKey(data, "ctrl+c") || matchesKey(data, "ctrl+d") || matchesKey(data, this.statusShortcut)) {
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 (matchesKey(data, Key.left) || data === "h") {
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" ? "↑/↓ or j/k scroll · PgUp/PgDn · ←/h/Esc back" : "↑/↓ or j/k select · Enter/→/l inspect · PgUp/PgDn");
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
- writeFileSync(capabilityPath, capabilityToken, {
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/workflow-doctor.ts
5493
- var lexical = (left, right) => left < right ? -1 : left > right ? 1 : 0;
5494
- var internalTargets = (step) => [
5495
- ...new Set(Object.values(step.transitions).filter((target) => target !== "$done" && target !== "$pause"))
5496
- ].sort(lexical);
5497
- var adjacencyFor = (definition) => Object.fromEntries(Object.entries(definition.steps).sort(([left], [right]) => lexical(left, right)).map(([stepId, step]) => [stepId, internalTargets(step)]));
5498
- var reachableSteps = (definition, adjacency) => {
5499
- const reachable = new Set;
5500
- const pending = [definition.start];
5501
- while (pending.length > 0) {
5502
- const stepId = pending.pop();
5503
- if (!stepId || reachable.has(stepId))
5504
- continue;
5505
- if (!definition.steps[stepId])
5506
- continue;
5507
- reachable.add(stepId);
5508
- pending.push(...adjacency[stepId] ?? []);
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
- return reachable;
5511
- };
5512
- var stepsThatCanComplete = (definition, adjacency) => {
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
- const pending = [...canComplete].sort(lexical);
5521
- while (pending.length > 0) {
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
- return canComplete;
5533
- };
5534
- var stronglyConnectedComponents = (definition, adjacency) => {
5535
- let nextIndex = 0;
5536
- const indexes = new Map;
5537
- const lowLinks = new Map;
5538
- const stack = [];
5539
- const onStack = new Set;
5540
- const components = [];
5541
- const visit = (stepId) => {
5542
- indexes.set(stepId, nextIndex);
5543
- lowLinks.set(stepId, nextIndex);
5544
- nextIndex += 1;
5545
- stack.push(stepId);
5546
- onStack.add(stepId);
5547
- for (const target of adjacency[stepId] ?? []) {
5548
- if (!indexes.has(target)) {
5549
- visit(target);
5550
- lowLinks.set(stepId, Math.min(lowLinks.get(stepId) ?? 0, lowLinks.get(target) ?? 0));
5551
- } else if (onStack.has(target)) {
5552
- lowLinks.set(stepId, Math.min(lowLinks.get(stepId) ?? 0, indexes.get(target) ?? 0));
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
- if (lowLinks.get(stepId) !== indexes.get(stepId))
5556
- return;
5557
- const component = [];
5558
- let member;
5559
- do {
5560
- member = stack.pop();
5561
- if (!member)
5562
- break;
5563
- onStack.delete(member);
5564
- component.push(member);
5565
- } while (member !== stepId);
5566
- components.push(component.sort(lexical));
5567
- };
5568
- for (const stepId of Object.keys(definition.steps).sort(lexical)) {
5569
- if (!indexes.has(stepId))
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
- return components.sort((left, right) => lexical(left.join("\x00"), right.join("\x00")));
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
- var isCycle = (adjacency, component) => {
5575
- if (component.length > 1)
5576
- return true;
5577
- const [stepId] = component;
5578
- return Boolean(stepId && adjacency[stepId]?.includes(stepId));
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
- function analyzeWorkflow(definition) {
5581
- const adjacency = adjacencyFor(definition);
5582
- const reachable = reachableSteps(definition, adjacency);
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
- if (stranded.length > 0) {
5595
- issues.push({
5596
- level: "error",
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
- const unreachable = Object.keys(definition.steps).filter((stepId) => !reachable.has(stepId)).sort(lexical);
5603
- if (unreachable.length > 0) {
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
- for (const component of stronglyConnectedComponents(definition, adjacency)) {
5612
- if (!isCycle(adjacency, component))
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) => run.pendingGate ? withRunUpdate(run, {
5911
- status: "running",
5912
- pendingGate: undefined,
5913
- gateFeedback: reason,
5914
- pausedFrom: undefined,
5915
- pauseReason: undefined,
5916
- failedStepId: undefined
5917
- }, now) : run;
5918
- var storeGateResolution = (run, resolution, now) => run.pendingGate ? withRunUpdate(run, { pendingGate: { ...run.pendingGate, resolution } }, now) : run;
5919
- var resolveGate = (workflow, run, resolution, now) => {
5920
- const pendingGate = run.pendingGate;
5921
- if (!pendingGate)
5922
- throw new Error("workflow has no pending gate");
5923
- const step = workflow.definition.steps[pendingGate.stepId];
5924
- if (!step?.gate) {
5925
- throw new Error(`gated step "${pendingGate.stepId}" no longer exists`);
5926
- }
5927
- if (run.currentStepId !== pendingGate.stepId) {
5928
- throw new Error("gate result does not match the current step");
5929
- }
5930
- const outcome = resolution.approved ? step.gate.approvedOutcome : step.gate.rejectedOutcome;
5931
- const stepStructuralDigest = workflow.stepStructuralDigests[pendingGate.stepId] ?? "";
5932
- if (resolution.approved && !stepStructuralDigest) {
5933
- throw new Error(`gated step "${pendingGate.stepId}" has no structural digest`);
5934
- }
5935
- const summary = resolution.approved ? pendingGate.summary ?? "" : resolution.feedback ? `Gate rejected: ${resolution.feedback}` : "Gate rejected";
5936
- const decidedRun = recordCurrentGateDecision(run, {
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: resolution.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
- gateFeedback: resolution.feedback
5784
+ gateArtifact: resolution.approved ? "" : pendingGate.artifact,
5785
+ gateFeedback: resolution.approved ? "" : feedback
5950
5786
  }, now);
5951
- const advanced = advanceRun(workflow, runnableRun, outcome, summary, now);
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: resolution.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: resolution.feedback
5807
+ reviewedFeedback: feedback
5969
5808
  } : {},
5970
- gateFeedback: resolution.feedback
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 (reachedDone !== (run.status === "completed")) {
6162
- return reachedDone ? "a workflow that reached $done must be completed" : "a completed workflow has no $done transition in its history";
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
- if (run.status === "completed" && run.currentStepDigest !== run.history.at(-1)?.stepDigest) {
6165
- return "completed workflow current-step digest does not match its terminal history";
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 (!sameVisitCounts(run.visits, expectedVisits)) {
6168
- return "workflow visit counts do not match its execution history";
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
- const reviewedArtifact = run.reviewedArtifact ?? "";
6171
- const reviewedFeedback = run.reviewedFeedback ?? "";
6172
- if (reviewedArtifact !== (latestApproval?.artifact ?? "") || reviewedFeedback !== (latestApproval?.feedback ?? "")) {
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
- const currentStep2 = workflow.definition.steps[run.currentStepId];
6176
- if (!currentStep2) {
6177
- return `current step "${run.currentStepId}" is missing from the workflow`;
6394
+ if (!isCurrentSession(this, sessionEpoch)) {
6395
+ context.ui.notify("Workflow start was superseded by a session change", "warning");
6396
+ return;
6178
6397
  }
6179
- const currentStepChanged = run.currentStepDigest !== workflow.stepDigests[run.currentStepId];
6180
- if (sameWorkflowDigest && currentStepChanged) {
6181
- return `current step "${run.currentStepId}" does not match the active workflow digest`;
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 (boundWorkspaceCwd && !currentStep2.subagent) {
6184
- return `bound workflow current step "${run.currentStepId}" must use a subagent`;
6403
+ if (!isCurrentSession(this, sessionEpoch)) {
6404
+ context.ui.notify("Workflow start was superseded by a session change", "warning");
6405
+ return;
6185
6406
  }
6186
- if (currentStepChanged)
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
- return validatePendingGate(run, currentStep2);
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
- // src/engine/run-reconciliation.ts
6192
- var reconcileRun = (run, workflow, now) => {
6193
- if (run.workflowId !== workflow.definition.id) {
6194
- return {
6195
- changed: false,
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
- const reconciledRun = run.workflowDigest === workflow.digest ? run : refreshApprovedGateHistory(run, workflow);
6200
- const changedHistoryIndex = run.workflowDigest === workflow.digest ? -1 : reconciledRun.history.findIndex((entry) => workflow.stepDigests[entry.stepId] !== entry.stepDigest);
6201
- const changedHistoryEntry = changedHistoryIndex >= 0 ? reconciledRun.history[changedHistoryIndex] : undefined;
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 (changedHistoryIndex < 0 && !workflow.definition.steps[run.currentStepId]) {
6209
- return {
6210
- changed: true,
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
- const semanticError = validateRunWorkflowSemantics(reconciledRun, workflow);
6215
- if (semanticError) {
6216
- return {
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.workflowDigest === workflow.digest) {
6222
- return { run, changed: false };
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
- if (changedHistoryIndex >= 0) {
6225
- const changedEntry = changedHistoryEntry;
6226
- if (!changedEntry) {
6227
- return { changed: true, error: "changed history entry is unavailable" };
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
- const retainedHistory = reconciledRun.history.slice(0, changedHistoryIndex);
6230
- const restartedStep = changedEntry.stepId;
6231
- const stepHandoff = retainedHistory.at(-1)?.summary ?? "";
6232
- const reviewedApproval = retainedReviewedApproval(workflow, retainedHistory);
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
- const currentDigest = workflow.stepDigests[run.currentStepId] ?? "";
6259
- const hasCurrentStepChanged = currentDigest !== reconciledRun.currentStepDigest;
6260
- const restartChanges = hasCurrentStepChanged ? {
6261
- status: "paused",
6262
- pendingGate: undefined,
6263
- pausedFrom: "running",
6264
- failedStepId: undefined,
6265
- pauseReason: `Configuration changed; restarted step "${run.currentStepId}"`,
6266
- gateFeedback: ""
6267
- } : {};
6268
- return {
6269
- changed: true,
6270
- ...hasCurrentStepChanged ? { restartedStep: run.currentStepId } : {},
6271
- run: withRunUpdate(reconciledRun, {
6272
- ...restartChanges,
6273
- workflowDigest: workflow.digest,
6274
- currentStepDigest: currentDigest
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 paused attempt:",
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 writeFileSync2
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: existsSync,
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
- writeFileSync2(path, content, {
8627
+ writeFileSync3(path, content, {
8322
8628
  encoding: "utf8",
8323
8629
  flag: "wx",
8324
8630
  mode: 384