agy-worker-mcp 0.3.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/CHANGELOG.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/commands/agy-ceiling.md +31 -0
  5. package/dist/broker/blockers.d.ts +86 -0
  6. package/dist/broker/blockers.d.ts.map +1 -0
  7. package/dist/broker/blockers.js +390 -0
  8. package/dist/broker/blockers.js.map +1 -0
  9. package/dist/broker/outcome.d.ts +85 -0
  10. package/dist/broker/outcome.d.ts.map +1 -0
  11. package/dist/broker/outcome.js +147 -0
  12. package/dist/broker/outcome.js.map +1 -0
  13. package/dist/broker/reconcile.d.ts +74 -0
  14. package/dist/broker/reconcile.d.ts.map +1 -0
  15. package/dist/broker/reconcile.js +415 -0
  16. package/dist/broker/reconcile.js.map +1 -0
  17. package/dist/broker/result.d.ts +88 -0
  18. package/dist/broker/result.d.ts.map +1 -0
  19. package/dist/broker/result.js +234 -0
  20. package/dist/broker/result.js.map +1 -0
  21. package/dist/broker/verify.d.ts +52 -0
  22. package/dist/broker/verify.d.ts.map +1 -0
  23. package/dist/broker/verify.js +174 -0
  24. package/dist/broker/verify.js.map +1 -0
  25. package/dist/contract/errors.d.ts +119 -0
  26. package/dist/contract/errors.d.ts.map +1 -0
  27. package/dist/contract/errors.js +99 -0
  28. package/dist/contract/errors.js.map +1 -0
  29. package/dist/contract/index.d.ts +10 -0
  30. package/dist/contract/index.d.ts.map +1 -0
  31. package/dist/contract/index.js +10 -0
  32. package/dist/contract/index.js.map +1 -0
  33. package/dist/contract/paths.d.ts +139 -0
  34. package/dist/contract/paths.d.ts.map +1 -0
  35. package/dist/contract/paths.js +359 -0
  36. package/dist/contract/paths.js.map +1 -0
  37. package/dist/contract/schema.sql +64 -0
  38. package/dist/contract/types.d.ts +1113 -0
  39. package/dist/contract/types.d.ts.map +1 -0
  40. package/dist/contract/types.js +114 -0
  41. package/dist/contract/types.js.map +1 -0
  42. package/dist/events/cursor.d.ts +65 -0
  43. package/dist/events/cursor.d.ts.map +1 -0
  44. package/dist/events/cursor.js +286 -0
  45. package/dist/events/cursor.js.map +1 -0
  46. package/dist/events/detect.d.ts +58 -0
  47. package/dist/events/detect.d.ts.map +1 -0
  48. package/dist/events/detect.js +183 -0
  49. package/dist/events/detect.js.map +1 -0
  50. package/dist/events/normalize.d.ts +30 -0
  51. package/dist/events/normalize.d.ts.map +1 -0
  52. package/dist/events/normalize.js +158 -0
  53. package/dist/events/normalize.js.map +1 -0
  54. package/dist/events/parse.d.ts +41 -0
  55. package/dist/events/parse.d.ts.map +1 -0
  56. package/dist/events/parse.js +89 -0
  57. package/dist/events/parse.js.map +1 -0
  58. package/dist/gate/bind.d.ts +28 -0
  59. package/dist/gate/bind.d.ts.map +1 -0
  60. package/dist/gate/bind.js +154 -0
  61. package/dist/gate/bind.js.map +1 -0
  62. package/dist/gate/gate.d.ts +139 -0
  63. package/dist/gate/gate.d.ts.map +1 -0
  64. package/dist/gate/gate.js +488 -0
  65. package/dist/gate/gate.js.map +1 -0
  66. package/dist/gate/hooks-file.d.ts +55 -0
  67. package/dist/gate/hooks-file.d.ts.map +1 -0
  68. package/dist/gate/hooks-file.js +104 -0
  69. package/dist/gate/hooks-file.js.map +1 -0
  70. package/dist/gate.d.ts +3 -0
  71. package/dist/gate.d.ts.map +1 -0
  72. package/dist/gate.js +20 -0
  73. package/dist/gate.js.map +1 -0
  74. package/dist/policy/ceiling-review.d.ts +22 -0
  75. package/dist/policy/ceiling-review.d.ts.map +1 -0
  76. package/dist/policy/ceiling-review.js +170 -0
  77. package/dist/policy/ceiling-review.js.map +1 -0
  78. package/dist/policy/ceiling.d.ts +128 -0
  79. package/dist/policy/ceiling.d.ts.map +1 -0
  80. package/dist/policy/ceiling.js +273 -0
  81. package/dist/policy/ceiling.js.map +1 -0
  82. package/dist/policy/containment.d.ts +100 -0
  83. package/dist/policy/containment.d.ts.map +1 -0
  84. package/dist/policy/containment.js +1071 -0
  85. package/dist/policy/containment.js.map +1 -0
  86. package/dist/policy/hard-deny.d.ts +28 -0
  87. package/dist/policy/hard-deny.d.ts.map +1 -0
  88. package/dist/policy/hard-deny.js +78 -0
  89. package/dist/policy/hard-deny.js.map +1 -0
  90. package/dist/policy/profiles.d.ts +82 -0
  91. package/dist/policy/profiles.d.ts.map +1 -0
  92. package/dist/policy/profiles.js +317 -0
  93. package/dist/policy/profiles.js.map +1 -0
  94. package/dist/policy/rules.d.ts +138 -0
  95. package/dist/policy/rules.d.ts.map +1 -0
  96. package/dist/policy/rules.js +1180 -0
  97. package/dist/policy/rules.js.map +1 -0
  98. package/dist/policy/seatbelt.d.ts +32 -0
  99. package/dist/policy/seatbelt.d.ts.map +1 -0
  100. package/dist/policy/seatbelt.js +59 -0
  101. package/dist/policy/seatbelt.js.map +1 -0
  102. package/dist/policy/tools.d.ts +36 -0
  103. package/dist/policy/tools.d.ts.map +1 -0
  104. package/dist/policy/tools.js +94 -0
  105. package/dist/policy/tools.js.map +1 -0
  106. package/dist/runner/gate-watchdog.d.ts +93 -0
  107. package/dist/runner/gate-watchdog.d.ts.map +1 -0
  108. package/dist/runner/gate-watchdog.js +153 -0
  109. package/dist/runner/gate-watchdog.js.map +1 -0
  110. package/dist/runner/idle.d.ts +51 -0
  111. package/dist/runner/idle.d.ts.map +1 -0
  112. package/dist/runner/idle.js +81 -0
  113. package/dist/runner/idle.js.map +1 -0
  114. package/dist/runner/inbox.d.ts +56 -0
  115. package/dist/runner/inbox.d.ts.map +1 -0
  116. package/dist/runner/inbox.js +134 -0
  117. package/dist/runner/inbox.js.map +1 -0
  118. package/dist/runner/reap.d.ts +65 -0
  119. package/dist/runner/reap.d.ts.map +1 -0
  120. package/dist/runner/reap.js +168 -0
  121. package/dist/runner/reap.js.map +1 -0
  122. package/dist/runner/runner.d.ts +51 -0
  123. package/dist/runner/runner.d.ts.map +1 -0
  124. package/dist/runner/runner.js +289 -0
  125. package/dist/runner/runner.js.map +1 -0
  126. package/dist/runner/spawn.d.ts +98 -0
  127. package/dist/runner/spawn.d.ts.map +1 -0
  128. package/dist/runner/spawn.js +230 -0
  129. package/dist/runner/spawn.js.map +1 -0
  130. package/dist/runner/verify.d.ts +24 -0
  131. package/dist/runner/verify.d.ts.map +1 -0
  132. package/dist/runner/verify.js +108 -0
  133. package/dist/runner/verify.js.map +1 -0
  134. package/dist/runner.d.ts +3 -0
  135. package/dist/runner.d.ts.map +1 -0
  136. package/dist/runner.js +15 -0
  137. package/dist/runner.js.map +1 -0
  138. package/dist/server/context.d.ts +41 -0
  139. package/dist/server/context.d.ts.map +1 -0
  140. package/dist/server/context.js +59 -0
  141. package/dist/server/context.js.map +1 -0
  142. package/dist/server/instructions.d.ts +13 -0
  143. package/dist/server/instructions.d.ts.map +1 -0
  144. package/dist/server/instructions.js +121 -0
  145. package/dist/server/instructions.js.map +1 -0
  146. package/dist/server/server.d.ts +20 -0
  147. package/dist/server/server.d.ts.map +1 -0
  148. package/dist/server/server.js +55 -0
  149. package/dist/server/server.js.map +1 -0
  150. package/dist/server/tools/cancel.d.ts +24 -0
  151. package/dist/server/tools/cancel.d.ts.map +1 -0
  152. package/dist/server/tools/cancel.js +59 -0
  153. package/dist/server/tools/cancel.js.map +1 -0
  154. package/dist/server/tools/capabilities.d.ts +36 -0
  155. package/dist/server/tools/capabilities.d.ts.map +1 -0
  156. package/dist/server/tools/capabilities.js +116 -0
  157. package/dist/server/tools/capabilities.js.map +1 -0
  158. package/dist/server/tools/ceiling.d.ts +31 -0
  159. package/dist/server/tools/ceiling.d.ts.map +1 -0
  160. package/dist/server/tools/ceiling.js +121 -0
  161. package/dist/server/tools/ceiling.js.map +1 -0
  162. package/dist/server/tools/index.d.ts +38 -0
  163. package/dist/server/tools/index.d.ts.map +1 -0
  164. package/dist/server/tools/index.js +115 -0
  165. package/dist/server/tools/index.js.map +1 -0
  166. package/dist/server/tools/listJobs.d.ts +30 -0
  167. package/dist/server/tools/listJobs.d.ts.map +1 -0
  168. package/dist/server/tools/listJobs.js +37 -0
  169. package/dist/server/tools/listJobs.js.map +1 -0
  170. package/dist/server/tools/logs.d.ts +31 -0
  171. package/dist/server/tools/logs.d.ts.map +1 -0
  172. package/dist/server/tools/logs.js +96 -0
  173. package/dist/server/tools/logs.js.map +1 -0
  174. package/dist/server/tools/result.d.ts +32 -0
  175. package/dist/server/tools/result.d.ts.map +1 -0
  176. package/dist/server/tools/result.js +95 -0
  177. package/dist/server/tools/result.js.map +1 -0
  178. package/dist/server/tools/send.d.ts +37 -0
  179. package/dist/server/tools/send.d.ts.map +1 -0
  180. package/dist/server/tools/send.js +71 -0
  181. package/dist/server/tools/send.js.map +1 -0
  182. package/dist/server/tools/sessions.d.ts +28 -0
  183. package/dist/server/tools/sessions.d.ts.map +1 -0
  184. package/dist/server/tools/sessions.js +54 -0
  185. package/dist/server/tools/sessions.js.map +1 -0
  186. package/dist/server/tools/start.d.ts +107 -0
  187. package/dist/server/tools/start.d.ts.map +1 -0
  188. package/dist/server/tools/start.js +482 -0
  189. package/dist/server/tools/start.js.map +1 -0
  190. package/dist/server/tools/wait.d.ts +29 -0
  191. package/dist/server/tools/wait.d.ts.map +1 -0
  192. package/dist/server/tools/wait.js +120 -0
  193. package/dist/server/tools/wait.js.map +1 -0
  194. package/dist/server.d.ts +3 -0
  195. package/dist/server.d.ts.map +1 -0
  196. package/dist/server.js +17 -0
  197. package/dist/server.js.map +1 -0
  198. package/dist/setup/install.d.ts +52 -0
  199. package/dist/setup/install.d.ts.map +1 -0
  200. package/dist/setup/install.js +108 -0
  201. package/dist/setup/install.js.map +1 -0
  202. package/dist/setup.d.ts +3 -0
  203. package/dist/setup.d.ts.map +1 -0
  204. package/dist/setup.js +24 -0
  205. package/dist/setup.js.map +1 -0
  206. package/dist/store/db.d.ts +49 -0
  207. package/dist/store/db.d.ts.map +1 -0
  208. package/dist/store/db.js +101 -0
  209. package/dist/store/db.js.map +1 -0
  210. package/dist/store/jobs.d.ts +45 -0
  211. package/dist/store/jobs.d.ts.map +1 -0
  212. package/dist/store/jobs.js +150 -0
  213. package/dist/store/jobs.js.map +1 -0
  214. package/dist/store/locks.d.ts +60 -0
  215. package/dist/store/locks.d.ts.map +1 -0
  216. package/dist/store/locks.js +172 -0
  217. package/dist/store/locks.js.map +1 -0
  218. package/dist/store/sessions.d.ts +37 -0
  219. package/dist/store/sessions.d.ts.map +1 -0
  220. package/dist/store/sessions.js +117 -0
  221. package/dist/store/sessions.js.map +1 -0
  222. package/dist/trace/digest.d.ts +77 -0
  223. package/dist/trace/digest.d.ts.map +1 -0
  224. package/dist/trace/digest.js +481 -0
  225. package/dist/trace/digest.js.map +1 -0
  226. package/package.json +68 -0
  227. package/skills/agy-ceiling/SKILL.md +72 -0
@@ -0,0 +1,1113 @@
1
+ /**
2
+ * Frozen contract types for agy-worker-mcp.
3
+ *
4
+ * READ-ONLY after Stage 1. If a downstream module needs a change here, report it
5
+ * through `contract_change_requests` instead of editing this file.
6
+ *
7
+ * Everything describing agy's own wire format is transcribed from measured
8
+ * output and raw NDJSON transcripts. No field in the `Agy*` types is invented.
9
+ */
10
+ /** Bumped whenever `schema.sql` changes shape. Stored in `meta.schema_version`. */
11
+ export declare const SCHEMA_VERSION = 1;
12
+ /**
13
+ * Shape version of `jobs/<id>/broker-result.json`, independent of the SQLite
14
+ * `SCHEMA_VERSION` (that file is not a table).
15
+ *
16
+ * 1 → 2: `verification.permission_denials` + `verification.environment_blocks`
17
+ * became the single `verification.blockers` list.
18
+ * 2 → 3: `verification.verify` was added (PR6, `verify_command`) — `null` on a
19
+ * migrated file, since no pre-0.2.0 job ever ran a verify command.
20
+ * `loadBrokerResult` migrates an older file in memory rather than failing, so a
21
+ * job directory written by an earlier release stays readable; an unknown
22
+ * *newer* version is refused loudly instead.
23
+ */
24
+ export declare const BROKER_RESULT_VERSION = 3;
25
+ /** Environment variables this package reads. Nothing else may be consulted. */
26
+ export declare const ENV: {
27
+ /** Overrides project-root discovery (see docs/operations.md). */
28
+ readonly PROJECT_ROOT: "AGY_WORKER_PROJECT";
29
+ /** Overrides `~/.agy-worker` as the state home. Tests set this. */
30
+ readonly STATE_HOME: "AGY_WORKER_HOME";
31
+ /** Absolute path to the `agy` executable. Tests point this at the fake. */
32
+ readonly AGY_BIN: "AGY_WORKER_AGY_BIN";
33
+ /**
34
+ * Overrides `binPath('gate')`. Tests point this at a nonexistent path to
35
+ * exercise `agy_start`'s `existsSync(gatePath)` check (PR3 §3.1) without
36
+ * having to move the real `dist/gate.js` out from under a build every other
37
+ * test also depends on.
38
+ */
39
+ readonly GATE_BIN: "AGY_WORKER_GATE_BIN";
40
+ /** Job id the runner process is executing. Set by the server when spawning. */
41
+ readonly JOB_ID: "AGY_WORKER_JOB_ID";
42
+ /** Scenario file consumed by `test/fake-agy` only; never read by src/. */
43
+ readonly FAKE_SCENARIO: "AGY_FAKE_SCENARIO";
44
+ };
45
+ /**
46
+ * Flags that must never appear in a generated argv, at any call site.
47
+ * `--continue` resumes a *global* "most recent conversation" which collides
48
+ * head-on with multi-session operation; `--prompt-interactive` needs a TTY.
49
+ *
50
+ * `--dangerously-skip-permissions` was on this list until 0.2.0. It is now
51
+ * emitted on every spawn (`buildAgyArgv`) — see the comment there for the
52
+ * three invariants that make that safe and the measurement (M3) behind it.
53
+ */
54
+ export declare const FORBIDDEN_AGY_FLAGS: readonly string[];
55
+ /** agy's own prefix on a hook denial, measured in `.spike/out/run6.events.ndjson`. */
56
+ export declare const HOOK_DENIAL_PREFIX = "tool call denied by pre-tool hook:";
57
+ /**
58
+ * Marker the gate embeds in its `reason` string so `events/detect.ts` can parse a
59
+ * denial losslessly instead of scraping prose. agy passes `reason` through to the
60
+ * model verbatim (measured, §10), so the human-readable guidance comes first and
61
+ * the machine payload trails it in brackets:
62
+ *
63
+ * `<guidance sentence> [agy-worker-denial:{"required_rule":"command(git push)",...}]`
64
+ */
65
+ export declare const GATE_DENIAL_MARKER = "agy-worker-denial:";
66
+ /** Output signatures that mean "the sandbox blocked this silently" (see docs/permissions.md). */
67
+ export declare const ENVIRONMENT_BLOCK_SIGNATURES: readonly string[];
68
+ /**
69
+ * The subset of {@link ENVIRONMENT_BLOCK_SIGNATURES} that means "the sandbox
70
+ * refused the network" rather than a filesystem or capability refusal. Since
71
+ * 0.2.1 the two are lifted the same way — by not running the job sandboxed
72
+ * (I2) — so this only shapes the blocker's wording.
73
+ */
74
+ export declare const NETWORK_BLOCK_SIGNATURES: readonly string[];
75
+ /**
76
+ * Substrings that identify a refusal by agy's **own** permission engine, as
77
+ * opposed to our gate.
78
+ *
79
+ * Both entries are verbatim fragments of a measured message; the observed line
80
+ * was:
81
+ *
82
+ * `permission check failed for unsandboxed "ls -la /Users/<user>/.jdks/": user
83
+ * denied permission to run command:\nls -la /Users/<user>/.jdks/`
84
+ *
85
+ * These refusals are not ours and no `permissions.allow` rule can lift them —
86
+ * the gate never saw the call. Matching them only changes how the warning is
87
+ * worded, never the outcome (they arrive as ordinary `state: 'ERROR'` tool
88
+ * steps, which `decideOutcome` deliberately does not count as blocks).
89
+ */
90
+ export declare const AGY_ENGINE_REFUSAL_SIGNATURES: readonly string[];
91
+ /**
92
+ * Token accounting attached to `step_update` and `result`.
93
+ * `input_tokens` / `output_tokens` / `total_tokens` appear in every measured
94
+ * sample; `thinking_tokens` / `cache_read_tokens` appear in every raw capture but
95
+ * are not in the §4 excerpt, so they stay optional.
96
+ */
97
+ export interface AgyUsage {
98
+ input_tokens: number;
99
+ output_tokens: number;
100
+ total_tokens: number;
101
+ thinking_tokens?: number;
102
+ cache_read_tokens?: number;
103
+ }
104
+ /** `tool_info.error` — the only structured failure signal agy emits (Class 1). */
105
+ export interface AgyToolError {
106
+ type: string;
107
+ message: string;
108
+ }
109
+ export interface AgyToolInfo {
110
+ name?: string;
111
+ parameters?: Record<string, unknown>;
112
+ output?: string;
113
+ error?: AgyToolError;
114
+ }
115
+ /** Measured values. Anything else must be treated as unknown, not as an error. */
116
+ export type AgyStepState = 'ACTIVE' | 'DONE' | 'ERROR';
117
+ export type AgyStepType = 'user_input' | 'agent_response' | 'tool' | 'system_message';
118
+ export type AgyResultStatus = 'SUCCESS' | 'ERROR';
119
+ /**
120
+ * First line of every run. `conversation_id` sits on the *envelope*, not inside
121
+ * `init` — this is what makes gate binding possible before the first tool call.
122
+ */
123
+ export interface AgyInitEvent {
124
+ event: 'init';
125
+ conversation_id: string;
126
+ init: {
127
+ model: string;
128
+ cwd: string;
129
+ permission_mode: string;
130
+ tools: string[];
131
+ };
132
+ }
133
+ export interface AgyStepUpdateEvent {
134
+ event: 'step_update';
135
+ step_update: {
136
+ conversation_id: string;
137
+ step_index: number;
138
+ state: AgyStepState | string;
139
+ step_type: AgyStepType | string;
140
+ tool_name?: string;
141
+ tool_info?: AgyToolInfo;
142
+ text_delta?: string;
143
+ duration_seconds?: number;
144
+ usage?: AgyUsage;
145
+ };
146
+ }
147
+ /** One per turn. `num_turns` increases and survives `--conversation` resume. */
148
+ export interface AgyResultEvent {
149
+ event: 'result';
150
+ result: {
151
+ conversation_id: string;
152
+ status: AgyResultStatus | string;
153
+ response: string;
154
+ error?: string;
155
+ duration_seconds: number;
156
+ num_turns: number;
157
+ usage: AgyUsage;
158
+ };
159
+ }
160
+ export type AgyEvent = AgyInitEvent | AgyStepUpdateEvent | AgyResultEvent;
161
+ /** An envelope whose `event` we do not model. Preserved, never dropped. */
162
+ export interface AgyUnknownEvent {
163
+ event: string;
164
+ [key: string]: unknown;
165
+ }
166
+ /** stream-json *input*, one line = one turn (§5). Byte-identical to agy's schema. */
167
+ export interface AgyStreamUserInput {
168
+ event: 'user';
169
+ message: {
170
+ role: 'user';
171
+ content: Array<{
172
+ type: 'text';
173
+ text: string;
174
+ }>;
175
+ };
176
+ }
177
+ export type Lifecycle = 'queued' | 'starting' | 'running' | 'canceling' | 'finished';
178
+ /**
179
+ * Broker verdict. Distinct from `agent_status` on purpose: agy reports SUCCESS
180
+ * with exit 0 for both permission denials and sandbox blocks (measured, §9/§11),
181
+ * so its self-report can never produce `verified_success` on its own.
182
+ */
183
+ export type Outcome =
184
+ /** Finished, and every check the broker could run actually passed. */
185
+ 'verified_success'
186
+ /** Finished with no detected block, but nothing verifiable was requested. */
187
+ | 'success_unverified'
188
+ /** Ran to completion but a Class 1 denial or Class 2 environment block bit. */
189
+ | 'blocked'
190
+ /** agy reported ERROR, or exited non-zero. */
191
+ | 'failed'
192
+ /** `deadline_at` passed and the process group was killed. */
193
+ | 'timed_out'
194
+ /** `agy_cancel` killed it. */
195
+ | 'canceled'
196
+ /** Runner vanished without writing `exit_code`. */
197
+ | 'process_error'
198
+ /** Recorded pid is alive but is a different process (pid reuse). */
199
+ | 'orphaned';
200
+ /** agy's *unverified* self-report. Never drives `outcome` alone. */
201
+ export type AgentStatus = 'SUCCESS' | 'ERROR' | 'unknown';
202
+ /** Did the job honour the structured contract the caller asked for? */
203
+ export type ContractStatus = 'not_required' | 'satisfied' | 'violated' | 'unknown';
204
+ /** Built-in profiles (see docs/permissions.md). */
205
+ export type Profile = 'research_readonly' | 'general_worker';
206
+ export type SessionMode = 'oneshot' | 'session';
207
+ export type OnDenial = 'abort' | 'continue' | 'guide';
208
+ /** Lock rows actually stored in the `locks` table. */
209
+ export type LockScope = 'cwd_write' | 'session';
210
+ /**
211
+ * What an acquisition attempt was for. `running_limit` is the per-project
212
+ * concurrency ceiling — it is enforced inside the same
213
+ * `BEGIN IMMEDIATE` but owns no row, so it is not a `LockScope`.
214
+ */
215
+ export type LockRequestScope = LockScope | 'running_limit';
216
+ export type SessionState = 'active' | 'closed';
217
+ /** Row shape of `jobs`. Column-for-column with `schema.sql`. */
218
+ export interface JobRow {
219
+ job_id: string;
220
+ session_id: string | null;
221
+ lifecycle: Lifecycle;
222
+ outcome: Outcome | null;
223
+ headline: string | null;
224
+ cwd: string;
225
+ profile: Profile;
226
+ /** 0 = read-only, 1 = write. Stored as INTEGER because SQLite has no boolean. */
227
+ write_mode: number;
228
+ session_mode: SessionMode;
229
+ pid: number | null;
230
+ pgid: number | null;
231
+ /** Opaque platform token guarding against pid reuse. Compared, never parsed. */
232
+ proc_start_time: string | null;
233
+ created_at: number;
234
+ started_at: number | null;
235
+ finished_at: number | null;
236
+ deadline_at: number | null;
237
+ exit_code: number | null;
238
+ agent_status: AgentStatus | null;
239
+ contract_status: ContractStatus | null;
240
+ on_denial: OnDenial;
241
+ requested_by: string | null;
242
+ parent_task_id: string | null;
243
+ }
244
+ /** Row shape of `sessions`. `conversation_id` is bound lazily from the first init event. */
245
+ export interface SessionRow {
246
+ session_id: string;
247
+ conversation_id: string | null;
248
+ cwd: string;
249
+ model: string | null;
250
+ effort: string | null;
251
+ profile: Profile | null;
252
+ turn_count: number;
253
+ last_job_id: string | null;
254
+ created_at: number;
255
+ last_used_at: number;
256
+ state: SessionState;
257
+ }
258
+ /** Row shape of `locks`. */
259
+ export interface LockRow {
260
+ scope: LockScope;
261
+ key: string;
262
+ holder_job_id: string;
263
+ acquired_at: number;
264
+ }
265
+ export type NormalizedKind = 'session_start' | 'phase' | 'command_start' | 'command_end' | 'tool_error' | 'environment_block' | 'agent_message' | 'final_response' | 'system' | 'raw';
266
+ export type NormalizedSeverity = 'info' | 'warning' | 'error';
267
+ /**
268
+ * One human-readable line derived from one or more raw events. Repeated token
269
+ * deltas, blank lines and duplicates collapse away before this exists.
270
+ */
271
+ export interface NormalizedEvent {
272
+ kind: NormalizedKind;
273
+ severity: NormalizedSeverity;
274
+ /** `step_index` when the source was a `step_update`, else null. */
275
+ step_idx: number | null;
276
+ /** Single-line, already truncated, safe to concatenate into a tail. */
277
+ text: string;
278
+ /** Present for tool events. */
279
+ tool?: string;
280
+ /** `run_command`'s `CommandLine`, when that is what happened. */
281
+ command?: string;
282
+ /** Wall time agy attributed to the step. */
283
+ duration_seconds?: number;
284
+ /** Byte offset of the source line in `events.ndjson`, when known. */
285
+ offset?: number;
286
+ }
287
+ /**
288
+ * Class 1 — a structured refusal. Detected by
289
+ * `step_type === 'tool' && state === 'ERROR'`; details in `tool_info.error.message`.
290
+ * Our own gate authors that message, so `required_rule` round-trips losslessly.
291
+ */
292
+ export interface DenialClass1 {
293
+ class: 1;
294
+ tool: string;
295
+ command: string | null;
296
+ /** Rule string the caller can paste straight into the next `permissions.allow`. */
297
+ required_rule: string | null;
298
+ /**
299
+ * Which stage of the gate's decision order refused, when our own gate did.
300
+ * Null for a refusal we did not author.
301
+ *
302
+ * The recovery differs by stage, so the caller needs it: `profile_allowlist`
303
+ * and `default` are fixed by adding `required_rule` to `permissions.allow`,
304
+ * while `containment` and `deny_list` cannot be — the first needs a different
305
+ * path, the second is not narrowable at all.
306
+ */
307
+ policy: GateDenialPayload['policy'] | null;
308
+ /** Whether the refusal came from our gate (parsed marker) or elsewhere. */
309
+ source: 'gate' | 'agy' | 'unknown';
310
+ message: string;
311
+ step_idx: number | null;
312
+ }
313
+ /**
314
+ * Class 2 — a silent environment block. No error event, exit 0, status SUCCESS.
315
+ * Only visible as ordinary command output, which is why this is the dangerous one.
316
+ */
317
+ export interface DenialClass2 {
318
+ class: 2;
319
+ tool: string;
320
+ command: string | null;
321
+ /** Which entry of `ENVIRONMENT_BLOCK_SIGNATURES` matched. */
322
+ signature: string;
323
+ /** Short excerpt of the output around the match. */
324
+ excerpt: string;
325
+ step_idx: number | null;
326
+ }
327
+ export interface DenialScan {
328
+ permission_denials: DenialClass1[];
329
+ environment_blocks: DenialClass2[];
330
+ }
331
+ export type RuleVerb = 'command' | 'read_file' | 'write_file' | 'fetch' | 'url' | 'mcp' | 'browser';
332
+ /** `command(git status)` / `regex:` opt-in, per §12. Strict non-regex by default. */
333
+ export interface ParsedRule {
334
+ verb: RuleVerb;
335
+ pattern: string;
336
+ /** True when the pattern was written as `regex:...`. */
337
+ regex: boolean;
338
+ raw: string;
339
+ }
340
+ /**
341
+ * What a client may ask for on `agy_start`. Narrowing only — never widening.
342
+ * `sandboxed: true` forces agy's OS sandbox on for this job even on general_worker.
343
+ * `read_roots` narrows the *human ceiling* (`policy/ceiling.ts`).
344
+ */
345
+ /** OS boundary for an allowed `run_command` (0.3.0 PR6). Ordered: none < seatbelt < agy. */
346
+ export type SandboxMode = 'none' | 'seatbelt' | 'agy';
347
+ export interface RequestedPermissions {
348
+ allow?: string[];
349
+ deny?: string[];
350
+ /** Forces agy's OS sandbox on for this job (0.2.0 behaviour). Narrowing only: true forces sandbox on; false or absent changes nothing. Same as `sandbox: 'agy'`. */
351
+ sandboxed?: boolean;
352
+ /** Tightening only: `seatbelt` or `agy`. Never loosens what the profile or ceiling set. */
353
+ sandbox?: 'seatbelt' | 'agy';
354
+ /** Paths to add as extra read/exec roots (`--add-dir`, M4). Each must match a ceiling glob. */
355
+ read_roots?: string[];
356
+ }
357
+ /**
358
+ * The resolved policy written to `jobs/<id>/policy.json` and read by the gate on
359
+ * every tool call. `allow` is already intersected with the profile ceiling and
360
+ * `deny` already unions the profile's list with `HARD_DENY`.
361
+ *
362
+ * `network` and `default_decision` are gone: a bound job's gate never
363
+ * answers `ask` (I1), so there is no verdict left for "nothing matched" to
364
+ * fall back to — it is simply `deny`.
365
+ * In 0.2.1, allowed commands run without agy's OS sandbox by default on general_worker.
366
+ * `bypass_sandbox` determines `overwrite.BypassSandbox` for allowed run_command calls.
367
+ */
368
+ export interface EffectivePolicy {
369
+ profile: Profile;
370
+ /** Canonical workspace. The gate forces `overwrite.Cwd` to this. */
371
+ workspace: string;
372
+ read_roots: string[];
373
+ write_roots: string[];
374
+ allow: string[];
375
+ deny: string[];
376
+ /**
377
+ * Which OS boundary an allowed `run_command` runs under (0.3.0 PR6):
378
+ * `none` (as the user, the 0.2.1 default), `seatbelt` (our own
379
+ * `sandbox-exec` profile: writes only inside `seatbelt_write_roots`), or
380
+ * `agy` (agy's sandbox, `BypassSandbox: false`; always on research_readonly).
381
+ * The strictest of profile / ceiling / request wins.
382
+ */
383
+ sandbox: SandboxMode;
384
+ /** Who set `sandbox`: 'default' when nothing asked for more than none. */
385
+ sandbox_source: 'profile' | 'ceiling' | 'request' | 'default';
386
+ /** Directories the seatbelt profile lets commands write to: `write_roots` plus tmp/dev. */
387
+ seatbelt_write_roots: string[];
388
+ /**
389
+ * Whether allowed run_command calls emit `overwrite.BypassSandbox: true` (I2).
390
+ * Derived: `sandbox !== 'agy'`. Kept as its own field because the gate,
391
+ * broker and every job policy file read it.
392
+ */
393
+ bypass_sandbox: boolean;
394
+ /** Records where bypass_sandbox: false came from, or null when bypassing. Derived from `sandbox_source`. */
395
+ sandbox_forced_by: 'profile' | 'ceiling' | 'request' | null;
396
+ /**
397
+ * Extra read-only roots beyond the workspace (`--add-dir`, M4). `[]` unless
398
+ * `requested.read_roots` is given *and* each entry matches a glob in
399
+ * the human ceiling's own `read_roots` (`policy/ceiling.ts`) — a
400
+ * non-matching entry is dropped and reported in `rejected_read_roots`,
401
+ * never silently widened.
402
+ */
403
+ add_dirs: string[];
404
+ /** Where add_dirs came from: 'ceiling' (default applied), 'request' (narrowed subset), or 'none'. */
405
+ add_dirs_source: 'ceiling' | 'request' | 'none';
406
+ /** Whether commands default to allow when no allow rule matches (0.2.2 PR3). */
407
+ command_policy: 'allowlist' | 'denylist';
408
+ /** Shape version of this file. */
409
+ policy_version: 3;
410
+ on_denial: OnDenial;
411
+ /**
412
+ * Abort the job after this many gate denials regardless of on_denial. Null = never.
413
+ * A middle ground between `abort` (one flaky-tool workaround kills the job) and
414
+ * `continue` (unbounded denial loops).
415
+ */
416
+ max_denials: number | null;
417
+ /** Allow entries the client asked for that the profile ceiling refused. */
418
+ rejected_allow: string[];
419
+ /** Profile deny rules the ceiling's `exceptions` lifted for this job (0.3.0), `{workspace}`-substituted. */
420
+ lifted: string[];
421
+ /** `requested.read_roots` entries that matched no glob in the human ceiling's `read_roots`. */
422
+ rejected_read_roots: string[];
423
+ /** Whether a project ceiling file existed when this policy was resolved (0.3.0). */
424
+ ceiling_present: boolean;
425
+ /** Where that file is (or would be); null when unknown (pre-0.3.0 job files). */
426
+ ceiling_path: string | null;
427
+ /** Pre-flight warnings produced during policy resolution. */
428
+ warnings?: string[];
429
+ }
430
+ /** Exactly the JSON agy writes to the hook's stdin. */
431
+ /**
432
+ * What `agy_start` tells the caller about the policy it just resolved.
433
+ *
434
+ * `agy_start`'s non-dry_run reply used to carry no policy information at all,
435
+ * so a caller whose `permissions.allow` was rejected wholesale could not tell
436
+ * (observed: a request of three build commands collapsed `allow` to `[]`,
437
+ * silently dropping the profile's own defaults too). Both replies carry this
438
+ * now, alongside `warnings` built by the same function.
439
+ */
440
+ export interface PolicySummary {
441
+ profile: Profile;
442
+ /**
443
+ * Size of the effective `allow` list. `0` means nothing is explicitly
444
+ * allowed — including the profile's own defaults, which a rejected
445
+ * `permissions.allow` request silently takes with it. Rejected entries are
446
+ * reported as `source: 'policy_ceiling'` blockers, not here.
447
+ */
448
+ allow_count: number;
449
+ /** Whether allowed commands run unsandboxed for this job. */
450
+ bypass_sandbox: boolean;
451
+ /** Why the sandbox is forced on, or null when bypassing. */
452
+ sandbox_forced_by: 'profile' | 'ceiling' | 'request' | null;
453
+ /** `EffectivePolicy.sandbox` / `sandbox_source`. */
454
+ sandbox: SandboxMode;
455
+ sandbox_source: 'profile' | 'ceiling' | 'request' | 'default';
456
+ /** `EffectivePolicy.add_dirs`, verbatim. */
457
+ add_dirs: string[];
458
+ /** Where add_dirs came from. */
459
+ add_dirs_source: 'ceiling' | 'request' | 'none';
460
+ }
461
+ export interface GatePayload {
462
+ conversationId: string;
463
+ stepIdx?: number;
464
+ modelName?: string;
465
+ toolCall: {
466
+ name: string;
467
+ args?: Record<string, unknown>;
468
+ };
469
+ workspacePaths?: string[];
470
+ transcriptPath?: string;
471
+ artifactDirectoryPath?: string;
472
+ }
473
+ /**
474
+ * Documented values are `allow` / `deny` / `ask` / `force_ask`.
475
+ *
476
+ * `ask` is the pass-through: it delegates to agy's built-in engine, which
477
+ * auto-approves under `proceed-in-sandbox`.
478
+ */
479
+ export type GateVerdict = 'allow' | 'deny' | 'ask' | 'force_ask';
480
+ /**
481
+ * What the gate writes to stdout.
482
+ *
483
+ * ⚠ `{}` IS A DENIAL (measured, §9). The gate must emit a `decision` on every
484
+ * path including parse failure, DB failure and unhandled exception — otherwise it
485
+ * breaks the user's own interactive agy sessions, the one place this package can
486
+ * affect anything outside its own jobs.
487
+ */
488
+ export interface GateDecision {
489
+ decision: GateVerdict;
490
+ /** Passed to the model verbatim; used as an instruction channel, not just a reason. */
491
+ reason?: string;
492
+ /** e.g. `["command(npm test)"]`. Scope of persistence is unmeasured — see §12. */
493
+ permissionOverrides?: string[];
494
+ /** Shallow-merged into the tool args. We use it to pin `Cwd`. */
495
+ overwrite?: Record<string, unknown>;
496
+ }
497
+ /**
498
+ * Which stage of `decide()`'s fixed order produced a verdict.
499
+ * `bound_passthrough`/`network` are gone with `ask` and
500
+ * `default_decision` (I1: a bound job's gate never answers `ask`). `unsupported`
501
+ * and `control` are new — every tool call is now classified, not just
502
+ * `run_command`.
503
+ */
504
+ export type GateDecisionStage = 'unsupported' | 'control' | 'containment' | 'deny_list' | 'profile_allowlist' | 'default' | 'denylist_default';
505
+ /** Machine payload embedded after {@link GATE_DENIAL_MARKER} inside `reason`. */
506
+ export interface GateDenialPayload {
507
+ job_id: string;
508
+ tool: string;
509
+ required_rule: string | null;
510
+ policy: GateDecisionStage;
511
+ on_denial: OnDenial;
512
+ }
513
+ /** Row appended to `jobs/<id>/gate-log.jsonl` — every verdict, allow included. */
514
+ export interface GateLogEntry {
515
+ ts: number;
516
+ job_id: string;
517
+ conversation_id: string;
518
+ step_idx: number | null;
519
+ tool: string;
520
+ command: string | null;
521
+ decision: GateVerdict;
522
+ policy: GateDecisionStage;
523
+ matched_rule: string | null;
524
+ reason: string | null;
525
+ abort_reason?: 'on_denial' | 'max_denials';
526
+ }
527
+ /** Verbatim `agy_start` input, persisted to `jobs/<id>/request.json`. */
528
+ export interface JobRequest {
529
+ prompt: string;
530
+ cwd?: string;
531
+ profile?: Profile;
532
+ model?: string;
533
+ effort?: string;
534
+ mode?: string;
535
+ session_id?: string;
536
+ session_mode?: SessionMode;
537
+ permissions?: RequestedPermissions;
538
+ on_denial?: OnDenial;
539
+ /**
540
+ * Abort the job after this many gate denials regardless of on_denial. Null = never.
541
+ * A middle ground between `abort` (one flaky-tool workaround kills the job) and
542
+ * `continue` (unbounded denial loops).
543
+ */
544
+ max_denials?: number;
545
+ timeout_ms?: number;
546
+ /**
547
+ * `session_mode: 'session'` only. Closes stdin (ending the process at EOF,
548
+ * §6) after this many idle ms following a *completed* turn with no new
549
+ * `agy_send` in between. Ignored for `oneshot`, which already closes stdin
550
+ * after its one turn.
551
+ *
552
+ * Deliberately separate from `timeout_ms`/`deadline_at`: `agy_send` never
553
+ * extends `deadline_at` (see the comment on `EffectiveConfig.deadline_at`
554
+ * below), so this is the mechanism that lets a multi-turn session end
555
+ * promptly instead of idling out the full hard timeout (a session-mode job
556
+ * that finished its turn would otherwise sit until the hard deadline elapsed).
557
+ */
558
+ idle_timeout_ms?: number;
559
+ /** Workspace-relative paths that must exist when the job finishes. */
560
+ expected_artifacts?: string[];
561
+ /** Path or inline JSON schema for `--json-schema`. */
562
+ json_schema?: string;
563
+ requested_by?: string;
564
+ parent_task_id?: string;
565
+ dry_run?: boolean;
566
+ /** `dry_run` only: shell commands to judge against the effective policy up front (0.2.2 PR5). */
567
+ expected_commands?: string[];
568
+ /**
569
+ * A command the *runner* runs once, after agy exits normally, against the
570
+ * final workspace state (PR6, see docs/permissions.md). Outside
571
+ * the model's own decisions — it cannot skip, reorder or narrow it — which is
572
+ * the whole reason it exists, not sandboxing: it runs as the user, with no
573
+ * sandbox, at the same trust level as the parent agent running the command
574
+ * itself. A failing verify is a job *failure* (`outcome: 'failed'`), never a
575
+ * `Blocker` — that vocabulary means "who refused", and nothing refused here.
576
+ */
577
+ verify_command?: string;
578
+ /** Default 600000ms, clamped to `limits.max_timeout_ms`. Ignored without `verify_command`. */
579
+ verify_timeout_ms?: number;
580
+ }
581
+ /**
582
+ * Everything resolved before spawning, persisted to
583
+ * `jobs/<id>/effective-config.json`. `agy_start(dry_run: true)` returns this
584
+ * without spawning so a caller can settle configuration without burning quota.
585
+ */
586
+ export interface EffectiveConfig {
587
+ job_id: string;
588
+ session_id: string;
589
+ /** Set only when resuming an existing conversation. */
590
+ conversation_id: string | null;
591
+ /** Canonical, symlink-resolved workspace. Also the `--add-dir` value. */
592
+ cwd: string;
593
+ profile: Profile;
594
+ model: string | null;
595
+ effort: string | null;
596
+ mode: string | null;
597
+ session_mode: SessionMode;
598
+ on_denial: OnDenial;
599
+ write_mode: boolean;
600
+ timeout_ms: number;
601
+ /**
602
+ * Hard ceiling, set once from `timeout_ms` at `agy_start` and never touched
603
+ * again — `agy_send` does not extend it.
604
+ *
605
+ * A queued turn keeps the *original* deadline. Pushing it out on
606
+ * every `agy_send` would let an actively-fed session hold its `session` and
607
+ * `cwd_write` locks indefinitely as long as something kept
608
+ * calling `agy_send` faster than it decayed — and because `reconcile` only
609
+ * runs from a tool entry point, nothing else would ever notice and
610
+ * cut it off. A fixed ceiling is the backstop that guarantees a `session`
611
+ * job eventually reconciles even if a client behaves badly.
612
+ *
613
+ * The intended way to run longer than one `deadline_at` window is not to
614
+ * stretch it, but to let the job finish (or idle-close, see
615
+ * `idle_timeout_ms`) and resume the same conversation with a fresh
616
+ * `agy_start(session_id=...)` — conversation resume is lossless,
617
+ * so nothing is lost by not stretching this field.
618
+ */
619
+ deadline_at: number;
620
+ /**
621
+ * Resolved `idle_timeout_ms`, or `null` when `session_mode !== 'session'`
622
+ * (a oneshot job has no idle window — its one turn's `result` is followed
623
+ * immediately by stdin close). See `JobRequest.idle_timeout_ms`.
624
+ */
625
+ idle_timeout_ms: number | null;
626
+ expected_artifacts: string[];
627
+ json_schema_path: string | null;
628
+ policy: EffectivePolicy;
629
+ /**
630
+ * Every `--add-dir` value actually passed to `buildAgyArgv` — `[cwd,
631
+ * ...policy.add_dirs]`. Kept
632
+ * distinct from `policy.add_dirs` (which is only the *extra* roots
633
+ * beyond the workspace) so this field is a direct, typed record of what the
634
+ * argv actually contains, without a reader having to reconstruct it from
635
+ * `cwd` + `policy`.
636
+ */
637
+ add_dirs: string[];
638
+ /**
639
+ * Resolved `verify_command`/`verify_timeout_ms`, or `null` when the job asked
640
+ * for no verify command at all. `command` is validated at `agy_start` the
641
+ * same way any other `command(...)` rule subject is — see `handleStart`'s
642
+ * `firstMatchForDenial` check against the resolved `policy.deny` — but is
643
+ * otherwise opaque here: the runner passes it to `sh -c` verbatim.
644
+ */
645
+ verify: {
646
+ command: string;
647
+ timeout_ms: number;
648
+ } | null;
649
+ /** Exact argv handed to `spawn`. No shell string exists anywhere. */
650
+ argv: string[];
651
+ /** Absolute path of the executable being spawned. */
652
+ agy_bin: string;
653
+ /** Allowlisted environment passed to the child. */
654
+ env: Record<string, string>;
655
+ created_at: number;
656
+ }
657
+ /** `jobs/<id>/state.json` — written atomically by the runner. */
658
+ export interface JobStateFile {
659
+ job_id: string;
660
+ lifecycle: Lifecycle;
661
+ pid: number | null;
662
+ pgid: number | null;
663
+ proc_start_time: string | null;
664
+ started_at: number | null;
665
+ finished_at: number | null;
666
+ updated_at: number;
667
+ /**
668
+ * True when the runner's own deadline watchdog killed the process group.
669
+ *
670
+ * Without it the fact is lost: the runner kills agy, writes the resulting
671
+ * `exit_code` (1, or a signal code), and `reconcile` — which only sees the
672
+ * exit code — classifies a timeout as an ordinary `failed`. Measured live:
673
+ * `timeout_ms: 25000` produced `outcome: "failed", exit_code: 1`.
674
+ *
675
+ * Optional because a `state.json` written by an older build will not have it.
676
+ */
677
+ timed_out?: boolean;
678
+ /**
679
+ * True when the runner's idle watchdog closed stdin because
680
+ * `idle_timeout_ms` elapsed with no `agy_send` after the last completed
681
+ * turn. Distinct from `timed_out`: this is a clean
682
+ * EOF close, not a `killpg` — the exit code it produces goes through the
683
+ * ordinary `finalizeJob` path exactly as an explicit `agy_send(close:true)`
684
+ * would, so it never forces `outcome`. Diagnostic only, so a caller can tell
685
+ * "the session idled itself shut" apart from "the client closed it" or "the
686
+ * process just exited on its own" after reading the exit code.
687
+ *
688
+ * Optional for the same reason as `timed_out`: an older `state.json` won't
689
+ * have it, and jobs that never had an idle watchdog (oneshot, or a
690
+ * `session` job the client closed itself) never set it either.
691
+ */
692
+ idle_closed?: boolean;
693
+ /**
694
+ * I4: whether the runner's own gate
695
+ * watchdog (`src/runner/gate-watchdog.ts`) saw `jobs/<id>/gate-log.jsonl`
696
+ * receive a line by the time the first `step_type: 'tool'` step reached
697
+ * DONE/ERROR (plus a short grace period), or within a long backstop cap if
698
+ * that first step never finished.
699
+ *
700
+ * - `true` — the gate confirmed itself; nothing else changes.
701
+ * - `false` — no gate-log line appeared even though a tool call completed
702
+ * (a PreToolUse hook gates execution, so a loaded gate must have logged
703
+ * before then), so the watchdog killed the process group:
704
+ * `hooks.json` never actually loaded and every
705
+ * tool call up to that point ran completely unguarded. `reconcile` forces
706
+ * `outcome: 'process_error'` for this, never `blocked` — a permission
707
+ * verdict implies the gate ran at all, which is exactly what did not
708
+ * happen here.
709
+ * - `null` (or absent, for a `state.json` written before this field
710
+ * existed) — the job ended before any tool step ever ran, so there was
711
+ * nothing for the gate to confirm; this never forces an outcome.
712
+ */
713
+ gate_confirmed?: boolean | null;
714
+ /**
715
+ * True once the runner has written `jobs/<id>/verify.json` (PR6 §6.2).
716
+ * `false` or absent otherwise — including when `config.verify` is null (no
717
+ * verify command was requested) or the deadline killed agy first
718
+ * (`timedOut === true` skips verify entirely; `verify_done` stays falsy and
719
+ * `verification.verify` reads `null`, exactly like "no verify configured").
720
+ */
721
+ verify_done?: boolean;
722
+ /**
723
+ * Which process the runner is currently waiting on (0.2.2 PR1).
724
+ *
725
+ * - `'agy'` — the agy child is (or was just) running.
726
+ * - `'verifying'` — agy has exited and the runner is running `verify_command`.
727
+ * agy's pid is gone in this phase and `exit_code` is not yet written, so a
728
+ * reconciler that reads only those two facts sees exactly what a lost
729
+ * runner looks like. Measured 2026-09-08 (audit `08`): every job that ran a
730
+ * multi-second verify (gradle build, `npm test`) came back `process_error`
731
+ * with a complete result landing a few seconds later.
732
+ * - `'done'` — the final write; `exit_code` follows immediately.
733
+ *
734
+ * Optional because a `state.json` written by an older build will not have it.
735
+ */
736
+ phase?: 'agy' | 'verifying' | 'done';
737
+ /**
738
+ * The runner's own pid, as opposed to `pid` (agy's). Lets `reconcile` tell
739
+ * "agy exited and the runner is still working" (`verifying`, or the short
740
+ * writeback window) apart from "the runner itself is gone". Optional for the
741
+ * same reason as `phase`.
742
+ */
743
+ runner_pid?: number;
744
+ }
745
+ /**
746
+ * A queued turn. Byte-identical to agy's stream-json input schema (§5) so the
747
+ * runner can relay the line to stdin without re-encoding it.
748
+ */
749
+ export type InboxUserLine = AgyStreamUserInput;
750
+ /**
751
+ * Control line the runner consumes and never relays. Uses a key agy would ignore
752
+ * anyway, but the runner filters it out before stdin so that never matters.
753
+ */
754
+ export interface InboxControlLine {
755
+ agy_worker_control: 'close';
756
+ ts: number;
757
+ }
758
+ export type InboxLine = InboxUserLine | InboxControlLine;
759
+ export interface ArtifactCheck {
760
+ path: string;
761
+ absolute: string;
762
+ exists: boolean;
763
+ size: number | null;
764
+ }
765
+ /**
766
+ * Written verbatim to `jobs/<id>/verify.json` by the runner (PR6 §6.2), once,
767
+ * right after `verify_command` exits or its own `verify_timeout_ms` fires.
768
+ * Never written at all when no `verify_command` was requested, or when the
769
+ * job's own deadline killed agy first (`timedOut === true` skips verify).
770
+ */
771
+ export interface VerifyRecord {
772
+ command: string;
773
+ exit_code: number | null;
774
+ /** Node's own signal name (e.g. `'SIGTERM'`), or null on an ordinary exit. */
775
+ signal: string | null;
776
+ started_at: number;
777
+ duration_ms: number;
778
+ /** True when `verify_timeout_ms` fired and the runner killed the process group. */
779
+ timed_out: boolean;
780
+ }
781
+ /**
782
+ * `Verification.verify` — {@link VerifyRecord} plus a display-sized excerpt of
783
+ * its own log, read fresh from disk by `verifyJob` rather than carried through
784
+ * the runner. `output_tail` is the last 2 KiB of `jobs/<id>/verify.log`, or
785
+ * `''` when the file is empty or missing.
786
+ */
787
+ export interface VerifyResult extends VerifyRecord {
788
+ output_tail: string;
789
+ }
790
+ /**
791
+ * Where a refusal came from. The one axis that decides what a caller should do
792
+ * next, so every judgement surface (outcome, counts, warnings, `agy_start`'s own
793
+ * reply) is derived from this and nothing else. The authoritative
794
+ * source → (`actionable`, `remedy`, `blocks_outcome`) table lives in
795
+ * `src/broker/blockers.ts`; do not re-derive it anywhere.
796
+ */
797
+ export type BlockerSource =
798
+ /** A requested `permissions.allow` entry the profile ceiling refused. Pre-flight, from `resolvePolicy`. */
799
+ 'policy_ceiling'
800
+ /** Our own PreToolUse gate refused the call. The only refusal we can confirm. */
801
+ | 'gate'
802
+ /** agy's own permission engine refused, outside our policy entirely (measured wording). */
803
+ | 'agy_engine'
804
+ /** agy's sandbox blocked it silently — a Class 2 signature match. */
805
+ | 'sandbox'
806
+ /** A broker-side check failed: a missing `expected_artifacts` entry. */
807
+ | 'broker'
808
+ /**
809
+ * An ordinary failing tool call carrying no refusal signature at all.
810
+ *
811
+ * Not one of the five refusal sources — it exists because a non-gate
812
+ * `state: 'ERROR'` step is indistinguishable from a real denial by shape
813
+ * (finding 17), and dropping those events would lose their message entirely.
814
+ * Never actionable through permissions, never `blocks_outcome`.
815
+ */
816
+ | 'tool_error';
817
+ /**
818
+ * One thing that stood between the job and `verified_success` — or, for
819
+ * `policy_ceiling`, between the request and the job it asked for.
820
+ *
821
+ * This is deliberately the same vocabulary as an error envelope
822
+ * (`detail` + `remedy`, `contract/errors.ts`): a caller reads `actionable` to
823
+ * decide whether retrying differently can possibly help, and `remedy` for what
824
+ * to change. When nothing can help, `remedy` is null and `message` says why.
825
+ */
826
+ export interface Blocker {
827
+ source: BlockerSource;
828
+ /** Whether changing the next `agy_start` can lift this. */
829
+ actionable: boolean;
830
+ /** What to change. Null when nothing will help — the reason is in `message`. */
831
+ remedy: string | null;
832
+ /**
833
+ * Whether this forces `outcome: 'blocked'`. True only for refusals we
834
+ * confirmed ourselves (`gate`, `sandbox`, `broker`); an `agy_engine` or
835
+ * `tool_error` entry cannot be told apart from an ordinary command failure,
836
+ * and reporting those as `blocked` would call every failing test a block.
837
+ */
838
+ blocks_outcome: boolean;
839
+ tool: string | null;
840
+ command: string | null;
841
+ /** Human-readable, and carrying the measured message verbatim where there is one. */
842
+ message: string;
843
+ /**
844
+ * The full original record this was derived from — a `DenialClass1`,
845
+ * `DenialClass2`, `ArtifactCheck`, or the rejected rule string. Nothing the
846
+ * pre-0.1.1 `permission_denials` / `environment_blocks` lists carried
847
+ * (`required_rule`, `signature`, `policy`, `step_idx`, …) is dropped.
848
+ */
849
+ detail?: Record<string, unknown>;
850
+ }
851
+ /**
852
+ * Written to `jobs/<id>/verification.json`. Facts only.
853
+ *
854
+ * `blockers` replaced the separate `permission_denials` / `environment_blocks`
855
+ * lists in `BROKER_RESULT_VERSION` 2: the caller's real question is "who
856
+ * refused, and can I fix it", which the split lists made them reassemble by
857
+ * hand — and got wrong, since a non-gate tool error sat in a list named
858
+ * `permission_denials` while never counting as a denial anywhere else.
859
+ */
860
+ export interface Verification {
861
+ blockers: Blocker[];
862
+ expected_artifacts: ArtifactCheck[];
863
+ /** `git status --porcelain` inside the workspace, when it is a repo. */
864
+ changed_files: string[];
865
+ /** Rendered `blockers`, plus non-blocker observations (an idle-closed session). */
866
+ warnings: string[];
867
+ contract_status: ContractStatus;
868
+ checked_at: number;
869
+ /**
870
+ * `null` when no `verify_command` was requested, or the deadline killed agy
871
+ * before it could run. Never a `Blocker` — a failing verify is a job
872
+ * *failure*, not a refusal, and `hasOutcomeBlocker` must never see it
873
+ * (`decideOutcome` reads this field directly instead — see `outcome.ts`).
874
+ */
875
+ verify: VerifyResult | null;
876
+ }
877
+ /** agy's own claims. Kept quarantined from the broker's findings on purpose. */
878
+ export interface AgentReport {
879
+ status: AgentStatus;
880
+ response: string | null;
881
+ error: string | null;
882
+ num_turns: number | null;
883
+ usage: AgyUsage | null;
884
+ conversation_id: string | null;
885
+ }
886
+ /** Deterministically derived from events, exit status and filesystem checks. */
887
+ export interface BrokerSummary {
888
+ /** One sentence. This is what a caller reads first. */
889
+ headline: string;
890
+ outcome: Outcome;
891
+ exit_code: number | null;
892
+ duration_ms: number | null;
893
+ counts: {
894
+ events: number;
895
+ steps: number;
896
+ tool_calls: number;
897
+ tool_errors: number;
898
+ turns: number;
899
+ malformed_lines: number;
900
+ };
901
+ /** Last normalized lines, already size-capped. */
902
+ log_tail: string[];
903
+ }
904
+ /**
905
+ * `jobs/<id>/broker-result.json` — the single source of truth.
906
+ * `agy_wait` projects a subset of this; `agy_result` pages all of it. Nothing
907
+ * else recomputes `outcome`.
908
+ */
909
+ export interface BrokerResult {
910
+ schema_version: number;
911
+ job_id: string;
912
+ session_id: string | null;
913
+ conversation_id: string | null;
914
+ lifecycle: Lifecycle;
915
+ cwd: string;
916
+ profile: Profile;
917
+ session_mode: SessionMode;
918
+ created_at: number;
919
+ started_at: number | null;
920
+ finished_at: number | null;
921
+ /** Unverified self-report. Never read to decide `outcome`. */
922
+ agent_report: AgentReport;
923
+ /** Verified facts. */
924
+ broker_summary: BrokerSummary;
925
+ verification: Verification;
926
+ agent_status: AgentStatus;
927
+ contract_status: ContractStatus;
928
+ /** Raw structured output when `--json-schema` was used; preserved even if invalid. */
929
+ structured_output: unknown;
930
+ finalized_at: number;
931
+ }
932
+ /**
933
+ * What `agy_wait` returns on completion — the judgement packet summary.
934
+ * Deliberately excludes lists and raw text; those live in `agy_result`.
935
+ */
936
+ export interface JudgementPacket {
937
+ job_id: string;
938
+ lifecycle: Lifecycle;
939
+ outcome: Outcome | null;
940
+ headline: string;
941
+ exit_code: number | null;
942
+ duration_ms: number | null;
943
+ agent_status: AgentStatus | null;
944
+ contract_status: ContractStatus | null;
945
+ counts: {
946
+ /**
947
+ * Size of `verification.blockers`. The packet stays a verdict, not a
948
+ * report: the per-item detail (source, remedy, verbatim message) is one
949
+ * `agy_result({ section: "verification" })` away.
950
+ *
951
+ * Invariant, for a job that actually ran to a conclusion (i.e. not
952
+ * `canceled` / `timed_out` / `failed`, which outrank verification in
953
+ * `decideOutcome`'s precedence): `outcome === 'blocked'` ⟺ some blocker
954
+ * has `blocks_outcome: true`. Both sides are computed from the same list.
955
+ */
956
+ blockers: number;
957
+ /** How many of those a different `agy_start` could lift (`actionable: true`). */
958
+ actionable: number;
959
+ /** Every `step_type: 'tool'`, `state: 'ERROR'` step in the raw stream. */
960
+ tool_errors: number;
961
+ /** `result` events, i.e. completed turns. */
962
+ turns: number;
963
+ };
964
+ warnings: string[];
965
+ log_tail: string[];
966
+ /** Byte offset to resume `agy_logs` / `agy_wait` from. */
967
+ cursor: number;
968
+ }
969
+ /**
970
+ * What the connected MCP client told us at `initialize`.
971
+ *
972
+ * Reported so a caller can see, rather than guess, which optional protocol
973
+ * features are actually negotiated on this connection — `capabilities.tasks`
974
+ * in particular decides whether a long call can be handed to the client as a
975
+ * background task instead of blocking its turn. Null before `initialize`
976
+ * completes, or when the server is driven without a client (tests).
977
+ */
978
+ export interface ClientSnapshot {
979
+ name: string | null;
980
+ version: string | null;
981
+ /** Verbatim `ClientCapabilities` — an empty object means "declared nothing". */
982
+ capabilities: Record<string, unknown> | null;
983
+ }
984
+ /**
985
+ * `agy_capabilities.ceiling` — the human-owned permission ceiling as loaded
986
+ * from `<project state dir>/policy.json` (`policy/ceiling.ts`, see
987
+ * `docs/permissions.md`). Rule strings are reported exactly as written in the
988
+ * file: capabilities has no per-job workspace, so a `{workspace}` placeholder
989
+ * inside `allow`/`deny`/`exceptions` is left literal rather than
990
+ * substituted — a caller sees the resolved, job-real form in `agy_start`'s own
991
+ * `policy_summary` instead.
992
+ */
993
+ export interface CeilingSummary {
994
+ /** Absolute path of the ceiling file, whether or not it exists. */
995
+ path: string;
996
+ present: boolean;
997
+ /** Schema version the file was written in (2 current; 1 is read and converted). Null when absent. */
998
+ version: 1 | 2 | null;
999
+ allow: string[];
1000
+ deny: string[];
1001
+ /** Profile deny rules this project lifts (0.3.0). Exact-string matches only; never touches HARD_DENY. */
1002
+ exceptions: string[];
1003
+ /** OS boundary for general_worker commands in this project: none (default) | seatbelt | agy. */
1004
+ sandbox: SandboxMode;
1005
+ /** `~`-expanded, canonicalized glob patterns, as loaded. Read/exec roots outside the workspace. */
1006
+ read_roots: string[];
1007
+ /** Extra directories jobs may write to (containment and, under seatbelt, the kernel). */
1008
+ write_roots: string[];
1009
+ command_policy: 'allowlist' | 'denylist';
1010
+ /** Non-fatal notes about the file (e.g. a v1 file whose keys should be renamed). */
1011
+ warnings: string[];
1012
+ }
1013
+ /** Risk class `agy_ceiling` attaches to every rule in a draft (`policy/ceiling-review.ts`). */
1014
+ export type CeilingRuleRisk = 'read_utility' | 'build' | 'vcs_local' | 'vcs_remote' | 'network' | 'install' | 'destructive' | 'privilege' | 'filesystem' | 'other';
1015
+ export interface CeilingRuleReview {
1016
+ key: 'allow' | 'deny' | 'exceptions';
1017
+ rule: string;
1018
+ risk: CeilingRuleRisk;
1019
+ /** Advisory notes: redundant, lifts nothing, needs explicit approval. */
1020
+ notes: string[];
1021
+ }
1022
+ /** `agy_ceiling({ draft })` — would the file load, and what is the human being asked to approve. */
1023
+ export interface CeilingDraftReview {
1024
+ /** True when `loadCeiling` would accept this object. */
1025
+ ok: boolean;
1026
+ errors: string[];
1027
+ warnings: string[];
1028
+ rules: CeilingRuleReview[];
1029
+ }
1030
+ /** `agy_ceiling` reply. Read-only: `writes_nothing` is literal. */
1031
+ export interface CeilingReply {
1032
+ path: string;
1033
+ present: boolean;
1034
+ ceiling: CeilingSummary;
1035
+ /** general_worker resolved against the draft (when it loads) or the current file. */
1036
+ effective: {
1037
+ profile: 'general_worker';
1038
+ allow: string[];
1039
+ deny: string[];
1040
+ hard_deny: string[];
1041
+ lifted: string[];
1042
+ read_roots: string[];
1043
+ command_policy: 'allowlist' | 'denylist';
1044
+ warnings: string[];
1045
+ };
1046
+ review: CeilingDraftReview | null;
1047
+ preflight: Array<{
1048
+ command: string;
1049
+ decision: 'allow' | 'deny';
1050
+ stage: string;
1051
+ required_rule: string | null;
1052
+ }> | null;
1053
+ /** Denial history and command frequencies across this project's jobs; null when a draft was given. */
1054
+ history: import('../trace/digest.js').ProjectDigest | null;
1055
+ writes_nothing: true;
1056
+ }
1057
+ /**
1058
+ * One entry of `Capabilities.models`: the `--effort` values agy accepts for
1059
+ * this model (measured, M8). Omitting `agy_start.effort` is
1060
+ * always allowed; an empty list means it must be omitted (`claude-*`), and a
1061
+ * name with the effort baked in (`gemini-*-high`) lists exactly that one
1062
+ * value — any other is rejected by agy before the turn starts.
1063
+ */
1064
+ export interface ModelCapability {
1065
+ name: string;
1066
+ efforts: readonly string[];
1067
+ }
1068
+ export interface Capabilities {
1069
+ server_version: string;
1070
+ schema_version: number;
1071
+ project_root: string;
1072
+ project_key: string;
1073
+ profiles: Array<{
1074
+ name: Profile;
1075
+ description: string;
1076
+ write: boolean;
1077
+ bypass_sandbox: boolean;
1078
+ }>;
1079
+ /** The human-owned ceiling this project's `agy_start` calls resolve against. */
1080
+ ceiling: CeilingSummary;
1081
+ models: ModelCapability[];
1082
+ efforts: string[];
1083
+ modes: string[];
1084
+ session_modes: SessionMode[];
1085
+ on_denial: OnDenial[];
1086
+ limits: {
1087
+ max_running_jobs: number;
1088
+ max_timeout_ms: number;
1089
+ default_timeout_ms: number;
1090
+ /**
1091
+ * `session_mode: 'session'` only. No live
1092
+ * measurement dictates this value — there is no recorded turn-cadence
1093
+ * data for how quickly a real caller re-sends after a turn finishes.
1094
+ * Chosen to comfortably outlast one round trip of "read the last turn,
1095
+ * decide the next prompt" without idling out a hard `timeout_ms`
1096
+ * (typically minutes) just to wait on a client that already went quiet.
1097
+ * Revisit if live use shows a different cadence.
1098
+ */
1099
+ default_idle_timeout_ms: number;
1100
+ /** Ceiling `idle_timeout_ms` is clamped to, mirroring `max_timeout_ms`. */
1101
+ max_idle_timeout_ms: number;
1102
+ /** `verify_timeout_ms` default (10 minutes) when `verify_command` is set but no timeout is given. Clamped to `max_timeout_ms`, same as `timeout_ms`. */
1103
+ default_verify_timeout_ms: number;
1104
+ max_response_bytes: number;
1105
+ max_log_tail_lines: number;
1106
+ };
1107
+ agy_bin: string;
1108
+ /** False when the configured binary is missing; `agy_start` will fail. */
1109
+ agy_bin_present: boolean;
1110
+ /** The connected client's own `initialize` declaration. See {@link ClientSnapshot}. */
1111
+ client: ClientSnapshot | null;
1112
+ }
1113
+ //# sourceMappingURL=types.d.ts.map