@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.
- package/README.md +79 -0
- package/cli.ts +50 -4
- package/fixtures/protocol/error.json +14 -0
- package/fixtures/protocol/success.json +7 -0
- package/index.ts +1 -0
- package/package.json +16 -1
- package/schemas/agency-envelope-v1.schema.json +38 -0
- package/src/cli-parser.test.ts +10 -0
- package/src/cli-parser.ts +26 -1
- package/src/cli.test.ts +68 -1
- package/src/commands/context.test.ts +379 -0
- package/src/commands/context.ts +29 -0
- package/src/commands/read-only.test.ts +8 -0
- package/src/commands/validate.ts +7 -0
- package/src/commands/work.test.ts +3 -3
- package/src/protocol.test.ts +87 -0
- package/src/protocol.ts +217 -0
- package/src/services/ContextService.ts +898 -0
- package/src/services/WorktreeService.test.ts +2 -2
- package/src/test-utils.ts +12 -3
- package/src/utils/effect.test.ts +9 -3
- package/src/utils/effect.ts +4 -2
package/src/protocol.ts
ADDED
|
@@ -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
|
+
}
|