harnery 0.45.2 → 0.46.1

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 (227) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/core/agents/index.d.ts +1 -0
  21. package/dist/core/agents/index.d.ts.map +1 -1
  22. package/dist/core/agents/index.js +1 -0
  23. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  24. package/dist/core/agents/live-authority-v3.js +11 -4
  25. package/dist/core/agents/render/session-context.d.ts +0 -6
  26. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  27. package/dist/core/agents/render/session-context.js +1 -6
  28. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  29. package/dist/core/agents/rules/claim-conflict.js +36 -16
  30. package/dist/core/agents/rules/stop-hook.d.ts +6 -0
  31. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  32. package/dist/core/agents/rules/stop-hook.js +20 -1
  33. package/dist/core/agents/session-events.d.ts.map +1 -1
  34. package/dist/core/agents/session-events.js +12 -4
  35. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  36. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  38. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.js +6 -2
  40. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  41. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  42. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  43. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  44. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  45. package/dist/core/agents/state/parent-reference.js +119 -0
  46. package/dist/core/config.d.ts +39 -0
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +63 -0
  49. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  50. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  51. package/dist/core/diagnostics/path-identity.js +51 -6
  52. package/dist/core/events/v3/canonical.js +43 -14
  53. package/dist/core/events/v3/index.d.ts +1 -1
  54. package/dist/core/events/v3/index.d.ts.map +1 -1
  55. package/dist/core/events/v3/index.js +1 -1
  56. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  57. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  58. package/dist/core/events/v3/producers/recorder.js +117 -13
  59. package/dist/core/events/v3/reader.d.ts.map +1 -1
  60. package/dist/core/events/v3/reader.js +53 -13
  61. package/dist/core/events/v3/validate.d.ts.map +1 -1
  62. package/dist/core/events/v3/validate.js +36 -1
  63. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  64. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  65. package/dist/core/hooks/adapter/native-parent.js +91 -0
  66. package/dist/core/hooks/cli.js +103 -5
  67. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  68. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  69. package/dist/core/hooks/resolve/owner.js +0 -23
  70. package/dist/core/hooks/resolve/transcript.d.ts +12 -0
  71. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  72. package/dist/core/hooks/resolve/transcript.js +54 -0
  73. package/dist/core/hooks/shell-admission.d.ts +84 -0
  74. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  75. package/dist/core/hooks/shell-admission.js +622 -0
  76. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  77. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  78. package/dist/core/hooks/shell-commit-tree.js +104 -0
  79. package/dist/core/inbox/contract.d.ts +12 -0
  80. package/dist/core/inbox/contract.d.ts.map +1 -1
  81. package/dist/core/inbox/contract.js +16 -0
  82. package/dist/core/inbox/peer-messages.d.ts +41 -16
  83. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  84. package/dist/core/inbox/peer-messages.js +372 -15
  85. package/dist/core/inbox/service.d.ts +8 -1
  86. package/dist/core/inbox/service.d.ts.map +1 -1
  87. package/dist/core/inbox/service.js +83 -3
  88. package/dist/core/inbox/targets.d.ts +17 -3
  89. package/dist/core/inbox/targets.d.ts.map +1 -1
  90. package/dist/core/inbox/targets.js +77 -2
  91. package/dist/core/presence/git.d.ts +18 -12
  92. package/dist/core/presence/git.d.ts.map +1 -1
  93. package/dist/core/presence/git.js +32 -38
  94. package/dist/core/presence/index.d.ts +2 -2
  95. package/dist/core/presence/index.d.ts.map +1 -1
  96. package/dist/core/presence/index.js +8 -6
  97. package/dist/core/presence/mail.d.ts +133 -0
  98. package/dist/core/presence/mail.d.ts.map +1 -0
  99. package/dist/core/presence/mail.js +464 -0
  100. package/dist/core/semantic/contract.d.ts +5 -5
  101. package/dist/core/storage/modes.d.ts +4 -0
  102. package/dist/core/storage/modes.d.ts.map +1 -1
  103. package/dist/core/storage/modes.js +4 -0
  104. package/dist/core/storage/segments.d.ts.map +1 -1
  105. package/dist/core/storage/segments.js +5 -4
  106. package/dist/lib/admission-history.d.ts +96 -0
  107. package/dist/lib/admission-history.d.ts.map +1 -0
  108. package/dist/lib/admission-history.js +196 -0
  109. package/dist/lib/admission.d.ts +56 -7
  110. package/dist/lib/admission.d.ts.map +1 -1
  111. package/dist/lib/admission.js +161 -19
  112. package/dist/lib/browser/client.d.ts +18 -0
  113. package/dist/lib/browser/client.d.ts.map +1 -1
  114. package/dist/lib/browser/client.js +100 -12
  115. package/dist/lib/browser/content-checks.d.ts +3 -1
  116. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  117. package/dist/lib/browser/content-checks.js +10 -2
  118. package/dist/lib/browser/critique.d.ts +32 -0
  119. package/dist/lib/browser/critique.d.ts.map +1 -1
  120. package/dist/lib/browser/critique.js +72 -3
  121. package/dist/lib/browser/geometry.d.ts +2 -0
  122. package/dist/lib/browser/geometry.d.ts.map +1 -1
  123. package/dist/lib/browser/geometry.js +61 -15
  124. package/dist/lib/browser/index.d.ts +1 -1
  125. package/dist/lib/browser/index.d.ts.map +1 -1
  126. package/dist/lib/browser/index.js +1 -1
  127. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  128. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  129. package/dist/lib/browser/page-review-contracts.js +6 -1
  130. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  131. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  132. package/dist/lib/browser/page-review-judge.js +41 -11
  133. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  134. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  135. package/dist/lib/browser/page-review-pack.js +5 -2
  136. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  137. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  138. package/dist/lib/browser/qa-run-contracts.js +11 -4
  139. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  140. package/dist/lib/browser/qa-run.js +73 -16
  141. package/dist/lib/browser/runts.d.ts +4 -2
  142. package/dist/lib/browser/runts.d.ts.map +1 -1
  143. package/dist/lib/browser/runts.js +84 -33
  144. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  145. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  146. package/dist/lib/browser/windows-chrome.js +42 -0
  147. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  148. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  149. package/dist/lib/browser/windows-login-sync.js +171 -0
  150. package/dist/lib/cookies/client.d.ts +30 -5
  151. package/dist/lib/cookies/client.d.ts.map +1 -1
  152. package/dist/lib/cookies/client.js +55 -10
  153. package/dist/lib/cookies/index.d.ts +1 -1
  154. package/dist/lib/cookies/index.d.ts.map +1 -1
  155. package/dist/lib/cookies/index.js +1 -1
  156. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  157. package/dist/lib/docs-metadata-sync.js +41 -2
  158. package/dist/lib/durable-job.d.ts +8 -0
  159. package/dist/lib/durable-job.d.ts.map +1 -1
  160. package/dist/lib/durable-job.js +10 -0
  161. package/dist/lib/http/client.d.ts +9 -8
  162. package/dist/lib/http/client.d.ts.map +1 -1
  163. package/dist/lib/http/client.js +44 -2
  164. package/dist/lib/instructions/templates.js +4 -4
  165. package/package.json +2 -2
  166. package/src/commander.ts +13 -0
  167. package/src/commands/admission.ts +272 -29
  168. package/src/commands/agents.ts +132 -27
  169. package/src/commands/browse.ts +208 -6
  170. package/src/commands/fetch.ts +1 -0
  171. package/src/commands/ledger-v3.ts +8 -2
  172. package/src/commands/presence.ts +49 -0
  173. package/src/commands/qa-run.ts +4 -3
  174. package/src/commands/review-pack.ts +41 -5
  175. package/src/core/agents/index.ts +6 -0
  176. package/src/core/agents/live-authority-v3.ts +11 -4
  177. package/src/core/agents/render/session-context.ts +1 -7
  178. package/src/core/agents/rules/claim-conflict.ts +50 -18
  179. package/src/core/agents/rules/stop-hook.ts +28 -1
  180. package/src/core/agents/session-events.ts +18 -3
  181. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  182. package/src/core/agents/state/live-coordination-view.ts +13 -6
  183. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  184. package/src/core/agents/state/parent-reference.ts +179 -0
  185. package/src/core/config.ts +85 -0
  186. package/src/core/diagnostics/path-identity.ts +52 -6
  187. package/src/core/events/v3/canonical.ts +46 -13
  188. package/src/core/events/v3/index.ts +3 -0
  189. package/src/core/events/v3/producers/recorder.ts +151 -13
  190. package/src/core/events/v3/reader.ts +55 -11
  191. package/src/core/events/v3/validate.ts +42 -1
  192. package/src/core/hooks/adapter/native-parent.ts +109 -0
  193. package/src/core/hooks/cli.ts +110 -4
  194. package/src/core/hooks/resolve/owner.ts +0 -23
  195. package/src/core/hooks/resolve/transcript.ts +61 -0
  196. package/src/core/hooks/shell-admission.ts +634 -0
  197. package/src/core/hooks/shell-commit-tree.ts +102 -0
  198. package/src/core/inbox/contract.ts +18 -0
  199. package/src/core/inbox/peer-messages.ts +501 -15
  200. package/src/core/inbox/service.ts +108 -3
  201. package/src/core/inbox/targets.ts +94 -3
  202. package/src/core/presence/git.ts +45 -37
  203. package/src/core/presence/index.ts +16 -8
  204. package/src/core/presence/mail.ts +620 -0
  205. package/src/core/storage/modes.ts +6 -0
  206. package/src/core/storage/segments.ts +5 -4
  207. package/src/lib/admission-history.ts +267 -0
  208. package/src/lib/admission.ts +217 -19
  209. package/src/lib/browser/client.ts +147 -35
  210. package/src/lib/browser/content-checks.ts +14 -3
  211. package/src/lib/browser/critique.ts +113 -3
  212. package/src/lib/browser/geometry.ts +63 -14
  213. package/src/lib/browser/index.ts +5 -0
  214. package/src/lib/browser/page-review-contracts.ts +6 -1
  215. package/src/lib/browser/page-review-judge.ts +58 -13
  216. package/src/lib/browser/page-review-pack.ts +13 -2
  217. package/src/lib/browser/qa-run-contracts.ts +30 -4
  218. package/src/lib/browser/qa-run.ts +78 -17
  219. package/src/lib/browser/runts.ts +85 -37
  220. package/src/lib/browser/windows-chrome.ts +50 -0
  221. package/src/lib/browser/windows-login-sync.ts +275 -0
  222. package/src/lib/cookies/client.ts +62 -10
  223. package/src/lib/cookies/index.ts +3 -0
  224. package/src/lib/docs-metadata-sync.ts +44 -2
  225. package/src/lib/durable-job.ts +15 -0
  226. package/src/lib/http/client.ts +63 -1
  227. package/src/lib/instructions/templates.ts +4 -4
@@ -0,0 +1,179 @@
1
+ import type {
2
+ CoordinationGenerationViewV3,
3
+ CoordinationViewV3,
4
+ } from "../../events/v3/coordination-view.ts";
5
+ import { readCoordinationViewV3 } from "../../events/v3/coordination-view.ts";
6
+ import { liveInstanceIdV3, nativeInstanceIdV3 } from "../../events/v3/live-route-observer.ts";
7
+ import {
8
+ type NativeParentObservation,
9
+ type NativeParentRequest,
10
+ readNativeParentReference,
11
+ } from "../../hooks/adapter/native-parent.ts";
12
+ import { resolveName } from "./names.ts";
13
+ import type { AgentParentReference } from "./parent-reference-contract.ts";
14
+
15
+ export type { AgentParentReference } from "./parent-reference-contract.ts";
16
+
17
+ export interface AgentParentSubject {
18
+ instance_id: string;
19
+ generation_id?: string;
20
+ v3_generation_id?: string;
21
+ platform?: string;
22
+ native_session_id?: string;
23
+ }
24
+
25
+ export interface AgentParentReferenceOptions {
26
+ view?: CoordinationViewV3;
27
+ readNativeParent?: (request: NativeParentRequest) => NativeParentObservation;
28
+ }
29
+
30
+ /** Display/recipient evidence only. Never use native evidence as a claim or lifecycle waiver. */
31
+ export function resolveAgentParentReference(
32
+ coordRoot: string,
33
+ child: AgentParentSubject,
34
+ options: AgentParentReferenceOptions = {},
35
+ ): AgentParentReference {
36
+ try {
37
+ const view = options.view ?? readCoordinationViewV3(coordRoot);
38
+ if (!view.source_complete || !view.authority_safe) return unknown("parent_authority_unsafe");
39
+ if (
40
+ child.generation_id &&
41
+ child.v3_generation_id &&
42
+ child.generation_id !== child.v3_generation_id
43
+ ) {
44
+ return unknown("child_generation_mismatch");
45
+ }
46
+ const instance = liveInstanceIdV3(child.instance_id);
47
+ const generationId = child.generation_id ?? child.v3_generation_id;
48
+ const generations = [
49
+ ...Object.values(view.instances),
50
+ ...Object.values(view.terminal_generations),
51
+ ];
52
+ const generation = generationId
53
+ ? generations.find(
54
+ (row) => row.generation_id === generationId && row.instance_id === instance,
55
+ )
56
+ : view.instances[instance];
57
+ if (!generation) return unknown("child_generation_unavailable");
58
+ if (!safe(view, generation)) return unknown("child_authority_unsafe");
59
+ const canonical = generation.parent_generation_id
60
+ ? generations.find((row) => row.generation_id === generation.parent_generation_id)
61
+ : undefined;
62
+ if (generation.parent_generation_id && !canonical)
63
+ return unknown("parent_generation_unavailable");
64
+ if (canonical && cyclic(generations, generation)) {
65
+ return { state: "conflict", reason: "parent_lineage_cycle" };
66
+ }
67
+ const adapter = generation.runtime_attestation.adapter;
68
+ const platform = adapter.state === "observed" ? adapter.value.id : child.platform;
69
+ const nativeId = child.native_session_id ?? nativeInstanceIdV3(instance);
70
+ const native =
71
+ platform === "codex" && liveInstanceIdV3(nativeId) === instance
72
+ ? (options.readNativeParent ?? readNativeParentReference)({
73
+ adapter: platform,
74
+ native_session_id: nativeId,
75
+ })
76
+ : ({ state: "unknown", reason: "native_child_identity_unavailable" } as const);
77
+ if (canonical) {
78
+ if (
79
+ native.state === "none" ||
80
+ (native.state === "observed" &&
81
+ liveInstanceIdV3(native.parent_session_id) !== canonical.instance_id)
82
+ ) {
83
+ return { state: "conflict", reason: "parent_sources_disagree" };
84
+ }
85
+ return reference(coordRoot, view, canonical.instance_id, canonical, "v3-session-link");
86
+ }
87
+ if (native.state === "observed") {
88
+ const parentInstance = liveInstanceIdV3(native.parent_session_id);
89
+ if (parentInstance === instance)
90
+ return { state: "conflict", reason: "parent_self_reference" };
91
+ const parent = recipientGeneration(view, parentInstance);
92
+ if (parent && cyclic(generations, generation, parent)) {
93
+ return { state: "conflict", reason: "parent_lineage_cycle" };
94
+ }
95
+ return reference(
96
+ coordRoot,
97
+ view,
98
+ parentInstance,
99
+ parent,
100
+ "native-session-metadata",
101
+ native.source_witness,
102
+ );
103
+ }
104
+ return { state: native.state, reason: native.reason };
105
+ } catch {
106
+ return unknown("parent_reference_unavailable");
107
+ }
108
+ }
109
+
110
+ function reference(
111
+ root: string,
112
+ view: CoordinationViewV3,
113
+ instance: string,
114
+ parent: CoordinationGenerationViewV3 | undefined,
115
+ source: "v3-session-link" | "native-session-metadata",
116
+ witness?: string,
117
+ ): AgentParentReference {
118
+ const native = nativeInstanceIdV3(instance);
119
+ const name = resolveName(root, native)?.name ?? null;
120
+ const recipient = recipientGeneration(view, instance);
121
+ return {
122
+ state: "verified",
123
+ relationship: source === "v3-session-link" ? "delegated-child" : "native-child",
124
+ parent_instance_id: native,
125
+ parent_v3_instance_id: instance,
126
+ parent_generation_id: parent?.generation_id ?? null,
127
+ parent_session_id: parent?.session_id ?? null,
128
+ parent_name: name,
129
+ source,
130
+ ...(witness ? { source_witness: witness } : {}),
131
+ addressable: Boolean(
132
+ parent && recipient?.generation_id === parent.generation_id && safe(view, parent) && name,
133
+ ),
134
+ };
135
+ }
136
+
137
+ function recipientGeneration(view: CoordinationViewV3, instance: string) {
138
+ return (
139
+ view.instances[instance] ??
140
+ Object.values(view.terminal_generations)
141
+ .filter((row) => row.instance_id === instance)
142
+ .sort(
143
+ (a, b) =>
144
+ b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
145
+ b.source_position.byte_offset - a.source_position.byte_offset,
146
+ )[0]
147
+ );
148
+ }
149
+
150
+ function safe(view: CoordinationViewV3, generation: CoordinationGenerationViewV3): boolean {
151
+ return !(view.diagnostics_by_generation[generation.generation_id] ?? []).some(
152
+ (d) => d.authority_blocking,
153
+ );
154
+ }
155
+
156
+ function cyclic(
157
+ generations: CoordinationGenerationViewV3[],
158
+ child: CoordinationGenerationViewV3,
159
+ first?: CoordinationGenerationViewV3,
160
+ ): boolean {
161
+ const seen = new Set([child.generation_id]);
162
+ let cursor = first ?? child;
163
+ for (let depth = 0; depth <= generations.length; depth++) {
164
+ if (cursor !== child) {
165
+ if (seen.has(cursor.generation_id) || cursor.instance_id === child.instance_id) return true;
166
+ seen.add(cursor.generation_id);
167
+ }
168
+ if (!cursor.parent_generation_id) return false;
169
+ const next = generations.find((g) => g.generation_id === cursor.parent_generation_id);
170
+ if (!next) return false;
171
+ if (seen.has(next.generation_id)) return true;
172
+ cursor = next;
173
+ }
174
+ return true;
175
+ }
176
+
177
+ function unknown(reason: string): AgentParentReference {
178
+ return { state: "unknown", reason };
179
+ }
@@ -92,6 +92,8 @@ interface HarneryConfig {
92
92
  requireGitFinalization?: boolean;
93
93
  finalizationRoots?: AgentFinalizationRoot[];
94
94
  };
95
+ /** Admission resource defaults and the heavy-command guard. Project config only. */
96
+ admission?: unknown;
95
97
  /** Host-owned agent instruction settings. */
96
98
  instructions?: {
97
99
  hostAddendumFile?: unknown;
@@ -633,6 +635,89 @@ export function ripgrepAutoInstall(coordRoot?: string | null): boolean {
633
635
  return readConfig(root).tools?.ripgrep?.autoInstall === true;
634
636
  }
635
637
 
638
+ /** Wait before queueing while Linux memory pressure is high (PSI `full avg10`). */
639
+ export interface AdmissionPressureConfig {
640
+ psi_full_avg10: number;
641
+ max_wait_seconds: number;
642
+ }
643
+
644
+ /** Per-resource defaults for `admission run`; command-line flags win. */
645
+ export interface AdmissionResourceConfig {
646
+ capacity?: number;
647
+ timeout_minutes?: number;
648
+ defer_on_pressure?: AdmissionPressureConfig;
649
+ }
650
+
651
+ export interface AdmissionProjectConfig {
652
+ resources: Record<string, AdmissionResourceConfig>;
653
+ /** Present when the agent hook should wrap heavy shell commands in a slot. */
654
+ guard?: { resource: string };
655
+ }
656
+
657
+ function positiveNumber(value: unknown, max: number): number | undefined {
658
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max
659
+ ? value
660
+ : undefined;
661
+ }
662
+
663
+ /**
664
+ * Admission settings from `.harnery/config.jsonc`:
665
+ *
666
+ * ```jsonc
667
+ * "admission": {
668
+ * "resources": {
669
+ * "build": {
670
+ * "capacity": 2,
671
+ * "timeout_minutes": 30,
672
+ * "defer_on_pressure": { "psi_full_avg10": 40, "max_wait_seconds": 300 }
673
+ * }
674
+ * },
675
+ * "guard": { "resource": "build" }
676
+ * }
677
+ * ```
678
+ *
679
+ * Project config only, like other machine-shaping policy. Malformed values are
680
+ * dropped one by one rather than rejecting the whole block. Undefined when the
681
+ * project declares no admission settings.
682
+ */
683
+ export function readAdmissionConfig(coordRoot?: string | null): AdmissionProjectConfig | undefined {
684
+ const root = coordRoot ?? findCoordRoot();
685
+ if (!root) return undefined;
686
+ const raw = readProjectConfig(root).admission;
687
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
688
+ const record = raw as { resources?: unknown; guard?: unknown };
689
+ const resources: Record<string, AdmissionResourceConfig> = {};
690
+ if (
691
+ record.resources &&
692
+ typeof record.resources === "object" &&
693
+ !Array.isArray(record.resources)
694
+ ) {
695
+ for (const [name, value] of Object.entries(record.resources as Record<string, unknown>)) {
696
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
697
+ const entry = value as Record<string, unknown>;
698
+ const capacity = positiveNumber(entry.capacity, 32);
699
+ const timeout = positiveNumber(entry.timeout_minutes, 1440);
700
+ const pressure = entry.defer_on_pressure as Record<string, unknown> | undefined;
701
+ const threshold = positiveNumber(pressure?.psi_full_avg10, 100);
702
+ const maxWait = positiveNumber(pressure?.max_wait_seconds, 86_400);
703
+ resources[name] = {
704
+ ...(capacity !== undefined && Number.isInteger(capacity) ? { capacity } : {}),
705
+ ...(timeout !== undefined && Number.isInteger(timeout) ? { timeout_minutes: timeout } : {}),
706
+ ...(threshold !== undefined && maxWait !== undefined
707
+ ? { defer_on_pressure: { psi_full_avg10: threshold, max_wait_seconds: maxWait } }
708
+ : {}),
709
+ };
710
+ }
711
+ }
712
+ const guard = record.guard as { resource?: unknown } | undefined;
713
+ return {
714
+ resources,
715
+ ...(guard && typeof guard.resource === "string" && guard.resource
716
+ ? { guard: { resource: guard.resource } }
717
+ : {}),
718
+ };
719
+ }
720
+
636
721
  /**
637
722
  * Whether this repo pins workflow runs to subscription billing:
638
723
  * `.harnery/config.jsonc` `{ "workflow": { "subscriptionOnly": true } }`.
@@ -1,15 +1,61 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { PERSONAL_DIR_MODE, PERSONAL_FILE_MODE } from "../storage/modes.ts";
2
6
 
3
7
  const HOME_PATH =
4
8
  /(?:[A-Za-z]:[\\/]Users[\\/](?:[^\\/\r\n\0:"<>|]+(?=[\\/])|[^\\/\s\0:]+)|\/home\/[^/\s\0:]+|\/Users\/[^/\s\0:]+)/g;
5
9
 
6
- /** One stable representation for private home paths in values and identity inputs. */
10
+ const KEY_PATTERN = /^[0-9a-f]{64}$/;
11
+ let cachedKey: Buffer | undefined;
12
+
13
+ /**
14
+ * Per-install secret for home-path pseudonyms. An unkeyed digest would let
15
+ * anyone holding a bundle recover the user name by hashing guesses, so the
16
+ * key stays in local state and never enters a bundle. Sanitized values carry
17
+ * the pseudonym itself, so replay elsewhere needs no key.
18
+ */
19
+ function identityKeyPath(): string {
20
+ const xdg = process.env.XDG_STATE_HOME;
21
+ const base = xdg?.trim() ? xdg : join(homedir(), ".local", "state");
22
+ return join(base, "harnery", "diagnostic-identity.key");
23
+ }
24
+
25
+ function readKey(path: string): Buffer | undefined {
26
+ try {
27
+ const text = readFileSync(path, "utf8").trim();
28
+ return KEY_PATTERN.test(text) ? Buffer.from(text, "hex") : undefined;
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function identityKey(): Buffer {
35
+ if (cachedKey) return cachedKey;
36
+ const path = identityKeyPath();
37
+ let key = readKey(path);
38
+ if (!key) {
39
+ const fresh = randomBytes(32);
40
+ try {
41
+ mkdirSync(dirname(path), { recursive: true, mode: PERSONAL_DIR_MODE });
42
+ writeFileSync(path, `${fresh.toString("hex")}\n`, { flag: "wx", mode: PERSONAL_FILE_MODE });
43
+ chmodSync(path, PERSONAL_FILE_MODE);
44
+ key = fresh;
45
+ } catch {
46
+ // A concurrent writer won the race, or state is unwritable. Prefer the
47
+ // stored key; otherwise stay private with a process-local key.
48
+ key = readKey(path) ?? fresh;
49
+ }
50
+ }
51
+ cachedKey = key;
52
+ return key;
53
+ }
54
+
55
+ /** One stable, non-reversible representation for private home paths in values and identity inputs. */
7
56
  export function canonicalizeDiagnosticIdentity(value: string): string {
8
57
  return value.replace(HOME_PATH, (home) => {
9
- const digest = createHash("sha256")
10
- .update("harnery-diagnostic-home-v1\0")
11
- .update(home)
12
- .digest("hex");
58
+ const digest = createHmac("sha256", identityKey()).update(home).digest("hex");
13
59
  return `[REDACTED:home:${digest}]`;
14
60
  });
15
61
  }
@@ -104,10 +104,19 @@ export function normalizeNativeIdV3(
104
104
  return `hid_${digest}`;
105
105
  }
106
106
 
107
+ /**
108
+ * NFC leaves ASCII unchanged, so ASCII strings and key sets skip normalization
109
+ * and the collision map. Every reader re-serializes each ledger frame to prove
110
+ * it is canonical, which makes this the hottest function on a cold read.
111
+ */
112
+ const NON_ASCII = /[\u0080-\uffff]/;
113
+
107
114
  function serialize(value: unknown, ancestors: Set<object>): string {
108
115
  if (value === null) return "null";
109
116
  if (typeof value === "boolean") return value ? "true" : "false";
110
- if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
117
+ if (typeof value === "string") {
118
+ return JSON.stringify(NON_ASCII.test(value) ? value.normalize("NFC") : value);
119
+ }
111
120
  if (typeof value === "number") {
112
121
  if (!Number.isFinite(value)) throw new Error("canonical JSON rejects non-finite numbers");
113
122
  return JSON.stringify(Object.is(value, -0) ? 0 : value);
@@ -119,29 +128,53 @@ function serialize(value: unknown, ancestors: Set<object>): string {
119
128
  ancestors.add(value);
120
129
  try {
121
130
  if (Array.isArray(value)) {
122
- return `[${value.map((item) => serialize(item, ancestors)).join(",")}]`;
131
+ let out = "[";
132
+ for (let index = 0; index < value.length; index++) {
133
+ if (index > 0) out += ",";
134
+ out += serialize(value[index], ancestors);
135
+ }
136
+ return `${out}]`;
123
137
  }
124
138
  const prototype = Object.getPrototypeOf(value);
125
139
  if (prototype !== Object.prototype && prototype !== null) {
126
140
  throw new Error("canonical JSON accepts only plain objects");
127
141
  }
128
- const normalized = new Map<string, unknown>();
129
- for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
130
- const normalizedKey = key.normalize("NFC");
131
- if (normalized.has(normalizedKey)) {
132
- throw new Error("canonical JSON key collision after Unicode normalization");
133
- }
134
- normalized.set(normalizedKey, item);
142
+ const object = value as Record<string, unknown>;
143
+ const keys = Object.keys(object);
144
+ if (keys.some((key) => NON_ASCII.test(key))) {
145
+ return serializeNormalizedObject(object, ancestors);
135
146
  }
136
- const keys = [...normalized.keys()].sort(compareUtf16);
137
- return `{${keys
138
- .map((key) => `${JSON.stringify(key)}:${serialize(normalized.get(key), ancestors)}`)
139
- .join(",")}}`;
147
+ keys.sort(compareUtf16);
148
+ let out = "{";
149
+ for (let index = 0; index < keys.length; index++) {
150
+ const key = keys[index] as string;
151
+ if (index > 0) out += ",";
152
+ out += `${JSON.stringify(key)}:${serialize(object[key], ancestors)}`;
153
+ }
154
+ return `${out}}`;
140
155
  } finally {
141
156
  ancestors.delete(value);
142
157
  }
143
158
  }
144
159
 
160
+ function serializeNormalizedObject(
161
+ object: Record<string, unknown>,
162
+ ancestors: Set<object>,
163
+ ): string {
164
+ const normalized = new Map<string, unknown>();
165
+ for (const [key, item] of Object.entries(object)) {
166
+ const normalizedKey = key.normalize("NFC");
167
+ if (normalized.has(normalizedKey)) {
168
+ throw new Error("canonical JSON key collision after Unicode normalization");
169
+ }
170
+ normalized.set(normalizedKey, item);
171
+ }
172
+ const keys = [...normalized.keys()].sort(compareUtf16);
173
+ return `{${keys
174
+ .map((key) => `${JSON.stringify(key)}:${serialize(normalized.get(key), ancestors)}`)
175
+ .join(",")}}`;
176
+ }
177
+
145
178
  function compareUtf16(left: string, right: string): number {
146
179
  return left < right ? -1 : left > right ? 1 : 0;
147
180
  }
@@ -247,7 +247,9 @@ export {
247
247
  type DrainHookIntakeSpoolV3Result,
248
248
  drainHookIntakeSpoolV3,
249
249
  type HookProducerStateRecordV3,
250
+ type HookProducerStateScanV3,
250
251
  type HookProducerStateV3,
252
+ type InvalidHookProducerStateFileV3,
251
253
  listHookProducerStateRecordsV3,
252
254
  type RecordApprovedSessionEndV3Input,
253
255
  type RecordApprovedSessionEndV3Result,
@@ -256,6 +258,7 @@ export {
256
258
  readHookProducerStateV3,
257
259
  recordApprovedSessionEndV3,
258
260
  recordHookSignalV3,
261
+ scanHookProducerStateRecordsV3,
259
262
  } from "./producers/recorder.ts";
260
263
  export {
261
264
  type ClaimProjectionV3,