@wichayutdew/pi-workflows 2.0.0 → 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
@@ -71,6 +71,14 @@ user-owned starting point for:
71
71
  | `/mr-review` | Fetch a hosted review, produce a child review, approve it, publish it, and verify the remote effect. |
72
72
  | `/mr-comment` | Fetch review comments, plan and approve fixes, edit only the current branch/worktree, verify, push, reply. |
73
73
 
74
+ The starter YAML explicitly selects Pi Subagents' standard role profiles:
75
+ `scout` for evidence gathering, `planner` for implementation plans, `worker`
76
+ for mutations and publication, and `reviewer` for independent review and
77
+ verification. Change those `subagent.agent` values when your own Pi Subagents
78
+ configuration uses different profiles. The generic `pi-workflows.step` profile
79
+ remains available for user-authored `subagent: {}` steps, but the starter kit
80
+ does not rely on it.
81
+
74
82
  Install the integrations required by these four examples:
75
83
 
76
84
  ```bash
@@ -137,10 +145,14 @@ workspace:
137
145
  `/work` and `/ticket` bind the exact worktree returned by their user-authored
138
146
  preparation prompt. `/mr-comment` intentionally has no workspace-binding step:
139
147
  every child stays on the branch and worktree from which the run started.
140
- Approval rejection pauses either review workflow; it never launches an
141
- automatic replacement plan. Remote publication prompts first check whether an
142
- approved effect already exists because the harness cannot guarantee
143
- 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.
144
156
 
145
157
  ## Add a workflow
146
158
 
@@ -210,16 +222,16 @@ loaded extensions or prompt resources are diagnosed before aliases register.
210
222
 
211
223
  Top-level fields:
212
224
 
213
- | Field | Required | Default | Description |
214
- | ----------------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
215
- | `version` | Yes | — | Configuration contract version. Currently `1`. |
216
- | `id` | Yes | — | Stable workflow identifier. |
217
- | `command` | Yes | — | Slash command without `/`. |
218
- | `description` | Yes | — | Command description. |
219
- | `start` | Yes | — | First step identifier. |
220
- | `steps` | Yes | — | Step map. Order is controlled by transitions, not file order. |
221
- | `maxStepVisits` | No | `5` | Loop guard for each step. |
222
- | `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. |
223
235
 
224
236
  Each step supports:
225
237
 
@@ -245,6 +257,7 @@ Supported prompt variables:
245
257
  {{last.summary}}
246
258
  {{reviewed.artifact}}
247
259
  {{reviewed.feedback}}
260
+ {{gate.artifact}}
248
261
  {{gate.feedback}}
249
262
  {{resume.input}}
250
263
  ```
@@ -252,10 +265,11 @@ Supported prompt variables:
252
265
  Unknown variables fail configuration loading.
253
266
 
254
267
  `{{last.summary}}` normally contains the previous completed step's handoff.
255
- After a step-requested `$pause`, it contains both the preserved incoming
256
- previous-step handoff and the latest paused-attempt summary. Gate artifacts and
257
- summaries stay separate: prompts opt into the opaque approved artifact through
258
- `{{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}}`.
259
273
  `{{resume.input}}` contains guidance supplied to the current attempt through
260
274
  `/workflow-resume [guidance]`. If a prompt omits that variable, the harness
261
275
  adds the guidance in a clearly delimited standard section, so every workflow
@@ -314,17 +328,19 @@ workspace:
314
328
  On a binding outcome, the result must include
315
329
  `workspace: { cwd: "/absolute/directory" }`. The harness resolves its real path,
316
330
  requires an existing directory under one configured root relative to the
317
- run-start directory, persists it once, and passes that exact directory to every
318
- reachable downstream child, revisit, recovery attempt, and resume. Other
319
- 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
320
335
  reachable nonterminal steps after a binding must be delegated because the main
321
336
  Pi process cannot change its working directory.
322
337
 
323
338
  The workflow prompt owns how the directory is prepared and what it represents.
324
339
  The harness does not know Git, worktrees, languages, frameworks, or command
325
- syntax, and it never derives a directory from summaries or gate artifacts. A
326
- legacy checkpoint without a captured directory fails closed before delegation;
327
- 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.
328
344
 
329
345
  Use a profile name directly when only the child profile changes:
330
346
 
@@ -524,7 +540,7 @@ gate:
524
540
  rejectedOutcome: changes-requested
525
541
  transitions:
526
542
  approved: implement
527
- changes-requested: $pause
543
+ changes-requested: plan
528
544
  blocked: $pause
529
545
  ```
530
546
 
@@ -532,9 +548,13 @@ When the step completes with outcome `submit`, it must include the full content
532
548
  in `artifact`. Pi shows Approve, Request changes, and Pause workflow. Requested
533
549
  changes are returned through `{{gate.feedback}}`; approval persists the
534
550
  artifact as `{{reviewed.artifact}}` while the step's `summary` remains the
535
- compact handoff. A requested change pauses at the same step with feedback, so
536
- revision starts only after explicit `/workflow-resume`. Dismissing the panel
537
- 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.
538
558
 
539
559
  Dialog-capable UI is available in Pi TUI and RPC modes. In print or JSON mode,
540
560
  the gate pauses safely until resumed in TUI or RPC.
@@ -575,7 +595,7 @@ gate:
575
595
  timeoutMs: 30000
576
596
  transitions:
577
597
  approved: implement
578
- changes-requested: $pause
598
+ changes-requested: plan
579
599
  blocked: $pause
580
600
  ```
581
601
 
@@ -589,6 +609,17 @@ review identifier and accepts only the matching decision. On approval, the
589
609
  artifact is preserved as an opaque template value; the separate `summary`
590
610
  remains the next step's compact handoff.
591
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
+
592
623
  The artifact is never substituted for a missing summary. If an older pending
593
624
  gate has no separately stored summary, its transition uses an empty compact
594
625
  handoff.
@@ -731,8 +762,10 @@ a failed or aborted run, and `◆` a paused step or pending review.
731
762
  Use `↑`/`↓` or `j`/`k` to select a step, then `Enter`, `→`, or `l` to inspect
732
763
  the bounded task supplied to each attempt, its result and gate decision, and a
733
764
  chronological execution log when available. In the detail view,
734
- `↑`/`↓` or `j`/`k` scrolls; `←`, `h`, or `Esc` returns to the board; `q` or the
735
- 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.
736
769
  Trace references and bounded task/result evidence live in the checkpoint, so
737
770
  completed, paused, resumed, and restored runs remain inspectable. Child logs
738
771
  are path-confined, size-bounded, control-sanitized, and redact common
@@ -763,7 +796,10 @@ Unreachable steps and cycles are reported as warnings; a cycle with an exit may
763
796
  run, but `maxStepVisits` pauses the uninterrupted run before a step can execute
764
797
  more than its configured limit. An explicit resume can continue from that
765
798
  checkpoint, so the guard bounds automatic graph advancement rather than
766
- 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.
767
803
 
768
804
  Configured aliases also accept multiline input. For example, if `work` is a
769
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>
@@ -11,7 +11,7 @@ steps:
11
11
  prompt:
12
12
  file: steps/mr-comment/fetch.md
13
13
  subagent:
14
- agent: pi-workflows.step
14
+ agent: scout
15
15
  context: fresh
16
16
  timeoutMs: 1800000
17
17
  permissions:
@@ -49,7 +49,7 @@ steps:
49
49
  prompt:
50
50
  file: steps/mr-comment/plan.md
51
51
  subagent:
52
- agent: pi-workflows.step
52
+ agent: planner
53
53
  context: fresh
54
54
  timeoutMs: 1800000
55
55
  permissions:
@@ -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:
@@ -84,7 +84,7 @@ steps:
84
84
  prompt:
85
85
  file: steps/mr-comment/implement.md
86
86
  subagent:
87
- agent: pi-workflows.step
87
+ agent: worker
88
88
  context: fresh
89
89
  timeoutMs: 7200000
90
90
  artifacts: true
@@ -104,7 +104,7 @@ steps:
104
104
  prompt:
105
105
  file: steps/mr-comment/verify.md
106
106
  subagent:
107
- agent: pi-workflows.step
107
+ agent: reviewer
108
108
  context: fresh
109
109
  timeoutMs: 3600000
110
110
  permissions:
@@ -125,7 +125,7 @@ steps:
125
125
  prompt:
126
126
  file: steps/mr-comment/publish.md
127
127
  subagent:
128
- agent: pi-workflows.step
128
+ agent: worker
129
129
  context: fresh
130
130
  timeoutMs: 1800000
131
131
  permissions: