@wichayutdew/pi-workflows 0.2.3 → 1.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 (142) hide show
  1. package/README.md +66 -38
  2. package/dist/index.js +5903 -4782
  3. package/package.json +1 -1
  4. package/src/command-names.ts +7 -1
  5. package/src/commands.ts +126 -52
  6. package/src/config/catalog.ts +162 -0
  7. package/src/config/ceiling.ts +116 -116
  8. package/src/config/command-conflicts.ts +4 -4
  9. package/src/config/diagnostics.ts +28 -0
  10. package/src/config/load-settings.ts +37 -0
  11. package/src/config/load-types.ts +53 -0
  12. package/src/config/load-workflows.ts +135 -0
  13. package/src/config/load.ts +43 -311
  14. package/src/config/types.ts +137 -135
  15. package/src/config/validate.ts +12 -1261
  16. package/src/config/validation/permissions.ts +291 -0
  17. package/src/config/validation/prompt.ts +19 -0
  18. package/src/config/validation/settings.ts +122 -0
  19. package/src/config/validation/shared.ts +129 -0
  20. package/src/config/validation/shortcut.ts +118 -0
  21. package/src/config/validation/step.ts +228 -0
  22. package/src/config/validation/subagent.ts +288 -0
  23. package/src/config/validation/workflow.ts +156 -0
  24. package/src/config/yaml.ts +29 -0
  25. package/src/digest.ts +38 -6
  26. package/src/engine/checkpoint.ts +13 -9
  27. package/src/engine/create-run.ts +42 -0
  28. package/src/engine/gate-transitions.ts +184 -0
  29. package/src/engine/reconciliation-history.ts +88 -0
  30. package/src/engine/resume.ts +19 -7
  31. package/src/engine/run-advance.ts +122 -0
  32. package/src/engine/run-lifecycle.ts +124 -0
  33. package/src/engine/run-reconciliation.ts +116 -0
  34. package/src/engine/run-validation.ts +128 -0
  35. package/src/engine/state-types.ts +64 -0
  36. package/src/engine/state.ts +10 -193
  37. package/src/engine/transition-helpers.ts +28 -0
  38. package/src/engine/transition-types.ts +8 -0
  39. package/src/engine/transitions.ts +17 -471
  40. package/src/harness/action-context.ts +181 -0
  41. package/src/harness/catalog.ts +45 -0
  42. package/src/harness/context-idle.ts +21 -0
  43. package/src/harness/core-actions.ts +299 -0
  44. package/src/harness/delegation-control-actions.ts +250 -0
  45. package/src/harness/delegation-failure.ts +215 -0
  46. package/src/harness/delegation-plan.ts +219 -0
  47. package/src/harness/delegation-recovery-validation.ts +159 -0
  48. package/src/harness/delegation-response-actions.ts +295 -0
  49. package/src/harness/delegation-retry-policy.ts +118 -0
  50. package/src/harness/dependencies.ts +165 -0
  51. package/src/harness/gate-submission-action.ts +154 -0
  52. package/src/harness/lifecycle-actions.ts +154 -0
  53. package/src/harness/pause-actions.ts +120 -0
  54. package/src/harness/plannotator-result-actions.ts +123 -0
  55. package/src/harness/prompt-gate-actions.ts +270 -0
  56. package/src/harness/resume-action.ts +261 -0
  57. package/src/harness/start-actions.ts +183 -0
  58. package/src/harness/status-actions.ts +191 -0
  59. package/src/harness/step-execution-actions.ts +283 -0
  60. package/src/harness/types.ts +76 -0
  61. package/src/harness.ts +237 -2201
  62. package/src/index.ts +73 -11
  63. package/src/integrations/plannotator-requests.ts +127 -0
  64. package/src/integrations/plannotator-responses.ts +141 -0
  65. package/src/integrations/plannotator-types.ts +44 -0
  66. package/src/integrations/plannotator.ts +16 -235
  67. package/src/integrations/prompt-gate.ts +17 -10
  68. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  69. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  70. package/src/integrations/subagents/child-policy-sections.ts +174 -0
  71. package/src/integrations/subagents/child-policy-types.ts +32 -0
  72. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  73. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  74. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  75. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  76. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  77. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  78. package/src/integrations/subagents/child-runtime.ts +140 -301
  79. package/src/integrations/subagents/client-delegation.ts +181 -0
  80. package/src/integrations/subagents/client-messages.ts +66 -0
  81. package/src/integrations/subagents/client-types.ts +36 -0
  82. package/src/integrations/subagents/client.ts +127 -219
  83. package/src/integrations/subagents/delegated-result.ts +31 -0
  84. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  85. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  86. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  87. package/src/integrations/subagents/diagnostics.ts +24 -799
  88. package/src/integrations/subagents/failure-correlation.ts +235 -0
  89. package/src/integrations/subagents/failure-transcript.ts +192 -0
  90. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  91. package/src/integrations/subagents/protocol-events.ts +27 -0
  92. package/src/integrations/subagents/protocol.ts +30 -375
  93. package/src/integrations/subagents/replay-audit.ts +147 -0
  94. package/src/integrations/subagents/replay-safety.ts +82 -0
  95. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  96. package/src/policy/approved-command-extraction.ts +71 -0
  97. package/src/policy/approved-commands.ts +15 -432
  98. package/src/policy/bash-authorization.ts +86 -0
  99. package/src/policy/bash-read-only.ts +161 -0
  100. package/src/policy/bash-types.ts +20 -0
  101. package/src/policy/bash.ts +4 -346
  102. package/src/policy/completion-batch.ts +21 -16
  103. package/src/policy/immutable-input.ts +5 -2
  104. package/src/policy/mcp-authorization.ts +71 -0
  105. package/src/policy/restricted-command.ts +141 -0
  106. package/src/policy/restricted-git.ts +30 -0
  107. package/src/policy/reviewed-artifact.ts +97 -0
  108. package/src/policy/reviewed-command-safety.ts +150 -0
  109. package/src/policy/reviewed-command-shape.ts +49 -0
  110. package/src/policy/reviewed-repository-contract.ts +138 -0
  111. package/src/policy/reviewed-repository-cwd.ts +102 -0
  112. package/src/policy/tool-call-authorization.ts +58 -0
  113. package/src/policy/tool-selection.ts +72 -0
  114. package/src/policy/tool-types.ts +14 -0
  115. package/src/policy/tools.ts +11 -150
  116. package/src/preflight.ts +96 -61
  117. package/src/prompt/main-workflow-notice.ts +41 -0
  118. package/src/prompt/retry-task.ts +83 -0
  119. package/src/prompt/step-contract.ts +101 -0
  120. package/src/prompt/step-sections.ts +75 -0
  121. package/src/prompt/step-task.ts +156 -0
  122. package/src/prompt/template.ts +81 -0
  123. package/src/prompt.ts +10 -245
  124. package/src/runtime/completion-tool.ts +3 -0
  125. package/src/runtime/main-step-completion.ts +78 -0
  126. package/src/runtime/main-step-lifecycle.ts +37 -0
  127. package/src/runtime/main-step-policy.ts +84 -0
  128. package/src/runtime/main-step-runtime-types.ts +64 -0
  129. package/src/runtime/main-step-runtime.ts +129 -200
  130. package/src/runtime/main-step-state.ts +85 -0
  131. package/src/runtime/serial-task-queue.ts +48 -13
  132. package/src/runtime/step-result.ts +48 -27
  133. package/src/workflow-list.ts +15 -6
  134. package/src/workflow-status/format-status.ts +52 -0
  135. package/src/workflow-status/formatting.ts +178 -0
  136. package/src/workflow-status/layout.ts +109 -0
  137. package/src/workflow-status/render-board.ts +101 -0
  138. package/src/workflow-status/render-path.ts +123 -0
  139. package/src/workflow-status/render-summary.ts +144 -0
  140. package/src/workflow-status/types.ts +40 -0
  141. package/src/workflow-status/view.ts +273 -0
  142. 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
+ }