@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
package/src/index.ts CHANGED
@@ -1,19 +1,81 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import type { KeyId } from '@earendil-works/pi-tui';
2
3
  import { defaultUserWorkflowDirectory, loadSettings } from './config/load.ts';
3
4
  import { WorkflowHarness } from './harness.ts';
4
5
  import { registerSubagentChildRuntime } from './integrations/subagents/child-runtime.ts';
5
6
  import { isSubagentRuntimeName } from './integrations/subagents/protocol.ts';
6
7
 
7
- export default async function piWorkflowsExtension(
8
- pi: ExtensionAPI,
9
- ): Promise<void> {
10
- if (process.env.PI_SUBAGENT_CHILD === '1') {
11
- const childAgent = process.env.PI_SUBAGENT_CHILD_AGENT?.trim();
12
- if (isSubagentRuntimeName(childAgent)) {
13
- registerSubagentChildRuntime(pi, { childAgent });
8
+ /**
9
+ * Asynchronous entry point accepted by Pi's extension loader.
10
+ */
11
+ export type PiWorkflowsExtension = (pi: ExtensionAPI) => Promise<void>;
12
+
13
+ /**
14
+ * Process-role information used during extension registration.
15
+ */
16
+ export type PiWorkflowsRuntimeEnvironment = {
17
+ readonly isSubagentChild: boolean;
18
+ readonly childAgent: string | undefined;
19
+ };
20
+
21
+ /**
22
+ * External effects required to create the extension entry point.
23
+ */
24
+ export type PiWorkflowsExtensionDependencies = {
25
+ readonly loadSettings: typeof loadSettings;
26
+ readonly userWorkflowDirectory: () => string;
27
+ readonly runtimeEnvironment: () => PiWorkflowsRuntimeEnvironment;
28
+ readonly isSubagentRuntimeName: typeof isSubagentRuntimeName;
29
+ readonly registerChildRuntime: (pi: ExtensionAPI, childAgent: string) => void;
30
+ readonly createHarness: (pi: ExtensionAPI, statusShortcut: KeyId) => void;
31
+ };
32
+
33
+ const DEFAULT_DEPENDENCIES = {
34
+ loadSettings,
35
+ userWorkflowDirectory: defaultUserWorkflowDirectory,
36
+ runtimeEnvironment: (): PiWorkflowsRuntimeEnvironment => ({
37
+ isSubagentChild: process.env.PI_SUBAGENT_CHILD === '1',
38
+ childAgent: process.env.PI_SUBAGENT_CHILD_AGENT?.trim(),
39
+ }),
40
+ isSubagentRuntimeName,
41
+ registerChildRuntime: (pi, childAgent): void => {
42
+ registerSubagentChildRuntime(pi, { childAgent });
43
+ },
44
+ createHarness: (pi, statusShortcut): void => {
45
+ new WorkflowHarness(pi, statusShortcut);
46
+ },
47
+ } as const satisfies PiWorkflowsExtensionDependencies;
48
+
49
+ /**
50
+ * Creates the Pi extension entry point from explicit environment and runtime
51
+ * dependencies.
52
+ *
53
+ * @param dependencies - Configuration and runtime effects for the extension.
54
+ * @returns An asynchronous Pi extension factory.
55
+ */
56
+ export function createPiWorkflowsExtension(
57
+ dependencies: PiWorkflowsExtensionDependencies,
58
+ ): PiWorkflowsExtension {
59
+ return async (pi): Promise<void> => {
60
+ const environment = dependencies.runtimeEnvironment();
61
+
62
+ if (environment.isSubagentChild) {
63
+ if (dependencies.isSubagentRuntimeName(environment.childAgent)) {
64
+ dependencies.registerChildRuntime(pi, environment.childAgent);
65
+ }
66
+ return;
14
67
  }
15
- return;
16
- }
17
- const { settings } = await loadSettings(defaultUserWorkflowDirectory());
18
- new WorkflowHarness(pi, settings.statusShortcut);
68
+
69
+ const { settings } = await dependencies.loadSettings(
70
+ dependencies.userWorkflowDirectory(),
71
+ );
72
+ dependencies.createHarness(pi, settings.statusShortcut);
73
+ };
19
74
  }
75
+
76
+ /**
77
+ * Registers the workflow harness or delegated child policy runtime with Pi.
78
+ */
79
+ const piWorkflowsExtension = createPiWorkflowsExtension(DEFAULT_DEPENDENCIES);
80
+
81
+ export default piWorkflowsExtension;
@@ -0,0 +1,127 @@
1
+ import {
2
+ normalizePlannotatorStartResponse,
3
+ normalizePlannotatorStatusResponse,
4
+ } from './plannotator-responses.ts';
5
+ import type {
6
+ EventBusLike,
7
+ PlannotatorStartResponse,
8
+ PlannotatorStatusResponse,
9
+ } from './plannotator-types.ts';
10
+
11
+ export const PLANNOTATOR_REQUEST_CHANNEL = 'plannotator:request';
12
+ export const PLANNOTATOR_RESULT_CHANNEL = 'plannotator:review-result';
13
+
14
+ type TimeoutHandle = ReturnType<typeof setTimeout>;
15
+
16
+ export type PlannotatorDependencies = {
17
+ readonly scheduleTimeout: (
18
+ callback: () => void,
19
+ timeoutMs: number,
20
+ ) => TimeoutHandle;
21
+ readonly cancelTimeout: (handle: TimeoutHandle) => void;
22
+ };
23
+
24
+ const DEFAULT_DEPENDENCIES = {
25
+ scheduleTimeout: (callback: () => void, timeoutMs: number) =>
26
+ setTimeout(callback, timeoutMs),
27
+ cancelTimeout: (handle: TimeoutHandle) => {
28
+ clearTimeout(handle);
29
+ },
30
+ } as const satisfies PlannotatorDependencies;
31
+
32
+ type RequestResponseOptions<TResponse> = {
33
+ readonly events: EventBusLike;
34
+ readonly request: Readonly<Record<string, unknown>>;
35
+ readonly timeoutMs: number;
36
+ readonly timeoutResponse: TResponse;
37
+ readonly normalize: (response: unknown) => TResponse;
38
+ readonly dependencies: PlannotatorDependencies;
39
+ };
40
+
41
+ const requestResponse = <TResponse>({
42
+ events,
43
+ request,
44
+ timeoutMs,
45
+ timeoutResponse,
46
+ normalize,
47
+ dependencies,
48
+ }: RequestResponseOptions<TResponse>): Promise<TResponse> =>
49
+ new Promise((resolve) => {
50
+ let isSettled = false;
51
+ const finish = (response: unknown): void => {
52
+ if (isSettled) return;
53
+ isSettled = true;
54
+ dependencies.cancelTimeout(timer);
55
+ resolve(normalize(response));
56
+ };
57
+ const timer = dependencies.scheduleTimeout(() => {
58
+ finish(timeoutResponse);
59
+ }, timeoutMs);
60
+ timer.unref();
61
+
62
+ events.emit(PLANNOTATOR_REQUEST_CHANNEL, {
63
+ ...request,
64
+ respond: finish,
65
+ });
66
+ });
67
+
68
+ /**
69
+ * Requests a new Plannotator plan review through the injected event bus.
70
+ *
71
+ * @param dependencies Timer boundaries, injectable for deterministic callers.
72
+ */
73
+ export const requestPlannotatorReview = (
74
+ events: EventBusLike,
75
+ requestId: string,
76
+ content: string,
77
+ origin: string,
78
+ timeoutMs: number,
79
+ dependencies: PlannotatorDependencies = DEFAULT_DEPENDENCIES,
80
+ ): Promise<PlannotatorStartResponse> =>
81
+ requestResponse({
82
+ events,
83
+ request: {
84
+ requestId,
85
+ action: 'plan-review',
86
+ payload: {
87
+ planContent: content,
88
+ origin,
89
+ },
90
+ },
91
+ timeoutMs,
92
+ timeoutResponse: {
93
+ status: 'unavailable',
94
+ error: `Plannotator did not respond within ${timeoutMs}ms`,
95
+ },
96
+ normalize: normalizePlannotatorStartResponse,
97
+ dependencies,
98
+ });
99
+
100
+ /**
101
+ * Requests the current durable status for a Plannotator review.
102
+ *
103
+ * @param dependencies Timer boundaries, injectable for deterministic callers.
104
+ */
105
+ export const requestPlannotatorReviewStatus = (
106
+ events: EventBusLike,
107
+ requestId: string,
108
+ reviewId: string,
109
+ timeoutMs: number,
110
+ dependencies: PlannotatorDependencies = DEFAULT_DEPENDENCIES,
111
+ ): Promise<PlannotatorStatusResponse> =>
112
+ requestResponse({
113
+ events,
114
+ request: {
115
+ requestId,
116
+ action: 'review-status',
117
+ payload: { reviewId },
118
+ },
119
+ timeoutMs,
120
+ timeoutResponse: {
121
+ status: 'unavailable',
122
+ error: `Plannotator did not respond within ${timeoutMs}ms`,
123
+ },
124
+ normalize: (response) =>
125
+ normalizePlannotatorStatusResponse(response, reviewId),
126
+ dependencies,
127
+ });
@@ -0,0 +1,141 @@
1
+ import type {
2
+ PlannotatorReviewResult,
3
+ PlannotatorStartResponse,
4
+ PlannotatorStatusResponse,
5
+ } from './plannotator-types.ts';
6
+
7
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
8
+ value !== null && typeof value === 'object' && !Array.isArray(value);
9
+
10
+ const errorText = (
11
+ value: Readonly<Record<string, unknown>>,
12
+ fallback: string,
13
+ ): string =>
14
+ typeof value.error === 'string' && value.error.trim()
15
+ ? value.error
16
+ : fallback;
17
+
18
+ /**
19
+ * Normalizes an untrusted response to a Plannotator start request.
20
+ */
21
+ export const normalizePlannotatorStartResponse = (
22
+ value: unknown,
23
+ ): PlannotatorStartResponse => {
24
+ if (!isRecord(value)) {
25
+ return {
26
+ status: 'error',
27
+ error: 'Plannotator returned an invalid response',
28
+ };
29
+ }
30
+ if (value.status === 'unavailable') {
31
+ return {
32
+ status: 'unavailable',
33
+ error: errorText(value, 'Plannotator is unavailable'),
34
+ };
35
+ }
36
+ if (value.status === 'error') {
37
+ return {
38
+ status: 'error',
39
+ error: errorText(value, 'Plannotator failed'),
40
+ };
41
+ }
42
+
43
+ const result = isRecord(value.result) ? value.result : undefined;
44
+ if (
45
+ value.status === 'handled' &&
46
+ result?.status === 'pending' &&
47
+ typeof result.reviewId === 'string'
48
+ ) {
49
+ return {
50
+ status: 'handled',
51
+ result: { status: 'pending', reviewId: result.reviewId },
52
+ };
53
+ }
54
+ return {
55
+ status: 'error',
56
+ error: 'Plannotator returned an invalid start result',
57
+ };
58
+ };
59
+
60
+ /**
61
+ * Normalizes an untrusted response to a Plannotator status request.
62
+ */
63
+ export const normalizePlannotatorStatusResponse = (
64
+ value: unknown,
65
+ requestedReviewId: string,
66
+ ): PlannotatorStatusResponse => {
67
+ if (!isRecord(value)) {
68
+ return {
69
+ status: 'error',
70
+ error: 'Plannotator returned an invalid response',
71
+ };
72
+ }
73
+ if (value.status === 'unavailable') {
74
+ return {
75
+ status: 'unavailable',
76
+ error: errorText(value, 'Plannotator is unavailable'),
77
+ };
78
+ }
79
+ if (value.status === 'error') {
80
+ return {
81
+ status: 'error',
82
+ error: errorText(value, 'Plannotator failed'),
83
+ };
84
+ }
85
+
86
+ const result = isRecord(value.result) ? value.result : undefined;
87
+ if (value.status !== 'handled' || !result) {
88
+ return {
89
+ status: 'error',
90
+ error: 'Plannotator returned an invalid status result',
91
+ };
92
+ }
93
+ if (result.status === 'pending' || result.status === 'missing') {
94
+ return { status: 'handled', result: { status: result.status } };
95
+ }
96
+ if (
97
+ result.status === 'completed' &&
98
+ typeof result.reviewId === 'string' &&
99
+ typeof result.approved === 'boolean'
100
+ ) {
101
+ if (result.reviewId !== requestedReviewId) {
102
+ return {
103
+ status: 'error',
104
+ error: 'Plannotator returned a result for a different review',
105
+ };
106
+ }
107
+ return {
108
+ status: 'handled',
109
+ result: {
110
+ status: 'completed',
111
+ reviewId: result.reviewId,
112
+ approved: result.approved,
113
+ feedback: typeof result.feedback === 'string' ? result.feedback : '',
114
+ },
115
+ };
116
+ }
117
+ return {
118
+ status: 'error',
119
+ error: 'Plannotator returned an invalid status result',
120
+ };
121
+ };
122
+
123
+ /**
124
+ * Parses a pushed Plannotator review result.
125
+ */
126
+ export const parsePlannotatorResult = (
127
+ value: unknown,
128
+ ): PlannotatorReviewResult | undefined => {
129
+ if (!isRecord(value)) return undefined;
130
+ if (
131
+ typeof value.reviewId !== 'string' ||
132
+ typeof value.approved !== 'boolean'
133
+ ) {
134
+ return undefined;
135
+ }
136
+ return {
137
+ reviewId: value.reviewId,
138
+ approved: value.approved,
139
+ feedback: typeof value.feedback === 'string' ? value.feedback : '',
140
+ };
141
+ };
@@ -0,0 +1,44 @@
1
+ export type EventBusLike = {
2
+ readonly emit: (channel: string, data: unknown) => void;
3
+ readonly on: (
4
+ channel: string,
5
+ handler: (data: unknown) => void,
6
+ ) => () => void;
7
+ };
8
+
9
+ export type PlannotatorReviewStarted = {
10
+ readonly status: 'pending';
11
+ readonly reviewId: string;
12
+ };
13
+
14
+ export type PlannotatorStartResponse =
15
+ | {
16
+ readonly status: 'handled';
17
+ readonly result: PlannotatorReviewStarted;
18
+ }
19
+ | { readonly status: 'unavailable'; readonly error?: string }
20
+ | { readonly status: 'error'; readonly error: string };
21
+
22
+ export type PlannotatorReviewStatus =
23
+ | { readonly status: 'pending' }
24
+ | {
25
+ readonly status: 'completed';
26
+ readonly reviewId: string;
27
+ readonly approved: boolean;
28
+ readonly feedback: string;
29
+ }
30
+ | { readonly status: 'missing' };
31
+
32
+ export type PlannotatorStatusResponse =
33
+ | {
34
+ readonly status: 'handled';
35
+ readonly result: PlannotatorReviewStatus;
36
+ }
37
+ | { readonly status: 'unavailable'; readonly error?: string }
38
+ | { readonly status: 'error'; readonly error: string };
39
+
40
+ export type PlannotatorReviewResult = {
41
+ readonly reviewId: string;
42
+ readonly approved: boolean;
43
+ readonly feedback: string;
44
+ };
@@ -1,235 +1,16 @@
1
- export const PLANNOTATOR_REQUEST_CHANNEL = 'plannotator:request';
2
- export const PLANNOTATOR_RESULT_CHANNEL = 'plannotator:review-result';
3
-
4
- export interface EventBusLike {
5
- emit(channel: string, data: unknown): void;
6
- on(channel: string, handler: (data: unknown) => void): () => void;
7
- }
8
-
9
- export interface PlannotatorReviewStarted {
10
- status: 'pending';
11
- reviewId: string;
12
- }
13
-
14
- export type PlannotatorStartResponse =
15
- | { status: 'handled'; result: PlannotatorReviewStarted }
16
- | { status: 'unavailable'; error?: string }
17
- | { status: 'error'; error: string };
18
-
19
- export type PlannotatorReviewStatus =
20
- | { status: 'pending' }
21
- | {
22
- status: 'completed';
23
- reviewId: string;
24
- approved: boolean;
25
- feedback: string;
26
- }
27
- | { status: 'missing' };
28
-
29
- export type PlannotatorStatusResponse =
30
- | { status: 'handled'; result: PlannotatorReviewStatus }
31
- | { status: 'unavailable'; error?: string }
32
- | { status: 'error'; error: string };
33
-
34
- export interface PlannotatorReviewResult {
35
- reviewId: string;
36
- approved: boolean;
37
- feedback: string;
38
- }
39
-
40
- function errorText(value: Record<string, unknown>, fallback: string): string {
41
- return typeof value.error === 'string' && value.error.trim()
42
- ? value.error
43
- : fallback;
44
- }
45
-
46
- function normalizeStartResponse(value: unknown): PlannotatorStartResponse {
47
- if (value === null || typeof value !== 'object') {
48
- return {
49
- status: 'error',
50
- error: 'Plannotator returned an invalid response',
51
- };
52
- }
53
- const response = value as Record<string, unknown>;
54
- if (response.status === 'unavailable') {
55
- return {
56
- status: 'unavailable',
57
- error: errorText(response, 'Plannotator is unavailable'),
58
- };
59
- }
60
- if (response.status === 'error') {
61
- return {
62
- status: 'error',
63
- error: errorText(response, 'Plannotator failed'),
64
- };
65
- }
66
- const result =
67
- response.result !== null && typeof response.result === 'object'
68
- ? (response.result as Record<string, unknown>)
69
- : undefined;
70
- if (
71
- response.status === 'handled' &&
72
- result?.status === 'pending' &&
73
- typeof result.reviewId === 'string'
74
- ) {
75
- return {
76
- status: 'handled',
77
- result: { status: 'pending', reviewId: result.reviewId },
78
- };
79
- }
80
- return {
81
- status: 'error',
82
- error: 'Plannotator returned an invalid start result',
83
- };
84
- }
85
-
86
- function normalizeStatusResponse(
87
- value: unknown,
88
- requestedReviewId: string,
89
- ): PlannotatorStatusResponse {
90
- if (value === null || typeof value !== 'object') {
91
- return {
92
- status: 'error',
93
- error: 'Plannotator returned an invalid response',
94
- };
95
- }
96
- const response = value as Record<string, unknown>;
97
- if (response.status === 'unavailable') {
98
- return {
99
- status: 'unavailable',
100
- error: errorText(response, 'Plannotator is unavailable'),
101
- };
102
- }
103
- if (response.status === 'error') {
104
- return {
105
- status: 'error',
106
- error: errorText(response, 'Plannotator failed'),
107
- };
108
- }
109
- const result =
110
- response.result !== null && typeof response.result === 'object'
111
- ? (response.result as Record<string, unknown>)
112
- : undefined;
113
- if (response.status !== 'handled' || !result) {
114
- return {
115
- status: 'error',
116
- error: 'Plannotator returned an invalid status result',
117
- };
118
- }
119
- if (result.status === 'pending' || result.status === 'missing') {
120
- return { status: 'handled', result: { status: result.status } };
121
- }
122
- if (
123
- result.status === 'completed' &&
124
- typeof result.reviewId === 'string' &&
125
- typeof result.approved === 'boolean'
126
- ) {
127
- if (result.reviewId !== requestedReviewId) {
128
- return {
129
- status: 'error',
130
- error: 'Plannotator returned a result for a different review',
131
- };
132
- }
133
- return {
134
- status: 'handled',
135
- result: {
136
- status: 'completed',
137
- reviewId: result.reviewId,
138
- approved: result.approved,
139
- feedback: typeof result.feedback === 'string' ? result.feedback : '',
140
- },
141
- };
142
- }
143
- return {
144
- status: 'error',
145
- error: 'Plannotator returned an invalid status result',
146
- };
147
- }
148
-
149
- export function parsePlannotatorResult(
150
- value: unknown,
151
- ): PlannotatorReviewResult | undefined {
152
- if (value === null || typeof value !== 'object') return undefined;
153
- const result = value as Record<string, unknown>;
154
- if (
155
- typeof result.reviewId !== 'string' ||
156
- typeof result.approved !== 'boolean'
157
- ) {
158
- return undefined;
159
- }
160
- return {
161
- reviewId: result.reviewId,
162
- approved: result.approved,
163
- feedback: typeof result.feedback === 'string' ? result.feedback : '',
164
- };
165
- }
166
-
167
- export function requestPlannotatorReview(
168
- events: EventBusLike,
169
- requestId: string,
170
- content: string,
171
- origin: string,
172
- timeoutMs: number,
173
- ): Promise<PlannotatorStartResponse> {
174
- return new Promise((resolve) => {
175
- let settled = false;
176
- const finish = (response: unknown) => {
177
- if (settled) return;
178
- settled = true;
179
- clearTimeout(timer);
180
- resolve(normalizeStartResponse(response));
181
- };
182
- const timer = setTimeout(
183
- () =>
184
- finish({
185
- status: 'unavailable',
186
- error: `Plannotator did not respond within ${timeoutMs}ms`,
187
- }),
188
- timeoutMs,
189
- );
190
- timer.unref?.();
191
-
192
- events.emit(PLANNOTATOR_REQUEST_CHANNEL, {
193
- requestId,
194
- action: 'plan-review',
195
- payload: {
196
- planContent: content,
197
- origin,
198
- },
199
- respond: finish,
200
- });
201
- });
202
- }
203
-
204
- export function requestPlannotatorReviewStatus(
205
- events: EventBusLike,
206
- requestId: string,
207
- reviewId: string,
208
- timeoutMs: number,
209
- ): Promise<PlannotatorStatusResponse> {
210
- return new Promise((resolve) => {
211
- let settled = false;
212
- const finish = (response: unknown) => {
213
- if (settled) return;
214
- settled = true;
215
- clearTimeout(timer);
216
- resolve(normalizeStatusResponse(response, reviewId));
217
- };
218
- const timer = setTimeout(
219
- () =>
220
- finish({
221
- status: 'unavailable',
222
- error: `Plannotator did not respond within ${timeoutMs}ms`,
223
- }),
224
- timeoutMs,
225
- );
226
- timer.unref?.();
227
-
228
- events.emit(PLANNOTATOR_REQUEST_CHANNEL, {
229
- requestId,
230
- action: 'review-status',
231
- payload: { reviewId },
232
- respond: finish,
233
- });
234
- });
235
- }
1
+ export {
2
+ PLANNOTATOR_REQUEST_CHANNEL,
3
+ PLANNOTATOR_RESULT_CHANNEL,
4
+ requestPlannotatorReview,
5
+ requestPlannotatorReviewStatus,
6
+ } from './plannotator-requests.ts';
7
+ export type { PlannotatorDependencies } from './plannotator-requests.ts';
8
+ export { parsePlannotatorResult } from './plannotator-responses.ts';
9
+ export type {
10
+ EventBusLike,
11
+ PlannotatorReviewResult,
12
+ PlannotatorReviewStarted,
13
+ PlannotatorReviewStatus,
14
+ PlannotatorStartResponse,
15
+ PlannotatorStatusResponse,
16
+ } from './plannotator-types.ts';