@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (142) hide show
  1. package/README.md +697 -260
  2. package/bin/connect-linux.sh +200 -25
  3. package/bin/kit.js +179 -157
  4. package/bootstrap.json +17 -0
  5. package/package.json +7 -2
  6. package/src/adapter-mcp.js +76 -12
  7. package/src/agents.js +63 -8
  8. package/src/binding.js +52 -0
  9. package/src/blocks.js +2 -26
  10. package/src/bridge.js +28 -14
  11. package/src/bytes.js +19 -8
  12. package/src/configuration.js +13 -209
  13. package/src/connector-login.js +3 -84
  14. package/src/contract.js +36 -11
  15. package/src/delivery.js +4 -7
  16. package/src/directory.js +6 -26
  17. package/src/draft.js +4 -82
  18. package/src/git-main.js +138 -0
  19. package/src/git.js +62 -0
  20. package/src/google/calendar.js +39 -45
  21. package/src/google/drive.js +61 -186
  22. package/src/google/entries.js +346 -0
  23. package/src/google/gateway.js +3 -6
  24. package/src/google/gmail.js +33 -23
  25. package/src/google/oauth.js +106 -66
  26. package/src/google/source.js +21 -23
  27. package/src/host.js +6 -2
  28. package/src/house-main.js +65 -8
  29. package/src/house.js +446 -127
  30. package/src/how-we-work.md +25 -0
  31. package/src/inbound.js +2 -18
  32. package/src/kit-work-stream.js +118 -0
  33. package/src/launcher.js +46 -10
  34. package/src/local-files.js +3 -11
  35. package/src/login.js +23 -11
  36. package/src/mcp.js +18 -0
  37. package/src/mount.js +13 -21
  38. package/src/original.js +4 -4
  39. package/src/pacing.js +41 -0
  40. package/src/plan.js +37 -45
  41. package/src/repository.js +3 -61
  42. package/src/session-bin/git +2 -0
  43. package/src/setup.js +14 -15
  44. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  45. package/src/skill-set/ask-matt/SKILL.md +91 -0
  46. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  47. package/src/skill-set/code-review/LICENSE +21 -0
  48. package/src/skill-set/code-review/SKILL.md +89 -0
  49. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  50. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  51. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/src/skill-set/codebase-design/LICENSE +21 -0
  53. package/src/skill-set/codebase-design/SKILL.md +116 -0
  54. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  55. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  56. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  57. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  58. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  59. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  60. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  61. package/src/skill-set/domain-modeling/LICENSE +21 -0
  62. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  63. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  64. package/src/skill-set/grill-me/LICENSE +21 -0
  65. package/src/skill-set/grill-me/SKILL.md +8 -0
  66. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  67. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  68. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  69. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  70. package/src/skill-set/grilling/LICENSE +21 -0
  71. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  72. package/src/skill-set/handoff/LICENSE +21 -0
  73. package/src/skill-set/handoff/SKILL.md +17 -0
  74. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  75. package/src/skill-set/implement/LICENSE +21 -0
  76. package/src/skill-set/implement/SKILL.md +16 -0
  77. package/src/skill-set/implement/agents/openai.yaml +4 -0
  78. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  79. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  80. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  81. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  82. package/src/skill-set/prototype/LICENSE +21 -0
  83. package/src/skill-set/prototype/LOGIC.md +67 -0
  84. package/src/skill-set/prototype/SKILL.md +28 -0
  85. package/src/skill-set/prototype/UI.md +112 -0
  86. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  87. package/src/skill-set/research/LICENSE +21 -0
  88. package/src/skill-set/research/SKILL.md +14 -0
  89. package/src/skill-set/research/agents/openai.yaml +3 -0
  90. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  91. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  92. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  93. package/src/skill-set/tdd/LICENSE +21 -0
  94. package/src/skill-set/tdd/SKILL.md +40 -0
  95. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  96. package/src/skill-set/tdd/mocking.md +59 -0
  97. package/src/skill-set/tdd/tests.md +77 -0
  98. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  99. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  100. package/src/skill-set/teach/LICENSE +21 -0
  101. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  102. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  103. package/src/skill-set/teach/SKILL.md +141 -0
  104. package/src/skill-set/teach/agents/openai.yaml +4 -0
  105. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  106. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  107. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  108. package/src/skill-set/to-spec/LICENSE +21 -0
  109. package/src/skill-set/to-spec/SKILL.md +76 -0
  110. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  111. package/src/skill-set/to-tickets/LICENSE +21 -0
  112. package/src/skill-set/to-tickets/SKILL.md +106 -0
  113. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  114. package/src/skill-set/wait-what/LICENSE +21 -0
  115. package/src/skill-set/wait-what/SKILL.md +8 -0
  116. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  117. package/src/skill-set/wayfinder/LICENSE +21 -0
  118. package/src/skill-set/wayfinder/SKILL.md +129 -0
  119. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  120. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  121. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  122. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  123. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  124. package/src/skill-set.js +85 -0
  125. package/src/source.js +93 -54
  126. package/src/state.js +19 -1
  127. package/src/sync-library.js +7 -11
  128. package/src/sync-state.js +16 -3
  129. package/src/sync.js +36 -21
  130. package/src/tasks.js +75 -0
  131. package/src/telegram-view.js +214 -0
  132. package/src/telegram.js +161 -66
  133. package/src/turn-frames.js +69 -0
  134. package/src/worker.js +674 -184
  135. package/src/working-copy.js +1221 -0
  136. package/src/workspace.js +40 -32
  137. package/src/google/declared.js +0 -146
  138. package/src/google/id.js +0 -1
  139. package/src/instance/google.js +0 -1
  140. package/src/starter-set.js +0 -17
  141. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  142. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/worker.js CHANGED
@@ -1,66 +1,91 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
2
4
  import { join } from "node:path";
3
5
  import { setTimeout as delay } from "node:timers/promises";
4
6
  import { fileURLToPath } from "node:url";
5
- import { agentReadiness, installedAgentCommand, updateDeclaredAgents } from "./agents.js";
7
+ import {
8
+ installedAgentCommand,
9
+ readAgentState,
10
+ reconcileAgentKinds,
11
+ } from "./agents.js";
6
12
  import { readBindings, removeBinding, writeBinding } from "./binding.js";
7
13
  import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
8
- import { openBridge } from "./bridge.js";
14
+ import { bridgedFrame, openBridge } from "./bridge.js";
15
+ import { transferBytes } from "./bytes.js";
9
16
  import { assistantBlocks } from "./blocks.js";
10
17
  import { retryConversationFiles } from "./conversation-originals.js";
11
- import { openAssistantDrafts } from "./draft.js";
12
- import { localReply, localRequest, reconcileInbound } from "./inbound.js";
18
+ import { localRequest, reconcileInbound } from "./inbound.js";
13
19
  import { localFiles } from "./local-files.js";
20
+ import { answerText, answerYaml, editRefusal } from "./mcp.js";
14
21
  import { inheritedEnvironment } from "./command.js";
22
+ import { sourceEntries } from "./google/entries.js";
23
+ import { watchKitWork } from "./kit-work-stream.js";
24
+ import { finalPlan } from "./plan.js";
25
+ import { coalescedTask, residentWake } from "./tasks.js";
26
+ import {
27
+ applyWorkingCopies,
28
+ deliverWorkingCopies,
29
+ ownerCaller,
30
+ pushWorkingCopy,
31
+ recordIndexChange,
32
+ uploadedPaths,
33
+ workingCopyPaths,
34
+ } from "./working-copy.js";
15
35
  import { readConfiguration } from "./configuration.js";
16
36
  import { launchBase, readyLaunchDirectory } from "./directory.js";
17
37
  import {
18
38
  BRIDGE_NONCE_ENVIRONMENT,
19
39
  BRIDGE_SOCKET_ENVIRONMENT,
40
+ MCP_REQUEST_BODY_MAX_BYTES,
20
41
  PROVIDER_CONTINUATION_UNAVAILABLE,
21
42
  WORKER_COMMAND_BATCH_MAX,
22
- WORKER_COMMAND_POLL_MS,
23
43
  WORKER_EXIT_GRACE_MS,
24
44
  WORKER_EXIT_POLL_MS,
25
- WORKER_HEARTBEAT_MS,
26
45
  WORKER_INHERITED_ENVIRONMENT,
27
46
  WORKER_SUPERVISION_POLL_MS,
47
+ WORKING_COPY_INDEX_METHOD,
48
+ WORKING_COPY_PUSH_METHOD,
49
+ WORKING_COPY_UPLOAD_COMMAND,
28
50
  } from "./contract.js";
29
- import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
51
+ import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
30
52
  import {
31
53
  acceptAssistantText,
32
54
  acquireCommands,
33
55
  acquireSetup,
34
- reportAgentCapabilities,
35
56
  acquireOneRun,
36
57
  acquireOneResumption,
37
58
  admitInteraction,
38
- admitPlanSnapshot,
39
59
  awaitSecretInput,
40
60
  rejectResumption,
41
61
  callHouseMcp,
42
62
  commitRun,
43
- desiredAgents,
63
+ controlSleep,
44
64
  maintainControl,
45
65
  mutatingCall,
46
66
  observeSessionStart,
47
67
  observeSessionStop,
48
68
  openSession,
69
+ readRunRequest,
49
70
  replaceSessionAuthority,
50
71
  reportContinuation,
51
72
  reportInterruptOutcome,
52
73
  reportInteractionOutcome,
53
74
  toolMutations,
54
75
  reportRunOutcome,
55
- reportSessionHeartbeat,
76
+ releaseControl,
56
77
  reportSessions,
78
+ serveResident,
57
79
  sessionAuthority,
58
- streamAssistantDraft,
80
+ updateDrain,
59
81
  } from "./house.js";
60
82
  import { operationId } from "./operation.js";
61
- import { runSetup } from "./setup.js";
83
+ import { runSetup, signInWantedAgent } from "./setup.js";
62
84
  import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
85
+ import { howWeWorkOwed, noteHowWeWork, reconcileSkillSet } from "./skill-set.js";
86
+ import { writeText } from "./state.js";
63
87
  import { failedReport, terminalReport } from "./terminal.js";
88
+ import { kitStreamWriter, turnFrames } from "./turn-frames.js";
64
89
  import {
65
90
  createSession,
66
91
  destroySession,
@@ -71,47 +96,32 @@ import {
71
96
  } from "./tmux.js";
72
97
 
73
98
  const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
99
+ const RESIDENT_INTERVAL_MS = 30_000;
100
+ const PASS_REACHES = ["commands", "runs"];
74
101
 
75
102
  async function dispatchSeal(context, bound) {
76
103
  try {
77
- const accepted = await acceptAssistantText(context.session, bound.session, bound.sealed);
78
- await resolveSeal(context, bound);
79
- return { accepted, failure: null, unresolved: false, refused: null };
104
+ await acceptAssistantText(context.session, bound.session, bound.sealed, bound.plan);
105
+ bound.stopped.textAccepted = true;
80
106
  } catch (refusal) {
81
107
  if (!(refusal instanceof KitRefusal)) throw refusal;
82
108
  const named = { code: refusal.code, detail: refusal.message };
83
- if (undecided(refusal)) return { accepted: null, failure: null, unresolved: true, refused: named };
84
- await resolveSeal(context, bound);
85
- return { accepted: null, failure: named, unresolved: false, refused: null };
109
+ if (undecided(context, refusal)) return named;
110
+ bound.stopped.failure ??= named;
86
111
  }
87
- }
88
-
89
- async function resolveSeal(context, bound) {
90
112
  bound.sealed = null;
91
113
  await writeBinding(context.directory, bound);
92
- }
93
-
94
- async function sealAssistantText(context, bound, said) {
95
- const blocks = assistantBlocks(said.join("\n\n"));
96
- if (blocks === null) return { accepted: null, failure: null, unresolved: false, refused: null };
97
- bound.sealed = { operation: operationId(), blocks };
98
- await writeBinding(context.directory, bound);
99
- return dispatchSeal(context, bound);
100
- }
101
-
102
- async function recoverAssistantSeal(context, binding) {
103
- if (binding.sealed === null) return { unresolved: false, refused: null };
104
- const sealed = await dispatchSeal(context, binding);
105
- return { unresolved: sealed.unresolved, refused: sealed.refused };
114
+ return null;
106
115
  }
107
116
 
108
117
  function unresolvedRun(bound, refused = null) {
109
118
  return { run: bound.run, session: bound.session, refused };
110
119
  }
111
120
 
112
- function undecided(refusal) {
121
+ function undecided(context, refusal) {
113
122
  return (
114
123
  houseFailed(refusal) ||
124
+ context.session.refused === true ||
115
125
  (refusal instanceof KitRefusal &&
116
126
  ["kit_command_cancelled", "kit_session_token_rejected"].includes(refusal.code))
117
127
  );
@@ -121,7 +131,7 @@ async function pendingCommands(context, kinds = ["stop"]) {
121
131
  try {
122
132
  return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
123
133
  } catch (refusal) {
124
- if (houseFailed(refusal)) return { stops: [], answers: [], interrupts: [] };
134
+ if (houseFailed(refusal) || context.session.refused === true) return null;
125
135
  throw refusal;
126
136
  }
127
137
  }
@@ -142,14 +152,6 @@ async function settleInterrupt(context, interrupt, outcome) {
142
152
  }
143
153
  }
144
154
 
145
- async function reportHeartbeat(context, bound) {
146
- try {
147
- await reportSessionHeartbeat(context.session, bound.session);
148
- } catch (refusal) {
149
- if (!(refusal instanceof KitRefusal)) throw refusal;
150
- }
151
- }
152
-
153
155
  function retryFiles(context) {
154
156
  if (context.retrying !== undefined) return;
155
157
  context.retrying = retryConversationFiles(context)
@@ -172,39 +174,32 @@ async function settleFiles(context) {
172
174
  }
173
175
 
174
176
  async function supervise(context, bound, bridge) {
175
- const terminal = bridge.departed;
176
- let live = true;
177
- terminal.then(() => {
178
- live = false;
179
- });
180
- let commanded = Date.now() + WORKER_COMMAND_POLL_MS;
181
- let pulsed = Date.now() + WORKER_HEARTBEAT_MS;
182
- while (live) {
183
- await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
184
- if (!live) return null;
185
- if (Date.now() >= commanded) {
186
- commanded = Date.now() + WORKER_COMMAND_POLL_MS;
177
+ const departed = new AbortController();
178
+ bridge.departed.then(() => departed.abort());
179
+ const waiting = AbortSignal.any([context.signal, departed.signal]);
180
+ let owed = false;
181
+ for (;;) {
182
+ const commanded = (await context.commands.wait(WORKER_SUPERVISION_POLL_MS, waiting)) || owed;
183
+ if (departed.signal.aborted) return null;
184
+ if (context.signal.aborted) throw new KitRefusal("kit_command_cancelled");
185
+ await context.update?.announce();
186
+ if (commanded) {
187
187
  const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
188
- for (const answer of polled.answers) {
188
+ owed = polled === null && context.session.refused !== true;
189
+ for (const answer of polled?.answers ?? []) {
189
190
  if (answer.session === bound.session) bridge.answered(answer);
190
191
  }
191
- for (const interrupt of polled.interrupts) {
192
+ for (const interrupt of polled?.interrupts ?? []) {
192
193
  if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
193
194
  if (!bridge.interrupt(interrupt)) {
194
195
  await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
195
196
  }
196
197
  }
197
- const stop = polled.stops.find((command) => command.run === bound.run);
198
+ const stop = polled?.stops.find((command) => command.run === bound.run);
198
199
  if (stop !== undefined) return stop;
199
200
  }
200
- if (Date.now() >= pulsed) {
201
- pulsed = Date.now() + WORKER_HEARTBEAT_MS;
202
- retryFiles(context);
203
- await reportHeartbeat(context, bound);
204
- }
205
201
  if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
206
202
  }
207
- return null;
208
203
  }
209
204
 
210
205
  async function grantExitGrace(runtime, handle) {
@@ -221,9 +216,9 @@ async function completeOwnerStop(context, bound, requestedAt) {
221
216
  const destroyed = await destroySession(context.runtime, bound.tmux);
222
217
  let failure = null;
223
218
  try {
224
- await observeSessionStop(context.session, bound.session);
219
+ await observeSessionStop(context.session, bound.session, false, bound.plan);
225
220
  } catch (refusal) {
226
- if (houseFailed(refusal)) return { destroyed, confirmed: false, failure: null };
221
+ if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
227
222
  if (!(refusal instanceof KitRefusal)) throw refusal;
228
223
  failure = { code: refusal.code, detail: refusal.message };
229
224
  }
@@ -234,10 +229,10 @@ async function completeOwnerStop(context, bound, requestedAt) {
234
229
 
235
230
  async function settleReported(context, bound) {
236
231
  try {
237
- await reportRunOutcome(context.session, bound.run, bound.reported);
232
+ await reportRunOutcome(context.session, bound.run, bound.reported, bound.plan);
238
233
  return { settled: true, failure: null };
239
234
  } catch (refusal) {
240
- if (undecided(refusal)) return { settled: false, failure: null };
235
+ if (undecided(context, refusal)) return { settled: false, failure: null };
241
236
  if (!(refusal instanceof KitRefusal)) throw refusal;
242
237
  if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
243
238
  return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
@@ -272,41 +267,134 @@ function endOnEpochReport(ended, act) {
272
267
  };
273
268
  }
274
269
 
270
+ function refusedWith(text) {
271
+ const { code, conflicts } = editRefusal(text);
272
+ const current =
273
+ conflicts?.map(({ path, state, revision }) =>
274
+ state === "absent" ? { path, revision: null, content: null } : { path, revision },
275
+ ) ?? null;
276
+ return { refused: { code, text, current } };
277
+ }
278
+
279
+ function acceptedWith(text) {
280
+ const written = new Map();
281
+ for (const { path, op, revision, from } of answerYaml(text).results) {
282
+ if (from !== undefined) written.set(from, { path: from, revision: null, content: null });
283
+ written.set(path, op === "remove" ? { path, revision: null, content: null } : { path, revision });
284
+ }
285
+ return { refused: null, written: [...written.values()] };
286
+ }
287
+
288
+ function toolFrame(name, args, operation) {
289
+ return bridgedFrame({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, operation);
290
+ }
291
+
292
+ function uploadBody(changes) {
293
+ const body = Buffer.from(JSON.stringify({ changes }), "utf8");
294
+ if (body.length > MCP_REQUEST_BODY_MAX_BYTES) return body;
295
+ return Buffer.concat([body, Buffer.alloc(MCP_REQUEST_BODY_MAX_BYTES + 1 - body.length, " ")]);
296
+ }
297
+
298
+ async function sessionPush(context, bound, authority, frame, signal) {
299
+ const call = async (sent, called) =>
300
+ callHouseMcp(context.session, await sessionAuthority(context.session, bound.session, authority, sent), called, sent);
301
+ const refused = (answer) =>
302
+ answer.error !== undefined
303
+ ? refusedWith(answer.error.message ?? "kit_house_refused")
304
+ : answer.result?.isError === true
305
+ ? refusedWith(answerText(answer.result))
306
+ : null;
307
+ const stage = async (body, sent) => {
308
+ const bytes = uploadBody(body.changes);
309
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
310
+ const prepare = { paths: uploadedPaths(body.changes), bytes: bytes.length, sha256 };
311
+ const prepared = await call(
312
+ sent,
313
+ toolFrame("run_command", { command: WORKING_COPY_UPLOAD_COMMAND, arguments: prepare }, body.operation_id),
314
+ );
315
+ const refusal = refused(prepared);
316
+ if (refusal !== null) return refusal;
317
+ const transfer = answerYaml(answerText(prepared.result))?.transfer;
318
+ const staged = (
319
+ await transferBytes(
320
+ { url: transfer?.url, method: "POST", operation: transfer?.operation, body: bytes, framed: true, signal: sent },
321
+ context.session.dependencies,
322
+ )
323
+ ).transfer;
324
+ if (typeof staged?.url !== "string" || typeof staged.operation !== "string") {
325
+ throw new KitRefusal("kit_house_answer_unreadable", WORKING_COPY_UPLOAD_COMMAND);
326
+ }
327
+ return { upload: { url: staged.url, operation: staged.operation, bytes: bytes.length, sha256 } };
328
+ };
329
+ const edit = async (body, sent, retain) => {
330
+ let { upload } = body;
331
+ const inline = toolFrame("edit", { changes: body.changes }, body.operation_id);
332
+ if (upload === undefined && Buffer.byteLength(JSON.stringify(inline)) > MCP_REQUEST_BODY_MAX_BYTES) {
333
+ const staged = await stage(body, sent);
334
+ if (staged.upload === undefined) return staged;
335
+ ({ upload } = staged);
336
+ await retain(upload);
337
+ }
338
+ const answer = await call(
339
+ sent,
340
+ upload === undefined ? inline : toolFrame("edit", { paths: uploadedPaths(body.changes), upload }, body.operation_id),
341
+ );
342
+ return refused(answer) ?? acceptedWith(answerText(answer.result));
343
+ };
344
+ let outcome;
345
+ try {
346
+ outcome = await pushWorkingCopy(context, frame.params, { binding: { session: bound.session }, submit: edit }, signal);
347
+ } catch (refusal) {
348
+ if (!(refusal instanceof KitRefusal)) throw refusal;
349
+ outcome = { refused: true, text: refusal.message };
350
+ }
351
+ return {
352
+ jsonrpc: "2.0",
353
+ id: frame.id,
354
+ result: { content: [{ type: "text", text: outcome.text }], ...(outcome.refused ? { isError: true } : {}) },
355
+ };
356
+ }
357
+
275
358
  async function openRunBridge(context, bound, authority, start) {
276
359
  const ended = new AbortController();
277
360
  const handed = (act) => endOnEpochReport(ended, act);
278
- const drafts = openAssistantDrafts({
279
- send: handed((blocks) => streamAssistantDraft(context.session, bound.session, blocks)),
280
- });
361
+ const frames = turnFrames(context.stream);
281
362
  const bridge = await openBridge({
282
363
  socket: bound.socket,
283
364
  nonce: bound.nonce,
284
365
  signal: AbortSignal.any([context.signal, ended.signal]),
285
366
  mutates: mutatingCall(await toolMutations(context.session, context.signal)),
286
367
  alive: () => sessionPresent(context.runtime, bound.tmux),
287
- draft: (text) => drafts.update(text),
368
+ draft: (view) => frames.draft(view),
369
+ plan: (snapshot) => frames.plan(snapshot),
288
370
  start,
289
371
  question: handed((batch) => admitInteraction(context.session, bound.session, batch)),
290
- plan: handed((snapshot) => admitPlanSnapshot(context.session, bound.session, snapshot)),
291
- interrupt: handed((interrupt, outcome) =>
292
- reportInterruptOutcome(context.session, interrupt, outcome),
372
+ howWeWork: () =>
373
+ noteHowWeWork(context.directory).catch((error) =>
374
+ reported(`How-we-work note unwritable (${error.code ?? "unknown"})`),
375
+ ),
376
+ interrupt: handed((interrupt, outcome, plan) =>
377
+ reportInterruptOutcome(context.session, interrupt, outcome, finalPlan(plan)),
293
378
  ),
294
379
  secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
295
380
  settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
296
- forward: handed((frame, signal) =>
297
- localFiles(async (sent) =>
298
- localReply(context.directory, bound.run, sent, await callHouseMcp(
381
+ forward: handed((frame, signal) => frame.method === WORKING_COPY_PUSH_METHOD
382
+ ? sessionPush(context, bound, authority, frame, signal)
383
+ : frame.method === WORKING_COPY_INDEX_METHOD
384
+ ? recordIndexChange(context, frame.params, signal).then(() => ({ jsonrpc: "2.0", id: frame.id, result: {} }))
385
+ : localFiles(async (sent) =>
386
+ callHouseMcp(
299
387
  context.session,
300
388
  await sessionAuthority(context.session, bound.session, authority, signal),
301
389
  sent,
302
390
  signal,
303
- )), context.session.dependencies, signal)(frame)),
391
+ ), context.session.dependencies, signal)(frame)),
304
392
  });
305
- return { bridge, drafts, ended };
393
+ return { bridge, frames, ended };
306
394
  }
307
395
 
308
396
  async function superviseBound(context, bound, opened, start) {
309
- const { bridge, drafts, ended } = opened;
397
+ const { bridge, ended } = opened;
310
398
  const spawn = { committed: false };
311
399
  let unsupervised = null;
312
400
  let destroyed = false;
@@ -320,23 +408,20 @@ async function superviseBound(context, bound, opened, start) {
320
408
  unsupervised = { code: refusal.code, detail: refusal.message };
321
409
  } finally {
322
410
  try {
323
- try {
324
- await settleFiles(context);
325
- for (const interaction of bridge.unsettledInteractions()) {
326
- await settleInteraction(context, interaction, "expired");
327
- }
328
- } finally {
329
- bridge.stopRecovery();
330
- if (owner === null) {
331
- try {
332
- destroyed = await grantExitGrace(context.runtime, bound.tmux);
333
- } finally {
334
- await bridge.close();
335
- }
336
- }
411
+ retryFiles(context);
412
+ await settleFiles(context);
413
+ for (const interaction of bridge.unsettledInteractions()) {
414
+ await settleInteraction(context, interaction, "expired");
337
415
  }
338
416
  } finally {
339
- drafts.close();
417
+ bridge.stopRecovery();
418
+ if (owner === null) {
419
+ try {
420
+ destroyed = await grantExitGrace(context.runtime, bound.tmux);
421
+ } finally {
422
+ await bridge.close();
423
+ }
424
+ }
340
425
  }
341
426
  }
342
427
  ended.signal.throwIfAborted();
@@ -352,6 +437,7 @@ async function superviseBound(context, bound, opened, start) {
352
437
  destroyed,
353
438
  stopped: null,
354
439
  };
440
+ bound.plan = finalPlan(bridge.plan());
355
441
  if (owner !== null) {
356
442
  const finished = await completeOwnerStop(context, bound, owner.requestedAt);
357
443
  return {
@@ -371,50 +457,65 @@ async function superviseBound(context, bound, opened, start) {
371
457
  held !== null &&
372
458
  stopped?.providerContinuation === true &&
373
459
  stopped.providerSessionId !== null;
374
- const turnObserved = stopped?.stopOutcome === "completed";
375
- const sealed = turnObserved
376
- ? await sealAssistantText(context, bound, outcome.assistant)
377
- : { accepted: null, failure: null, unresolved: false, refused: null };
378
- if (sealed.unresolved) return { ...outcome, refused: sealed.refused, settled: false };
379
- if (sealed.failure !== null) outcome.failure ??= sealed.failure;
380
- const execution = outcome.failure;
381
- try {
382
- await observeSessionStop(context.session, bound.session, sealed.accepted !== null);
383
- } catch (refusal) {
384
- if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
385
- outcome.failure ??= { code: refusal.code, detail: refusal.message };
386
- await observeSessionStop(context.session, bound.session);
460
+ if (continuable) {
461
+ await writeContinuation(context.directory, { ...held, providerSessionId: stopped.providerSessionId });
387
462
  }
388
- const report = terminalReport({
389
- conversation: bound.conversation === true,
390
- turnCompleted: turnObserved,
463
+ const turnCompleted = stopped?.stopOutcome === "completed";
464
+ const blocks = turnCompleted ? assistantBlocks(outcome.assistant.join("\n\n")) : null;
465
+ bound.sealed = blocks === null ? null : { operation: operationId(), blocks };
466
+ bound.stopped = {
467
+ turnCompleted,
391
468
  capacityExhausted,
392
469
  providerRefused,
393
- failure: execution,
394
470
  continuable,
395
- });
396
- const retained = report === null;
397
- if (retained) {
398
- await writeContinuation(context.directory, {
399
- ...held,
400
- providerSessionId: stopped.providerSessionId,
471
+ failure: outcome.failure,
472
+ textAccepted: false,
473
+ };
474
+ await writeBinding(context.directory, bound);
475
+ return { ...outcome, ...(await settleStopped(context, bound)) };
476
+ }
477
+
478
+ function stoppedReport(bound) {
479
+ if (bound.stopped === null || bound.stopped === undefined) return null;
480
+ return terminalReport({ ...bound.stopped, conversation: bound.conversation === true });
481
+ }
482
+
483
+ async function settleStopped(context, bound) {
484
+ const { stopped } = bound;
485
+ const unresolved = (refused) => ({ failure: stopped.failure, refused, settled: false });
486
+ if (bound.sealed !== null) {
487
+ const refused = await dispatchSeal(context, bound);
488
+ if (refused !== null) return unresolved(refused);
489
+ }
490
+ let failure = stopped.failure;
491
+ try {
492
+ await observeSessionStop(context.session, bound.session, stopped.textAccepted, bound.plan);
493
+ } catch (refusal) {
494
+ if (undecided(context, refusal)) return unresolved(null);
495
+ if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
496
+ failure ??= { code: refusal.code, detail: refusal.message };
497
+ await observeSessionStop(context.session, bound.session, false, bound.plan);
498
+ }
499
+ const report = stoppedReport(bound);
500
+ if (report !== null) await removeContinuation(context.directory, bound.run);
501
+ try {
502
+ await reportContinuation(context.session, bound.session, {
503
+ providerContinuation: report === null,
504
+ providerCapacityExhausted: stopped.capacityExhausted,
401
505
  });
402
- } else await removeContinuation(context.directory, bound.run);
403
- await reportContinuation(context.session, bound.session, {
404
- providerContinuation: retained,
405
- providerCapacityExhausted: capacityExhausted,
406
- });
407
- if (retained) {
506
+ } catch (refusal) {
507
+ if (undecided(context, refusal)) return unresolved(null);
508
+ if (!(refusal instanceof KitRefusal) || refusal.code !== "worker_run_settled") throw refusal;
509
+ await removeContinuation(context.directory, bound.run);
510
+ await removeBinding(context.directory, bound.tmux);
511
+ return { failure, refused: null, settled: true };
512
+ }
513
+ if (report === null) {
408
514
  await removeBinding(context.directory, bound.tmux);
409
- return outcome;
515
+ return { failure, refused: null, settled: true };
410
516
  }
411
517
  const settled = await reportTerminalState(context, bound, report);
412
- return {
413
- ...outcome,
414
- reported: settled.reported,
415
- refused: settled.failure,
416
- settled: settled.settled,
417
- };
518
+ return { failure, reported: settled.reported, refused: settled.failure, settled: settled.settled };
418
519
  }
419
520
 
420
521
  function assignmentRoute(context, assignment) {
@@ -425,6 +526,11 @@ function assignmentRoute(context, assignment) {
425
526
  return assignment.route;
426
527
  }
427
528
 
529
+ async function appliedWorkingCopies(context) {
530
+ await context.workingCopiesApplied?.();
531
+ return workingCopyPaths(context.directory);
532
+ }
533
+
428
534
  async function prepareAssignment(context, assignment, resumed) {
429
535
  const route = assignmentRoute(context, assignment);
430
536
  const command = await installedAgentCommand(context.directory, route.adapter.kind);
@@ -448,10 +554,14 @@ async function prepareAssignment(context, assignment, resumed) {
448
554
  ownerStop: null,
449
555
  sealed: null,
450
556
  reported: null,
557
+ plan: null,
451
558
  };
452
559
  const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
453
- const opened = await openRunBridge(context, bound, authority, {
560
+ const start = {
561
+ base_instructions: assignment.baseInstructions,
562
+ create_how_we_work: await howWeWorkOwed(context.directory),
454
563
  working_directory: assignment.workingDirectory,
564
+ working_copies: await appliedWorkingCopies(context),
455
565
  execution_authority: context.configuration.executionAuthority,
456
566
  adapter: {
457
567
  kind: route.adapter.kind,
@@ -461,10 +571,16 @@ async function prepareAssignment(context, assignment, resumed) {
461
571
  effort: route.effort,
462
572
  environment: {},
463
573
  },
464
- request: await localRequest(context.directory, assignment.run, assignment.request),
574
+ request: null,
465
575
  provider_session: assignment.providerSessionId ?? null,
466
- });
467
- return { bound, opened };
576
+ };
577
+ const opened = await openRunBridge(context, bound, authority, start);
578
+ const request = async () => {
579
+ const token = await sessionAuthority(context.session, bound.session, authority);
580
+ start.request = await localRequest(context.directory, bound.run,
581
+ await readRunRequest(context.session, bound.run, token));
582
+ };
583
+ return { bound, opened, request };
468
584
  }
469
585
 
470
586
  async function refuseContinuation(context, command, failure) {
@@ -502,14 +618,15 @@ async function superviseAssignment(context, assignment, resumed = false) {
502
618
  detail: refusal instanceof KitRefusal ? refusal.message : null,
503
619
  };
504
620
  if (resumed) return refuseContinuation(context, assignment, failure);
505
- if (undecided(refusal)) throw refusal;
621
+ if (undecided(context, refusal)) throw refusal;
506
622
  return refuseAssignment(context, assignment, failure);
507
623
  }
508
- const { bound, opened } = prepared;
624
+ const { bound, opened, request } = prepared;
509
625
  return superviseBound(context, bound, opened, async (spawn) => {
510
626
  if (!resumed) await commitRun(context.session, assignment.run);
511
627
  spawn.committed = true;
512
628
  await writeBinding(context.directory, bound);
629
+ await request();
513
630
  await createSession(context.runtime, {
514
631
  session: bound.tmux,
515
632
  workingDirectory: assignment.workingDirectory,
@@ -520,7 +637,7 @@ async function superviseAssignment(context, assignment, resumed = false) {
520
637
  },
521
638
  command: context.launcher,
522
639
  });
523
- await observeSessionStart(context.session, bound.session);
640
+ opened.frames.identify(await observeSessionStart(context.session, bound.session));
524
641
  });
525
642
  }
526
643
 
@@ -531,7 +648,7 @@ async function superviseResumption(context, command) {
531
648
  }
532
649
  return superviseAssignment(context, {
533
650
  ...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
534
- conversation: held.conversation, providerSessionId: held.providerSessionId, request: null,
651
+ conversation: held.conversation, providerSessionId: held.providerSessionId,
535
652
  }, true);
536
653
  }
537
654
 
@@ -539,7 +656,7 @@ async function superviseRestored(context, binding, authority) {
539
656
  const opened = await openRunBridge(context, binding, authority);
540
657
  return superviseBound(context, binding, opened, async (spawn) => {
541
658
  spawn.committed = true;
542
- await observeSessionStart(context.session, binding.session);
659
+ opened.frames.identify(await observeSessionStart(context.session, binding.session));
543
660
  });
544
661
  }
545
662
 
@@ -551,7 +668,7 @@ async function settleAbsentSession(context, binding) {
551
668
  providerCapacityExhausted: false,
552
669
  });
553
670
  } catch (refusal) {
554
- if (undecided(refusal)) return { settled: false, failure: null };
671
+ if (undecided(context, refusal)) return { settled: false, failure: null };
555
672
  if (!(refusal instanceof KitRefusal)) throw refusal;
556
673
  }
557
674
  await removeContinuation(context.directory, binding.run);
@@ -567,7 +684,7 @@ async function confirmRestorable(context, binding) {
567
684
  try {
568
685
  authority = await replaceSessionAuthority(context.session, binding.session);
569
686
  } catch (refusal) {
570
- if (undecided(refusal)) return { authority: null, unresolved: true };
687
+ if (undecided(context, refusal)) return { authority: null, unresolved: true };
571
688
  if (!(refusal instanceof KitRefusal)) throw refusal;
572
689
  await destroySession(context.runtime, binding.tmux);
573
690
  await removeBinding(context.directory, binding.tmux);
@@ -586,20 +703,20 @@ async function reconcile(context) {
586
703
  }
587
704
  const bindings = await readBindings(context.directory);
588
705
  if (bindings.length === 0) return { run: null, stopped, unresolved };
589
- const commands = (await pendingCommands(context)).stops;
706
+ const commands = (await pendingCommands(context))?.stops ?? [];
590
707
  const carried = [];
591
708
  for (const binding of bindings) {
592
- const resealed = await recoverAssistantSeal(context, binding);
593
- if (resealed.unresolved) {
594
- unresolved.push(unresolvedRun(binding, resealed.refused));
595
- continue;
596
- }
597
709
  if (binding.reported !== null) {
598
710
  const settled = await settleReported(context, binding);
599
711
  if (settled.settled) await removeBinding(context.directory, binding.tmux);
600
712
  else unresolved.push(unresolvedRun(binding, settled.failure));
601
713
  continue;
602
714
  }
715
+ if (binding.stopped !== null) {
716
+ const settled = await settleStopped(context, binding);
717
+ if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.refused));
718
+ continue;
719
+ }
603
720
  const command = commands.find((pending) => pending.run === binding.run) ?? null;
604
721
  const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
605
722
  if (requestedAt === null) {
@@ -639,7 +756,7 @@ async function reportHeldSessions(context) {
639
756
  const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
640
757
  const held = new Map();
641
758
  for (const entry of [...settlements, ...bindings, ...continued]) {
642
- held.set(entry.run, held.get(entry.run) ?? entry.reported);
759
+ held.set(entry.run, held.get(entry.run) ?? entry.reported ?? stoppedReport(entry));
643
760
  }
644
761
  const reported = await reportSessions(
645
762
  context.session,
@@ -666,39 +783,55 @@ async function afterEpochReport(context, pass) {
666
783
  return pass();
667
784
  }
668
785
 
669
- async function workerPass(context, dependencies) {
670
- const { configuration, session, signal } = context;
786
+ function unlessDraining(context, act) {
787
+ return context.update?.requested ? null : act();
788
+ }
789
+
790
+ async function workerPass(context, dependencies, baseConfiguration, reach) {
791
+ const { session, signal } = context;
671
792
  const reconciled = await reconcile(context);
672
- if (reconciled.run !== null || reconciled.unresolved.length > 0) return reconciled;
673
- context.inbound = await reconcileInbound(context.directory, session);
674
- const setup = await acquireSetup(session, signal);
793
+ if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
794
+ return reconciled;
795
+ }
796
+ context.busy = false;
797
+ const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
798
+ context.startLogins?.(needsLogin);
799
+ context.busy = true;
800
+ const { configuration } = context;
801
+ context.inbound = await unlessDraining(context, () => reconcileInbound(context.directory, session));
802
+ const setup = await unlessDraining(context, () => acquireSetup(session, signal));
675
803
  if (setup !== null) {
676
804
  context.setup = await runSetup(
677
- { session, configuration, directory: context.directory, dependencies, signal },
805
+ {
806
+ session,
807
+ configuration,
808
+ directory: context.directory,
809
+ dependencies,
810
+ signal,
811
+ waitAgentLogin: context.waitAgentLogin,
812
+ },
678
813
  setup.request,
679
814
  );
815
+ await context.recordSetupLogins?.(context.setup);
816
+ const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
817
+ context.startLogins?.(needsLogin);
680
818
  }
681
- context.agents = await updateDeclaredAgents(
682
- { directory: context.directory, at: dependencies.now?.() },
683
- configuration,
684
- dependencies,
685
- );
686
- const readiness = await agentReadiness({ directory: context.directory }, configuration, dependencies);
687
- await reportAgentCapabilities(session, readiness.agents);
688
- if (readiness.refused !== null) throw readiness.refused;
689
- const resumption = await acquireOneResumption(session);
819
+ const resumption = await unlessDraining(context, () => acquireOneResumption(session));
690
820
  if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
691
- const assignment = await acquireOneRun(session);
821
+ if (reach !== "runs") return reconciled;
822
+ const assignment = await unlessDraining(context, () => acquireOneRun(session));
692
823
  if (assignment === null) return reconciled;
693
824
  return { ...reconciled, run: await superviseAssignment(context, assignment) };
694
825
  }
695
826
 
696
- async function workerContext(options, dependencies) {
827
+ async function workerContext(options, dependencies, resident = false) {
697
828
  const configuration = await readConfiguration(options.configuration);
698
829
  const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
699
830
  const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
700
831
  const tmux = await readyTmux(runtime);
701
- const session = await openSession(options, dependencies);
832
+ const session = resident
833
+ ? await residentSession(options, dependencies)
834
+ : await openSession({ ...options, acquire: true }, dependencies);
702
835
  const context = {
703
836
  configuration,
704
837
  runtime,
@@ -707,24 +840,97 @@ async function workerContext(options, dependencies) {
707
840
  launchBase: launchBase(dependencies),
708
841
  container: dependencies.container ?? process.env.HOUSE_KIT_CONTAINER === "1",
709
842
  directory: options.directory,
843
+ home: dependencies.home ?? homedir(),
710
844
  files: { originals: [], failures: [] },
845
+ commands: residentWake(),
846
+ stream: kitStreamWriter(),
711
847
  epochReport: null,
848
+ agentRefresh: Promise.resolve(),
849
+ configurationPath: options.configuration,
850
+ busy: false,
851
+ deferredRemoval: false,
712
852
  };
713
853
  return { context, tmux };
714
854
  }
715
855
 
716
- async function controlledPass(context, dependencies, configuration, signal) {
717
- context.configuration = { ...configuration, agents: await desiredAgents(context.session, signal) };
856
+ async function refreshAgentCapabilities(context, dependencies, signal, configuration, pointer = false) {
857
+ const base = configuration ?? (await readConfiguration(context.configurationPath));
858
+ const busy = context.busy || (await readBindings(context.directory)).length > 0;
859
+ const result = await reconcileAgentKinds(
860
+ { directory: context.directory },
861
+ context.session,
862
+ base,
863
+ dependencies,
864
+ signal,
865
+ {
866
+ pointer,
867
+ busy,
868
+ lastDesired: context.lastDesired,
869
+ onDesired: (wanted) => context.cancelLogins?.(wanted),
870
+ },
871
+ );
872
+ context.deferredRemoval = result.retained.length > 0;
873
+ context.configuration = result.selected;
874
+ context.agents = result.updated;
875
+ context.lastDesired = [...result.selected.agents];
876
+ if (result.refused !== null) throw result.refused;
877
+ return result.reported.filter(
878
+ (agent) =>
879
+ result.selected.agents.includes(agent.kind) && agent.release !== null && !agent.signed_in,
880
+ );
881
+ }
882
+
883
+ function queueAgentRefresh(context, dependencies, signal, configuration, pointer = false) {
884
+ const refreshed = context.agentRefresh.then(() =>
885
+ refreshAgentCapabilities(context, dependencies, signal, configuration, pointer),
886
+ );
887
+ context.agentRefresh = refreshed.catch(() => {});
888
+ return refreshed;
889
+ }
890
+
891
+ async function controlledPass(context, dependencies, configuration, signal, reach = "runs") {
718
892
  context.signal = signal;
719
893
  context.session.signal = signal;
720
- return afterEpochReport(context, () => workerPass(context, dependencies));
894
+ await reconcileSkillSet({
895
+ directory: context.directory,
896
+ home: context.home,
897
+ accepted: configuration.skills,
898
+ });
899
+ context.busy = true;
900
+ try {
901
+ return await afterEpochReport(context, () => workerPass(context, dependencies, configuration, reach));
902
+ } finally {
903
+ context.busy = false;
904
+ if (context.deferredRemoval) context.onIdle?.();
905
+ }
721
906
  }
722
907
 
723
908
  export async function runWorkers(options, dependencies = {}) {
724
909
  const { context, tmux } = await workerContext(options, dependencies);
725
910
  const { session, runtime } = context;
726
911
  const supervised = await maintainControl(session, async (signal) => {
727
- return controlledPass(context, dependencies, context.configuration, signal);
912
+ const watcher = new AbortController();
913
+ const commanded = (subject) => {
914
+ if (subject === null || subject === "commands") context.commands.notify();
915
+ };
916
+ const watching = (dependencies.watchKitWork ?? watchKitWork)(
917
+ session,
918
+ AbortSignal.any([signal, watcher.signal]),
919
+ {
920
+ available: commanded,
921
+ changed: () => {},
922
+ opened: context.stream.opened,
923
+ closed: context.stream.closed,
924
+ },
925
+ dependencies,
926
+ ).catch((error) => reported(`control stream ended: ${error?.code ?? error?.message ?? error}`));
927
+ try {
928
+ await reportedWorkingCopies(applyWorkingCopies, context, signal);
929
+ return await controlledPass(context, dependencies, context.configuration, signal);
930
+ } finally {
931
+ watcher.abort();
932
+ await watching;
933
+ }
728
934
  }, session.signal);
729
935
  return {
730
936
  tmux,
@@ -740,17 +946,301 @@ export async function runWorkers(options, dependencies = {}) {
740
946
  };
741
947
  }
742
948
 
949
+ function reported(line) {
950
+ process.stderr.write(`kit: ${line}\n`);
951
+ }
952
+
953
+ async function reportedWorkingCopies(act, context, signal) {
954
+ const outcome = await act(context, signal);
955
+ for (const refused of outcome.refused) {
956
+ reported(`working copy ${refused.path} refused: ${refused.code}`);
957
+ }
958
+ return outcome;
959
+ }
960
+
961
+ function residentUpdate(context, wake) {
962
+ let requested = existsSync(updateDrain(context.directory));
963
+ let ready = null;
964
+ const waiting = [];
965
+ return {
966
+ get requested() {
967
+ return requested;
968
+ },
969
+ async request(serving) {
970
+ if (!requested) {
971
+ requested = true;
972
+ await writeText(updateDrain(context.directory), "");
973
+ }
974
+ wake.notify();
975
+ const sessions = await heldSessions(context);
976
+ if (ready !== null) return ready;
977
+ if (serving) return { ready: false, sessions, serving };
978
+ if (sessions.length > 0) return { ready: false, sessions };
979
+ return new Promise((resolve) => waiting.push(resolve));
980
+ },
981
+ answer(answer) {
982
+ for (const resolve of waiting.splice(0)) resolve(answer);
983
+ },
984
+ async announce() {
985
+ if (waiting.length === 0) return;
986
+ const sessions = await heldSessions(context);
987
+ if (sessions.length > 0) this.answer({ ready: false, sessions });
988
+ },
989
+ quiesce() {
990
+ ready = { ready: true, sessions: [] };
991
+ this.answer(ready);
992
+ },
993
+ };
994
+ }
995
+
996
+ async function heldSessions(context) {
997
+ const bindings = await readBindings(context.directory);
998
+ if (!context.session.refused) {
999
+ return [...bindings, ...(await readSettlements(context.directory))].map((held) => held.run);
1000
+ }
1001
+ const live = [];
1002
+ for (const binding of bindings) {
1003
+ if (binding.stopped === null && (await sessionPresent(context.runtime, binding.tmux))) {
1004
+ live.push(binding.run);
1005
+ }
1006
+ }
1007
+ return live;
1008
+ }
1009
+
1010
+ async function residentSession(options, dependencies) {
1011
+ for (;;) {
1012
+ try {
1013
+ return await openSession({ ...options, resident: true }, dependencies);
1014
+ } catch (refusal) {
1015
+ if (refusal?.code !== "environment_control_lease_active") throw refusal;
1016
+ await controlSleep(dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS, options.signal);
1017
+ }
1018
+ }
1019
+ }
1020
+
743
1021
  export async function runResident(options, dependencies = {}) {
744
- const { context } = await workerContext(options, dependencies);
745
- await maintainControl(context.session, async (signal) => {
746
- while (!signal.aborted) {
747
- const configuration = await readConfiguration(options.configuration);
748
- await controlledPass(context, dependencies, configuration, signal);
1022
+ const { context } = await workerContext(options, dependencies, true);
1023
+ const { session } = context;
1024
+ const interval = dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS;
1025
+ const wake = residentWake();
1026
+ const update = residentUpdate(context, wake);
1027
+ context.update = update;
1028
+ let sources = null;
1029
+ const served = await serveResident(
1030
+ session,
1031
+ context.directory,
1032
+ options.signal,
1033
+ (serving) => update.request(serving),
1034
+ (request, signal) =>
1035
+ pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
1036
+ (error) => ({ refused: true, text: formatCommandError(error) }),
1037
+ ),
1038
+ (source) =>
1039
+ sources === null ? Promise.reject(new KitRefusal("kit_resident_unavailable")) : sources.capture(source),
1040
+ context.stream,
1041
+ );
1042
+ const serve = async (signal) => {
1043
+ context.busy = true;
1044
+ const watcher = new AbortController();
1045
+ let failure = null;
1046
+ const fail = (error) => {
1047
+ if (signal.aborted || session.refused) return;
1048
+ failure = error;
1049
+ wake.notify();
1050
+ };
1051
+ const logins = new Map();
1052
+ const attemptedLogins = new Map();
1053
+ context.cancelLogins = (wanted) => {
1054
+ for (const [kind, running] of logins) {
1055
+ if (!wanted.includes(kind)) running.controller.abort();
1056
+ }
1057
+ for (const kind of attemptedLogins.keys()) {
1058
+ if (!wanted.includes(kind)) attemptedLogins.delete(kind);
1059
+ }
1060
+ };
1061
+ context.recordSetupLogins = async (modules) => {
1062
+ for (const module of modules) {
1063
+ if (module.category !== "agent" || module.status !== "needs_attention") continue;
1064
+ const release = (await readAgentState(context.directory, module.name)).release;
1065
+ if (release !== null) attemptedLogins.set(module.name, release);
1066
+ }
1067
+ };
1068
+ context.startLogins = (agents) => {
1069
+ if (update.requested) return;
1070
+ for (const agent of agents) {
1071
+ const { kind, release } = agent;
1072
+ if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
1073
+ const controller = new AbortController();
1074
+ const loginSignal = AbortSignal.any([signal, controller.signal]);
1075
+ const task = (async () => {
1076
+ if ((await acquireSetup(context.session, loginSignal)) !== null) return;
1077
+ attemptedLogins.set(kind, release);
1078
+ await signInWantedAgent({ ...context, dependencies, signal: loginSignal }, kind);
1079
+ await queueAgentRefresh(context, dependencies, signal, undefined, true);
1080
+ })()
1081
+ .catch((error) => {
1082
+ if (loginSignal.aborted) return;
1083
+ fail(error);
1084
+ })
1085
+ .finally(() => logins.delete(kind));
1086
+ logins.set(kind, { controller, task });
1087
+ }
1088
+ };
1089
+ context.waitAgentLogin = async (kind) => {
1090
+ await logins.get(kind)?.task;
1091
+ };
1092
+ const agentRefresh = coalescedTask(signal, fail, async () => {
1093
+ const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
1094
+ context.startLogins(needsLogin);
1095
+ });
1096
+ const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
1097
+ const changed = () => {
1098
+ if (!update.requested) deliveries.trigger();
1099
+ };
1100
+ const behind = (outcome) => {
1101
+ if (!outcome.behind) return;
1102
+ controlSleep(interval, signal).then(changed, () => {});
1103
+ };
1104
+ const copies = coalescedTask(signal, unavailable, async () =>
1105
+ behind(await reportedWorkingCopies(applyWorkingCopies, context, signal)),
1106
+ );
1107
+ const deliveries = coalescedTask(signal, unavailable, async () =>
1108
+ behind(await reportedWorkingCopies(deliverWorkingCopies, context, signal)),
1109
+ );
1110
+ sources = sourceEntries({
1111
+ session: context.session,
1112
+ directory: context.directory,
1113
+ dependencies,
1114
+ signal: AbortSignal.any([signal, watcher.signal]),
1115
+ now: session.now,
1116
+ draining: () => update.requested,
1117
+ unavailable: (error) => reported(`google sources unavailable: ${error?.message ?? error}`),
1118
+ });
1119
+ await sources.start();
1120
+ const pointer = () => {
1121
+ if (!update.requested) agentRefresh.trigger();
1122
+ };
1123
+ let passOwed = null;
1124
+ let deferred = null;
1125
+ let retrying = null;
1126
+ const higher = (reach, than) =>
1127
+ PASS_REACHES.indexOf(reach) > PASS_REACHES.indexOf(than) ? reach : than;
1128
+ const owe = (reach) => {
1129
+ passOwed = higher(reach, passOwed);
1130
+ wake.notify();
1131
+ };
1132
+ const defer = (reach) => {
1133
+ deferred = higher(reach, deferred);
1134
+ if (retrying !== null) return;
1135
+ retrying = new AbortController();
1136
+ controlSleep(interval, AbortSignal.any([signal, retrying.signal])).then(() => {
1137
+ retrying = null;
1138
+ const owed = deferred;
1139
+ deferred = null;
1140
+ owe(owed);
1141
+ }, () => {});
1142
+ };
1143
+ const settle = (reach) => {
1144
+ retrying?.abort();
1145
+ retrying = null;
1146
+ if (higher(deferred, reach) !== reach) owe(deferred);
1147
+ deferred = null;
1148
+ };
1149
+ const available = (subject) => {
1150
+ if (subject === null || subject === "runs") owe("runs");
1151
+ if (subject === null || subject === "commands") {
1152
+ context.commands.notify();
1153
+ owe("commands");
1154
+ }
1155
+ if (update.requested) return;
1156
+ if (subject === null || subject === "agents") agentRefresh.trigger();
1157
+ if (subject === null || subject === "working_copy") copies.trigger();
1158
+ if (subject === null || subject === "sources") sources.reconcile();
1159
+ };
1160
+ context.onIdle = pointer;
1161
+ context.workingCopiesApplied = copies.settled;
1162
+ const watching = (dependencies.watchKitWork ?? watchKitWork)(
1163
+ context.session,
1164
+ AbortSignal.any([signal, watcher.signal]),
1165
+ {
1166
+ available,
1167
+ changed,
1168
+ opened: context.stream.opened,
1169
+ closed: context.stream.closed,
1170
+ viewed: (frame) => served.forward(frame),
1171
+ },
1172
+ dependencies,
1173
+ ).catch(fail);
1174
+ try {
1175
+ while (!signal.aborted) {
1176
+ if (failure !== null) throw failure;
1177
+ if (passOwed !== null && !session.refused) {
1178
+ const reach = passOwed;
1179
+ passOwed = null;
1180
+ const configuration = await readConfiguration(options.configuration);
1181
+ const passed = await controlledPass(context, dependencies, configuration, signal, reach).catch(
1182
+ (error) => {
1183
+ if (!session.refused) throw error;
1184
+ return null;
1185
+ },
1186
+ );
1187
+ if ((passed?.run ?? null) !== null) owe(reach);
1188
+ if ((passed?.unresolved.length ?? 0) > 0) defer(reach);
1189
+ else if (passed !== null) settle(reach);
1190
+ }
1191
+ const draining = session.refused || update.requested;
1192
+ if (draining) {
1193
+ const sessions = await heldSessions(context);
1194
+ if (sessions.length > 0) update.answer({ ready: false, sessions });
1195
+ else if (
1196
+ session.refused ||
1197
+ (!served.serving() &&
1198
+ logins.size === 0 &&
1199
+ agentRefresh.idle() &&
1200
+ copies.idle() &&
1201
+ deliveries.idle() &&
1202
+ sources.idle())
1203
+ ) {
1204
+ return;
1205
+ }
1206
+ }
1207
+ if (session.refused || passOwed === null) await wake.wait(draining ? interval : Infinity, signal);
1208
+ }
1209
+ } finally {
1210
+ watcher.abort();
1211
+ await watching;
1212
+ await agentRefresh.settled();
1213
+ await copies.settled();
1214
+ await deliveries.settled();
1215
+ await sources.settled();
1216
+ for (const running of logins.values()) running.controller.abort();
1217
+ await Promise.all([...logins.values()].map((running) => running.task));
1218
+ await context.agentRefresh;
1219
+ }
1220
+ };
1221
+ try {
1222
+ for (;;) {
749
1223
  try {
750
- await delay(dependencies.residentIntervalMs ?? 30_000, undefined, { signal });
1224
+ await maintainControl(session, serve, options.signal, { release: false });
751
1225
  } catch (error) {
752
- if (!signal.aborted) throw error;
1226
+ await releaseControl(session);
1227
+ throw error;
1228
+ }
1229
+ if (session.refused) break;
1230
+ try {
1231
+ await releaseControl(session, { required: true });
1232
+ break;
1233
+ } catch (refusal) {
1234
+ if (!(refusal instanceof KitRefusal)) throw refusal;
1235
+ if (session.refused) break;
1236
+ update.answer({ ready: false, sessions: [], refused: refusal.code });
753
1237
  }
1238
+ await wake.wait(interval, options.signal);
754
1239
  }
755
- }, options.signal);
1240
+ update.quiesce();
1241
+ while (!options.signal.aborted) await wake.wait(Infinity, options.signal);
1242
+ throw new KitRefusal("kit_command_cancelled");
1243
+ } finally {
1244
+ await served.close();
1245
+ }
756
1246
  }