@markjaquith/agency 3.15.0 → 3.16.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.
- package/README.md +29 -0
- package/cli-main.ts +19 -0
- package/package.json +1 -1
- package/src/act-schema.ts +1 -0
- package/src/cli-parser.ts +12 -0
- package/src/commands/act-actions.ts +40 -0
- package/src/commands/act.ts +2 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/phase.ts +3 -0
- package/src/commands/review.ts +3 -0
- package/src/commands/status.ts +7 -1
- package/src/commands/sync.ts +5 -0
- package/src/commands/task.ts +3 -0
- package/src/services/ContextService.ts +2 -0
- package/src/services/PhaseService.ts +7 -1
- package/src/services/ReviewService.ts +2 -0
- package/src/services/SyncService.ts +10 -0
- package/src/services/TaskService.ts +7 -1
- package/src/services/WorkbaseService.ts +13 -0
- package/src/services/auto-archive.ts +82 -0
- package/src/workbase/AGENTS.md +10 -0
- package/src/workbase/schemas.ts +1 -0
package/README.md
CHANGED
|
@@ -1229,6 +1229,35 @@ parent epic implicitly. Versioned lifecycle provenance preserves parent
|
|
|
1229
1229
|
declarations and dependency edges for restoration. Archived IDs are reserved
|
|
1230
1230
|
until restored.
|
|
1231
1231
|
|
|
1232
|
+
#### Automatic archiving
|
|
1233
|
+
|
|
1234
|
+
```text
|
|
1235
|
+
agency config auto-archive # show the effective setting
|
|
1236
|
+
agency config auto-archive on # enable for this workbase
|
|
1237
|
+
agency config auto-archive off # disable
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
The boolean `autoArchive` setting in the workbase's `agency.json` defaults to
|
|
1241
|
+
`false`. Enabling it authorizes automatic task archiving on future terminal
|
|
1242
|
+
transitions; it does not sweep existing terminal work. `agency act --json`
|
|
1243
|
+
lists these configuration commands, and context/status JSON includes the
|
|
1244
|
+
effective setting.
|
|
1245
|
+
|
|
1246
|
+
Explicit `task status` / `phase status` changes to `done` or `dropped`, non-PR
|
|
1247
|
+
completion, `review finish`, and merged-PR reconciliation automatically
|
|
1248
|
+
attempt to archive the owning task. A multi-phase task waits until every phase
|
|
1249
|
+
is terminal; individual phases and epics are never auto-archived. Sync defers
|
|
1250
|
+
cleanup until its reconciliation pass finishes, and `sync --dry-run` never
|
|
1251
|
+
archives anything.
|
|
1252
|
+
|
|
1253
|
+
Automatic cleanup uses the same preflight as `archive task --dry-run`, without
|
|
1254
|
+
forcing past dirty checkouts, retained dependents, or other safety
|
|
1255
|
+
failures. A blocked attempt leaves the terminal status intact and the task
|
|
1256
|
+
unarchived, with the reason reported in human output and the triggering command's
|
|
1257
|
+
JSON `autoArchive` result (`sync` returns an array). After resolving the blocker,
|
|
1258
|
+
use explicit `archive` to retry safely. Successful automatic archiving removes
|
|
1259
|
+
clean managed checkouts, so paths inside the task may cease to exist.
|
|
1260
|
+
|
|
1232
1261
|
### Work, Publication, and Pull Requests
|
|
1233
1262
|
|
|
1234
1263
|
```text
|
package/cli-main.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { push, help as pushHelp } from "./src/commands/push"
|
|
|
10
10
|
import { work, workPrepare, help as workHelp } from "./src/commands/work"
|
|
11
11
|
import { worktree, help as worktreeHelp } from "./src/commands/worktree"
|
|
12
12
|
import { status, help as statusHelp } from "./src/commands/status"
|
|
13
|
+
import { config, help as configHelp } from "./src/commands/config"
|
|
13
14
|
import { doctor, help as doctorHelp } from "./src/commands/doctor"
|
|
14
15
|
import { validate, help as validateHelp } from "./src/commands/validate"
|
|
15
16
|
import { context, help as contextHelp } from "./src/commands/context"
|
|
@@ -579,6 +580,23 @@ const commands: Record<string, Command> = {
|
|
|
579
580
|
)
|
|
580
581
|
},
|
|
581
582
|
},
|
|
583
|
+
config: {
|
|
584
|
+
async run(args: string[], options: Record<string, any>) {
|
|
585
|
+
if (options.help) {
|
|
586
|
+
console.log(configHelp)
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
await runCommand(
|
|
590
|
+
config({
|
|
591
|
+
silent: options.silent,
|
|
592
|
+
verbose: options.verbose,
|
|
593
|
+
json: options.json,
|
|
594
|
+
cwd: options.cwd,
|
|
595
|
+
args,
|
|
596
|
+
}),
|
|
597
|
+
)
|
|
598
|
+
},
|
|
599
|
+
},
|
|
582
600
|
status: {
|
|
583
601
|
run: async (_args: string[], options: Record<string, any>) => {
|
|
584
602
|
if (options.help) {
|
|
@@ -729,6 +747,7 @@ Commands:
|
|
|
729
747
|
review refresh|finish Refresh or finish a pinned review task
|
|
730
748
|
repo <subcommand> Manage workbase repositories
|
|
731
749
|
status Show status for the current workbase
|
|
750
|
+
config Show or change workbase settings
|
|
732
751
|
doctor Diagnose workbase health and integrations
|
|
733
752
|
validate [path] Validate a workbase
|
|
734
753
|
context [target] Return complete target context
|
package/package.json
CHANGED
package/src/act-schema.ts
CHANGED
package/src/cli-parser.ts
CHANGED
|
@@ -958,6 +958,18 @@ const commands = {
|
|
|
958
958
|
options: ["select", "json"],
|
|
959
959
|
},
|
|
960
960
|
},
|
|
961
|
+
config: {
|
|
962
|
+
usage: "agency config auto-archive [on|off] [--json]",
|
|
963
|
+
options: outputOptions,
|
|
964
|
+
subcommands: {
|
|
965
|
+
"auto-archive": {
|
|
966
|
+
usage: "agency config auto-archive [on|off] [--json]",
|
|
967
|
+
minArgs: 0,
|
|
968
|
+
maxArgs: 1,
|
|
969
|
+
options: ["json"],
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
961
973
|
status: {
|
|
962
974
|
usage: "agency status [filters] [--json]",
|
|
963
975
|
options: { ...outputOptions, ...viewOptions },
|
|
@@ -14,6 +14,7 @@ import { phase } from "./phase"
|
|
|
14
14
|
import { push } from "./push"
|
|
15
15
|
import { sync } from "./sync"
|
|
16
16
|
import { status } from "./status"
|
|
17
|
+
import { config } from "./config"
|
|
17
18
|
import { pr, prCreate } from "./pr"
|
|
18
19
|
import { validate } from "./validate"
|
|
19
20
|
import { work as startWork, type StartWork } from "./work"
|
|
@@ -33,6 +34,7 @@ type NativeOperation =
|
|
|
33
34
|
| ReturnType<typeof push>
|
|
34
35
|
| ReturnType<typeof sync>
|
|
35
36
|
| ReturnType<typeof status>
|
|
37
|
+
| ReturnType<typeof config>
|
|
36
38
|
| ReturnType<typeof doctor>
|
|
37
39
|
| ReturnType<typeof integration>
|
|
38
40
|
| ReturnType<typeof validate>
|
|
@@ -188,6 +190,22 @@ const actionPresentation: Record<
|
|
|
188
190
|
icon: "",
|
|
189
191
|
color: macchiato.green,
|
|
190
192
|
},
|
|
193
|
+
"auto-archive-show": {
|
|
194
|
+
description: "Show the effective automatic archive setting.",
|
|
195
|
+
icon: "",
|
|
196
|
+
color: macchiato.blue,
|
|
197
|
+
},
|
|
198
|
+
"auto-archive-enable": {
|
|
199
|
+
description:
|
|
200
|
+
"Automatically archive tasks on future terminal transitions, subject to safety checks.",
|
|
201
|
+
icon: "",
|
|
202
|
+
color: macchiato.green,
|
|
203
|
+
},
|
|
204
|
+
"auto-archive-disable": {
|
|
205
|
+
description: "Leave terminal tasks active until explicitly archived.",
|
|
206
|
+
icon: "",
|
|
207
|
+
color: macchiato.yellow,
|
|
208
|
+
},
|
|
191
209
|
doctor: {
|
|
192
210
|
description: "Diagnose workbase, repository, and integration health.",
|
|
193
211
|
icon: "",
|
|
@@ -441,6 +459,9 @@ export const actionGroups = [
|
|
|
441
459
|
"sync-all",
|
|
442
460
|
"integration-status",
|
|
443
461
|
"integration-sync",
|
|
462
|
+
"auto-archive-show",
|
|
463
|
+
"auto-archive-enable",
|
|
464
|
+
"auto-archive-disable",
|
|
444
465
|
],
|
|
445
466
|
},
|
|
446
467
|
{
|
|
@@ -830,6 +851,25 @@ export const actActions = (
|
|
|
830
851
|
}),
|
|
831
852
|
),
|
|
832
853
|
),
|
|
854
|
+
...(["show", "enable", "disable"] as const).map((operation) => {
|
|
855
|
+
const args = [
|
|
856
|
+
"auto-archive",
|
|
857
|
+
...(operation === "show"
|
|
858
|
+
? []
|
|
859
|
+
: [operation === "enable" ? "on" : "off"]),
|
|
860
|
+
]
|
|
861
|
+
return immediate(
|
|
862
|
+
{
|
|
863
|
+
id: `auto-archive-${operation}`,
|
|
864
|
+
label: `${operation === "show" ? "Show" : operation === "enable" ? "Enable" : "Disable"} auto archive`,
|
|
865
|
+
blockedReason: null,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
command: ["agency", "config", ...args],
|
|
869
|
+
run: config({ ...options, args }),
|
|
870
|
+
},
|
|
871
|
+
)
|
|
872
|
+
}),
|
|
833
873
|
immediate(
|
|
834
874
|
{ id: "validate", label: "Validate workbase", blockedReason: null },
|
|
835
875
|
{
|
package/src/commands/act.ts
CHANGED
|
@@ -481,6 +481,7 @@ const actStep = (
|
|
|
481
481
|
creationDefaults: creationDefaults(config),
|
|
482
482
|
workbase: {
|
|
483
483
|
root,
|
|
484
|
+
autoArchive: config.autoArchive ?? false,
|
|
484
485
|
repositories: graph.nodes
|
|
485
486
|
.filter((node) => node.kind === "repository")
|
|
486
487
|
.map((node) => node.key),
|
|
@@ -790,6 +791,7 @@ A workbase path opens Workbase actions. Otherwise the positional value is a
|
|
|
790
791
|
task ID. Selectors skip item selection.
|
|
791
792
|
JSON discovery includes creation defaults identifying when callers should omit
|
|
792
793
|
--branch so the workbase branchNameCommand can choose it.
|
|
794
|
+
JSON discovery also includes the effective auto-archive setting and its commands.
|
|
793
795
|
|
|
794
796
|
Options:
|
|
795
797
|
--action <id> Start an action or filter discovery (IDs from --json)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
export const config = (
|
|
7
|
+
options: BaseCommandOptions & { readonly args: readonly string[] },
|
|
8
|
+
) =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const [setting, value] = options.args
|
|
11
|
+
if (
|
|
12
|
+
setting !== "auto-archive" ||
|
|
13
|
+
(value !== undefined && value !== "on" && value !== "off")
|
|
14
|
+
) {
|
|
15
|
+
return yield* Effect.fail(
|
|
16
|
+
new Error("Usage: agency config auto-archive [on|off]"),
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
const workbase = yield* WorkbaseService
|
|
20
|
+
const { log } = createLoggers(options)
|
|
21
|
+
const result =
|
|
22
|
+
value === undefined
|
|
23
|
+
? yield* workbase.loadConfig(options.cwd).pipe(
|
|
24
|
+
Effect.map(({ root, config }) => ({
|
|
25
|
+
root,
|
|
26
|
+
autoArchive: config.autoArchive ?? false,
|
|
27
|
+
})),
|
|
28
|
+
)
|
|
29
|
+
: yield* workbase.setAutoArchive(value === "on", options.cwd)
|
|
30
|
+
log(
|
|
31
|
+
options.json
|
|
32
|
+
? JSON.stringify(result, null, 2)
|
|
33
|
+
: `Auto archive: ${result.autoArchive ? "on" : "off"}`,
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const help = `
|
|
38
|
+
Usage: agency config auto-archive [on|off] [--json]
|
|
39
|
+
|
|
40
|
+
Show, enable, or disable automatic archiving in this workbase.
|
|
41
|
+
Defaults to off. Enabling applies to future terminal transitions, not existing
|
|
42
|
+
terminal work. Archive safety checks are never bypassed.
|
|
43
|
+
`
|
package/src/commands/phase.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { autoArchiveMessage } from "../services/auto-archive"
|
|
2
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
4
|
import { PhaseService } from "../services/PhaseService"
|
|
4
5
|
import { createLoggers } from "../utils/effect"
|
|
@@ -199,6 +200,8 @@ export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
|
199
200
|
? JSON.stringify(output, null, 2)
|
|
200
201
|
: `Marked phase '${phaseId}' as ${record.data.status}`,
|
|
201
202
|
)
|
|
203
|
+
const notice = autoArchiveMessage(record.autoArchive)
|
|
204
|
+
if (!options.json && notice) log(notice)
|
|
202
205
|
return
|
|
203
206
|
}
|
|
204
207
|
case "update": {
|
package/src/commands/review.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { ReviewService } from "../services/ReviewService"
|
|
3
|
+
import { autoArchiveMessage } from "../services/auto-archive"
|
|
3
4
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
5
|
import { createLoggers } from "../utils/effect"
|
|
5
6
|
|
|
@@ -33,6 +34,8 @@ export const review = (options: ReviewOptions) =>
|
|
|
33
34
|
? JSON.stringify(result, null, 2)
|
|
34
35
|
: `Finished review '${options.taskId}'`,
|
|
35
36
|
)
|
|
37
|
+
const notice = autoArchiveMessage(result.autoArchive)
|
|
38
|
+
if (!options.json && notice) log(notice)
|
|
36
39
|
return
|
|
37
40
|
}
|
|
38
41
|
const result = yield* service.refresh(
|
package/src/commands/status.ts
CHANGED
|
@@ -34,7 +34,13 @@ export const status = (options: StatusOptions = {}) =>
|
|
|
34
34
|
pr: options.pr,
|
|
35
35
|
})).executionRows
|
|
36
36
|
: []
|
|
37
|
-
const
|
|
37
|
+
const { config } = yield* workbase.loadConfig(cwd)
|
|
38
|
+
const data = {
|
|
39
|
+
...report,
|
|
40
|
+
autoArchive: config.autoArchive ?? false,
|
|
41
|
+
repositories: repos,
|
|
42
|
+
work: executionRows,
|
|
43
|
+
}
|
|
38
44
|
|
|
39
45
|
if (options.json) {
|
|
40
46
|
log(JSON.stringify(data, null, 2))
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { SyncService } from "../services/SyncService"
|
|
3
|
+
import { autoArchiveMessage } from "../services/auto-archive"
|
|
3
4
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
5
|
import { createLoggers } from "../utils/effect"
|
|
5
6
|
import { createProgress, type Progress } from "../utils/progress"
|
|
@@ -87,6 +88,10 @@ export const sync = (
|
|
|
87
88
|
`${change.status === "applied" ? "Applied" : "Planned"} ${change.kind} '${change.target}': ${change.message}`,
|
|
88
89
|
)
|
|
89
90
|
}
|
|
91
|
+
for (const archive of result.autoArchive) {
|
|
92
|
+
const notice = autoArchiveMessage(archive)
|
|
93
|
+
if (notice) log(notice)
|
|
94
|
+
}
|
|
90
95
|
for (const { notice: warning, targets } of groupedNotices(
|
|
91
96
|
result.warnings,
|
|
92
97
|
)) {
|
package/src/commands/task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { autoArchiveMessage } from "../services/auto-archive"
|
|
2
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
4
|
import { TaskService } from "../services/TaskService"
|
|
4
5
|
import { EpicService } from "../services/EpicService"
|
|
@@ -491,6 +492,8 @@ export const task = (
|
|
|
491
492
|
? JSON.stringify(output, null, 2)
|
|
492
493
|
: `Marked task '${id}' as ${record.data.status}`,
|
|
493
494
|
)
|
|
495
|
+
const notice = autoArchiveMessage(record.autoArchive)
|
|
496
|
+
if (!options.json && notice) log(notice)
|
|
494
497
|
return
|
|
495
498
|
}
|
|
496
499
|
case "update": {
|
|
@@ -329,6 +329,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
329
329
|
root,
|
|
330
330
|
configPath: join(root, "agency.json"),
|
|
331
331
|
version: config.version,
|
|
332
|
+
autoArchive: config.autoArchive ?? false,
|
|
332
333
|
vcs: "git",
|
|
333
334
|
},
|
|
334
335
|
target: { kind: "workbase", path: root },
|
|
@@ -1174,6 +1175,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1174
1175
|
root,
|
|
1175
1176
|
configPath: join(root, "agency.json"),
|
|
1176
1177
|
version: config.version,
|
|
1178
|
+
autoArchive: config.autoArchive ?? false,
|
|
1177
1179
|
vcs: "git",
|
|
1178
1180
|
},
|
|
1179
1181
|
target,
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
formatWorkDocumentBody,
|
|
18
18
|
parseFrontmatter,
|
|
19
19
|
} from "../workbase/frontmatter"
|
|
20
|
-
import { canTransitionStatus } from "../readiness"
|
|
20
|
+
import { canTransitionStatus, isTerminalStatus } from "../readiness"
|
|
21
|
+
import { autoArchiveTask, type AutoArchiveResult } from "./auto-archive"
|
|
21
22
|
import { documentRevision } from "../workbase/document-revision"
|
|
22
23
|
import { archivedPhaseDirectory } from "../workbase/archive"
|
|
23
24
|
import {
|
|
@@ -43,6 +44,7 @@ export interface PhaseRecord {
|
|
|
43
44
|
readonly content: string
|
|
44
45
|
readonly revision: string
|
|
45
46
|
readonly data: PhaseData
|
|
47
|
+
readonly autoArchive?: AutoArchiveResult
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface CreatePhaseInput {
|
|
@@ -573,11 +575,15 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
573
575
|
: { ...record.data, status: validStatus }
|
|
574
576
|
const content = formatMarkdownDocument(data, parsed.body)
|
|
575
577
|
yield* fs.writeFile(record.path, content)
|
|
578
|
+
const autoArchive = isTerminalStatus(data.status)
|
|
579
|
+
? yield* autoArchiveTask(taskId, startPath)
|
|
580
|
+
: undefined
|
|
576
581
|
return {
|
|
577
582
|
...record,
|
|
578
583
|
content,
|
|
579
584
|
revision: documentRevision(content),
|
|
580
585
|
data,
|
|
586
|
+
...(autoArchive ? { autoArchive } : {}),
|
|
581
587
|
} satisfies PhaseRecord
|
|
582
588
|
}),
|
|
583
589
|
}),
|
|
@@ -7,6 +7,7 @@ import { PhaseService } from "./PhaseService"
|
|
|
7
7
|
import { RepositoryService } from "./RepositoryService"
|
|
8
8
|
import { TaskService } from "./TaskService"
|
|
9
9
|
import { WorkbaseService } from "./WorkbaseService"
|
|
10
|
+
import { autoArchiveTask } from "./auto-archive"
|
|
10
11
|
import {
|
|
11
12
|
WorktreeService,
|
|
12
13
|
type WorktreeRemovalSnapshot,
|
|
@@ -467,6 +468,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
467
468
|
path: task.path,
|
|
468
469
|
revision: documentRevision(content),
|
|
469
470
|
data,
|
|
471
|
+
autoArchive: yield* autoArchiveTask(taskId, root),
|
|
470
472
|
}
|
|
471
473
|
}),
|
|
472
474
|
}),
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type RepositorySetupResult,
|
|
32
32
|
} from "./RepositoryService"
|
|
33
33
|
import { VersionControlService } from "./VersionControlService"
|
|
34
|
+
import { autoArchiveTask, type AutoArchiveResult } from "./auto-archive"
|
|
34
35
|
|
|
35
36
|
class SyncError extends Data.TaggedError("SyncError")<{
|
|
36
37
|
readonly message: string
|
|
@@ -111,6 +112,7 @@ interface SyncResult {
|
|
|
111
112
|
readonly unresolved: readonly SyncNotice[]
|
|
112
113
|
readonly executions: readonly ExecutionSyncState[]
|
|
113
114
|
readonly repositories: RepositorySetupResult
|
|
115
|
+
readonly autoArchive: readonly AutoArchiveResult[]
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
export interface SyncProgress {
|
|
@@ -370,6 +372,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
370
372
|
|
|
371
373
|
const apply = options.apply === true
|
|
372
374
|
const changes: SyncChange[] = []
|
|
375
|
+
const completedTasks = new Set<string>()
|
|
373
376
|
const warnings: SyncNotice[] = []
|
|
374
377
|
const unresolved: SyncNotice[] = []
|
|
375
378
|
for (const issue of repositorySetup.unresolved) {
|
|
@@ -1285,6 +1288,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1285
1288
|
})
|
|
1286
1289
|
data = completed.data
|
|
1287
1290
|
revision = completed.revision
|
|
1291
|
+
completedTasks.add(record.taskId)
|
|
1288
1292
|
}
|
|
1289
1293
|
changes.push({
|
|
1290
1294
|
kind: "mark-done",
|
|
@@ -1372,6 +1376,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1372
1376
|
reportExecution(`task:${task.id}`)
|
|
1373
1377
|
}
|
|
1374
1378
|
|
|
1379
|
+
const autoArchive: AutoArchiveResult[] = []
|
|
1380
|
+
if (apply && config.autoArchive) {
|
|
1381
|
+
for (const taskId of completedTasks)
|
|
1382
|
+
autoArchive.push(yield* autoArchiveTask(taskId, root))
|
|
1383
|
+
}
|
|
1375
1384
|
return {
|
|
1376
1385
|
root,
|
|
1377
1386
|
mode: apply ? "apply" : "dry-run",
|
|
@@ -1380,6 +1389,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1380
1389
|
unresolved,
|
|
1381
1390
|
executions,
|
|
1382
1391
|
repositories: repositorySetup,
|
|
1392
|
+
autoArchive,
|
|
1383
1393
|
} satisfies SyncResult
|
|
1384
1394
|
}),
|
|
1385
1395
|
}),
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
parseFrontmatter,
|
|
23
23
|
parseFrontmatterSync,
|
|
24
24
|
} from "../workbase/frontmatter"
|
|
25
|
-
import { canTransitionStatus } from "../readiness"
|
|
25
|
+
import { canTransitionStatus, isTerminalStatus } from "../readiness"
|
|
26
|
+
import { autoArchiveTask, type AutoArchiveResult } from "./auto-archive"
|
|
26
27
|
import { documentRevision } from "../workbase/document-revision"
|
|
27
28
|
import { archivedTaskDirectory } from "../workbase/archive"
|
|
28
29
|
import {
|
|
@@ -47,6 +48,7 @@ export interface TaskRecord {
|
|
|
47
48
|
readonly content: string
|
|
48
49
|
readonly revision: string
|
|
49
50
|
readonly data: TaskData
|
|
51
|
+
readonly autoArchive?: AutoArchiveResult
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface CreateTaskInput {
|
|
@@ -684,11 +686,15 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
684
686
|
: { ...record.data, status: validStatus }
|
|
685
687
|
const content = formatMarkdownDocument(data, parsed.body)
|
|
686
688
|
yield* fs.writeFile(record.path, content)
|
|
689
|
+
const autoArchive = isTerminalStatus(data.status)
|
|
690
|
+
? yield* autoArchiveTask(id, startPath)
|
|
691
|
+
: undefined
|
|
687
692
|
return {
|
|
688
693
|
...record,
|
|
689
694
|
content,
|
|
690
695
|
revision: documentRevision(content),
|
|
691
696
|
data,
|
|
697
|
+
...(autoArchive ? { autoArchive } : {}),
|
|
692
698
|
} satisfies TaskRecord
|
|
693
699
|
}),
|
|
694
700
|
}),
|
|
@@ -416,6 +416,19 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
416
416
|
return { root, config: decoded.value }
|
|
417
417
|
}),
|
|
418
418
|
|
|
419
|
+
setAutoArchive: (enabled: boolean, startPath: string = process.cwd()) =>
|
|
420
|
+
Effect.gen(function* () {
|
|
421
|
+
const service = yield* WorkbaseService
|
|
422
|
+
const fs = yield* FileSystemService
|
|
423
|
+
const { root, config } = yield* service.loadConfig(startPath)
|
|
424
|
+
const updated = { ...config, autoArchive: enabled }
|
|
425
|
+
yield* fs.writeFile(
|
|
426
|
+
join(root, "agency.json"),
|
|
427
|
+
`${JSON.stringify(updated, null, 2)}\n`,
|
|
428
|
+
)
|
|
429
|
+
return { root, autoArchive: enabled }
|
|
430
|
+
}),
|
|
431
|
+
|
|
419
432
|
repositoryAliases: (startPath: string = process.cwd()) =>
|
|
420
433
|
Effect.gen(function* () {
|
|
421
434
|
const service = yield* WorkbaseService
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type {
|
|
3
|
+
GitVersionControlService,
|
|
4
|
+
VersionControlService,
|
|
5
|
+
} from "./VersionControlService"
|
|
6
|
+
import type { ArchiveService } from "./ArchiveService"
|
|
7
|
+
import { WorkbaseService } from "./WorkbaseService"
|
|
8
|
+
import type { FileSystemService } from "./FileSystemService"
|
|
9
|
+
import { TaskService } from "./TaskService"
|
|
10
|
+
import { PhaseService } from "./PhaseService"
|
|
11
|
+
import type { EpicService } from "./EpicService"
|
|
12
|
+
import type { WorktreeService } from "./WorktreeService"
|
|
13
|
+
import { aggregateProgress, isTerminalStatus } from "../readiness"
|
|
14
|
+
|
|
15
|
+
export interface AutoArchiveResult {
|
|
16
|
+
readonly taskId: string
|
|
17
|
+
readonly status: "disabled" | "non-terminal" | "archived" | "skipped"
|
|
18
|
+
readonly path?: string
|
|
19
|
+
readonly reason?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A terminal transition is durable even when archive safety checks reject cleanup.
|
|
23
|
+
// Explicit typing also keeps the lazy service references free of inference cycles.
|
|
24
|
+
export const autoArchiveTask = (
|
|
25
|
+
taskId: string,
|
|
26
|
+
startPath: string,
|
|
27
|
+
): Effect.Effect<
|
|
28
|
+
AutoArchiveResult,
|
|
29
|
+
never,
|
|
30
|
+
| WorkbaseService
|
|
31
|
+
| ArchiveService
|
|
32
|
+
| FileSystemService
|
|
33
|
+
| TaskService
|
|
34
|
+
| PhaseService
|
|
35
|
+
| EpicService
|
|
36
|
+
| WorktreeService
|
|
37
|
+
| GitVersionControlService
|
|
38
|
+
| VersionControlService
|
|
39
|
+
> =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const workbase = yield* WorkbaseService
|
|
42
|
+
const { root, config } = yield* workbase.loadConfig(startPath)
|
|
43
|
+
if (!config.autoArchive) return { taskId, status: "disabled" as const }
|
|
44
|
+
const tasks = yield* TaskService
|
|
45
|
+
const task = yield* tasks.show(taskId, root)
|
|
46
|
+
let status
|
|
47
|
+
if ("phases" in task.data) {
|
|
48
|
+
const phases = yield* PhaseService
|
|
49
|
+
const records = yield* Effect.forEach(task.data.phases, (phase) =>
|
|
50
|
+
phases.show(taskId, phase.id, root),
|
|
51
|
+
)
|
|
52
|
+
status = aggregateProgress(
|
|
53
|
+
records.map((phase) => phase.data.status),
|
|
54
|
+
).status
|
|
55
|
+
} else status = task.data.status
|
|
56
|
+
if (!isTerminalStatus(status))
|
|
57
|
+
return { taskId, status: "non-terminal" as const }
|
|
58
|
+
// ArchiveService uses TaskService for preflight; resolve it only after the
|
|
59
|
+
// terminal write to avoid an eager module initialization cycle.
|
|
60
|
+
const { ArchiveService } = yield* Effect.promise(
|
|
61
|
+
() => import("./ArchiveService"),
|
|
62
|
+
)
|
|
63
|
+
const archives = yield* ArchiveService
|
|
64
|
+
// archiveTask always performs the same preflight as --dry-run before applying.
|
|
65
|
+
const result = yield* archives.archiveTask(taskId, root)
|
|
66
|
+
return { taskId, status: "archived" as const, path: result.path }
|
|
67
|
+
}).pipe(
|
|
68
|
+
Effect.catchAll((error) =>
|
|
69
|
+
Effect.succeed({
|
|
70
|
+
taskId,
|
|
71
|
+
status: "skipped" as const,
|
|
72
|
+
reason: error.message,
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
export const autoArchiveMessage = (result: AutoArchiveResult | undefined) =>
|
|
78
|
+
result?.status === "archived"
|
|
79
|
+
? `Auto-archived task '${result.taskId}' to ${result.path}`
|
|
80
|
+
: result?.status === "skipped"
|
|
81
|
+
? `Auto-archive skipped for task '${result.taskId}': ${result.reason}`
|
|
82
|
+
: undefined
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -187,6 +187,16 @@ agent from an active agent session; creating a pull request; archiving, restorin
|
|
|
187
187
|
dropping, reopening, finishing a review, or completing work without a pull
|
|
188
188
|
request; or using `--force` to override readiness.
|
|
189
189
|
|
|
190
|
+
The workbase's `autoArchive` setting defaults to off. Enabling it with
|
|
191
|
+
`agency config auto-archive on` is standing consent for automatic task archiving
|
|
192
|
+
on terminal transitions, including merged-PR sync and explicit non-PR completion.
|
|
193
|
+
Show or disable it with `agency config auto-archive [off]`; context/status JSON
|
|
194
|
+
reports its effective value. Multi-phase tasks wait until every phase is terminal.
|
|
195
|
+
Archive preflight safeguards still apply: inspect the triggering command's
|
|
196
|
+
`autoArchive` result for skips, resolve their reasons, and never force cleanup.
|
|
197
|
+
Successful auto-archive can remove the current task directory and clean checkouts;
|
|
198
|
+
continue lifecycle commands from the workbase root rather than a removed path.
|
|
199
|
+
|
|
190
200
|
## Investigation Handoffs
|
|
191
201
|
|
|
192
202
|
An explicit request for a new, separate, or follow-up item overrides reuse of
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -103,6 +103,7 @@ const DeliveryProvider = Schema.Struct({
|
|
|
103
103
|
export const WorkbaseConfig = Schema.Struct({
|
|
104
104
|
version: Schema.Literal(2),
|
|
105
105
|
vcs: Schema.optional(Schema.Literal("git")),
|
|
106
|
+
autoArchive: Schema.optional(Schema.Boolean),
|
|
106
107
|
repositories: Schema.optional(
|
|
107
108
|
Schema.Record({ key: RepositoryAlias, value: RepositoryDeclaration }),
|
|
108
109
|
),
|