@markjaquith/agency 3.7.0 → 3.9.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.
@@ -169,29 +169,18 @@ const entitySummary = (node: ActEntity) => ({
169
169
  readiness: node.readiness,
170
170
  revision: node.data.sha256,
171
171
  })
172
- const actionIcons: Record<string, string> = {
173
- reopen: "󰑓",
174
- drop: "󰅖",
175
- complete: "󰄬",
176
- sync: "󰑓",
177
- "pr-ready": "󰄬",
178
- "pr-close": "",
179
- }
180
172
  const actionChoices = (actions: readonly ActAction[]): Choice<string>[] =>
181
- actions.map(({ id, label }) => {
182
- const icon =
183
- actionIcons[id] ??
184
- actionGroups.find((group) =>
185
- group.actions.some((action) => action === id),
186
- )!.icon
187
- const color =
188
- id === "drop" || id === "pr-close" ? macchiato.red : macchiato.overlay1
173
+ actions.map(({ id, label, description, icon, color }) => {
189
174
  return {
190
175
  key: id,
191
176
  value: id,
192
- label: `${icon} ${label}`,
193
- plainLabel: label,
194
- segments: [{ text: `${icon} `, color }, { text: label }],
177
+ label: `${icon} ${label} — ${description}`,
178
+ plainLabel: `${label} — ${description}`,
179
+ segments: [
180
+ { text: `${icon} `, color },
181
+ { text: label },
182
+ { text: ` — ${description}`, color: macchiato.overlay0 },
183
+ ],
195
184
  }
196
185
  })
197
186
 
@@ -568,7 +557,7 @@ const actStep = (
568
557
  graph.nodes
569
558
  .filter((node) => node.kind === "repository")
570
559
  .map((node) => node.key),
571
- nodes.filter((node) => node.kind !== "epic").map((node) => node.key),
560
+ nodes.map((node) => node.key),
572
561
  config.chooserCommand,
573
562
  ),
574
563
  )
@@ -26,11 +26,13 @@ export const archive = (options: ArchiveOptions) =>
26
26
  const [id, phaseId] = options.args
27
27
  let archiveType = options.type
28
28
  let archiveId = id
29
+ let archiveTaskId: string | undefined
29
30
 
30
- if (!archiveType && id) {
31
- const target = yield* archives.resolvePathTarget(id, cwd)
31
+ if (!archiveType) {
32
+ const target = yield* archives.resolvePathTarget(id ?? ".", cwd)
32
33
  archiveType = target.kind
33
34
  archiveId = target.id
35
+ archiveTaskId = target.taskId
34
36
  }
35
37
 
36
38
  if (options.type === "list") {
@@ -106,13 +108,18 @@ export const archive = (options: ArchiveOptions) =>
106
108
  })
107
109
  break
108
110
  case "phase":
109
- if (!id || !phaseId)
111
+ if (!(archiveTaskId && archiveId) && (!id || !phaseId))
110
112
  return yield* Effect.fail(
111
113
  new Error("Usage: agency archive phase <task-id> <phase-id>"),
112
114
  )
113
- result = yield* archives.archivePhase(id, phaseId, cwd, {
114
- dryRun: options.dryRun,
115
- })
115
+ result = yield* archives.archivePhase(
116
+ archiveTaskId ?? id!,
117
+ archiveTaskId ? archiveId! : phaseId!,
118
+ cwd,
119
+ {
120
+ dryRun: options.dryRun,
121
+ },
122
+ )
116
123
  break
117
124
  default:
118
125
  return yield* Effect.fail(
@@ -149,14 +156,15 @@ export const archive = (options: ArchiveOptions) =>
149
156
  })
150
157
 
151
158
  export const help = `
152
- Usage: agency archive <path|list|show|epic|task|tasks|phase>
159
+ Usage: agency archive [path|list|show|epic|task|tasks|phase]
153
160
 
154
161
  Browse or archive work items after preflighting worktrees and graph references.
155
162
 
156
- An existing path within an active epic or task infers that work item.
163
+ With no target, the current epic, task, or phase is inferred. An existing path
164
+ within one of those items infers it too.
157
165
 
158
166
  Commands:
159
- <path> Archive the containing epic or task
167
+ [path] Archive the containing epic, task, or phase
160
168
  list [filters] List archived work
161
169
  show <type> <id> Show an archived epic or task
162
170
  show phase <task-id> <phase-id> Show an archived phase
@@ -129,7 +129,8 @@ repositories, worktrees, branches, references, and pull requests.
129
129
  Safe reconciliation transitions are applied by default.
130
130
  When a task, phase, or item path is provided, only that target and its
131
131
  repositories are queried or reconciled. Task and epic scopes include their
132
- child execution units.
132
+ child execution units. With no target, the current item is inferred when the
133
+ command runs inside one; the workbase root retains whole-workbase scope.
133
134
 
134
135
  Options:
135
136
  --dry-run Report planned safe transitions without changing state
@@ -139,12 +139,8 @@ export const work = (
139
139
  const integrations = yield* IntegrationService
140
140
  const { log, verboseLog } = createLoggers(options)
141
141
  const cwd = options.cwd ?? process.cwd()
142
- const directoryPath = options.directory
143
- ? resolve(cwd, options.directory)
144
- : undefined
145
- const isDirectory = directoryPath
146
- ? yield* fs.isDirectory(directoryPath)
147
- : false
142
+ const directoryPath = resolve(cwd, options.directory ?? ".")
143
+ const isDirectory = yield* fs.isDirectory(directoryPath)
148
144
  const startPath = isDirectory && directoryPath ? directoryPath : cwd
149
145
  const inputAllowed = options.inputAllowed ?? true
150
146
  const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
@@ -660,16 +656,17 @@ export const help = `
660
656
  Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto]
661
657
  agency work prepare [target] [--evidence <json-or-path>] [--force] [--dry-run] [--json]
662
658
 
663
- Launch an agent for an epic, task, or phase. With no directory, select one
664
- interactively. A positional argument resolves as a directory first, then as a task
665
- ID. Use '.' for the current directory. Outside a workbase, select a registered
666
- workbase first. Managed OpenCode launches receive whole-workbase access through
667
- Agency's project plugin; Agency context remains authoritative for writes. Automatic
668
- agent discovery checks opencode2, opencode, pi, then claude.
659
+ Launch an agent for an epic, task, or phase. With no directory, infer the current
660
+ item or select one interactively at the workbase root. A positional argument
661
+ resolves as a directory first, then as a task ID. Outside a workbase, select a
662
+ registered workbase first. Managed OpenCode launches receive whole-workbase access
663
+ through Agency's project plugin; Agency context remains authoritative for writes.
664
+ Automatic agent discovery checks opencode2, opencode, pi, then claude.
669
665
 
670
666
  The prepare subcommand resolves and materializes an execution workspace without
671
- launching an agent or changing lifecycle status. --dry-run reports planned Git
672
- changes without fetching, creating branches, or creating worktrees.
667
+ launching an agent or changing lifecycle status. Its omitted target defaults to
668
+ the current execution unit. --dry-run reports planned Git changes without
669
+ fetching, creating branches, or creating worktrees.
673
670
  It emits revision-bound validation evidence and an idempotent external-orchestrator
674
671
  contract. Evidence is reused only while the target, workbase, configuration, and
675
672
  repository mapping remain unchanged. Dynamic readiness and workspace safety checks
@@ -54,8 +54,9 @@ class ArchiveError extends Data.TaggedError("ArchiveError")<{
54
54
  export type ArchiveKind = "epic" | "task" | "phase"
55
55
 
56
56
  interface ArchivePathTarget {
57
- readonly kind: "epic" | "task"
57
+ readonly kind: ArchiveKind
58
58
  readonly id: string
59
+ readonly taskId?: string
59
60
  }
60
61
 
61
62
  const LifecycleEventSchema = Schema.Struct({
@@ -489,14 +490,11 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
489
490
  : undefined
490
491
  if (!kind || !id) {
491
492
  return yield* new ArchiveError({
492
- message: `Archive path must be within an active epic or task: ${canonicalPath}`,
493
- })
494
- }
495
- if (kind === "task" && parts[2] === "phases") {
496
- return yield* new ArchiveError({
497
- message: `Archive path identifies a phase; use 'agency archive phase <task-id> <phase-id>': ${canonicalPath}`,
493
+ message: `Archive path must be within an active epic, task, or phase: ${canonicalPath}`,
498
494
  })
499
495
  }
496
+ const phaseId =
497
+ kind === "task" && parts[2] === "phases" ? parts[3] : undefined
500
498
 
501
499
  const document = join(
502
500
  root,
@@ -509,6 +507,26 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
509
507
  message: `Archive path does not identify an active ${kind}: ${canonicalPath}`,
510
508
  })
511
509
  }
510
+ if (phaseId) {
511
+ const phaseDocument = join(
512
+ root,
513
+ "tasks",
514
+ id,
515
+ "phases",
516
+ phaseId,
517
+ "PHASE.md",
518
+ )
519
+ if (!(yield* fs.exists(phaseDocument))) {
520
+ return yield* new ArchiveError({
521
+ message: `Archive path does not identify an active phase: ${canonicalPath}`,
522
+ })
523
+ }
524
+ return {
525
+ kind: "phase",
526
+ id: phaseId,
527
+ taskId: id,
528
+ } satisfies ArchivePathTarget
529
+ }
512
530
  return { kind, id } satisfies ArchivePathTarget
513
531
  }),
514
532
 
@@ -229,13 +229,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
229
229
  const repositories = yield* RepositoryService
230
230
  const versionControl = yield* VersionControlService
231
231
  const cwd = resolve(options.cwd ?? process.cwd())
232
- const candidate = options.taskId
233
- ? resolve(cwd, options.taskId)
234
- : undefined
235
- const candidateExists =
236
- candidate !== undefined && !options.phaseId
237
- ? yield* fs.exists(candidate)
238
- : false
232
+ const candidate = resolve(cwd, options.taskId ?? ".")
233
+ const candidateExists = !options.phaseId
234
+ ? yield* fs.exists(candidate)
235
+ : false
239
236
  const { root, config } = yield* workbase.loadConfig(
240
237
  candidateExists ? candidate : cwd,
241
238
  )
@@ -262,7 +259,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
262
259
  const existingTaskSelector =
263
260
  options.taskId !== undefined &&
264
261
  documents.tasks.some((task) => task.id === options.taskId)
265
- if (candidateExists && candidate && !existingTaskSelector) {
262
+ if (candidateExists && !existingTaskSelector) {
266
263
  const canonicalPath = yield* fs.realPath(candidate)
267
264
  const canonicalRoot = yield* fs.realPath(root)
268
265
  const parts = relative(canonicalRoot, canonicalPath).split(sep)
@@ -272,7 +269,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
272
269
  } else if (parts[0] === "tasks" && parts[1]) {
273
270
  taskId = parts[1]
274
271
  phaseId = parts[2] === "phases" && parts[3] ? parts[3] : undefined
275
- } else {
272
+ } else if (options.taskId !== undefined) {
276
273
  return yield* new SyncError({
277
274
  message: `Sync path does not identify an active task, phase, or epic: ${canonicalPath}`,
278
275
  })