@wichayutdew/pi-workflows 0.3.0 → 1.0.1

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 (144) hide show
  1. package/README.md +55 -39
  2. package/dist/index.js +5771 -4794
  3. package/examples/mr-comments.workflow.yaml +2 -0
  4. package/package.json +1 -1
  5. package/schemas/workflow.schema.json +10 -1
  6. package/src/command-names.ts +7 -1
  7. package/src/commands.ts +126 -52
  8. package/src/config/catalog.ts +162 -0
  9. package/src/config/ceiling.ts +120 -116
  10. package/src/config/command-conflicts.ts +4 -4
  11. package/src/config/diagnostics.ts +28 -0
  12. package/src/config/load-settings.ts +37 -0
  13. package/src/config/load-types.ts +53 -0
  14. package/src/config/load-workflows.ts +135 -0
  15. package/src/config/load.ts +43 -311
  16. package/src/config/types.ts +150 -137
  17. package/src/config/validate.ts +12 -1261
  18. package/src/config/validation/permissions.ts +312 -0
  19. package/src/config/validation/prompt.ts +21 -0
  20. package/src/config/validation/settings.ts +122 -0
  21. package/src/config/validation/shared.ts +129 -0
  22. package/src/config/validation/shortcut.ts +118 -0
  23. package/src/config/validation/step.ts +228 -0
  24. package/src/config/validation/subagent.ts +288 -0
  25. package/src/config/validation/workflow.ts +156 -0
  26. package/src/config/yaml.ts +29 -0
  27. package/src/digest.ts +38 -6
  28. package/src/engine/checkpoint.ts +13 -9
  29. package/src/engine/create-run.ts +43 -0
  30. package/src/engine/gate-transitions.ts +187 -0
  31. package/src/engine/reconciliation-history.ts +88 -0
  32. package/src/engine/resume.ts +19 -7
  33. package/src/engine/run-advance.ts +122 -0
  34. package/src/engine/run-lifecycle.ts +124 -0
  35. package/src/engine/run-reconciliation.ts +116 -0
  36. package/src/engine/run-validation.ts +129 -0
  37. package/src/engine/state-types.ts +66 -0
  38. package/src/engine/state.ts +10 -193
  39. package/src/engine/transition-helpers.ts +28 -0
  40. package/src/engine/transition-types.ts +8 -0
  41. package/src/engine/transitions.ts +17 -471
  42. package/src/harness/action-context.ts +181 -0
  43. package/src/harness/catalog.ts +45 -0
  44. package/src/harness/context-idle.ts +21 -0
  45. package/src/harness/core-actions.ts +299 -0
  46. package/src/harness/delegation-control-actions.ts +250 -0
  47. package/src/harness/delegation-failure.ts +215 -0
  48. package/src/harness/delegation-plan.ts +219 -0
  49. package/src/harness/delegation-recovery-validation.ts +159 -0
  50. package/src/harness/delegation-response-actions.ts +295 -0
  51. package/src/harness/delegation-retry-policy.ts +118 -0
  52. package/src/harness/dependencies.ts +165 -0
  53. package/src/harness/gate-submission-action.ts +154 -0
  54. package/src/harness/lifecycle-actions.ts +154 -0
  55. package/src/harness/pause-actions.ts +120 -0
  56. package/src/harness/plannotator-result-actions.ts +123 -0
  57. package/src/harness/prompt-gate-actions.ts +270 -0
  58. package/src/harness/resume-action.ts +261 -0
  59. package/src/harness/start-actions.ts +183 -0
  60. package/src/harness/status-actions.ts +191 -0
  61. package/src/harness/step-execution-actions.ts +283 -0
  62. package/src/harness/types.ts +76 -0
  63. package/src/harness.ts +236 -2303
  64. package/src/index.ts +73 -11
  65. package/src/integrations/plannotator-requests.ts +127 -0
  66. package/src/integrations/plannotator-responses.ts +141 -0
  67. package/src/integrations/plannotator-types.ts +44 -0
  68. package/src/integrations/plannotator.ts +16 -235
  69. package/src/integrations/prompt-gate.ts +17 -10
  70. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  71. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  72. package/src/integrations/subagents/child-policy-sections.ts +186 -0
  73. package/src/integrations/subagents/child-policy-types.ts +32 -0
  74. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  75. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  76. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  77. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  78. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  79. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  80. package/src/integrations/subagents/child-runtime.ts +140 -301
  81. package/src/integrations/subagents/client-delegation.ts +181 -0
  82. package/src/integrations/subagents/client-messages.ts +66 -0
  83. package/src/integrations/subagents/client-types.ts +36 -0
  84. package/src/integrations/subagents/client.ts +127 -219
  85. package/src/integrations/subagents/delegated-result.ts +31 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  87. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  88. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  89. package/src/integrations/subagents/diagnostics.ts +24 -977
  90. package/src/integrations/subagents/failure-correlation.ts +235 -0
  91. package/src/integrations/subagents/failure-transcript.ts +192 -0
  92. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  93. package/src/integrations/subagents/protocol-events.ts +27 -0
  94. package/src/integrations/subagents/protocol.ts +30 -375
  95. package/src/integrations/subagents/replay-audit.ts +147 -0
  96. package/src/integrations/subagents/replay-safety.ts +82 -0
  97. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  98. package/src/policy/approved-command-extraction.ts +71 -0
  99. package/src/policy/approved-commands.ts +15 -432
  100. package/src/policy/bash-authorization.ts +86 -0
  101. package/src/policy/bash-read-only.ts +161 -0
  102. package/src/policy/bash-types.ts +20 -0
  103. package/src/policy/bash.ts +4 -346
  104. package/src/policy/completion-batch.ts +21 -16
  105. package/src/policy/immutable-input.ts +5 -2
  106. package/src/policy/mcp-authorization.ts +71 -0
  107. package/src/policy/restricted-command.ts +141 -0
  108. package/src/policy/restricted-git.ts +30 -0
  109. package/src/policy/reviewed-artifact.ts +97 -0
  110. package/src/policy/reviewed-command-safety.ts +150 -0
  111. package/src/policy/reviewed-command-shape.ts +49 -0
  112. package/src/policy/reviewed-repository-contract.ts +138 -0
  113. package/src/policy/reviewed-repository-cwd.ts +102 -0
  114. package/src/policy/tool-call-authorization.ts +58 -0
  115. package/src/policy/tool-selection.ts +72 -0
  116. package/src/policy/tool-types.ts +14 -0
  117. package/src/policy/tools.ts +11 -150
  118. package/src/preflight.ts +96 -61
  119. package/src/prompt/main-workflow-notice.ts +41 -0
  120. package/src/prompt/retry-task.ts +83 -0
  121. package/src/prompt/step-contract.ts +101 -0
  122. package/src/prompt/step-sections.ts +75 -0
  123. package/src/prompt/step-task.ts +156 -0
  124. package/src/prompt/template.ts +83 -0
  125. package/src/prompt.ts +10 -254
  126. package/src/runtime/completion-tool.ts +3 -0
  127. package/src/runtime/main-step-completion.ts +78 -0
  128. package/src/runtime/main-step-lifecycle.ts +37 -0
  129. package/src/runtime/main-step-policy.ts +84 -0
  130. package/src/runtime/main-step-runtime-types.ts +64 -0
  131. package/src/runtime/main-step-runtime.ts +129 -200
  132. package/src/runtime/main-step-state.ts +85 -0
  133. package/src/runtime/serial-task-queue.ts +48 -13
  134. package/src/runtime/step-result.ts +48 -27
  135. package/src/workflow-list.ts +15 -6
  136. package/src/workflow-status/format-status.ts +52 -0
  137. package/src/workflow-status/formatting.ts +178 -0
  138. package/src/workflow-status/layout.ts +109 -0
  139. package/src/workflow-status/render-board.ts +101 -0
  140. package/src/workflow-status/render-path.ts +123 -0
  141. package/src/workflow-status/render-summary.ts +144 -0
  142. package/src/workflow-status/types.ts +40 -0
  143. package/src/workflow-status/view.ts +273 -0
  144. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,154 @@
1
+ import type { LoadedWorkflow } from '../config/types.ts';
2
+ import type { WorkflowRun } from '../engine/state.ts';
3
+ import {
4
+ attachGateReviewId,
5
+ beginGate,
6
+ failGate,
7
+ failRun,
8
+ resolveGate,
9
+ } from '../engine/transitions.ts';
10
+ import { reviewedCommandShapeError } from '../policy/approved-commands.ts';
11
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
12
+
13
+ type HarnessActionContext = Pick<
14
+ FullHarnessActionContext,
15
+ | 'dependencies'
16
+ | 'isSessionActive'
17
+ | 'latestContext'
18
+ | 'launchPromptReview'
19
+ | 'persist'
20
+ | 'pi'
21
+ | 'restoreBaselineTools'
22
+ | 'run'
23
+ | 'sessionEpoch'
24
+ | 'settleAfterTransition'
25
+ | 'updateStatus'
26
+ >;
27
+
28
+ export type GateSubmissionAction = {
29
+ submitGate: (
30
+ this: HarnessActionContext,
31
+ workflow: LoadedWorkflow,
32
+ originalRun: WorkflowRun,
33
+ outcome: string,
34
+ artifact: string,
35
+ ) => Promise<void>;
36
+ };
37
+
38
+ function isCurrentGateRequest(
39
+ run: WorkflowRun | undefined,
40
+ originalRun: WorkflowRun,
41
+ requestId: string,
42
+ ): run is WorkflowRun {
43
+ return (
44
+ run !== undefined &&
45
+ run.runId === originalRun.runId &&
46
+ run.currentStepId === originalRun.currentStepId &&
47
+ run.pendingGate?.requestId === requestId &&
48
+ run.pendingGate.reviewId === undefined &&
49
+ (run.status === 'awaiting-gate' || run.status === 'paused')
50
+ );
51
+ }
52
+
53
+ async function submitGate(
54
+ this: HarnessActionContext,
55
+ workflow: LoadedWorkflow,
56
+ originalRun: WorkflowRun,
57
+ outcome: string,
58
+ artifact: string,
59
+ ): Promise<void> {
60
+ const requestSessionEpoch = this.sessionEpoch;
61
+ const requestId =
62
+ `${originalRun.runId}:${originalRun.currentStepId}:` +
63
+ this.dependencies.createRequestId();
64
+ const step = workflow.definition.steps[originalRun.currentStepId];
65
+ if (!step?.gate) throw new Error('Current step has no gate');
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
+ this.run = beginGate(
96
+ workflow,
97
+ originalRun,
98
+ outcome,
99
+ artifact,
100
+ requestId,
101
+ this.dependencies.now(),
102
+ );
103
+ this.persist();
104
+ this.restoreBaselineTools();
105
+ this.updateStatus();
106
+
107
+ if (step.gate.provider === 'prompt') {
108
+ this.launchPromptReview(workflow, this.run, this.latestContext);
109
+ return;
110
+ }
111
+
112
+ const response = await this.dependencies.requestPlannotatorReview(
113
+ this.pi.events,
114
+ requestId,
115
+ artifact,
116
+ `pi-workflows:${workflow.definition.id}:${originalRun.currentStepId}`,
117
+ step.gate.timeoutMs,
118
+ );
119
+ const currentRun = this.run;
120
+ if (
121
+ !this.isSessionActive ||
122
+ this.sessionEpoch !== requestSessionEpoch ||
123
+ !isCurrentGateRequest(currentRun, originalRun, requestId)
124
+ ) {
125
+ throw new Error('Gate request was superseded by a workflow state change');
126
+ }
127
+ if (response.status !== 'handled') {
128
+ const reason = response.error ?? 'Plannotator is unavailable';
129
+ const gateFailed = failGate(currentRun, reason, this.dependencies.now());
130
+ this.run = failRun(gateFailed, reason, this.dependencies.now());
131
+ this.persist();
132
+ this.restoreBaselineTools();
133
+ this.updateStatus();
134
+ throw new Error(reason);
135
+ }
136
+ this.run = attachGateReviewId(
137
+ currentRun,
138
+ response.result.reviewId,
139
+ this.dependencies.now(),
140
+ );
141
+ this.persist();
142
+ this.updateStatus();
143
+ this.latestContext?.ui.notify(
144
+ `Submitted "${originalRun.currentStepId}" for Plannotator review ${response.result.reviewId}`,
145
+ 'info',
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Returns the gate-submission action for harness composition.
151
+ */
152
+ export function createGateSubmissionAction(): GateSubmissionAction {
153
+ return { submitGate };
154
+ }
@@ -0,0 +1,154 @@
1
+ import { failRun } from '../engine/transitions.ts';
2
+ import { buildMainWorkflowNotice } from '../prompt.ts';
3
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
4
+ import { parseAvailableSkills } from './catalog.ts';
5
+ import { waitForEventContextIdle } from './context-idle.ts';
6
+
7
+ type HarnessActionContext = Pick<
8
+ FullHarnessActionContext,
9
+ | 'cancelActiveDelegation'
10
+ | 'cancelPromptReview'
11
+ | 'captureSkills'
12
+ | 'catalog'
13
+ | 'dependencies'
14
+ | 'enqueueMutation'
15
+ | 'isSessionActive'
16
+ | 'latestContext'
17
+ | 'mainSteps'
18
+ | 'persist'
19
+ | 'pi'
20
+ | 'registeredWorkflowCommands'
21
+ | 'reloadCatalog'
22
+ | 'restoreBaselineTools'
23
+ | 'restoreFromSession'
24
+ | 'run'
25
+ | 'sessionEpoch'
26
+ | 'startNow'
27
+ | 'statusShortcutLabel'
28
+ | 'stopStatusRefresh'
29
+ | 'updateStatus'
30
+ >;
31
+
32
+ export type LifecycleActions = {
33
+ registerMultilineCommandInput: (this: HarnessActionContext) => void;
34
+ registerLifecycle: (this: HarnessActionContext) => void;
35
+ registerPolicy: (this: HarnessActionContext) => void;
36
+ };
37
+
38
+ function registerMultilineCommandInput(this: HarnessActionContext): void {
39
+ this.pi.on('input', async (event, context) => {
40
+ if (
41
+ event.source === 'extension' ||
42
+ event.images?.length ||
43
+ !event.text.startsWith('/')
44
+ ) {
45
+ return;
46
+ }
47
+ const newlineIndex = event.text.indexOf('\n');
48
+ if (newlineIndex === -1) return;
49
+ const command = event.text.slice(1, newlineIndex).replace(/\r$/, '');
50
+ if (!this.registeredWorkflowCommands.has(command)) return;
51
+ const workflow = [...this.catalog.workflows.values()].find(
52
+ (candidate) => candidate.definition.command === command,
53
+ );
54
+ if (!workflow) return;
55
+
56
+ const input = event.text.slice(newlineIndex + 1);
57
+ const skills = parseAvailableSkills(context.getSystemPrompt());
58
+ try {
59
+ await this.enqueueMutation(context, (sessionEpoch) =>
60
+ this.startNow(
61
+ workflow.definition.id,
62
+ input,
63
+ {
64
+ context,
65
+ skills: () => skills,
66
+ waitForIdle: () =>
67
+ waitForEventContextIdle(context, this.dependencies),
68
+ },
69
+ sessionEpoch,
70
+ ),
71
+ );
72
+ } catch (error) {
73
+ context.ui.notify(
74
+ `Cannot start workflow: ${error instanceof Error ? error.message : String(error)}`,
75
+ 'error',
76
+ );
77
+ }
78
+ return { action: 'handled' as const };
79
+ });
80
+ }
81
+
82
+ function registerLifecycle(this: HarnessActionContext): void {
83
+ this.pi.on('session_start', async (_event, context) => {
84
+ this.sessionEpoch += 1;
85
+ this.isSessionActive = false;
86
+ this.cancelPromptReview();
87
+ this.mainSteps.deactivate();
88
+ await this.cancelActiveDelegation('Pi session changed');
89
+ if (this.run) this.restoreBaselineTools();
90
+ this.run = undefined;
91
+ this.latestContext = context;
92
+ if (!(await this.reloadCatalog(context, false))) return;
93
+ this.restoreFromSession(context);
94
+ this.isSessionActive = true;
95
+ });
96
+
97
+ this.pi.on('session_tree', async (_event, context) => {
98
+ this.sessionEpoch += 1;
99
+ this.isSessionActive = false;
100
+ this.cancelPromptReview();
101
+ this.mainSteps.deactivate();
102
+ await this.cancelActiveDelegation('Pi session tree changed');
103
+ this.latestContext = context;
104
+ if (!(await this.reloadCatalog(context, false))) return;
105
+ this.restoreFromSession(context);
106
+ this.isSessionActive = true;
107
+ });
108
+
109
+ this.pi.on('session_shutdown', async () => {
110
+ this.sessionEpoch += 1;
111
+ this.isSessionActive = false;
112
+ this.cancelPromptReview();
113
+ this.mainSteps.deactivate();
114
+ await this.cancelActiveDelegation('Pi session shut down');
115
+ if (this.run) this.restoreBaselineTools();
116
+ this.run = undefined;
117
+ this.latestContext = undefined;
118
+ this.stopStatusRefresh();
119
+ });
120
+ }
121
+
122
+ function registerPolicy(this: HarnessActionContext): void {
123
+ this.pi.on('before_agent_start', (event, context) => {
124
+ this.latestContext = context;
125
+ this.captureSkills(event.systemPromptOptions.skills);
126
+ if (!this.run || this.run.status !== 'running') return;
127
+ const workflow = this.catalog.workflows.get(this.run.workflowId);
128
+ if (!workflow) {
129
+ this.run = failRun(
130
+ this.run,
131
+ 'Workflow configuration disappeared; reload or restore it',
132
+ this.dependencies.now(),
133
+ );
134
+ this.persist();
135
+ this.restoreBaselineTools();
136
+ this.updateStatus();
137
+ return;
138
+ }
139
+ return {
140
+ systemPrompt: `${event.systemPrompt}\n\n${buildMainWorkflowNotice(workflow, this.run, this.statusShortcutLabel)}`,
141
+ };
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Returns Pi lifecycle and input registration actions for composition.
147
+ */
148
+ export function createLifecycleActions(): LifecycleActions {
149
+ return {
150
+ registerMultilineCommandInput,
151
+ registerLifecycle,
152
+ registerPolicy,
153
+ };
154
+ }
@@ -0,0 +1,120 @@
1
+ import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import { abortRun, pauseRun } from '../engine/transitions.ts';
3
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
4
+
5
+ type HarnessActionContext = Pick<
6
+ FullHarnessActionContext,
7
+ | 'cancelActiveDelegation'
8
+ | 'cancelPromptReview'
9
+ | 'dependencies'
10
+ | 'isolateMainSessionTools'
11
+ | 'mainSteps'
12
+ | 'persist'
13
+ | 'restoreBaselineTools'
14
+ | 'run'
15
+ | 'updateStatus'
16
+ >;
17
+
18
+ export type PauseActions = {
19
+ pauseNow: (
20
+ this: HarnessActionContext,
21
+ reason: string,
22
+ context: ExtensionCommandContext,
23
+ ) => Promise<void>;
24
+ abortNow: (
25
+ this: HarnessActionContext,
26
+ reason: string,
27
+ context: ExtensionCommandContext,
28
+ ) => Promise<void>;
29
+ };
30
+
31
+ async function pauseNow(
32
+ this: HarnessActionContext,
33
+ reason: string,
34
+ context: ExtensionCommandContext,
35
+ ): Promise<void> {
36
+ if (
37
+ !this.run ||
38
+ this.run.status === 'completed' ||
39
+ this.run.status === 'aborted'
40
+ ) {
41
+ context.ui.notify('No active workflow to pause', 'warning');
42
+ return;
43
+ }
44
+ if (this.run.status === 'paused') {
45
+ context.ui.notify(
46
+ `Workflow is already paused${this.run.pauseReason ? `: ${this.run.pauseReason}` : ''}`,
47
+ 'info',
48
+ );
49
+ return;
50
+ }
51
+ this.cancelPromptReview();
52
+ const isMainStepSuspended = this.mainSteps.suspend();
53
+ const isCancellationConfirmed = await this.cancelActiveDelegation(
54
+ 'Workflow paused by user',
55
+ );
56
+ if (!context.isIdle()) {
57
+ context.abort();
58
+ if (isMainStepSuspended) await context.waitForIdle();
59
+ }
60
+
61
+ this.run = pauseRun(this.run, reason, this.dependencies.now());
62
+ this.persist();
63
+ if (isCancellationConfirmed) {
64
+ this.restoreBaselineTools();
65
+ } else {
66
+ this.isolateMainSessionTools();
67
+ }
68
+ this.updateStatus();
69
+ context.ui.notify(
70
+ isCancellationConfirmed
71
+ ? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}". Fix the issue, then run /workflow-resume.`
72
+ : `Pause recorded at "${this.run.currentStepId}", but child cancellation is not confirmed. Main tools remain isolated until it exits.`,
73
+ isCancellationConfirmed ? 'info' : 'warning',
74
+ );
75
+ }
76
+
77
+ async function abortNow(
78
+ this: HarnessActionContext,
79
+ reason: string,
80
+ context: ExtensionCommandContext,
81
+ ): Promise<void> {
82
+ if (
83
+ !this.run ||
84
+ this.run.status === 'completed' ||
85
+ this.run.status === 'aborted'
86
+ ) {
87
+ context.ui.notify('No active workflow to abort', 'warning');
88
+ return;
89
+ }
90
+ this.cancelPromptReview();
91
+ const isMainStepSuspended = this.mainSteps.suspend();
92
+ const isCancellationConfirmed = await this.cancelActiveDelegation(
93
+ 'Workflow aborted by user',
94
+ );
95
+ if (!context.isIdle()) {
96
+ context.abort();
97
+ if (isMainStepSuspended) await context.waitForIdle();
98
+ }
99
+ this.run = abortRun(this.run, reason, this.dependencies.now());
100
+ this.persist();
101
+ if (isCancellationConfirmed) {
102
+ this.restoreBaselineTools();
103
+ } else {
104
+ this.isolateMainSessionTools();
105
+ }
106
+ this.updateStatus();
107
+ context.ui.notify(
108
+ isCancellationConfirmed
109
+ ? `Aborted workflow "${this.run.workflowId}"`
110
+ : `Workflow "${this.run.workflowId}" is aborted, but its child has not confirmed cancellation; main tools remain isolated`,
111
+ isCancellationConfirmed ? 'info' : 'warning',
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Returns pause and abort actions for harness composition.
117
+ */
118
+ export function createPauseActions(): PauseActions {
119
+ return { pauseNow, abortNow };
120
+ }
@@ -0,0 +1,123 @@
1
+ import type { GateResolution } from '../engine/state.ts';
2
+ import {
3
+ failRun,
4
+ resolveGate,
5
+ storeGateResolution,
6
+ } from '../engine/transitions.ts';
7
+ import {
8
+ parsePlannotatorResult,
9
+ PLANNOTATOR_RESULT_CHANNEL,
10
+ } from '../integrations/plannotator.ts';
11
+ import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
12
+
13
+ type HarnessActionContext = Pick<
14
+ FullHarnessActionContext,
15
+ | 'catalog'
16
+ | 'dependencies'
17
+ | 'handlePlannotatorResult'
18
+ | 'isSessionActive'
19
+ | 'latestContext'
20
+ | 'mutationQueue'
21
+ | 'persist'
22
+ | 'pi'
23
+ | 'restoreBaselineTools'
24
+ | 'run'
25
+ | 'settleAfterTransition'
26
+ | 'updateStatus'
27
+ >;
28
+
29
+ export type PlannotatorResultActions = {
30
+ registerPlannotatorResults: (this: HarnessActionContext) => void;
31
+ handlePlannotatorResult: (
32
+ this: HarnessActionContext,
33
+ data: unknown,
34
+ ) => Promise<void>;
35
+ };
36
+
37
+ function registerPlannotatorResults(this: HarnessActionContext): void {
38
+ this.pi.events.on(PLANNOTATOR_RESULT_CHANNEL, (data) => {
39
+ void this.mutationQueue
40
+ .run(() => this.handlePlannotatorResult(data))
41
+ .catch((error: unknown) => {
42
+ this.latestContext?.ui.notify(
43
+ `Cannot apply Plannotator result: ${
44
+ error instanceof Error ? error.message : String(error)
45
+ }`,
46
+ 'error',
47
+ );
48
+ });
49
+ });
50
+ }
51
+
52
+ async function handlePlannotatorResult(
53
+ this: HarnessActionContext,
54
+ data: unknown,
55
+ ): Promise<void> {
56
+ if (
57
+ !this.isSessionActive ||
58
+ this.run?.pendingGate?.provider !== 'plannotator' ||
59
+ !this.run.pendingGate.reviewId
60
+ ) {
61
+ return;
62
+ }
63
+ const result = parsePlannotatorResult(data);
64
+ if (!result || result.reviewId !== this.run.pendingGate.reviewId) return;
65
+
66
+ const resolution: GateResolution = {
67
+ approved: result.approved,
68
+ feedback: result.feedback,
69
+ resolvedAt: this.dependencies.now(),
70
+ };
71
+ if (this.run.status === 'paused') {
72
+ this.run = storeGateResolution(
73
+ this.run,
74
+ resolution,
75
+ this.dependencies.now(),
76
+ );
77
+ this.persist();
78
+ this.latestContext?.ui.notify(
79
+ `Review ${result.reviewId} finished while paused. Run /workflow-resume to apply it.`,
80
+ 'info',
81
+ );
82
+ return;
83
+ }
84
+ if (this.run.status !== 'awaiting-gate') return;
85
+
86
+ const workflow = this.catalog.workflows.get(this.run.workflowId);
87
+ if (!workflow) {
88
+ this.run = failRun(
89
+ this.run,
90
+ 'Gate result arrived, but workflow configuration is unavailable',
91
+ this.dependencies.now(),
92
+ );
93
+ this.persist();
94
+ this.restoreBaselineTools();
95
+ this.updateStatus();
96
+ return;
97
+ }
98
+ try {
99
+ this.run = resolveGate(
100
+ workflow,
101
+ this.run,
102
+ resolution,
103
+ this.dependencies.now(),
104
+ );
105
+ this.settleAfterTransition(workflow);
106
+ } 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
+ );
112
+ this.persist();
113
+ this.restoreBaselineTools();
114
+ this.updateStatus();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Returns Plannotator result-channel actions for harness composition.
120
+ */
121
+ export function createPlannotatorResultActions(): PlannotatorResultActions {
122
+ return { registerPlannotatorResults, handlePlannotatorResult };
123
+ }