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

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 +430 -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 +36 -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/contract.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
2
2
 
3
- export const KIT_CONTRACT_VERSION = "2026-09-27-private-room-scope";
3
+ export const KIT_CONTRACT_VERSION = "3";
4
4
  export const KIT_CAPTURE_BATCH_MAX = 100;
5
5
  export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
6
6
  export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
@@ -21,10 +21,11 @@ export const COMMAND_ADAPTER_SESSION_TIMEOUT_MS = 30_000;
21
21
  export const WORKER_EXIT_GRACE_MS = 5_000;
22
22
  export const WORKER_EXIT_POLL_MS = 100;
23
23
  export const WORKER_SUPERVISION_POLL_MS = 2_000;
24
- export const WORKER_COMMAND_POLL_MS = 2_000;
25
- export const WORKER_HEARTBEAT_MS = 30_000;
26
24
  export const WORKER_COMMAND_BATCH_MAX = 10;
27
25
  export const WORKER_RUN_TOKEN_MARGIN_MS = 60_000;
26
+ export const KIT_STREAM_RECONNECT_MS = 1_000;
27
+ export const KIT_STREAM_RECONNECT_MAX_MS = 30_000;
28
+ export const KIT_STREAM_REJOIN_MS = 2_000;
28
29
  export const TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES = 10 * 1024 * 1024;
29
30
  export const CONVERSATION_FILE_TRANSFER_TIMEOUT_MS = 30_000;
30
31
  export const WORKER_INHERITED_ENVIRONMENT = ["PATH", "HOME"];
@@ -40,17 +41,15 @@ export function runOutcomePath(run) {
40
41
  return `/kit/runs/${encodeURIComponent(run)}/outcome`;
41
42
  }
42
43
 
44
+ export function runRequestPath(run) {
45
+ return `/kit/runs/${encodeURIComponent(run)}/request`;
46
+ }
47
+
43
48
  export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
44
49
  export const KIT_SESSIONS_REPORT_MAX = 256;
45
50
 
46
- export const ASSISTANT_TEXT_MAX_BYTES = 4096;
47
- export const ASSISTANT_BLOCKS_MAX = 500;
48
51
  export const ASSISTANT_TABLE_COLUMNS_MAX = 20;
49
52
 
50
- export function assistantDraftsPath(workerSession) {
51
- return `/kit/sessions/${encodeURIComponent(workerSession)}/drafts`;
52
- }
53
-
54
53
  export function assistantTextPath(workerSession) {
55
54
  return `/kit/sessions/${encodeURIComponent(workerSession)}/assistant-messages`;
56
55
  }
@@ -61,6 +60,10 @@ export const TELEGRAM_DELIVERY_BATCH_MAX = 10;
61
60
  export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
62
61
  export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
63
62
  export const TELEGRAM_GATEWAY_POLL_MS = 2_000;
63
+ export const TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX = 32_768;
64
+ export const TELEGRAM_MESSAGE_CHARACTERS_MAX = 4_096;
65
+ export const TELEGRAM_CAPTION_CHARACTERS_MAX = 1_024;
66
+ export const TELEGRAM_RICH_MESSAGE_BLOCKS_MAX = 500;
64
67
  export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
65
68
  export const TELEGRAM_MEDIA_KINDS = [
66
69
  "document",
@@ -114,6 +117,10 @@ export const BRIDGE_NONCE_ENVIRONMENT = "HOUSE_KIT_BRIDGE_NONCE";
114
117
  export const BRIDGE_RECONNECT_MS = 200;
115
118
  export const BRIDGE_RECONNECT_MAX_MS = 5_000;
116
119
 
120
+ export const HOUSE_CLI_LINE = "The agents.house house CLI is available: run house --help.";
121
+ export const HOW_WE_WORK_PATH = "/private/library/how-we-work.md";
122
+ export const SHELL_RENDER = "shell/1";
123
+
117
124
  export const ACP_PROTOCOL = 1;
118
125
  export const ACP_AUTH_REQUIRED = -32000;
119
126
  export const ADAPTER_EXIT_GRACE_MS = 2_000;
@@ -195,7 +202,7 @@ export const ADAPTER_CONTRACTS = Object.freeze({
195
202
  guidedLogin: CLAUDE_PASTED_CODE,
196
203
  identity: Object.freeze({ agentInfo: "@agentclientprotocol/claude-agent-acp" }),
197
204
  nativeQuestion: null,
198
- sessionLoad: false,
205
+ sessionLoad: true,
199
206
  sessionConfig: Object.freeze({ model: "model", effort: "effort" }),
200
207
  capabilities: Object.freeze({
201
208
  field_kinds: ELICITATION_FIELD_KINDS,
@@ -251,6 +258,10 @@ export const KIT_LOGIN_REPORT_PATH = "/kit/logins/report";
251
258
  export const GUIDED_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
252
259
  export const GUIDED_LOGIN_OUTPUT_MAX_BYTES = 64 * 1024;
253
260
 
261
+ export const KIT_SOURCES_DESIRED_PATH = "/kit/sources/desired";
262
+ export const KIT_SOURCES_REPORT_PATH = "/kit/sources/report";
263
+ export const SOURCE_CAPTURE_INTERVAL_MS = 15 * 60 * 1000;
264
+
254
265
  export const EXECUTION_AUTHORITIES = ["allow", "refuse"];
255
266
 
256
267
  export const PERMISSION_OPTION_KINDS = Object.freeze({
@@ -265,16 +276,21 @@ export const KIT_WORKSPACES_ACQUIRE_PATH = "/kit/workspaces/acquire";
265
276
  export const WORKSPACE_ACQUIRE_MAX = 10;
266
277
  export const WORKSPACE_POLL_MS = 2_000;
267
278
  export const WORKSPACE_LABEL = "house-kit.workspace";
268
- export const WORKSPACE_MOUNT = "/workspace";
269
- export const WORKSPACE_KIT_HOME = "/workspace/.house-kit";
279
+ export const WORKSPACE_KIT_HOME = "/kit-home";
270
280
  export const WORKSPACE_DISK_IMAGE = "disk.img";
271
281
  export const WORKSPACE_DISK_MOUNT = "mnt";
282
+ export const WORKSPACE_PREVIOUS_SUFFIX = "-previous";
283
+ export const WORKSPACE_DISK_KIT_HOME = "kit-home";
284
+ export const WORKSPACE_DISK_HOUSE = "house";
285
+ export const WORKSPACE_SELECTED_IMAGE = "/opt/house-kit/image";
286
+ export const WORKSPACE_RELEASE_IMAGE = /^ghcr\.io\/agentshouse\/kit@sha256:[0-9a-f]{64}$/;
272
287
  export const WORKSPACE_MOUNT_OPTIONS = "loop,nosuid,nodev";
273
288
  export const HOST_MOUNT_INFO = "/proc/self/mountinfo";
274
289
  export const WORKSPACE_SYSTEM_RESERVE_BYTES = 512 * 1024 * 1024;
275
290
  export const WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
276
291
  export const WORKSPACE_READY_ATTEMPTS = 60;
277
292
  export const WORKSPACE_READY_POLL_MS = 1_000;
293
+ export const WORKSPACE_LOCK_POLL_MS = 1_000;
278
294
  export const WORKSPACE_STOP_TIMEOUT_SECONDS = 10;
279
295
  export const WORKSPACE_OPERATION_WITHDRAWN = "hosted_environment_operation_not_found";
280
296
  export const CGROUP_CONTROLLERS = "/sys/fs/cgroup/cgroup.controllers";
@@ -288,9 +304,25 @@ export const KIT_DOOR_DISCOVER_PATH = "/kit/door/discover";
288
304
  export const KIT_DOOR_BOOTSTRAP_PATH = "/kit/door/bootstrap";
289
305
  export const KIT_DOOR_ENUMERATE_PATH = "/kit/door/enumerate";
290
306
  export const KIT_DOOR_BATCH_PATH = "/kit/door/batch";
307
+ export const KIT_DOOR_REVISIONS_PATH = "/kit/door/revisions";
291
308
  export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
292
309
  export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
293
310
 
311
+ export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
312
+ export const WORKING_COPY_VERSION = "working-copy-state/3";
313
+ export const WORKING_COPIES_HOME = "working-copies";
314
+ export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
315
+ export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
316
+ export const WORKING_COPY_RECEIVED_REF = "refs/house/received";
317
+ export const WORKING_COPY_BRANCH = "main";
318
+ export const ROOM_REFERENCE = /^r_[0-9a-z]+\.[0-9a-z]$/;
319
+ export const WORKING_COPY_PUSH_METHOD = "house/git/push";
320
+ export const WORKING_COPY_INDEX_METHOD = "house/git/index";
321
+ export const WORKING_COPY_PUSH_HELP =
322
+ "Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
323
+ export const WORKING_COPY_UPLOAD_COMMAND = "prepare_edit_upload";
324
+ export const MCP_REQUEST_BODY_MAX_BYTES = 4 * 1024 * 1024;
325
+
294
326
  export const MIGRATOR_CONTRACT = "kit-migrator/1";
295
327
  export const MIGRATOR_CATEGORY = "migrator";
296
328
  export const MIGRATOR_MANIFEST = "migrator.json";
@@ -43,9 +43,7 @@ export async function holdConversationFile(context, file, bytes) {
43
43
  const path = statePath(context.directory, identity);
44
44
  const noted = await readNote(path, null);
45
45
  const observed = bytes === null || oversized ? null : describedBytes(bytes);
46
- // House fixes a version's descriptor the first time one is declared and refuses
47
- // every later one, so a file held again keeps the descriptor its note carries.
48
- // Bytes that no longer answer that descriptor are not this original's bytes.
46
+
49
47
  const descriptor = noted?.descriptor ?? observed;
50
48
  const beyondBound = descriptor === null && (oversized || noted?.exceeded === true);
51
49
  const held = {
package/src/delivery.js CHANGED
@@ -54,10 +54,8 @@ const FAILURE_CATEGORIES = {
54
54
  google_provider_unavailable: "provider",
55
55
  google_provider_response_invalid: "provider",
56
56
  google_checkpoint_invalid: "provider",
57
- google_service_unsupported: "config",
58
- google_oauth_client_unconfigured: "config",
59
- google_drive_selection_exceeded: "config",
60
57
  google_drive_item_missing: "provider",
58
+ api_disabled: "config",
61
59
  };
62
60
 
63
61
  export function fileDescriptor(file) {
@@ -184,8 +182,7 @@ export function stallOf(code, houseRetry = null) {
184
182
 
185
183
  export function healthOfRun(stall) {
186
184
  if (stall === null) return { state: "ok", category: null };
187
- return {
188
- state: stall.disposition === "retry" ? "retrying" : "failed",
189
- category: stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal",
190
- };
185
+ const category = stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal";
186
+ if (stall.disposition === "retry") return { state: "retrying", category };
187
+ return { state: "failed", category, cause: stall.code };
191
188
  }
package/src/directory.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { constants } from "node:fs";
2
- import { access, mkdir, realpath, rename, rm, stat } from "node:fs/promises";
3
- import { randomUUID } from "node:crypto";
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { access, mkdir, realpath, stat } from "node:fs/promises";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
4
  import { LAUNCH_DIRECTORY_BASE } from "./contract.js";
6
5
  import { KitRefusal } from "./refusal.js";
7
- import { seedStarterSet } from "./starter-set.js";
8
6
 
9
7
  export function launchBase(dependencies = {}) {
10
8
  return dependencies.launchBase ?? LAUNCH_DIRECTORY_BASE;
@@ -39,25 +37,9 @@ function unavailable(path, cause) {
39
37
  return new KitRefusal("kit_launch_directory_unavailable", `${path} ${cause}`);
40
38
  }
41
39
 
42
- async function isDirectory(path) {
43
- try {
44
- return (await stat(path)).isDirectory();
45
- } catch {
46
- return false;
47
- }
48
- }
49
-
50
- async function seedDefaultDirectory(selected, base, path) {
51
- const staging = join(base, `.${randomUUID()}.staging`);
52
- try {
53
- await seedStarterSet(staging);
54
- await rename(staging, path);
55
- } catch (error) {
56
- await rm(staging, { recursive: true, force: true }).catch(() => undefined);
57
- if (!(await isDirectory(path))) {
58
- throw unavailable(selected, `could not be prepared (${error.code ?? "unknown"})`);
59
- }
60
- }
40
+ async function createDefaultDirectory(selected, path) {
41
+ const refused = await made(path);
42
+ if (refused !== null) throw unavailable(selected, `could not be created (${refused})`);
61
43
  }
62
44
 
63
45
  function within(base, selected) {
@@ -73,9 +55,7 @@ export async function readyLaunchDirectory(selected, base, agentId = null, optio
73
55
  if (options.container && !within(resolve(base), path)) {
74
56
  throw unavailable(selected, `resolves outside ${base}`);
75
57
  }
76
- if (path === defaultDirectory(base, agentId) && !(await isDirectory(path))) {
77
- await seedDefaultDirectory(selected, base, path);
78
- }
58
+ if (path === defaultDirectory(base, agentId)) await createDefaultDirectory(selected, path);
79
59
  let described;
80
60
  try {
81
61
  described = await stat(path);
package/src/draft.js CHANGED
@@ -1,89 +1,11 @@
1
- import { assistantBlocks, blocksText, withinAssistantBound } from "./blocks.js";
2
-
3
- const DRAFT_INTERVAL_MS = 500;
1
+ import { pacedLatest } from "./pacing.js";
4
2
 
5
3
  export function observedAssistantDrafts(options) {
6
- const intervalMs = options.intervalMs ?? DRAFT_INTERVAL_MS;
7
- const now = options.now ?? (() => Date.now());
8
- let timer = null;
9
- let sentAt = null;
10
- let sent = null;
11
- let latest = null;
12
-
13
- function write() {
14
- timer = null;
15
- if (latest === null || latest === sent) return;
16
- sent = latest;
17
- sentAt = now();
18
- options.send(sent);
19
- }
20
-
21
- function cancel() {
22
- if (timer === null) return;
23
- clearTimeout(timer);
24
- timer = null;
25
- }
26
-
4
+ const paced = pacedLatest(options);
27
5
  return {
28
6
  observe(view) {
29
- if (typeof view !== "string" || view === "") return;
30
- latest = view;
31
- if (timer !== null) return;
32
- const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
33
- if (wait <= 0) {
34
- write();
35
- return;
36
- }
37
- timer = setTimeout(write, wait);
38
- timer.unref?.();
39
- },
40
- complete(view) {
41
- if (typeof view !== "string" || view === "") return;
42
- latest = view;
43
- cancel();
44
- write();
45
- },
46
- close: cancel,
47
- };
48
- }
49
-
50
- export function openAssistantDrafts(options) {
51
- let pending = null;
52
- let sending = null;
53
- let sent = null;
54
- let closed = false;
55
-
56
- function pump() {
57
- if (sending !== null || pending === null) return;
58
- const view = pending;
59
- pending = null;
60
- sent = view.text;
61
- let started;
62
- try {
63
- started = Promise.resolve(options.send(view.blocks));
64
- } catch {
65
- started = Promise.resolve();
66
- }
67
- sending = started
68
- .catch(() => undefined)
69
- .finally(() => {
70
- sending = null;
71
- pump();
72
- });
73
- }
74
-
75
- return {
76
- update(text) {
77
- if (closed || typeof text !== "string" || text === "") return;
78
- if (text === sent || text === pending?.text) return;
79
- const blocks = assistantBlocks(text);
80
- if (blocks === null || !withinAssistantBound(blocksText(blocks))) return;
81
- pending = { text, blocks };
82
- pump();
83
- },
84
- close() {
85
- closed = true;
86
- pump();
7
+ if (typeof view === "string" && view !== "") paced.observe(view);
87
8
  },
9
+ close: paced.close,
88
10
  };
89
11
  }
@@ -0,0 +1,138 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
+ import { constants as system } from "node:os";
4
+ import { delimiter, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ BRIDGE_NONCE_ENVIRONMENT,
8
+ BRIDGE_SOCKET_ENVIRONMENT,
9
+ WORKING_COPY_INDEX_METHOD,
10
+ WORKING_COPY_RECEIVED_REF,
11
+ } from "./contract.js";
12
+ import { bridgeCall } from "./mcp.js";
13
+ import { formatCommandError, KitRefusal } from "./refusal.js";
14
+
15
+ const SHIM = realpathSync(fileURLToPath(new URL("session-bin/git", import.meta.url)));
16
+ const VALUED = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--attr-source", "--super-prefix"]);
17
+ const RELAYED = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
18
+
19
+ function nativeGit() {
20
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
21
+ if (directory === "") continue;
22
+ const candidate = join(directory, "git");
23
+ try {
24
+ if (!statSync(candidate).isFile() || realpathSync(candidate) === SHIM) continue;
25
+ accessSync(candidate, constants.X_OK);
26
+ return candidate;
27
+ } catch {}
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function command(args) {
33
+ let index = 0;
34
+ while (index < args.length && args[index].startsWith("-")) index += VALUED.has(args[index]) ? 2 : 1;
35
+ return { globals: args.slice(0, index), name: args[index], rest: args.slice(index + 1) };
36
+ }
37
+
38
+ function delegated(git, args) {
39
+ return new Promise((resolve) => {
40
+ const child = spawn(git, args, { stdio: "inherit" });
41
+ const relay = (signal) => child.kill(signal);
42
+ for (const signal of RELAYED) process.on(signal, relay);
43
+ const settle = (outcome) => {
44
+ for (const signal of RELAYED) process.off(signal, relay);
45
+ resolve(outcome);
46
+ };
47
+ child.once("error", (error) => {
48
+ process.stderr.write(`kit_git_unavailable: ${error.message}\n`);
49
+ settle({ status: 127, signal: null });
50
+ });
51
+ child.once("close", (status, signal) => settle({ status, signal }));
52
+ });
53
+ }
54
+
55
+ function captured(git, args, env = process.env) {
56
+ const run = spawnSync(git, args, { stdio: ["ignore", "pipe", "ignore"], maxBuffer: Infinity, env });
57
+ return run.status === 0 ? run.stdout.toString("utf8") : null;
58
+ }
59
+
60
+ function indexed(listing) {
61
+ const entries = new Map();
62
+ for (const record of listing.split("\0")) {
63
+ const tab = record.indexOf("\t");
64
+ if (tab < 0) continue;
65
+ const [mode, object, stage] = record.slice(0, tab).split(" ");
66
+ entries.set(record.slice(tab + 1), stage === "0" ? `${mode} ${object}` : null);
67
+ }
68
+ return entries;
69
+ }
70
+
71
+ function renamings(output) {
72
+ return (output ?? "").split("\n").filter((line) => line.startsWith("Renaming ")).map((line) => line.slice(9));
73
+ }
74
+
75
+ function movedPaths(renaming, before, after) {
76
+ for (let at = renaming.indexOf(" to "); at >= 0; at = renaming.indexOf(" to ", at + 1)) {
77
+ const from = renaming.slice(0, at);
78
+ const to = renaming.slice(at + 4);
79
+ if (before.has(from) && !after.has(from) && after.get(to) === before.get(from)) return [{ from, to }];
80
+ }
81
+ return [];
82
+ }
83
+
84
+ async function recorded(cwd, change) {
85
+ const socket = process.env[BRIDGE_SOCKET_ENVIRONMENT];
86
+ const nonce = process.env[BRIDGE_NONCE_ENVIRONMENT];
87
+ if (socket === undefined || nonce === undefined) return;
88
+ try {
89
+ const frame = await bridgeCall({ socket, nonce }, { method: WORKING_COPY_INDEX_METHOD, params: { cwd, ...change } });
90
+ if (frame.error !== undefined) throw new KitRefusal(frame.error.message ?? "kit_house_refused");
91
+ } catch (refusal) {
92
+ process.stderr.write(`${formatCommandError(refusal)}: house git push will not see this index change\n`);
93
+ }
94
+ }
95
+
96
+ async function observed(git, { globals, name, rest }, args) {
97
+ const located = captured(git, [...globals, "rev-parse", "--show-toplevel", "--show-cdup", WORKING_COPY_RECEIVED_REF, "HEAD"]);
98
+ if (located === null) return delegated(git, args);
99
+ const [top, cdup, , head] = located.split("\n");
100
+ const listing = [...globals, "ls-files", "--stage", "-z", "--full-name", "--", cdup === "" ? "." : cdup];
101
+ const before = captured(git, listing);
102
+ const rehearsal = [...globals, "mv", "--dry-run", ...rest];
103
+ const planned = name === "mv" ? renamings(captured(git, rehearsal, { ...process.env, LC_ALL: "C" })) : [];
104
+ const outcome = await delegated(git, args);
105
+ const after = before === null ? null : captured(git, listing);
106
+ if (after === null) return outcome;
107
+ const [prior, next] = [indexed(before), indexed(after)];
108
+ const moves = outcome.status === 0 ? planned.flatMap((renaming) => movedPaths(renaming, prior, next)) : [];
109
+ const sources = new Set(moves.map((move) => move.from));
110
+ const destinations = new Set(moves.map((move) => move.to));
111
+ const gone = [...prior.keys()].filter((path) => !next.has(path) && !sources.has(path));
112
+ const arrived = [...next.keys()].filter((path) => !prior.has(path) && !destinations.has(path));
113
+ const heads = captured(git, [...globals, "rev-parse", "HEAD", ...(name === "rebase" ? ["ORIG_HEAD"] : [])]);
114
+ const [current, tip = head] = heads?.trim().split("\n") ?? [];
115
+ const commit = current === undefined || current === head ? [] : [tip, current];
116
+ if (moves.length > 0 || gone.length > 0 || arrived.length > 0 || commit.length > 0) {
117
+ await recorded(top, { moves, gone, arrived, commit, command: name });
118
+ }
119
+ return outcome;
120
+ }
121
+
122
+ async function run(args) {
123
+ const git = nativeGit();
124
+ if (git === null) {
125
+ process.stderr.write("kit_git_unavailable\n");
126
+ return { status: 127, signal: null };
127
+ }
128
+ return observed(git, command(args), args);
129
+ }
130
+
131
+ run(process.argv.slice(2)).then(({ status, signal }) => {
132
+ if (signal === null) {
133
+ process.exitCode = status;
134
+ return;
135
+ }
136
+ process.exitCode = 128 + system.signals[signal];
137
+ process.kill(process.pid, signal);
138
+ });
package/src/git.js ADDED
@@ -0,0 +1,62 @@
1
+ import { spawn } from "node:child_process";
2
+ import { KitRefusal } from "./refusal.js";
3
+
4
+ function cancelledOr(signal, error, code) {
5
+ return signal?.aborted === true || error?.name === "AbortError"
6
+ ? new KitRefusal("kit_command_cancelled")
7
+ : new KitRefusal(code);
8
+ }
9
+
10
+ function stdio(capture, input) {
11
+ if (input !== null) return ["pipe", capture ? "pipe" : "inherit", "inherit"];
12
+ return capture ? ["ignore", "pipe", "inherit"] : "inherit";
13
+ }
14
+
15
+ export function runGit(cwd, args, options = {}) {
16
+ const { dependencies = {}, capture = false, input = null, signal, failure } = options;
17
+ if (signal?.aborted === true) return Promise.reject(new KitRefusal("kit_command_cancelled"));
18
+ const launcher = dependencies.spawn ?? spawn;
19
+ return new Promise((resolve, reject) => {
20
+ let child;
21
+ try {
22
+ child = launcher("git", args, {
23
+ cwd,
24
+ env: process.env,
25
+ stdio: stdio(capture, input),
26
+ signal,
27
+ killSignal: "SIGKILL",
28
+ });
29
+ } catch (error) {
30
+ reject(cancelledOr(signal, error, "kit_git_unavailable"));
31
+ return;
32
+ }
33
+ const output = [];
34
+ let settled = false;
35
+ const finish = (operation) => {
36
+ if (settled) return;
37
+ settled = true;
38
+ operation();
39
+ };
40
+ child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
41
+ child.once("error", (error) =>
42
+ finish(() => reject(cancelledOr(signal, error, "kit_git_unavailable"))),
43
+ );
44
+ child.once("close", (status) => {
45
+ finish(() => {
46
+ if (signal?.aborted === true) {
47
+ reject(new KitRefusal("kit_command_cancelled"));
48
+ return;
49
+ }
50
+ if (status !== 0) {
51
+ reject(new KitRefusal(failure));
52
+ return;
53
+ }
54
+ resolve(Buffer.concat(output));
55
+ });
56
+ });
57
+ if (input !== null) {
58
+ child.stdin.on("error", () => undefined);
59
+ child.stdin.end(input);
60
+ }
61
+ });
62
+ }
@@ -4,9 +4,10 @@ import { googleJson } from "./gateway.js";
4
4
  import { GOOGLE_SCOPES } from "./oauth.js";
5
5
 
6
6
  const CALENDAR_ORIGIN = "https://www.googleapis.com";
7
+ const CALENDAR = "primary";
7
8
 
8
- function cursorOf(value, calendars) {
9
- if (value === null) return { calendar: 0, pageToken: null, syncTokens: {} };
9
+ function cursorOf(value) {
10
+ if (value === null) return { pageToken: null, syncToken: null };
10
11
  let cursor;
11
12
  try {
12
13
  cursor = JSON.parse(value);
@@ -16,16 +17,8 @@ function cursorOf(value, calendars) {
16
17
  if (
17
18
  typeof cursor !== "object" ||
18
19
  cursor === null ||
19
- !Number.isInteger(cursor.calendar) ||
20
- cursor.calendar < 0 ||
21
- cursor.calendar >= calendars.length ||
22
20
  (cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
23
- typeof cursor.syncTokens !== "object" ||
24
- cursor.syncTokens === null ||
25
- Array.isArray(cursor.syncTokens) ||
26
- Object.entries(cursor.syncTokens).some(
27
- ([calendar, token]) => !calendars.includes(calendar) || typeof token !== "string",
28
- )
21
+ (cursor.syncToken !== null && typeof cursor.syncToken !== "string")
29
22
  ) {
30
23
  throw new KitRefusal("google_checkpoint_invalid", "calendar");
31
24
  }
@@ -45,7 +38,7 @@ function authorOf(event) {
45
38
  : event.organizer.email;
46
39
  }
47
40
 
48
- function normalizedEvent(event, calendar) {
41
+ function normalizedEvent(event) {
49
42
  if (
50
43
  typeof event?.id !== "string" ||
51
44
  typeof event.etag !== "string" ||
@@ -71,45 +64,53 @@ function normalizedEvent(event, calendar) {
71
64
  author: authorOf(event),
72
65
  url: event.htmlLink ?? null,
73
66
  attachments: [],
74
- container: `google/calendar/${calendar}`,
67
+ container: `google/calendar/${CALENDAR}`,
75
68
  };
76
69
  }
77
70
 
78
- function pageUrl(calendar, cursor, limit, backfillFloor) {
79
- const url = new URL(
80
- `/calendar/v3/calendars/${encodeURIComponent(calendar)}/events`,
81
- CALENDAR_ORIGIN,
82
- );
71
+ function common(options) {
72
+ return {
73
+ directory: options.directory,
74
+ account: options.account,
75
+ requiredScopes: [GOOGLE_SCOPES.calendar],
76
+ signal: options.signal,
77
+ };
78
+ }
79
+
80
+ function pageUrl(cursor, limit, backfillFloor) {
81
+ const url = new URL(`/calendar/v3/calendars/${CALENDAR}/events`, CALENDAR_ORIGIN);
83
82
  url.searchParams.set("maxResults", `${limit}`);
84
83
  url.searchParams.set("singleEvents", "true");
85
84
  url.searchParams.set("showDeleted", "true");
86
85
  if (cursor.pageToken !== null) url.searchParams.set("pageToken", cursor.pageToken);
87
- const syncToken = cursor.syncTokens[calendar];
88
- if (syncToken !== undefined) url.searchParams.set("syncToken", syncToken);
86
+ if (cursor.syncToken !== null) url.searchParams.set("syncToken", cursor.syncToken);
89
87
  else if (backfillFloor !== null && backfillFloor !== undefined) {
90
88
  url.searchParams.set("timeMin", backfillFloor);
91
89
  }
92
90
  return url.toString();
93
91
  }
94
92
 
93
+ export async function calendarEmail(options, dependencies = {}) {
94
+ const calendar = await googleJson(
95
+ { ...common(options), url: `${CALENDAR_ORIGIN}/calendar/v3/calendars/${CALENDAR}` },
96
+ dependencies,
97
+ );
98
+ if (typeof calendar.id !== "string") {
99
+ throw new KitRefusal("google_provider_response_invalid", "calendar");
100
+ }
101
+ return calendar.id;
102
+ }
103
+
95
104
  export async function calendarPage(options, dependencies = {}) {
96
- const calendars = options.selection.calendars;
97
- let cursor = cursorOf(options.cursor, calendars);
98
- const calendar = calendars[cursor.calendar];
99
- const common = {
100
- directory: options.directory,
101
- instance: options.instance,
102
- client: options.client,
103
- requiredScopes: [GOOGLE_SCOPES.calendar],
104
- signal: options.signal,
105
- };
105
+ let cursor = cursorOf(options.cursor);
106
+ const shared = common(options);
106
107
  let answer;
107
108
  try {
108
109
  answer = await googleJson(
109
110
  {
110
- ...common,
111
+ ...shared,
111
112
  goneCode: "google_calendar_cursor_expired",
112
- url: pageUrl(calendar, cursor, options.limit, options.backfillFloor),
113
+ url: pageUrl(cursor, options.limit, options.backfillFloor),
113
114
  },
114
115
  dependencies,
115
116
  );
@@ -117,11 +118,9 @@ export async function calendarPage(options, dependencies = {}) {
117
118
  if (!(error instanceof KitRefusal) || error.code !== "google_calendar_cursor_expired") {
118
119
  throw error;
119
120
  }
120
- const syncTokens = { ...cursor.syncTokens };
121
- delete syncTokens[calendar];
122
- cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
121
+ cursor = { pageToken: null, syncToken: null };
123
122
  answer = await googleJson(
124
- { ...common, url: pageUrl(calendar, cursor, options.limit, options.backfillFloor) },
123
+ { ...shared, url: pageUrl(cursor, options.limit, options.backfillFloor) },
125
124
  dependencies,
126
125
  );
127
126
  }
@@ -135,23 +134,18 @@ export async function calendarPage(options, dependencies = {}) {
135
134
  }
136
135
  let next;
137
136
  if (answer.nextPageToken !== undefined) {
138
- next = { calendar: cursor.calendar, pageToken: answer.nextPageToken, syncTokens: cursor.syncTokens };
137
+ next = { pageToken: answer.nextPageToken, syncToken: cursor.syncToken };
139
138
  } else {
140
139
  if (typeof answer.nextSyncToken !== "string") {
141
140
  throw new KitRefusal("google_provider_response_invalid", "calendar sync token");
142
141
  }
143
- const syncTokens = { ...cursor.syncTokens, [calendar]: answer.nextSyncToken };
144
- next = {
145
- calendar: cursor.calendar + 1 === calendars.length ? 0 : cursor.calendar + 1,
146
- pageToken: null,
147
- syncTokens,
148
- };
142
+ next = { pageToken: null, syncToken: answer.nextSyncToken };
149
143
  }
150
144
  const normalized = [];
151
145
  const dropped = [];
152
146
  for (const event of items) {
153
147
  try {
154
- normalized.push(normalizedEvent(event, calendar));
148
+ normalized.push(normalizedEvent(event));
155
149
  } catch (error) {
156
150
  if (
157
151
  error instanceof KitRefusal &&
@@ -172,6 +166,6 @@ export async function calendarPage(options, dependencies = {}) {
172
166
  items: normalized,
173
167
  dropped,
174
168
  cursor: JSON.stringify(next),
175
- more: answer.nextPageToken !== undefined || cursor.calendar + 1 < calendars.length,
169
+ more: answer.nextPageToken !== undefined,
176
170
  };
177
171
  }