@wichayutdew/pi-workflows 1.0.1 → 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 -223
  2. package/agents/step.md +4 -11
  3. package/dist/index.js +2554 -976
  4. package/examples/mr-comments.workflow.yaml +16 -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 -44
  31. package/src/command-names.ts +1 -0
  32. package/src/commands.ts +33 -4
  33. package/src/config/ceiling.ts +6 -12
  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 -22
  37. package/src/config/validation/permissions.ts +5 -66
  38. package/src/config/validation/prompt.ts +1 -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 +3 -0
  43. package/src/engine/gate-transitions.ts +58 -9
  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 +235 -0
  49. package/src/engine/run-workflow-validation.ts +191 -0
  50. package/src/engine/state-types.ts +108 -5
  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 -84
  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 +3 -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
@@ -32,6 +32,9 @@ export function delegationFailureFingerprint(
32
32
  tool: normalizedFingerprintField(failure.diagnostic.tool),
33
33
  call: normalizedFingerprintField(failure.diagnostic.call),
34
34
  output: normalizedFingerprintField(failure.diagnostic.output),
35
+ postCompletionWarning: normalizedFingerprintField(
36
+ failure.diagnostic.postCompletionWarning,
37
+ ),
35
38
  }
36
39
  : undefined,
37
40
  });
@@ -62,6 +65,7 @@ export function isRetryableTerminalFailure(
62
65
  failure: DelegationFailureDetails,
63
66
  ): boolean {
64
67
  if (failure.recoveryBlocker !== undefined) return false;
68
+ if (failure.diagnostic?.postCompletionWarning !== undefined) return false;
65
69
  if (
66
70
  failure.status === 'timed_out' ||
67
71
  failure.status === 'turn_budget_exhausted' ||
@@ -96,9 +100,7 @@ export function isSafeToRetryDelegation(
96
100
  ): boolean {
97
101
  return (
98
102
  replayAudit?.replaySafe === true &&
99
- (isReplayExplicitlyAuthorized ||
100
- policy.permissions.bash.mode === 'deny' ||
101
- policy.permissions.bash.mode === 'read-only')
103
+ (isReplayExplicitlyAuthorized || policy.permissions.bash.mode === 'deny')
102
104
  );
103
105
  }
104
106
 
@@ -15,6 +15,7 @@ import {
15
15
  type SubagentDelegationClientController,
16
16
  } from '../integrations/subagents/client.ts';
17
17
  import {
18
+ auditCompletedDelegationTranscript,
18
19
  readDelegationReplayAudit,
19
20
  readToolFailureDiagnostic,
20
21
  } from '../integrations/subagents/diagnostics.ts';
@@ -28,6 +29,10 @@ import {
28
29
  } from '../runtime/serial-task-queue.ts';
29
30
  import { showWorkflowStatus } from '../workflow-status.ts';
30
31
  import type { ActiveDelegation } from './types.ts';
32
+ import {
33
+ resolveWorkspaceDirectory,
34
+ type ResolveWorkspaceDirectoryOptions,
35
+ } from './workspace-directory.ts';
31
36
 
32
37
  const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
33
38
 
@@ -47,14 +52,17 @@ export type WorkflowHarnessDependencies = {
47
52
  readonly removeDelegationWorkspace: (
48
53
  resultDirectory: string,
49
54
  ) => Promise<void>;
50
- readonly currentWorkingDirectory: () => string;
51
55
  readonly waitForDelay: (delayMs: number) => Promise<void>;
56
+ readonly resolveWorkspaceDirectory: (
57
+ options: ResolveWorkspaceDirectoryOptions,
58
+ ) => string;
52
59
  readonly loadCatalog: typeof loadCatalog;
53
60
  readonly requestPlannotatorReview: typeof requestPlannotatorReview;
54
61
  readonly requestPlannotatorReviewStatus: typeof requestPlannotatorReviewStatus;
55
62
  readonly requestPromptGateReview: typeof requestPromptGateReview;
56
63
  readonly readDelegationReplayAudit: typeof readDelegationReplayAudit;
57
64
  readonly readToolFailureDiagnostic: typeof readToolFailureDiagnostic;
65
+ readonly auditCompletedDelegationTranscript: typeof auditCompletedDelegationTranscript;
58
66
  readonly showWorkflowStatus: typeof showWorkflowStatus;
59
67
  readonly createSubagentClient: (
60
68
  pi: ExtensionAPI,
@@ -132,15 +140,16 @@ const DEFAULT_DEPENDENCIES: WorkflowHarnessDependencies = {
132
140
  readDelegatedResult,
133
141
  removeDelegationWorkspace: async (resultDirectory) =>
134
142
  rm(resultDirectory, { recursive: true, force: true }),
135
- currentWorkingDirectory: () => process.cwd(),
136
143
  waitForDelay: async (delayMs) =>
137
144
  new Promise((resolve) => setTimeout(resolve, delayMs)),
145
+ resolveWorkspaceDirectory,
138
146
  loadCatalog,
139
147
  requestPlannotatorReview,
140
148
  requestPlannotatorReviewStatus,
141
149
  requestPromptGateReview,
142
150
  readDelegationReplayAudit,
143
151
  readToolFailureDiagnostic,
152
+ auditCompletedDelegationTranscript,
144
153
  showWorkflowStatus,
145
154
  createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
146
155
  createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
@@ -5,10 +5,9 @@ import {
5
5
  beginGate,
6
6
  failGate,
7
7
  failRun,
8
- resolveGate,
9
8
  } from '../engine/transitions.ts';
10
- import { reviewedCommandShapeError } from '../policy/approved-commands.ts';
11
9
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
10
+ import { reportFailedStep } from './step-reporting.ts';
12
11
 
13
12
  type HarnessActionContext = Pick<
14
13
  FullHarnessActionContext,
@@ -21,7 +20,6 @@ type HarnessActionContext = Pick<
21
20
  | 'restoreBaselineTools'
22
21
  | 'run'
23
22
  | 'sessionEpoch'
24
- | 'settleAfterTransition'
25
23
  | 'updateStatus'
26
24
  >;
27
25
 
@@ -31,6 +29,7 @@ export type GateSubmissionAction = {
31
29
  workflow: LoadedWorkflow,
32
30
  originalRun: WorkflowRun,
33
31
  outcome: string,
32
+ summary: string,
34
33
  artifact: string,
35
34
  ) => Promise<void>;
36
35
  };
@@ -55,6 +54,7 @@ async function submitGate(
55
54
  workflow: LoadedWorkflow,
56
55
  originalRun: WorkflowRun,
57
56
  outcome: string,
57
+ summary: string,
58
58
  artifact: string,
59
59
  ): Promise<void> {
60
60
  const requestSessionEpoch = this.sessionEpoch;
@@ -64,34 +64,6 @@ async function submitGate(
64
64
  const step = workflow.definition.steps[originalRun.currentStepId];
65
65
  if (!step?.gate) throw new Error('Current step has no gate');
66
66
 
67
- const commandShapeError = reviewedCommandShapeError(artifact);
68
- if (commandShapeError) {
69
- const awaitingReview = beginGate(
70
- workflow,
71
- originalRun,
72
- outcome,
73
- artifact,
74
- requestId,
75
- this.dependencies.now(),
76
- );
77
- this.run = resolveGate(
78
- workflow,
79
- awaitingReview,
80
- {
81
- approved: false,
82
- feedback: commandShapeError,
83
- resolvedAt: this.dependencies.now(),
84
- },
85
- this.dependencies.now(),
86
- );
87
- this.latestContext?.ui.notify(
88
- `Plan contract needs repair before review: ${commandShapeError}`,
89
- 'warning',
90
- );
91
- this.settleAfterTransition(workflow);
92
- return;
93
- }
94
-
95
67
  this.run = beginGate(
96
68
  workflow,
97
69
  originalRun,
@@ -99,6 +71,7 @@ async function submitGate(
99
71
  artifact,
100
72
  requestId,
101
73
  this.dependencies.now(),
74
+ summary,
102
75
  );
103
76
  this.persist();
104
77
  this.restoreBaselineTools();
@@ -129,6 +102,7 @@ async function submitGate(
129
102
  const gateFailed = failGate(currentRun, reason, this.dependencies.now());
130
103
  this.run = failRun(gateFailed, reason, this.dependencies.now());
131
104
  this.persist();
105
+ reportFailedStep(this.pi, workflow, this.run, reason);
132
106
  this.restoreBaselineTools();
133
107
  this.updateStatus();
134
108
  throw new Error(reason);
@@ -3,6 +3,7 @@ import { buildMainWorkflowNotice } from '../prompt.ts';
3
3
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
4
4
  import { parseAvailableSkills } from './catalog.ts';
5
5
  import { waitForEventContextIdle } from './context-idle.ts';
6
+ import { reportFailedStep } from './step-reporting.ts';
6
7
 
7
8
  type HarnessActionContext = Pick<
8
9
  FullHarnessActionContext,
@@ -126,12 +127,10 @@ function registerPolicy(this: HarnessActionContext): void {
126
127
  if (!this.run || this.run.status !== 'running') return;
127
128
  const workflow = this.catalog.workflows.get(this.run.workflowId);
128
129
  if (!workflow) {
129
- this.run = failRun(
130
- this.run,
131
- 'Workflow configuration disappeared; reload or restore it',
132
- this.dependencies.now(),
133
- );
130
+ const reason = 'Workflow configuration disappeared; reload or restore it';
131
+ this.run = failRun(this.run, reason, this.dependencies.now());
134
132
  this.persist();
133
+ reportFailedStep(this.pi, undefined, this.run, reason);
135
134
  this.restoreBaselineTools();
136
135
  this.updateStatus();
137
136
  return;
@@ -1,15 +1,18 @@
1
1
  import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
2
  import { abortRun, pauseRun } from '../engine/transitions.ts';
3
3
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
4
+ import { conciseStepPauseSummary, reportPausedStep } from './step-reporting.ts';
4
5
 
5
6
  type HarnessActionContext = Pick<
6
7
  FullHarnessActionContext,
7
8
  | 'cancelActiveDelegation'
8
9
  | 'cancelPromptReview'
10
+ | 'catalog'
9
11
  | 'dependencies'
10
12
  | 'isolateMainSessionTools'
11
13
  | 'mainSteps'
12
14
  | 'persist'
15
+ | 'pi'
13
16
  | 'restoreBaselineTools'
14
17
  | 'run'
15
18
  | 'updateStatus'
@@ -60,6 +63,12 @@ async function pauseNow(
60
63
 
61
64
  this.run = pauseRun(this.run, reason, this.dependencies.now());
62
65
  this.persist();
66
+ reportPausedStep(
67
+ this.pi,
68
+ this.catalog.workflows.get(this.run.workflowId),
69
+ this.run,
70
+ this.run.pauseReason ?? reason,
71
+ );
63
72
  if (isCancellationConfirmed) {
64
73
  this.restoreBaselineTools();
65
74
  } else {
@@ -68,7 +77,7 @@ async function pauseNow(
68
77
  this.updateStatus();
69
78
  context.ui.notify(
70
79
  isCancellationConfirmed
71
- ? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}". Fix the issue, then run /workflow-resume.`
80
+ ? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}": ${conciseStepPauseSummary(this.run.pauseReason ?? reason)}`
72
81
  : `Pause recorded at "${this.run.currentStepId}", but child cancellation is not confirmed. Main tools remain isolated until it exits.`,
73
82
  isCancellationConfirmed ? 'info' : 'warning',
74
83
  );
@@ -9,6 +9,7 @@ import {
9
9
  PLANNOTATOR_RESULT_CHANNEL,
10
10
  } from '../integrations/plannotator.ts';
11
11
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
12
+ import { reportFailedStep } from './step-reporting.ts';
12
13
 
13
14
  type HarnessActionContext = Pick<
14
15
  FullHarnessActionContext,
@@ -85,31 +86,39 @@ async function handlePlannotatorResult(
85
86
 
86
87
  const workflow = this.catalog.workflows.get(this.run.workflowId);
87
88
  if (!workflow) {
88
- this.run = failRun(
89
- this.run,
90
- 'Gate result arrived, but workflow configuration is unavailable',
91
- this.dependencies.now(),
92
- );
89
+ const reason =
90
+ 'Gate result arrived, but workflow configuration is unavailable';
91
+ this.run = failRun(this.run, reason, this.dependencies.now());
93
92
  this.persist();
93
+ reportFailedStep(this.pi, undefined, this.run, reason);
94
94
  this.restoreBaselineTools();
95
95
  this.updateStatus();
96
96
  return;
97
97
  }
98
98
  try {
99
+ const stepId = this.run.currentStepId;
100
+ const gate = workflow.definition.steps[stepId]?.gate;
101
+ if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
99
102
  this.run = resolveGate(
100
103
  workflow,
101
104
  this.run,
102
105
  resolution,
103
106
  this.dependencies.now(),
104
107
  );
105
- this.settleAfterTransition(workflow);
108
+ this.settleAfterTransition(workflow, {
109
+ stepId,
110
+ outcome: resolution.approved
111
+ ? gate.approvedOutcome
112
+ : gate.rejectedOutcome,
113
+ summary: this.run.lastSummary,
114
+ });
106
115
  } catch (error) {
107
- this.run = failRun(
108
- this.run,
109
- `Cannot apply gate result: ${error instanceof Error ? error.message : String(error)}`,
110
- this.dependencies.now(),
111
- );
116
+ const reason = `Cannot apply gate result: ${
117
+ error instanceof Error ? error.message : String(error)
118
+ }`;
119
+ this.run = failRun(this.run, reason, this.dependencies.now());
112
120
  this.persist();
121
+ reportFailedStep(this.pi, workflow, this.run, reason);
113
122
  this.restoreBaselineTools();
114
123
  this.updateStatus();
115
124
  }
@@ -9,6 +9,12 @@ import {
9
9
  } from '../engine/transitions.ts';
10
10
  import type { PromptGateReviewResult } from '../integrations/prompt-gate.ts';
11
11
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
12
+ import {
13
+ conciseStepFailureSummary,
14
+ conciseStepPauseSummary,
15
+ reportFailedStep,
16
+ reportPausedStep,
17
+ } from './step-reporting.ts';
12
18
  import type { ActivePromptReview } from './types.ts';
13
19
 
14
20
  type HarnessActionContext = Pick<
@@ -23,6 +29,7 @@ type HarnessActionContext = Pick<
23
29
  | 'mutationQueue'
24
30
  | 'pausePromptGate'
25
31
  | 'persist'
32
+ | 'pi'
26
33
  | 'queuePromptReviewFailure'
27
34
  | 'queuePromptReviewResult'
28
35
  | 'restoreBaselineTools'
@@ -205,13 +212,22 @@ async function finishPromptReview(
205
212
  return;
206
213
  }
207
214
  try {
215
+ const stepId = this.run.currentStepId;
216
+ const gate = workflow.definition.steps[stepId]?.gate;
217
+ if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
208
218
  this.run = resolveGate(
209
219
  workflow,
210
220
  this.run,
211
221
  resolution,
212
222
  this.dependencies.now(),
213
223
  );
214
- this.settleAfterTransition(workflow);
224
+ this.settleAfterTransition(workflow, {
225
+ stepId,
226
+ outcome: resolution.approved
227
+ ? gate.approvedOutcome
228
+ : gate.rejectedOutcome,
229
+ summary: this.run.lastSummary,
230
+ });
215
231
  } catch (error) {
216
232
  this.pausePromptGate(
217
233
  active.requestId,
@@ -234,16 +250,38 @@ function pausePromptGate(
234
250
  ) {
235
251
  return;
236
252
  }
253
+ const wasAwaitingGate = this.run.status === 'awaiting-gate';
254
+ const didFail = wasAwaitingGate && isFailed;
255
+ const didPause = wasAwaitingGate && !isFailed;
237
256
  if (this.run.status === 'awaiting-gate') {
238
257
  this.run = isFailed
239
258
  ? failRun(this.run, reason, this.dependencies.now())
240
259
  : pauseRun(this.run, reason, this.dependencies.now());
241
260
  }
242
261
  this.persist();
262
+ if (didFail) {
263
+ reportFailedStep(
264
+ this.pi,
265
+ this.catalog.workflows.get(this.run.workflowId),
266
+ this.run,
267
+ reason,
268
+ );
269
+ } else if (didPause) {
270
+ reportPausedStep(
271
+ this.pi,
272
+ this.catalog.workflows.get(this.run.workflowId),
273
+ this.run,
274
+ reason,
275
+ );
276
+ }
243
277
  this.restoreBaselineTools();
244
278
  this.updateStatus();
245
279
  this.latestContext?.ui.notify(
246
- `Workflow paused at "${this.run.currentStepId}": ${reason}`,
280
+ `Workflow paused at "${this.run.currentStepId}": ${
281
+ isFailed
282
+ ? conciseStepFailureSummary(reason)
283
+ : conciseStepPauseSummary(reason)
284
+ }`,
247
285
  'warning',
248
286
  );
249
287
  }
@@ -5,13 +5,20 @@ import {
5
5
  reconcileRun,
6
6
  resolveGate,
7
7
  resumeRun,
8
+ setResumeInput,
8
9
  storeGateResolution,
9
10
  } from '../engine/transitions.ts';
11
+ import { MAX_RESUME_INPUT_CHARS } from '../engine/state.ts';
10
12
  import {
11
13
  captureResumeCheckpoint,
12
14
  matchesResumeCheckpoint,
13
15
  } from '../engine/resume.ts';
16
+ import { analyzeWorkflow } from '../workflow-doctor.ts';
14
17
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
18
+ import {
19
+ conciseStepFailureSummary,
20
+ reportFailedStep,
21
+ } from './step-reporting.ts';
15
22
 
16
23
  type HarnessActionContext = Pick<
17
24
  FullHarnessActionContext,
@@ -29,6 +36,7 @@ type HarnessActionContext = Pick<
29
36
  | 'restoreBaselineTools'
30
37
  | 'run'
31
38
  | 'sessionEpoch'
39
+ | 'settleAfterTransition'
32
40
  | 'updateStatus'
33
41
  >;
34
42
 
@@ -36,13 +44,23 @@ export type ResumeAction = {
36
44
  resumeNow: (
37
45
  this: HarnessActionContext,
38
46
  context: ExtensionCommandContext,
47
+ input?: string,
39
48
  ) => Promise<void>;
40
49
  };
41
50
 
42
51
  async function resumeNow(
43
52
  this: HarnessActionContext,
44
53
  context: ExtensionCommandContext,
54
+ input = '',
45
55
  ): Promise<void> {
56
+ const resumeInput = input.trim();
57
+ if (resumeInput.length > MAX_RESUME_INPUT_CHARS) {
58
+ context.ui.notify(
59
+ `Resume guidance exceeds ${MAX_RESUME_INPUT_CHARS} characters`,
60
+ 'error',
61
+ );
62
+ return;
63
+ }
46
64
  if (!this.run || this.run.status !== 'paused') {
47
65
  context.ui.notify('No paused workflow to resume', 'warning');
48
66
  return;
@@ -92,6 +110,16 @@ async function resumeNow(
92
110
  );
93
111
  return;
94
112
  }
113
+ const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter(
114
+ (issue) => issue.level === 'error',
115
+ );
116
+ if (livenessErrors.length > 0) {
117
+ context.ui.notify(
118
+ `Cannot resume workflow; run /workflow-doctor ${workflow.definition.id}:\n${livenessErrors.map((issue) => issue.message).join('\n')}`,
119
+ 'error',
120
+ );
121
+ return;
122
+ }
95
123
 
96
124
  let resumed = reconciled.run;
97
125
  if (
@@ -144,6 +172,16 @@ async function resumeNow(
144
172
  );
145
173
  return;
146
174
  }
175
+ const latestLivenessErrors = analyzeWorkflow(
176
+ workflow.definition,
177
+ ).issues.filter((issue) => issue.level === 'error');
178
+ if (latestLivenessErrors.length > 0) {
179
+ context.ui.notify(
180
+ `Cannot resume workflow; run /workflow-doctor ${workflow.definition.id}:\n${latestLivenessErrors.map((issue) => issue.message).join('\n')}`,
181
+ 'error',
182
+ );
183
+ return;
184
+ }
147
185
  resumed = latest.run;
148
186
 
149
187
  if (
@@ -187,14 +225,29 @@ async function resumeNow(
187
225
  }
188
226
 
189
227
  const storedResolution = resumed.pendingGate?.resolution;
228
+ let settledGate:
229
+ | {
230
+ readonly stepId: string;
231
+ readonly outcome: string;
232
+ }
233
+ | undefined;
190
234
  if (storedResolution) {
191
235
  try {
236
+ const stepId = resumed.pendingGate?.stepId ?? resumed.currentStepId;
237
+ const gate = workflow.definition.steps[stepId]?.gate;
238
+ if (!gate) throw new Error(`gated step "${stepId}" no longer exists`);
192
239
  resumed = resolveGate(
193
240
  workflow,
194
241
  resumed,
195
242
  storedResolution,
196
243
  this.dependencies.now(),
197
244
  );
245
+ settledGate = {
246
+ stepId,
247
+ outcome: storedResolution.approved
248
+ ? gate.approvedOutcome
249
+ : gate.rejectedOutcome,
250
+ };
198
251
  } catch (error) {
199
252
  context.ui.notify(
200
253
  `Cannot apply stored gate result: ${error instanceof Error ? error.message : String(error)}`,
@@ -205,8 +258,21 @@ async function resumeNow(
205
258
  } else {
206
259
  resumed = resumeRun(resumed, this.dependencies.now());
207
260
  }
261
+ resumed = setResumeInput(
262
+ resumed,
263
+ resumed.status === 'running' ? resumeInput : '',
264
+ this.dependencies.now(),
265
+ );
208
266
  this.run = resumed;
209
267
 
268
+ if (settledGate) {
269
+ this.settleAfterTransition(workflow, {
270
+ ...settledGate,
271
+ summary: this.run.lastSummary,
272
+ });
273
+ return;
274
+ }
275
+
210
276
  if (this.run.status === 'awaiting-gate') {
211
277
  this.persist();
212
278
  this.restoreBaselineTools();
@@ -238,10 +304,18 @@ async function resumeNow(
238
304
  this.dependencies.now(),
239
305
  );
240
306
  this.persist();
307
+ reportFailedStep(
308
+ this.pi,
309
+ workflow,
310
+ this.run,
311
+ this.run.pauseReason ?? 'Step preflight failed',
312
+ );
241
313
  this.restoreBaselineTools();
242
314
  this.updateStatus();
243
315
  context.ui.notify(
244
- `Cannot resume workflow:\n${preflightErrors.join('\n')}`,
316
+ `Cannot resume workflow: ${conciseStepFailureSummary(
317
+ preflightErrors.join('; '),
318
+ )}`,
245
319
  'error',
246
320
  );
247
321
  return;
@@ -1,7 +1,9 @@
1
1
  import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
2
  import { createRun } from '../engine/state.ts';
3
+ import { analyzeWorkflow, formatWorkflowDoctor } from '../workflow-doctor.ts';
3
4
  import { formatWorkflowList } from '../workflow-list.ts';
4
5
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
6
+ import { formatCatalogDiagnostics } from './catalog.ts';
5
7
  import type { WorkflowStartContext } from './types.ts';
6
8
 
7
9
  type HarnessActionContext = Pick<
@@ -13,7 +15,6 @@ type HarnessActionContext = Pick<
13
15
  | 'isSessionActive'
14
16
  | 'isolateMainSessionTools'
15
17
  | 'launchCurrentStep'
16
- | 'openWorkflowStatus'
17
18
  | 'persist'
18
19
  | 'pi'
19
20
  | 'preflight'
@@ -40,6 +41,11 @@ export type StartActions = {
40
41
  this: HarnessActionContext,
41
42
  context: ExtensionCommandContext,
42
43
  ) => Promise<void>;
44
+ doctorWorkflows: (
45
+ this: HarnessActionContext,
46
+ workflowId: string,
47
+ context: ExtensionCommandContext,
48
+ ) => Promise<void>;
43
49
  startNow: (
44
50
  this: HarnessActionContext,
45
51
  workflowId: string,
@@ -76,6 +82,48 @@ async function listWorkflows(
76
82
  });
77
83
  }
78
84
 
85
+ async function doctorWorkflows(
86
+ this: HarnessActionContext,
87
+ workflowId: string,
88
+ context: ExtensionCommandContext,
89
+ ): Promise<void> {
90
+ const catalog = await this.dependencies.loadCatalog({
91
+ cwd: context.cwd,
92
+ projectTrusted: context.isProjectTrusted(),
93
+ });
94
+ if (catalog.diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
95
+ context.ui.notify(
96
+ `Workflow configuration errors:\n${formatCatalogDiagnostics(catalog)}`,
97
+ 'warning',
98
+ );
99
+ }
100
+ const selected = workflowId
101
+ ? [catalog.workflows.get(workflowId)].filter(
102
+ (workflow) => workflow !== undefined,
103
+ )
104
+ : [...catalog.workflows.values()].sort((left, right) =>
105
+ left.definition.id.localeCompare(right.definition.id),
106
+ );
107
+ if (workflowId && selected.length === 0) {
108
+ context.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
109
+ return;
110
+ }
111
+ if (selected.length === 0) {
112
+ context.ui.notify(
113
+ `No workflows loaded from ${catalog.userDirectory}`,
114
+ catalog.diagnostics.length > 0 ? 'warning' : 'info',
115
+ );
116
+ return;
117
+ }
118
+ this.pi.sendMessage({
119
+ customType: 'workflow-doctor',
120
+ content: formatWorkflowDoctor(
121
+ selected.map((workflow) => analyzeWorkflow(workflow.definition)),
122
+ ),
123
+ display: true,
124
+ });
125
+ }
126
+
79
127
  async function startNow(
80
128
  this: HarnessActionContext,
81
129
  workflowId: string,
@@ -134,6 +182,16 @@ async function startNow(
134
182
  context.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
135
183
  return;
136
184
  }
185
+ const livenessErrors = analyzeWorkflow(workflow.definition).issues.filter(
186
+ (issue) => issue.level === 'error',
187
+ );
188
+ if (livenessErrors.length > 0) {
189
+ context.ui.notify(
190
+ `Cannot start workflow; run /workflow-doctor ${workflowId}:\n${livenessErrors.map((issue) => issue.message).join('\n')}`,
191
+ 'error',
192
+ );
193
+ return;
194
+ }
137
195
  const preflightErrors = this.preflight(workflow, workflow.definition.start);
138
196
  if (preflightErrors.length > 0) {
139
197
  context.ui.notify(
@@ -143,17 +201,33 @@ async function startNow(
143
201
  return;
144
202
  }
145
203
 
204
+ let canonicalStartCwd: string;
205
+ try {
206
+ canonicalStartCwd = this.dependencies.resolveWorkspaceDirectory({
207
+ candidateCwd: context.cwd,
208
+ startCwd: context.cwd,
209
+ allowedRoots: ['.'],
210
+ });
211
+ } catch (error) {
212
+ context.ui.notify(
213
+ `Cannot capture workflow working directory: ${
214
+ error instanceof Error ? error.message : String(error)
215
+ }`,
216
+ 'error',
217
+ );
218
+ return;
219
+ }
146
220
  this.run = createRun(
147
221
  workflow,
148
222
  input.trim(),
149
223
  this.pi.getActiveTools(),
150
224
  this.dependencies.createRequestId(),
151
225
  this.dependencies.now(),
226
+ canonicalStartCwd,
152
227
  );
153
228
  this.persist();
154
229
  this.isolateMainSessionTools();
155
230
  this.updateStatus();
156
- this.openWorkflowStatus(context);
157
231
  this.launchCurrentStep(workflow);
158
232
  }
159
233
 
@@ -179,5 +253,5 @@ async function reloadNow(
179
253
  * Returns workflow listing, start, and reload actions for harness composition.
180
254
  */
181
255
  export function createStartActions(): StartActions {
182
- return { listWorkflows, startNow, reloadNow };
256
+ return { listWorkflows, doctorWorkflows, startNow, reloadNow };
183
257
  }