@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.
@@ -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;
@@ -1,6 +1,7 @@
1
1
  export { createRun } from './create-run.ts';
2
2
  export { isWorkflowRun } from './run-validation.ts';
3
3
  export {
4
+ MAX_GATE_FEEDBACK_CHARS,
4
5
  MAX_RESUME_INPUT_CHARS,
5
6
  MAX_STEP_TRACE_ATTEMPTS,
6
7
  MAX_STEP_TRACE_LOG_CHARS,
@@ -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 paused-attempt
11
- * summary without duplicating identical content.
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 paused attempt:',
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
  };
@@ -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 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.`,
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 (matchesKey(data, Key.left) || data === 'h') {
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
- ? '↑/↓ or j/k scroll · PgUp/PgDn · ←/h/Esc back'
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
  }