@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
package/src/act-schema.ts CHANGED
@@ -64,6 +64,7 @@ export const ActDiscovery = Schema.Struct({
64
64
  }),
65
65
  workbase: Schema.Struct({
66
66
  root: Schema.String,
67
+ autoArchive: Schema.optional(Schema.Boolean),
67
68
  repositories: Argv,
68
69
  actions: Schema.Array(Action),
69
70
  }),
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
  {
@@ -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
+ `
@@ -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": {
@@ -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(
@@ -34,7 +34,13 @@ export const status = (options: StatusOptions = {}) =>
34
34
  pr: options.pr,
35
35
  })).executionRows
36
36
  : []
37
- const data = { ...report, repositories: repos, work: executionRows }
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))
@@ -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
  )) {
@@ -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
@@ -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
@@ -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
  ),