@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.41

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