@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.
- package/README.md +30 -62
- package/cli-main.ts +20 -83
- package/fixtures/protocol/orchestration-recipes.json +9 -34
- package/package.json +1 -4
- package/schemas/agency-graph-v1.schema.json +2 -31
- package/src/cli-parser.test.ts +34 -132
- package/src/cli-parser.ts +45 -102
- package/src/cli.test.ts +60 -117
- package/src/commands/act.ts +2 -6
- package/src/commands/sync.ts +3 -3
- package/src/commands/validate.ts +3 -1
- package/src/commands/work.test.ts +70 -8
- package/src/commands/work.ts +15 -8
- package/src/graph-schema.ts +0 -2
- package/src/protocol.test.ts +0 -25
- package/src/protocol.ts +0 -11
- package/src/readiness.test.ts +2 -2
- package/src/services/ArchiveBulkService.test.ts +0 -88
- package/src/services/ArchiveService.ts +0 -32
- package/src/services/GraphMutationService.ts +0 -26
- package/src/services/GraphService.test.ts +1 -1
- package/src/services/IntegrationService.test.ts +7 -4
- package/src/services/LifecycleTransaction.ts +5 -1
- package/src/services/PhaseService.ts +1 -8
- package/src/services/ReadinessService.test.ts +0 -34
- package/src/services/ReadinessService.ts +1 -20
- package/src/services/ReviewService.test.ts +0 -64
- package/src/services/ReviewService.ts +0 -5
- package/src/services/SyncService.test.ts +75 -88
- package/src/services/SyncService.ts +52 -123
- package/src/services/TaskPhaseService.test.ts +13 -1
- package/src/services/TaskService.ts +1 -7
- package/src/services/WorkbaseService.ts +0 -3
- package/src/services/WorktreeService.test.ts +192 -1
- package/src/services/WorktreeService.ts +169 -34
- package/src/test-utils.ts +0 -2
- package/src/usage-log.test.ts +89 -5
- package/src/usage-log.ts +139 -31
- package/src/workbase/AGENTS.md +9 -15
- package/src/workbase/agent-command.test.ts +1 -3
- package/src/workbase/agent-command.ts +1 -6
- package/src/workbase/document-revision.ts +0 -4
- package/src/workbase/opencode-plugin-file.ts +1 -0
- package/src/workbase/schemas.test.ts +12 -25
- package/src/workbase/schemas.ts +0 -16
- package/src/commands/claim.ts +0 -122
- package/src/services/ClaimService.test.ts +0 -415
- 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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
184
|
+
pruneExpiredEvents(database, env)
|
|
185
|
+
const eventJourneyId = journeyId(env)
|
|
82
186
|
database
|
|
83
187
|
.query(`
|
|
84
188
|
INSERT INTO usage_events (
|
|
85
|
-
event_version,
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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,
|
|
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
|
-
|
|
142
|
-
|
|
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
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -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.
|
|
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
|
|
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,
|
|
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
|
|
193
|
-
|
|
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
|
-
-
|
|
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,
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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", () => {
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -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>
|
package/src/commands/claim.ts
DELETED
|
@@ -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
|
-
`
|