@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 +60 -32
- package/dist/index.js +96 -29
- package/examples/mr-comments.workflow.yaml +1 -1
- package/examples/prompts/mr-comments/plan.md +8 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +1 -1
- package/examples/starter-kit/mr-review.workflow.yaml +1 -1
- package/examples/starter-kit/steps/mr-comment/plan.md +9 -2
- package/examples/starter-kit/steps/mr-review/review.md +9 -2
- package/examples/starter-kit/steps/shared/prepare-workspace.md +42 -4
- package/examples/starter-kit/steps/ticket/plan.md +31 -2
- package/examples/starter-kit/steps/work/plan.md +31 -2
- package/examples/starter-kit/ticket.workflow.yaml +14 -2
- package/examples/starter-kit/work.workflow.yaml +14 -2
- package/package.json +1 -1
- package/src/config/types.ts +1 -1
- package/src/config/validation/prompt.ts +1 -0
- package/src/engine/create-run.ts +1 -0
- package/src/engine/gate-transitions.ts +80 -33
- package/src/engine/run-advance.ts +20 -3
- package/src/engine/run-reconciliation.ts +2 -0
- package/src/engine/run-validation.ts +8 -1
- package/src/engine/state-types.ts +3 -0
- package/src/engine/state.ts +1 -0
- package/src/prompt/template.ts +4 -3
- package/src/workflow-doctor.ts +1 -1
- package/src/workflow-status/view.ts +29 -3
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
summaries stay separate: prompts opt into
|
|
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
|
|
326
|
-
reachable downstream child, revisit, recovery attempt, and resume.
|
|
327
|
-
|
|
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.
|
|
334
|
-
|
|
335
|
-
|
|
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:
|
|
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.
|
|
544
|
-
|
|
545
|
-
|
|
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:
|
|
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
|
|
743
|
-
|
|
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
|
-
|
|
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 (
|
|
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" ? "
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
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 ?? "" :
|
|
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
|
|
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
|
-
|
|
6008
|
+
gateArtifact: resolution.approved ? "" : pendingGate.artifact,
|
|
6009
|
+
gateFeedback: resolution.approved ? "" : feedback
|
|
5950
6010
|
}, now);
|
|
5951
|
-
const
|
|
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
|
|
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:
|
|
6031
|
+
reviewedFeedback: feedback
|
|
5969
6032
|
} : {},
|
|
5970
|
-
|
|
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
|
|
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
|
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
Create a concrete, decision-ready implementation plan from the inspected
|
|
2
|
-
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>
|
|
@@ -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.
|
|
62
|
-
|
|
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.
|
|
69
|
-
create a
|
|
75
|
+
publishable review. A rejected proposal is revised here; never restart the
|
|
76
|
+
workflow or create a new workspace.
|