@markjaquith/agency 3.2.2 → 3.2.4

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 (48) hide show
  1. package/README.md +30 -62
  2. package/cli-main.ts +20 -83
  3. package/fixtures/protocol/orchestration-recipes.json +9 -34
  4. package/package.json +1 -4
  5. package/schemas/agency-graph-v1.schema.json +2 -31
  6. package/src/cli-parser.test.ts +34 -132
  7. package/src/cli-parser.ts +45 -102
  8. package/src/cli.test.ts +60 -117
  9. package/src/commands/act.ts +2 -6
  10. package/src/commands/sync.ts +3 -3
  11. package/src/commands/validate.ts +3 -1
  12. package/src/commands/work.test.ts +70 -8
  13. package/src/commands/work.ts +15 -8
  14. package/src/graph-schema.ts +0 -2
  15. package/src/protocol.test.ts +0 -25
  16. package/src/protocol.ts +0 -11
  17. package/src/readiness.test.ts +2 -2
  18. package/src/services/ArchiveBulkService.test.ts +0 -88
  19. package/src/services/ArchiveService.ts +0 -32
  20. package/src/services/GraphMutationService.ts +0 -26
  21. package/src/services/GraphService.test.ts +1 -1
  22. package/src/services/IntegrationService.test.ts +7 -4
  23. package/src/services/LifecycleTransaction.ts +5 -1
  24. package/src/services/PhaseService.ts +1 -8
  25. package/src/services/ReadinessService.test.ts +0 -34
  26. package/src/services/ReadinessService.ts +1 -20
  27. package/src/services/ReviewService.test.ts +0 -64
  28. package/src/services/ReviewService.ts +0 -5
  29. package/src/services/SyncService.test.ts +75 -88
  30. package/src/services/SyncService.ts +52 -123
  31. package/src/services/TaskPhaseService.test.ts +13 -1
  32. package/src/services/TaskService.ts +1 -7
  33. package/src/services/WorkbaseService.ts +0 -3
  34. package/src/services/WorktreeService.test.ts +192 -1
  35. package/src/services/WorktreeService.ts +169 -34
  36. package/src/test-utils.ts +0 -2
  37. package/src/usage-log.test.ts +89 -5
  38. package/src/usage-log.ts +139 -31
  39. package/src/workbase/AGENTS.md +9 -15
  40. package/src/workbase/agent-command.test.ts +1 -3
  41. package/src/workbase/agent-command.ts +1 -6
  42. package/src/workbase/document-revision.ts +0 -4
  43. package/src/workbase/opencode-plugin-file.ts +1 -0
  44. package/src/workbase/schemas.test.ts +12 -25
  45. package/src/workbase/schemas.ts +0 -16
  46. package/src/commands/claim.ts +0 -122
  47. package/src/services/ClaimService.test.ts +0 -415
  48. package/src/services/ClaimService.ts +0 -608
package/src/usage-log.ts CHANGED
@@ -1,21 +1,92 @@
1
1
  import { Database } from "bun:sqlite"
2
+ import { createHash } from "node:crypto"
2
3
  import { mkdir } from "node:fs/promises"
3
4
  import { dirname, join } from "node:path"
4
5
 
5
6
  const USAGE_EVENT_VERSION = 2 as const
6
7
  const DEFAULT_RETENTION_DAYS = 90
7
8
 
9
+ const INVOCATION_SOURCES = new Set(["human", "agent", "automation"])
10
+ const USAGE_EVENT_COLUMNS = new Set([
11
+ "id",
12
+ "event_version",
13
+ "journey_id",
14
+ "journey_sequence",
15
+ "invocation_source",
16
+ "is_test",
17
+ "occurred_at",
18
+ "agency_version",
19
+ "command_path",
20
+ "flag_names",
21
+ "duration_ms",
22
+ "outcome",
23
+ "outcome_code",
24
+ "exit_status",
25
+ "vcs",
26
+ "terminal_stage",
27
+ "category",
28
+ ])
29
+
30
+ export type UsageOutcomeCode =
31
+ | "SUCCESS"
32
+ | "NONZERO_EXIT"
33
+ | "CLI_USAGE"
34
+ | "WORKBASE_NOT_FOUND"
35
+ | "WORKBASE_INVALID"
36
+ | "VALIDATION_FAILED"
37
+ | "CONFLICT"
38
+ | "FILESYSTEM_ERROR"
39
+ | "PROCESS_ERROR"
40
+ | "COMMAND_FAILED"
41
+
8
42
  export interface UsageEvent {
9
43
  readonly commandPath: string
10
44
  readonly flagNames: readonly string[]
11
45
  readonly durationMs: number
12
46
  readonly exitStatus: number
13
47
  readonly outcome: "success" | "failure"
48
+ readonly outcomeCode: UsageOutcomeCode
14
49
  readonly vcs?: "git"
15
50
  readonly terminalStage?: string
16
51
  readonly category?: string
17
52
  }
18
53
 
54
+ export const usageOutcomeCode = (error: unknown): UsageOutcomeCode => {
55
+ const tag =
56
+ typeof error === "object" &&
57
+ error !== null &&
58
+ "_tag" in error &&
59
+ typeof error._tag === "string"
60
+ ? error._tag
61
+ : error instanceof Error
62
+ ? error.name
63
+ : undefined
64
+ switch (tag) {
65
+ case "CliUsageError":
66
+ return "CLI_USAGE"
67
+ case "WorkbaseNotFoundError":
68
+ return "WORKBASE_NOT_FOUND"
69
+ case "WorkbaseConfigError":
70
+ case "WorkbaseRegistryError":
71
+ case "FrontmatterParseError":
72
+ return "WORKBASE_INVALID"
73
+ case "ValidationFailedError":
74
+ return "VALIDATION_FAILED"
75
+ case "ClaimConflictError":
76
+ case "ClaimOwnershipError":
77
+ case "RevisionConflictError":
78
+ case "ExecutionGuardError":
79
+ return "CONFLICT"
80
+ case "FileNotFoundError":
81
+ case "FileSystemError":
82
+ return "FILESYSTEM_ERROR"
83
+ case "ProcessError":
84
+ return "PROCESS_ERROR"
85
+ default:
86
+ return "COMMAND_FAILED"
87
+ }
88
+ }
89
+
19
90
  const stateDirectory = (env: NodeJS.ProcessEnv) =>
20
91
  env.XDG_STATE_HOME ?? join(env.HOME ?? ".", ".local", "state")
21
92
 
@@ -32,36 +103,68 @@ const retentionDays = (env: NodeJS.ProcessEnv) => {
32
103
  : DEFAULT_RETENTION_DAYS
33
104
  }
34
105
 
106
+ const invocationSource = (env: NodeJS.ProcessEnv) => {
107
+ const source = env.AGENCY_INVOCATION_SOURCE?.toLowerCase()
108
+ if (source && INVOCATION_SOURCES.has(source)) return source
109
+ return env.AGENCY_SESSION_ID ? "agent" : "human"
110
+ }
111
+
112
+ const isTestInvocation = (env: NodeJS.ProcessEnv) =>
113
+ ["1", "true", "yes"].includes((env.AGENCY_USAGE_TEST ?? "").toLowerCase())
114
+
115
+ const journeyId = (env: NodeJS.ProcessEnv) => {
116
+ if (!env.AGENCY_SESSION_ID) return null
117
+ return `sha256:${createHash("sha256").update(env.AGENCY_SESSION_ID).digest("hex")}`
118
+ }
119
+
120
+ const pruneExpiredEvents = (database: Database, env: NodeJS.ProcessEnv) => {
121
+ database
122
+ .query(
123
+ "DELETE FROM usage_events WHERE datetime(occurred_at) < datetime('now', ?)",
124
+ )
125
+ .run(`-${retentionDays(env)} days`)
126
+ }
127
+
35
128
  const openDatabase = async (env: NodeJS.ProcessEnv) => {
36
129
  const path = usageDatabasePath(env)
37
130
  await mkdir(dirname(path), { recursive: true, mode: 0o700 })
38
131
  const database = new Database(path, { create: true, strict: true })
39
132
  database.run("PRAGMA journal_mode = WAL")
40
133
  database.run("PRAGMA busy_timeout = 1000")
134
+ const existingColumns = database
135
+ .query("PRAGMA table_info(usage_events)")
136
+ .all() as { name: string }[]
137
+ if (
138
+ existingColumns.length > 0 &&
139
+ (existingColumns.length !== USAGE_EVENT_COLUMNS.size ||
140
+ existingColumns.some(({ name }) => !USAGE_EVENT_COLUMNS.has(name)))
141
+ ) {
142
+ // Version 1 could contain positional values. Do not preserve unsafe telemetry.
143
+ database.run("DROP TABLE usage_events")
144
+ }
41
145
  database.run(`
42
146
  CREATE TABLE IF NOT EXISTS usage_events (
43
147
  id INTEGER PRIMARY KEY AUTOINCREMENT,
44
148
  event_version INTEGER NOT NULL,
45
- session_id TEXT NOT NULL,
46
- session_sequence INTEGER NOT NULL,
149
+ journey_id TEXT,
150
+ journey_sequence INTEGER,
151
+ invocation_source TEXT NOT NULL,
152
+ is_test INTEGER NOT NULL,
47
153
  occurred_at TEXT NOT NULL,
48
154
  agency_version TEXT NOT NULL,
49
155
  command_path TEXT NOT NULL,
50
156
  flag_names TEXT NOT NULL,
51
157
  duration_ms INTEGER NOT NULL,
52
158
  outcome TEXT NOT NULL,
53
- exit_status INTEGER NOT NULL
159
+ outcome_code TEXT NOT NULL,
160
+ exit_status INTEGER NOT NULL,
161
+ vcs TEXT,
162
+ terminal_stage TEXT,
163
+ category TEXT
54
164
  )
55
165
  `)
56
- for (const column of ["vcs", "terminal_stage", "category"]) {
57
- try {
58
- database.run(`ALTER TABLE usage_events ADD COLUMN ${column} TEXT`)
59
- } catch {
60
- // Existing databases already have migrated columns.
61
- }
62
- }
63
166
  database.run(
64
- "CREATE INDEX IF NOT EXISTS usage_events_session ON usage_events(session_id, session_sequence)",
167
+ "CREATE INDEX IF NOT EXISTS usage_events_journey ON usage_events(journey_id, journey_sequence)",
65
168
  )
66
169
  database.run(
67
170
  "CREATE INDEX IF NOT EXISTS usage_events_command ON usage_events(command_path, occurred_at)",
@@ -78,42 +181,42 @@ export async function recordUsageEvent(
78
181
  let database: Database | undefined
79
182
  try {
80
183
  database = await openDatabase(env)
81
- const sessionId = env.AGENCY_SESSION_ID || `process-${process.pid}`
184
+ pruneExpiredEvents(database, env)
185
+ const eventJourneyId = journeyId(env)
82
186
  database
83
187
  .query(`
84
188
  INSERT INTO usage_events (
85
- event_version, session_id, session_sequence, occurred_at,
189
+ event_version, journey_id, journey_sequence,
190
+ invocation_source, is_test, occurred_at,
86
191
  agency_version, command_path, flag_names, duration_ms,
87
- outcome, exit_status, vcs, terminal_stage, category
192
+ outcome, outcome_code, exit_status, vcs, terminal_stage, category
88
193
  ) VALUES (
89
194
  ?, ?,
90
- (SELECT COALESCE(MAX(session_sequence), 0) + 1 FROM usage_events WHERE session_id = ?),
91
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
195
+ CASE WHEN ? IS NULL THEN NULL ELSE
196
+ (SELECT COALESCE(MAX(journey_sequence), 0) + 1 FROM usage_events WHERE journey_id = ?)
197
+ END,
198
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
92
199
  )
93
200
  `)
94
201
  .run(
95
202
  USAGE_EVENT_VERSION,
96
- sessionId,
97
- sessionId,
203
+ eventJourneyId,
204
+ eventJourneyId,
205
+ eventJourneyId,
206
+ invocationSource(env),
207
+ isTestInvocation(env) ? 1 : 0,
98
208
  new Date().toISOString(),
99
209
  agencyVersion,
100
210
  event.commandPath,
101
211
  JSON.stringify([...new Set(event.flagNames)].sort()),
102
212
  Math.max(0, Math.round(event.durationMs)),
103
213
  event.outcome,
214
+ event.outcomeCode,
104
215
  event.exitStatus,
105
216
  event.vcs ?? null,
106
217
  event.terminalStage ?? null,
107
218
  event.category ?? null,
108
219
  )
109
- if (Math.random() < 0.01) {
110
- const days = retentionDays(env)
111
- database
112
- .query(
113
- "DELETE FROM usage_events WHERE occurred_at < datetime('now', ?)",
114
- )
115
- .run(`-${days} days`)
116
- }
117
220
  } catch {
118
221
  // Usage logging must never affect command behavior.
119
222
  } finally {
@@ -128,24 +231,29 @@ export async function exportUsageEvents(
128
231
  let database: Database | undefined
129
232
  try {
130
233
  database = await openDatabase(env)
234
+ pruneExpiredEvents(database, env)
131
235
  const rows = database
132
236
  .query(`
133
- SELECT event_version, session_id, session_sequence, occurred_at,
237
+ SELECT event_version, journey_id, journey_sequence,
238
+ invocation_source, is_test, occurred_at,
134
239
  agency_version, command_path, flag_names, duration_ms,
135
- outcome, exit_status, vcs, terminal_stage, category
240
+ outcome, outcome_code, exit_status, vcs, terminal_stage, category
136
241
  FROM usage_events ORDER BY occurred_at, id
137
242
  `)
138
- .all() as Record<string, string | number>[]
243
+ .all() as Record<string, string | number | null>[]
139
244
  return rows.map((row) => ({
140
245
  version: row.event_version,
141
- sessionId: row.session_id,
142
- sessionSequence: row.session_sequence,
246
+ journeyId: row.journey_id,
247
+ journeySequence: row.journey_sequence,
248
+ invocationSource: row.invocation_source,
249
+ isTest: row.is_test === 1,
143
250
  occurredAt: row.occurred_at,
144
251
  agencyVersion: row.agency_version,
145
252
  commandPath: row.command_path,
146
253
  flagNames: JSON.parse(String(row.flag_names)),
147
254
  durationMs: row.duration_ms,
148
255
  outcome: row.outcome,
256
+ outcomeCode: row.outcome_code,
149
257
  exitStatus: row.exit_status,
150
258
  ...(row.vcs == null ? {} : { vcs: row.vcs }),
151
259
  ...(row.terminal_stage == null
@@ -46,9 +46,7 @@ retain `--if-revision` guards when shown, and do not add flags that are not show
46
46
  `agency push --json`. Create and record a pull request with
47
47
  `agency pr create <task> [phase] [--draft] [--title <title>] [--label <label>] --json`;
48
48
  do not run a separate push first because `pr create` owns publication.
49
- 12. Complete genuine non-PR work. For an active claim, run
50
- `agency finish <task> [phase] --session-id <id> --revision <revision> --outcome done --no-pull-request --summary <text> [--evidence-url <url>]`.
51
- Without a claim, run
49
+ 12. Complete genuine non-PR work. Run
52
50
  `agency task status <task> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`
53
51
  or
54
52
  `agency phase status <task> <phase> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`.
@@ -162,14 +160,14 @@ revision stale, and Agency must not silently rewrite that evidence.
162
160
 
163
161
  ## Safety
164
162
 
165
- - Stop on validation errors, dependency blockers, an unexpected writable
166
- repository, or a conflicting active claim.
163
+ - Stop on validation errors, dependency blockers, or an unexpected writable
164
+ repository.
167
165
  - Do not manually create, move, or remove worktrees under `code/`.
168
166
  - Use `agency archive`, rather than moving work item folders manually.
169
167
  - Do not edit bare repositories or repository symlinks under `repos/`.
170
168
  - Never invent entity IDs, revisions, PR state, dependency completion, or
171
169
  checkout state. Preserve parent backlinks and dependency declarations.
172
- - Do not bypass dirty-worktree, active-claim, revision, or readiness protections.
170
+ - Do not bypass dirty-worktree, revision, or readiness protections.
173
171
  - Do not run `agency work` from an active agent session unless the user
174
172
  explicitly asks to launch another agent.
175
173
  - Run `agency validate` before worktree or pull-request operations.
@@ -189,10 +187,8 @@ resolve its reported commits and remediation commands before retrying.
189
187
 
190
188
  `agency work` is the human launch flow: it reconciles managed integration,
191
189
  selects work, checks readiness, prepares checkouts, marks execution work
192
- `working` without creating a claim, and starts the agent. Epic and multi-phase
193
- task launches remain orchestration-only. External orchestrators instead claim
194
- an execution unit, launch and monitor their agent separately, and finish or
195
- release the claim with the current document revision.
190
+ `working`, and starts the agent. Epic and multi-phase task launches remain
191
+ orchestration-only.
196
192
 
197
193
  An Agency-launched agent receives process-local worker identity through both
198
194
  the `AGENCY_SESSION_ID` and `AGENCY_TARGET` environment variables and a generated
@@ -228,13 +224,11 @@ intent, `--no-pull-request`, and a durable outcome summary.
228
224
  At each closeout trigger (creating or updating a PR, marking it ready, completing
229
225
  a refinement loop, or pausing or handing off completed implementation work):
230
226
 
231
- - Finish an active claim with the current revision via `agency finish`; a
232
- successful claim outcome leaves unmerged work `working`. For unclaimed work,
233
- keep the execution unit `working` through review and merge.
227
+ - Keep the execution unit `working` through review and merge.
234
228
  - After merge, run `agency sync` to reconcile the execution unit to
235
229
  `done`.
236
- - For an approved non-PR outcome, finish an active claim or update unclaimed
237
- status with `--no-pull-request --summary <text>` and optional supporting URL.
230
+ - For an approved non-PR outcome, update status with
231
+ `--no-pull-request --summary <text>` and an optional supporting URL.
238
232
  - Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
239
233
  PR state, current head, diff summary, and verification results after later
240
234
  pushes when those details are maintained there.
@@ -12,9 +12,7 @@ const variables = {
12
12
  target: "execution-unit:phase/task/build",
13
13
  task: "task",
14
14
  phase: "build",
15
- claimant: "orchestrator",
16
15
  sessionId: "session-1",
17
- claimRevision: "revision-1",
18
16
  }
19
17
 
20
18
  describe("agent commands", () => {
@@ -116,7 +114,7 @@ describe("agent commands", () => {
116
114
 
117
115
  expect(environment).toMatchObject({
118
116
  AGENCY_AGENT: "custom",
119
- AGENCY_CLAIMANT: "orchestrator",
117
+ AGENCY_INVOCATION_SOURCE: "agent",
120
118
  AGENCY_SESSION_ID: "session-1",
121
119
  AGENCY_WORKBASE: "/workbase",
122
120
  AGENCY_TARGET: "execution-unit:phase/task/build",
@@ -6,9 +6,7 @@ export interface AgentCommandVariables {
6
6
  readonly target: string
7
7
  readonly task: string
8
8
  readonly phase: string
9
- readonly claimant: string
10
9
  readonly sessionId: string
11
- readonly claimRevision: string
12
10
  }
13
11
 
14
12
  interface AgentDefinition {
@@ -25,9 +23,7 @@ const PLACEHOLDERS = new Set<keyof AgentCommandVariables>([
25
23
  "target",
26
24
  "task",
27
25
  "phase",
28
- "claimant",
29
26
  "sessionId",
30
- "claimRevision",
31
27
  ])
32
28
 
33
29
  const BUILTIN_AGENTS: Readonly<Record<string, AgentDefinition>> = {
@@ -122,9 +118,8 @@ export const agentEnvironment = (
122
118
  variables: AgentCommandVariables,
123
119
  ): Record<string, string> => ({
124
120
  AGENCY_AGENT: agent,
125
- AGENCY_CLAIMANT: variables.claimant,
121
+ AGENCY_INVOCATION_SOURCE: "agent",
126
122
  AGENCY_SESSION_ID: variables.sessionId,
127
- AGENCY_CLAIM_REVISION: variables.claimRevision,
128
123
  AGENCY_WORKBASE: variables.workbase,
129
124
  AGENCY_TARGET: variables.target,
130
125
  AGENCY_TASK_ID: variables.task,
@@ -3,9 +3,6 @@ import { Data } from "effect"
3
3
  export const documentRevision = (content: string) =>
4
4
  new Bun.CryptoHasher("sha256").update(content).digest("hex")
5
5
 
6
- export const isDocumentRevision = (revision: string) =>
7
- /^[a-f0-9]{64}$/.test(revision)
8
-
9
6
  export class RevisionConflictError extends Data.TaggedError(
10
7
  "RevisionConflictError",
11
8
  )<{
@@ -14,5 +11,4 @@ export class RevisionConflictError extends Data.TaggedError(
14
11
  readonly target?: string
15
12
  readonly expectedRevision: string
16
13
  readonly currentRevision: string
17
- readonly claim?: unknown
18
14
  }> {}
@@ -195,6 +195,7 @@ const plugin: Plugin = async ({ directory }) => {
195
195
  if (!sessionID) return
196
196
  const context = workerSessions.get(sessionID)
197
197
  if (!context?.target) return
198
+ output.env.AGENCY_INVOCATION_SOURCE = "agent"
198
199
  output.env.AGENCY_SESSION_ID = sessionID
199
200
  output.env.AGENCY_TARGET = context.target
200
201
  if (context.root) output.env.AGENCY_WORKBASE = context.root
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
2
2
  import { Schema } from "@effect/schema"
3
3
  import {
4
4
  EntityId,
5
- ClaimRecord,
6
5
  EpicFrontmatter,
7
6
  PhaseFrontmatter,
8
7
  TaskFrontmatter,
@@ -401,30 +400,18 @@ describe("work status", () => {
401
400
  })
402
401
  })
403
402
 
404
- describe("claim records", () => {
405
- const record = {
406
- claimant: "orchestrator",
407
- agent: "agent",
408
- sessionId: "job-1",
409
- startedAt: "2026-07-17T12:00:00.000Z",
410
- targetRevision: "0".repeat(64),
411
- expiresAt: "2026-07-17T13:00:00.000Z",
412
- state: "active" as const,
413
- }
414
-
415
- test("accepts explicit ownership and revision metadata", () => {
416
- expect(Schema.decodeUnknownSync(ClaimRecord)(record)).toEqual(record)
417
- })
418
-
419
- test("rejects malformed timestamps, revisions, and empty identities", () => {
420
- for (const invalid of [
421
- { ...record, claimant: "" },
422
- { ...record, startedAt: "today" },
423
- { ...record, targetRevision: "abc" },
424
- ]) {
425
- expect(() => Schema.decodeUnknownSync(ClaimRecord)(invalid)).toThrow()
426
- }
427
- })
403
+ test("rejects removed claim frontmatter", () => {
404
+ expect(() =>
405
+ Schema.decodeUnknownSync(TaskFrontmatter, { onExcessProperty: "error" })({
406
+ ticketUrl: null,
407
+ repo: "agency",
408
+ branch: "task/example",
409
+ base: "main",
410
+ pr: null,
411
+ status: "working",
412
+ claim: { state: "active" },
413
+ }),
414
+ ).toThrow()
428
415
  })
429
416
 
430
417
  describe("workbase registry", () => {
@@ -50,19 +50,6 @@ export const DocumentRevision = Schema.String.pipe(
50
50
  Schema.pattern(/^[a-f0-9]{64}$/),
51
51
  )
52
52
 
53
- export const ClaimRecord = Schema.Struct({
54
- claimant: NonEmptyString,
55
- agent: NonEmptyString,
56
- sessionId: NonEmptyString,
57
- startedAt: IsoTimestamp,
58
- targetRevision: DocumentRevision,
59
- expiresAt: Schema.optional(IsoTimestamp),
60
- state: Schema.Literal("active", "released", "finished"),
61
- releasedAt: Schema.optional(IsoTimestamp),
62
- finishedAt: Schema.optional(IsoTimestamp),
63
- outcome: Schema.optional(Schema.Literal("done", "dropped")),
64
- })
65
-
66
53
  const Url = NonEmptyString.pipe(Schema.pattern(/^[a-zA-Z][a-zA-Z0-9+.-]*:/))
67
54
 
68
55
  const GitHubPullRequestUrl = NonEmptyString.pipe(
@@ -163,7 +150,6 @@ const ExecutionUnit = {
163
150
  base: NonEmptyString,
164
151
  pr: Schema.NullOr(Schema.Union(GitHubPullRequestUrl, PullRequestRecord)),
165
152
  status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
166
- claim: Schema.optional(ClaimRecord),
167
153
  completion: Schema.optional(CompletionRecord),
168
154
  }
169
155
 
@@ -262,7 +248,6 @@ const ReviewTaskFrontmatter = Schema.Struct({
262
248
  ...TaskMetadata,
263
249
  review: ReviewRecord,
264
250
  status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
265
- claim: Schema.optional(ClaimRecord),
266
251
  completion: Schema.optional(CompletionRecord),
267
252
  })
268
253
 
@@ -288,7 +273,6 @@ export type RepositoryDeclaration = Schema.Schema.Type<
288
273
  typeof RepositoryDeclaration
289
274
  >
290
275
  export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
291
- export type ClaimRecord = Schema.Schema.Type<typeof ClaimRecord>
292
276
  export type PullRequestRecord = Schema.Schema.Type<typeof PullRequestRecord>
293
277
  export type ReviewSource = Schema.Schema.Type<typeof ReviewSource>
294
278
  export type ReviewRecord = Schema.Schema.Type<typeof ReviewRecord>
@@ -1,122 +0,0 @@
1
- import { Effect } from "effect"
2
- import { ClaimService } from "../services/ClaimService"
3
- import type { BaseCommandOptions } from "../utils/command"
4
- import { createLoggers } from "../utils/effect"
5
-
6
- interface ClaimCommandOptions extends BaseCommandOptions {
7
- readonly operation: "claim" | "release" | "finish"
8
- readonly taskId?: string
9
- readonly phaseId?: string
10
- readonly claimant?: string
11
- readonly agent?: string
12
- readonly sessionId?: string
13
- readonly revision?: string
14
- readonly expiresAt?: string
15
- readonly outcome?: string
16
- readonly noPullRequest?: boolean
17
- readonly summary?: string
18
- readonly evidenceUrl?: string
19
- readonly json?: boolean
20
- }
21
-
22
- export const claimCommand = (options: ClaimCommandOptions) =>
23
- Effect.gen(function* () {
24
- const claims = yield* ClaimService
25
- const { log } = createLoggers(options)
26
- const cwd = options.cwd ?? process.cwd()
27
- if (!options.taskId || !options.sessionId || !options.revision) {
28
- return yield* Effect.fail(new Error("Missing required claim arguments"))
29
- }
30
- if (
31
- options.operation === "claim" &&
32
- (!options.claimant || !options.agent)
33
- ) {
34
- return yield* Effect.fail(
35
- new Error("Claimant and agent identities are required"),
36
- )
37
- }
38
- if (options.noPullRequest && !options.summary?.trim()) {
39
- return yield* Effect.fail(
40
- new Error("Non-PR completion requires a non-empty summary"),
41
- )
42
- }
43
- if (options.noPullRequest && options.outcome !== "done") {
44
- return yield* Effect.fail(
45
- new Error("Non-PR completion is valid only with a done outcome"),
46
- )
47
- }
48
- if (
49
- options.operation === "finish" &&
50
- options.outcome !== "done" &&
51
- options.outcome !== "dropped"
52
- ) {
53
- return yield* Effect.fail(
54
- new Error("Finish outcome must be done or dropped"),
55
- )
56
- }
57
-
58
- const common = {
59
- taskId: options.taskId,
60
- ...(options.phaseId ? { phaseId: options.phaseId } : {}),
61
- sessionId: options.sessionId,
62
- revision: options.revision,
63
- }
64
- const result =
65
- options.operation === "claim"
66
- ? yield* claims.claim(
67
- {
68
- ...common,
69
- claimant: options.claimant!,
70
- agent: options.agent!,
71
- ...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
72
- },
73
- cwd,
74
- )
75
- : options.operation === "release"
76
- ? yield* claims.release(common, cwd)
77
- : yield* claims.finish(
78
- {
79
- ...common,
80
- outcome: options.outcome as "done" | "dropped",
81
- ...(options.noPullRequest
82
- ? {
83
- nonPrCompletion: {
84
- summary: options.summary!,
85
- ...(options.evidenceUrl
86
- ? { evidenceUrl: options.evidenceUrl }
87
- : {}),
88
- },
89
- }
90
- : {}),
91
- },
92
- cwd,
93
- )
94
-
95
- const { data: _, ...output } = result
96
- log(
97
- options.json
98
- ? JSON.stringify(output, null, 2)
99
- : `${options.operation === "claim" ? "Claimed" : options.operation === "release" ? "Released" : "Finished"} ${result.target} at revision ${result.revision}`,
100
- )
101
- })
102
-
103
- export const claimHelp = `
104
- Usage: agency claim <task-id> [phase-id] --claimant <id> --agent <id> --session-id <id> --revision <sha256>
105
-
106
- Claim an execution unit. Use distinct claimant and agent identities for delegated
107
- work. --expires-at accepts an optional future ISO-8601 timestamp.
108
- `
109
-
110
- export const releaseHelp = `
111
- Usage: agency release <task-id> [phase-id] --session-id <id> --revision <sha256>
112
-
113
- Release an execution unit owned by the session.
114
- `
115
-
116
- export const finishHelp = `
117
- Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--no-pull-request --summary <text> [--evidence-url <url>]]
118
-
119
- Finish a claim owned by the session. A done claim outcome leaves unmerged work
120
- working; agency sync marks the execution unit done after merge. Use
121
- --no-pull-request with a summary for an explicit non-PR completion.
122
- `