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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Per-session page-QA signal: the pointer that lets `agents status` report a
3
+ * verdict with the runner's own clock instead of leaving an operator to read
4
+ * session age as if it were QA time.
5
+ *
6
+ * The problem this closes: a status box showing `session 58m` invites the
7
+ * reading "page QA took 58 minutes". Session age measures the agent, not the
8
+ * run. So the runner's wall time is recorded here and rendered beside the
9
+ * verdict, and admission-queue wait is carried as a separate number because
10
+ * `wall_time_ms.total` is runner stages only and never includes the queue
11
+ * (see QaRunResult.wall_time_ms in the toolkit contracts).
12
+ *
13
+ * Layering: the qa-run matrix runner is toolkit tier (`src/lib/browser/**`)
14
+ * and must not import `src/core`, so the runner cannot write this pointer
15
+ * itself. The command layer owns the write and calls `recordQaSignal()` after
16
+ * a run finishes. Core importing toolkit *types* is the permitted direction.
17
+ *
18
+ * Storage: `<coordRoot>/.harnery/qa/<instance-id>.json`, one file per session
19
+ * generation, atomic temp+rename so a concurrent status read never sees a
20
+ * torn write. Last run wins; each run directory remains the authoritative
21
+ * record of the run itself.
22
+ *
23
+ * Every function here is best-effort by contract. A missing, unreadable,
24
+ * malformed, or partial pointer yields null rather than throwing: the status
25
+ * box must render even when QA state is broken.
26
+ */
27
+ import type { QaRunEvidenceSource, QaRunResult, QaRunVerdict } from "../../lib/browser/qa-run-contracts.js";
28
+ export declare const QA_SIGNAL_SCHEMA_VERSION: 1;
29
+ /** Pointers older than this render as `stale (<age>)` and nothing else: an
30
+ * age-of-day-old verdict says nothing about the page as it stands now, and
31
+ * showing its timings beside a current session invites the same conflation
32
+ * this signal exists to prevent. */
33
+ export declare const QA_SIGNAL_STALE_AFTER_MS: number;
34
+ /** Timings split the way the result contract splits them: `total` is runner
35
+ * stages only, `queue` is admission wait before any browser work and is never
36
+ * part of `total`. */
37
+ export interface QaSignalWallTime {
38
+ total: number;
39
+ queue?: number;
40
+ }
41
+ export interface QaSignalPointer {
42
+ schema_version: typeof QA_SIGNAL_SCHEMA_VERSION;
43
+ run_id: string;
44
+ verdict: QaRunVerdict;
45
+ evidence_source: QaRunEvidenceSource;
46
+ /** ISO-8601 UTC instant the run completed. */
47
+ completed_at: string;
48
+ /** Absolute run directory holding the authoritative result document. */
49
+ out_dir: string;
50
+ wall_time_ms: QaSignalWallTime;
51
+ target: string;
52
+ }
53
+ /**
54
+ * Absolute path of one session's QA pointer. Throws on an instance id that
55
+ * could escape the directory; callers in this module treat that as "no
56
+ * pointer" rather than propagating it.
57
+ */
58
+ export declare function qaSignalPath(coordRoot: string, instanceId: string): string;
59
+ export interface QaSignalTarget {
60
+ /** Defaults to the resolved coordination root. */
61
+ coordRoot?: string | null;
62
+ /** Defaults to the current session's instance id. */
63
+ instanceId?: string | null;
64
+ }
65
+ /**
66
+ * Write the pointer for one completed run. Returns what was written, or null
67
+ * when the session/root could not be resolved or the write failed — a QA run
68
+ * must never fail because its status breadcrumb could not be recorded.
69
+ */
70
+ export declare function recordQaSignal(result: QaRunResult, target?: QaSignalTarget): QaSignalPointer | null;
71
+ /** Read one session's pointer. Null when absent, unreadable, or malformed. */
72
+ export declare function readQaSignal(target?: QaSignalTarget): QaSignalPointer | null;
73
+ /**
74
+ * Validate an untrusted pointer document. Fail-closed: a partial document —
75
+ * the shape a torn write or a schema change produces — is not a pointer, and
76
+ * renders no row at all rather than a half-true one.
77
+ */
78
+ export declare function parseQaSignal(document: unknown): QaSignalPointer | null;
79
+ /**
80
+ * Coarse single-unit age, matching the status box's existing age vocabulary
81
+ * (`4m`, `3h`, `2d`). Deliberately coarser than the duration formatter: an
82
+ * operator reads age to judge relevance, not to compare timings.
83
+ */
84
+ export declare function formatQaSignalAge(ms: number): string;
85
+ /**
86
+ * Duration in the units an operator compares runs by. Seconds stay seconds up
87
+ * to two minutes so a 90-second run reads `90s` rather than being rounded into
88
+ * a minute bucket that hides the difference between fast runs.
89
+ */
90
+ export declare function formatQaSignalDuration(ms: number): string;
91
+ /**
92
+ * Render the status-box `qa` value, or null when there is nothing honest to
93
+ * say. Three shapes:
94
+ * - fresh runner evidence: `passed 4m ago · 90s runner (2m queued)`
95
+ * - fresh manual evidence: `manual 12m ago · not a pass`
96
+ * - anything over the staleness horizon: `stale (2d)`
97
+ *
98
+ * Manual evidence never reports a verdict or a runner clock: nothing
99
+ * re-executable ran, so the contract caps it below a pass and there is no
100
+ * runner time to attribute.
101
+ */
102
+ export declare function formatQaSignalRow(pointer: QaSignalPointer | null, now?: number): string | null;
103
+ /**
104
+ * The whole status-box contribution in one best-effort call: read this
105
+ * session's pointer and render its row. Null means render no `qa` row.
106
+ */
107
+ export declare function qaSignalStatusRow(target?: QaSignalTarget, now?: number): {
108
+ value: string;
109
+ pointer: QaSignalPointer;
110
+ } | null;
111
+ //# sourceMappingURL=qa-signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qa-signal.d.ts","sourceRoot":"","sources":["../../../src/core/agents/qa-signal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EACX,YAAY,EACb,MAAM,uCAAuC,CAAC;AAG/C,eAAO,MAAM,wBAAwB,EAAG,CAAU,CAAC;AAEnD;;;oCAGoC;AACpC,eAAO,MAAM,wBAAwB,QAAsB,CAAC;AAE5D;;sBAEsB;AACtB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,OAAO,wBAAwB,CAAC;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,EAAE,mBAAmB,CAAC;IACrC,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAU1E;AAED,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AASD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,MAAM,GAAE,cAAmB,GAC1B,eAAe,GAAG,IAAI,CA4BxB;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,MAAM,GAAE,cAAmB,GAAG,eAAe,GAAG,IAAI,CAUhF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CA6BvE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,eAAe,GAAG,IAAI,EAC/B,GAAG,GAAE,MAAmB,GACvB,MAAM,GAAG,IAAI,CAcf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,cAAmB,EAC3B,GAAG,GAAE,MAAmB,GACvB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAAG,IAAI,CASpD"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Per-session page-QA signal: the pointer that lets `agents status` report a
3
+ * verdict with the runner's own clock instead of leaving an operator to read
4
+ * session age as if it were QA time.
5
+ *
6
+ * The problem this closes: a status box showing `session 58m` invites the
7
+ * reading "page QA took 58 minutes". Session age measures the agent, not the
8
+ * run. So the runner's wall time is recorded here and rendered beside the
9
+ * verdict, and admission-queue wait is carried as a separate number because
10
+ * `wall_time_ms.total` is runner stages only and never includes the queue
11
+ * (see QaRunResult.wall_time_ms in the toolkit contracts).
12
+ *
13
+ * Layering: the qa-run matrix runner is toolkit tier (`src/lib/browser/**`)
14
+ * and must not import `src/core`, so the runner cannot write this pointer
15
+ * itself. The command layer owns the write and calls `recordQaSignal()` after
16
+ * a run finishes. Core importing toolkit *types* is the permitted direction.
17
+ *
18
+ * Storage: `<coordRoot>/.harnery/qa/<instance-id>.json`, one file per session
19
+ * generation, atomic temp+rename so a concurrent status read never sees a
20
+ * torn write. Last run wins; each run directory remains the authoritative
21
+ * record of the run itself.
22
+ *
23
+ * Every function here is best-effort by contract. A missing, unreadable,
24
+ * malformed, or partial pointer yields null rather than throwing: the status
25
+ * box must render even when QA state is broken.
26
+ */
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
28
+ import { resolve } from "node:path";
29
+ import { resolveCoordRoot, resolveOwner } from "./coord-client.js";
30
+ export const QA_SIGNAL_SCHEMA_VERSION = 1;
31
+ /** Pointers older than this render as `stale (<age>)` and nothing else: an
32
+ * age-of-day-old verdict says nothing about the page as it stands now, and
33
+ * showing its timings beside a current session invites the same conflation
34
+ * this signal exists to prevent. */
35
+ export const QA_SIGNAL_STALE_AFTER_MS = 24 * 60 * 60 * 1000;
36
+ const INSTANCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
37
+ const VERDICTS = ["passed", "failed", "incomplete"];
38
+ const EVIDENCE_SOURCES = ["runner", "manual"];
39
+ /**
40
+ * Absolute path of one session's QA pointer. Throws on an instance id that
41
+ * could escape the directory; callers in this module treat that as "no
42
+ * pointer" rather than propagating it.
43
+ */
44
+ export function qaSignalPath(coordRoot, instanceId) {
45
+ if (!INSTANCE_ID_PATTERN.test(instanceId)) {
46
+ throw new Error("instance_id must be 1-128 ASCII letters, digits, hyphens, or underscores");
47
+ }
48
+ const directory = resolve(coordRoot, ".harnery", "qa");
49
+ const candidate = resolve(directory, `${instanceId}.json`);
50
+ if (!candidate.startsWith(`${directory}/`)) {
51
+ throw new Error("resolved qa signal path escapes the qa directory");
52
+ }
53
+ return candidate;
54
+ }
55
+ function resolveTarget(target) {
56
+ const coordRoot = target.coordRoot ?? resolveCoordRoot();
57
+ const instanceId = target.instanceId ?? resolveOwner();
58
+ if (!coordRoot || !instanceId)
59
+ return null;
60
+ return { coordRoot, instanceId };
61
+ }
62
+ /**
63
+ * Write the pointer for one completed run. Returns what was written, or null
64
+ * when the session/root could not be resolved or the write failed — a QA run
65
+ * must never fail because its status breadcrumb could not be recorded.
66
+ */
67
+ export function recordQaSignal(result, target = {}) {
68
+ try {
69
+ const resolved = resolveTarget(target);
70
+ if (!resolved)
71
+ return null;
72
+ const pointer = {
73
+ schema_version: QA_SIGNAL_SCHEMA_VERSION,
74
+ run_id: result.run.run_id,
75
+ verdict: result.verdict,
76
+ evidence_source: result.evidence_source,
77
+ completed_at: result.run.completed_at,
78
+ out_dir: result.run.out_dir,
79
+ wall_time_ms: {
80
+ total: result.wall_time_ms.total,
81
+ ...(typeof result.wall_time_ms.queue === "number"
82
+ ? { queue: result.wall_time_ms.queue }
83
+ : {}),
84
+ },
85
+ target: result.target,
86
+ };
87
+ const path = qaSignalPath(resolved.coordRoot, resolved.instanceId);
88
+ mkdirSync(resolve(path, ".."), { recursive: true });
89
+ const tmp = `${path}.tmp.${process.pid}`;
90
+ writeFileSync(tmp, `${JSON.stringify(pointer, null, 2)}\n`, "utf8");
91
+ renameSync(tmp, path);
92
+ return pointer;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /** Read one session's pointer. Null when absent, unreadable, or malformed. */
99
+ export function readQaSignal(target = {}) {
100
+ try {
101
+ const resolved = resolveTarget(target);
102
+ if (!resolved)
103
+ return null;
104
+ const path = qaSignalPath(resolved.coordRoot, resolved.instanceId);
105
+ if (!existsSync(path))
106
+ return null;
107
+ return parseQaSignal(JSON.parse(readFileSync(path, "utf8")));
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ /**
114
+ * Validate an untrusted pointer document. Fail-closed: a partial document —
115
+ * the shape a torn write or a schema change produces — is not a pointer, and
116
+ * renders no row at all rather than a half-true one.
117
+ */
118
+ export function parseQaSignal(document) {
119
+ if (!document || typeof document !== "object" || Array.isArray(document))
120
+ return null;
121
+ const value = document;
122
+ if (value.schema_version !== QA_SIGNAL_SCHEMA_VERSION)
123
+ return null;
124
+ if (typeof value.run_id !== "string" || value.run_id.length === 0)
125
+ return null;
126
+ if (!VERDICTS.includes(value.verdict))
127
+ return null;
128
+ if (!EVIDENCE_SOURCES.includes(value.evidence_source))
129
+ return null;
130
+ if (typeof value.completed_at !== "string" || Number.isNaN(Date.parse(value.completed_at))) {
131
+ return null;
132
+ }
133
+ if (typeof value.out_dir !== "string" || value.out_dir.length === 0)
134
+ return null;
135
+ if (typeof value.target !== "string" || value.target.length === 0)
136
+ return null;
137
+ const wall = value.wall_time_ms;
138
+ if (!wall || typeof wall !== "object" || Array.isArray(wall))
139
+ return null;
140
+ const { total, queue } = wall;
141
+ if (typeof total !== "number" || !Number.isFinite(total) || total < 0)
142
+ return null;
143
+ if (queue !== undefined && (typeof queue !== "number" || !Number.isFinite(queue) || queue < 0)) {
144
+ return null;
145
+ }
146
+ return {
147
+ schema_version: QA_SIGNAL_SCHEMA_VERSION,
148
+ run_id: value.run_id,
149
+ verdict: value.verdict,
150
+ evidence_source: value.evidence_source,
151
+ completed_at: value.completed_at,
152
+ out_dir: value.out_dir,
153
+ wall_time_ms: { total, ...(typeof queue === "number" ? { queue } : {}) },
154
+ target: value.target,
155
+ };
156
+ }
157
+ /**
158
+ * Coarse single-unit age, matching the status box's existing age vocabulary
159
+ * (`4m`, `3h`, `2d`). Deliberately coarser than the duration formatter: an
160
+ * operator reads age to judge relevance, not to compare timings.
161
+ */
162
+ export function formatQaSignalAge(ms) {
163
+ const seconds = Math.max(0, Math.floor(ms / 1000));
164
+ if (seconds < 60)
165
+ return `${seconds}s`;
166
+ if (seconds < 3600)
167
+ return `${Math.floor(seconds / 60)}m`;
168
+ if (seconds < 86400)
169
+ return `${Math.floor(seconds / 3600)}h`;
170
+ return `${Math.floor(seconds / 86400)}d`;
171
+ }
172
+ /**
173
+ * Duration in the units an operator compares runs by. Seconds stay seconds up
174
+ * to two minutes so a 90-second run reads `90s` rather than being rounded into
175
+ * a minute bucket that hides the difference between fast runs.
176
+ */
177
+ export function formatQaSignalDuration(ms) {
178
+ const value = Math.max(0, ms);
179
+ if (value < 120_000)
180
+ return `${Math.round(value / 1000)}s`;
181
+ if (value < 3_600_000)
182
+ return `${Math.round(value / 60_000)}m`;
183
+ const hours = Math.floor(value / 3_600_000);
184
+ const minutes = Math.round((value % 3_600_000) / 60_000);
185
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
186
+ }
187
+ /**
188
+ * Render the status-box `qa` value, or null when there is nothing honest to
189
+ * say. Three shapes:
190
+ * - fresh runner evidence: `passed 4m ago · 90s runner (2m queued)`
191
+ * - fresh manual evidence: `manual 12m ago · not a pass`
192
+ * - anything over the staleness horizon: `stale (2d)`
193
+ *
194
+ * Manual evidence never reports a verdict or a runner clock: nothing
195
+ * re-executable ran, so the contract caps it below a pass and there is no
196
+ * runner time to attribute.
197
+ */
198
+ export function formatQaSignalRow(pointer, now = Date.now()) {
199
+ if (!pointer)
200
+ return null;
201
+ const completedAt = Date.parse(pointer.completed_at);
202
+ if (Number.isNaN(completedAt))
203
+ return null;
204
+ const ageMs = Math.max(0, now - completedAt);
205
+ const age = formatQaSignalAge(ageMs);
206
+ if (ageMs > QA_SIGNAL_STALE_AFTER_MS)
207
+ return `stale (${age})`;
208
+ if (pointer.evidence_source === "manual")
209
+ return `manual ${age} ago · not a pass`;
210
+ const runner = `${formatQaSignalDuration(pointer.wall_time_ms.total)} runner`;
211
+ const queued = typeof pointer.wall_time_ms.queue === "number" && pointer.wall_time_ms.queue > 0
212
+ ? ` (${formatQaSignalDuration(pointer.wall_time_ms.queue)} queued)`
213
+ : "";
214
+ return `${pointer.verdict} ${age} ago · ${runner}${queued}`;
215
+ }
216
+ /**
217
+ * The whole status-box contribution in one best-effort call: read this
218
+ * session's pointer and render its row. Null means render no `qa` row.
219
+ */
220
+ export function qaSignalStatusRow(target = {}, now = Date.now()) {
221
+ try {
222
+ const pointer = readQaSignal(target);
223
+ const value = formatQaSignalRow(pointer, now);
224
+ if (!pointer || !value)
225
+ return null;
226
+ return { value, pointer };
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
@@ -7,9 +7,29 @@
7
7
  export interface SessionNameDisplayState {
8
8
  suggested_session_name?: string;
9
9
  session_name_seen_for?: string;
10
+ session_name_display_requested_for?: string;
10
11
  }
11
12
  export declare const SESSION_NAME_DISPLAY_NOTE = "Next, before any prose or another tool call, send suggested_session_name verbatim by itself in a fenced code block.";
12
13
  export declare function sessionNameDisplayPending(row: SessionNameDisplayState | null | undefined): string | null;
14
+ /**
15
+ * Every title whose exact display satisfies the current latch.
16
+ *
17
+ * The pending title always counts. The title the agent was last instructed to
18
+ * display also counts, because the harness — not the agent — is what changed
19
+ * the target after asking. Accepting it costs nothing the operator can see: a
20
+ * clean one-line block did open that reply. Refusing it left Cursor sessions
21
+ * permanently latched, since no Cursor surface can re-open a missed window.
22
+ */
23
+ export declare function sessionNameDisplayAcceptedNames(row: SessionNameDisplayState | null | undefined): string[];
24
+ /**
25
+ * Match assistant text against any accepted title and report which one closed
26
+ * the latch, so a drifted display can be stamped against the pending title and
27
+ * logged as drift rather than silently discarded.
28
+ */
29
+ export declare function matchSessionNameDisplay(row: SessionNameDisplayState | null | undefined, text: string | undefined, startsWithBlock?: (text: string, expectedName: string) => boolean): {
30
+ pending: string;
31
+ displayed: string;
32
+ } | null;
13
33
  export declare function sessionNameDisplayBlock(name: string): string;
14
34
  export declare function sessionNameDisplayInstruction(name: string): string;
15
35
  export declare function sessionNameDisplayRecoveryInstruction(name: string, binName: string): string;
@@ -26,11 +46,6 @@ export declare function sessionNameDisplayRecoveryInstruction(name: string, binN
26
46
  * activating the display ritual.
27
47
  */
28
48
  export declare function toolResponseMintedSessionName(response: unknown, name: string): boolean;
29
- /**
30
- * Accept the exact unlabelled block Harnery requests. `text` and `plaintext`
31
- * are tolerated for older prompts, but the code block may contain only the
32
- * suggested name and must be the first non-whitespace user-facing content.
33
- */
34
49
  export declare function assistantTextStartsWithSessionNameBlock(text: string, name: string): boolean;
35
50
  /**
36
51
  * The display latch must not block the two coordination commands that can
@@ -1 +1 @@
1
- {"version":3,"file":"session-name-display.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-name-display.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,uBAAuB;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,eAAO,MAAM,yBAAyB,wHACiF,CAAC;AAExH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAC9C,MAAM,GAAG,IAAI,CAIf;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlE;AAED,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtF;AAgDD;;;;GAIG;AACH,wBAAgB,uCAAuC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAK3F;AAUD;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAmBT"}
1
+ {"version":3,"file":"session-name-display.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-name-display.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,uBAAuB;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kCAAkC,CAAC,EAAE,MAAM,CAAC;CAC7C;AAED,eAAO,MAAM,yBAAyB,wHACiF,CAAC;AAExH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAC9C,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAC9C,MAAM,EAAE,CAKV;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,EAC/C,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,eAAe,GAAE,CACf,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,KACjB,OAAiD,GACrD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO/C;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlE;AAED,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtF;AAqFD,wBAAgB,uCAAuC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3F;AAUD;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAmBT"}
@@ -11,6 +11,37 @@ export function sessionNameDisplayPending(row) {
11
11
  return null;
12
12
  return name;
13
13
  }
14
+ /**
15
+ * Every title whose exact display satisfies the current latch.
16
+ *
17
+ * The pending title always counts. The title the agent was last instructed to
18
+ * display also counts, because the harness — not the agent — is what changed
19
+ * the target after asking. Accepting it costs nothing the operator can see: a
20
+ * clean one-line block did open that reply. Refusing it left Cursor sessions
21
+ * permanently latched, since no Cursor surface can re-open a missed window.
22
+ */
23
+ export function sessionNameDisplayAcceptedNames(row) {
24
+ const pending = sessionNameDisplayPending(row);
25
+ if (!pending)
26
+ return [];
27
+ const requested = row?.session_name_display_requested_for;
28
+ return requested && requested !== pending ? [pending, requested] : [pending];
29
+ }
30
+ /**
31
+ * Match assistant text against any accepted title and report which one closed
32
+ * the latch, so a drifted display can be stamped against the pending title and
33
+ * logged as drift rather than silently discarded.
34
+ */
35
+ export function matchSessionNameDisplay(row, text, startsWithBlock = assistantTextStartsWithSessionNameBlock) {
36
+ if (!text)
37
+ return null;
38
+ const accepted = sessionNameDisplayAcceptedNames(row);
39
+ const pending = accepted[0];
40
+ if (!pending)
41
+ return null;
42
+ const displayed = accepted.find((candidate) => startsWithBlock(text, candidate));
43
+ return displayed ? { pending, displayed } : null;
44
+ }
14
45
  export function sessionNameDisplayBlock(name) {
15
46
  return `\`\`\`\n${name}\n\`\`\``;
16
47
  }
@@ -62,10 +93,16 @@ function responseContainsSessionNameMint(value, name, seen, depth) {
62
93
  }
63
94
  }
64
95
  catch {
65
- // Wrapper prose and partial JSON are not authoritative mint evidence.
96
+ // Fall through to the textual check below.
66
97
  }
67
98
  }
68
- return false;
99
+ // The mint command's stdout often reaches the transcript mangled: cut
100
+ // short by `cut`/`head -c`, or with a tail-truncated first line. Whole-JSON
101
+ // parsing then fails even though the name and the mint flag are both
102
+ // legible, and a session that did mint can never latch. Accept the text
103
+ // when the exact quoted name sits under its key AND a mint flag is true;
104
+ // ordinary status output carries the name without any flag.
105
+ return textCarriesSessionNameMint(value, name);
69
106
  }
70
107
  if (typeof value !== "object")
71
108
  return false;
@@ -82,17 +119,40 @@ function responseContainsSessionNameMint(value, name, seen, depth) {
82
119
  }
83
120
  return Object.values(row).some((item) => responseContainsSessionNameMint(item, name, seen, depth + 1));
84
121
  }
122
+ const MINT_FLAG_PATTERN = /"(?:first_of_session|name_reminted|session_name_retry)"\s*:\s*true\b/;
123
+ function textCarriesSessionNameMint(text, name) {
124
+ const quotedName = JSON.stringify(name);
125
+ const keyIndex = text.indexOf('"suggested_session_name"');
126
+ if (keyIndex < 0)
127
+ return false;
128
+ const afterKey = text.slice(keyIndex + '"suggested_session_name"'.length);
129
+ const valueMatch = /^\s*:\s*("(?:[^"\\]|\\.)*")/.exec(afterKey);
130
+ if (!valueMatch || valueMatch[1] !== quotedName)
131
+ return false;
132
+ return MINT_FLAG_PATTERN.test(text);
133
+ }
85
134
  /**
86
- * Accept the exact unlabelled block Harnery requests. `text` and `plaintext`
87
- * are tolerated for older prompts, but the code block may contain only the
88
- * suggested name and must be the first non-whitespace user-facing content.
135
+ * A leading fenced block whose only line is the suggested name.
136
+ *
137
+ * What the contract protects is what the operator sees: the reply opens with a
138
+ * code block containing exactly the title, so it renders as one clean
139
+ * copy-pasteable line. The fence's info string changes none of that, so any
140
+ * single-word language tag passes. Restricting it to `text`/`plaintext` meant a
141
+ * model that wrote ```txt or ```markdown produced a display the operator could
142
+ * read perfectly while the latch refused to close — and since nothing else can
143
+ * close a Cursor latch, that mismatch stranded the whole session.
144
+ *
145
+ * Still exact in every load-bearing way: the block is the first user-facing
146
+ * content, holds one line, and that line equals the name. The backreference
147
+ * requires the closing fence to match the opening run, so a longer fence cannot
148
+ * be closed by a shorter one.
89
149
  */
150
+ const LEADING_SESSION_NAME_BLOCK = /^(`{3,})[ \t]*[A-Za-z0-9_.+#-]*[ \t]*\n([^\n]*)\n\1[ \t]*(?:\n|$)/;
90
151
  export function assistantTextStartsWithSessionNameBlock(text, name) {
91
152
  if (!text || !name)
92
153
  return false;
93
154
  const normalized = text.replace(/\r\n?/g, "\n").trimStart();
94
- const match = /^```(?:text|plaintext)?[ \t]*\n([^\n]*)\n```(?:[ \t]*(?:\n|$))/.exec(normalized);
95
- return match?.[1] === name;
155
+ return LEADING_SESSION_NAME_BLOCK.exec(normalized)?.[2] === name;
96
156
  }
97
157
  /**
98
158
  * A trailing stream redirect to `/dev/null` or another descriptor. Agents append
@@ -29,6 +29,13 @@ export interface Heartbeat {
29
29
  session_name_seen_at?: string;
30
30
  /** The specific suggested name proven present by the sighting stamp. */
31
31
  session_name_seen_for?: string;
32
+ /**
33
+ * The suggested name the agent was last actually instructed to display.
34
+ * Usually identical to `suggested_session_name`; it differs only when the
35
+ * title changed after the instruction went out, and a display of it still
36
+ * counts so that drift cannot strand the latch.
37
+ */
38
+ session_name_display_requested_for?: string;
32
39
  last_status_at?: string;
33
40
  current_turn_id?: string;
34
41
  parent_instance_id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-reader.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-reader.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAIhE,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uFAAuF;IACvF,cAAc,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC;IAClC,gBAAgB,CAAC,EAAE,OAAO,MAAM,EAAE,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,aAAa,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAClC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAU3E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQrF"}
1
+ {"version":3,"file":"heartbeat-reader.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-reader.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAIhE,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uFAAuF;IACvF,cAAc,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC;IAClC,gBAAgB,CAAC,EAAE,OAAO,MAAM,EAAE,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,aAAa,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAClC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAU3E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQrF"}
@@ -44,6 +44,17 @@ export declare function buildLifecycleSuggestedName(currentSessionName: string |
44
44
  * different suggested name, a bare "already seen" stamp would suppress the scan
45
45
  * forever and leave the Stop-hook naming rule permanently unsatisfiable. */
46
46
  export declare function stampSessionNameSeen(coordRoot: string, instanceId: string, name?: string): Heartbeat | null;
47
+ /**
48
+ * Remember which title the agent was actually asked to display.
49
+ *
50
+ * The gate can only ask for a display through the PostToolUse instruction, and
51
+ * the title it names can change afterwards (an assigned-name rewrite of an
52
+ * `Agent unknown - ...` suggestion, a lifecycle re-mint, a rebuilt cache that
53
+ * re-minted from the current task). Without this record the agent's correct
54
+ * display of the title it was handed matches nothing, the latch never closes,
55
+ * and on Cursor no later surface can repair it.
56
+ */
57
+ export declare function stampSessionNameRequested(coordRoot: string, instanceId: string, name: string): Heartbeat | null;
47
58
  export declare function releaseClaim(coordRoot: string, instanceId: string, path: string): Heartbeat | null;
48
59
  /** Materialize one V3 write-claim authority transition into the heartbeat cache. */
49
60
  export declare function acquireClaim(coordRoot: string, instanceId: string, path: string): Heartbeat | null;
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,OAAO,EAAE,KAAK,SAAS,EAAgC,MAAM,uBAAuB,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,KAAK,SAAS,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAatE,kFAAkF;AAClF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,oBAAoB,CAAC,EAAE,MAAM,GAC5B,SAAS,GAAG,IAAI,CAQlB;AAeD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAoB7F;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,SAAS,GAAG,IAAI,CAOlB;AAED;;;;6BAI6B;AAC7B,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,GAAG,UAAU,GAAG,WAAuB,GACrD,SAAS,GAAG,IAAI,CAelB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EAAE,GACzB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvD;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CACzC,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,KAAK,EAAE,SAAS,GACf,MAAM,GAAG,IAAI,CAOf;AAED;;;;4EAI4E;AAC5E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,SAAS,GAAG,IAAI,CAMlB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAQlB;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAWlB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,eAAe,EAAE,CA6BnB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CASrF;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAiBnF"}
1
+ {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,OAAO,EAAE,KAAK,SAAS,EAAgC,MAAM,uBAAuB,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,KAAK,SAAS,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAatE,kFAAkF;AAClF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,oBAAoB,CAAC,EAAE,MAAM,GAC5B,SAAS,GAAG,IAAI,CAQlB;AAeD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAoB7F;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,SAAS,GAAG,IAAI,CAOlB;AAED;;;;6BAI6B;AAC7B,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,GAAG,UAAU,GAAG,WAAuB,GACrD,SAAS,GAAG,IAAI,CAelB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EAAE,GACzB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvD;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CACzC,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,KAAK,EAAE,SAAS,GACf,MAAM,GAAG,IAAI,CAOf;AAED;;;;4EAI4E;AAC5E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,SAAS,GAAG,IAAI,CAMlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAOlB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAQlB;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAWlB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,eAAe,EAAE,CA6BnB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CASrF;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAiBnF"}
@@ -128,6 +128,23 @@ export function stampSessionNameSeen(coordRoot, instanceId, name) {
128
128
  ...(name ? { session_name_seen_for: name } : {}),
129
129
  }));
130
130
  }
131
+ /**
132
+ * Remember which title the agent was actually asked to display.
133
+ *
134
+ * The gate can only ask for a display through the PostToolUse instruction, and
135
+ * the title it names can change afterwards (an assigned-name rewrite of an
136
+ * `Agent unknown - ...` suggestion, a lifecycle re-mint, a rebuilt cache that
137
+ * re-minted from the current task). Without this record the agent's correct
138
+ * display of the title it was handed matches nothing, the latch never closes,
139
+ * and on Cursor no later surface can repair it.
140
+ */
141
+ export function stampSessionNameRequested(coordRoot, instanceId, name) {
142
+ if (!name)
143
+ return null;
144
+ return mutate(coordRoot, instanceId, (hb) => hb.session_name_display_requested_for === name
145
+ ? hb
146
+ : { ...hb, session_name_display_requested_for: name });
147
+ }
131
148
  export function releaseClaim(coordRoot, instanceId, path) {
132
149
  const target = canonicalClaimPath(coordRoot, path);
133
150
  return mutate(coordRoot, instanceId, (hb) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"live-coordination-view.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/live-coordination-view.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,KAAK,4BAA4B,EAIlC,MAAM,sCAAsC,CAAC;AAM9C,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,uBAAuB,CAAC;AAGtE;;;GAGG;AACH,MAAM,WAAW,0BAA2B,SAAQ,SAAS;IAC3D,cAAc,EAAE,CAAC,CAAC;IAClB,cAAc,EAAE,QAAQ,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,OAAO,MAAM,EAAE,CAAC;IAClC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,KAAK,GAAG,SAAS,CAAC;CAClC;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,EAAE,CA2BxF;AAED,kEAAkE;AAClE,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB,0BAA0B,GAAG,IAAI,CAuBnC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,GAAG,IAAI,CAWhB;AA8BD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACnC,UAAU,EAAE,4BAA4B,GACvC,KAAK,IAAI,SAAS,CAMpB;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,4BAA4B,EACxC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,MAAM,CAAC,EAAE,4BAA4B,GACpC,0BAA0B,CAqD5B;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,4BAA4B,GAAG,MAAM,GAAG,SAAS,CAG9F;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,OAAO,CAEvE"}
1
+ {"version":3,"file":"live-coordination-view.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/live-coordination-view.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,KAAK,4BAA4B,EAIlC,MAAM,sCAAsC,CAAC;AAM9C,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,uBAAuB,CAAC;AAGtE;;;GAGG;AACH,MAAM,WAAW,0BAA2B,SAAQ,SAAS;IAC3D,cAAc,EAAE,CAAC,CAAC;IAClB,cAAc,EAAE,QAAQ,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,OAAO,MAAM,EAAE,CAAC;IAClC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,KAAK,GAAG,SAAS,CAAC;CAClC;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,EAAE,CA2BxF;AAED,kEAAkE;AAClE,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB,0BAA0B,GAAG,IAAI,CAuBnC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,GAAG,IAAI,CAWhB;AA8BD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACnC,UAAU,EAAE,4BAA4B,GACvC,KAAK,IAAI,SAAS,CAMpB;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,4BAA4B,EACxC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,MAAM,CAAC,EAAE,4BAA4B,GACpC,0BAA0B,CAsD5B;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,4BAA4B,GAAG,MAAM,GAAG,SAAS,CAG9F;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,OAAO,CAEvE"}
@@ -133,6 +133,7 @@ export function projectHeartbeatV3(coordRoot, generation, cache, nativeInstanceI
133
133
  suggested_session_name: cache?.suggested_session_name,
134
134
  session_name_seen_at: cache?.session_name_seen_at,
135
135
  session_name_seen_for: cache?.session_name_seen_for,
136
+ session_name_display_requested_for: cache?.session_name_display_requested_for,
136
137
  workflow_run_id: generation.run_id,
137
138
  workflow_agent_id: generation.workflow_agent_id,
138
139
  parent_instance_id: parent ? nativeInstanceIdV3(parent.instance_id) : undefined,
@@ -59,6 +59,11 @@ function materializeLiveCoordinationHeartbeat(coordRoot, nativeInstanceId, nativ
59
59
  ...(current.session_name_seen_at
60
60
  ? { session_name_seen_at: current.session_name_seen_at }
61
61
  : {}),
62
+ ...(current.session_name_display_requested_for
63
+ ? {
64
+ session_name_display_requested_for: current.session_name_display_requested_for,
65
+ }
66
+ : {}),
62
67
  }
63
68
  : {};
64
69
  const materialized = {
@@ -1,5 +1,5 @@
1
1
  /** Read-only artifact layout constants safe for browser-side server modules. */
2
2
  export declare const ARTIFACTS_DIR = ".harnery/artifacts";
3
3
  export declare const ARTIFACT_MANIFEST = ".harnery-artifact.json";
4
- export declare const ARTIFACT_SCHEMA_VERSION: 1;
4
+ export declare const ARTIFACT_SCHEMA_VERSION: 2;
5
5
  //# sourceMappingURL=constants.d.ts.map
@@ -1,4 +1,4 @@
1
1
  /** Read-only artifact layout constants safe for browser-side server modules. */
2
2
  export const ARTIFACTS_DIR = ".harnery/artifacts";
3
3
  export const ARTIFACT_MANIFEST = ".harnery-artifact.json";
4
- export const ARTIFACT_SCHEMA_VERSION = 1;
4
+ export const ARTIFACT_SCHEMA_VERSION = 2;