@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 +15 -11
- package/package.json +1 -1
- package/src/cli-parser.ts +3 -3
- package/src/commands/archive.ts +17 -9
- package/src/commands/sync.ts +2 -1
- package/src/commands/work.ts +11 -14
- package/src/services/ArchiveService.ts +25 -7
- package/src/services/SyncService.ts +6 -9
- package/src/utils/interactive.js +16 -4
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.
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
package/src/cli-parser.ts
CHANGED
|
@@ -697,7 +697,7 @@ const commands = {
|
|
|
697
697
|
},
|
|
698
698
|
},
|
|
699
699
|
archive: {
|
|
700
|
-
usage: "agency archive
|
|
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
|
|
711
|
-
minArgs:
|
|
710
|
+
usage: "agency archive [path] [--dry-run] [--json]",
|
|
711
|
+
minArgs: 0,
|
|
712
712
|
maxArgs: 1,
|
|
713
713
|
options: ["dry-run", "json"],
|
|
714
714
|
},
|
package/src/commands/archive.ts
CHANGED
|
@@ -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
|
|
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(
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/commands/sync.ts
CHANGED
|
@@ -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
|
package/src/commands/work.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
workbase first. Managed OpenCode launches receive whole-workbase access
|
|
667
|
-
Agency's project plugin; Agency context remains authoritative for writes.
|
|
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.
|
|
672
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 &&
|
|
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
|
})
|
package/src/utils/interactive.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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),
|