@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 +68 -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 +6 -6
- package/examples/starter-kit/mr-review.workflow.yaml +5 -5
- 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 +18 -6
- package/examples/starter-kit/work.workflow.yaml +18 -6
- 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
|
@@ -110,6 +110,7 @@ export const reconcileRun = (
|
|
|
110
110
|
pausedFrom: 'running',
|
|
111
111
|
failedStepId: undefined,
|
|
112
112
|
pauseReason: `Configuration changed; restarted step "${restartedStep}"`,
|
|
113
|
+
gateArtifact: '',
|
|
113
114
|
gateFeedback: '',
|
|
114
115
|
},
|
|
115
116
|
now,
|
|
@@ -127,6 +128,7 @@ export const reconcileRun = (
|
|
|
127
128
|
pausedFrom: 'running',
|
|
128
129
|
failedStepId: undefined,
|
|
129
130
|
pauseReason: `Configuration changed; restarted step "${run.currentStepId}"`,
|
|
131
|
+
gateArtifact: '',
|
|
130
132
|
gateFeedback: '',
|
|
131
133
|
}
|
|
132
134
|
: {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from 'node:path';
|
|
2
2
|
import {
|
|
3
|
+
MAX_GATE_FEEDBACK_CHARS,
|
|
3
4
|
MAX_RESUME_INPUT_CHARS,
|
|
4
5
|
MAX_STEP_TRACE_ARTIFACT_CHARS,
|
|
5
6
|
MAX_STEP_TRACE_ATTEMPTS,
|
|
@@ -40,6 +41,7 @@ const isGateApproval = (value: unknown): value is GateApproval =>
|
|
|
40
41
|
typeof value.artifact === 'string' &&
|
|
41
42
|
value.artifact.trim().length > 0 &&
|
|
42
43
|
typeof value.feedback === 'string' &&
|
|
44
|
+
value.feedback.length <= MAX_GATE_FEEDBACK_CHARS &&
|
|
43
45
|
typeof value.stepStructuralDigest === 'string' &&
|
|
44
46
|
value.stepStructuralDigest.length > 0;
|
|
45
47
|
|
|
@@ -221,6 +223,7 @@ const isGateResolution = (value: unknown): value is GateResolution =>
|
|
|
221
223
|
isRecord(value) &&
|
|
222
224
|
typeof value.approved === 'boolean' &&
|
|
223
225
|
typeof value.feedback === 'string' &&
|
|
226
|
+
value.feedback.length <= MAX_GATE_FEEDBACK_CHARS &&
|
|
224
227
|
typeof value.resolvedAt === 'number';
|
|
225
228
|
|
|
226
229
|
const isPendingGate = (value: unknown): value is PendingGate =>
|
|
@@ -333,13 +336,17 @@ export const isWorkflowRun = (value: unknown): value is WorkflowRun => {
|
|
|
333
336
|
typeof value.startedAt === 'number' &&
|
|
334
337
|
typeof value.updatedAt === 'number' &&
|
|
335
338
|
typeof value.lastSummary === 'string' &&
|
|
336
|
-
typeof value.gateFeedback === 'string'
|
|
339
|
+
typeof value.gateFeedback === 'string' &&
|
|
340
|
+
value.gateFeedback.length <= MAX_GATE_FEEDBACK_CHARS;
|
|
337
341
|
if (!hasValidRequiredFields) return false;
|
|
338
342
|
|
|
339
343
|
const hasValidOptionalFields =
|
|
340
344
|
isOptionalString(value.reviewedArtifact) &&
|
|
341
345
|
isOptionalString(value.reviewedFeedback) &&
|
|
346
|
+
(typeof value.reviewedFeedback !== 'string' ||
|
|
347
|
+
value.reviewedFeedback.length <= MAX_GATE_FEEDBACK_CHARS) &&
|
|
342
348
|
isOptionalString(value.stepHandoff) &&
|
|
349
|
+
isOptionalString(value.gateArtifact) &&
|
|
343
350
|
isOptionalResumeInput(value.resumeInput) &&
|
|
344
351
|
isOptionalString(value.pauseReason) &&
|
|
345
352
|
isOptionalString(value.failedStepId) &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const RUN_STATE_VERSION = 1 as const;
|
|
2
|
+
export const MAX_GATE_FEEDBACK_CHARS = 50_000;
|
|
2
3
|
export const MAX_RESUME_INPUT_CHARS = 16_000;
|
|
3
4
|
export const MAX_STEP_TRACE_TASK_CHARS = 64_000;
|
|
4
5
|
export const MAX_STEP_TRACE_ATTEMPTS = 16;
|
|
@@ -158,6 +159,8 @@ export type WorkflowRun = {
|
|
|
158
159
|
*/
|
|
159
160
|
readonly stepHandoff?: string | undefined;
|
|
160
161
|
readonly lastSummary: string;
|
|
162
|
+
/** Opaque artifact returned by the latest rejected or failed gate. */
|
|
163
|
+
readonly gateArtifact?: string | undefined;
|
|
161
164
|
readonly gateFeedback: string;
|
|
162
165
|
/** User-authored guidance supplied for the current resume attempt. */
|
|
163
166
|
readonly resumeInput?: string | undefined;
|
package/src/engine/state.ts
CHANGED
package/src/prompt/template.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { WorkflowRun } from '../engine/state.ts';
|
|
|
7
7
|
export type TemplateValues = Readonly<Record<string, string>>;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Combines the incoming handoff with the latest
|
|
11
|
-
*
|
|
10
|
+
* Combines the incoming handoff with the latest current-step summary without
|
|
11
|
+
* duplicating identical content.
|
|
12
12
|
*
|
|
13
13
|
* @param run - Current workflow run.
|
|
14
14
|
* @returns The handoff text for the active step.
|
|
@@ -27,7 +27,7 @@ export function currentStepHandoff(run: WorkflowRun): string {
|
|
|
27
27
|
'Incoming previous-step handoff:',
|
|
28
28
|
incomingHandoff,
|
|
29
29
|
'',
|
|
30
|
-
'Latest
|
|
30
|
+
'Latest current-step summary:',
|
|
31
31
|
run.lastSummary,
|
|
32
32
|
].join('\n');
|
|
33
33
|
}
|
|
@@ -78,6 +78,7 @@ export function createTemplateValues({
|
|
|
78
78
|
'last.summary': currentStepHandoff(run),
|
|
79
79
|
'reviewed.artifact': run.reviewedArtifact ?? '',
|
|
80
80
|
'reviewed.feedback': run.reviewedFeedback ?? '',
|
|
81
|
+
'gate.artifact': run.gateArtifact ?? '',
|
|
81
82
|
'gate.feedback': run.gateFeedback,
|
|
82
83
|
'resume.input': run.resumeInput ?? '',
|
|
83
84
|
};
|
package/src/workflow-doctor.ts
CHANGED
|
@@ -235,7 +235,7 @@ export function formatWorkflowDoctor(
|
|
|
235
235
|
'',
|
|
236
236
|
`Result: ${errors.length > 0 ? 'ERROR' : warnings.length > 0 ? 'WARNING' : 'PASS'}`,
|
|
237
237
|
'',
|
|
238
|
-
`Runtime loop guard: each step
|
|
238
|
+
`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.`,
|
|
239
239
|
'',
|
|
240
240
|
);
|
|
241
241
|
if (report.issues.length === 0) {
|
|
@@ -135,6 +135,7 @@ function nextScrollOffset(state: ViewportState, value: number): number {
|
|
|
135
135
|
export class WorkflowStatusView implements Component {
|
|
136
136
|
private timer: RefreshTimer | undefined;
|
|
137
137
|
private state = initialViewportState();
|
|
138
|
+
private pendingDetailTopKey = false;
|
|
138
139
|
private readonly statusShortcutLabel: string;
|
|
139
140
|
private readonly dependencies: WorkflowStatusViewDependencies;
|
|
140
141
|
private readonly transcriptCache = new Map<string, StepTranscriptViewState>();
|
|
@@ -180,11 +181,15 @@ export class WorkflowStatusView implements Component {
|
|
|
180
181
|
|
|
181
182
|
/** Handle close and scrolling key input. */
|
|
182
183
|
handleInput(data: string): void {
|
|
184
|
+
const isDetailHalfPageDown =
|
|
185
|
+
this.state.mode === 'detail' && matchesKey(data, Key.ctrl('d'));
|
|
186
|
+
const isDetailHalfPageUp =
|
|
187
|
+
this.state.mode === 'detail' && matchesKey(data, Key.ctrl('u'));
|
|
183
188
|
if (
|
|
184
189
|
data === 'q' ||
|
|
185
190
|
data === 'Q' ||
|
|
186
191
|
matchesKey(data, 'ctrl+c') ||
|
|
187
|
-
matchesKey(data, 'ctrl+d') ||
|
|
192
|
+
(this.state.mode !== 'detail' && matchesKey(data, 'ctrl+d')) ||
|
|
188
193
|
matchesKey(data, this.statusShortcut)
|
|
189
194
|
) {
|
|
190
195
|
this.close();
|
|
@@ -199,13 +204,31 @@ export class WorkflowStatusView implements Component {
|
|
|
199
204
|
return;
|
|
200
205
|
}
|
|
201
206
|
const pageSize = Math.max(1, this.state.viewportRows - 2);
|
|
207
|
+
const contentHeight = Math.max(1, this.state.viewportRows - 1);
|
|
208
|
+
const halfPageSize = Math.max(1, Math.floor(contentHeight / 2));
|
|
202
209
|
if (this.state.mode === 'detail') {
|
|
203
|
-
if (
|
|
210
|
+
if (data === 'gg' || (data === 'g' && this.pendingDetailTopKey)) {
|
|
211
|
+
this.pendingDetailTopKey = false;
|
|
212
|
+
this.setScrollOffset(0);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (data === 'g') {
|
|
216
|
+
this.pendingDetailTopKey = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.pendingDetailTopKey = false;
|
|
220
|
+
if (data === 'G') {
|
|
221
|
+
this.setScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
222
|
+
} else if (matchesKey(data, Key.left) || data === 'h') {
|
|
204
223
|
this.showBoard();
|
|
205
224
|
} else if (matchesKey(data, Key.down) || data === 'j') {
|
|
206
225
|
this.setScrollOffset(this.state.scrollOffset + 1);
|
|
207
226
|
} else if (matchesKey(data, Key.up) || data === 'k') {
|
|
208
227
|
this.setScrollOffset(this.state.scrollOffset - 1);
|
|
228
|
+
} else if (isDetailHalfPageDown) {
|
|
229
|
+
this.setScrollOffset(this.state.scrollOffset + halfPageSize);
|
|
230
|
+
} else if (isDetailHalfPageUp) {
|
|
231
|
+
this.setScrollOffset(this.state.scrollOffset - halfPageSize);
|
|
209
232
|
} else if (matchesKey(data, Key.pageDown)) {
|
|
210
233
|
this.setScrollOffset(this.state.scrollOffset + pageSize);
|
|
211
234
|
} else if (matchesKey(data, Key.pageUp)) {
|
|
@@ -217,6 +240,7 @@ export class WorkflowStatusView implements Component {
|
|
|
217
240
|
}
|
|
218
241
|
return;
|
|
219
242
|
}
|
|
243
|
+
this.pendingDetailTopKey = false;
|
|
220
244
|
if (matchesKey(data, Key.down) || data === 'j') {
|
|
221
245
|
this.moveSelection(1);
|
|
222
246
|
} else if (matchesKey(data, Key.up) || data === 'k') {
|
|
@@ -287,7 +311,7 @@ export class WorkflowStatusView implements Component {
|
|
|
287
311
|
this.statusShortcutLabel,
|
|
288
312
|
this.theme,
|
|
289
313
|
this.state.mode === 'detail'
|
|
290
|
-
? '
|
|
314
|
+
? '↑↓/jk · Ctrl+D/U half-page · gg/G top/bottom · PgUp/PgDn · ←/h/Esc'
|
|
291
315
|
: '↑/↓ or j/k select · Enter/→/l inspect · PgUp/PgDn',
|
|
292
316
|
);
|
|
293
317
|
this.state = page.state;
|
|
@@ -339,6 +363,7 @@ export class WorkflowStatusView implements Component {
|
|
|
339
363
|
if (!snapshot) return;
|
|
340
364
|
this.normalizeSelection(snapshot);
|
|
341
365
|
if (!selectedStepDetail(snapshot, this.state.selectedIndex)) return;
|
|
366
|
+
this.pendingDetailTopKey = false;
|
|
342
367
|
this.state = { ...this.state, mode: 'detail', scrollOffset: 0 };
|
|
343
368
|
this.ensureSelectedTranscripts(snapshot);
|
|
344
369
|
this.tui.requestRender(true);
|
|
@@ -346,6 +371,7 @@ export class WorkflowStatusView implements Component {
|
|
|
346
371
|
|
|
347
372
|
private showBoard(): void {
|
|
348
373
|
if (this.state.mode === 'board') return;
|
|
374
|
+
this.pendingDetailTopKey = false;
|
|
349
375
|
this.state = { ...this.state, mode: 'board', scrollOffset: 0 };
|
|
350
376
|
this.tui.requestRender(true);
|
|
351
377
|
}
|