harnery 0.41.0 → 0.42.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.
Files changed (212) hide show
  1. package/dist/commander.js +3 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +9 -1
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +113 -12
  6. package/dist/commands/browse.d.ts +164 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +263 -51
  9. package/dist/commands/claude-desktop.d.ts.map +1 -1
  10. package/dist/commands/claude-desktop.js +52 -0
  11. package/dist/commands/disk.d.ts +6 -0
  12. package/dist/commands/disk.d.ts.map +1 -0
  13. package/dist/commands/disk.js +80 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +48 -1
  17. package/dist/commands/files.d.ts +16 -0
  18. package/dist/commands/files.d.ts.map +1 -1
  19. package/dist/commands/files.js +54 -2
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +43 -9
  23. package/dist/commands/rm.d.ts +4 -0
  24. package/dist/commands/rm.d.ts.map +1 -0
  25. package/dist/commands/rm.js +87 -0
  26. package/dist/commands/servers.d.ts +7 -0
  27. package/dist/commands/servers.d.ts.map +1 -0
  28. package/dist/commands/servers.js +394 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +82 -2
  31. package/dist/core/adapters/profiles.d.ts +9 -0
  32. package/dist/core/adapters/profiles.d.ts.map +1 -1
  33. package/dist/core/adapters/profiles.js +22 -1
  34. package/dist/core/adapters/types.d.ts +9 -0
  35. package/dist/core/adapters/types.d.ts.map +1 -1
  36. package/dist/core/agents/cli.js +13 -6
  37. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/session-context.js +2 -0
  39. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +60 -0
  42. package/dist/core/artifacts/delivery-card.js +9 -2
  43. package/dist/core/artifacts/index.d.ts +140 -4
  44. package/dist/core/artifacts/index.d.ts.map +1 -1
  45. package/dist/core/artifacts/index.js +522 -49
  46. package/dist/core/config.d.ts +68 -11
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +128 -3
  49. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  50. package/dist/core/diagnostics/bundle.js +2 -2
  51. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  52. package/dist/core/events/v3/producers/recorder.js +16 -0
  53. package/dist/core/events/v3/projection.js +9 -1
  54. package/dist/core/hooks/adapter/events.d.ts +17 -0
  55. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  56. package/dist/core/hooks/adapter/events.js +5 -0
  57. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  58. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  59. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  60. package/dist/core/hooks/adapter/wiring.js +62 -4
  61. package/dist/core/hooks/cli.js +45 -6
  62. package/dist/core/hooks/run-markers.d.ts +37 -0
  63. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  64. package/dist/core/hooks/run-markers.js +109 -0
  65. package/dist/core/servers/index.d.ts +211 -0
  66. package/dist/core/servers/index.d.ts.map +1 -0
  67. package/dist/core/servers/index.js +560 -0
  68. package/dist/core/servers/net.d.ts +58 -0
  69. package/dist/core/servers/net.d.ts.map +1 -0
  70. package/dist/core/servers/net.js +270 -0
  71. package/dist/core/servers/tunnels.d.ts +14 -0
  72. package/dist/core/servers/tunnels.d.ts.map +1 -0
  73. package/dist/core/servers/tunnels.js +33 -0
  74. package/dist/core/storage/builtins.js +1 -0
  75. package/dist/core/workflow/engine.d.ts.map +1 -1
  76. package/dist/core/workflow/engine.js +30 -1
  77. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  78. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  79. package/dist/core/workflow/sandbox-projection.js +23 -0
  80. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  81. package/dist/core/workflow/spawn-claude.js +6 -1
  82. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  83. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  84. package/dist/core/workflow/spawn-codex.js +20 -1
  85. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  86. package/dist/core/workflow/spawn-cursor.js +6 -1
  87. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  88. package/dist/core/workflow/spawn-opencode.js +4 -1
  89. package/dist/core/workflow/types.d.ts +47 -2
  90. package/dist/core/workflow/types.d.ts.map +1 -1
  91. package/dist/core/workflow/worker-access.d.ts +92 -0
  92. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  93. package/dist/core/workflow/worker-access.js +450 -0
  94. package/dist/lib/browser/batch-steps.d.ts +18 -0
  95. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  96. package/dist/lib/browser/batch-steps.js +82 -0
  97. package/dist/lib/browser/collect/collector.d.ts +99 -0
  98. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  99. package/dist/lib/browser/collect/collector.js +202 -0
  100. package/dist/lib/browser/collect/fields.d.ts +31 -0
  101. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  102. package/dist/lib/browser/collect/fields.js +75 -0
  103. package/dist/lib/browser/collect/index.d.ts +6 -0
  104. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  105. package/dist/lib/browser/collect/index.js +5 -0
  106. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  107. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  108. package/dist/lib/browser/collect/page-driver.js +306 -0
  109. package/dist/lib/browser/collect/presets.d.ts +52 -0
  110. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  111. package/dist/lib/browser/collect/presets.js +176 -0
  112. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  113. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  114. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  115. package/dist/lib/browser/index.d.ts +1 -1
  116. package/dist/lib/browser/index.d.ts.map +1 -1
  117. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  118. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  119. package/dist/lib/browser/page-review-judge.js +13 -0
  120. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  121. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  122. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  123. package/dist/lib/browser/qa-run.js +1 -0
  124. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  125. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  126. package/dist/lib/browser/windows-chrome.js +179 -0
  127. package/dist/lib/claude-desktop-share.d.ts +136 -0
  128. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  129. package/dist/lib/claude-desktop-share.js +394 -0
  130. package/dist/lib/disk-usage.d.ts +50 -0
  131. package/dist/lib/disk-usage.d.ts.map +1 -0
  132. package/dist/lib/disk-usage.js +303 -0
  133. package/dist/lib/docs-lint.d.ts +6 -0
  134. package/dist/lib/docs-lint.d.ts.map +1 -1
  135. package/dist/lib/docs-lint.js +8 -3
  136. package/dist/lib/guarded-remove.d.ts +34 -0
  137. package/dist/lib/guarded-remove.d.ts.map +1 -0
  138. package/dist/lib/guarded-remove.js +310 -0
  139. package/dist/lib/instructions/templates.js +8 -8
  140. package/dist/lib/tunnel/error-page.d.ts +1 -1
  141. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  142. package/dist/lib/tunnel/error-page.js +19 -8
  143. package/dist/lib/tunnel/gate.js +20 -1
  144. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  145. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  146. package/dist/lib/tunnel/path-scope.js +62 -0
  147. package/dist/lib/tunnel/state.d.ts +5 -0
  148. package/dist/lib/tunnel/state.d.ts.map +1 -1
  149. package/dist/lib/tunnel/state.js +17 -3
  150. package/package.json +6 -1
  151. package/schemas/config.schema.json +91 -0
  152. package/src/commander.ts +19 -0
  153. package/src/commands/agents.ts +14 -0
  154. package/src/commands/artifacts.ts +171 -19
  155. package/src/commands/browse.ts +400 -57
  156. package/src/commands/claude-desktop.ts +61 -0
  157. package/src/commands/disk.ts +114 -0
  158. package/src/commands/doctor.ts +52 -1
  159. package/src/commands/files.ts +93 -2
  160. package/src/commands/init.ts +55 -5
  161. package/src/commands/rm.ts +115 -0
  162. package/src/commands/servers.ts +506 -0
  163. package/src/commands/tunnel.ts +102 -2
  164. package/src/core/adapters/profiles.ts +22 -1
  165. package/src/core/adapters/types.ts +10 -0
  166. package/src/core/agents/cli.ts +14 -6
  167. package/src/core/agents/render/session-context.ts +1 -0
  168. package/src/core/agents/state/live-coordination-view.ts +73 -0
  169. package/src/core/artifacts/delivery-card.ts +10 -2
  170. package/src/core/artifacts/index.ts +697 -44
  171. package/src/core/config.ts +183 -4
  172. package/src/core/diagnostics/bundle.ts +2 -1
  173. package/src/core/events/v3/producers/recorder.ts +16 -0
  174. package/src/core/events/v3/projection.ts +11 -1
  175. package/src/core/hooks/adapter/events.ts +19 -0
  176. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  177. package/src/core/hooks/adapter/wiring.ts +116 -8
  178. package/src/core/hooks/cli.ts +47 -6
  179. package/src/core/hooks/run-markers.ts +132 -0
  180. package/src/core/servers/index.ts +712 -0
  181. package/src/core/servers/net.ts +283 -0
  182. package/src/core/servers/tunnels.ts +39 -0
  183. package/src/core/storage/builtins.ts +1 -0
  184. package/src/core/workflow/engine.ts +38 -1
  185. package/src/core/workflow/sandbox-projection.ts +38 -2
  186. package/src/core/workflow/spawn-claude.ts +5 -1
  187. package/src/core/workflow/spawn-codex.ts +18 -1
  188. package/src/core/workflow/spawn-cursor.ts +5 -1
  189. package/src/core/workflow/spawn-opencode.ts +4 -1
  190. package/src/core/workflow/types.ts +62 -2
  191. package/src/core/workflow/worker-access.ts +525 -0
  192. package/src/lib/browser/batch-steps.ts +79 -0
  193. package/src/lib/browser/collect/collector.ts +306 -0
  194. package/src/lib/browser/collect/fields.ts +87 -0
  195. package/src/lib/browser/collect/index.ts +47 -0
  196. package/src/lib/browser/collect/page-driver.ts +327 -0
  197. package/src/lib/browser/collect/presets.ts +237 -0
  198. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  199. package/src/lib/browser/index.ts +1 -0
  200. package/src/lib/browser/page-review-judge.ts +16 -0
  201. package/src/lib/browser/qa-run-contracts.ts +11 -0
  202. package/src/lib/browser/qa-run.ts +1 -0
  203. package/src/lib/browser/windows-chrome.ts +227 -0
  204. package/src/lib/claude-desktop-share.ts +511 -0
  205. package/src/lib/disk-usage.ts +347 -0
  206. package/src/lib/docs-lint.ts +8 -3
  207. package/src/lib/guarded-remove.ts +376 -0
  208. package/src/lib/instructions/templates.ts +8 -8
  209. package/src/lib/tunnel/error-page.ts +25 -9
  210. package/src/lib/tunnel/gate.ts +22 -1
  211. package/src/lib/tunnel/path-scope.ts +73 -0
  212. package/src/lib/tunnel/state.ts +20 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -137,6 +137,11 @@
137
137
  "types": "./dist/core/supervisor/index.d.ts",
138
138
  "import": "./dist/core/supervisor/index.js"
139
139
  },
140
+ "./core/servers": {
141
+ "bun": "./src/core/servers/index.ts",
142
+ "types": "./dist/core/servers/index.d.ts",
143
+ "import": "./dist/core/servers/index.js"
144
+ },
140
145
  "./core/diagnostics": {
141
146
  "bun": "./src/core/diagnostics/index.ts",
142
147
  "types": "./dist/core/diagnostics/index.d.ts",
@@ -240,6 +240,53 @@
240
240
  "maximum": 3650,
241
241
  "default": 3,
242
242
  "description": "Retention for `harn artifacts create` when --days is absent. Env override: HARNERY_ARTIFACT_RETENTION_DAYS."
243
+ },
244
+ "auto_clean": {
245
+ "type": "boolean",
246
+ "default": true,
247
+ "description": "Run the throttled artifact cleanup at session start and before new artifact work. Env override: HARNERY_ARTIFACT_AUTO_CLEAN."
248
+ },
249
+ "max_bytes": {
250
+ "type": "integer",
251
+ "minimum": 67108864,
252
+ "maximum": 1099511627776,
253
+ "default": 21474836480,
254
+ "description": "Soft repository budget in bytes of disk use for unheld managed workspaces. Env override: HARNERY_ARTIFACT_MAX_BYTES."
255
+ },
256
+ "max_unit_bytes": {
257
+ "type": "integer",
258
+ "minimum": 16777216,
259
+ "maximum": 1099511627776,
260
+ "default": 1073741824,
261
+ "description": "Per-workspace ceiling in bytes of disk use; a workspace created with --big is exempt. Env override: HARNERY_ARTIFACT_MAX_UNIT_BYTES."
262
+ },
263
+ "idle_grace_hours": {
264
+ "type": "integer",
265
+ "minimum": 1,
266
+ "maximum": 8760,
267
+ "default": 24,
268
+ "description": "Hours an unexpired workspace must be idle (no file change, no owner heartbeat) before a size rule may delete it. Env override: HARNERY_ARTIFACT_IDLE_GRACE_HOURS."
269
+ },
270
+ "hold_days": {
271
+ "type": "integer",
272
+ "minimum": 1,
273
+ "maximum": 365,
274
+ "default": 14,
275
+ "description": "Days a hold protects a workspace before it lapses unless renewed with `harn artifacts hold` (same id and reason). Env override: HARNERY_ARTIFACT_HOLD_DAYS."
276
+ },
277
+ "max_held_bytes": {
278
+ "type": "integer",
279
+ "minimum": 67108864,
280
+ "maximum": 1099511627776,
281
+ "default": 21474836480,
282
+ "description": "Advisory budget in bytes of disk use for held workspaces. Exceeding it produces warnings; held work is never deleted. Env override: HARNERY_ARTIFACT_MAX_HELD_BYTES."
283
+ },
284
+ "min_free_bytes": {
285
+ "type": "integer",
286
+ "minimum": 0,
287
+ "maximum": 1125899906842624,
288
+ "default": 21474836480,
289
+ "description": "Free-disk floor for the artifact store. Below it, artifact commands warn and `artifacts create --big` refuses without --allow-low-disk. 0 disables the floor. Env override: HARNERY_ARTIFACT_MIN_FREE_BYTES."
243
290
  }
244
291
  }
245
292
  },
@@ -306,6 +353,37 @@
306
353
  "type": "boolean",
307
354
  "default": false,
308
355
  "description": "Pin `workflow run` to subscription billing: API-key vars are scrubbed from every child env. Stamped true by `harn init` when the key is absent. Env override: HARNERY_WORKFLOW_SUBSCRIPTION_ONLY=1|0."
356
+ },
357
+ "workerFullAccess": {
358
+ "type": "object",
359
+ "additionalProperties": false,
360
+ "description": "Let qualifying workflow workers launch without their vendor sandbox (ADR 0192). A launch qualifies only when this is enabled, the launching session itself can be shown to run with full access, and the worker's exact model and effort meet a floor listed here. Anything else keeps the ordinary sandboxed projection. Project config only; a user-global file cannot grant it. HARNERY_WORKFLOW_WORKER_FULL_ACCESS=0 turns it off for one process. An invalid value disables it entirely.",
361
+ "properties": {
362
+ "enabled": { "type": "boolean", "default": false },
363
+ "floors": {
364
+ "type": "object",
365
+ "description": "Per-adapter list of models a worker may run unsandboxed on. Keys are adapter ids.",
366
+ "additionalProperties": {
367
+ "type": "array",
368
+ "items": {
369
+ "type": "object",
370
+ "additionalProperties": false,
371
+ "required": ["model"],
372
+ "properties": {
373
+ "model": {
374
+ "type": "string",
375
+ "minLength": 1,
376
+ "description": "Exact model id the worker must name. Aliases and the adapter's default model never match."
377
+ },
378
+ "minEffort": {
379
+ "type": "string",
380
+ "description": "Lowest qualifying effort on the adapter's declared effort scale. Required for an adapter with an effort scale; not allowed for one that encodes effort in the model id."
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
309
387
  }
310
388
  }
311
389
  },
@@ -349,6 +427,19 @@
349
427
  "default": 65536
350
428
  }
351
429
  }
430
+ },
431
+ "codexWindowsBridge": {
432
+ "type": "object",
433
+ "additionalProperties": false,
434
+ "required": ["entryPoint"],
435
+ "description": "Route Windows-native Codex hooks through a resident WSL bridge (decision record 0197). init writes a commandWindows on each Codex hook that calls entryPoint when it is on PATH and the working directory is a WSL path, and otherwise runs the usual command. Project config only.",
436
+ "properties": {
437
+ "entryPoint": {
438
+ "type": "string",
439
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
440
+ "description": "Windows command name the bridge installs, for example codex-wsl-hook. Only a bridge that implements the decision record 0197 contract should install it."
441
+ }
442
+ }
352
443
  }
353
444
  }
354
445
  },
package/src/commander.ts CHANGED
@@ -320,6 +320,12 @@ function harneryCommandBundles({
320
320
  binName,
321
321
  }: CommandBundleContext): LazyCommandBundle[] {
322
322
  return [
323
+ lazy(
324
+ "disk [path]",
325
+ "Measure checkout disk usage, including nested repositories and ignored files",
326
+ async (program) => (await import("./commands/disk.ts")).registerDiskCommand(program, emit),
327
+ { hasOptions: true },
328
+ ),
323
329
  lazy(
324
330
  "tokens <files...>",
325
331
  "Count tokens in text/markdown files (offline, uses o200k_base as Claude proxy)",
@@ -596,6 +602,13 @@ function harneryCommandBundles({
596
602
  async (program) =>
597
603
  (await import("./commands/storage.ts")).registerStorageCommand(program, emit, context),
598
604
  ),
605
+ lazy(
606
+ "rm <paths...>",
607
+ "Preview guarded removal of untracked files; --yes applies permanent deletion",
608
+ async (program) =>
609
+ (await import("./commands/rm.ts")).registerRmCommand(program, emit, context),
610
+ { hasOptions: true },
611
+ ),
599
612
  lazy("logs", "List and query bounded Harnery log families", async (program) =>
600
613
  (await import("./commands/logs.ts")).registerLogsCommand(program, emit, context),
601
614
  ),
@@ -661,6 +674,12 @@ function harneryCommandBundles({
661
674
  async (program) =>
662
675
  (await import("./commands/tunnel.ts")).registerTunnelCommand(program, emit, context),
663
676
  ),
677
+ lazy(
678
+ "servers",
679
+ "Every local server started in this project: list, stop, clean up idle session servers, read logs",
680
+ async (program) =>
681
+ (await import("./commands/servers.ts")).registerServersCommand(program, emit, context),
682
+ ),
664
683
  lazy(
665
684
  "docs",
666
685
  "Documentation tooling: freshness report, metadata, lint, sweep, index",
@@ -78,6 +78,9 @@ import {
78
78
  readHeartbeat as readHeartbeatCache,
79
79
  } from "../core/agents/state/heartbeat-writer.ts";
80
80
  import {
81
+ describeLiveAuthorityBlockV3,
82
+ liveAuthorityBlockHintV3,
83
+ liveAuthorityBlockV3,
81
84
  readLiveCoordinationRow,
82
85
  readLiveCoordinationRows,
83
86
  } from "../core/agents/state/live-coordination-view.ts";
@@ -1486,6 +1489,10 @@ function normalizeKind(kind: string | undefined | null): string {
1486
1489
  * command refuses in turn and the caller has no way to guess the path back.
1487
1490
  */
1488
1491
  function noLiveGenerationMessage(root: string, owner: string): string {
1492
+ const block = liveAuthorityBlockV3(root, owner);
1493
+ if (block) {
1494
+ return `resolved owner=${owner}; ${describeLiveAuthorityBlockV3(block)}; ${liveAuthorityBlockHintV3(resolveBinName(root))}`;
1495
+ }
1489
1496
  return `resolved owner=${owner} but no authority-safe live V3 generation exists for it; ${lifecycleReopenHint(root)}`;
1490
1497
  }
1491
1498
 
@@ -5230,6 +5237,13 @@ function runHeal(opts: {
5230
5237
  }
5231
5238
 
5232
5239
  if (proc.status !== 0) {
5240
+ const block = liveAuthorityBlockV3(root, owner);
5241
+ if (block) {
5242
+ emitHealFailure(
5243
+ "authority_blocked",
5244
+ `${describeLiveAuthorityBlockV3(block)}; a cache rebuild cannot restore authority the ledger withholds; ${liveAuthorityBlockHintV3(resolveBinName(root))}`,
5245
+ );
5246
+ }
5233
5247
  emitHealFailure(
5234
5248
  healFailureReason(root, owner, sessionId, healAdapter),
5235
5249
  spawnFailureMessage(proc, `agent-coord ${action}`),
@@ -6,9 +6,13 @@ import {
6
6
  type ArtifactActor,
7
7
  type ArtifactDeliveryManifest,
8
8
  adoptUnmanagedArtifactFiles,
9
+ allowBigArtifact,
9
10
  artifactCapabilities,
11
+ artifactQuickUsage,
10
12
  artifactReviewGuidance,
11
13
  artifactsRoot,
14
+ artifactUsageReport,
15
+ assertArtifactDiskFloor,
12
16
  autoCleanArtifacts,
13
17
  cleanArtifacts,
14
18
  createArtifact,
@@ -17,7 +21,9 @@ import {
17
21
  inventoryArtifacts,
18
22
  migrateArtifacts,
19
23
  parseArtifactDeliverySpec,
24
+ readArtifactDeletions,
20
25
  releaseArtifact,
26
+ removeArtifact,
21
27
  renderArtifactDeliveryCard,
22
28
  renewArtifact,
23
29
  repairArtifactActivity,
@@ -28,7 +34,10 @@ import {
28
34
  } from "../core/artifacts/index.ts";
29
35
  import {
30
36
  artifactDefaultRetentionDays,
37
+ artifactIdleGraceHours,
38
+ artifactMaxUnitBytes,
31
39
  coordFreshnessSeconds,
40
+ resolveBinName,
32
41
  reviewPackAutoCleanEnabled,
33
42
  } from "../core/config.ts";
34
43
  import { deleteExpiredPacks } from "../lib/browser/page-review-pack.ts";
@@ -50,8 +59,16 @@ export function registerArtifactsCommand(
50
59
  .option("--days <n>", "Retention in days (default from artifacts.default_retention_days)")
51
60
  .option("--minutes <n>", "Retention in minutes; cannot be combined with --days")
52
61
  .option("--big", "Acknowledge that this workspace may exceed the per-bundle size ceiling")
62
+ .option(
63
+ "--allow-low-disk",
64
+ "Create a --big workspace even though free disk is below artifacts.min_free_bytes",
65
+ )
53
66
  .option("--hold <id>", "Create the workspace with this hold already present")
54
67
  .option("--hold-reason <text>", "Why the initial hold is required")
68
+ .option(
69
+ "--hold-persistent",
70
+ "Make the initial hold persistent: it never lapses and counts toward the held budget. Only for a hold that mirrors an external lease, such as an open checkout with unsynchronized work",
71
+ )
55
72
  .option("--actor <instance-id>", "Stable owner identity for the initial hold")
56
73
  .action(
57
74
  (
@@ -61,8 +78,10 @@ export function registerArtifactsCommand(
61
78
  days?: string;
62
79
  minutes?: string;
63
80
  big?: boolean;
81
+ allowLowDisk?: boolean;
64
82
  hold?: string;
65
83
  holdReason?: string;
84
+ holdPersistent?: boolean;
66
85
  actor?: string;
67
86
  },
68
87
  ) => {
@@ -73,6 +92,8 @@ export function registerArtifactsCommand(
73
92
  const actor = opts.actor ? { instance_id: opts.actor } : currentActor(repoRoot);
74
93
  if (!!opts.hold !== !!opts.holdReason)
75
94
  throw new Error("--hold and --hold-reason must be used together");
95
+ if (opts.holdPersistent && !opts.hold)
96
+ throw new Error("--hold-persistent requires --hold");
76
97
  const retentionDays = opts.days
77
98
  ? parseDays(opts.days)
78
99
  : artifactDefaultRetentionDays(repoRoot);
@@ -86,6 +107,7 @@ export function registerArtifactsCommand(
86
107
  "warn",
87
108
  );
88
109
  }
110
+ if (opts.big && !opts.allowLowDisk) assertArtifactDiskFloor(repoRoot);
89
111
  const created = createArtifact(repoRoot, {
90
112
  slug,
91
113
  purpose: opts.purpose,
@@ -93,7 +115,15 @@ export function registerArtifactsCommand(
93
115
  retentionMinutes,
94
116
  actor,
95
117
  big: opts.big,
96
- holds: opts.hold ? [{ id: opts.hold, reason: opts.holdReason! }] : [],
118
+ holds: opts.hold
119
+ ? [
120
+ {
121
+ id: opts.hold,
122
+ reason: opts.holdReason!,
123
+ ...(opts.holdPersistent ? { persistent: true } : {}),
124
+ },
125
+ ]
126
+ : [],
97
127
  });
98
128
  emit.data({
99
129
  path: created.path,
@@ -101,6 +131,13 @@ export function registerArtifactsCommand(
101
131
  expires_at: created.manifest.retention.expires_at,
102
132
  owner_instance_id: actor?.instance_id ?? null,
103
133
  holds: created.manifest.holds,
134
+ ...usageFields(emit, repoRoot),
135
+ ...(opts.big
136
+ ? {}
137
+ : {
138
+ size_ceiling_bytes: artifactMaxUnitBytes(repoRoot),
139
+ size_note: `Cleanup deletes this workspace before it expires if it grows past ${artifactMaxUnitBytes(repoRoot)} bytes on disk and then sits idle for ${artifactIdleGraceHours(repoRoot)}h. Create it with --big, or run ${resolveBinName(repoRoot)} artifacts allow-big later, if it will grow that large.`,
140
+ }),
104
141
  after_review: artifactReviewGuidance(repoRoot, created.manifest.artifact_id),
105
142
  });
106
143
  });
@@ -144,7 +181,14 @@ export function registerArtifactsCommand(
144
181
  const rows = inventoryArtifacts(repoRoot, {
145
182
  freshnessSeconds: coordFreshnessSeconds(repoRoot),
146
183
  });
147
- emit.data({ rows, meta: summarize(rows) });
184
+ const recentDeletions = readArtifactDeletions(repoRoot, {
185
+ since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
186
+ });
187
+ emit.data({
188
+ rows,
189
+ meta: inventoryMeta(emit, repoRoot, rows),
190
+ recent_deletions: recentDeletions,
191
+ });
148
192
  });
149
193
  });
150
194
 
@@ -154,11 +198,21 @@ export function registerArtifactsCommand(
154
198
  .action((ref: string) => {
155
199
  run(emit, () => {
156
200
  const repoRoot = requireRepoRoot(context);
157
- emit.data(
158
- showArtifact(repoRoot, ref, {
159
- freshnessSeconds: coordFreshnessSeconds(repoRoot),
160
- }),
161
- );
201
+ try {
202
+ emit.data(
203
+ showArtifact(repoRoot, ref, {
204
+ freshnessSeconds: coordFreshnessSeconds(repoRoot),
205
+ }),
206
+ );
207
+ } catch (error) {
208
+ const deleted = readArtifactDeletions(repoRoot)
209
+ .reverse()
210
+ .find((record) => [record.artifact_id, record.name].includes(ref));
211
+ if (!deleted) throw error;
212
+ throw new Error(
213
+ `artifact "${ref}" was deleted by cleanup at ${deleted.deleted_at} as ${deleted.classification}: ${deleted.reason}`,
214
+ );
215
+ }
162
216
  });
163
217
  });
164
218
 
@@ -221,6 +275,18 @@ export function registerArtifactsCommand(
221
275
  });
222
276
  });
223
277
 
278
+ root
279
+ .command("allow-big <ref>")
280
+ .description(
281
+ "Exempt an existing workspace from the per-bundle size ceiling, as create --big would; expiry still applies.",
282
+ )
283
+ .action((ref: string) => {
284
+ run(emit, () => {
285
+ const repoRoot = requireRepoRoot(context);
286
+ emit.data(allowBigArtifact(repoRoot, ref, { actor: currentActor(repoRoot) }));
287
+ });
288
+ });
289
+
224
290
  root
225
291
  .command("release <ref>")
226
292
  .description("Release active-owner protection without shortening retention.")
@@ -251,6 +317,20 @@ export function registerArtifactsCommand(
251
317
  });
252
318
  });
253
319
 
320
+ root
321
+ .command("remove <ref>")
322
+ .description("Preview immediate removal of one owned artifact; --yes permanently deletes it.")
323
+ .requiredOption("--reason <text>", "Why these reviewed files are no longer needed")
324
+ .option("--yes", "Permanently delete this one workspace after safety checks")
325
+ .action((ref: string, opts: { reason: string; yes?: boolean }) => {
326
+ run(emit, () => {
327
+ const repoRoot = requireRepoRoot(context);
328
+ emit.data(
329
+ removeArtifact(repoRoot, ref, opts.reason, { ...opts, actor: currentActor(repoRoot) }),
330
+ );
331
+ });
332
+ });
333
+
254
334
  root
255
335
  .command("capabilities")
256
336
  .description("Report artifact schema and hold capabilities for embedding clients.")
@@ -278,21 +358,46 @@ export function registerArtifactsCommand(
278
358
 
279
359
  root
280
360
  .command("hold <ref>")
281
- .description("Protect an artifact from cleanup until its owner removes this exact hold.")
361
+ .description(
362
+ "Protect an artifact from cleanup until the hold lapses or its owner removes it; repeating the same id and reason renews it.",
363
+ )
282
364
  .requiredOption("--id <id>", "Unique hold id within this artifact")
283
365
  .requiredOption("--reason <text>", "Why cleanup must retain these files")
284
366
  .option("--actor <instance-id>", "Stable hold owner; defaults to the current agent")
285
- .action((ref: string, opts: { id: string; reason: string; actor?: string }) =>
286
- run(emit, () => {
287
- const repoRoot = requireRepoRoot(context);
288
- emit.data(
289
- holdArtifact(repoRoot, ref, {
367
+ .option("--days <n>", "Hold lifetime in days, 1 to 365 (default from artifacts.hold_days)")
368
+ .option("--minutes <n>", "Hold lifetime in minutes; cannot be combined with --days")
369
+ .option(
370
+ "--persistent",
371
+ "Never lapse; counts toward the held budget. Only for a hold that mirrors an external lease, such as an open checkout with unsynchronized work",
372
+ )
373
+ .action(
374
+ (
375
+ ref: string,
376
+ opts: {
377
+ id: string;
378
+ reason: string;
379
+ actor?: string;
380
+ days?: string;
381
+ minutes?: string;
382
+ persistent?: boolean;
383
+ },
384
+ ) =>
385
+ run(emit, () => {
386
+ const repoRoot = requireRepoRoot(context);
387
+ if (opts.days !== undefined && opts.minutes !== undefined)
388
+ throw new Error("choose either --days or --minutes");
389
+ if (opts.persistent && (opts.days !== undefined || opts.minutes !== undefined))
390
+ throw new Error("--persistent cannot be combined with --days or --minutes");
391
+ const manifest = holdArtifact(repoRoot, ref, {
290
392
  id: opts.id,
291
393
  reason: opts.reason,
292
394
  actor: requireHoldActor(repoRoot, opts.actor),
293
- }),
294
- );
295
- }),
395
+ ...(opts.days !== undefined ? { days: parseHoldDays(opts.days) } : {}),
396
+ ...(opts.minutes !== undefined ? { minutes: parseHoldMinutes(opts.minutes) } : {}),
397
+ ...(opts.persistent ? { persistent: true } : {}),
398
+ });
399
+ emit.data({ ...manifest, ...usageFields(emit, repoRoot) });
400
+ }),
296
401
  );
297
402
 
298
403
  root
@@ -325,7 +430,7 @@ export function registerArtifactsCommand(
325
430
  : undefined;
326
431
  emit.data({
327
432
  rows,
328
- meta: summarize(rows),
433
+ meta: inventoryMeta(emit, repoRoot, rows),
329
434
  ...(reviewPacks ? { review_packs: reviewPacks } : {}),
330
435
  });
331
436
  });
@@ -370,20 +475,67 @@ function parseMinutes(value: string): number {
370
475
  return parsed;
371
476
  }
372
477
 
478
+ function parseHoldDays(value: string): number {
479
+ const parsed = Number(value);
480
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 365)
481
+ throw new Error("--days must be between 1 and 365 for a hold");
482
+ return parsed;
483
+ }
484
+
485
+ function parseHoldMinutes(value: string): number {
486
+ const parsed = Number(value);
487
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 525600)
488
+ throw new Error("--minutes must be between 1 and 525600 for a hold");
489
+ return parsed;
490
+ }
491
+
492
+ /** Cheap usage for create and hold: cached held bytes plus live free disk. */
493
+ function usageFields(
494
+ emit: EmitContext,
495
+ repoRoot: string,
496
+ ): { usage: ReturnType<typeof artifactQuickUsage>["usage"]; warnings: string[] } {
497
+ const quick = artifactQuickUsage(repoRoot);
498
+ for (const warning of quick.warnings) emit.log(warning, "warn");
499
+ return { usage: quick.usage, warnings: quick.warnings };
500
+ }
501
+
502
+ /** Summary plus held budget and free disk for a full inventory; warnings also go to stderr. */
503
+ function inventoryMeta(
504
+ emit: EmitContext,
505
+ repoRoot: string,
506
+ rows: Parameters<typeof artifactUsageReport>[1],
507
+ ): Record<string, unknown> {
508
+ const report = artifactUsageReport(repoRoot, rows);
509
+ for (const warning of report.warnings) emit.log(warning, "warn");
510
+ return { ...summarize(rows), ...report };
511
+ }
512
+
373
513
  function collect(value: string, previous: string[]): string[] {
374
514
  return [...previous, value];
375
515
  }
376
516
 
377
517
  function summarize(
378
- rows: { classification: string; action: string; bytes?: number | null }[],
518
+ rows: {
519
+ classification: string;
520
+ action: string;
521
+ bytes?: number | null;
522
+ apparent_bytes?: number | null;
523
+ warning?: string | null;
524
+ }[],
379
525
  ): Record<string, unknown> {
380
526
  const classifications: Record<string, number> = {};
381
527
  for (const row of rows) {
382
528
  classifications[row.classification] = (classifications[row.classification] ?? 0) + 1;
383
529
  }
530
+ const bytes = rows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
384
531
  return {
385
532
  total: rows.length,
386
- bytes: rows.reduce((sum, row) => sum + (row.bytes ?? 0), 0),
533
+ // Disk use. `apparent_bytes` is the sum of file lengths; a large gap
534
+ // between the two means sparse or hard-linked files.
535
+ bytes,
536
+ apparent_bytes: rows.reduce((sum, row) => sum + (row.apparent_bytes ?? 0), 0),
537
+ // Rows carrying a per-row `warning`; `warnings` holds store-level sentences.
538
+ row_warnings: rows.filter((row) => row.warning).length,
387
539
  classifications,
388
540
  would_delete: rows.filter((row) => row.action === "would-delete").length,
389
541
  deleted: rows.filter((row) => row.action === "deleted").length,