harnery 0.46.1 → 0.47.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 (176) hide show
  1. package/README.md +24 -0
  2. package/bin/README.md +10 -0
  3. package/bin/agent-coord.mjs +3 -0
  4. package/bin/agent-hook.mjs +3 -0
  5. package/bin/harn.mjs +3 -0
  6. package/bin/run.mjs +43 -0
  7. package/dist/commander.d.ts +18 -6
  8. package/dist/commander.d.ts.map +1 -1
  9. package/dist/commander.js +74 -2
  10. package/dist/commands/agents.d.ts +25 -0
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/agents.js +70 -27
  13. package/dist/commands/autoupdate.d.ts +12 -0
  14. package/dist/commands/autoupdate.d.ts.map +1 -0
  15. package/dist/commands/autoupdate.js +190 -0
  16. package/dist/commands/computer.d.ts +45 -0
  17. package/dist/commands/computer.d.ts.map +1 -0
  18. package/dist/commands/computer.js +509 -0
  19. package/dist/commands/docs.js +3 -1
  20. package/dist/commands/fetch.js +11 -1
  21. package/dist/commands/grep.d.ts +72 -0
  22. package/dist/commands/grep.d.ts.map +1 -1
  23. package/dist/commands/grep.js +200 -47
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +15 -9
  26. package/dist/commands/rm.js +2 -2
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +2 -0
  29. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  30. package/dist/core/agents/canonical-emit.js +2 -3
  31. package/dist/core/agents/coord-bin.d.ts +3 -0
  32. package/dist/core/agents/coord-bin.d.ts.map +1 -1
  33. package/dist/core/agents/coord-bin.js +6 -0
  34. package/dist/core/agents/coord-client.d.ts.map +1 -1
  35. package/dist/core/agents/coord-client.js +6 -2
  36. package/dist/core/agents/render/prompt-context.d.ts +55 -7
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +122 -48
  39. package/dist/core/agents/state/proc-start.d.ts +3 -1
  40. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  41. package/dist/core/agents/state/proc-start.js +26 -4
  42. package/dist/core/autoupdate/engine.d.ts +97 -0
  43. package/dist/core/autoupdate/engine.d.ts.map +1 -0
  44. package/dist/core/autoupdate/engine.js +548 -0
  45. package/dist/core/autoupdate/fingerprint.d.ts +16 -0
  46. package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
  47. package/dist/core/autoupdate/fingerprint.js +73 -0
  48. package/dist/core/autoupdate/git.d.ts +30 -0
  49. package/dist/core/autoupdate/git.d.ts.map +1 -0
  50. package/dist/core/autoupdate/git.js +82 -0
  51. package/dist/core/autoupdate/index.d.ts +6 -0
  52. package/dist/core/autoupdate/index.d.ts.map +1 -0
  53. package/dist/core/autoupdate/index.js +4 -0
  54. package/dist/core/autoupdate/state.d.ts +59 -0
  55. package/dist/core/autoupdate/state.d.ts.map +1 -0
  56. package/dist/core/autoupdate/state.js +214 -0
  57. package/dist/core/autoupdate/types.d.ts +113 -0
  58. package/dist/core/autoupdate/types.d.ts.map +1 -0
  59. package/dist/core/autoupdate/types.js +35 -0
  60. package/dist/core/config.d.ts +53 -1
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +114 -1
  63. package/dist/core/events/v3/bootstrap.js +1 -0
  64. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  65. package/dist/core/events/v3/runtime-build.js +1 -0
  66. package/dist/core/hooks/adapter/wiring.d.ts +12 -2
  67. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  68. package/dist/core/hooks/adapter/wiring.js +38 -8
  69. package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
  70. package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
  71. package/dist/core/hooks/autoupdate-cue.js +53 -0
  72. package/dist/core/hooks/cli.js +28 -3
  73. package/dist/core/hooks/effects/index.js +4 -4
  74. package/dist/core/hooks/resolve/coord-root.js +1 -0
  75. package/dist/core/semantic/contract.d.ts +3 -3
  76. package/dist/core/semantic/service.d.ts.map +1 -1
  77. package/dist/core/semantic/service.js +4 -2
  78. package/dist/core/storage/modes.d.ts +1 -1
  79. package/dist/core/storage/modes.d.ts.map +1 -1
  80. package/dist/core/storage/modes.js +5 -1
  81. package/dist/core/workflow/durable-record.d.ts +2 -0
  82. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  83. package/dist/core/workflow/durable-record.js +13 -1
  84. package/dist/core/workflow/engine.js +4 -0
  85. package/dist/core/workflow/spawn-claude.d.ts +7 -0
  86. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  87. package/dist/core/workflow/spawn-claude.js +16 -4
  88. package/dist/core/workflow/specialists.d.ts.map +1 -1
  89. package/dist/core/workflow/specialists.js +1 -0
  90. package/dist/core/workflow/types.d.ts +10 -0
  91. package/dist/core/workflow/types.d.ts.map +1 -1
  92. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  93. package/dist/core/workflow/workspaces/leases.js +1 -9
  94. package/dist/lib/computer/cua.d.ts +50 -0
  95. package/dist/lib/computer/cua.d.ts.map +1 -0
  96. package/dist/lib/computer/cua.js +474 -0
  97. package/dist/lib/computer/host.d.ts +19 -0
  98. package/dist/lib/computer/host.d.ts.map +1 -0
  99. package/dist/lib/computer/host.js +72 -0
  100. package/dist/lib/computer/lease.d.ts +13 -0
  101. package/dist/lib/computer/lease.d.ts.map +1 -0
  102. package/dist/lib/computer/lease.js +269 -0
  103. package/dist/lib/computer/types.d.ts +163 -0
  104. package/dist/lib/computer/types.d.ts.map +1 -0
  105. package/dist/lib/computer/types.js +10 -0
  106. package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
  107. package/dist/lib/docs-metadata-v2.js +6 -5
  108. package/dist/lib/exec.d.ts.map +1 -1
  109. package/dist/lib/exec.js +1 -0
  110. package/dist/lib/guarded-remove.d.ts.map +1 -1
  111. package/dist/lib/guarded-remove.js +5 -4
  112. package/dist/lib/headless/index.d.ts +33 -0
  113. package/dist/lib/headless/index.d.ts.map +1 -1
  114. package/dist/lib/headless/index.js +38 -2
  115. package/dist/lib/http/client.d.ts +2 -9
  116. package/dist/lib/http/client.d.ts.map +1 -1
  117. package/dist/lib/http/client.js +30 -9
  118. package/dist/lib/instructions/apply.d.ts +9 -0
  119. package/dist/lib/instructions/apply.d.ts.map +1 -1
  120. package/dist/lib/instructions/apply.js +92 -14
  121. package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
  122. package/dist/lib/instructions/git-hooks.js +8 -4
  123. package/dist/lib/instructions/templates.d.ts +11 -0
  124. package/dist/lib/instructions/templates.d.ts.map +1 -1
  125. package/dist/lib/instructions/templates.js +81 -1
  126. package/package.json +5 -5
  127. package/schemas/config.schema.json +89 -1
  128. package/src/commander.ts +98 -8
  129. package/src/commands/agents.ts +100 -32
  130. package/src/commands/autoupdate.ts +250 -0
  131. package/src/commands/computer.ts +682 -0
  132. package/src/commands/docs.ts +3 -1
  133. package/src/commands/fetch.ts +11 -1
  134. package/src/commands/grep.ts +274 -59
  135. package/src/commands/init.ts +17 -7
  136. package/src/commands/rm.ts +2 -2
  137. package/src/commands/web.ts +2 -0
  138. package/src/core/agents/canonical-emit.ts +2 -3
  139. package/src/core/agents/coord-bin.ts +30 -0
  140. package/src/core/agents/coord-client.ts +6 -2
  141. package/src/core/agents/render/prompt-context.ts +152 -53
  142. package/src/core/agents/state/proc-start.ts +29 -5
  143. package/src/core/autoupdate/engine.ts +691 -0
  144. package/src/core/autoupdate/fingerprint.ts +89 -0
  145. package/src/core/autoupdate/git.ts +120 -0
  146. package/src/core/autoupdate/index.ts +29 -0
  147. package/src/core/autoupdate/state.ts +269 -0
  148. package/src/core/autoupdate/types.ts +135 -0
  149. package/src/core/config.ts +142 -1
  150. package/src/core/events/v3/bootstrap.ts +1 -0
  151. package/src/core/events/v3/runtime-build.ts +1 -0
  152. package/src/core/hooks/adapter/wiring.ts +51 -6
  153. package/src/core/hooks/autoupdate-cue.ts +72 -0
  154. package/src/core/hooks/cli.ts +27 -1
  155. package/src/core/hooks/effects/index.ts +4 -4
  156. package/src/core/hooks/resolve/coord-root.ts +1 -0
  157. package/src/core/semantic/service.ts +6 -2
  158. package/src/core/storage/modes.ts +7 -1
  159. package/src/core/workflow/durable-record.ts +15 -1
  160. package/src/core/workflow/engine.ts +3 -0
  161. package/src/core/workflow/spawn-claude.ts +16 -4
  162. package/src/core/workflow/specialists.ts +1 -0
  163. package/src/core/workflow/types.ts +10 -0
  164. package/src/core/workflow/workspaces/leases.ts +1 -9
  165. package/src/lib/computer/cua.ts +572 -0
  166. package/src/lib/computer/host.ts +87 -0
  167. package/src/lib/computer/lease.ts +299 -0
  168. package/src/lib/computer/types.ts +177 -0
  169. package/src/lib/docs-metadata-v2.ts +24 -5
  170. package/src/lib/exec.ts +1 -0
  171. package/src/lib/guarded-remove.ts +5 -3
  172. package/src/lib/headless/index.ts +66 -2
  173. package/src/lib/http/client.ts +33 -19
  174. package/src/lib/instructions/apply.ts +100 -12
  175. package/src/lib/instructions/git-hooks.ts +8 -4
  176. package/src/lib/instructions/templates.ts +95 -1
@@ -7,9 +7,10 @@
7
7
  * and forwards the result as the adapter-shaped additionalContext payload.
8
8
  *
9
9
  * Four subsections combined into one additionalContext payload:
10
- * 1. Peer table: semantically-relevant peer fields hashed; only re-emits
11
- * when peers change (name + instance_id + session_id + kind + started_at
12
- * + sorted(files_touched) + platform, sorted by instance_id).
10
+ * 1. Peer table: re-emits only when a live peer arrives or leaves, changes
11
+ * lifecycle, or changes its claimed files. Activity and task text never
12
+ * trigger a reprint. Peers past the heartbeat-freshness window are left
13
+ * out and counted in "+N more".
13
14
  * 2. Council pending: pending open-council IDs hashed; re-emits when the
14
15
  * ID set changes.
15
16
  * 3. Focus nudge. Until the session has displayed its current suggested
@@ -24,14 +25,37 @@
24
25
  * 5. Codex status footer: a non-deduplicated reminder to put the live status
25
26
  * box after the substantive answer. Stop stays observe-only, so missing the
26
27
  * footer can never trigger a replacement response.
28
+ * 6. Turn ritual: for adapters whose Stop hook enforces the ritual, shown on
29
+ * the first prompt, after a compaction, and after a bounced turn.
27
30
  *
28
31
  * Hash files live at:
32
+ * .harnery/.last-turn-ritual.<instance_id>
29
33
  * .harnery/.last-peer-hash.<instance_id>
30
34
  * .harnery/.last-council-hash.<instance_id>
31
35
  * .harnery/.last-task-nudge-hash.<instance_id>
32
36
  *
33
37
  * First call always emits (hash files don't exist).
34
38
  */
39
+ import { type RemoteMachine } from "../../presence/index.js";
40
+ import type { AgentActivity, TaskState } from "../state/session-state.js";
41
+ interface HeartbeatRow {
42
+ instance_id?: string;
43
+ name?: string;
44
+ kind?: string;
45
+ session_id?: string;
46
+ started_at?: string;
47
+ last_heartbeat?: string;
48
+ files_touched?: string[];
49
+ platform?: string;
50
+ task?: string;
51
+ activity?: AgentActivity;
52
+ task_state?: TaskState;
53
+ task_state_reason?: string;
54
+ task_updated_at?: string | null;
55
+ suggested_session_name?: string;
56
+ session_name_seen_for?: string;
57
+ workflow_run_id?: string;
58
+ }
35
59
  export interface PromptContextOpts {
36
60
  coordRoot: string;
37
61
  instanceId: string;
@@ -50,10 +74,9 @@ export interface PromptContextOpts {
50
74
  * The caller enables this only for human-facing Codex sessions. */
51
75
  statusFooterNudge?: boolean;
52
76
  /** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
53
- * status check). When set, append a fresh per-prompt reminder of that
54
- * contract so the model satisfies it before ending the turn instead of
55
- * being bounced into a retry. Not hash-deduped, same rationale as the
56
- * status footer. */
77
+ * status check). When set, append a reminder of that contract on the
78
+ * session's first prompt, after a context compaction, and after a turn the
79
+ * Stop hook bounced (see `markTurnRitualBounced`). */
57
80
  turnRitualNudge?: string;
58
81
  /** The emitter acknowledges exactly these IDs only after output succeeds. */
59
82
  onInboxPrepared?: (messageIds: readonly string[]) => void;
@@ -74,4 +97,29 @@ export declare function renderPromptContext(opts: PromptContextOpts): string;
74
97
  * re-emit the same advice.
75
98
  */
76
99
  export declare function resourceWarningIfChanged(coordRoot: string, instanceId: string): string;
100
+ /**
101
+ * Called by the Stop hook when it blocks a turn for a missing ritual step, so
102
+ * the next prompt carries the reminder again.
103
+ */
104
+ export declare function markTurnRitualBounced(coordRoot: string, instanceId: string): void;
105
+ export interface PeerTableInput {
106
+ peers: HeartbeatRow[];
107
+ remote: RemoteMachine[];
108
+ mySessionId: string;
109
+ nowMs: number;
110
+ /** Sessions whose last heartbeat is older than this are folded into "+N more". */
111
+ freshnessSeconds: number;
112
+ relations?: Map<string, string>;
113
+ }
114
+ /**
115
+ * Pure peer-table render plus its change key. The key covers only what makes
116
+ * the table news: which live sessions exist, their lifecycle, and the files
117
+ * they claim. Activity and task text change on nearly every peer tool call, so
118
+ * they render in the table but never trigger a reprint by themselves.
119
+ */
120
+ export declare function renderPeerTable(input: PeerTableInput): {
121
+ hash: string;
122
+ table: string;
123
+ };
124
+ export {};
77
125
  //# sourceMappingURL=prompt-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAiDH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA8EnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA6CtF"}
1
+ {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAwBH,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AASjF,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG1E,UAAU,YAAY;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;0DAGsD;IACtD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA6EnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA6CtF;AAsBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEjF;AA0LD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA2CtF"}
@@ -7,9 +7,10 @@
7
7
  * and forwards the result as the adapter-shaped additionalContext payload.
8
8
  *
9
9
  * Four subsections combined into one additionalContext payload:
10
- * 1. Peer table: semantically-relevant peer fields hashed; only re-emits
11
- * when peers change (name + instance_id + session_id + kind + started_at
12
- * + sorted(files_touched) + platform, sorted by instance_id).
10
+ * 1. Peer table: re-emits only when a live peer arrives or leaves, changes
11
+ * lifecycle, or changes its claimed files. Activity and task text never
12
+ * trigger a reprint. Peers past the heartbeat-freshness window are left
13
+ * out and counted in "+N more".
13
14
  * 2. Council pending: pending open-council IDs hashed; re-emits when the
14
15
  * ID set changes.
15
16
  * 3. Focus nudge. Until the session has displayed its current suggested
@@ -24,8 +25,11 @@
24
25
  * 5. Codex status footer: a non-deduplicated reminder to put the live status
25
26
  * box after the substantive answer. Stop stays observe-only, so missing the
26
27
  * footer can never trigger a replacement response.
28
+ * 6. Turn ritual: for adapters whose Stop hook enforces the ritual, shown on
29
+ * the first prompt, after a compaction, and after a bounced turn.
27
30
  *
28
31
  * Hash files live at:
32
+ * .harnery/.last-turn-ritual.<instance_id>
29
33
  * .harnery/.last-peer-hash.<instance_id>
30
34
  * .harnery/.last-council-hash.<instance_id>
31
35
  * .harnery/.last-task-nudge-hash.<instance_id>
@@ -36,7 +40,8 @@ import { createHash } from "node:crypto";
36
40
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
37
41
  import { join } from "node:path";
38
42
  import { coordEnv } from "../../../lib/env.js";
39
- import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.js";
43
+ import { coordFreshnessSeconds, endOfTurnStatusCommand, hostPromptReminder, resolveBinName, } from "../../config.js";
44
+ import { readContextState } from "../../context/index.js";
40
45
  import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.js";
41
46
  import { readRemoteMachines } from "../../presence/index.js";
42
47
  import { readResourceStatus } from "../../resources/status.js";
@@ -107,13 +112,12 @@ export function renderPromptContext(opts) {
107
112
  if (statusMsg)
108
113
  sections.push(statusMsg);
109
114
  }
110
- // 7. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
111
- // on every prompt. Reminding before the reply is drafted is far cheaper than
112
- // the bounce-and-retry the Stop hook otherwise forces: enforcement-only
113
- // compliance was measured missing on a double-digit share of turns. Not
114
- // hash-deduped for the same reason as the Codex footer.
115
+ // 7. Adapters whose Stop hook ENFORCES the turn ritual get the reminder when
116
+ // it is news: on the session's first prompt, after a context compaction,
117
+ // and after a turn the Stop hook bounced. The Stop hook keeps enforcing on
118
+ // every turn, so a missed ritual still costs one retry and re-arms this.
115
119
  if (turnRitualNudge) {
116
- const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, turnRitualNudge);
120
+ const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, sessionId, turnRitualNudge);
117
121
  if (ritualMsg)
118
122
  sections.push(ritualMsg);
119
123
  }
@@ -181,17 +185,46 @@ function renderHostPromptReminder(coordRoot, selfInstanceId) {
181
185
  return "";
182
186
  return hostPromptReminder(coordRoot) ?? "";
183
187
  }
184
- function renderTurnRitualReminder(coordRoot, selfInstanceId, adapter) {
188
+ function turnRitualStatePath(coordRoot, instanceId) {
189
+ return join(coordRoot, ".harnery", `.last-turn-ritual.${instanceId}`);
190
+ }
191
+ const TURN_RITUAL_BOUNCED = "bounced";
192
+ /**
193
+ * Called by the Stop hook when it blocks a turn for a missing ritual step, so
194
+ * the next prompt carries the reminder again.
195
+ */
196
+ export function markTurnRitualBounced(coordRoot, instanceId) {
197
+ writeHashFile(turnRitualStatePath(coordRoot, instanceId), TURN_RITUAL_BOUNCED);
198
+ }
199
+ function renderTurnRitualReminder(coordRoot, selfInstanceId, sessionId, adapter) {
185
200
  const hb = readLiveCoordinationRow(coordRoot, selfInstanceId);
186
201
  if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
187
202
  return "";
203
+ // The key changes when native context compaction drops the earlier reminder.
204
+ let generation = 0;
205
+ try {
206
+ generation = readContextState(coordRoot, sessionId)?.generation ?? 0;
207
+ }
208
+ catch {
209
+ /* no continuity state: treat as the first context generation */
210
+ }
211
+ const key = `context-generation=${generation}`;
212
+ const statePath = turnRitualStatePath(coordRoot, selfInstanceId);
213
+ const previous = safeRead(statePath);
214
+ if (previous === key)
215
+ return "";
216
+ writeHashFile(statePath, key);
217
+ const bounced = previous === TURN_RITUAL_BOUNCED
218
+ ? "Your previous turn was bounced by the Stop hook because the ritual was incomplete. "
219
+ : "";
188
220
  const bin = resolveBinName(coordRoot);
189
221
  const statusCommand = endOfTurnStatusCommand(coordRoot);
190
222
  // Claude Code and Cursor can both enforce completed assistant reply text.
191
223
  const paste = adapter === "claude-code" || adapter === "cursor"
192
224
  ? " and paste its output verbatim in a fenced code block at the end of your reply"
193
225
  : "";
194
- return (`Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
226
+ return (bounced +
227
+ `Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
195
228
  "A turn that ends without these is bounced by the Stop hook and costs a retry.");
196
229
  }
197
230
  function renderStatusFooterReminder(coordRoot, selfInstanceId) {
@@ -299,8 +332,43 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
299
332
  const remote = readRemoteMachinesSafe(coordRoot);
300
333
  if (peers.length === 0 && remote.length === 0)
301
334
  return "";
302
- // Build hash basis: sorted-by-instance_id projection of semantically-relevant fields.
303
- const basis = peers
335
+ const rendered = renderPeerTable({
336
+ peers,
337
+ remote,
338
+ mySessionId,
339
+ nowMs: Date.now(),
340
+ freshnessSeconds: coordFreshnessSeconds(coordRoot),
341
+ relations: forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))),
342
+ });
343
+ if (!rendered.table)
344
+ return "";
345
+ const hashFile = join(coordRoot, ".harnery", `.last-peer-hash.${selfInstanceId}`);
346
+ const oldHash = safeRead(hashFile);
347
+ if (oldHash && oldHash === rendered.hash)
348
+ return "";
349
+ // Persist hash atomically (temp + rename, same convention as other coord writes).
350
+ writeHashFile(hashFile, rendered.hash);
351
+ return rendered.table;
352
+ }
353
+ /**
354
+ * Pure peer-table render plus its change key. The key covers only what makes
355
+ * the table news: which live sessions exist, their lifecycle, and the files
356
+ * they claim. Activity and task text change on nearly every peer tool call, so
357
+ * they render in the table but never trigger a reprint by themselves.
358
+ */
359
+ export function renderPeerTable(input) {
360
+ const { peers, remote, mySessionId, nowMs, freshnessSeconds } = input;
361
+ const cutoffMs = nowMs - freshnessSeconds * 1000;
362
+ const live = [];
363
+ const stale = [];
364
+ for (const peer of peers) {
365
+ const seenMs = Date.parse(peer.last_heartbeat ?? "");
366
+ if (Number.isFinite(seenMs) && seenMs >= cutoffMs)
367
+ live.push(peer);
368
+ else
369
+ stale.push(peer);
370
+ }
371
+ const basis = live
304
372
  .map((p) => ({
305
373
  name: p.name ?? null,
306
374
  instance_id: p.instance_id ?? null,
@@ -309,39 +377,28 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
309
377
  started_at: p.started_at ?? null,
310
378
  files_touched: Array.from(p.files_touched ?? []).sort(),
311
379
  platform: p.platform ?? null,
312
- activity: p.activity ?? "unknown",
313
380
  task_state: p.task_state ?? "active",
314
381
  task_state_reason: p.task_state_reason ?? null,
315
382
  }))
316
383
  .sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
317
- // Remote basis: machine + per-agent identity/task/files (published_at is
318
- // excluded — a keepalive re-publish alone shouldn't re-emit the table).
384
+ // Remote basis: machine + per-agent identity, lifecycle, and files
385
+ // (published_at, activity, and task are excluded for the same reason).
319
386
  const remoteBasis = remote.map((m) => ({
320
387
  machine: m.machine,
321
388
  agents: m.agents.map((a) => ({
322
389
  instance_id: a.instance_id,
323
390
  name: a.name ?? null,
324
- task: a.task ?? null,
325
- activity: a.activity,
326
391
  task_state: a.task_state,
327
392
  task_state_reason: a.task_state_reason ?? null,
328
393
  files_touched: Array.from(a.files_touched ?? []).sort(),
329
394
  })),
330
395
  }));
331
- const newHash = sha256Hex16(JSON.stringify({ local: basis, remote: remoteBasis }));
332
- const hashFile = join(coordRoot, ".harnery", `.last-peer-hash.${selfInstanceId}`);
333
- const oldHash = safeRead(hashFile);
334
- if (oldHash && oldHash === newHash)
335
- return "";
336
- // Render peer table via the same formatter used at SessionStart.
337
- const local = formatPeerTable(peers, mySessionId, forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))));
338
- const remoteTable = formatRemoteTable(remote);
396
+ const hash = sha256Hex16(JSON.stringify({ local: basis, remote: remoteBasis }));
397
+ const nowSec = Math.floor(nowMs / 1000);
398
+ const local = formatPeerTable(live, stale, mySessionId, nowSec, input.relations);
399
+ const remoteTable = formatRemoteTable(remote, nowSec);
339
400
  const table = [local, remoteTable].filter(Boolean).join("\n\n");
340
- if (!table)
341
- return "";
342
- // Persist hash atomically (temp + rename, same convention as other coord writes).
343
- writeHashFile(hashFile, newHash);
344
- return table;
401
+ return { hash, table };
345
402
  }
346
403
  /** readRemoteMachines with a local failure guard (rendering must never throw). */
347
404
  function readRemoteMachinesSafe(coordRoot) {
@@ -353,10 +410,9 @@ function readRemoteMachinesSafe(coordRoot) {
353
410
  }
354
411
  }
355
412
  /** Render sessions on other machines (presence transport) as a subtable. */
356
- function formatRemoteTable(remote) {
413
+ function formatRemoteTable(remote, nowSec) {
357
414
  if (remote.length === 0)
358
415
  return "";
359
- const nowSec = Math.floor(Date.now() / 1000);
360
416
  const lines = [];
361
417
  for (const m of remote) {
362
418
  for (const a of m.agents.slice(0, 10)) {
@@ -451,16 +507,11 @@ function writeHashFile(path, value) {
451
507
  /* swallow */
452
508
  }
453
509
  }
454
- /* The peer-table formatter is a near-duplicate of session-context.ts's, but
455
- * intentionally co-located here so the prompt-context renderer is self-contained
456
- * (no cross-file imports of formatting internals). If both renderers diverge,
457
- * pull the shared bits into a small util module. */
458
- function formatPeerTable(peers, mySessionId, relations = new Map()) {
510
+ function formatPeerTable(peers, stalePeers, mySessionId, nowSec, relations = new Map()) {
459
511
  if (peers.length === 0)
460
512
  return "";
461
- const nowSec = Math.floor(Date.now() / 1000);
462
513
  const fold = {};
463
- for (const p of peers) {
514
+ for (const p of [...peers, ...stalePeers]) {
464
515
  const kind = p.kind ?? "unknown";
465
516
  if (kind === "transient" && p.session_id) {
466
517
  fold[p.session_id] = (fold[p.session_id] ?? []).concat(p.files_touched ?? []);
@@ -474,28 +525,52 @@ function formatPeerTable(peers, mySessionId, relations = new Map()) {
474
525
  const relation = relations.get(p.instance_id ?? "");
475
526
  return { ...p, display_files: display, ...(relation ? { relation } : {}) };
476
527
  });
528
+ assignUniqueLabels(rows);
529
+ const staleRows = stalePeers.filter((p) => (p.kind ?? "unknown") !== "transient");
477
530
  // Related sessions sort first so a fork parent never hides in "+N more".
478
531
  const blocking = rows
479
532
  .filter((p) => p.session_id !== mySessionId)
480
533
  .sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
481
534
  const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
535
+ const staleBlocking = staleRows.filter((p) => p.session_id !== mySessionId).length;
536
+ const staleGroup = staleRows.length - staleBlocking;
482
537
  const out = [];
483
- const blk = renderSubtable(blocking, "Other agent groups active (their files block you):", nowSec);
538
+ const blk = renderSubtable(blocking, staleBlocking, "Other agent groups active (their files block you):", nowSec);
484
539
  if (blk)
485
540
  out.push(blk);
486
- const grp = renderSubtable(group, "Your group (subagents / parent / siblings; no mutual block):", nowSec);
541
+ const grp = renderSubtable(group, staleGroup, "Your group (subagents / parent / siblings; no mutual block):", nowSec);
487
542
  if (grp)
488
543
  out.push(grp);
489
544
  return out.join("\n\n");
490
545
  }
546
+ /**
547
+ * One full, distinct label per row. An unnamed session shows its whole
548
+ * instance id (an 8-character prefix collides for time-ordered ids), and a
549
+ * name two live rows share carries its instance id so they read as two.
550
+ */
551
+ function assignUniqueLabels(rows) {
552
+ const base = (r) => r.name ?? r.instance_id ?? "unknown";
553
+ const counts = new Map();
554
+ for (const r of rows)
555
+ counts.set(base(r), (counts.get(base(r)) ?? 0) + 1);
556
+ for (const r of rows) {
557
+ const label = base(r);
558
+ r.label =
559
+ (counts.get(label) ?? 0) > 1 && r.name && r.instance_id
560
+ ? `${label} (${r.instance_id})`
561
+ : label;
562
+ }
563
+ }
491
564
  function byStartedAt(a, b) {
492
565
  return (a.started_at ?? "").localeCompare(b.started_at ?? "");
493
566
  }
494
- function renderSubtable(rows, header, nowSec) {
567
+ function renderSubtable(rows, staleCount, header, nowSec) {
495
568
  if (rows.length === 0)
496
569
  return "";
497
570
  const first = rows.slice(0, 10).map((r) => formatRow(r, nowSec));
498
- const overflow = rows.length > 10 ? `\n +${rows.length - 10} more` : "";
571
+ const more = Math.max(0, rows.length - 10) + staleCount;
572
+ const staleNote = staleCount > 0 ? ` (${staleCount} with no recent heartbeat)` : "";
573
+ const overflow = more > 0 ? `\n +${more} more${staleNote}` : "";
499
574
  return `${header}\n${first.join("\n")}${overflow}`;
500
575
  }
501
576
  function formatRow(r, nowSec) {
@@ -506,9 +581,8 @@ function formatRow(r, nowSec) {
506
581
  const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
507
582
  const ageFrom = startedSec === null ? "age unknown" : fmtAge(nowSec - startedSec);
508
583
  const filesPart = fmtFiles(r.display_files);
509
- // Prefer a short instance_id over a bare "unknown" so an incomplete row is
510
- // still identifiable.
511
- const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
584
+ // An unnamed row shows its full instance id; a prefix can collide.
585
+ const label = r.label ?? r.name ?? r.instance_id ?? "unknown";
512
586
  const relation = r.relation ? ` [${r.relation}]` : "";
513
587
  return ` - agent-${label}${relation}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
514
588
  }
@@ -31,7 +31,7 @@
31
31
  * two callers reading the same live process disagree.
32
32
  */
33
33
  /** Which probe answers "when did this process start?" on this machine. */
34
- export type StartTokenProbe = "procfs" | "ps";
34
+ export type StartTokenProbe = "procfs" | "ps" | "windows";
35
35
  /**
36
36
  * Which probe this machine uses, decided once.
37
37
  *
@@ -68,6 +68,8 @@ export declare function psStartToken(pid: number, run?: (pid: number) => {
68
68
  * will not say.
69
69
  */
70
70
  export declare function processStartToken(pid: number): string | null;
71
+ /** Windows process creation time in invariant UTC ticks, independent of locale. */
72
+ export declare function windowsStartToken(pid: number): string | null;
71
73
  export type ProcessGenerationVerdict = "same" | "different" | "unknown";
72
74
  type ReadProcessStartToken = (pid: number) => string | null;
73
75
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,CAAC;AAc9C;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAOjD;AAyBD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAiB,GAC7E,MAAM,GAAG,IAAI,CAUf;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAExE,KAAK,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,SAAS,GAAE,qBAAyC,GACnD,wBAAwB,CAuB1B;AAOD,MAAM,WAAW,mCAAmC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAClE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,YAAY,GAAE,mCAAwC,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,CAUtD"}
1
+ {"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC;AAc1D;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAQjD;AAyBD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAiB,GAC7E,MAAM,GAAG,IAAI,CAUf;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAExE,KAAK,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,SAAS,GAAE,qBAAyC,GACnD,wBAAwB,CAuB1B;AAOD,MAAM,WAAW,mCAAmC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAClE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,YAAY,GAAE,mCAAwC,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,CAUtD"}
@@ -52,10 +52,11 @@ let cachedBootId;
52
52
  */
53
53
  export function startTokenProbe() {
54
54
  const override = coordEnv("PID_PROBE");
55
- if (override === "procfs" || override === "ps")
55
+ if (override === "procfs" || override === "ps" || override === "windows")
56
56
  return override;
57
57
  if (cachedProbe === undefined) {
58
- cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
58
+ cachedProbe =
59
+ process.platform === "win32" ? "windows" : existsSync("/proc/self/stat") ? "procfs" : "ps";
59
60
  }
60
61
  return cachedProbe;
61
62
  }
@@ -147,9 +148,30 @@ function defaultPsRun(pid) {
147
148
  * will not say.
148
149
  */
149
150
  export function processStartToken(pid) {
150
- if (!Number.isFinite(pid) || pid <= 0)
151
+ if (!Number.isSafeInteger(pid) || pid <= 0)
151
152
  return null;
152
- return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
153
+ const probe = startTokenProbe();
154
+ if (probe === "windows")
155
+ return windowsStartToken(pid);
156
+ return probe === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
157
+ }
158
+ /** Windows process creation time in invariant UTC ticks, independent of locale. */
159
+ export function windowsStartToken(pid) {
160
+ if (!Number.isSafeInteger(pid) || pid <= 0)
161
+ return null;
162
+ try {
163
+ const out = spawnSync("powershell.exe", [
164
+ "-NoProfile",
165
+ "-NonInteractive",
166
+ "-Command",
167
+ `try { (Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks } catch { exit 1 }`,
168
+ ], { encoding: "utf8", timeout: 4_000, windowsHide: true });
169
+ const ticks = out.stdout?.trim();
170
+ return out.status === 0 && ticks && /^\d+$/.test(ticks) ? `w${ticks}` : null;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
153
175
  }
154
176
  /**
155
177
  * Does `pid` still name the run that wrote `recordedToken`?
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Project self-update engine (ADR 0229).
3
+ *
4
+ * `autoUpdateTick` is the hot path every prompt, session start, and host CLI
5
+ * command calls: one stat of the throttle stamp when nothing is due. When the
6
+ * stamp is stale it claims the stamp and launches a detached
7
+ * `<harn> autoupdate run --background`, which decides with `checkForUpdate`
8
+ * and, only when needed, runs the host's configured steps under a lock.
9
+ */
10
+ import { type GitRunner } from "./git.js";
11
+ import { type LockInspection, type LockOwner } from "./state.js";
12
+ import { type AutoUpdateConfig, type AutoUpdateNotice, type AutoUpdateState, type CheckDecision, type RunOutcome, type RunRecord, type TickResult } from "./types.js";
13
+ /** Set on the detached runner and every step so nested CLI calls never tick. */
14
+ export declare const AUTOUPDATE_CHILD_ENV = "HARNERY_AUTOUPDATE_CHILD";
15
+ export interface StepRunResult {
16
+ exitCode: number | null;
17
+ timedOut: boolean;
18
+ output: string;
19
+ ms: number;
20
+ }
21
+ export type StepRunner = (argv: string[], opts: {
22
+ cwd: string;
23
+ timeoutMs: number;
24
+ env: NodeJS.ProcessEnv;
25
+ onChunk?: (chunk: string, stream: "stdout" | "stderr") => void;
26
+ }) => Promise<StepRunResult>;
27
+ export interface AutoUpdateDeps {
28
+ now?: () => number;
29
+ env?: NodeJS.ProcessEnv;
30
+ readConfig?: (coordRoot: string) => AutoUpdateConfig | null;
31
+ git?: GitRunner;
32
+ /** Launch the detached runner (tick). */
33
+ spawnRunner?: (coordRoot: string) => void;
34
+ runStep?: StepRunner;
35
+ /** Expired-artifact cleanup; best effort. */
36
+ runCleanup?: (coordRoot: string) => void;
37
+ binName?: string;
38
+ }
39
+ /** The CLI entry the detached runner executes with `process.execPath`. */
40
+ export declare function harnCliEntry(): string;
41
+ /** Set the throttle stamp's mtime (creating it), which is all the tick reads. */
42
+ export declare function setStamp(coordRoot: string, atMs: number): void;
43
+ /** Keep the log bounded: past 256 KB, keep only its last 64 KB. */
44
+ export declare function rotateLog(path: string): void;
45
+ export declare function isInstructionPath(path: string): boolean;
46
+ export declare function isHookConfigPath(path: string): boolean;
47
+ /** `harnery-notice:` lines in step output. */
48
+ export declare function parseNoticeLines(output: string): string[];
49
+ /**
50
+ * Activity hook. Never throws. When throttled it reads the config and stats
51
+ * one file; it runs no Git and parses no state.
52
+ */
53
+ export declare function autoUpdateTick(coordRoot: string, deps?: AutoUpdateDeps): TickResult;
54
+ export declare function checkForUpdate(coordRoot: string, cfg: AutoUpdateConfig, state: AutoUpdateState, deps?: AutoUpdateDeps & {
55
+ force?: boolean;
56
+ }): CheckDecision;
57
+ /**
58
+ * Resolve a step's argv for the current platform. On Windows a bare command
59
+ * name such as `bun` or `git` is looked up on PATH with PATHEXT, because
60
+ * spawning without a shell does not try `.cmd` shims; a `.cmd` or `.bat`
61
+ * match runs through `cmd.exe`. Elsewhere the argv is used as is.
62
+ */
63
+ export declare function resolveStepCommand(cmd: string, args: string[], env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (path: string) => boolean): {
64
+ cmd: string;
65
+ args: string[];
66
+ verbatim: boolean;
67
+ };
68
+ export declare const defaultStepRunner: StepRunner;
69
+ export interface RunAutoUpdateOptions {
70
+ force?: boolean;
71
+ /** Detached runner: output goes to the log only. */
72
+ background?: boolean;
73
+ /** Foreground streaming of step output (in addition to the log). */
74
+ onOutput?: (chunk: string, stream: "stdout" | "stderr") => void;
75
+ deps?: AutoUpdateDeps;
76
+ }
77
+ export interface RunAutoUpdateResult {
78
+ outcome: RunOutcome;
79
+ decision: CheckDecision | null;
80
+ record: RunRecord | null;
81
+ /** Notices recorded by this run. */
82
+ notices: AutoUpdateNotice[];
83
+ /** Holder of the lock when `skipped-locked`. */
84
+ lockOwner?: LockOwner | null;
85
+ }
86
+ export declare function runAutoUpdate(coordRootRaw: string, opts?: RunAutoUpdateOptions): Promise<RunAutoUpdateResult>;
87
+ export interface AutoUpdateStatus {
88
+ config: AutoUpdateConfig | null;
89
+ state: AutoUpdateState;
90
+ /** Milliseconds since the throttle stamp (negative while backing off); null when never checked. */
91
+ stampAge: number | null;
92
+ /** Milliseconds until a tick would launch a check; 0 when due. */
93
+ nextCheckMs: number | null;
94
+ locked: LockInspection;
95
+ }
96
+ export declare function autoUpdateStatus(coordRootRaw: string, deps?: AutoUpdateDeps): AutoUpdateStatus;
97
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/autoupdate/engine.ts"],"names":[],"mappings":"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"}