@markjaquith/agency 3.8.0 → 3.9.1

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
@@ -775,9 +775,11 @@ structured `changes`, `warnings`, `unresolved`, and per-execution evidence. The
775
775
  default mode applies safe reconciliation transitions; `--dry-run` is explicitly
776
776
  observational.
777
777
 
778
- Pass `<task-id>` to scope reconciliation to one task and its repositories. A
779
- multi-phase task scope includes all of its phases; add `[phase-id]` to select one
780
- phase. Scoped sync does not query, materialize, or reconcile unrelated work.
778
+ Pass `<task-id>` to scope reconciliation to one task and its repositories. With
779
+ no target, invocation inside an epic, task, or phase scopes to that current item;
780
+ invocation at the workbase root retains whole-workbase scope. A multi-phase task
781
+ scope includes all of its phases; add `[phase-id]` to select one phase. Scoped
782
+ sync does not query, materialize, or reconcile unrelated work.
781
783
 
782
784
  `agency sync` performs only these safe transitions:
783
785
 
@@ -1103,15 +1105,16 @@ agency archive task <task-id> [--dry-run] [--json]
1103
1105
  agency archive tasks [--dry-run] [--json]
1104
1106
  agency archive phase <task-id> <phase-id> [--dry-run] [--json]
1105
1107
  agency archive <path> [--dry-run] [--json]
1108
+ agency archive [--dry-run] [--json]
1106
1109
  agency restore epic <epic-id> [--dry-run] [--json]
1107
1110
  agency restore task <task-id> [--dry-run] [--json]
1108
1111
  agency restore phase <task-id> <phase-id> [--dry-run] [--json]
1109
1112
  ```
1110
1113
 
1111
- An existing path within an active epic or task infers that work item, so
1112
- `agency archive .` works from its directory. Collection roots are ambiguous,
1113
- phase paths require the explicit `archive phase` form, and paths outside active
1114
- epic or task trees are rejected. Archived work keeps its hierarchy under
1114
+ With no target, `agency archive` infers the active epic, task, or phase containing
1115
+ the current directory. An existing path within one of those items is inferred
1116
+ the same way. Collection roots are ambiguous, and paths outside active work item
1117
+ trees are rejected. Archived work keeps its hierarchy under
1115
1118
  `archive/`. Epic archiving includes its
1116
1119
  listed tasks. A task can be archived only when its effective status is terminal
1117
1120
  (`done` or `dropped`). Multi-phase task status is derived from its phases, every
@@ -1145,10 +1148,11 @@ agency pr create <task-id> [phase-id] [--draft] [--title <title>] [--head <branc
1145
1148
  agency pr [args...]
1146
1149
  ```
1147
1150
 
1148
- `agency work` presents the full hierarchy in the native OpenTUI selector or the
1149
- configured external chooser. Pass a directory, including `.` for the current
1150
- directory, to infer its epic, task, or phase. Outside a workbase, Agency first
1151
- presents the registered workbases, then the selected workbase's hierarchy.
1151
+ Inside an epic, task, or phase, `agency work` defaults to that current item.
1152
+ At the workbase root it presents the full hierarchy in the native OpenTUI
1153
+ selector or the configured external chooser. An explicit directory can target a
1154
+ different item. Outside a workbase, Agency first presents the registered
1155
+ workbases, then the selected workbase's hierarchy.
1152
1156
 
1153
1157
  Agency automatically uses the first available agent in this order: `opencode2`,
1154
1158
  `opencode`, then `claude`. `--opencode` and `--claude` remain aliases for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.8.0",
3
+ "version": "3.9.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
package/src/cli-parser.ts CHANGED
@@ -697,7 +697,7 @@ const commands = {
697
697
  },
698
698
  },
699
699
  archive: {
700
- usage: "agency archive <path|list|show|epic|task|tasks|phase>",
700
+ usage: "agency archive [path|list|show|epic|task|tasks|phase]",
701
701
  options: {
702
702
  ...outputOptions,
703
703
  ...entitySelectorOptions,
@@ -707,8 +707,8 @@ const commands = {
707
707
  repository: { type: "string", multiple: true },
708
708
  },
709
709
  command: {
710
- usage: "agency archive <path> [--dry-run] [--json]",
711
- minArgs: 1,
710
+ usage: "agency archive [path] [--dry-run] [--json]",
711
+ minArgs: 0,
712
712
  maxArgs: 1,
713
713
  options: ["dry-run", "json"],
714
714
  },
@@ -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
  })
@@ -260,7 +260,8 @@ var InteractiveSelectPrompt = (props) => {
260
260
  const dimensions = useTerminalDimensions();
261
261
  const [query, setQuery] = createSignal("");
262
262
  const editing = createReadlineEditing(() => input, setQuery);
263
- const [selected, setSelected] = createSignal(0);
263
+ const initialIndex = props.initialKey ? props.choices.findIndex((choice) => choice.key === props.initialKey) : 0;
264
+ const [selected, setSelected] = createSignal(Math.max(initialIndex, 0));
264
265
  const choices = createMemo(() => fuzzyChoices(props.choices, query()));
265
266
  const displaySegments = (choice) => choice.segments ?? [{
266
267
  text: choice.label
@@ -684,6 +685,9 @@ var InteractiveTabbedPrompt = (props) => {
684
685
  get emptyLabel() {
685
686
  return tab.emptyLabel;
686
687
  },
688
+ get initialKey() {
689
+ return tab.initialKey;
690
+ },
687
691
  onTab: cycle,
688
692
  get onDone() {
689
693
  return props.onDone;
@@ -727,6 +731,7 @@ var createInteractiveSession = async (onCancel = () => {}, initialTabs = []) =>
727
731
  let quitRequested = false;
728
732
  let activeTab = 0;
729
733
  let frameTabs = initialTabs;
734
+ const tabSelections = new Map;
730
735
  let navigationRequested = false;
731
736
  const navigate = (index) => {
732
737
  activeTab = index;
@@ -901,9 +906,16 @@ var createInteractiveSession = async (onCancel = () => {}, initialTabs = []) =>
901
906
  cancelled = false;
902
907
  setNotice(message);
903
908
  },
904
- tabs: (tabs) => {
905
- frameTabs = tabs;
906
- return ask("", undefined, tabs);
909
+ tabs: async (tabs) => {
910
+ frameTabs = tabs.map((tab) => ({
911
+ ...tab,
912
+ initialKey: tabSelections.get(tab.id)
913
+ }));
914
+ const selected = await ask("", undefined, frameTabs);
915
+ const tab = frameTabs[activeTab];
916
+ if (selected !== null && tab?.choices.some((choice) => choice.key === selected))
917
+ tabSelections.set(tab.id, selected);
918
+ return selected;
907
919
  },
908
920
  text: (prompt) => ask(prompt),
909
921
  select: (prompt, choices) => ask(prompt, choices),