harnery 0.37.0 → 0.38.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 (184) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -2
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +61 -1
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +48 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +349 -21
  17. package/dist/commands/fetch.js +1 -0
  18. package/dist/commands/qa-record.d.ts +144 -0
  19. package/dist/commands/qa-record.d.ts.map +1 -0
  20. package/dist/commands/qa-record.js +0 -0
  21. package/dist/commands/qa-run.d.ts +7 -3
  22. package/dist/commands/qa-run.d.ts.map +1 -1
  23. package/dist/commands/qa-run.js +268 -13
  24. package/dist/commands/qa-status.d.ts +71 -0
  25. package/dist/commands/qa-status.d.ts.map +1 -0
  26. package/dist/commands/qa-status.js +490 -0
  27. package/dist/commands/qa-verify.d.ts +40 -0
  28. package/dist/commands/qa-verify.d.ts.map +1 -0
  29. package/dist/commands/qa-verify.js +180 -0
  30. package/dist/commands/review-pack.d.ts +4 -0
  31. package/dist/commands/review-pack.d.ts.map +1 -0
  32. package/dist/commands/review-pack.js +1001 -0
  33. package/dist/core/agents/qa-signal.d.ts +111 -0
  34. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  35. package/dist/core/agents/qa-signal.js +231 -0
  36. package/dist/core/agents/session-name-display.d.ts +20 -5
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +67 -7
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  41. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  43. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  44. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  45. package/dist/core/agents/state/live-coordination-view.js +1 -0
  46. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  47. package/dist/core/artifacts/constants.d.ts +1 -1
  48. package/dist/core/artifacts/constants.js +1 -1
  49. package/dist/core/artifacts/index.d.ts +57 -6
  50. package/dist/core/artifacts/index.d.ts.map +1 -1
  51. package/dist/core/artifacts/index.js +265 -10
  52. package/dist/core/config.d.ts +8 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +16 -0
  55. package/dist/core/diagnostics/bundle.d.ts +16 -0
  56. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  57. package/dist/core/diagnostics/bundle.js +101 -7
  58. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  59. package/dist/core/events/v3/bootstrap.js +10 -0
  60. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  61. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  62. package/dist/core/events/v3/coordination-view.js +69 -8
  63. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  64. package/dist/core/events/v3/producers/intake.js +9 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts +23 -0
  66. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  67. package/dist/core/events/v3/producers/recorder.js +264 -18
  68. package/dist/core/hooks/cli.js +120 -27
  69. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  70. package/dist/core/hooks/resolve/transcript.js +10 -3
  71. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  72. package/dist/core/hooks/session-name-presence.js +4 -1
  73. package/dist/core/qa-artifacts.d.ts +20 -0
  74. package/dist/core/qa-artifacts.d.ts.map +1 -0
  75. package/dist/core/qa-artifacts.js +110 -0
  76. package/dist/core/resources/contract.d.ts +6 -0
  77. package/dist/core/resources/contract.d.ts.map +1 -1
  78. package/dist/core/resources/sampler.d.ts +7 -0
  79. package/dist/core/resources/sampler.d.ts.map +1 -1
  80. package/dist/core/resources/sampler.js +106 -5
  81. package/dist/lib/admission.d.ts +71 -0
  82. package/dist/lib/admission.d.ts.map +1 -0
  83. package/dist/lib/admission.js +264 -0
  84. package/dist/lib/agent-browser/client.d.ts +1 -1
  85. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  86. package/dist/lib/agent-browser/client.js +1 -5
  87. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  88. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  89. package/dist/lib/browser/capture-fidelity.js +84 -0
  90. package/dist/lib/browser/client.d.ts +41 -1
  91. package/dist/lib/browser/client.d.ts.map +1 -1
  92. package/dist/lib/browser/client.js +167 -9
  93. package/dist/lib/browser/critique.d.ts +38 -1
  94. package/dist/lib/browser/critique.d.ts.map +1 -1
  95. package/dist/lib/browser/critique.js +34 -6
  96. package/dist/lib/browser/index.d.ts +4 -2
  97. package/dist/lib/browser/index.d.ts.map +1 -1
  98. package/dist/lib/browser/index.js +2 -0
  99. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  100. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  101. package/dist/lib/browser/page-review-judge.js +270 -0
  102. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  103. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  104. package/dist/lib/browser/page-review-pack.js +1751 -0
  105. package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
  106. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  107. package/dist/lib/browser/qa-run-contracts.js +136 -1
  108. package/dist/lib/browser/qa-run.d.ts +100 -10
  109. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  110. package/dist/lib/browser/qa-run.js +768 -169
  111. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  112. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  113. package/dist/lib/browser/request-diagnostics.js +18 -0
  114. package/dist/lib/browser/tiling.d.ts +19 -0
  115. package/dist/lib/browser/tiling.d.ts.map +1 -1
  116. package/dist/lib/browser/tiling.js +28 -0
  117. package/dist/lib/cookies/client.d.ts +9 -0
  118. package/dist/lib/cookies/client.d.ts.map +1 -1
  119. package/dist/lib/cookies/client.js +197 -44
  120. package/dist/lib/cookies/extra.d.ts +18 -0
  121. package/dist/lib/cookies/extra.d.ts.map +1 -0
  122. package/dist/lib/cookies/extra.js +14 -0
  123. package/dist/lib/cookies/index.d.ts +2 -1
  124. package/dist/lib/cookies/index.d.ts.map +1 -1
  125. package/dist/lib/cookies/index.js +2 -1
  126. package/dist/lib/durable-job.d.ts +124 -0
  127. package/dist/lib/durable-job.d.ts.map +1 -0
  128. package/dist/lib/durable-job.js +296 -0
  129. package/dist/lib/http/client.d.ts +7 -1
  130. package/dist/lib/http/client.d.ts.map +1 -1
  131. package/dist/lib/http/client.js +2 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +5 -2
  134. package/package.json +8 -2
  135. package/src/commander.ts +50 -2
  136. package/src/commands/admission.ts +699 -0
  137. package/src/commands/agents.ts +87 -1
  138. package/src/commands/artifacts.ts +97 -21
  139. package/src/commands/browse-ai.ts +10 -5
  140. package/src/commands/browse.ts +481 -20
  141. package/src/commands/fetch.ts +1 -0
  142. package/src/commands/qa-record.ts +682 -0
  143. package/src/commands/qa-run.ts +335 -16
  144. package/src/commands/qa-status.ts +608 -0
  145. package/src/commands/qa-verify.ts +238 -0
  146. package/src/commands/review-pack.ts +1281 -0
  147. package/src/core/agents/qa-signal.ts +261 -0
  148. package/src/core/agents/session-name-display.ts +78 -7
  149. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  150. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  151. package/src/core/agents/state/live-coordination-view.ts +1 -0
  152. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  153. package/src/core/artifacts/constants.ts +1 -1
  154. package/src/core/artifacts/index.ts +370 -21
  155. package/src/core/config.ts +23 -0
  156. package/src/core/diagnostics/bundle.ts +119 -11
  157. package/src/core/events/v3/bootstrap.ts +10 -0
  158. package/src/core/events/v3/coordination-view.ts +100 -11
  159. package/src/core/events/v3/producers/intake.ts +9 -2
  160. package/src/core/events/v3/producers/recorder.ts +312 -18
  161. package/src/core/hooks/cli.ts +140 -32
  162. package/src/core/hooks/resolve/transcript.ts +10 -3
  163. package/src/core/hooks/session-name-presence.ts +6 -1
  164. package/src/core/qa-artifacts.ts +126 -0
  165. package/src/core/resources/contract.ts +7 -0
  166. package/src/core/resources/sampler.ts +138 -6
  167. package/src/lib/admission.ts +347 -0
  168. package/src/lib/agent-browser/client.ts +2 -10
  169. package/src/lib/browser/capture-fidelity.ts +98 -0
  170. package/src/lib/browser/client.ts +206 -10
  171. package/src/lib/browser/critique.ts +62 -7
  172. package/src/lib/browser/index.ts +36 -0
  173. package/src/lib/browser/page-review-judge.ts +360 -0
  174. package/src/lib/browser/page-review-pack.ts +2384 -0
  175. package/src/lib/browser/qa-run-contracts.ts +366 -3
  176. package/src/lib/browser/qa-run.ts +868 -190
  177. package/src/lib/browser/request-diagnostics.ts +27 -0
  178. package/src/lib/browser/tiling.ts +32 -0
  179. package/src/lib/cookies/client.ts +228 -42
  180. package/src/lib/cookies/extra.ts +28 -0
  181. package/src/lib/cookies/index.ts +2 -0
  182. package/src/lib/durable-job.ts +407 -0
  183. package/src/lib/http/client.ts +13 -1
  184. package/src/lib/instructions/templates.ts +5 -2
@@ -56,6 +56,7 @@ import {
56
56
  queueMailboxMessage,
57
57
  suggestNames,
58
58
  } from "../core/agents/mailbox.ts";
59
+ import { qaSignalStatusRow } from "../core/agents/qa-signal.ts";
59
60
  import { reconcileCoordinationV3 } from "../core/agents/reconcile-coordination-v3.ts";
60
61
  import {
61
62
  listSessionFinalizationRequestsV3,
@@ -91,7 +92,10 @@ import {
91
92
  } from "../core/config.ts";
92
93
  import type { EventTypeV3 } from "../core/events/v3/contract.ts";
93
94
  import { readEventV3ControlState } from "../core/events/v3/control.ts";
94
- import { projectCoordinationViewV3 } from "../core/events/v3/coordination-view.ts";
95
+ import {
96
+ projectCoordinationViewV3,
97
+ readCoordinationViewV3,
98
+ } from "../core/events/v3/coordination-view.ts";
95
99
  import { liveInstanceIdV3 } from "../core/events/v3/live-routing.ts";
96
100
  import {
97
101
  countSummarizedSinceV3,
@@ -2399,6 +2403,12 @@ function runStatus(opts: {
2399
2403
  /* non-fatal: status box should not fail on council errors */
2400
2404
  }
2401
2405
 
2406
+ // Latest page-QA verdict for this session, with the RUNNER's clock. Session
2407
+ // age is not QA time; without this row an operator reads "session 58m" as if
2408
+ // the QA run took 58 minutes. Best-effort by contract — no pointer, a
2409
+ // malformed one, or any read failure renders no row.
2410
+ const qaSignal = qaSignalStatusRow({ coordRoot: root, instanceId: hb.instance_id });
2411
+
2402
2412
  const data = {
2403
2413
  name: displayName,
2404
2414
  instance_id: hb.instance_id,
@@ -2436,6 +2446,7 @@ function runStatus(opts: {
2436
2446
  timestamp_iso: new Date().toISOString(),
2437
2447
  timestamp_local: timeStr,
2438
2448
  ...(quality ? { quality } : {}),
2449
+ ...(qaSignal ? { qa: qaSignal.pointer } : {}),
2439
2450
  ...(finalization ? { finalization } : {}),
2440
2451
  ...(sessionEnd ? { session_end: sessionEnd } : {}),
2441
2452
  };
@@ -2474,6 +2485,10 @@ function runStatus(opts: {
2474
2485
  const idx = rows.findIndex((row) => row[0] === "time");
2475
2486
  rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
2476
2487
  }
2488
+ if (qaSignal) {
2489
+ const idx = rows.findIndex((row) => row[0] === "time");
2490
+ rows.splice(idx, 0, ["qa", qaSignal.value]);
2491
+ }
2477
2492
  if (sessionEnd) {
2478
2493
  const idx = rows.findIndex((row) => row[0] === "time");
2479
2494
  rows.splice(idx, 0, [
@@ -3101,6 +3116,13 @@ export type EventLedgerHealthV3 =
3101
3116
  /** Times the summary gate failed open (loose write proceeded). */
3102
3117
  mitigation_fail_open: number;
3103
3118
  };
3119
+ coordination_authority: {
3120
+ safe: boolean;
3121
+ global_diagnostics: number;
3122
+ isolated_diagnostics: number;
3123
+ affected_generations: string[];
3124
+ codes: string[];
3125
+ };
3104
3126
  span_pressure: Array<{ instance_id: string; generation_id: string; span_count: number }>;
3105
3127
  collection_errors: string[];
3106
3128
  };
@@ -3280,6 +3302,57 @@ export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): Ev
3280
3302
  collectionErrors.push(`diagnostic summaries unreadable: ${errorText(error)}`);
3281
3303
  }
3282
3304
 
3305
+ let coordinationAuthority: Extract<
3306
+ EventLedgerHealthV3,
3307
+ { state: "live" }
3308
+ >["coordination_authority"] = {
3309
+ safe: false,
3310
+ global_diagnostics: 0,
3311
+ isolated_diagnostics: 0,
3312
+ affected_generations: [],
3313
+ codes: [],
3314
+ };
3315
+ try {
3316
+ const view = readCoordinationViewV3(root);
3317
+ const globalDiagnostics = view.global_diagnostics.filter(
3318
+ (diagnostic) => diagnostic.authority_blocking,
3319
+ );
3320
+ const isolatedDiagnostics = new Map<string, (typeof globalDiagnostics)[number]>();
3321
+ const affectedGenerations: string[] = [];
3322
+ for (const [generationId, diagnostics] of Object.entries(view.diagnostics_by_generation)) {
3323
+ const blocking = diagnostics.filter((diagnostic) => diagnostic.authority_blocking);
3324
+ if (blocking.length === 0) continue;
3325
+ affectedGenerations.push(generationId);
3326
+ for (const diagnostic of blocking) {
3327
+ isolatedDiagnostics.set(
3328
+ [
3329
+ diagnostic.code,
3330
+ diagnostic.source_code ?? "",
3331
+ diagnostic.event_id ?? "",
3332
+ diagnostic.generation_id ?? "",
3333
+ diagnostic.subject_instance_id ?? "",
3334
+ ].join("\0"),
3335
+ diagnostic,
3336
+ );
3337
+ }
3338
+ }
3339
+ coordinationAuthority = {
3340
+ safe: view.authority_safe,
3341
+ global_diagnostics: globalDiagnostics.length,
3342
+ isolated_diagnostics: isolatedDiagnostics.size,
3343
+ affected_generations: affectedGenerations.sort(),
3344
+ codes: [
3345
+ ...new Set(
3346
+ [...globalDiagnostics, ...isolatedDiagnostics.values()].map(
3347
+ (diagnostic) => diagnostic.source_code ?? diagnostic.code,
3348
+ ),
3349
+ ),
3350
+ ].sort(),
3351
+ };
3352
+ } catch (error) {
3353
+ collectionErrors.push(`coordination authority unreadable: ${errorText(error)}`);
3354
+ }
3355
+
3283
3356
  return {
3284
3357
  state: "live",
3285
3358
  mode: control.state,
@@ -3303,6 +3376,7 @@ export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): Ev
3303
3376
  recent_by_category: recentByCategory,
3304
3377
  mitigation_fail_open: mitigationFailOpen,
3305
3378
  },
3379
+ coordination_authority: coordinationAuthority,
3306
3380
  span_pressure: spanPressure,
3307
3381
  collection_errors: collectionErrors,
3308
3382
  };
@@ -4058,6 +4132,17 @@ function runHealth(opts: { since: string; json?: boolean }): void {
4058
4132
  );
4059
4133
  }
4060
4134
  if (eventLedger.state === "live") {
4135
+ const authority = eventLedger.coordination_authority;
4136
+ if (authority.global_diagnostics > 0) {
4137
+ findings.push(
4138
+ `${authority.global_diagnostics} global coordination authority diagnostic(s): ${authority.codes.join(", ")}`,
4139
+ );
4140
+ }
4141
+ if (authority.isolated_diagnostics > 0) {
4142
+ findings.push(
4143
+ `${authority.isolated_diagnostics} coordination authority diagnostic(s) isolated to ${authority.affected_generations.length} generation(s): ${authority.codes.join(", ")}`,
4144
+ );
4145
+ }
4061
4146
  // Open spans whose generation has no open turn: the turn ended without its
4062
4147
  // tool spans closing, so an explicit end can only queue, never finalize.
4063
4148
  const orphanGenerations = eventLedger.open_spans.generations.filter(
@@ -4198,6 +4283,7 @@ function renderHealthBox(report: HealthReport): void {
4198
4283
  )
4199
4284
  .join(", ");
4200
4285
  ledgerStr = [
4286
+ `authority ${ledger.coordination_authority.safe ? "safe" : "unsafe"}${ledger.coordination_authority.isolated_diagnostics > 0 ? ` (${ledger.coordination_authority.isolated_diagnostics} isolated)` : ""}`,
4201
4287
  `open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
4202
4288
  `pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
4203
4289
  `intake ${ledger.intake_spool.total}`,
@@ -5,12 +5,16 @@ import { readLiveCoordinationRow } from "../core/agents/state/live-coordination-
5
5
  import {
6
6
  type ArtifactActor,
7
7
  adoptUnmanagedArtifactFiles,
8
+ artifactCapabilities,
8
9
  cleanArtifacts,
9
10
  createArtifact,
11
+ holdArtifact,
10
12
  inventoryArtifacts,
13
+ migrateArtifacts,
11
14
  releaseArtifact,
12
15
  renewArtifact,
13
16
  showArtifact,
17
+ unholdArtifact,
14
18
  } from "../core/artifacts/index.ts";
15
19
  import { artifactDefaultRetentionDays, coordFreshnessSeconds } from "../core/config.ts";
16
20
 
@@ -30,28 +34,47 @@ export function registerArtifactsCommand(
30
34
  .requiredOption("--purpose <text>", "What the files are for")
31
35
  .option("--days <n>", "Retention in days (default from artifacts.default_retention_days)")
32
36
  .option("--big", "Acknowledge that this workspace may exceed the per-bundle size ceiling")
33
- .action((slug: string, opts: { purpose: string; days?: string; big?: boolean }) => {
34
- run(emit, () => {
35
- const repoRoot = requireRepoRoot(context);
36
- const actor = currentActor(repoRoot);
37
- const retentionDays = opts.days
38
- ? parseDays(opts.days)
39
- : artifactDefaultRetentionDays(repoRoot);
40
- const created = createArtifact(repoRoot, {
41
- slug,
42
- purpose: opts.purpose,
43
- retentionDays,
44
- actor,
45
- big: opts.big,
46
- });
47
- emit.data({
48
- path: created.path,
49
- artifact_id: created.manifest.artifact_id,
50
- expires_at: created.manifest.retention.expires_at,
51
- owner_instance_id: actor?.instance_id ?? null,
37
+ .option("--hold <id>", "Create the workspace with this hold already present")
38
+ .option("--hold-reason <text>", "Why the initial hold is required")
39
+ .option("--actor <instance-id>", "Stable owner identity for the initial hold")
40
+ .action(
41
+ (
42
+ slug: string,
43
+ opts: {
44
+ purpose: string;
45
+ days?: string;
46
+ big?: boolean;
47
+ hold?: string;
48
+ holdReason?: string;
49
+ actor?: string;
50
+ },
51
+ ) => {
52
+ run(emit, () => {
53
+ const repoRoot = requireRepoRoot(context);
54
+ const actor = opts.actor ? { instance_id: opts.actor } : currentActor(repoRoot);
55
+ if (!!opts.hold !== !!opts.holdReason)
56
+ throw new Error("--hold and --hold-reason must be used together");
57
+ const retentionDays = opts.days
58
+ ? parseDays(opts.days)
59
+ : artifactDefaultRetentionDays(repoRoot);
60
+ const created = createArtifact(repoRoot, {
61
+ slug,
62
+ purpose: opts.purpose,
63
+ retentionDays,
64
+ actor,
65
+ big: opts.big,
66
+ holds: opts.hold ? [{ id: opts.hold, reason: opts.holdReason! }] : [],
67
+ });
68
+ emit.data({
69
+ path: created.path,
70
+ artifact_id: created.manifest.artifact_id,
71
+ expires_at: created.manifest.retention.expires_at,
72
+ owner_instance_id: actor?.instance_id ?? null,
73
+ holds: created.manifest.holds,
74
+ });
52
75
  });
53
- });
54
- });
76
+ },
77
+ );
55
78
 
56
79
  root
57
80
  .command("adopt-unmanaged")
@@ -133,6 +156,53 @@ export function registerArtifactsCommand(
133
156
  });
134
157
  });
135
158
 
159
+ root
160
+ .command("capabilities")
161
+ .description("Report artifact schema and hold capabilities for embedding clients.")
162
+ .option("--json", "Emit the capability document as JSON")
163
+ .action(() => emit.data(artifactCapabilities()));
164
+
165
+ root
166
+ .command("migrate")
167
+ .description("Preview the explicit v1 to v2 manifest migration, preserving preimages.")
168
+ .option("--yes", "Apply the migration to valid v1 manifests")
169
+ .action((opts: { yes?: boolean }) =>
170
+ run(emit, () => emit.data({ rows: migrateArtifacts(requireRepoRoot(context), opts) })),
171
+ );
172
+
173
+ root
174
+ .command("hold <ref>")
175
+ .description("Protect an artifact from cleanup until its owner removes this exact hold.")
176
+ .requiredOption("--id <id>", "Unique hold id within this artifact")
177
+ .requiredOption("--reason <text>", "Why cleanup must retain these files")
178
+ .option("--actor <instance-id>", "Stable hold owner; defaults to the current agent")
179
+ .action((ref: string, opts: { id: string; reason: string; actor?: string }) =>
180
+ run(emit, () => {
181
+ const repoRoot = requireRepoRoot(context);
182
+ emit.data(
183
+ holdArtifact(repoRoot, ref, {
184
+ id: opts.id,
185
+ reason: opts.reason,
186
+ actor: requireHoldActor(repoRoot, opts.actor),
187
+ }),
188
+ );
189
+ }),
190
+ );
191
+
192
+ root
193
+ .command("unhold <ref>")
194
+ .description("Remove one exact hold belonging to the supplied owner.")
195
+ .requiredOption("--id <id>", "Exact hold id to remove")
196
+ .option("--actor <instance-id>", "Stable hold owner; defaults to the current agent")
197
+ .action((ref: string, opts: { id: string; actor?: string }) =>
198
+ run(emit, () => {
199
+ const repoRoot = requireRepoRoot(context);
200
+ emit.data(
201
+ unholdArtifact(repoRoot, ref, opts.id, { actor: requireHoldActor(repoRoot, opts.actor) }),
202
+ );
203
+ }),
204
+ );
205
+
136
206
  root
137
207
  .command("clean")
138
208
  .description("Preview expired artifact deletion; pass --yes to delete.")
@@ -149,6 +219,12 @@ export function registerArtifactsCommand(
149
219
  });
150
220
  }
151
221
 
222
+ function requireHoldActor(repoRoot: string, explicit?: string): ArtifactActor {
223
+ const actor = explicit ? { instance_id: explicit } : currentActor(repoRoot);
224
+ if (!actor) throw new Error("hold operations require --actor or a current agent identity");
225
+ return actor;
226
+ }
227
+
152
228
  function currentActor(repoRoot: string): ArtifactActor | undefined {
153
229
  const instanceId = resolveOwner();
154
230
  if (!instanceId) return undefined;
@@ -1,9 +1,9 @@
1
1
  import { homedir } from "node:os";
2
2
  import { resolve } from "node:path";
3
3
  import type { Command } from "commander";
4
- import type { EmitContext } from "../commander.ts";
4
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
5
5
  import { AgentBrowser, type ExecResult } from "../lib/agent-browser/index.ts";
6
- import { CookieJar } from "../lib/cookies/index.ts";
6
+ import { applyExtraCookies, CookieJar } from "../lib/cookies/index.ts";
7
7
 
8
8
  // Module-scoped emit assigned by registerBrowseAiCommand. Same pattern as
9
9
  // cookies/read/browse: helper functions close over `emit` so action
@@ -50,7 +50,11 @@ interface BrowseAiOpts {
50
50
  timeout: string;
51
51
  }
52
52
 
53
- export function registerBrowseAiCommand(program: Command, emitParam: EmitContext): void {
53
+ export function registerBrowseAiCommand(
54
+ program: Command,
55
+ emitParam: EmitContext,
56
+ context?: HarneryProgramContext,
57
+ ): void {
54
58
  emit = emitParam;
55
59
  program
56
60
  .command("browse-ai <url>")
@@ -86,7 +90,7 @@ export function registerBrowseAiCommand(program: Command, emitParam: EmitContext
86
90
  .option("--timeout <ms>", "Per-step timeout in ms", "60000")
87
91
  .action((url: string, opts: BrowseAiOpts) => {
88
92
  try {
89
- runBrowseAi(url, opts);
93
+ runBrowseAi(url, opts, context);
90
94
  } catch (err: unknown) {
91
95
  const msg = err instanceof Error ? err.message : String(err);
92
96
  emit.error({ code: "browse_ai_error", message: msg });
@@ -95,11 +99,12 @@ export function registerBrowseAiCommand(program: Command, emitParam: EmitContext
95
99
  });
96
100
  }
97
101
 
98
- function runBrowseAi(url: string, opts: BrowseAiOpts): void {
102
+ function runBrowseAi(url: string, opts: BrowseAiOpts, context?: HarneryProgramContext): void {
99
103
  const jar =
100
104
  opts.cookies === false
101
105
  ? null
102
106
  : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse-ai" });
107
+ if (jar) applyExtraCookies(url, jar, context?.extraCookies);
103
108
  const ab = new AgentBrowser({
104
109
  jar,
105
110
  timeoutMs: Number.parseInt(opts.timeout, 10),