@markjaquith/agency 2.8.0 → 2.10.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.
@@ -0,0 +1,217 @@
1
+ import { Schema } from "@effect/schema"
2
+
3
+ export const PROTOCOL_VERSION = 1 as const
4
+
5
+ const ErrorFields = Schema.Record({
6
+ key: Schema.String,
7
+ value: Schema.Unknown,
8
+ })
9
+
10
+ export const SuccessEnvelope = Schema.Struct({
11
+ version: Schema.Literal(PROTOCOL_VERSION),
12
+ ok: Schema.Literal(true),
13
+ result: Schema.Unknown,
14
+ })
15
+
16
+ export const ErrorDetail = Schema.Struct({
17
+ code: Schema.String,
18
+ message: Schema.String,
19
+ fields: ErrorFields,
20
+ retryable: Schema.Boolean,
21
+ remediation: Schema.optional(Schema.String),
22
+ })
23
+
24
+ export const ErrorEnvelope = Schema.Struct({
25
+ version: Schema.Literal(PROTOCOL_VERSION),
26
+ ok: Schema.Literal(false),
27
+ error: ErrorDetail,
28
+ })
29
+
30
+ export const AgencyEnvelope = Schema.Union(SuccessEnvelope, ErrorEnvelope)
31
+
32
+ export type SuccessEnvelope = Schema.Schema.Type<typeof SuccessEnvelope>
33
+ export type ErrorEnvelope = Schema.Schema.Type<typeof ErrorEnvelope>
34
+ export type AgencyEnvelope = Schema.Schema.Type<typeof AgencyEnvelope>
35
+
36
+ interface ErrorMetadata {
37
+ readonly code: string
38
+ readonly retryable: boolean
39
+ readonly remediation?: string
40
+ }
41
+
42
+ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
43
+ CliUsageError: {
44
+ code: "CLI_USAGE",
45
+ retryable: false,
46
+ remediation: "Correct the arguments using the usage value in error.fields.",
47
+ },
48
+ WorkbaseNotFoundError: {
49
+ code: "WORKBASE_NOT_FOUND",
50
+ retryable: false,
51
+ remediation:
52
+ "Run the command from an Agency workbase or provide an explicit workbase path.",
53
+ },
54
+ WorkbaseConfigError: {
55
+ code: "WORKBASE_CONFIG_INVALID",
56
+ retryable: false,
57
+ remediation: "Correct the workbase configuration and retry the command.",
58
+ },
59
+ WorkbaseRegistryError: {
60
+ code: "WORKBASE_REGISTRY_ERROR",
61
+ retryable: false,
62
+ remediation: "Correct the registered workbase entry and retry the command.",
63
+ },
64
+ FileNotFoundError: {
65
+ code: "FILE_NOT_FOUND",
66
+ retryable: false,
67
+ remediation: "Restore the required file or correct the supplied path.",
68
+ },
69
+ FileSystemError: { code: "FILESYSTEM_ERROR", retryable: false },
70
+ FrontmatterParseError: {
71
+ code: "FRONTMATTER_INVALID",
72
+ retryable: false,
73
+ remediation: "Correct the document frontmatter and retry the command.",
74
+ },
75
+ ValidationFailedError: {
76
+ code: "VALIDATION_FAILED",
77
+ retryable: false,
78
+ remediation: "Resolve the validation issues in error.fields and retry.",
79
+ },
80
+ RepositoryError: { code: "REPOSITORY_ERROR", retryable: false },
81
+ EpicError: { code: "EPIC_ERROR", retryable: false },
82
+ TaskError: { code: "TASK_ERROR", retryable: false },
83
+ PhaseError: { code: "PHASE_ERROR", retryable: false },
84
+ ArchiveError: { code: "ARCHIVE_ERROR", retryable: false },
85
+ WorktreeError: { code: "WORKTREE_ERROR", retryable: false },
86
+ PullRequestError: { code: "PULL_REQUEST_ERROR", retryable: false },
87
+ ContextError: {
88
+ code: "CONTEXT_ERROR",
89
+ retryable: false,
90
+ remediation:
91
+ "Run the command from an Agency entity or provide a valid target.",
92
+ },
93
+ ProcessError: { code: "PROCESS_ERROR", retryable: true },
94
+ ProtocolOutputError: {
95
+ code: "PROTOCOL_OUTPUT_ERROR",
96
+ retryable: false,
97
+ remediation: "Report this Agency protocol violation.",
98
+ },
99
+ }
100
+
101
+ class ProtocolOutputError extends Error {
102
+ readonly _tag = "ProtocolOutputError"
103
+ }
104
+
105
+ let resultCollector: ((value: unknown) => void) | undefined
106
+
107
+ const parseCommandResult = (value: unknown): unknown => {
108
+ if (typeof value !== "string") return value
109
+ try {
110
+ return JSON.parse(value)
111
+ } catch {
112
+ return value
113
+ }
114
+ }
115
+
116
+ export const emitCommandResult = (value: unknown): void => {
117
+ if (resultCollector) {
118
+ resultCollector(parseCommandResult(value))
119
+ return
120
+ }
121
+ console.log(value)
122
+ }
123
+
124
+ export const collectCommandResult = async (
125
+ run: () => Promise<void>,
126
+ ): Promise<unknown> => {
127
+ if (resultCollector) {
128
+ throw new ProtocolOutputError(
129
+ "A machine result collector is already active.",
130
+ )
131
+ }
132
+
133
+ let emitted = false
134
+ let result: unknown = null
135
+ const originalLog = console.log
136
+ resultCollector = (value) => {
137
+ if (emitted) {
138
+ throw new ProtocolOutputError(
139
+ "A machine command emitted more than one result.",
140
+ )
141
+ }
142
+ emitted = true
143
+ result = value
144
+ }
145
+ console.log = (...values) => {
146
+ resultCollector?.(
147
+ parseCommandResult(values.length === 1 ? values[0] : values.join(" ")),
148
+ )
149
+ }
150
+
151
+ try {
152
+ await run()
153
+ return result
154
+ } finally {
155
+ console.log = originalLog
156
+ resultCollector = undefined
157
+ }
158
+ }
159
+
160
+ const errorTag = (error: unknown): string | undefined => {
161
+ if (typeof error !== "object" || error === null) return undefined
162
+ if ("_tag" in error && typeof error._tag === "string") return error._tag
163
+ if (error instanceof Error && error.name !== "Error") return error.name
164
+ return undefined
165
+ }
166
+
167
+ const errorMessage = (error: unknown): string => {
168
+ if (
169
+ typeof error === "object" &&
170
+ error !== null &&
171
+ "message" in error &&
172
+ typeof error.message === "string"
173
+ ) {
174
+ return error.message
175
+ }
176
+ return String(error)
177
+ }
178
+
179
+ const errorFields = (error: unknown): Record<string, unknown> => {
180
+ if (typeof error !== "object" || error === null) return {}
181
+ return Object.fromEntries(
182
+ Object.entries(error).filter(
183
+ ([key, value]) =>
184
+ !key.startsWith("_") &&
185
+ key !== "name" &&
186
+ key !== "message" &&
187
+ key !== "cause" &&
188
+ value !== undefined,
189
+ ),
190
+ )
191
+ }
192
+
193
+ export const successEnvelope = (result: unknown): SuccessEnvelope => ({
194
+ version: PROTOCOL_VERSION,
195
+ ok: true,
196
+ result: result === undefined ? null : result,
197
+ })
198
+
199
+ export const errorEnvelope = (error: unknown): ErrorEnvelope => {
200
+ const metadata = errorMetadata[errorTag(error) ?? ""] ?? {
201
+ code: "COMMAND_FAILED",
202
+ retryable: false,
203
+ }
204
+ return {
205
+ version: PROTOCOL_VERSION,
206
+ ok: false,
207
+ error: {
208
+ ...metadata,
209
+ message: errorMessage(error),
210
+ fields: errorFields(error),
211
+ },
212
+ }
213
+ }
214
+
215
+ export const writeEnvelope = (envelope: AgencyEnvelope): void => {
216
+ process.stdout.write(`${JSON.stringify(envelope)}\n`)
217
+ }