@markjaquith/agency 3.2.0 → 3.2.2

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 CHANGED
@@ -1107,13 +1107,21 @@ without creating a pull request. It requires a valid, registered writable
1107
1107
  checkout in `working` state, fetches the configured delivery remote, verifies
1108
1108
  that the declared base is in the publication history, validates every outgoing
1109
1109
  commit's description, author, and conflict state, and refuses non-fast-forward
1110
- updates.
1110
+ updates. Local commit validation runs before network access when cached remote
1111
+ state is available, and the network fetch is limited to the declared base and
1112
+ delivery branch.
1111
1113
 
1112
1114
  YAML `branch` must exactly match the checked-out local branch, the worktree must
1113
1115
  be clean, and `HEAD` is pushed with upstream tracking. Missing commit descriptions
1114
1116
  or authors stop publication with exact commits and remediation commands. Push
1115
1117
  reports deterministic fetch, inspection, validation, and publication progress on
1116
- stderr, including while `--json` reserves stdout for one machine result.
1118
+ stderr, including while `--json` reserves stdout for one machine result. Git
1119
+ authentication is non-interactive. Fetch defaults to a 30-second deadline and
1120
+ one retry for transient failures; push defaults to a 120-second deadline. Set
1121
+ `AGENCY_PUSH_FETCH_TIMEOUT_MS` or `AGENCY_PUSH_TIMEOUT_MS` to positive millisecond
1122
+ values to override them. After a failed or timed-out push, Agency compares the
1123
+ exact remote delivery ref with the expected tip before reporting success, a safe
1124
+ retryable failure, or an unknown publication outcome.
1117
1125
 
1118
1126
  Task-aware `agency pr create <task-id> [phase-id]` uses Agency's delivery flow,
1119
1127
  including readiness checks and durable PR recording. It accepts draft, title,
package/cli-main.ts CHANGED
@@ -828,6 +828,42 @@ let usageFlagNames = rawArguments
828
828
  .filter((argument) => argument.startsWith("--"))
829
829
  .map((argument) => argument.slice(2).split("=", 1)[0]!)
830
830
 
831
+ const pushUsageDetails = (error?: unknown) => {
832
+ if (usageCommandPath !== "push") return {}
833
+ const seen = new Set<object>()
834
+ const visit = (
835
+ value: unknown,
836
+ ): { stage?: string; category?: string } | null => {
837
+ if (typeof value !== "object" || value === null || seen.has(value))
838
+ return null
839
+ seen.add(value)
840
+ if (
841
+ "stage" in value &&
842
+ typeof value.stage === "string" &&
843
+ "category" in value &&
844
+ typeof value.category === "string"
845
+ ) {
846
+ return { stage: value.stage, category: value.category }
847
+ }
848
+ for (const nestedValue of [
849
+ ...Object.values(value),
850
+ ...Object.getOwnPropertySymbols(value).map(
851
+ (symbol) => (value as Record<symbol, unknown>)[symbol],
852
+ ),
853
+ ]) {
854
+ const nested = visit(nestedValue)
855
+ if (nested) return nested
856
+ }
857
+ return null
858
+ }
859
+ const details = error ? visit(error) : null
860
+ return {
861
+ vcs: "git" as const,
862
+ terminalStage: details?.stage ?? (error ? "unknown" : "publish"),
863
+ category: details?.category ?? (error ? "unknown" : "success"),
864
+ }
865
+ }
866
+
831
867
  try {
832
868
  const {
833
869
  commandName,
@@ -921,6 +957,7 @@ try {
921
957
  durationMs: performance.now() - invocationStartedAt,
922
958
  outcome: exitStatus === 0 ? "success" : "failure",
923
959
  exitStatus,
960
+ ...pushUsageDetails(),
924
961
  },
925
962
  VERSION,
926
963
  )
@@ -932,6 +969,7 @@ try {
932
969
  durationMs: performance.now() - invocationStartedAt,
933
970
  outcome: "failure",
934
971
  exitStatus: 1,
972
+ ...pushUsageDetails(error),
935
973
  },
936
974
  VERSION,
937
975
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -282,6 +282,20 @@ status: dropped
282
282
  })
283
283
 
284
284
  test("reads each workbase document at most once per invocation", async () => {
285
+ const unrelatedArchive = join(root, "archive/tasks/unrelated/TASK.md")
286
+ await write(
287
+ root,
288
+ "archive/tasks/unrelated/TASK.md",
289
+ `---
290
+ ticketUrl: null
291
+ repo: agency
292
+ branch: unrelated
293
+ base: main
294
+ pr: null
295
+ status: done
296
+ ---
297
+ `,
298
+ )
285
299
  const reads = new Map<string, number>()
286
300
  const originalFile = Bun.file
287
301
  Bun.file = mock((path: string) => {
@@ -304,6 +318,48 @@ status: dropped
304
318
  )
305
319
  expect(documents.length).toBeGreaterThan(0)
306
320
  expect(documents.every(([, count]) => count === 1)).toBe(true)
321
+ expect(reads.has(unrelatedArchive)).toBe(false)
322
+ })
323
+
324
+ test("limits repository inspection and overlaps independent Git observations", async () => {
325
+ const commands: string[][] = []
326
+ let activeGitCommands = 0
327
+ let maxActiveGitCommands = 0
328
+ const originalSpawn = Bun.spawn
329
+ ;(Bun as { spawn: unknown }).spawn = mock(((
330
+ args: string[],
331
+ options: Parameters<typeof Bun.spawn>[1],
332
+ ) => {
333
+ const command = [...args]
334
+ const child = originalSpawn(command, options)
335
+ if (command[0] !== "git") return child
336
+ commands.push(command)
337
+ activeGitCommands += 1
338
+ maxActiveGitCommands = Math.max(maxActiveGitCommands, activeGitCommands)
339
+ const exited = child.exited.finally(() => {
340
+ activeGitCommands -= 1
341
+ })
342
+ return new Proxy(child, {
343
+ get(target, property) {
344
+ return property === "exited"
345
+ ? exited
346
+ : Reflect.get(target, property, target)
347
+ },
348
+ })
349
+ }) as unknown as typeof Bun.spawn)
350
+ try {
351
+ await readContext(root, "tasks/agent-contract/phases/context-command")
352
+ } finally {
353
+ ;(Bun as { spawn: unknown }).spawn = originalSpawn
354
+ }
355
+
356
+ expect(
357
+ commands.some(
358
+ (command) =>
359
+ command.includes("config") && command.includes("--show-origin"),
360
+ ),
361
+ ).toBe(false)
362
+ expect(maxActiveGitCommands).toBeGreaterThanOrEqual(3)
307
363
  })
308
364
 
309
365
  test("resolves a bare task ID and returns root discovery context", async () => {
@@ -28,9 +28,10 @@ describe("push command", () => {
28
28
  publish: (_cwd: string, options: any) => {
29
29
  for (const stage of [
30
30
  "context",
31
- "fetch",
32
31
  "inspect",
33
32
  "validate",
33
+ "fetch",
34
+ "validate",
34
35
  "publish",
35
36
  ] as const)
36
37
  options.onProgress(stage)
@@ -44,9 +45,10 @@ describe("push command", () => {
44
45
  expect(logs).toEqual([JSON.stringify(result, null, 2)])
45
46
  expect(updates).toEqual([
46
47
  "start:Inspecting Agency execution context",
47
- "start:Fetching remote state",
48
48
  "start:Selecting the publication tip",
49
49
  "start:Validating outgoing changes",
50
+ "start:Fetching remote state",
51
+ "start:Validating outgoing changes",
50
52
  "start:Publishing the declared branch",
51
53
  "succeed:Published task/example to origin",
52
54
  ])
@@ -10,6 +10,7 @@ const stageMessage = {
10
10
  inspect: "Selecting the publication tip",
11
11
  validate: "Validating outgoing changes",
12
12
  publish: "Publishing the declared branch",
13
+ reconcile: "Confirming the remote publication outcome",
13
14
  } as const
14
15
 
15
16
  export const push = (
@@ -22,6 +23,7 @@ export const push = (
22
23
  const showProgress = !options.silent
23
24
  const result = yield* publications
24
25
  .publish(options.cwd ?? process.cwd(), {
26
+ forwardOutput: options.verbose,
25
27
  onProgress: showProgress
26
28
  ? (stage) => progress.start(stageMessage[stage])
27
29
  : undefined,
@@ -161,4 +161,28 @@ describe("machine protocol", () => {
161
161
  },
162
162
  })
163
163
  })
164
+
165
+ test("uses dynamic protocol metadata without duplicating it in fields", () => {
166
+ expect(
167
+ errorEnvelope({
168
+ _tag: "PushError",
169
+ message: "remote timed out",
170
+ protocolCode: "PUSH_TIMEOUT",
171
+ retryable: true,
172
+ remediation: "Retry after checking connectivity.",
173
+ category: "timeout",
174
+ stage: "fetch",
175
+ }),
176
+ ).toEqual({
177
+ version: 1,
178
+ ok: false,
179
+ error: {
180
+ code: "PUSH_TIMEOUT",
181
+ message: "remote timed out",
182
+ fields: { category: "timeout", stage: "fetch" },
183
+ retryable: true,
184
+ remediation: "Retry after checking connectivity.",
185
+ },
186
+ })
187
+ })
164
188
  })
package/src/protocol.ts CHANGED
@@ -198,6 +198,36 @@ const errorTag = (error: unknown): string | undefined => {
198
198
  return undefined
199
199
  }
200
200
 
201
+ const unwrapEffectError = (error: unknown): unknown => {
202
+ const seen = new Set<object>()
203
+ const visit = (value: unknown): unknown => {
204
+ if (typeof value !== "object" || value === null || seen.has(value))
205
+ return null
206
+ seen.add(value)
207
+ if ("protocolCode" in value && typeof value.protocolCode === "string") {
208
+ return value
209
+ }
210
+ for (const nested of [
211
+ ...Object.values(value),
212
+ ...Object.getOwnPropertySymbols(value).map(
213
+ (symbol) => (value as Record<symbol, unknown>)[symbol],
214
+ ),
215
+ ]) {
216
+ const found = visit(nested)
217
+ if (found) return found
218
+ }
219
+ if (
220
+ "_tag" in value &&
221
+ typeof value._tag === "string" &&
222
+ value._tag.endsWith("Error")
223
+ ) {
224
+ return value
225
+ }
226
+ return null
227
+ }
228
+ return visit(error) ?? error
229
+ }
230
+
201
231
  const errorMessage = (error: unknown): string => {
202
232
  if (
203
233
  typeof error === "object" &&
@@ -219,6 +249,9 @@ const errorFields = (error: unknown): Record<string, unknown> => {
219
249
  key !== "name" &&
220
250
  key !== "message" &&
221
251
  key !== "cause" &&
252
+ key !== "protocolCode" &&
253
+ key !== "retryable" &&
254
+ key !== "remediation" &&
222
255
  value !== undefined,
223
256
  ),
224
257
  )
@@ -231,17 +264,36 @@ export const successEnvelope = (result: unknown): SuccessEnvelope => ({
231
264
  })
232
265
 
233
266
  export const errorEnvelope = (error: unknown): ErrorEnvelope => {
234
- const metadata = errorMetadata[errorTag(error) ?? ""] ?? {
267
+ const normalized = unwrapEffectError(error)
268
+ const defaults = errorMetadata[errorTag(normalized) ?? ""] ?? {
235
269
  code: "COMMAND_FAILED",
236
270
  retryable: false,
237
271
  }
272
+ const dynamic =
273
+ typeof normalized === "object" && normalized !== null
274
+ ? {
275
+ ...("protocolCode" in normalized &&
276
+ typeof normalized.protocolCode === "string"
277
+ ? { code: normalized.protocolCode }
278
+ : {}),
279
+ ...("retryable" in normalized &&
280
+ typeof normalized.retryable === "boolean"
281
+ ? { retryable: normalized.retryable }
282
+ : {}),
283
+ ...("remediation" in normalized &&
284
+ typeof normalized.remediation === "string"
285
+ ? { remediation: normalized.remediation }
286
+ : {}),
287
+ }
288
+ : {}
238
289
  return {
239
290
  version: PROTOCOL_VERSION,
240
291
  ok: false,
241
292
  error: {
242
- ...metadata,
243
- message: errorMessage(error),
244
- fields: errorFields(error),
293
+ ...defaults,
294
+ ...dynamic,
295
+ message: errorMessage(normalized),
296
+ fields: errorFields(normalized),
245
297
  },
246
298
  }
247
299
  }