@wichayutdew/pi-workflows 2.0.1 → 2.1.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 CHANGED
@@ -145,10 +145,14 @@ workspace:
145
145
  `/work` and `/ticket` bind the exact worktree returned by their user-authored
146
146
  preparation prompt. `/mr-comment` intentionally has no workspace-binding step:
147
147
  every child stays on the branch and worktree from which the run started.
148
- Approval rejection pauses either review workflow; it never launches an
149
- automatic replacement plan. Remote publication prompts first check whether an
150
- approved effect already exists because the harness cannot guarantee
151
- exactly-once external side effects.
148
+ Requested changes return only to the gated plan or review step with the user's
149
+ feedback; they never restart workspace preparation or the whole workflow. The
150
+ starter `/work` and `/ticket` planners use a separate `workspace-refresh`
151
+ outcome only for source-ancestry drift. It revisits preparation in the same
152
+ canonical worktree; the prompt may safely rebase its clean run-owned branch but
153
+ can never bind a replacement directory.
154
+ Remote publication prompts first check whether an approved effect already
155
+ exists because the harness cannot guarantee exactly-once external side effects.
152
156
 
153
157
  ## Add a workflow
154
158
 
@@ -218,16 +222,16 @@ loaded extensions or prompt resources are diagnosed before aliases register.
218
222
 
219
223
  Top-level fields:
220
224
 
221
- | Field | Required | Default | Description |
222
- | ----------------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
223
- | `version` | Yes | — | Configuration contract version. Currently `1`. |
224
- | `id` | Yes | — | Stable workflow identifier. |
225
- | `command` | Yes | — | Slash command without `/`. |
226
- | `description` | Yes | — | Command description. |
227
- | `start` | Yes | — | First step identifier. |
228
- | `steps` | Yes | — | Step map. Order is controlled by transitions, not file order. |
229
- | `maxStepVisits` | No | `5` | Loop guard for each step. |
230
- | `summaryMaxChars` | No | `4000` | Maximum step `summary` length. Reviewed gate artifacts use their separate 200,000-character limit. |
225
+ | Field | Required | Default | Description |
226
+ | ----------------- | -------- | ------- | ----------------------------------------------------------------------------------------- |
227
+ | `version` | Yes | — | Configuration contract version. Currently `1`. |
228
+ | `id` | Yes | — | Stable workflow identifier. |
229
+ | `command` | Yes | — | Slash command without `/`. |
230
+ | `description` | Yes | — | Command description. |
231
+ | `start` | Yes | — | First step identifier. |
232
+ | `steps` | Yes | — | Step map. Order is controlled by transitions, not file order. |
233
+ | `maxStepVisits` | No | `5` | Loop guard for each step. |
234
+ | `summaryMaxChars` | No | `4000` | Maximum step `summary` length. Gate artifacts use their separate 200,000-character limit. |
231
235
 
232
236
  Each step supports:
233
237
 
@@ -253,6 +257,7 @@ Supported prompt variables:
253
257
  {{last.summary}}
254
258
  {{reviewed.artifact}}
255
259
  {{reviewed.feedback}}
260
+ {{gate.artifact}}
256
261
  {{gate.feedback}}
257
262
  {{resume.input}}
258
263
  ```
@@ -260,10 +265,11 @@ Supported prompt variables:
260
265
  Unknown variables fail configuration loading.
261
266
 
262
267
  `{{last.summary}}` normally contains the previous completed step's handoff.
263
- After a step-requested `$pause`, it contains both the preserved incoming
264
- previous-step handoff and the latest paused-attempt summary. Gate artifacts and
265
- summaries stay separate: prompts opt into the opaque approved artifact through
266
- `{{reviewed.artifact}}`.
268
+ During a step-requested `$pause` or same-step human gate revision, it contains
269
+ both the preserved incoming handoff and latest current-step summary. Gate
270
+ artifacts and summaries stay separate: prompts opt into an opaque approved
271
+ artifact through `{{reviewed.artifact}}` or the latest rejected artifact
272
+ through `{{gate.artifact}}`.
267
273
  `{{resume.input}}` contains guidance supplied to the current attempt through
268
274
  `/workflow-resume [guidance]`. If a prompt omits that variable, the harness
269
275
  adds the guidance in a clearly delimited standard section, so every workflow
@@ -322,17 +328,19 @@ workspace:
322
328
  On a binding outcome, the result must include
323
329
  `workspace: { cwd: "/absolute/directory" }`. The harness resolves its real path,
324
330
  requires an existing directory under one configured root relative to the
325
- run-start directory, persists it once, and passes that exact directory to every
326
- reachable downstream child, revisit, recovery attempt, and resume. Other
327
- outcomes must omit `workspace`; later results cannot replace the binding. All
331
+ run-start directory, persists that canonical identity, and passes it to every
332
+ reachable downstream child, revisit, recovery attempt, and resume. If the sole
333
+ binding step is revisited, it may re-affirm the same canonical directory; a
334
+ different directory is rejected. Other outcomes must omit `workspace`. All
328
335
  reachable nonterminal steps after a binding must be delegated because the main
329
336
  Pi process cannot change its working directory.
330
337
 
331
338
  The workflow prompt owns how the directory is prepared and what it represents.
332
339
  The harness does not know Git, worktrees, languages, frameworks, or command
333
- syntax, and it never derives a directory from summaries or gate artifacts. A
334
- legacy checkpoint without a captured directory fails closed before delegation;
335
- abort it and start a new run.
340
+ syntax, and it never derives a directory from summaries or gate artifacts. The
341
+ starter kit's guarded rebase policy is therefore prompt-owned domain behavior,
342
+ not extension behavior. A legacy checkpoint without a captured directory fails
343
+ closed before delegation; abort it and start a new run.
336
344
 
337
345
  Use a profile name directly when only the child profile changes:
338
346
 
@@ -532,7 +540,7 @@ gate:
532
540
  rejectedOutcome: changes-requested
533
541
  transitions:
534
542
  approved: implement
535
- changes-requested: $pause
543
+ changes-requested: plan
536
544
  blocked: $pause
537
545
  ```
538
546
 
@@ -540,9 +548,13 @@ When the step completes with outcome `submit`, it must include the full content
540
548
  in `artifact`. Pi shows Approve, Request changes, and Pause workflow. Requested
541
549
  changes are returned through `{{gate.feedback}}`; approval persists the
542
550
  artifact as `{{reviewed.artifact}}` while the step's `summary` remains the
543
- compact handoff. A requested change pauses at the same step with feedback, so
544
- revision starts only after explicit `/workflow-resume`. Dismissing the panel
545
- keeps the pending artifact, so `/workflow-resume` reopens the same review.
551
+ compact handoff. Here, `changes-requested: plan` immediately runs a fresh
552
+ attempt of that exact gated step with the rejected draft in
553
+ `{{gate.artifact}}`, the original incoming step handoff, and the new feedback,
554
+ then opens another review. Use
555
+ `changes-requested: $pause` instead when revision should require an explicit
556
+ `/workflow-resume`. Dismissing the panel keeps the pending artifact, so
557
+ `/workflow-resume` reopens the same review.
546
558
 
547
559
  Dialog-capable UI is available in Pi TUI and RPC modes. In print or JSON mode,
548
560
  the gate pauses safely until resumed in TUI or RPC.
@@ -583,7 +595,7 @@ gate:
583
595
  timeoutMs: 30000
584
596
  transitions:
585
597
  approved: implement
586
- changes-requested: $pause
598
+ changes-requested: plan
587
599
  blocked: $pause
588
600
  ```
589
601
 
@@ -597,6 +609,17 @@ review identifier and accepts only the matching decision. On approval, the
597
609
  artifact is preserved as an opaque template value; the separate `summary`
598
610
  remains the next step's compact handoff.
599
611
 
612
+ The `approved` boolean from Plannotator is authoritative; feedback text and
613
+ annotation labels stay opaque. A rejection follows only the YAML transition
614
+ shown above. A same-step rejection is an explicitly human-controlled revision
615
+ loop, so that human-mediated transition bypasses the visit-limit check and may
616
+ continue until approval. The visit is still recorded, and later automatic
617
+ entries remain guarded. A same-step agent retry during revision preserves the
618
+ incoming handoff, rejected artifact, and feedback, but it does not bypass the
619
+ visit-limit check. Gate feedback is capped at 50,000 characters; history and
620
+ user notifications use a compact rejection summary while `{{gate.feedback}}`
621
+ receives the bounded full value.
622
+
600
623
  The artifact is never substituted for a missing summary. If an older pending
601
624
  gate has no separately stored summary, its transition uses an empty compact
602
625
  handoff.
@@ -739,8 +762,10 @@ a failed or aborted run, and `◆` a paused step or pending review.
739
762
  Use `↑`/`↓` or `j`/`k` to select a step, then `Enter`, `→`, or `l` to inspect
740
763
  the bounded task supplied to each attempt, its result and gate decision, and a
741
764
  chronological execution log when available. In the detail view,
742
- `↑`/`↓` or `j`/`k` scrolls; `←`, `h`, or `Esc` returns to the board; `q` or the
743
- configured shortcut closes it. PgUp/PgDn and Home/End retain page navigation.
765
+ `↑`/`↓` or `j`/`k` scrolls one line, while `Ctrl+D` and `Ctrl+U` scroll down and
766
+ up by half a page. `gg` jumps to the top and `G` jumps to the bottom. `←`, `h`,
767
+ or `Esc` returns to the board; `q` or the configured shortcut closes it.
768
+ PgUp/PgDn and Home/End retain page navigation.
744
769
  Trace references and bounded task/result evidence live in the checkpoint, so
745
770
  completed, paused, resumed, and restored runs remain inspectable. Child logs
746
771
  are path-confined, size-bounded, control-sanitized, and redact common
@@ -771,7 +796,10 @@ Unreachable steps and cycles are reported as warnings; a cycle with an exit may
771
796
  run, but `maxStepVisits` pauses the uninterrupted run before a step can execute
772
797
  more than its configured limit. An explicit resume can continue from that
773
798
  checkpoint, so the guard bounds automatic graph advancement rather than
774
- guaranteeing completion or wall-clock duration.
799
+ guaranteeing completion or wall-clock duration. An explicit human gate
800
+ rejection back to that same gated step bypasses the check for that transition;
801
+ the visit remains recorded and every iteration must stop for another human
802
+ decision.
775
803
 
776
804
  Configured aliases also accept multiline input. For example, if `work` is a
777
805
  loaded workflow command, Pi Workflows normalizes:
package/dist/index.js CHANGED
@@ -139,6 +139,7 @@ var PROMPT_VARIABLES = new Set([
139
139
  "last.summary",
140
140
  "reviewed.artifact",
141
141
  "reviewed.feedback",
142
+ "gate.artifact",
142
143
  "gate.feedback",
143
144
  "resume.input"
144
145
  ]);
@@ -3634,6 +3635,7 @@ function registerMainStepLifecycle({
3634
3635
 
3635
3636
  // src/engine/state-types.ts
3636
3637
  var RUN_STATE_VERSION = 1;
3638
+ var MAX_GATE_FEEDBACK_CHARS = 50000;
3637
3639
  var MAX_RESUME_INPUT_CHARS = 16000;
3638
3640
  var MAX_STEP_TRACE_TASK_CHARS = 64000;
3639
3641
  var MAX_STEP_TRACE_ATTEMPTS = 16;
@@ -4493,6 +4495,7 @@ var createRun = (workflow, input, baselineTools, runId, now, cwd) => {
4493
4495
  ...cwd ? { startCwd: cwd, cwd } : {},
4494
4496
  stepHandoff: "",
4495
4497
  lastSummary: "",
4498
+ gateArtifact: "",
4496
4499
  gateFeedback: ""
4497
4500
  };
4498
4501
  };
@@ -4815,7 +4818,7 @@ function recordCurrentGateDecision(run, decision, now) {
4815
4818
  // src/engine/run-validation.ts
4816
4819
  var isRecord8 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4817
4820
  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;
4821
+ 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
4822
  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
4823
  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
4824
  var isSafeTraceIdentityField = (value) => typeof value === "string" && value.length > 0 && !value.includes("\x00") && !value.includes("/") && !value.includes("\\") && value !== "." && value !== "..";
@@ -4862,7 +4865,7 @@ var isStepExecutionAttempts = (value) => Array.isArray(value) && value.length <=
4862
4865
  return value.slice(0, index).every((earlier) => earlier.ordinal === undefined || earlier.ordinal < ordinal);
4863
4866
  });
4864
4867
  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";
4868
+ 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
4869
  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
4870
  var isVisitCounts = (value) => isRecord8(value) && Object.values(value).every((count) => typeof count === "number" && Number.isInteger(count) && count >= 0);
4868
4871
  var isOptionalString = (value) => value === undefined || typeof value === "string";
@@ -4890,10 +4893,10 @@ var hasValidWorkspaceState = (run, history) => {
4890
4893
  var isWorkflowRun = (value) => {
4891
4894
  if (!isRecord8(value))
4892
4895
  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";
4896
+ 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
4897
  if (!hasValidRequiredFields)
4895
4898
  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");
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");
4897
4900
  if (!hasValidOptionalFields)
4898
4901
  return false;
4899
4902
  const pendingGate = value.pendingGate;
@@ -5081,6 +5084,7 @@ class WorkflowStatusView {
5081
5084
  statusShortcut;
5082
5085
  timer;
5083
5086
  state = initialViewportState();
5087
+ pendingDetailTopKey = false;
5084
5088
  statusShortcutLabel;
5085
5089
  dependencies;
5086
5090
  transcriptCache = new Map;
@@ -5106,7 +5110,9 @@ class WorkflowStatusView {
5106
5110
  }
5107
5111
  invalidate() {}
5108
5112
  handleInput(data) {
5109
- if (data === "q" || data === "Q" || matchesKey(data, "ctrl+c") || matchesKey(data, "ctrl+d") || matchesKey(data, this.statusShortcut)) {
5113
+ const isDetailHalfPageDown = this.state.mode === "detail" && matchesKey(data, Key.ctrl("d"));
5114
+ const isDetailHalfPageUp = this.state.mode === "detail" && matchesKey(data, Key.ctrl("u"));
5115
+ if (data === "q" || data === "Q" || matchesKey(data, "ctrl+c") || this.state.mode !== "detail" && matchesKey(data, "ctrl+d") || matchesKey(data, this.statusShortcut)) {
5110
5116
  this.close();
5111
5117
  return;
5112
5118
  }
@@ -5119,13 +5125,31 @@ class WorkflowStatusView {
5119
5125
  return;
5120
5126
  }
5121
5127
  const pageSize = Math.max(1, this.state.viewportRows - 2);
5128
+ const contentHeight = Math.max(1, this.state.viewportRows - 1);
5129
+ const halfPageSize = Math.max(1, Math.floor(contentHeight / 2));
5122
5130
  if (this.state.mode === "detail") {
5123
- if (matchesKey(data, Key.left) || data === "h") {
5131
+ if (data === "gg" || data === "g" && this.pendingDetailTopKey) {
5132
+ this.pendingDetailTopKey = false;
5133
+ this.setScrollOffset(0);
5134
+ return;
5135
+ }
5136
+ if (data === "g") {
5137
+ this.pendingDetailTopKey = true;
5138
+ return;
5139
+ }
5140
+ this.pendingDetailTopKey = false;
5141
+ if (data === "G") {
5142
+ this.setScrollOffset(Number.MAX_SAFE_INTEGER);
5143
+ } else if (matchesKey(data, Key.left) || data === "h") {
5124
5144
  this.showBoard();
5125
5145
  } else if (matchesKey(data, Key.down) || data === "j") {
5126
5146
  this.setScrollOffset(this.state.scrollOffset + 1);
5127
5147
  } else if (matchesKey(data, Key.up) || data === "k") {
5128
5148
  this.setScrollOffset(this.state.scrollOffset - 1);
5149
+ } else if (isDetailHalfPageDown) {
5150
+ this.setScrollOffset(this.state.scrollOffset + halfPageSize);
5151
+ } else if (isDetailHalfPageUp) {
5152
+ this.setScrollOffset(this.state.scrollOffset - halfPageSize);
5129
5153
  } else if (matchesKey(data, Key.pageDown)) {
5130
5154
  this.setScrollOffset(this.state.scrollOffset + pageSize);
5131
5155
  } else if (matchesKey(data, Key.pageUp)) {
@@ -5137,6 +5161,7 @@ class WorkflowStatusView {
5137
5161
  }
5138
5162
  return;
5139
5163
  }
5164
+ this.pendingDetailTopKey = false;
5140
5165
  if (matchesKey(data, Key.down) || data === "j") {
5141
5166
  this.moveSelection(1);
5142
5167
  } else if (matchesKey(data, Key.up) || data === "k") {
@@ -5168,7 +5193,7 @@ class WorkflowStatusView {
5168
5193
  this.ensureSelectedTranscripts(snapshot);
5169
5194
  }
5170
5195
  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");
5196
+ 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
5197
  this.state = page.state;
5173
5198
  return page.lines;
5174
5199
  }
@@ -5210,6 +5235,7 @@ class WorkflowStatusView {
5210
5235
  this.normalizeSelection(snapshot);
5211
5236
  if (!selectedStepDetail(snapshot, this.state.selectedIndex))
5212
5237
  return;
5238
+ this.pendingDetailTopKey = false;
5213
5239
  this.state = { ...this.state, mode: "detail", scrollOffset: 0 };
5214
5240
  this.ensureSelectedTranscripts(snapshot);
5215
5241
  this.tui.requestRender(true);
@@ -5217,6 +5243,7 @@ class WorkflowStatusView {
5217
5243
  showBoard() {
5218
5244
  if (this.state.mode === "board")
5219
5245
  return;
5246
+ this.pendingDetailTopKey = false;
5220
5247
  this.state = { ...this.state, mode: "board", scrollOffset: 0 };
5221
5248
  this.tui.requestRender(true);
5222
5249
  }
@@ -5635,7 +5662,7 @@ function formatWorkflowDoctor(reports) {
5635
5662
  for (const report of reports) {
5636
5663
  const errors = report.issues.filter((issue) => issue.level === "error");
5637
5664
  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.`, "");
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.`, "");
5639
5666
  if (report.issues.length === 0) {
5640
5667
  lines.push("- No liveness issues found.", "");
5641
5668
  continue;
@@ -5789,7 +5816,7 @@ var completedStep = (run, outcome, summary, now, effects) => ({
5789
5816
  ...run.currentStepOmittedAttempts ? { omittedAttempts: run.currentStepOmittedAttempts } : {},
5790
5817
  completedAt: now
5791
5818
  });
5792
- var advanceRun = (workflow, run, outcome, summary, now, effects = {}) => {
5819
+ var advanceRun = (workflow, run, outcome, summary, now, effects = {}, options = {}) => {
5793
5820
  if (run.status !== "running") {
5794
5821
  throw new Error(`workflow is ${run.status}; only a running workflow can advance`);
5795
5822
  }
@@ -5828,6 +5855,7 @@ var advanceRun = (workflow, run, outcome, summary, now, effects = {}) => {
5828
5855
  ...cwd ? { cwd } : {},
5829
5856
  stepHandoff: summary,
5830
5857
  lastSummary: summary,
5858
+ gateArtifact: "",
5831
5859
  gateFeedback: "",
5832
5860
  pausedFrom: undefined,
5833
5861
  pendingGate: undefined,
@@ -5837,8 +5865,9 @@ var advanceRun = (workflow, run, outcome, summary, now, effects = {}) => {
5837
5865
  if (!workflow.definition.steps[target]) {
5838
5866
  throw new Error(`transition target "${target}" does not exist`);
5839
5867
  }
5868
+ const preservesGateRevisionContext = target === run.currentStepId && (options.sameStepHumanGateRevision || Boolean(run.gateArtifact));
5840
5869
  const nextVisitCount = (run.visits[target] ?? 0) + 1;
5841
- const isOverVisitLimit = nextVisitCount > workflow.definition.maxStepVisits;
5870
+ const isOverVisitLimit = !options.sameStepHumanGateRevision && nextVisitCount > workflow.definition.maxStepVisits;
5842
5871
  const visitLimitChanges = isOverVisitLimit ? {
5843
5872
  status: "paused",
5844
5873
  pausedFrom: "running",
@@ -5859,14 +5888,26 @@ var advanceRun = (workflow, run, outcome, summary, now, effects = {}) => {
5859
5888
  currentStepAttempts: undefined,
5860
5889
  currentStepOmittedAttempts: undefined,
5861
5890
  ...cwd ? { cwd } : {},
5862
- stepHandoff: summary,
5891
+ stepHandoff: preservesGateRevisionContext ? run.stepHandoff : summary,
5863
5892
  lastSummary: summary,
5864
- gateFeedback: "",
5893
+ gateArtifact: preservesGateRevisionContext ? run.gateArtifact : "",
5894
+ gateFeedback: preservesGateRevisionContext ? run.gateFeedback : "",
5865
5895
  resumeInput: undefined
5866
5896
  }, now);
5867
5897
  };
5868
5898
 
5869
5899
  // src/engine/gate-transitions.ts
5900
+ var GATE_FEEDBACK_TRUNCATION_SUFFIX = `
5901
+ … [gate feedback truncated by Pi Workflows]`;
5902
+ var MAX_GATE_REJECTION_SUMMARY_CHARS = 500;
5903
+ 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}`;
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)}…`;
5910
+ };
5870
5911
  var beginGate = (workflow, run, outcome, artifact, requestId, now, summary) => {
5871
5912
  if (run.status !== "running") {
5872
5913
  throw new Error(`workflow is ${run.status}; gate submission requires a running workflow`);
@@ -5907,15 +5948,32 @@ var attachGateReviewId = (run, reviewId, now) => {
5907
5948
  }
5908
5949
  return withRunUpdate(run, { pendingGate: { ...run.pendingGate, reviewId } }, now);
5909
5950
  };
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;
5951
+ var failGate = (run, reason, now) => {
5952
+ if (!run.pendingGate)
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);
5963
+ };
5964
+ var storeGateResolution = (run, resolution, now) => {
5965
+ if (!run.pendingGate)
5966
+ return run;
5967
+ return withRunUpdate(run, {
5968
+ pendingGate: {
5969
+ ...run.pendingGate,
5970
+ resolution: {
5971
+ ...resolution,
5972
+ feedback: boundedGateFeedback(resolution.feedback)
5973
+ }
5974
+ }
5975
+ }, now);
5976
+ };
5919
5977
  var resolveGate = (workflow, run, resolution, now) => {
5920
5978
  const pendingGate = run.pendingGate;
5921
5979
  if (!pendingGate)
@@ -5928,16 +5986,17 @@ var resolveGate = (workflow, run, resolution, now) => {
5928
5986
  throw new Error("gate result does not match the current step");
5929
5987
  }
5930
5988
  const outcome = resolution.approved ? step.gate.approvedOutcome : step.gate.rejectedOutcome;
5989
+ const feedback = boundedGateFeedback(resolution.feedback);
5931
5990
  const stepStructuralDigest = workflow.stepStructuralDigests[pendingGate.stepId] ?? "";
5932
5991
  if (resolution.approved && !stepStructuralDigest) {
5933
5992
  throw new Error(`gated step "${pendingGate.stepId}" has no structural digest`);
5934
5993
  }
5935
- const summary = resolution.approved ? pendingGate.summary ?? "" : resolution.feedback ? `Gate rejected: ${resolution.feedback}` : "Gate rejected";
5994
+ const summary = resolution.approved ? pendingGate.summary ?? "" : gateRejectionSummary(feedback);
5936
5995
  const decidedRun = recordCurrentGateDecision(run, {
5937
5996
  provider: pendingGate.provider,
5938
5997
  requestId: pendingGate.requestId,
5939
5998
  approved: resolution.approved,
5940
- feedback: resolution.feedback,
5999
+ feedback,
5941
6000
  resolvedAt: resolution.resolvedAt,
5942
6001
  ...pendingGate.reviewId ? { reviewId: pendingGate.reviewId } : {}
5943
6002
  }, now);
@@ -5946,9 +6005,13 @@ var resolveGate = (workflow, run, resolution, now) => {
5946
6005
  pendingGate: undefined,
5947
6006
  pausedFrom: undefined,
5948
6007
  pauseReason: undefined,
5949
- gateFeedback: resolution.feedback
6008
+ gateArtifact: resolution.approved ? "" : pendingGate.artifact,
6009
+ gateFeedback: resolution.approved ? "" : feedback
5950
6010
  }, now);
5951
- const advanced = advanceRun(workflow, runnableRun, outcome, summary, now);
6011
+ const isSameStepHumanRevision = !resolution.approved && step.transitions[outcome] === pendingGate.stepId;
6012
+ const advanced = advanceRun(workflow, runnableRun, outcome, summary, now, {}, {
6013
+ sameStepHumanGateRevision: isSameStepHumanRevision
6014
+ });
5952
6015
  const completedApprovedGate = resolution.approved && advanced.history.length > runnableRun.history.length;
5953
6016
  const history = completedApprovedGate ? advanced.history.map((entry, index) => index === advanced.history.length - 1 ? {
5954
6017
  ...entry,
@@ -5956,7 +6019,7 @@ var resolveGate = (workflow, run, resolution, now) => {
5956
6019
  approval: {
5957
6020
  requestId: pendingGate.requestId,
5958
6021
  artifact: pendingGate.artifact,
5959
- feedback: resolution.feedback,
6022
+ feedback,
5960
6023
  stepStructuralDigest
5961
6024
  }
5962
6025
  } : entry) : advanced.history;
@@ -5965,9 +6028,10 @@ var resolveGate = (workflow, run, resolution, now) => {
5965
6028
  history,
5966
6029
  ...completedApprovedGate ? {
5967
6030
  reviewedArtifact: pendingGate.artifact,
5968
- reviewedFeedback: resolution.feedback
6031
+ reviewedFeedback: feedback
5969
6032
  } : {},
5970
- gateFeedback: resolution.feedback
6033
+ gateArtifact: resolution.approved ? "" : pendingGate.artifact,
6034
+ gateFeedback: resolution.approved ? "" : feedback
5971
6035
  };
5972
6036
  };
5973
6037
  // src/engine/run-lifecycle.ts
@@ -6251,6 +6315,7 @@ var reconcileRun = (run, workflow, now) => {
6251
6315
  pausedFrom: "running",
6252
6316
  failedStepId: undefined,
6253
6317
  pauseReason: `Configuration changed; restarted step "${restartedStep}"`,
6318
+ gateArtifact: "",
6254
6319
  gateFeedback: ""
6255
6320
  }, now)
6256
6321
  };
@@ -6263,6 +6328,7 @@ var reconcileRun = (run, workflow, now) => {
6263
6328
  pausedFrom: "running",
6264
6329
  failedStepId: undefined,
6265
6330
  pauseReason: `Configuration changed; restarted step "${run.currentStepId}"`,
6331
+ gateArtifact: "",
6266
6332
  gateFeedback: ""
6267
6333
  } : {};
6268
6334
  return {
@@ -6732,7 +6798,7 @@ function currentStepHandoff(run) {
6732
6798
  "Incoming previous-step handoff:",
6733
6799
  incomingHandoff,
6734
6800
  "",
6735
- "Latest paused attempt:",
6801
+ "Latest current-step summary:",
6736
6802
  run.lastSummary
6737
6803
  ].join(`
6738
6804
  `);
@@ -6757,6 +6823,7 @@ function createTemplateValues({
6757
6823
  "last.summary": currentStepHandoff(run),
6758
6824
  "reviewed.artifact": run.reviewedArtifact ?? "",
6759
6825
  "reviewed.feedback": run.reviewedFeedback ?? "",
6826
+ "gate.artifact": run.gateArtifact ?? "",
6760
6827
  "gate.feedback": run.gateFeedback,
6761
6828
  "resume.input": run.resumeInput ?? ""
6762
6829
  };
@@ -68,7 +68,7 @@ steps:
68
68
  timeoutMs: 30000
69
69
  transitions:
70
70
  approved: implement
71
- changes-requested: $pause
71
+ changes-requested: plan
72
72
  blocked: $pause
73
73
  implement:
74
74
  title: Implement the approved plan
@@ -1,8 +1,15 @@
1
1
  Create a concrete, decision-ready implementation plan from the inspected
2
- feedback. Account for previous Plannotator feedback:
2
+ feedback. The previously rejected artifact is:
3
+
4
+ {{gate.artifact}}
5
+
6
+ Account for previous Plannotator feedback:
3
7
 
4
8
  {{gate.feedback}}
5
9
 
10
+ When feedback is non-empty, revise the rejected artifact and submit the complete
11
+ proposal for another review. Each rejection returns to this same planning step.
12
+
6
13
  Write the artifact for a human reviewer first. Use this order:
7
14
 
8
15
  # <Short outcome-oriented title>
@@ -76,7 +76,7 @@ steps:
76
76
  timeoutMs: 30000
77
77
  transitions:
78
78
  approved: implement
79
- changes-requested: $pause
79
+ changes-requested: plan
80
80
  blocked: $pause
81
81
 
82
82
  implement:
@@ -76,7 +76,7 @@ steps:
76
76
  timeoutMs: 30000
77
77
  transitions:
78
78
  approved: publish
79
- changes-requested: $pause
79
+ changes-requested: review
80
80
  blocked: $pause
81
81
 
82
82
  publish:
@@ -6,9 +6,16 @@ Review input:
6
6
  Fetched evidence:
7
7
  {{last.summary}}
8
8
 
9
+ Previously rejected artifact:
10
+ {{gate.artifact}}
11
+
9
12
  Feedback from a previously rejected review:
10
13
  {{gate.feedback}}
11
14
 
15
+ When feedback is non-empty, revise the rejected artifact against current
16
+ evidence and submit the complete proposal for another review. Each rejection
17
+ returns to this same planning step on the existing checkout.
18
+
12
19
  Re-verify the same review and the current Git root, registered worktree,
13
20
  branch, HEAD, and status. Work on top of this checkout exactly as it exists.
14
21
  Never create, switch, reset, clean, delete, or prepare another branch or
@@ -58,5 +65,5 @@ Call `structured_output` alone with outcome `submit` only when the complete
58
65
  plan can be implemented and published without another planning decision. Put
59
66
  the Markdown plan in `artifact` and a self-contained handoff, including the
60
67
  unchanged JSON contract, in `summary`. Use `blocked` when identity, scope,
61
- authority, anchors, or evidence cannot be made safe. Rejection pauses through
62
- the YAML transition; never automatically replan or create a new workspace.
68
+ authority, anchors, or evidence cannot be made safe. A rejected proposal is
69
+ revised here; never restart the workflow or create a new workspace.
@@ -7,9 +7,16 @@ Review input:
7
7
  Fetched evidence:
8
8
  {{last.summary}}
9
9
 
10
+ Previously rejected artifact:
11
+ {{gate.artifact}}
12
+
10
13
  Feedback from a previously rejected review:
11
14
  {{gate.feedback}}
12
15
 
16
+ When feedback is non-empty, revise the rejected artifact against current
17
+ evidence and submit the complete review proposal for another review. Each
18
+ rejection returns to this same review step.
19
+
13
20
  Treat the fetched packet as evidence, not a verdict. Refresh the same-host head
14
21
  SHA, diff, checks, and discussions with configured read-only MCP/CLI/cURL calls.
15
22
  Inspect changed code, callers, tests, repository instructions, and relevant
@@ -65,5 +72,5 @@ delete, force-push, cross hosts, or perform an unlisted mutation.
65
72
  Call `structured_output` alone with outcome `submit`. Put the complete Markdown
66
73
  review in `artifact` and a self-contained review/publication handoff in
67
74
  `summary`. Use `blocked` when current evidence cannot support a safe,
68
- publishable review. Rejection follows the YAML `$pause` transition; never
69
- create a replacement plan automatically.
75
+ publishable review. A rejected proposal is revised here; never restart the
76
+ workflow or create a new workspace.