@wichayutdew/pi-workflows 1.0.0 → 2.0.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.
Files changed (131) hide show
  1. package/README.md +312 -221
  2. package/agents/step.md +4 -11
  3. package/dist/index.js +2556 -961
  4. package/examples/mr-comments.workflow.yaml +18 -8
  5. package/examples/prompts/mr-comments/implement.md +13 -9
  6. package/examples/prompts/mr-comments/plan.md +44 -15
  7. package/examples/prompts/mr-comments/verify.md +8 -7
  8. package/examples/settings.yaml +7 -1
  9. package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
  10. package/examples/starter-kit/mr-review.workflow.yaml +130 -0
  11. package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
  12. package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
  13. package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
  14. package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
  15. package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
  16. package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
  17. package/examples/starter-kit/steps/mr-review/publish.md +30 -0
  18. package/examples/starter-kit/steps/mr-review/review.md +69 -0
  19. package/examples/starter-kit/steps/mr-review/verify.md +24 -0
  20. package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
  21. package/examples/starter-kit/steps/ticket/implement.md +35 -0
  22. package/examples/starter-kit/steps/ticket/plan.md +40 -0
  23. package/examples/starter-kit/steps/ticket/verify.md +29 -0
  24. package/examples/starter-kit/steps/work/implement.md +34 -0
  25. package/examples/starter-kit/steps/work/plan.md +38 -0
  26. package/examples/starter-kit/steps/work/verify.md +30 -0
  27. package/examples/starter-kit/ticket.workflow.yaml +100 -0
  28. package/examples/starter-kit/work.workflow.yaml +97 -0
  29. package/package.json +3 -3
  30. package/schemas/workflow.schema.json +78 -35
  31. package/src/command-names.ts +1 -0
  32. package/src/commands.ts +33 -4
  33. package/src/config/ceiling.ts +6 -8
  34. package/src/config/load-workflows.ts +12 -1
  35. package/src/config/step-digests.ts +19 -0
  36. package/src/config/types.ts +17 -11
  37. package/src/config/validation/permissions.ts +5 -45
  38. package/src/config/validation/prompt.ts +3 -0
  39. package/src/config/validation/shared.ts +0 -2
  40. package/src/config/validation/step.ts +106 -2
  41. package/src/config/validation/workflow.ts +76 -0
  42. package/src/engine/create-run.ts +4 -0
  43. package/src/engine/gate-transitions.ts +58 -6
  44. package/src/engine/reconciliation-history.ts +61 -24
  45. package/src/engine/run-advance.ts +37 -4
  46. package/src/engine/run-lifecycle.ts +11 -0
  47. package/src/engine/run-reconciliation.ts +51 -21
  48. package/src/engine/run-validation.ts +236 -0
  49. package/src/engine/run-workflow-validation.ts +191 -0
  50. package/src/engine/state-types.ts +109 -4
  51. package/src/engine/state.ts +15 -1
  52. package/src/engine/step-trace.ts +495 -0
  53. package/src/engine/transitions.ts +1 -0
  54. package/src/harness/action-context.ts +31 -6
  55. package/src/harness/core-actions.ts +30 -1
  56. package/src/harness/delegation-control-actions.ts +27 -2
  57. package/src/harness/delegation-failure.ts +42 -9
  58. package/src/harness/delegation-plan.ts +96 -40
  59. package/src/harness/delegation-recovery-validation.ts +3 -1
  60. package/src/harness/delegation-response-actions.ts +73 -3
  61. package/src/harness/delegation-retry-policy.ts +5 -3
  62. package/src/harness/dependencies.ts +11 -2
  63. package/src/harness/gate-submission-action.ts +5 -31
  64. package/src/harness/lifecycle-actions.ts +4 -5
  65. package/src/harness/pause-actions.ts +10 -1
  66. package/src/harness/plannotator-result-actions.ts +20 -11
  67. package/src/harness/prompt-gate-actions.ts +40 -2
  68. package/src/harness/resume-action.ts +75 -1
  69. package/src/harness/start-actions.ts +77 -3
  70. package/src/harness/status-actions.ts +7 -17
  71. package/src/harness/step-effects.ts +61 -0
  72. package/src/harness/step-execution-actions.ts +181 -25
  73. package/src/harness/step-reporting.ts +168 -0
  74. package/src/harness/types.ts +1 -0
  75. package/src/harness/workspace-directory.ts +67 -0
  76. package/src/harness.ts +37 -9
  77. package/src/integrations/plannotator-requests.ts +40 -5
  78. package/src/integrations/subagents/child-policy-sections.ts +71 -72
  79. package/src/integrations/subagents/child-policy-types.ts +7 -7
  80. package/src/integrations/subagents/child-policy-validation.ts +10 -4
  81. package/src/integrations/subagents/child-runtime-completion.ts +1 -0
  82. package/src/integrations/subagents/child-runtime-files.ts +29 -86
  83. package/src/integrations/subagents/child-runtime-policy.ts +8 -33
  84. package/src/integrations/subagents/child-runtime.ts +3 -15
  85. package/src/integrations/subagents/delegated-result.ts +1 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +5 -0
  87. package/src/integrations/subagents/diagnostic-types.ts +7 -1
  88. package/src/integrations/subagents/diagnostics.ts +2 -0
  89. package/src/integrations/subagents/failure-correlation.ts +52 -2
  90. package/src/integrations/subagents/failure-transcript.ts +60 -1
  91. package/src/integrations/subagents/replay-audit.ts +0 -1
  92. package/src/integrations/subagents/replay-safety.ts +12 -27
  93. package/src/integrations/subagents/session-diagnostics.ts +102 -3
  94. package/src/policy/bash-authorization.ts +2 -21
  95. package/src/policy/bash-types.ts +0 -5
  96. package/src/policy/bash.ts +1 -2
  97. package/src/policy/tool-call-authorization.ts +1 -7
  98. package/src/preflight.ts +0 -5
  99. package/src/prompt/retry-task.ts +11 -3
  100. package/src/prompt/step-contract.ts +10 -56
  101. package/src/prompt/step-sections.ts +8 -10
  102. package/src/prompt/step-task.ts +41 -10
  103. package/src/prompt/template.ts +5 -2
  104. package/src/runtime/completion-tool.ts +14 -0
  105. package/src/runtime/main-step-completion.ts +3 -0
  106. package/src/runtime/main-step-policy.ts +2 -1
  107. package/src/runtime/main-step-runtime-types.ts +9 -1
  108. package/src/runtime/main-step-runtime.ts +2 -0
  109. package/src/runtime/main-step-state.ts +6 -0
  110. package/src/runtime/main-step-trace.ts +45 -0
  111. package/src/runtime/step-result.ts +57 -0
  112. package/src/step-log.ts +178 -0
  113. package/src/workflow-doctor.ts +254 -0
  114. package/src/workflow-status/format-status.ts +32 -3
  115. package/src/workflow-status/render-board.ts +7 -4
  116. package/src/workflow-status/render-path.ts +51 -24
  117. package/src/workflow-status/render-step-detail.ts +379 -0
  118. package/src/workflow-status/render-summary.ts +22 -0
  119. package/src/workflow-status/transcript-reader.ts +231 -0
  120. package/src/workflow-status/types.ts +8 -0
  121. package/src/workflow-status/view.ts +161 -14
  122. package/src/workflow-status.ts +6 -0
  123. package/src/policy/approved-command-extraction.ts +0 -71
  124. package/src/policy/approved-commands.ts +0 -15
  125. package/src/policy/bash-read-only.ts +0 -161
  126. package/src/policy/restricted-git.ts +0 -30
  127. package/src/policy/reviewed-artifact.ts +0 -97
  128. package/src/policy/reviewed-command-safety.ts +0 -150
  129. package/src/policy/reviewed-command-shape.ts +0 -49
  130. package/src/policy/reviewed-repository-contract.ts +0 -138
  131. package/src/policy/reviewed-repository-cwd.ts +0 -102
@@ -0,0 +1,495 @@
1
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
2
+ import type { WorkflowStepResult } from '../runtime/step-result.ts';
3
+ import { redactStepLogText } from '../step-log.ts';
4
+ import {
5
+ MAX_STEP_TRACE_ARTIFACT_CHARS,
6
+ MAX_STEP_TRACE_ATTEMPTS,
7
+ MAX_STEP_TRACE_LOG_CHARS,
8
+ MAX_STEP_TRACE_LOG_EVENTS,
9
+ MAX_STEP_TRACE_SUMMARY_CHARS,
10
+ MAX_STEP_TRACE_TASK_CHARS,
11
+ MAX_WORKFLOW_TRACE_CHARS,
12
+ type StepAttemptResult,
13
+ type StepExecutionAttempt,
14
+ type StepGateDecision,
15
+ type SubagentTranscriptReference,
16
+ type WorkflowRun,
17
+ } from './state-types.ts';
18
+
19
+ type CompactedTask = {
20
+ readonly task: string;
21
+ readonly taskTruncated?: true;
22
+ readonly omittedTaskChars?: number;
23
+ };
24
+
25
+ const COMPACTED_FIELD_CHARS = 512;
26
+ const COMPACTED_LOG_CHARS = 4_096;
27
+
28
+ type CompactedLog = {
29
+ readonly log?: ReadonlyArray<string>;
30
+ readonly logTruncated?: true;
31
+ readonly omittedLogEvents?: number;
32
+ };
33
+
34
+ function logChars(lines: ReadonlyArray<string> | undefined): number {
35
+ return lines?.reduce((total, line) => total + line.length, 0) ?? 0;
36
+ }
37
+
38
+ function attemptSize(attempt: StepExecutionAttempt): number {
39
+ return (
40
+ attempt.requestId.length +
41
+ attempt.task.length +
42
+ (attempt.kind === 'subagent' ? attempt.agent.length : 0) +
43
+ (attempt.kind === 'main' ? logChars(attempt.log) : 0) +
44
+ (attempt.result?.outcome.length ?? 0) +
45
+ (attempt.result?.summary.length ?? 0) +
46
+ (attempt.result?.artifact?.length ?? 0) +
47
+ (attempt.result?.workspaceCwd?.length ?? 0) +
48
+ (attempt.kind === 'subagent'
49
+ ? (attempt.transcript?.trustedRoot.length ?? 0) +
50
+ (attempt.transcript?.sessionFile.length ?? 0) +
51
+ (attempt.transcript?.runId.length ?? 0)
52
+ : 0) +
53
+ (attempt.gateDecision?.requestId.length ?? 0) +
54
+ (attempt.gateDecision?.feedback.length ?? 0) +
55
+ (attempt.gateDecision?.reviewId?.length ?? 0)
56
+ );
57
+ }
58
+
59
+ function compactMainLog(attempt: StepExecutionAttempt): CompactedLog {
60
+ if (attempt.kind !== 'main' || !attempt.log) return {};
61
+ const retained: Array<string> = [];
62
+ let chars = 0;
63
+ for (const line of attempt.log) {
64
+ if (chars + line.length > COMPACTED_LOG_CHARS) break;
65
+ retained.push(line);
66
+ chars += line.length;
67
+ }
68
+ const newlyOmitted = attempt.log.length - retained.length;
69
+ const omittedLogEvents = (attempt.omittedLogEvents ?? 0) + newlyOmitted;
70
+ return {
71
+ log: retained,
72
+ ...(attempt.logTruncated || omittedLogEvents > 0
73
+ ? { logTruncated: true, omittedLogEvents }
74
+ : {}),
75
+ };
76
+ }
77
+
78
+ function compactAttempt(attempt: StepExecutionAttempt): StepExecutionAttempt {
79
+ const task = attempt.task.slice(0, COMPACTED_FIELD_CHARS);
80
+ const removedTaskChars = attempt.task.length - task.length;
81
+ const result = attempt.result
82
+ ? {
83
+ ...attempt.result,
84
+ summary: attempt.result.summary.slice(0, COMPACTED_FIELD_CHARS),
85
+ ...(attempt.result.summary.length > COMPACTED_FIELD_CHARS
86
+ ? { summaryTruncated: true as const }
87
+ : {}),
88
+ ...(attempt.result.artifact !== undefined
89
+ ? {
90
+ artifact: attempt.result.artifact.slice(0, COMPACTED_FIELD_CHARS),
91
+ ...(attempt.result.artifact.length > COMPACTED_FIELD_CHARS
92
+ ? { artifactTruncated: true as const }
93
+ : {}),
94
+ }
95
+ : {}),
96
+ }
97
+ : undefined;
98
+ const gateDecision = attempt.gateDecision
99
+ ? {
100
+ ...attempt.gateDecision,
101
+ feedback: attempt.gateDecision.feedback.slice(0, COMPACTED_FIELD_CHARS),
102
+ ...(attempt.gateDecision.feedback.length > COMPACTED_FIELD_CHARS
103
+ ? { feedbackTruncated: true as const }
104
+ : {}),
105
+ }
106
+ : undefined;
107
+ const compactedLog = compactMainLog(attempt);
108
+ return {
109
+ ...attempt,
110
+ task,
111
+ ...(attempt.taskTruncated || removedTaskChars > 0
112
+ ? {
113
+ taskTruncated: true as const,
114
+ omittedTaskChars: (attempt.omittedTaskChars ?? 0) + removedTaskChars,
115
+ }
116
+ : {}),
117
+ ...(result ? { result } : {}),
118
+ ...(gateDecision ? { gateDecision } : {}),
119
+ ...(attempt.kind === 'main' && attempt.log ? compactedLog : {}),
120
+ };
121
+ }
122
+
123
+ /** Returns the bounded checkpoint payload attributable to step traces. */
124
+ export function workflowTraceChars(run: WorkflowRun): number {
125
+ return [
126
+ ...run.history.flatMap((entry) => entry.attempts ?? []),
127
+ ...(run.currentStepAttempts ?? []),
128
+ ].reduce((total, attempt) => total + attemptSize(attempt), 0);
129
+ }
130
+
131
+ function compactRunTraceBudget(run: WorkflowRun): WorkflowRun {
132
+ let remaining = workflowTraceChars(run);
133
+ if (remaining <= MAX_WORKFLOW_TRACE_CHARS) return run;
134
+
135
+ const compactAttempts = (
136
+ values: ReadonlyArray<StepExecutionAttempt>,
137
+ ): Array<StepExecutionAttempt> => {
138
+ const attempts = [...values];
139
+ for (let index = 0; index < attempts.length; index += 1) {
140
+ if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
141
+ const attempt = attempts[index];
142
+ if (!attempt) continue;
143
+ const compacted = compactAttempt(attempt);
144
+ remaining -= attemptSize(attempt) - attemptSize(compacted);
145
+ attempts[index] = compacted;
146
+ }
147
+ return attempts;
148
+ };
149
+ const history = run.history.map((entry) =>
150
+ entry.attempts
151
+ ? { ...entry, attempts: compactAttempts(entry.attempts) }
152
+ : entry,
153
+ );
154
+ const currentStepAttempts = [...(run.currentStepAttempts ?? [])];
155
+ const compactedCurrent = compactAttempts(currentStepAttempts);
156
+ for (let index = 0; index < history.length; index += 1) {
157
+ if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
158
+ const entry = history[index];
159
+ if (!entry?.attempts || entry.attempts.length <= 1) continue;
160
+ const attempts = [...entry.attempts];
161
+ let omittedAttempts = entry.omittedAttempts ?? 0;
162
+ while (remaining > MAX_WORKFLOW_TRACE_CHARS && attempts.length > 2) {
163
+ const removed = attempts.splice(1, 1)[0];
164
+ if (removed) remaining -= attemptSize(removed);
165
+ omittedAttempts += 1;
166
+ }
167
+ history[index] = { ...entry, attempts, omittedAttempts };
168
+ }
169
+ let currentOmittedAttempts = run.currentStepOmittedAttempts ?? 0;
170
+ while (remaining > MAX_WORKFLOW_TRACE_CHARS && compactedCurrent.length > 2) {
171
+ const removed = compactedCurrent.splice(1, 1)[0];
172
+ if (removed) remaining -= attemptSize(removed);
173
+ currentOmittedAttempts += 1;
174
+ }
175
+ for (let index = 0; index < history.length; index += 1) {
176
+ if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
177
+ const entry = history[index];
178
+ if (!entry?.attempts || entry.attempts.length === 0) continue;
179
+ const attempts = [...entry.attempts];
180
+ let omittedAttempts = entry.omittedAttempts ?? 0;
181
+ while (remaining > MAX_WORKFLOW_TRACE_CHARS && attempts.length > 0) {
182
+ const removed = attempts.shift();
183
+ if (removed) remaining -= attemptSize(removed);
184
+ omittedAttempts += 1;
185
+ }
186
+ history[index] = { ...entry, attempts, omittedAttempts };
187
+ }
188
+ while (remaining > MAX_WORKFLOW_TRACE_CHARS && compactedCurrent.length > 0) {
189
+ const removed = compactedCurrent.shift();
190
+ if (removed) remaining -= attemptSize(removed);
191
+ currentOmittedAttempts += 1;
192
+ }
193
+
194
+ return {
195
+ ...run,
196
+ history,
197
+ ...(run.currentStepAttempts
198
+ ? { currentStepAttempts: compactedCurrent }
199
+ : {}),
200
+ ...(currentOmittedAttempts > 0
201
+ ? { currentStepOmittedAttempts: currentOmittedAttempts }
202
+ : {}),
203
+ };
204
+ }
205
+
206
+ function compactTask(task: string): CompactedTask {
207
+ if (task.length <= MAX_STEP_TRACE_TASK_CHARS) return { task };
208
+ return {
209
+ task: task.slice(0, MAX_STEP_TRACE_TASK_CHARS),
210
+ taskTruncated: true,
211
+ omittedTaskChars: task.length - MAX_STEP_TRACE_TASK_CHARS,
212
+ };
213
+ }
214
+
215
+ function appendAttempt(
216
+ run: WorkflowRun,
217
+ attempt: StepExecutionAttempt,
218
+ now: number,
219
+ ): WorkflowRun {
220
+ if (
221
+ run.currentStepAttempts?.some(
222
+ (candidate) => candidate.requestId === attempt.requestId,
223
+ )
224
+ ) {
225
+ return run;
226
+ }
227
+ const existing = run.currentStepAttempts ?? [];
228
+ const isFull = existing.length >= MAX_STEP_TRACE_ATTEMPTS;
229
+ const retained = isFull
230
+ ? [existing[0], ...existing.slice(-(MAX_STEP_TRACE_ATTEMPTS - 2))].filter(
231
+ (candidate): candidate is StepExecutionAttempt =>
232
+ candidate !== undefined,
233
+ )
234
+ : existing;
235
+ const omittedAttempts =
236
+ (run.currentStepOmittedAttempts ?? 0) + (isFull ? 1 : 0);
237
+ return compactRunTraceBudget({
238
+ ...run,
239
+ currentStepAttempts: [...retained, attempt],
240
+ ...(omittedAttempts > 0
241
+ ? { currentStepOmittedAttempts: omittedAttempts }
242
+ : {}),
243
+ updatedAt: now,
244
+ });
245
+ }
246
+
247
+ function nextAttemptOrdinal(run: WorkflowRun): number {
248
+ const attempts = run.currentStepAttempts ?? [];
249
+ const largestRecordedOrdinal = attempts.reduce(
250
+ (largest, attempt) => Math.max(largest, attempt.ordinal ?? 0),
251
+ 0,
252
+ );
253
+ const attemptedCount =
254
+ attempts.length + (run.currentStepOmittedAttempts ?? 0);
255
+ return Math.max(largestRecordedOrdinal, attemptedCount) + 1;
256
+ }
257
+
258
+ /** Records the exact task supplied for one main-agent attempt. */
259
+ export function beginMainStepAttempt(
260
+ run: WorkflowRun,
261
+ requestId: string,
262
+ task: string,
263
+ now: number,
264
+ ): WorkflowRun {
265
+ if (!requestId || requestId.includes('\0') || !task.trim()) return run;
266
+ const compacted = compactTask(task);
267
+ return appendAttempt(
268
+ run,
269
+ {
270
+ kind: 'main',
271
+ requestId,
272
+ ordinal: nextAttemptOrdinal(run),
273
+ ...compacted,
274
+ startedAt: now,
275
+ },
276
+ now,
277
+ );
278
+ }
279
+
280
+ /** Records the exact task body supplied for one child attempt. */
281
+ export function beginSubagentStepAttempt(
282
+ run: WorkflowRun,
283
+ requestId: string,
284
+ agent: string,
285
+ task: string,
286
+ now: number,
287
+ ): WorkflowRun {
288
+ if (!requestId || requestId.includes('\0') || !agent || !task.trim()) {
289
+ return run;
290
+ }
291
+ const compacted = compactTask(task);
292
+ return appendAttempt(
293
+ run,
294
+ {
295
+ kind: 'subagent',
296
+ requestId,
297
+ ordinal: nextAttemptOrdinal(run),
298
+ agent,
299
+ ...compacted,
300
+ startedAt: now,
301
+ },
302
+ now,
303
+ );
304
+ }
305
+
306
+ /** Appends a redacted, bounded prefix of main-agent events to one exact attempt. */
307
+ export function appendMainStepLog(
308
+ run: WorkflowRun,
309
+ requestId: string,
310
+ lines: ReadonlyArray<string>,
311
+ now: number,
312
+ ): WorkflowRun {
313
+ if (!requestId || requestId.includes('\0') || lines.length === 0) return run;
314
+ const attempts = run.currentStepAttempts;
315
+ if (!attempts || attempts.length === 0) return run;
316
+ const index = attempts.length - 1;
317
+ const attempt = attempts[index];
318
+ if (!attempt || attempt.kind !== 'main' || attempt.requestId !== requestId) {
319
+ return run;
320
+ }
321
+
322
+ const safeLines = lines
323
+ .map(redactStepLogText)
324
+ .filter((line) => line.length > 0);
325
+ if (safeLines.length === 0) return run;
326
+
327
+ const existing = [...(attempt.log ?? [])];
328
+ let chars = logChars(existing);
329
+ let accepted = 0;
330
+ if (!attempt.logTruncated) {
331
+ for (const line of safeLines) {
332
+ if (
333
+ existing.length >= MAX_STEP_TRACE_LOG_EVENTS ||
334
+ chars + line.length > MAX_STEP_TRACE_LOG_CHARS
335
+ ) {
336
+ break;
337
+ }
338
+ existing.push(line);
339
+ chars += line.length;
340
+ accepted += 1;
341
+ }
342
+ }
343
+ const newlyOmitted = safeLines.length - accepted;
344
+ const omittedLogEvents = (attempt.omittedLogEvents ?? 0) + newlyOmitted;
345
+ const currentStepAttempts = [...attempts];
346
+ currentStepAttempts[index] = {
347
+ ...attempt,
348
+ log: existing,
349
+ ...(attempt.logTruncated || omittedLogEvents > 0
350
+ ? { logTruncated: true, omittedLogEvents }
351
+ : {}),
352
+ };
353
+ return compactRunTraceBudget({
354
+ ...run,
355
+ currentStepAttempts,
356
+ updatedAt: now,
357
+ });
358
+ }
359
+
360
+ function isSafeTranscriptReference(
361
+ reference: SubagentTranscriptReference,
362
+ ): boolean {
363
+ if (
364
+ !isAbsolute(reference.trustedRoot) ||
365
+ !isAbsolute(reference.sessionFile) ||
366
+ reference.trustedRoot.includes('\0') ||
367
+ reference.sessionFile.includes('\0') ||
368
+ !reference.runId ||
369
+ reference.runId.includes('\0') ||
370
+ reference.runId.includes('/') ||
371
+ reference.runId.includes('\\') ||
372
+ reference.runId === '.' ||
373
+ reference.runId === '..' ||
374
+ !Number.isSafeInteger(reference.childIndex) ||
375
+ reference.childIndex < 0
376
+ ) {
377
+ return false;
378
+ }
379
+ const expected = resolve(
380
+ reference.trustedRoot,
381
+ reference.runId,
382
+ `run-${reference.childIndex}`,
383
+ 'session.jsonl',
384
+ );
385
+ const relativePath = relative(
386
+ resolve(reference.trustedRoot),
387
+ resolve(reference.sessionFile),
388
+ );
389
+ const isWithinTrustedRoot =
390
+ relativePath !== '' &&
391
+ relativePath !== '..' &&
392
+ !relativePath.startsWith(`..${sep}`) &&
393
+ !isAbsolute(relativePath);
394
+ return isWithinTrustedRoot && resolve(reference.sessionFile) === expected;
395
+ }
396
+
397
+ /**
398
+ * Attaches a lexically confined child transcript reference to its attempt.
399
+ *
400
+ * The status reader repeats this check and then verifies real paths, file type,
401
+ * no-follow opening, and a stable bounded read before displaying anything.
402
+ */
403
+ export function attachSubagentTranscript(
404
+ run: WorkflowRun,
405
+ requestId: string,
406
+ reference: SubagentTranscriptReference,
407
+ now: number,
408
+ ): WorkflowRun {
409
+ if (!isSafeTranscriptReference(reference)) return run;
410
+ const attempts = run.currentStepAttempts;
411
+ const index = attempts?.findIndex(
412
+ (attempt) => attempt.requestId === requestId && attempt.kind === 'subagent',
413
+ );
414
+ if (index === undefined || index < 0 || !attempts) return run;
415
+ const currentStepAttempts = [...attempts];
416
+ const attempt = currentStepAttempts[index];
417
+ if (!attempt || attempt.kind !== 'subagent') return run;
418
+ currentStepAttempts[index] = { ...attempt, transcript: reference };
419
+ return compactRunTraceBudget({ ...run, currentStepAttempts, updatedAt: now });
420
+ }
421
+
422
+ function attemptResult(
423
+ result: WorkflowStepResult,
424
+ workspaceCwd?: string,
425
+ ): StepAttemptResult {
426
+ const summary = result.summary.slice(0, MAX_STEP_TRACE_SUMMARY_CHARS);
427
+ const artifact = result.artifact?.slice(0, MAX_STEP_TRACE_ARTIFACT_CHARS);
428
+ return {
429
+ outcome: result.outcome,
430
+ summary,
431
+ ...(summary.length < result.summary.length
432
+ ? { summaryTruncated: true as const }
433
+ : {}),
434
+ ...(artifact !== undefined ? { artifact } : {}),
435
+ ...(artifact !== undefined &&
436
+ artifact.length < (result.artifact?.length ?? 0)
437
+ ? { artifactTruncated: true as const }
438
+ : {}),
439
+ ...(workspaceCwd ? { workspaceCwd } : {}),
440
+ };
441
+ }
442
+
443
+ /** Stores the submitted result on the latest attempt before transition. */
444
+ export function recordCurrentStepResult(
445
+ run: WorkflowRun,
446
+ result: WorkflowStepResult,
447
+ now: number,
448
+ workspaceCwd?: string,
449
+ ): WorkflowRun {
450
+ const attempts = run.currentStepAttempts;
451
+ if (!attempts || attempts.length === 0) return run;
452
+ const index = attempts.length - 1;
453
+ const latest = attempts[index];
454
+ if (!latest) return run;
455
+ const currentStepAttempts = [...attempts];
456
+ currentStepAttempts[index] = {
457
+ ...latest,
458
+ result: attemptResult(result, workspaceCwd),
459
+ };
460
+ return compactRunTraceBudget({
461
+ ...run,
462
+ currentStepAttempts,
463
+ updatedAt: now,
464
+ });
465
+ }
466
+
467
+ /** Durably pairs a human gate decision with the attempt it reviewed. */
468
+ export function recordCurrentGateDecision(
469
+ run: WorkflowRun,
470
+ decision: StepGateDecision,
471
+ now: number,
472
+ ): WorkflowRun {
473
+ const attempts = run.currentStepAttempts;
474
+ if (!attempts || attempts.length === 0) return run;
475
+ const index = attempts.length - 1;
476
+ const latest = attempts[index];
477
+ if (!latest) return run;
478
+ const feedback = decision.feedback.slice(0, MAX_STEP_TRACE_SUMMARY_CHARS);
479
+ const currentStepAttempts = [...attempts];
480
+ currentStepAttempts[index] = {
481
+ ...latest,
482
+ gateDecision: {
483
+ ...decision,
484
+ feedback,
485
+ ...(feedback.length < decision.feedback.length
486
+ ? { feedbackTruncated: true }
487
+ : {}),
488
+ },
489
+ };
490
+ return compactRunTraceBudget({
491
+ ...run,
492
+ currentStepAttempts,
493
+ updatedAt: now,
494
+ });
495
+ }
@@ -12,6 +12,7 @@ export {
12
12
  failRun,
13
13
  pauseRun,
14
14
  resumeRun,
15
+ setResumeInput,
15
16
  } from './run-lifecycle.ts';
16
17
  export { reconcileRun } from './run-reconciliation.ts';
17
18
  export type { ReconcileResult } from './transition-types.ts';
@@ -22,6 +22,7 @@ import type { WorkflowStepResult } from '../runtime/step-result.ts';
22
22
  import type { WorkflowStatusSnapshot } from '../workflow-status.ts';
23
23
  import type { DelegationFailureActions } from './delegation-failure.ts';
24
24
  import type { WorkflowHarnessDependencies } from './dependencies.ts';
25
+ import type { SettledStepReport } from './step-reporting.ts';
25
26
  import type {
26
27
  ActiveDelegation,
27
28
  ActivePromptReview,
@@ -61,6 +62,10 @@ export type HarnessActionContext = {
61
62
  legacyProgressWidgetContext: ExtensionContext | undefined;
62
63
  workflowIds: () => Array<string>;
63
64
  list: (context: ExtensionCommandContext) => Promise<void>;
65
+ doctor: (
66
+ workflowId: string,
67
+ context: ExtensionCommandContext,
68
+ ) => Promise<void>;
64
69
  start: (
65
70
  workflowId: string,
66
71
  input: string,
@@ -74,8 +79,11 @@ export type HarnessActionContext = {
74
79
  ) => Promise<void>;
75
80
  pause: (reason: string, context: ExtensionCommandContext) => Promise<void>;
76
81
  pauseNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
77
- resume: (context: ExtensionCommandContext) => Promise<void>;
78
- resumeNow: (context: ExtensionCommandContext) => Promise<void>;
82
+ resume: (input: string, context: ExtensionCommandContext) => Promise<void>;
83
+ resumeNow: (
84
+ context: ExtensionCommandContext,
85
+ input?: string,
86
+ ) => Promise<void>;
79
87
  abort: (reason: string, context: ExtensionCommandContext) => Promise<void>;
80
88
  abortNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
81
89
  reload: (context: ExtensionCommandContext) => Promise<void>;
@@ -93,6 +101,16 @@ export type HarnessActionContext = {
93
101
  run: WorkflowRun,
94
102
  step: WorkflowStep,
95
103
  ) => void;
104
+ queueMainStepLog: (
105
+ identity: MainStepIdentity,
106
+ lines: ReadonlyArray<string>,
107
+ context: ExtensionContext,
108
+ ) => Promise<void>;
109
+ recordMainStepLog: (
110
+ identity: MainStepIdentity,
111
+ lines: ReadonlyArray<string>,
112
+ context: ExtensionContext,
113
+ ) => Promise<void>;
96
114
  queueMainStepResult: (
97
115
  identity: MainStepIdentity,
98
116
  result: WorkflowStepResult | undefined,
@@ -123,8 +141,12 @@ export type HarnessActionContext = {
123
141
  failure: DelegationFailureDetails | undefined,
124
142
  reason: string,
125
143
  ) => boolean;
126
- pauseForDelegationFailure: (reason: string) => void;
127
- pauseForExecutionFailure: (label: string, reason: string) => void;
144
+ pauseForDelegationFailure: (reason: string, failureSummary?: string) => void;
145
+ pauseForExecutionFailure: (
146
+ label: string,
147
+ reason: string,
148
+ failureSummary?: string,
149
+ ) => void;
128
150
  retainUnconfirmedDelegation: (
129
151
  active: ActiveDelegation,
130
152
  reason: string,
@@ -134,6 +156,7 @@ export type HarnessActionContext = {
134
156
  workflow: LoadedWorkflow,
135
157
  originalRun: WorkflowRun,
136
158
  outcome: string,
159
+ summary: string,
137
160
  artifact: string,
138
161
  ) => Promise<void>;
139
162
  launchPromptReview: (
@@ -157,7 +180,10 @@ export type HarnessActionContext = {
157
180
  cancelPromptReview: () => void;
158
181
  registerPlannotatorResults: () => void;
159
182
  handlePlannotatorResult: (data: unknown) => Promise<void>;
160
- settleAfterTransition: (workflow: LoadedWorkflow) => void;
183
+ settleAfterTransition: (
184
+ workflow: LoadedWorkflow,
185
+ report: SettledStepReport,
186
+ ) => void;
161
187
  preflight: (workflow: LoadedWorkflow, stepId: string) => Array<string>;
162
188
  isolateMainSessionTools: () => void;
163
189
  restoreBaselineTools: () => void;
@@ -176,6 +202,5 @@ export type HarnessActionContext = {
176
202
  refreshStatusWhileRunning: () => void;
177
203
  stopStatusRefresh: () => void;
178
204
  registerWorkflowStatusShortcut: () => void;
179
- openWorkflowStatus: (context: ExtensionContext) => void;
180
205
  showWorkflowStatus: (context: ExtensionContext) => Promise<void>;
181
206
  };
@@ -7,6 +7,14 @@ import { failRun, pauseRun } from '../engine/transitions.ts';
7
7
  import { preflightStep } from '../preflight.ts';
8
8
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
9
9
  import { formatCatalogDiagnostics } from './catalog.ts';
10
+ import {
11
+ conciseStepFailureSummary,
12
+ conciseStepPauseSummary,
13
+ reportFailedStep,
14
+ reportPausedStep,
15
+ reportSettledStep,
16
+ type SettledStepReport,
17
+ } from './step-reporting.ts';
10
18
 
11
19
  const STATE_ENTRY_TYPE = 'pi-workflows-state-v1';
12
20
 
@@ -39,6 +47,7 @@ export type CoreActions = {
39
47
  settleAfterTransition: (
40
48
  this: HarnessActionContext,
41
49
  workflow: LoadedWorkflow,
50
+ report: SettledStepReport,
42
51
  ) => void;
43
52
  preflight: (
44
53
  this: HarnessActionContext,
@@ -71,6 +80,7 @@ export type CoreActions = {
71
80
  function settleAfterTransition(
72
81
  this: HarnessActionContext,
73
82
  workflow: LoadedWorkflow,
83
+ report: SettledStepReport,
74
84
  ): void {
75
85
  if (!this.run) return;
76
86
  if (this.run.status === 'running') {
@@ -85,6 +95,14 @@ function settleAfterTransition(
85
95
  }
86
96
 
87
97
  this.persist();
98
+ reportSettledStep(this.pi, workflow, this.run, report);
99
+ if (
100
+ this.run.status === 'paused' &&
101
+ this.run.failedStepId === this.run.currentStepId &&
102
+ this.run.pauseReason
103
+ ) {
104
+ reportFailedStep(this.pi, workflow, this.run, this.run.pauseReason);
105
+ }
88
106
  if (this.run.status !== 'running') {
89
107
  this.restoreBaselineTools();
90
108
  this.updateStatus();
@@ -94,8 +112,13 @@ function settleAfterTransition(
94
112
  'info',
95
113
  );
96
114
  } else if (this.run.status === 'paused') {
115
+ const reason = this.run.pauseReason ?? 'manual action required';
97
116
  this.latestContext?.ui.notify(
98
- `Workflow paused: ${this.run.pauseReason ?? 'manual action required'}`,
117
+ `Workflow paused: ${
118
+ this.run.failedStepId
119
+ ? conciseStepFailureSummary(reason)
120
+ : conciseStepPauseSummary(reason)
121
+ }`,
99
122
  'warning',
100
123
  );
101
124
  }
@@ -195,6 +218,12 @@ function restoreFromSession(
195
218
  this.dependencies.now(),
196
219
  );
197
220
  this.persist();
221
+ reportPausedStep(
222
+ this.pi,
223
+ this.catalog.workflows.get(this.run.workflowId),
224
+ this.run,
225
+ this.run.pauseReason ?? 'Session was restored',
226
+ );
198
227
  }
199
228
  if (!this.run && previousBaseline) {
200
229
  this.pi.setActiveTools([...previousBaseline]);