@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
package/bootstrap.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.1.0-alpha.21",
4
+ "hosts": {
5
+ "linux": {
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh)",
7
+ "asset": {
8
+ "name": "connect-linux.sh",
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh"
10
+ },
11
+ "workspace": {
12
+ "argument": "--workspace",
13
+ "quoting": "posix"
14
+ }
15
+ }
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.21",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,11 +9,16 @@
9
9
  "private": false,
10
10
  "type": "module",
11
11
  "bin": {
12
+ "house": "src/session-bin/house",
12
13
  "kit": "bin/kit.js"
13
14
  },
15
+ "exports": {
16
+ "./bootstrap": "./bootstrap.json"
17
+ },
14
18
  "dependencies": {
15
19
  "@agentclientprotocol/sdk": "1.5.0",
16
- "@agentshouse/mdmodel": "0.1.0-alpha.13",
20
+ "@agentshouse/mdmodel": "0.1.0-alpha.15",
21
+ "ws": "8.22.0",
17
22
  "yaml": "2.9.1",
18
23
  "zod": "4.6.5"
19
24
  },
@@ -29,16 +29,3 @@ export async function commandHouseInstructions(bridge) {
29
29
  }
30
30
  return frame.result.instructions;
31
31
  }
32
-
33
- export async function commandHouseRequest(bridge) {
34
- const frame = await commandHouseReply(bridge, { method: "tools/call", params: { name: "session_get_request", arguments: {} } });
35
- if (frame.error !== undefined || frame.result?.isError === true) throw new KitRefusal("kit_run_input_unreadable");
36
- try {
37
- const content = frame.result.content;
38
- const input = JSON.parse(content.find((item) => item.type === "text").text);
39
- if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error();
40
- return input;
41
- } catch {
42
- throw new KitRefusal("kit_run_input_unreadable");
43
- }
44
- }
package/src/agents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { readFile, rm } from "node:fs/promises";
2
+ import { readFile, readdir, rm } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
@@ -11,6 +11,7 @@ import {
11
11
  } from "./adapter.js";
12
12
  import { inheritedEnvironment } from "./command.js";
13
13
  import {
14
+ ADAPTER_KINDS,
14
15
  AGENT_HOME,
15
16
  AGENT_INSTALL_TIMEOUT_MS,
16
17
  AGENT_RELEASES_HOME,
@@ -261,6 +262,60 @@ export async function updateDeclaredAgents(options, configuration, dependencies
261
262
  return updated;
262
263
  }
263
264
 
265
+ export async function removeUndeclaredAgents(directory, wanted, keep = false) {
266
+ const home = join(directory, AGENT_HOME);
267
+ let entries;
268
+ try {
269
+ entries = await readdir(home, { withFileTypes: true });
270
+ } catch (error) {
271
+ if (error.code === "ENOENT") return [];
272
+ throw error;
273
+ }
274
+ const retained = [];
275
+ for (const entry of entries) {
276
+ if (!wanted.includes(entry.name)) {
277
+ if (keep && ADAPTER_KINDS.includes(entry.name)) retained.push(entry.name);
278
+ else await rm(join(home, entry.name), { recursive: true, force: true });
279
+ }
280
+ }
281
+ return retained;
282
+ }
283
+
284
+ export async function reconcileAgentKinds(
285
+ options,
286
+ session,
287
+ configuration,
288
+ dependencies = {},
289
+ signal = session.signal,
290
+ state = {},
291
+ ) {
292
+ const selected = { ...configuration, agents: await desiredAgents(session, signal) };
293
+ state.onDesired?.(selected.agents);
294
+ const newlyWanted = selected.agents.filter((kind) => !state.lastDesired?.includes(kind));
295
+ const retained = await removeUndeclaredAgents(options.directory, selected.agents, state.busy);
296
+ const pointerKinds = state.busy
297
+ ? (
298
+ await Promise.all(
299
+ newlyWanted.map(async (kind) =>
300
+ (await readAgentState(options.directory, kind)).release === null ? kind : null,
301
+ ),
302
+ )
303
+ ).filter((kind) => kind !== null)
304
+ : newlyWanted;
305
+ const updated = await updateDeclaredAgents(
306
+ { directory: options.directory, at: dependencies.now?.(), forced: state.forced },
307
+ { ...selected, agents: state.pointer ? pointerKinds : selected.agents },
308
+ dependencies,
309
+ );
310
+ const readiness = await agentReadiness(
311
+ { directory: options.directory },
312
+ { ...selected, agents: [...selected.agents, ...retained] },
313
+ dependencies,
314
+ );
315
+ await reportAgentCapabilities(session, readiness.agents, signal);
316
+ return { selected, updated, reported: readiness.agents, refused: readiness.refused, retained };
317
+ }
318
+
264
319
  export async function agentSignedIn(directory, kind, dependencies = {}) {
265
320
  const contract = compiledContract(kind);
266
321
  const state = await readAgentState(directory, kind);
@@ -341,15 +396,15 @@ export async function updateAgents(options, dependencies = {}) {
341
396
  return maintainControl(
342
397
  session,
343
398
  async (signal) => {
344
- const selected = { ...configuration, agents: await desiredAgents(session, signal) };
345
- const updated = await updateDeclaredAgents(
346
- { directory: options.directory, at: dependencies.now?.(), forced: true },
347
- selected,
399
+ const result = await reconcileAgentKinds(
400
+ options,
401
+ session,
402
+ configuration,
348
403
  dependencies,
404
+ signal,
405
+ { forced: true },
349
406
  );
350
- const { agents } = await agentReadiness(options, selected, dependencies);
351
- await reportAgentCapabilities(session, agents, signal);
352
- return { updated, reported: agents };
407
+ return { updated: result.updated, reported: result.reported };
353
408
  },
354
409
  options.signal,
355
410
  );
package/src/binding.js CHANGED
@@ -15,6 +15,18 @@ function bindingPath(directory, tmux) {
15
15
  return join(bindingDirectory(directory), `${tmux}.json`);
16
16
  }
17
17
 
18
+ function stoppedNote(stopped) {
19
+ if (stopped === null || stopped === undefined) return null;
20
+ return {
21
+ turn_completed: stopped.turnCompleted,
22
+ capacity_exhausted: stopped.capacityExhausted,
23
+ provider_refused: stopped.providerRefused,
24
+ continuable: stopped.continuable,
25
+ failure: stopped.failure,
26
+ text_accepted: stopped.textAccepted,
27
+ };
28
+ }
29
+
18
30
  export async function writeBinding(directory, binding) {
19
31
  const reported = binding.reported ?? null;
20
32
  await writeNote(bindingPath(directory, binding.tmux), {
@@ -29,6 +41,7 @@ export async function writeBinding(directory, binding) {
29
41
  sealed: binding.sealed === null || binding.sealed === undefined
30
42
  ? null
31
43
  : { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
44
+ stopped: stoppedNote(binding.stopped),
32
45
  reported: reported === null ? null : reportedNote(reported),
33
46
  });
34
47
  }
@@ -45,6 +58,31 @@ function retainedSeal(stored) {
45
58
  return { operation: stored.operation, blocks: stored.blocks };
46
59
  }
47
60
 
61
+ function retainedFailure(stored) {
62
+ if (stored === null) return null;
63
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
64
+ if (typeof stored.code !== "string" || stored.code.length === 0) return undefined;
65
+ if (stored.detail !== null && typeof stored.detail !== "string") return undefined;
66
+ return { code: stored.code, detail: stored.detail };
67
+ }
68
+
69
+ function retainedStop(stored) {
70
+ if (stored === null || stored === undefined) return null;
71
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
72
+ const flags = ["turn_completed", "capacity_exhausted", "provider_refused", "continuable", "text_accepted"];
73
+ if (flags.some((flag) => typeof stored[flag] !== "boolean")) return undefined;
74
+ const failure = retainedFailure(stored.failure);
75
+ if (failure === undefined) return undefined;
76
+ return {
77
+ turnCompleted: stored.turn_completed,
78
+ capacityExhausted: stored.capacity_exhausted,
79
+ providerRefused: stored.provider_refused,
80
+ continuable: stored.continuable,
81
+ failure,
82
+ textAccepted: stored.text_accepted,
83
+ };
84
+ }
85
+
48
86
  function completeBinding(directory, stored) {
49
87
  if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
50
88
  const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
@@ -54,6 +92,8 @@ function completeBinding(directory, stored) {
54
92
  if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
55
93
  const sealed = retainedSeal(stored.sealed);
56
94
  if (sealed === undefined) return null;
95
+ const stopped = retainedStop(stored.stopped);
96
+ if (stopped === undefined) return null;
57
97
  const reported = retainedReport(stored.reported);
58
98
  if (reported === undefined) return null;
59
99
  return {
@@ -65,6 +105,7 @@ function completeBinding(directory, stored) {
65
105
  conversation: stored.conversation === true,
66
106
  ownerStop: stored.owner_stop,
67
107
  sealed,
108
+ stopped,
68
109
  reported,
69
110
  };
70
111
  }
package/src/bridge.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  import { operationId } from "./operation.js";
19
19
  import { houseFailed } from "./refusal.js";
20
20
 
21
- function bridgedFrame(request, operation) {
21
+ export function bridgedFrame(request, operation) {
22
22
  if (typeof request.method !== "string") return request;
23
23
  const params = typeof request.params === "object" && request.params !== null ? request.params : {};
24
24
  const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
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-26-house-agent-kinds";
3
+ export const KIT_CONTRACT_VERSION = "1";
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;
@@ -40,6 +40,10 @@ export function runOutcomePath(run) {
40
40
  return `/kit/runs/${encodeURIComponent(run)}/outcome`;
41
41
  }
42
42
 
43
+ export function runRequestPath(run) {
44
+ return `/kit/runs/${encodeURIComponent(run)}/request`;
45
+ }
46
+
43
47
  export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
44
48
  export const KIT_SESSIONS_REPORT_MAX = 256;
45
49
 
@@ -195,7 +199,7 @@ export const ADAPTER_CONTRACTS = Object.freeze({
195
199
  guidedLogin: CLAUDE_PASTED_CODE,
196
200
  identity: Object.freeze({ agentInfo: "@agentclientprotocol/claude-agent-acp" }),
197
201
  nativeQuestion: null,
198
- sessionLoad: false,
202
+ sessionLoad: true,
199
203
  sessionConfig: Object.freeze({ model: "model", effort: "effort" }),
200
204
  capabilities: Object.freeze({
201
205
  field_kinds: ELICITATION_FIELD_KINDS,
@@ -288,9 +292,23 @@ export const KIT_DOOR_DISCOVER_PATH = "/kit/door/discover";
288
292
  export const KIT_DOOR_BOOTSTRAP_PATH = "/kit/door/bootstrap";
289
293
  export const KIT_DOOR_ENUMERATE_PATH = "/kit/door/enumerate";
290
294
  export const KIT_DOOR_BATCH_PATH = "/kit/door/batch";
291
- export const EXPLICIT_SYNC_VERSION = "explicit-sync/1";
295
+ export const KIT_DOOR_REVISIONS_PATH = "/kit/door/revisions";
296
+ export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
292
297
  export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
293
298
 
299
+ export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
300
+ export const WORKING_COPY_VERSION = "working-copy-state/3";
301
+ export const WORKING_COPIES_HOME = "working-copies";
302
+ export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
303
+ export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
304
+ export const WORKING_COPY_RECEIVED_REF = "refs/house/received";
305
+ export const WORKING_COPY_BRANCH = "main";
306
+ export const ROOM_REFERENCE = /^r_[0-9a-z]+\.[0-9a-z]$/;
307
+ export const WORKING_COPY_PUSH_METHOD = "house/git/push";
308
+ export const WORKING_COPY_INDEX_METHOD = "house/git/index";
309
+ export const WORKING_COPY_PUSH_HELP =
310
+ "Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
311
+
294
312
  export const MIGRATOR_CONTRACT = "kit-migrator/1";
295
313
  export const MIGRATOR_CATEGORY = "migrator";
296
314
  export const MIGRATOR_MANIFEST = "migrator.json";
@@ -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
+ }
package/src/host.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile, statfs } from "node:fs/promises";
2
2
  import { readConfiguration } from "./configuration.js";
3
3
  import { HOST_CPU_SAMPLE_MS } from "./contract.js";
4
- import { openSession, reportHostCapacity } from "./house.js";
4
+ import { maintainControl, openSession, reportHostCapacity } from "./house.js";
5
5
  import { KitRefusal } from "./refusal.js";
6
6
 
7
7
  const PROC_STAT = "/proc/stat";
@@ -116,6 +116,10 @@ export async function reportHost(options, dependencies = {}) {
116
116
  ? observed
117
117
  : { ...observed, maxima: declaredMaxima(configuration.host.maxima) };
118
118
  const session = await openSession(options, dependencies);
119
- await reportHostCapacity(session, declaration);
119
+ await maintainControl(
120
+ session,
121
+ (signal) => reportHostCapacity(session, declaration, signal),
122
+ options.signal,
123
+ );
120
124
  return declaration;
121
125
  }
package/src/house-main.js CHANGED
@@ -1,9 +1,19 @@
1
- import { resolve } from "node:path";
2
- import { BRIDGE_NONCE_ENVIRONMENT, BRIDGE_SOCKET_ENVIRONMENT } from "./contract.js";
3
- import { bridgeCall } from "./mcp.js";
4
- import { KitRefusal } from "./refusal.js";
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import {
4
+ BRIDGE_NONCE_ENVIRONMENT,
5
+ BRIDGE_SOCKET_ENVIRONMENT,
6
+ WORKING_COPY_PUSH_HELP,
7
+ WORKING_COPY_PUSH_METHOD,
8
+ } from "./contract.js";
9
+ import { storedHouse } from "./login.js";
10
+ import { answerText, bridgeCall } from "./mcp.js";
11
+ import { formatCommandError, KitRefusal } from "./refusal.js";
12
+ import { ownerPush } from "./working-copy.js";
5
13
 
6
14
  const HELP = ["--help", "-h"];
15
+ const OWNER = "--owner";
16
+ const GIT_PUSH_USAGE = `usage: house git push [commit] [${OWNER}]`;
7
17
  const usage = (verb) => `usage: house ${verb} ['<arguments as one JSON object>' | - for standard input]`;
8
18
 
9
19
  async function answered(method, params) {
@@ -49,10 +59,59 @@ function located(verb, args) {
49
59
  return args;
50
60
  }
51
61
 
62
+ function printed(result) {
63
+ const text = answerText(result);
64
+ return result?.isError === true ? { err: text, status: 1 } : { out: text };
65
+ }
66
+
67
+ async function ownerPushed(request) {
68
+ const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
69
+ const cancellation = new AbortController();
70
+ const cancel = () => cancellation.abort();
71
+ process.once("SIGINT", cancel);
72
+ process.once("SIGTERM", cancel);
73
+ try {
74
+ const pushed = await ownerPush(
75
+ { house: await storedHouse(directory), directory, signal: cancellation.signal },
76
+ request,
77
+ );
78
+ return pushed.refused ? { err: pushed.text, status: 1 } : { out: pushed.text };
79
+ } catch (refusal) {
80
+ return { err: formatCommandError(refusal), status: 1 };
81
+ } finally {
82
+ process.removeListener("SIGINT", cancel);
83
+ process.removeListener("SIGTERM", cancel);
84
+ }
85
+ }
86
+
87
+ async function gitPush([action, ...rest]) {
88
+ if (action !== "push") throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
89
+ if (rest.some((argument) => HELP.includes(argument))) return { out: `${GIT_PUSH_USAGE}\n\n${WORKING_COPY_PUSH_HELP}` };
90
+ const owner = rest.includes(OWNER);
91
+ const named = rest.filter((argument) => argument !== OWNER);
92
+ if (named.length > 1 || named.some((argument) => argument.startsWith("-"))) {
93
+ throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
94
+ }
95
+ const request = { cwd: process.cwd(), commit: named[0] ?? "HEAD" };
96
+ if (process.env[BRIDGE_SOCKET_ENVIRONMENT] === undefined) {
97
+ if (!owner) throw new KitRefusal("kit_bridge_unavailable");
98
+ return ownerPushed(request);
99
+ }
100
+ if (owner) throw new KitRefusal("kit_house_arguments_invalid", `${OWNER} is only for use outside an Agent Session`);
101
+ return printed(await answered(WORKING_COPY_PUSH_METHOD, request));
102
+ }
103
+
52
104
  async function house([verb, argument, ...rest]) {
105
+ if (verb === "git") return gitPush([argument, ...rest].filter((part) => part !== undefined));
53
106
  if (verb === undefined || HELP.includes(verb)) {
54
107
  const tools = await listed();
55
- return { out: [usage("<verb>"), ...tools.map((tool) => `${tool.name}\n${tool.description}`)].join("\n\n") };
108
+ return {
109
+ out: [
110
+ usage("<verb>"),
111
+ ...tools.map((tool) => `${tool.name}\n${tool.description}`),
112
+ `git push [commit]\n${WORKING_COPY_PUSH_HELP}`,
113
+ ].join("\n\n"),
114
+ };
56
115
  }
57
116
  if (HELP.includes(argument)) {
58
117
  const tool = (await listed()).find((one) => one.name === verb);
@@ -61,9 +120,7 @@ async function house([verb, argument, ...rest]) {
61
120
  }
62
121
  if (rest.length > 0) throw new KitRefusal("kit_house_arguments_invalid", usage(verb));
63
122
  const args = located(verb, await argumentsOf(verb, argument));
64
- const result = await answered("tools/call", { name: verb, arguments: args });
65
- const text = (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
66
- return result?.isError === true ? { err: text, status: 1 } : { out: text };
123
+ return printed(await answered("tools/call", { name: verb, arguments: args }));
67
124
  }
68
125
 
69
126
  house(process.argv.slice(2)).then(