workspace-mgr 0.7.2

Fixed-policy repository workspace manager for coding agents
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
# Command reference

This reference describes the public CLI. Run `workspace-mgr --help` or
`workspace-mgr <command> --help` for the same syntax at the installed version.
The [user guide](guide.md) explains how the commands form one workflow.

## Conventions

- Repository paths are relative to the Git root, even when a command is run
  from a task directory. `/` is their only separator: on the supported Linux
  and macOS targets a backslash is an ordinary file-name character that is
  never rewritten, so typed paths, Git paths, and storage metadata compare
  exactly. The established storage path convention excludes backslashes, so an S3
  boundary path may not contain one. Automatic placement, explicit
  `storage set --to s3`, and a `storage reset` whose automatic policy selects
  S3 refuse such a path before any metadata is written, and `move` refuses it
  as a destination for content already in S3; rename the path, or keep it in
  Git with `storage set --to git`. Storage metadata that an earlier release
  left at such a path remains excluded from normal operations, so `plan`, `publish`,
  `storage hydrate`, `storage set`, and `untrack` refuse it with status 2 and
  a `workspace-mgr move` recovery hint.
- `--repo <path>` selects the starting repository or task path and defaults to
  the current directory.
- Task-scoped commands discover `.workspace-mgr-task.toml` from the starting
  path for deliverable tasks. Infrastructure tasks always require their private
  manifest selected explicitly with `--manifest <path>`.
- `--include <path>` records a user-authorized one-invocation scope and requires
  a one-line `--scope-note <reason>`. It does not create authorization. Repeat
  `--include` for multiple paths.
- `--dry-run` previews local mutation for commands that support it. Task discard
  also saves a private revision-bound confirmation plan.
- Human output is concise YAML, except Markdown from `instructions`, TOML
  from `config show`, the compact table from `task list`, and bare paths from
  `task path` or `task list --paths`. Use global `--format json` or set
  `WORKSPACE_MGR_FORMAT=json` for stable structured output.
- Errors exit with status 2 and start with `workspace-mgr:`.
- The offline discovery commands `task list`, `task path`, and `task show`,
  help output, and argument errors skip the update check. Other invocations,
  including `--version`, perform a best-effort cached update check. A newer
  applicable release produces exactly one `workspace-mgr: update available`
  line on stderr; stdout, structured output, and command exit status are
  unchanged. The CLI never updates itself. Agents report the versions and ask
  the user before updating, then run `workspace-mgr setup`; scaffold changes are
  reconciled with `workspace-mgr init` in an infrastructure task.
- A repository whose `.workspace-mgr.toml` declares a `minimum_cli_version`
  that the installed CLI does not meet is refused, with status 2, by every
  command that reads the repository configuration, including `instructions`;
  only `doctor` still runs and reports it. The error names the required
  version, where it was declared, and the installed version, and tells the
  agent to report both versions to the user and ask before updating with
  `cargo install --locked workspace-mgr`, followed by `workspace-mgr setup`.
  Commands that fetch apply the same check to what they fetched before they
  change anything: `task create` and `task discard` to the base branch,
  `task rename`, `plan`, and `publish` to the base branch and the task branch,
  and `refresh` to the incoming revision; their errors name
  `.workspace-mgr.toml on <remote>/<branch>`. A pre-release meets a
  declaration of its own release. The
  [configuration reference]configuration.md describes the declaration.
- While a task is waiting for the user's cloud-usage decision, `storage
  status`, `storage set`, `storage reset`, `storage hydrate`, `move`, `remove`,
  `untrack`, `task rename`, and `task discard` print one `workspace-mgr: task
  <id> is waiting for the user's cloud-usage decision` line on stderr as soon as
  the task is resolved. It is a reminder that task work stops until the user
  answers, not an error; stdout, structured output, and exit status are
  unchanged. It repeats the projection last measured by `plan` or `publish`, so
  it does not interrupt carrying out an answer the user already gave; `plan`
  re-measures afterward. The line disappears once the approval recorded in the
  task manifest covers the pending projection or a later `plan` or `publish`
  measures the task within its limit.

## `workspace-mgr setup`

Verify Git and the built-in native storage engine.

```text
workspace-mgr setup [--runtime-dir <path>] [--dry-run]
```

No separate runtime or package download is needed. `--runtime-dir` remains an
accepted compatibility option and has no filesystem effect, including when it
names an existing user directory. Setup leaves former runtimes unchanged.

## `workspace-mgr init`

Initialize a repository or reconcile its managed scaffolding.

```text
workspace-mgr init [--repo <path>]
  [--s3-url <url> [--s3-endpoint-url <url>]]
  [--dry-run]
```

`--s3-url` must use `s3://` and is a tracked, non-secret storage location; userinfo,
queries, fragments, and other credential-bearing URL forms are rejected.
Re-running `init` validates public configuration and deterministically repairs
or upgrades product-owned scaffolding. Ownership is established by the
initialized repository and reserved path, not inferred from file content, so
old, edited, or damaged `AGENTS.md` and internal storage configuration are
replaced with their current deterministic forms, as are the private engine's
ignore files. Before the first successful initialization, an existing
`AGENTS.md`, root `.gitignore`, or private internal-storage scaffold is instead
an atomic collision that the caller must move or remove explicitly; for the
root `.gitignore` the message says where its rules belong.

The root `.gitignore` is the one reserved path a repository is likely to have
arranged for itself long before workspace-mgr existed, so the product owns it
only once it wrote it, which the generated first line records. A root
`.gitignore` that the product did not generate is never reconciled over: `init`
and `doctor` both refuse it and say to move this repository's own rules into
`.workspace-mgr/repository.gitignore`, remove the root file, and run `init`
again. Every repository initialized by an earlier release performs that
migration once; see the upgrade note in the guide. Below the generated header
the file is product-owned like the others, so a hand edit there is drift that
`init` repairs.

The generated root `.gitignore` is the product's fixed rules, followed by this
repository's own rules imported verbatim from
`.workspace-mgr/repository.gitignore`, followed by any
`# workspace-mgr local begin` blocks the root file already holds. The fixed
rules are written in these groups:

- private workspace-mgr state: `/.workspace-mgr/local/`;
- operating-system metadata: `.DS_Store`, `._*`, `.AppleDouble`,
  `.LSOverride`, `__MACOSX/`, `Thumbs.db`, `ehthumbs.db`, `[Dd]esktop.ini`,
  `.directory`, `.fuse_hidden*`, `.Trash-*`, `.nfs*`;
- editor swap, backup, and per-user state: `[._]*.sw[a-p]`, `*~`, `\#*\#`,
  `.\#*`, `*.iws`, `.idea/**/workspace.xml`, `.idea/**/shelf`;
- Python bytecode, environments, and tool caches: `__pycache__/`,
  `*.py[codz]`, `*$py.class`, `*.egg-info/`, `.eggs/`, `.venv/`, `venv/`,
  `__pypackages__/`, `.pdm-build/`, `.ipynb_checkpoints/`, `.pytest_cache/`,
  `.mypy_cache/`, `.dmypy.json`, `.ruff_cache/`, `.pytype/`, `.pyre/`,
  `.tox/`, `.nox/`, `.hypothesis/`, `.coverage`, `.coverage.*`, `htmlcov/`,
  `cython_debug/`, `__marimo__/`, `.ropeproject`;
- JavaScript dependencies, caches, and framework output: `node_modules/`,
  `.npm/`, `.pnpm-store/`, `npm-debug.log*`, `yarn-debug.log*`,
  `yarn-error.log*`, `.eslintcache`, `.stylelintcache`, `*.tsbuildinfo`,
  `.parcel-cache/`, `.next/`, `.nuxt/`, `.svelte-kit/`, `.vite/`,
  `.node_repl_history`;
- R, Julia, and Rust session and tool by-products: `.Rhistory`,
  `.Rapp.history`, `.RDataTmp`, `.Rproj.user/`, `*.jl.cov`, `*.jl.*.cov`,
  `*.jl.mem`, `*.jl.*.mem`, `**/*.rs.bk`, `rustc-ice-*.txt`;
- credentials and private runtime configuration: `.env`, `.env.*`,
  `!.env.example`, `.Renviron`, `.httr-oauth`, `.pypirc`,
  `.streamlit/secrets.toml`.

The set draws on GitHub's common ignore templates but is curated rather than
their union. The private product-state rule keeps locks, private manifests,
indexes, and pending transaction records out of Git while leaving
`.workspace-mgr/repository.gitignore` and
`.workspace-mgr/instructions/repository.md` trackable. Apart from that rule and
the credentials group, a fixed rule covers output a tool regenerates under a
name that cannot plausibly be retained content. Names
that are as often retained data as build output — `target/`, `build/`,
`dist/`, `lib/`, `out/`, `docs/`, `*.log`, `coverage`, `.RData`, knitr's
`*_cache/`, and Julia's `Manifest.toml` — are left to a task's own
`.gitignore` or to `.workspace-mgr/repository.gitignore`.
The module is optional, repository-owned, and limited to 64 KiB of UTF-8; an
absent or empty module produces no import section. It carries ignore patterns
only: those two block markers belong to `untrack`, and a module containing one
is refused, because regeneration harvests them back out of the file it writes
and the file would never settle. Regeneration preserves a well-formed block
byte for byte. `untrack` writes its block into the ignore file of the path's
own directory, which today is always inside a task, so a block in the root file
is a state this format supports rather than one a command produces; a marker
without its partner has no readable extent, so regeneration drops it and the
reported action names the marker it dropped. `doctor` reports a hand-edited
root file through its `repository-scaffold` check. `init` refuses to change the S3
location while retained S3 boundaries exist. It keeps an existing
`minimum_cli_version` exactly and never adds one. It never contacts or writes a
remote. The generated `AGENTS.md` includes an approval-gated command that
installs the latest stable release from crates.io, followed by `setup` and an
instructions retry, so a new machine can bootstrap without inventing a
lower-level workflow.

```sh
workspace-mgr init
workspace-mgr init \
  --s3-url s3://example-bucket/workspace
workspace-mgr init --dry-run
```

## `workspace-mgr instructions`

Render the shared workspace model and effective agent policy.

```text
workspace-mgr instructions [all|model|core|task|publish|artifacts|storage|shared-checkout|infrastructure|repository]
  [--repo <path>]
```

With no topic, `all` is used. Both default and explicit `all` return the short
mental model, operation directory, session-wide constraints and current
repository control facts. They do not concatenate operation-specific sections.
`model` returns only the short conceptual document. Other existing topics remain
available as detailed on-demand compatibility views with the same applicable
repository policies. The relevant command's `--help` is the primary operation
entrypoint; execution output contains outcome-specific guidance.

When `.workspace-mgr/instructions/repository.md` exists, default output indexes
it and requires reading it before task work. `instructions repository`
reproduces the user-owned module; it is not silently dropped. Its current bytes
still affect `all`'s policy hash. Every response includes CLI version, product
policy version, topic and hash. Product-owned default wording remains compact
regardless of that module's length.

```sh
workspace-mgr instructions
workspace-mgr instructions model
workspace-mgr instructions storage
workspace-mgr --format json instructions publish
```

## `workspace-mgr doctor`

Diagnose the repository configuration, product-owned scaffold, Git state, and
required private execution engines.

```text
workspace-mgr doctor [--repo <path>]
```

The command is read-only. When S3 is configured it reads the bucket-versioning
setting and rejects a bucket that is not enabled. It exits with status 2 if any
reported check is not healthy.

Whenever `.workspace-mgr.toml` is readable, the `cli-version` check follows
`repository-config`. It compares the installed CLI with the higher of the
checkout's `minimum_cli_version` and the declaration committed at the base
branch's remote-tracking ref, `refs/remotes/<remote>/<branch>`, as last
fetched; doctor itself never fetches. Its detail is `installed <version>,
repository requires <version>` for the checkout's declaration, `installed
<version>, <remote>/<branch> requires <version>` when the fetched base branch
requires more, or `installed <version>, repository declares no minimum
version`. The check is `ok` when the installed CLI meets that requirement and
`error` when it does not, for example in a checkout that still has to be
refreshed after the base branch was raised. Unlike other commands, doctor does
not refuse such a repository: it reads the declaration even when the rest of
the file uses fields this CLI does not know, which `repository-config` then
reports as an error.

## `workspace-mgr config show`

Parse, validate, and print `.workspace-mgr.toml`.

```text
workspace-mgr config show [--repo <path>]
```

Human output is TOML. JSON output exposes the public configuration model and
does not expose private engine configuration or credentials.

## `workspace-mgr task list`

List tasks in the current local repository, including directories already
grouped under time folders.

```text
workspace-mgr task list [<query>]
  [--kind deliverable|infrastructure]
  [--placement top-level|nested|repository]
  [--paths] [--repo <path>]
```

The default human output is a compact table of kind, metadata, placement,
current name, path, and title. The optional query matches a
case-insensitive substring of the immutable task ID, current basename, current
slug, title, or repository-relative path. Filters combine with the query.
Placement describes the current location: `top-level` deliverables are directly
under the repository root, `nested` deliverables are below another directory,
and `repository` infrastructure tasks have private metadata and no task
directory. Nested placement alone says nothing about completion or review.

Discovery walks the current filesystem, including ignored and untracked task
directories. It stops at each task root and does not follow symbolic links.
A root with a valid current task manifest remains a known task even if it has
its own Git controls. Other nested Git checkouts are excluded before legacy
candidate detection. Timestamped directories that have neither a task manifest
nor a nested Git checkout appear as legacy candidates; their presence does not
establish ownership or make them eligible for adoption or archive. The `metadata` field
is `managed`, `legacy`, or `invalid`. Malformed current metadata appears as
invalid with a diagnostic, so an unreadable manifest does
not silently remove the task from the list. Private infrastructure metadata is
read in place, including its previous private-state location before migration.
Discovery does not migrate or repair it.

`--paths` prints only repository-relative deliverable paths, one per line,
without a table or headings. Infrastructure entries are omitted. If the
selected deliverable metadata is invalid, the command refuses instead of
emitting an incomplete path list. With global `--format json`, normal listing
returns `{repo, tasks, warnings}` and `--paths` returns a string array.
With `--paths`, discovery warnings go to stderr in both formats, leaving stdout
as the path list or JSON array.

These discovery commands are read-only and offline: they do not fetch, query
GitHub or S3, run the update check, write a cache, or migrate private state.
Local archive receipt status is reported as `archive_status`; it is not a live
check that the task's PR merged or that S3 publication or cleanup finished.

```sh
workspace-mgr task list
workspace-mgr task list model --kind deliverable
workspace-mgr task list --placement nested
workspace-mgr task list --kind deliverable --paths
workspace-mgr --format json task list --repo /path/to/repository
```

## `workspace-mgr task path`

Resolve one task to its current deliverable directory.

```text
workspace-mgr task path <selector> [--relative] [--repo <path>]
```

The selector must exactly match the immutable task ID, current basename,
current slug, repository-relative directory path, or absolute directory path.
Matching does not use fuzzy search, an old renamed slug, or a latest-task
fallback. If more than one task matches, the command exits with status 2 and
lists candidates; choose an ID or current path that identifies one task.
Malformed current metadata also refuses resolution.

Human output is one bare absolute path. `--relative` returns the current path
relative to the repository root, even when the command starts in a task
directory. Global `--format json` returns `{repo, id, path}` and applies the
same `--relative` choice to `path`. Infrastructure tasks have no deliverable
directory, so `task path` refuses them; use `task show` to obtain their manifest.
When capturing the path in a shell command, pass `--format human` explicitly
so a `WORKSPACE_MGR_FORMAT=json` environment setting cannot change the output.

```sh
cd "$(workspace-mgr --format human task path example-task)"
workspace-mgr task path 20261007-120000-example-task --relative
workspace-mgr task path 2026/10/20261007-120000-example-task
```

## `workspace-mgr task show`

Inspect one task's current local identity and metadata.

```text
workspace-mgr task show <selector> [--repo <path>]
```

Selection follows the exact matching and ambiguity rules of `task path`, and
also supports infrastructure task identity. Human output is concise YAML;
global `--format json` returns `{repo, task}`. The task includes its immutable
ID, current name, slug, title, purpose, branch, scopes, placement, and absolute
manifest path. Deliverable directory paths are repository-relative;
infrastructure tasks have no deliverable directory. Legacy candidates remain
explicitly marked as candidates, and malformed current metadata refuses.

The output describes the current filesystem and local receipt. It grants no
write scope and does not establish completion or live review status. Use
`task status` for resolved task-scoped status, `plan` for publication
assessment, and `archive --dry-run` for current archive eligibility.

```sh
workspace-mgr task show example-task
workspace-mgr --format json task show 20261007-120000-example-task
```

## `workspace-mgr task create`

Create one deliverable workspace or repository-infrastructure workspace.

```text
workspace-mgr task create <slug> --title <title> --purpose <purpose>
  [--kind deliverable|infrastructure]
  [--scope <path>... --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

The slug is lowercase kebab case. The default `deliverable` kind creates a
timestamped top-level directory, README, tracked manifest, and the unmounted
target branch `codex/<slug>`. The scaffolded README's directory map tells the
task to keep its tools, process, decisions, and hard-to-reproduce results in
that directory and to list them there; which files carry them is the agent's
choice. The `infrastructure` kind requires at least one `--scope` plus a
`--scope-note`; it creates the unmounted branch `codex/infra-<slug>` and a
private manifest below the primary checkout's `.workspace-mgr/local/`, with no
repository task directory or separate worktree. Both kinds work in the shared
checkout on the configured main branch. Infrastructure creation reports `path` as the repository root and
`manifest` as an absolute path; pass that path using `--manifest` to subsequent
task-scoped commands. The shared HEAD must equal the fetched base revision;
run `refresh` before creation if it is behind. Infrastructure planning and
publication also require shared HEAD to match the fetched base, so an upstream
change cannot be overwritten from stale local files; refresh before retrying.
Every infrastructure scope is
explicit. Both kinds fetch the configured base branch, reject an existing
directory or local/remote branch, and publish nothing. Before creating a branch,
directory, or manifest, they refuse a base branch whose `minimum_cli_version`
the installed CLI does not meet. `--dry-run` reads the remote base branch without
moving any ref and
fetches its commit only when it is not available locally.

The report contains a structured `review` handoff. Deliverable creation reports
`creation_timing: immediate-after-scaffold-publication`; the agent must
immediately plan and publish the initial scaffold, then create and verify the
one draft pull request. Infrastructure creation reports
`creation_timing: after-first-scoped-publication`. Both report
`synchronization_cadence: before-every-turn-end`, requiring the agent to
reconcile the task, remote branch, and pull request automatically before each
writable-task turn ends.

```sh
workspace-mgr task create training-report \
  --title "Training report" \
  --purpose "Produce the final training report"
workspace-mgr task create urgent-fix --title "Urgent fix" \
  --purpose "Repair the release input" --dry-run
workspace-mgr task create shared-policy --kind infrastructure \
  --title "Shared policy" --purpose "Update repository-wide policy" \
  --scope AGENTS.md --scope .github/workflows/ci.yml \
  --scope-note "The user requested this infrastructure change"
task_manifest=/absolute/path/reported/by/task-create
workspace-mgr plan --manifest "$task_manifest"
workspace-mgr publish --manifest "$task_manifest" -m "Publish shared policy"
```

## `workspace-mgr task rename`

Change the current human-readable slug without replacing the task, target
branch, or pull request.

```text
workspace-mgr task rename <new-slug>
  [--repo <path>] [--manifest <path>] [--dry-run]
```

The new slug uses the same lowercase ASCII kebab-case validation as creation.
For a deliverable, the command preserves the timestamp and moves the entire
task directory from `<timestamp>-<old-slug>` to
`<timestamp>-<new-slug>`. Its README, retained content, S3 pointers, placement
sidecars, and manifest move together. The manifest is atomically rewritten with
the new current slug and path; it keeps every other field, including a
cloud-usage approval and archive completion checkpoint. It writes the lowest
schema representing those fields: schema 2, schema 3 with an approval, or
schema 4 with a checkpoint. Infrastructure tasks keep
their identity-owned private manifest path and update only the private current
slug metadata.

The task ID and target branch are immutable. Keeping the branch stable lets the
agent reuse the one existing draft pull request; renaming an open pull request's
head branch can close it on hosting providers. The report tells the agent to
update that pull request's title and description after publication.

Rename fetches the shared and task refs to reject merged tasks, changed remote
identity, published destination collisions, local destination collisions, and
staged source/destination changes. Before it moves or rewrites anything, also
with `--dry-run`, it refuses when the installed CLI does not meet the
`minimum_cli_version` of the fetched shared branch or task branch. It writes no
Git or S3 remote. On a published
deliverable, the next normal `plan` includes the published old path as an
identity-derived cleanup scope, preserves published Git/S3 placement at the new
path, and `publish` deletes the old tree while advancing the same branch.
Because version-aware S3 IDs are bound to object paths, rename clears those old
bindings from moved pointers; publish creates and verifies new object versions
at the new path, publishes Git, then permanently deletes every version at the
old path unless another current remote branch or tag still references it.

```sh
workspace-mgr task rename current-research-question --dry-run
workspace-mgr task rename current-research-question
workspace-mgr plan
workspace-mgr publish -m "Rename the task for its current topic"
```

A successful deliverable directory move reports a `notices` entry with code
`manual-content-audit-after-relocation`, reminding the user to manually inspect
and repair affected links or path references. The tool neither checks nor
repairs those payload dependencies. Dry-run, no-change and infrastructure
metadata-only rename do not emit this success notice.

## `workspace-mgr task upgrade`

Upgrade supported current task configuration without moving content or writing
a remote.

```text
workspace-mgr task upgrade
  [--repo <path>] [--manifest <path>] [--dry-run]
```

Without `--manifest`, discover the current deliverable task from `--repo` or the
working directory. Select another current task explicitly with `--manifest`.
Current manifest loading is strict: unknown fields or unsupported schemas are
not guessed. Upgrade preserves identity, scopes, cloud-usage approval and
compatible saved review metadata, including existing schema 4 fields.

The operation is local and idempotent. It fetches the configured shared branch
and validates current published manifest identity, staged manifest state and
client compatibility. It does not query PRs, inspect historical configuration,
compare ordinary task directory trees or require ordinary staged, modified or
untracked payloads to be clean.
`--dry-run` previews the same current-metadata change without rewriting it.
Reports retain `previous_schema_version`, `schema_version`,
`completion_recorded` and `remote_writes: false`; `completion_recorded` indicates
compatible saved metadata, not verified task-content history or live eligibility.
No new completion checkpoint is synthesized. A changed current manifest must
still be published within its authorized scope.

Archive is independent of upgrade: it checks supported current metadata and
current associated PR closure, not old directory history or saved proof.

## `workspace-mgr archive`

Organize completed deliverable task directories through a user-requested
repository-infrastructure task.

```text
workspace-mgr archive [<task-path> ...]
  [--layout <template>] [--repo <path>] [--manifest <path>] [--dry-run]
workspace-mgr archive [<source-or-destination> ...]
  --cancel --manifest <owning-infrastructure-manifest> [--dry-run]
```

With no paths, inspect top-level deliverable directories and skip tasks without
a corresponding closed pull request. An explicitly named task whose PR is
open, missing, or unverifiable is refused. Archive reads only the
current task configuration for task identity, branch, and any saved associated
review branches, then verifies corresponding PR state in the
configured repository through `gh`. The PR need not target today's configured
base branch. An associated open PR blocks archive, including when the task's
current branch has changed.
Both merged PRs and PRs closed without merging qualify; an open PR does not.
Current configuration remains strictly validated.

Archive does not inspect historical configuration, directory-tree history,
commit-to-PR associations, or historical checkpoint proofs. Saved review
metadata supplies branch lookup hints only; its historical tree and ancestry
checks are not replayed. If there is no matching PR, a pre-0.7 adoption record
can provide a current branch hint; malformed or unrelated records add no
extra refusal. Archive needs a verifiable closed PR and no associated open PR.
It does not require earlier commits to have reviewed PRs, full Git history,
or retained branch tips to equal or descend from reviewed heads. Changes to
ordinary task contents do not determine eligibility. Active task directories
stay at the top level,
and merge or turn-end synchronization never runs archive automatically.

`--layout` uses `{year}` and `{month}` from each task directory's creation
timestamp. The default is `{year}/{month}`; `{year}` and `{year}{month}` also
work. The rendered relative path must include the year and must not target
hidden repository-control directories. The task directory's basename, retained
contents, immutable ID, and target branch are preserved.

Run `--dry-run` from the shared checkout or an infrastructure task to inspect
`tasks`, `skipped`, and `required_scopes`. It reads current PR state and
versioned S3 history but changes no repository content or remote. Applying
archive requires an infrastructure task with both source and destination paths
declared. The command moves complete local directories, updates manifest paths,
and writes `.workspace-mgr-archive.json` migration receipts. It writes neither
Git nor S3 remotes. It refuses destination collisions, invalid current task
metadata, inconsistent managed-storage pointers or hashes, and unavailable
referenced S3 generations. It validates supported pointer hashes and complete
directory metadata, verifies hydrated payload hashes, and checks exact S3
versions for unhydrated payloads. Ordinary tracked, staged, untracked,
ignored, and local-only contents move with the directory; unpublished content
alone does not prevent archive. The shared Git index is left unchanged.

Archive preserves ordinary local contents byte for byte, including scripts,
README commands, historical logs, ignored caches, symlinks, and Python
environments.
It does not scan runtime paths or cross-task dependencies, run reproduction
commands, rebuild environments, or rewrite their references. A script that
depends on the former directory may need separate maintenance after moving;
that does not prevent archive.

Every nested Git repository must be ignored by the outer repository's shared
ignore rules and contain no outer-tracked files or gitlinks. Use a repository
or task-local `.gitignore` rule covering the whole nested directory, for example
`vendor/tool/`, and ensure the rule still covers its archived destination.
Local `.git/info/exclude` or a global ignore file is insufficient because
another clone must enforce the same boundary. Archive refuses a nested
repository that violates this rule before moving. Ignored nested repositories
move unchanged: Git pointer files, registrations and external administrative
files are neither parsed for relocation nor repaired. This may leave their
old references unusable. Zero-byte `.git` cache markers are ordinary content
and do not establish a nested repository.

The rule need not already be tracked: a new task-local `.gitignore` can travel
with the task and be carried by its publication. `plan` and `publish` enforce
the same nested-repository boundary before evaluating storage placement in
the selected directory scopes; they also verify that the publication carries
its ignore rules.

The normal `plan` and `publish` flow handles the migration. Publication copies
every retained data version and delete marker under the source task prefix,
including superseded versions and retired paths absent from current storage
pointers. It verifies destination versions, rewrites standalone and directory
managed-storage cloud metadata automatically, and publishes the archive
registry before publishing Git. Content hashes and file sizes stay fixed;
copied versions and recreated markers receive new native IDs and timestamps,
which the receipt maps to their originals. Copying history is charged to the
infrastructure task's cloud-usage projection, so its full retained history must
fit that task's approved limit before migration starts.

Before the copied receipt merges into the configured shared branch, current
remote branches or tags containing the source directory defer cleanup,
including legacy trees without manifests. Once that receipt is merged, old
branches and tags can read exact mapped versions through the registry; they
remain intact and no longer require duplicate history at the original path.
New referenced generations without mappings remain protected. Retirement is
complete only after a full version-history scan finds neither data versions nor delete markers under
the original prefix. `storage.purge.status: cleanup_pending` reports protected
history or a prefix waiting for its copied receipt to reach the shared branch.
Even an archive whose original version inventory is empty keeps a durable
`pending_prefixes` intent until a complete version-and-marker scan confirms
that the old prefix is empty.
`blocked_unmapped` reports concurrent versions without a verified
mapping. Both preserve retry records and report a warning. Git's `pushed` or
`updated` status does not mean storage retirement is complete. Concurrent
unmapped or foreign data is preserved and blocks completion; it is never
silently erased or forgotten from the retry queue. A failed publication
preserves source history and retry journals. Historical Git checkouts use
`workspace-mgr storage hydrate` to resolve the durable registry and verify their
original content hashes after source cleanup. Reading old pointers directly with the underlying storage engine cannot
resolve the changed keys and version IDs.

Archive publication requires `minimum_cli_version = "0.7.0"` regardless of
the task manifest schema, including archives with an empty S3 inventory.
The CLI reconciles this declaration in its private publication index before
upload. Private S3 purge queues and archive copy journals use schema 2; 0.6.0
rejects them before deletion rather than ignoring newer protection fields.
The new CLI can read legacy schema 1 private state, but a destructive retry
durably upgrades it first. Preview leaves old bytes unchanged. Public copied
receipts and the immutable registry remain schema 1, so exact historical
version mappings keep their data format. Cancellation also upgrades restored
cleanup state; do not remove the repository version declaration or downgrade
private journals to resume with an older CLI.

Before copying, an immutable source reservation under
`refs/tags/workspace-mgr/archive-copy/` chooses one attempt by Git
compare-and-create, so competing publishers cannot both copy into the same
destination. The canonical registry is then bound to the complete copied
receipt by a separate control tag under
`refs/tags/workspace-mgr/archive-registry/` on the configured Git remote.
The reservation binds a normalized planned receipt and its private journal's
attempt nonce; the canonical binding records the complete copied receipt.
Exact remote object ID checks guard mutations. Conflicting receipts or registry
history refuse publication and cleanup.
Neither ownership claim expires or permits takeover. The remote must permit
creating and conditionally deleting both sets of control tags.
For Backblaze B2, the registry writer disables automatic SDK checksum headers
and sends Content-MD5. It first attempts conditional Put; a provider's explicit
not-implemented/not-supported response permits an unconditional fallback only
while the verified Git binding owns this exact receipt. Source history is
retired under the same completion checks as other providers. Same-key copy
generations are spaced by at least one second to preserve B2's current version
ordering. Unknown endpoints keep conditional publication and fail safely if
their provider rejects it.

`--cancel --dry-run` previews a journaled local attempt. Apply restores its
source directory, original manifest, receipt, storage pointer bytes and permissions.
All local payloads, including nested Git controls,
ignored and hydrated content and files added after moving, travel with the
directory. Cancellation changes neither the shared Git index nor another
task's files. A failed publication's generated local tree and retirement queue
are reversed only for the selected archive paths; other scoped work is retained.
Independent metadata/ref edits and destination collisions refuse cancellation
before movement. Repeating cancel is safe, including after interruption.
Existing attempt journals remain readable, including saved relocation
metadata from older attempts; new attempts do not rewrite nested Git controls.

Cancellation verifies that all original source versions and markers remain
intact, withdraws only this attempt's exact registry versions, deletes its
copied data versions and delete markers, and aborts its recorded multipart
uploads. It records their verified absence durably, restores local directories,
metadata, refs and retirement state, removes its empty generated parents, then
releases its exact canonical binding and copy reservation before marking the
attempt cancelled. Preview performs none of these writes. Foreign destination
data, changed original source versions, or a conflicting registry blocks this
initial remote cleanup and preserves a resumable attempt.
Once remote cleanup is durably verified, an interrupted retry can finish local
restoration while preserving any later foreign writes or a newer owner's
claims. Completed cancellation does not require the source generations to
remain after a newer archive retires them. Retries use exact version IDs without
creating new delete markers. A subsequent archive starts a fresh copy transaction
rather than reusing cancelled copies.
After verified Git push, use a reviewed revert. Receipts from an older CLI that
did not save the local attempt journal cannot provide a verified lossless cancel.

Legacy directories without a manifest appear in `skipped` with an adoption
instruction. First adopt explicitly through an infrastructure task scoped to
that directory, selecting its merged PR. Adoption verifies current PR and
branch/ref control association, then creates the manifest and
`.workspace-mgr-legacy.json` review record. It does not compare historical or
current payload trees, inspect earlier content imports or require ordinary task
payloads to be clean. Publish and review that new control metadata through the normal task flow;
its separate adoption PR need not merge before archive. Archive verifies the
current associated PR, and neither inventory nor archive invents legacy ownership.

```sh
workspace-mgr task adopt <legacy-task-path> --pull-request <number> \
  --title "Retained task" --purpose "Retain reviewed work" \
  --manifest "$task_manifest" --dry-run
# Repeat without --dry-run; publish the adopted control metadata through its normal review.
```

Cancel only an archive attempt that has already moved locally and remains
unpublished. Adoption alone does not create such an attempt:

```sh
workspace-mgr archive <source-or-destination> --cancel --manifest "$task_manifest" --dry-run
workspace-mgr archive <source-or-destination> --cancel --manifest "$task_manifest"
```

```sh
workspace-mgr archive --dry-run
workspace-mgr archive --layout '{year}{month}' --dry-run
# Apply in the shared checkout after declaring the reported scopes.
task_manifest=/absolute/path/reported/by/task-create
workspace-mgr archive --manifest "$task_manifest"
workspace-mgr plan --manifest "$task_manifest"
workspace-mgr publish --manifest "$task_manifest" -m "Organize completed tasks"
```

After a successful directory move, `notices` includes
`manual-content-audit-after-relocation`. It requests a manual audit of the moved
payload's links and path references; archive itself never inspects or repairs
them. Preview, cancellation and invocations with no move do not emit the
success-only reminder. This notice is not a runtime precondition or refusal.

## `workspace-mgr task status`

Show the immutable task identity, current slug, manifest, branch, remote, base
branch, scopes, current working changes inside those scopes, and the task's
recorded cloud-usage state.

```text
workspace-mgr task status [--repo <path>] [--manifest <path>]
```

This is a local read-only view. Use `plan` for the complete prospective
publication state. Its `cloud_usage` object reports the effective
`threshold_bytes`, the task's `limit_bytes`, the `approval` recorded in the
task manifest (`limit_bytes` and `note`), and the `pending` decision left in
this clone's private state by the last `plan` or `publish` that measured the
task above its limit; absent values are `null`.

## `workspace-mgr task discard`

Permanently abandon one unmerged task after its pull request is closed or
verified absent by the agent.

```text
workspace-mgr task discard (--dry-run | --confirm <task-id>)
  [--repo <path>] [--manifest <path>]
```

Always run `--dry-run` first. It creates no repository-content or remote change,
but writes a private `discard-plan.json` containing the observed task identity,
local and remote task refs, and local and remote shared refs. Its structured
report includes:

- every working change in the task's declared scopes;
- the deliverable task directory to delete, when present;
- each shared scope to restore from the local shared branch;
- whether the agent must close a pull request or verify that none exists;
- current local or published managed S3 object paths and recorded exact version
  IDs queued for permanent deletion after the branch is removed.

After explicit user authorization, the agent verifies the task is unmerged,
closes the matching pull request if it exists, and verifies that provider state.
Run confirmation from the shared checkout and pass the manifest printed by the
dry run, because the task workspace itself will be deleted:

```sh
workspace-mgr task discard --dry-run
workspace-mgr task discard \
  --manifest /absolute/path/to/.workspace-mgr-task.toml \
  --confirm 20260830-120000-example
```

Both modes fetch the shared branch first and refuse, before writing a plan,
deleting a ref, or purging anything, when the installed CLI does not meet its
`minimum_cli_version`.

Confirmation requires the exact task ID and an unchanged private plan. It
refuses changed refs, a branch with another task identity, a task already
contained in the shared branch, a target branch checked out anywhere, or an
invocation whose current directory would be deleted. It deletes an existing
remote task branch with `force-with-lease`, verifies absence, deletes local and
remote-tracking refs, then removes the local workspace and private task state.
Deliverable scopes are first moved into private quarantine; additional scopes
and their shared-index entries are restored from the local shared branch. A
remote failure restores quarantined paths and their prior index state.
Infrastructure confirmation restores its declared scopes, removes its private
manifest and task state, and never deletes the shared repository directory.

`task discard` does not verify pull-request state itself; its report makes that
agent responsibility explicit. Before branch deletion,
discard queues every versioned S3 object path owned by the task. After the Git
branch is removed, it permanently deletes every version of paths that no
current remote branch or tag still references. Protected paths remain pending
and are retried by a later publish, refresh, or discard.

## `workspace-mgr task approve-cloud-usage`

Record the user's explicit approval of a cloud-usage limit for this task.

```text
workspace-mgr task approve-cloud-usage --limit <size> --note <decision>
  [--allow-non-shared-head --scope-note <reason>]
  [--repo <path>] [--manifest <path>] [--dry-run]
```

The command records a decision the user already made in the task's chat. It does
not create authorization; agents run it only after the user explicitly approves
that limit. It writes only the manifest copy that the task's publications
carry, so it applies the checkout rules of `plan` and `publish`: both kinds run
from the shared checkout on the base branch while their task branch is not
checked out anywhere. Infrastructure approval requires its explicit private
`--manifest` path. In an explicitly authorized alternate workflow, where
the deliverable is published from another checkout head with
`--allow-non-shared-head --scope-note <reason>`, the approval takes the same
override, with the same one-line scope note, and the task branch must still not
be the checkout's head. This override is deliverable-only; infrastructure tasks
always use the configured shared branch. Any other unauthorized checkout is
refused before anything is written, also with `--dry-run`. Every task's default
limit is the fixed 1 GiB (1073741824 bytes) threshold. `--limit` is a byte count
or a number with a decimal unit (`B`, `KB`, `MB`, `GB`, `TB`) or binary unit
(`KiB`, `MiB`, `GiB`, `TiB`), case-insensitive, with or without a space. A
fraction needs a unit and must come to a whole number of bytes; bare `K`, `M`,
`G`, and `T` suffixes are rejected. The limit must be at least the threshold,
because an approval can only raise the limit, and at most 9223372036854775807
bytes, the largest integer the manifest can hold. `--note` is required, must be
one line, and records the user's decision.

The approval is written into the task manifest as a `[cloud_usage_approval]`
table with `limit_bytes` and `note`, which requires at least schema 3; the
[configuration reference](configuration.md#task-manifests) describes the
format. The command rewrites the manifest atomically, validates the result, and
restores the previous manifest if validation fails. It replaces any earlier
approval, and a limit equal to the threshold removes the table. The manifest
returns to schema 2 only when it has no archive completion checkpoint;
otherwise it remains schema 4. `task rename` keeps the approval, and confirmed
`task discard` removes it with the task. The command reads no remote and
reports `remote_writes: false`.

A deliverable manifest lies inside the task directory, so the next publication
carries the change for review. If the published `.workspace-mgr.toml` does not
yet require a release that reads schema 3, that publication also raises its
`minimum_cli_version`, as described under `plan`; a build older than that
release refuses to publish the approval at all. After a reset, the next
publication withdraws a raise that no task manifest in it still needs, but
never below the base branch's declaration. An infrastructure manifest stays
private and never raises it. While the manifest records an approval, every
publication commit carries an audit trailer:

```text
Cloud-Usage-Approval: limit_bytes=<n>; note=<note>
```

The trailer is written for reviewers; `workspace-mgr` never reads it back. For
an infrastructure task it is the only published record of the approval.

The report contains `status`, `operation`, `task_id`, the `manifest` path, the
manifest's resulting `schema_version`, `threshold_bytes`,
`previous_limit_bytes`, `limit_bytes` with its readable `limit`, `note`, the
`pending` decision from the last over-limit `plan` or `publish`, `blocked`,
`remote_writes`, and `next_step`. `status` is `recorded` when the manifest
changed, `dry_run` for a rehearsal, and `unchanged` when the manifest already
records exactly this decision, such as the same approval recorded again or a
reset of a task that has no approval. Nothing is written then, and `next_step`
says that this command changed nothing instead of pointing to a publication;
for a deliverable it adds that `plan` shows whether earlier manifest changes,
such as the same decision recorded before, are still unpublished.
`blocked: true` means the pending projection from that last measurement still
exceeds the new limit, so the approval alone does not unblock the task; if the
user also chose a cleanup, perform it and let `plan` re-measure. The command
works with or without a pending decision, so the user can approve a limit before
large content is produced. Run `plan` afterward to re-measure the task, then
`publish`. `--dry-run` validates and reports without writing the manifest; its
`next_step` says that nothing was recorded and that the command must be rerun
without `--dry-run` once the user has approved the limit.

```sh
workspace-mgr task approve-cloud-usage --limit 1.5GiB \
  --note "The user approved 1.5 GiB for the training checkpoints"
workspace-mgr task approve-cloud-usage --limit 3GiB \
  --note "The user approved 3 GiB in this chat" --dry-run
```

## `workspace-mgr storage status`

Explain effective Git/S3 placement or explicit local-only state.

```text
workspace-mgr storage status [<path> ...]
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>]
```

With paths, status reports those paths, including a descendant inherited from a
directory boundary. The path need not currently be materialized if published
metadata can determine its placement. With no paths, status lists ordinary Git
files and one row per explicit or published S3 boundary in all resolved scopes.
An explicitly queried directory must itself be a selected or published S3
boundary; otherwise it has no single placement because automatic evaluation
operates on its files independently. Query without paths to inspect those files.
Each row includes `target`, `basis`, effective `boundary`, available
`payload_bytes` and `payload_files`, an explicit semantic `reason` when one
exists, and structured `warnings`. It never writes a remote.

`semantic-placement-review` reports an automatic Git size fallback from
1 through 10 MiB: review whether collaboration or artifact semantics warrant an
explicit choice. `small-s3-boundary` reports a materialized S3 boundary below
1 MiB, measured by aggregate regular-file bytes. These are placement advice,
not refusals; an explicit user choice still succeeds at any size.

```sh
workspace-mgr storage status
workspace-mgr storage status 20260829-180000-report/results/model.bin
```

## `workspace-mgr storage set`

Record an explicit Git or S3 placement.

For a local-only path, this explicitly resumes tracking and removes only the
ignore rule owned by `untrack`. User-authored ignore rules are preserved; if
they still prevent tracking, resolve the reported conflict first.

```text
workspace-mgr storage set <path>... --to git|s3 --reason <reason>
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

Every target must exist and remain in the resolved scopes. The reason must be a
non-empty single line. S3 must be configured before selecting it, and an S3
target path may not contain a backslash, which the storage engine reads as a
separator. Setting a directory creates one recursive boundary; nested or
overlapping existing boundaries are rejected. The command updates local desired
state and reports `remote_writes: false`. Explicit S3 below the recommended
1 MiB aggregate boundary size remains valid but reports `small-s3-boundary`;
select Git or a larger meaningful boundary when practical.

```sh
workspace-mgr storage set 20260829-180000-report/report.pdf \
  --to git --reason "Review the report directly"
workspace-mgr storage set 20260829-180000-report/data \
  --to s3 --reason "Retain the dataset as one boundary"
```

## `workspace-mgr storage reset`

Remove an explicit choice and return paths to automatic policy. Published
placement remains sticky: resetting a published S3 boundary keeps it in S3;
use `storage set --to git` for an intentional placement change.
Local-only paths refuse reset: use an explicit `storage set --to git|s3` to
resume tracking.

```text
workspace-mgr storage reset <path>...
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

This may locally convert a prior S3 boundary back to ordinary content. Resetting
a directory removes the directory boundary, so its files can be evaluated
individually by the next `plan` or `publish`; the reset report therefore has no
single placement row for an unpublished directory boundary. No remote is changed.
Because the reset applies automatic policy immediately, resetting a path whose
name contains a backslash into S3 is refused; the explicit choice is rolled back
and kept.

## `workspace-mgr storage hydrate`

Materialize exact S3 content locally without publication.

```text
workspace-mgr storage hydrate [<path> ...]
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

With no paths, every S3 boundary in scope is selected. A descendant path selects
its containing S3 directory boundary. Hydration fetches from S3, checks out the
content, and verifies it. It refuses locally modified outputs and never writes
Git or S3 remotes.

## `workspace-mgr move`

Move a path while preserving its effective placement.

```text
workspace-mgr move <old-path> <new-path>
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

Both paths must remain inside the resolved scopes, the source must exist, and
the destination must not. An S3 boundary exists once its metadata does, so its
payload need not be materialized. A move may stay within a containing directory
boundary or move the boundary itself; it may not cross into or out of another
directory boundary. The command changes local desired state only. A later
`publish` writes and verifies the new S3 object path, publishes the Git revision,
then permanently deletes every version of the old path unless another current
remote branch or tag still references it.

The recorded S3 version belongs to the old object path, so the move discards it.
When the source payload is not materialized, `move` therefore first fetches it
through the old metadata, before it changes anything, and then materializes it
at the destination, where the next publication uploads it under the new path. A
fetch failure leaves everything as it was.

## `workspace-mgr remove`

Delete ordinary Git content, a complete S3 boundary, or a descendant inside an
S3 directory boundary without interpreting an unhydrated output as deletion.

```text
workspace-mgr remove <path>...
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

The command changes local desired state only and refuses to remove an entire
task scope; use confirmed task discard for that. A later `publish` first makes
the deletion authoritative in Git, then permanently deletes every S3 version
at object paths removed by the operation. Current remote branches and tags are
reference guards, so protected objects remain in private pending state until a
later `publish`, `refresh`, or discard can delete them safely.

## `workspace-mgr untrack`

Keep content locally, add a managed ignore rule, and remove its payload from
Git and S3 on the next publication.

```text
workspace-mgr untrack <path>...
  [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

The command preserves local bytes, writes a placement sidecar with `target =
"local"`, adds an anchored literal rule to the parent `.gitignore`, and removes
any S3 pointer for the boundary. The sidecar and ignore rule remain in Git;
the payload does not. It writes no remote and does not modify the shared Git
index. Repeating it is safe and does not duplicate ignore rules. `--dry-run`
previews the change without modifying local files or metadata.

The first conversion requires materialized content. If the S3 output is absent,
run `storage hydrate <path>` first. Local-only placement remains valid on a new
clone where the payload has never existed. A directory is one recursive local
boundary. An ordinary directory must first be selected as a boundary with
`storage set <directory> --to git --reason <reason>`. Nested placement
boundaries, task control files, and entire task
scopes cannot be untracked; operate on a complete existing boundary or first
reorganize its placement. The payload, sidecar, and parent `.gitignore` must
all be inside the authorized scopes.

`plan` reports `storage.local_only`, the Git changes, and retired S3 versions
in `storage.purge.queued`. `publish` first publishes the Git deletion, then
permanently cleans obsolete S3 object versions. Current remote branches and
tags can defer cleanup; for example, `main` protects the previous S3 content
until the deletion is merged. A later `publish` or `refresh` retries pending
cleanup. Git history is not rewritten.

After merge, `refresh` retains existing local-only bytes. Automatic placement,
publication, and hydration do not upload or recreate them. Resume tracking with
`storage set <path> --to git|s3 --reason <reason>`; `storage reset` deliberately
refuses to resume tracking implicitly.

`move` currently refuses local-only boundaries and their descendants; resume
tracking before using managed moves. `remove` can delete a complete local-only
boundary, including its owned ignore rule.

```sh
workspace-mgr untrack 20260829-180000-report/data.bin --dry-run
workspace-mgr untrack 20260829-180000-report/data.bin
workspace-mgr plan
workspace-mgr publish -m "Keep data.bin local only"
```

## `workspace-mgr plan`

Preview the complete task transaction.

```text
workspace-mgr plan [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--allow-non-shared-head --scope-note <reason>]
  [--repo <path>]
```

Plan fetches the configured base and target Git refs, evaluates automatic
placement, validates S3 metadata and local outputs, constructs a private
preview tree that excludes payloads destined for S3, and reports changed paths,
object IDs, and pending placement. Its placement report gives the fixed 1 MiB
recommended S3 minimum and 10 MiB automatic threshold, automatic decisions in
the 1–10 MiB semantic-review band or above the S3 threshold, and existing
boundaries with actionable warnings. Exact generated S3 metadata is established
by `publish`, because `plan` does not rewrite it. Plan may create ignored local
locks, preview state, or private cloud-usage state. It never creates a commit,
uploads S3 content, or pushes a Git branch.

Plan also measures the task's cloud usage and reports it in `cloud_usage`,
directly after `status` and `operation`:

- `status` is `within_limit` when the projected total is at most the limit and
  `approval_required` otherwise; `publish_allowed` says whether `publish` would
  pass the usage gate, and `cleanup_only` whether this publication only removes
  content, apart from at most 1 MiB (1048576 bytes) of new workspace-mgr
  control-file content per publication, where metadata that only drops entries
  is free.
- `threshold_bytes` is the fixed 1 GiB (1073741824 bytes) threshold,
  `limit_bytes` the task's effective limit, and `approval` the approval
  recorded in the task manifest (`limit_bytes` and `note`) or `null`.
- `published` and `projected` each report `git_bytes`,
  `git_uncompressed_bytes`, `git_lfs_bytes`, `s3_bytes`, and `total_bytes`.
- `git_measure`, `headroom_bytes`, `suggested_limit_bytes`, and
  `git_history_exceeds_limit` qualify those totals.
- `contributors` lists up to ten of the largest paths with `store` (`git` or
  `s3`), `bytes`, `versions`, and `state` (`published` or `pending`). Git
  contributor bytes are compressed estimates when `git_measure` is `packed`,
  so they compare with the packed totals, and uncompressed object sizes
  otherwise; both include referenced Git LFS objects.
- `message` explains the decision the task is waiting for and appears only when
  approval is required.

Published usage is what the task already keeps on the remotes: the Git objects
its published branch adds beyond the base branch, plus every S3 object version
its publications retain at paths that still exist, including superseded
versions. Projected usage adds this publication: new Git objects from the
preview tree, and S3 uploads for new automatic placements and for changed or
not-yet-uploaded content, minus S3 paths this publication removes. Git bytes are
uncompressed object sizes plus the sizes of referenced Git LFS objects. When a
total exceeds the limit, Git objects are measured again as packed data, which
is closer to what a hosting provider stores; `git_measure` then reads `packed`,
and `git_uncompressed_bytes` keeps the uncompressed figure. Published Git
history never shrinks, so `git_history_exceeds_limit: true` means only an
approval or task discard can resolve the decision. `suggested_limit_bytes` is a
ceiling with headroom for continued work: the projection plus the larger of 25%
or 256 MiB (268435456 bytes), rounded up to a multiple of 256 MiB, and at least
256 MiB above the current limit when approval is required.

Plan never refuses because of cloud usage. An `approval_required` plan records
the pending decision in the task's private `cloud-usage.json`, which
task-scoped commands remind about and `task status` reports; a `within_limit`
plan clears it, and the file exists only while a decision is pending. The
user's approval is not private state: it lives in the task manifest. Plan also
keeps a disposable measurement cache in `cloud-usage-cache.json` in the same
private task state.

Placement is evaluated before usage is measured. A plan that finds an S3
boundary the storage engine cannot address therefore refuses with status 2 for
that boundary instead of reporting `cloud_usage`, and leaves an earlier pending
decision as it was until the renamed task is measured again.

Plan refuses, like `publish`, when the installed CLI does not meet the
`minimum_cli_version` of the fetched base branch or task branch. Plan also
previews how `publish` reconciles that declaration with the task manifests in
the published tree, as the
[configuration reference](configuration.md#minimum-workspace-mgr-version)
describes: a task that needs no newer release keeps the configuration of the
point where its branch left the base branch, a schema 3 manifest that records
a cloud-usage approval raises the declaration to at least 0.4.0, schema 4
completion evidence requires 0.7.0, a nested
archived task manifest of any supported schema requires at least 0.5.0, a branch
whose manifests no longer need its earlier raise withdraws it but never below
the fetched base branch's declaration, and a branch whose configuration
carries a user-authorized change keeps it and only raises its declaration,
also to follow the base branch. When a task manifest needs a newer release
than the installed CLI, plan and publish refuse with status 2 because this
build cannot publish that task state. The refusal offers recording the default
limit only when removing the task's own approval clears its schema requirement;
an archived-path requirement needs an update. For another task's manifest in
the publication, such as one merged on the base branch, it names the manifest
and asks only for an update. The reconciliation rewrites `.workspace-mgr.toml`
in the private preview index only and lists it in `changed_paths` although it
is outside the declared
scopes. When the published declaration differs from the task branch's, plan
reports `repository_requirement` directly after `changed_paths`:

- `path` is `.workspace-mgr.toml`;
- `change` is `raise` when a task manifest needs a newer release, `follow`
  when the publication takes the fetched base branch's higher declaration,
  either because a task manifest needs a newer release or because the task
  branch must not declare less than the base branch, and `withdraw` when no
  task manifest in the publication needs the task branch's earlier raise any
  more and the base branch declares less;
- `minimum_cli_version` is the published declaration, or `null` when the
  withdrawal removes it;
- `previous_minimum_cli_version` is the declaration in the publication's
  `.workspace-mgr.toml` before the reconciliation, or `null`;
- `task_manifest_schema` is the actual schema of the manifest that drives the
  newer requirement, or `null` when no manifest drives the change. The path
  may drive that requirement, so an archived schema 2 manifest reports `2`
  while requiring 0.5.0.

The field is omitted when the published declaration equals the task branch's.
Plan and publish never change the shared checkout's `.workspace-mgr.toml`; it
receives the declaration when the merged publication is refreshed. A
publication whose tree needs a raise but has no `.workspace-mgr.toml` is
refused.

Plan reports the ignored paths inside the resolved scopes as `ignored_paths`
beside the `ignored_entries` count, and structured `warnings`. Both fields are
present only when they are not empty: `ignored_entries` is always exact, while
`ignored_paths` carries up to its first fifty entries so a pattern-based ignore
rule cannot fill the report. For a deliverable task, `task-record-unchanged`
reports a publication that changes content inside the task directory while none
of the task's own documentation changed with it; ignore it when the work
produced nothing worth recording. `plan` counts the S3 metadata of outputs the
storage engine reports files added to, changed in, or removed from, which
`publish` rewrites when it commits them, so both give the same advice; metadata
whose objects are only missing from the local cache is committed back unchanged
and counts in neither. When the task is over its cloud-usage limit and the
publication is allowed only because it is cleanup-only, the warning adds that
the publication should go out as it is and the decision be recorded in the first
publication the limit allows, because a record added to it would be content the
limit refuses. `bulk-publication` reports a publication that adds more than
200 new files, or more than 256 MiB (268435456 bytes), of new content inside the
task directory; the thresholds are fixed product policy. New content routed to
S3 by automatic placement is counted from the placement decision and measured on
disk, because its payload never reaches the private index, and its pointer is
not counted a second time once `publish` has written it; an explicitly selected
boundary counts as the one pointer it adds. A file that only moved — every
published file of a renamed task, for instance — is not new content and is not
counted. Both are checks rather than refusals.

Plan refuses, before it changes placement or uploads anything, a deliverable
publication that would add or change content inside its own task directory while
that directory documents nothing, and any staged symbolic link whose target is
outside the repository. A task documents itself with Markdown files of its own
choosing inside its directory; a README still carrying only the creation
scaffold's directory map is not yet a record. A storage pointer or placement
record counts as the content it addresses, so a result routed to S3 is judged
like one kept in Git. The staged metadata of a boundary shows a change to its
outputs only once `publish` commits it, after placement and just before the
upload, so for a task that documents nothing the preview asks the storage engine
whether any boundary in the task gained or changed files. A file the engine
cannot compare because its directory's manifest is in neither the local cache
nor the remote counts only when the directory's aggregate digest changed.
`publish` judges the metadata the engine commits again before it uploads
anything, because a background writer may change a boundary's outputs after the
preview; when that check or the cloud-usage re-check refuses, the metadata the
engine committed is restored, so a retry after the late content is gone is not
judged by it. A publication that only retires content is not refused; one that
removes the task's last record while publishing content is refused by name.
Removing files from a directory boundary in S3 retires content, whether or not
the storage engine has committed the boundary yet: the rewritten metadata names
only files the published metadata names, with the same digest or stored
version, and every line it adds records one of those entries. A description,
`meta`, labels, or a comment added to the metadata is text it publishes, so such
a rewrite counts as content. Untracking published content retires it too: the
placement record `untrack` writes then takes the payload or metadata the task
published out of Git and S3, so it does not count as publishing content, and a
task that documents nothing can still publish the cleanup a user chose while the
task is over its cloud-usage limit. A result kept local before it was ever
published retires nothing, so its placement record, the only durable trace of
it, counts as the content it addresses, except while the task waits for the
user's cloud-usage decision: a record added to that cleanup would be growth the
limit refuses, so the record of such a result is judged once the gate has
measured, and counts as content only when the task is within its limit. The
symbolic-link check reads the staged tree, so a link inside a boundary already
placed in S3 or kept local with `untrack` is not classified.

Plan also refuses, at the same point, a path inside the resolved scopes that
only a machine-local ignore rule hides: the user's global excludes file,
`.git/info/exclude`, or an ignore file whose matching bytes this publication
does not carry. Such a rule keeps the file out of every other clone and out of
review, so the path is in neither of the two states task content may be in. The
message names the path, the rule, and the file the rule came from, lists at most
the first five paths, and counts the rest. It points first at the task's own
`<task>/.gitignore`, which this publication stages and which stays inside a
deliverable task's write boundary, and states that the repository layer is a
shared root path needing explicit authorization and its own publication.

Carrying is decided on content, not on the path being tracked: the work-tree
bytes of the ignore file must be the bytes this publication holds for it, so a
rule added to a tracked root `.gitignore` and never published is machine-local
like any other. The product's own fixed rules are the exception — every
installation regenerates them — so an ordinary `.DS_Store` never triggers the
refusal, including before the generated root file has been published. Git
resolves the deepest matching ignore file before `.git/info/exclude` and the
global excludes, so a carried repository or task rule that also matches is the
reported source and does not trigger the refusal. An ignore file this
publication itself adds already counts as carried.

`git status --ignored` collapses a directory whose every entry is ignored into
a single entry, which a file-level rule such as `*.log` does not itself match.
Those directories, and only those, are re-listed with `--ignored=matching` so
the individual files resolve to their rule; a directory a directory rule already
covers stays collapsed and is never walked. The same rule applies to an
infrastructure task over its declared scopes.

`publish` applies the same refusals.

All of these refusals are decided on the preview, before the cloud-usage
measurement, so a refused plan or publication records no pending cloud-usage
decision, and the usage `plan` reports afterwards describes the publication
those guards accept.

`--allow-non-shared-head` is a deliverable-only checkout override and requires a
scope note. It still refuses when the target task branch is currently checked
out.

```sh
workspace-mgr plan
workspace-mgr plan --include docs/shared.md \
  --scope-note "The user requested this shared documentation update"
```

## `workspace-mgr publish`

Publish one verified scoped transaction.

```text
workspace-mgr publish -m <message> [--manifest <path>]
  [--include <path> --scope-note <reason>]
  [--allow-non-shared-head --scope-note <reason>]
  [--repo <path>] [--dry-run]
```

The message is required and must be one line. Publication uploads and verifies
all live in-scope S3 boundaries before creating and pushing the Git commit. The
Git tree is based on the existing remote task branch, or the configured base
branch for its first publication, and includes only resolved scopes plus the
reconciled `.workspace-mgr.toml` that `plan` describes. The remote branch
object ID is verified after push. The checkout and shared Git index are not
switched to the task branch. Both kinds publish through a private index; an
infrastructure task is selected by its private `--manifest` path. A
`.workspace-mgr.toml` reconciled only for publication stays in the Git tree
without overwriting the shared checkout's file.

Before it places, commits, or uploads anything, publish evaluates the same
`cloud_usage` report as `plan` and refuses with status 2 when
`publish_allowed` is false: the projected total exceeds the task's limit and
the publication is not cleanup-only. A cleanup-only publication uploads
nothing to S3 and adds or changes no Git content other than workspace-mgr
control files (task manifests, placement records, S3 metadata, `.gitignore`
files, and the root `.workspace-mgr.toml`); every other change is a deletion.
It may carry at most 1 MiB (1048576 bytes) of new workspace-mgr control-file
content per publication, where metadata that only drops entries is free: each
added or changed control file that the remote does not hold yet is charged its
full new size, whether it grew, kept its size, or shrank. The exception is S3
metadata in which every entry names an object path and version (or, where no
version is recorded, content) that the metadata it replaces already names: it
is charged only for the lines it does not share with that version. Each added
control file is also charged its path plus 28 bytes for the entry it adds to
the Git trees above it. More new control-file content counts as added content.
A cleanup-only publication remains allowed while the task is over its limit.
The refusal prints no report. Its message names the task, the published and
projected Git, S3, and total usage, and the limit, says that the task is
waiting for the user's decision, and points to `plan` for the largest
contributors. The gate is not the first refusal: placement is evaluated before
usage is measured, so a publication that also holds an S3 boundary the storage
engine cannot address is refused for that boundary, before any usage is
measured or recorded. The same holds for the refusals `plan` describes: a
deliverable that documents nothing, a staged symbolic link that escapes the
repository, and a path only a machine-local ignore rule hides are refused before
usage is measured, because resolving them can change what the publication holds.

A real publication checks again after local placement and S3 metadata are
committed but before the upload, and again from the final Git tree before it
creates the commit, because content can change while it runs. A late refusal
leaves local placement and S3 metadata applied, like other publication failures
after placement. A refusal from the final check can also leave objects already
uploaded to S3 unreferenced; they count as projected usage until a later
publication succeeds. Every evaluation records or clears the pending decision.
The report's `cloud_usage` reflects the final check.

The commit message ends with trailers in this order: `Workspace-Task`,
`Workspace-Scope`, one `Scope-Authorization` per authorized additional scope,
then `Workspace-Requirement` when the publication changes the task branch's
`minimum_cli_version`, and `Cloud-Usage-Approval` while the task manifest
records an approval:

```text
Workspace-Requirement: minimum_cli_version=<version> (task manifest schema <n>)
Cloud-Usage-Approval: limit_bytes=<n>; note=<note>
```

The requirement trailer above records a raise. A follow reads
`minimum_cli_version=<version> (task manifest schema <n>; follows
<remote>/<branch>)`, or `(follows <remote>/<branch>)` when no manifest drives
it. A withdrawal reads `minimum_cli_version=<version> (withdraws this branch's
raise to <version>; no task manifest in this publication needs it)`, with
`minimum_cli_version removed` in place of the first value when neither the
task's starting point nor the base branch declares anything. Both trailers are written for reviewers only.
The report includes `repository_requirement` as described for `plan`.
The trailer retains the requiring manifest's actual schema even when its
archived path, rather than its schema, requires the newer release.

`publish --dry-run` performs the same non-publishing behavior as `plan` while
still requiring a message argument, and also rehearses the cloud-usage gate:
unlike `plan`, it refuses with status 2 wherever a real publication would be
refused before placement.
Publication reports the same `warnings` and `ignored_paths` as `plan` and
applies the same refusals before it changes placement or uploads content.

```sh
workspace-mgr publish -m "Publish the training report"
```

The command does not create, update, merge, or close a pull request. Its output
contains a provider-neutral review handoff: pull-request policy, initial state,
manager, merge authority, remote, base branch, and head branch. The responsible
agent uses those facts with the repository hosting workflow.

## `workspace-mgr refresh`

Safely fast-forward a shared checkout after remote changes are merged.

```text
workspace-mgr refresh [--repo <path>] [--dry-run]
```

The remote and shared branch come from `[git]`. The checkout must be on that branch,
the shared index must have no staged or unresolved entries, and the remote
revision must be a fast-forward. Refresh preserves unrelated working-tree
overlays, materializes safe ordinary Git additions, modifications, and
deletions, and hydrates incoming S3 boundaries. It reads Git, optional GitHub
merge evidence, and S3. After synchronization succeeds, it can delete verified
merged refs on the configured Git remote and retry already pending,
unreferenced S3 purge paths.

Before it changes anything, refresh reads `minimum_cli_version` from the
incoming revision's `.workspace-mgr.toml` and refuses with status 2 when that
revision requires a newer CLI, leaving the checkout untouched. This check comes
first, before refresh inspects any incoming storage metadata, including the
unaddressable boundaries described below, because a newer release may write
metadata this one cannot read; `refresh --dry-run` applies it the same way.
After the user approves and completes the update, rerun refresh.

After incoming materialization and storage verification succeed, refresh
automatically checks local and configured-remote branches for cleanup, even
when the shared branch was already current. `--dry-run` reports planned
deletions without deleting refs.

Cleanup uses the installed, authenticated GitHub CLI (`gh`) to verify a merged
same-repository pull request against the configured base. Its merge commit must
be reachable from the fetched base, and its recorded head must match every
remaining local and remote ref for that branch. Squash merges qualify; new
local commits, resumed branches, open or ambiguous pull requests, fork pull
requests, and other-base pull requests do not. The configured base, current and
default branches and protected remote branches are kept. Remote deletion uses
an exact lease; local deletion checks the expected head again.

Branches checked out in any legacy or custom worktree are skipped. Refresh
never detaches a worktree or removes its directory or files.

The `branch_cleanup` report contains:

| Field | Meaning |
| --- | --- |
| `status` | `dry_run`, `complete`, `unavailable`, or `not_applicable`; `complete` may still have skips or errors |
| `planned`, `deleted` | Entries name `branch`, `head_oid`, `pull_request`, and whether `local` or `remote` refs are involved |
| `skipped` | Retained branches with a `reason` |
| `errors` | Failed cleanup attempts with `branch`, `action`, and `error` |
| `warnings` | Actionable cleanup warnings |
| `remote_writes` | Whether cleanup confirmed a configured-remote branch deletion |

For a non-GitHub remote, cleanup returns `not_applicable` without a warning.
Unavailable GitHub CLI/authentication returns `unavailable`, keeps all refs,
and reports a warning. Cleanup errors and warnings do not undo successful
shared-branch synchronization. Top-level
`warnings` use `branch-cleanup-unavailable` and `branch-cleanup-failed` codes;
the detailed report names each branch and failure. Retained task
directories and payloads are not deleted by branch cleanup. The normal pending
S3 purge retry can proceed once a deleted branch no longer protects a queued
path; moving completed task directories still requires an explicit user
request and `archive` in an infrastructure task.

When the shared branch was already current but refs were deleted, refresh's
overall `status` is `branches_cleaned`.

GitHub access is optional for synchronization. To diagnose unavailable cleanup,
check the CLI and its authentication, then rerun refresh:

```sh
gh --version
gh auth status
workspace-mgr refresh --dry-run
```

An incoming S3 boundary whose path contains a backslash is the one exception.
Older storage engines interpreted backslashes inconsistently. The native
migration preserves the existing boundary convention and hydrates only
addressable paths, while allowing synchronization of everything else. Refresh
detects those
boundaries before it changes the branch, the index, the working tree, or stored
content, then advances everything else and leaves only their payload
unhydrated. It lists them in `storage.unaddressable` and reports one
`unaddressable-storage-metadata` entry in `warnings`, which names them and the
recovery. `refresh --dry-run` reports the same condition, so a preview never
reports plain success for a refresh that would leave a boundary behind.

Refresh cannot replace or verify a payload at such a path either, so it refuses,
before anything changes, when this checkout already holds one that the incoming
metadata does not describe byte for byte, or holds one without metadata beside
it. A payload that already matches is kept.

Until such a boundary is renamed, `storage hydrate` refuses it, and a
scope-wide `storage hydrate` refuses its whole scope; name the other boundaries
to hydrate them. Recover each boundary with the user's authorization in an
infrastructure task in the shared checkout. Declare as its scope the directory
that holds the boundary and the destination directory if that differs: the
rename rewrites
each directory's `.gitignore` and both metadata files. Select its private
manifest with `--manifest` and `move` the boundary to a path without backslashes,
which fetches its payload and materializes it at the destination; hydrate the
other boundaries in those
directories by naming them, because publication requires every boundary in its
scope to be present; then publish the task and merge it. A later refresh
hydrates the renamed boundary in every checkout.

## Help and version

```sh
workspace-mgr --help
workspace-mgr storage set --help
workspace-mgr --version
```

Help describes syntax. Repository operating policy comes from
`workspace-mgr instructions`, which is why the generated `AGENTS.md` invokes
`instructions` rather than `help`.