@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/bootstrap.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.1.0-alpha.41",
4
+ "hosts": {
5
+ "linux": {
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.41/connect-linux.sh)",
7
+ "asset": {
8
+ "name": "connect-linux.sh",
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.41/connect-linux.sh"
10
+ },
11
+ "workspace": {
12
+ "argument": "--workspace",
13
+ "quoting": "posix"
14
+ }
15
+ },
16
+ "windows": {
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.41/connect-windows.ps1)))",
18
+ "asset": {
19
+ "name": "connect-windows.ps1",
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.41/connect-windows.ps1"
21
+ },
22
+ "workspace": {
23
+ "argument": "--workspace",
24
+ "quoting": "powershell"
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,104 @@
1
+ # Configuration
2
+
3
+ `kit.json` in Kit home is the one configuration file. Every section below is
4
+ required; a file that omits one is refused.
5
+
6
+ ```json
7
+ {
8
+ "availability": "continuous",
9
+ "execution_authority": "allow",
10
+ "migrators": [],
11
+ "instances": [
12
+ {
13
+ "id": "acme",
14
+ "preset": "custom",
15
+ "services": [
16
+ {
17
+ "service": "notes",
18
+ "source_ref": "src_acme_notes",
19
+ "rooms": ["work-room"],
20
+ "backfill": { "days": 30 },
21
+ "connector": {
22
+ "command": ["/usr/local/bin/acme-export", "--ndjson"],
23
+ "timeout_ms": 120000,
24
+ "environment": { "ACME_REGION": "eu" },
25
+ "secrets": {
26
+ "ACME_TOKEN": { "environment": "ACME_TOKEN_SOURCE" },
27
+ "ACME_KEY": { "file": "/run/secrets/acme-key" }
28
+ }
29
+ }
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "clients": [
35
+ {
36
+ "kind": "claude-code",
37
+ "settings": "/home/ada/.claude/settings.json"
38
+ }
39
+ ],
40
+ "host": null,
41
+ "skills": true
42
+ }
43
+ ```
44
+
45
+ - `availability`: `"continuous"` for a computer that stays online,
46
+ `"intermittent"` for a laptop.
47
+ - `execution_authority`: `"allow"` or `"refuse"`, this computer's answer to
48
+ every permission request an agent makes.
49
+ - `instances`: your own sources, each with a `custom` connector (below).
50
+ - `clients`: agent clients outside Kit whose settings get House's session-start
51
+ hook. `claude-code` is the one supported kind; declare `[]` for none.
52
+ - `migrators`: `[]` unless you run a migrator.
53
+ - `host`: `null` unless this computer reports its capacity to House.
54
+ - `skills`: `true` accepts the Skill set, `false` declines it. The connect
55
+ command sets it.
56
+
57
+ Agents and agent CLIs are chosen in House, not here. Send your changes to
58
+ House with:
59
+
60
+ ```sh
61
+ kit apply
62
+ ```
63
+
64
+ ## Secrets
65
+
66
+ Never write a secret into `kit.json`. Under `secrets`, name an environment
67
+ variable (`{ "environment": "NAME" }`) or a file (`{ "file": "/path" }`); Kit
68
+ passes its value to the connector only.
69
+
70
+ ## Custom connectors
71
+
72
+ A connector is your own command. Kit runs it without a shell, with only `PATH`,
73
+ `environment` and the resolved secrets, and never gives it a House credential.
74
+ It must finish within `timeout_ms` (at most 120000).
75
+
76
+ Kit writes one request line to its standard input:
77
+
78
+ ```json
79
+ { "contract": "2026-08-22", "instance": "acme", "cursor": null, "limit": 100 }
80
+ ```
81
+
82
+ The command prints NDJSON: `item` lines, then at most one `cursor` line.
83
+ `cursor` in the next request is the last one it printed.
84
+
85
+ ```
86
+ {"type":"item","namespace":"acme/notes","native_object_id":"note-91","native_version":"7","observed_at":"2026-08-20T09:15:00.000Z","body":"the note","author":"ada","url":"https://acme.example/notes/91","attachments":[{"url":"https://acme.example/files/1","name":"plan.pdf"}]}
87
+ {"type":"cursor","cursor":"page-2"}
88
+ ```
89
+
90
+ `namespace`, `native_object_id`, `observed_at` and `body` are required. An
91
+ attachment is a `url`, or a local `file` with its exact `path`, `bytes`,
92
+ `sha256` and optional `media_type`. A malformed line, a non-zero exit or a
93
+ timeout rejects the whole run and nothing is delivered.
94
+
95
+ `backfill` bounds the first capture: `{ "days": 30 }`, `{ "count": 500 }` or
96
+ `{ "all": true }`.
97
+
98
+ ```sh
99
+ kit source capture acme # capture every service of an instance now
100
+ kit source capture acme notes # one service
101
+ kit source status acme # health, cursor, gaps and unsaved files
102
+ kit source retry-files acme # save files a capture could not save
103
+ kit source reset acme notes # start again from the backfill boundary
104
+ ```
package/docs/google.md ADDED
@@ -0,0 +1,33 @@
1
+ # Google sources
2
+
3
+ You add Gmail, Calendar and Drive sources in the House App: an account name you
4
+ choose, the service, the Rooms it goes to, and how much history the first
5
+ capture takes. Nothing is configured on this computer.
6
+
7
+ Each service captures, read-only:
8
+
9
+ - Gmail: every message except Spam and Trash.
10
+ - Calendar: the account's primary calendar.
11
+ - Drive: the files the account owns.
12
+
13
+ ## Signing in
14
+
15
+ Kit ships no Google OAuth client. Each account signs in with your own:
16
+
17
+ 1. In your Google Cloud project, create an OAuth client of type **Desktop app**
18
+ and enable the API of each service you use.
19
+ 2. When House asks, enter the client id and secret on its page.
20
+ 3. Approve Google's consent. Google then sends the browser to a
21
+ `http://127.0.0.1:<port>/` address that does not load; copy that whole
22
+ address and paste it into the House page.
23
+
24
+ Kit then captures each signed-in source every 15 minutes.
25
+
26
+ ```sh
27
+ kit source capture work # capture every source of the account now
28
+ kit source capture work gmail # one service
29
+ kit source status work
30
+ ```
31
+
32
+ If `kit source status` reports `api_disabled`, enable the API it names in your
33
+ Google Cloud project.
package/docs/linux.md ADDED
@@ -0,0 +1,77 @@
1
+ # Linux
2
+
3
+ Run the Linux command from the [README](../README.md#connect-a-computer) in a
4
+ terminal. It needs `bash`, `curl` and Docker Engine.
5
+
6
+ ## Docker Engine
7
+
8
+ - A Docker Engine that already answers is used as it is, on any distribution.
9
+ - On Ubuntu 22.04, 24.04 or 26.04 LTS, anything missing is installed from
10
+ Docker's own apt repository through `sudo`, which asks for your password.
11
+ The command starts the `docker` service and adds you to the `docker` group.
12
+ - Ubuntu packages that conflict with Docker's (`docker.io`, `docker-compose`,
13
+ `docker-compose-v2`, `docker-doc`, `docker-buildx`, `podman-docker`,
14
+ `containerd`, `runc`) are not removed for you: remove them and run again.
15
+ - On any other distribution, install Docker Engine first.
16
+
17
+ A new `docker` group membership applies from your next sign-in. If a later run
18
+ says so, sign out and back in, then run the command again.
19
+
20
+ ## What it creates
21
+
22
+ - `~/.house-kit`: Kit home, with your credential and `kit.json`.
23
+ - `~/AgentsHouse`: your workspace, mounted in the container as
24
+ `/agents/house`. Pass `--workspace /absolute/path` to use another directory.
25
+
26
+ Both are readable only by you. The credential is never printed.
27
+
28
+ ## Updating
29
+
30
+ Run a newer release's command. While an Agent session is still running, the
31
+ update names it and leaves Kit as it is; run the command again once it ends.
32
+ Your credential, Kit home and workspace are kept.
33
+
34
+ ## Pushing a working copy
35
+
36
+ From a directory under your workspace, such as a Room copy under
37
+ `working-copies/rooms/`, run:
38
+
39
+ ```sh
40
+ bash <(curl -fsSL …/connect-linux.sh) house git push --owner
41
+ ```
42
+
43
+ ## A House on this computer
44
+
45
+ `--house http://127.0.0.1:<port>` or `--house http://localhost:<port>` runs Kit
46
+ on the host network so it can reach a House served on this computer.
47
+
48
+ ## An Always-on server that hosts containers
49
+
50
+ An Always-on Ubuntu server that will run persistent container Environments
51
+ connects natively instead of in a container. Run the command with `--linux`:
52
+
53
+ ```sh
54
+ bash <(curl -fsSL …/connect-linux.sh) --linux
55
+ ```
56
+
57
+ It needs Ubuntu 26.04 LTS on `amd64`; any other host is refused. Every other
58
+ computer takes the ordinary command. It sets up:
59
+
60
+ - Docker Engine, as above, and `git` and `tmux` from Ubuntu's apt repository.
61
+ - `/opt/house-kit`: House Kit and the Node.js it runs on, copied from the
62
+ release's image and owned by root.
63
+ - `~/.house-kit`: Kit home, readable only by you.
64
+ - `/agents/house`: your Agents' workspace, readable only by you.
65
+ - `house-kit.service`: the systemd service that runs Kit as you and starts it
66
+ with the server.
67
+
68
+ The server connects as a continuous Environment. Put `--linux` before `kit` or
69
+ `house` to run a Kit command, as in
70
+ `bash <(curl -fsSL …/connect-linux.sh) --linux kit apply`, and run a newer
71
+ release's command with `--linux` to update it, as above.
72
+
73
+ The same run updates the container Environments the server hosts to that
74
+ release. A running one waits, and is named, until its own Agent sessions end;
75
+ run the command again then. A stopped one is updated and stays stopped. Each
76
+ keeps its files, sign-ins and CPU, memory and disk limits; only what was
77
+ installed outside its workspace and Kit home is not kept.
package/docs/macos.md ADDED
@@ -0,0 +1,26 @@
1
+ # macOS
2
+
3
+ Run the Linux command from the [README](../README.md#connect-a-computer) in
4
+ Terminal. It works on Apple silicon and Intel Macs, including from a terminal
5
+ running under Rosetta.
6
+
7
+ ## Docker Desktop
8
+
9
+ Kit on a Mac needs Docker Desktop.
10
+
11
+ - A Docker Desktop that already answers is used as it is.
12
+ - Other engines, such as Colima or OrbStack, are refused.
13
+ - Without `/Applications/Docker.app`, the command downloads Docker Desktop for
14
+ your chip and installs it through `sudo`, which asks for your password.
15
+ - The command opens Docker Desktop and waits up to five minutes while you
16
+ accept its terms or finish what its window asks. If it is still not ready,
17
+ run the command again.
18
+
19
+ ## Notes
20
+
21
+ - A `--workspace` directory must be one Docker Desktop shares, such as one
22
+ under `/Users`.
23
+ - A House on this computer (`--house http://127.0.0.1:<port>`) needs host
24
+ networking enabled in Docker Desktop's settings.
25
+
26
+ Everything else works as on [Linux](linux.md).
@@ -0,0 +1,29 @@
1
+ # Telegram bot
2
+
3
+ Kit can connect your own Telegram bot so your House conversations appear there.
4
+ The bot token stays on this computer; House never receives it.
5
+
6
+ 1. Create a bot with BotFather and turn on its Threaded Mode.
7
+ 2. Send the bot one ordinary private message outside any topic, so Kit can
8
+ tell the bot is yours.
9
+ 3. Connect it, entering the token at the hidden prompt:
10
+
11
+ ```sh
12
+ kit telegram connect
13
+ ```
14
+
15
+ 4. Serve it; this keeps running until you stop it:
16
+
17
+ ```sh
18
+ kit telegram serve
19
+ ```
20
+
21
+ To release the bot:
22
+
23
+ ```sh
24
+ kit telegram disconnect
25
+ ```
26
+
27
+ Run each command through the connect command as the
28
+ [README](../README.md#run-a-kit-command) shows. Only one computer can serve a
29
+ given bot.
@@ -0,0 +1,37 @@
1
+ # Windows
2
+
3
+ Run the Windows command from the [README](../README.md#connect-a-computer) in
4
+ the 64-bit Windows PowerShell. It needs Windows 11 23H2 (build 22631) or later
5
+ on `amd64`. Windows Server, Windows 10 and Windows on ARM are refused.
6
+
7
+ ## WSL 2 and Docker Desktop
8
+
9
+ - A Docker Desktop that already runs Linux containers on WSL 2 is used as it is.
10
+ - Without WSL, the command runs `wsl --install --no-distribution`. Approve the
11
+ administrator prompt, restart Windows, and run the command again. An old WSL
12
+ is updated the same way.
13
+ - Without Docker Desktop, the command downloads it and installs it for your
14
+ account only, without administrator rights.
15
+ - The command starts Docker Desktop and waits up to five minutes while you
16
+ accept its terms or finish what its window asks. If it is still not ready,
17
+ run the command again.
18
+ - Windows containers, the Hyper-V engine and other Docker engines are refused.
19
+
20
+ ## What it creates
21
+
22
+ - The Docker volume `house-kit-home`: Kit home, with your credential and
23
+ `kit.json`, mounted in the container as `/kit-home`. It lives on Docker
24
+ Desktop's own WSL 2 disk, not on your Windows drive. Docker Desktop's
25
+ Volumes view shows its files; double-click `kit.json` there to edit it.
26
+ - `%USERPROFILE%\AgentsHouse`: your workspace. Pass
27
+ `--workspace 'D:\Agents'` to use another directory. It is restricted to your
28
+ Windows account, and a path containing a comma is refused.
29
+
30
+ ## Notes
31
+
32
+ - Append `kit` and a command to run it, as on [Linux](linux.md). Each run
33
+ leaves its exit status in `$LASTEXITCODE`.
34
+ - A command whose output is redirected also writes its prompts and errors there.
35
+ - A House on this computer needs host networking enabled in Docker Desktop's
36
+ settings.
37
+ - This bootstrap has not yet been run on a real Windows computer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.41",
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
  },
@@ -1,7 +1,15 @@
1
- import { COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS, MCP_PROTOCOL_VERSION } from "./contract.js";
2
- import { bridgeCall } from "./mcp.js";
1
+ import { readFileSync } from "node:fs";
2
+ import {
3
+ COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
4
+ HOW_WE_WORK_PATH,
5
+ MCP_PROTOCOL_VERSION,
6
+ SHELL_RENDER,
7
+ } from "./contract.js";
8
+ import { answerText, bridgeCall, editRefusal } from "./mcp.js";
3
9
  import { KitRefusal } from "./refusal.js";
4
10
 
11
+ const HOW_WE_WORK_TEXT = readFileSync(new URL("how-we-work.md", import.meta.url), "utf8");
12
+
5
13
  function readableDiscovery(discovery) {
6
14
  const identity = discovery?._meta?.["io.modelcontextprotocol/serverInfo"];
7
15
  return Array.isArray(discovery?.supportedVersions) &&
@@ -30,15 +38,87 @@ export async function commandHouseInstructions(bridge) {
30
38
  return frame.result.instructions;
31
39
  }
32
40
 
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");
41
+ const CUT_NOTICE = "shell: output_cut ";
42
+ const CONTINUATION = "; continue with: ";
43
+ const BYTE_CONTINUATION = /\| tail -c \+[1-9][0-9]*$/u;
44
+
45
+ function shellAnswer(text) {
46
+ const lines = text.split("\n");
47
+ if (!lines[0].startsWith(`protocol: render=${SHELL_RENDER} `)) return null;
48
+ const exit = Number(lines[2].slice("exit: ".length));
49
+ const truncation = lines[3].slice("truncation: ".length);
50
+ let at = lines[5]?.startsWith("authority: ") ? 6 : 5;
51
+ if (exit === 0 && truncation === "none") {
52
+ return { exit, truncation, stderr: [], stdout: lines.slice(at).join("\n"), continuation: null };
53
+ }
54
+ const stderr = [];
55
+ let continuation = null;
56
+ while (continuation === null && lines[at]?.startsWith("stderr: ")) {
57
+ const line = lines[at++].slice("stderr: ".length);
58
+ stderr.push(line);
59
+ const separator = line.indexOf(CONTINUATION);
60
+ if (line.startsWith(CUT_NOTICE) && separator !== -1) {
61
+ continuation = line.slice(separator + CONTINUATION.length);
62
+ }
63
+ }
64
+ return { exit, truncation, stderr, stdout: lines.slice(at).join("\n"), continuation };
65
+ }
66
+
67
+ async function howWeWorkReply(bridge, command, first) {
68
+ const frame = await bridgeCall(bridge, {
69
+ method: "tools/call",
70
+ params: { name: "shell", arguments: { command } },
71
+ });
72
+ if (frame.error !== undefined) {
73
+ throw new KitRefusal("kit_how_we_work_unreadable", frame.error.message ?? null);
74
+ }
75
+ const text = answerText(frame.result);
76
+ if (frame.result?.isError === true) {
77
+ if (first && text.split(":")[0] === "room_not_found") return null;
78
+ throw new KitRefusal("kit_how_we_work_unreadable", text);
79
+ }
80
+ const reply = shellAnswer(text);
81
+ if (reply === null) throw new KitRefusal("kit_how_we_work_unreadable", text);
82
+ if (reply.exit !== 0) {
83
+ if (first && reply.stderr.length === 1 && reply.stderr[0].split(" ")[1] === "path_not_found") return null;
84
+ throw new KitRefusal("kit_how_we_work_unreadable", reply.stderr.join("\n"));
85
+ }
86
+ if (reply.truncation !== "none" && reply.continuation === null) {
87
+ throw new KitRefusal("kit_how_we_work_unreadable", reply.stderr.join("\n"));
88
+ }
89
+ return reply;
90
+ }
91
+
92
+ export async function commandHouseHowWeWork(bridge) {
93
+ let document = "";
94
+ let command = `cat ${HOW_WE_WORK_PATH}`;
95
+ let first = true;
96
+ while (command !== null) {
97
+ const reply = await howWeWorkReply(bridge, command, first);
98
+ if (reply === null) return "";
99
+ document += reply.stdout;
100
+ command = reply.continuation;
101
+ if (command !== null && !BYTE_CONTINUATION.test(command)) document += "\n";
102
+ first = false;
103
+ }
104
+ return document;
105
+ }
106
+
107
+ export async function commandHouseCreateHowWeWork(bridge) {
108
+ const frame = await bridgeCall(bridge, {
109
+ method: "tools/call",
110
+ params: {
111
+ name: "edit",
112
+ arguments: { changes: [{ op: "create", path: HOW_WE_WORK_PATH, content: HOW_WE_WORK_TEXT }] },
113
+ },
114
+ });
115
+ if (frame.error !== undefined) {
116
+ throw new KitRefusal("kit_how_we_work_unwritable", frame.error.message ?? null);
43
117
  }
118
+ if (frame.result?.isError !== true) return true;
119
+ const text = answerText(frame.result);
120
+ const { code, conflicts } = editRefusal(text);
121
+ if (code === "room_not_found" || code === "operation_denied") return false;
122
+ if (code === "edit_conflict" && conflicts?.some((conflict) => conflict.code === "path_exists")) return true;
123
+ throw new KitRefusal("kit_how_we_work_unwritable", text);
44
124
  }
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
@@ -1,5 +1,6 @@
1
1
  import { readdir, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { finalPlan, planSnapshot } from "./plan.js";
3
4
  import { KitRefusal } from "./refusal.js";
4
5
  import { readNote, writeNote } from "./state.js";
5
6
  import { reportedNote, retainedReport } from "./terminal.js";
@@ -15,6 +16,18 @@ function bindingPath(directory, tmux) {
15
16
  return join(bindingDirectory(directory), `${tmux}.json`);
16
17
  }
17
18
 
19
+ function stoppedNote(stopped) {
20
+ if (stopped === null || stopped === undefined) return null;
21
+ return {
22
+ turn_completed: stopped.turnCompleted,
23
+ capacity_exhausted: stopped.capacityExhausted,
24
+ provider_refused: stopped.providerRefused,
25
+ continuable: stopped.continuable,
26
+ failure: stopped.failure,
27
+ text_accepted: stopped.textAccepted,
28
+ };
29
+ }
30
+
18
31
  export async function writeBinding(directory, binding) {
19
32
  const reported = binding.reported ?? null;
20
33
  await writeNote(bindingPath(directory, binding.tmux), {
@@ -29,7 +42,9 @@ export async function writeBinding(directory, binding) {
29
42
  sealed: binding.sealed === null || binding.sealed === undefined
30
43
  ? null
31
44
  : { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
45
+ stopped: stoppedNote(binding.stopped),
32
46
  reported: reported === null ? null : reportedNote(reported),
47
+ plan: binding.plan ?? null,
33
48
  });
34
49
  }
35
50
 
@@ -45,6 +60,37 @@ function retainedSeal(stored) {
45
60
  return { operation: stored.operation, blocks: stored.blocks };
46
61
  }
47
62
 
63
+ function retainedPlan(stored) {
64
+ if (stored === null || stored === undefined) return null;
65
+ const plan = finalPlan(planSnapshot(stored));
66
+ return plan === null || JSON.stringify(plan) !== JSON.stringify(stored) ? undefined : plan;
67
+ }
68
+
69
+ function retainedFailure(stored) {
70
+ if (stored === null) return null;
71
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
72
+ if (typeof stored.code !== "string" || stored.code.length === 0) return undefined;
73
+ if (stored.detail !== null && typeof stored.detail !== "string") return undefined;
74
+ return { code: stored.code, detail: stored.detail };
75
+ }
76
+
77
+ function retainedStop(stored) {
78
+ if (stored === null || stored === undefined) return null;
79
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
80
+ const flags = ["turn_completed", "capacity_exhausted", "provider_refused", "continuable", "text_accepted"];
81
+ if (flags.some((flag) => typeof stored[flag] !== "boolean")) return undefined;
82
+ const failure = retainedFailure(stored.failure);
83
+ if (failure === undefined) return undefined;
84
+ return {
85
+ turnCompleted: stored.turn_completed,
86
+ capacityExhausted: stored.capacity_exhausted,
87
+ providerRefused: stored.provider_refused,
88
+ continuable: stored.continuable,
89
+ failure,
90
+ textAccepted: stored.text_accepted,
91
+ };
92
+ }
93
+
48
94
  function completeBinding(directory, stored) {
49
95
  if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
50
96
  const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
@@ -54,8 +100,12 @@ function completeBinding(directory, stored) {
54
100
  if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
55
101
  const sealed = retainedSeal(stored.sealed);
56
102
  if (sealed === undefined) return null;
103
+ const stopped = retainedStop(stored.stopped);
104
+ if (stopped === undefined) return null;
57
105
  const reported = retainedReport(stored.reported);
58
106
  if (reported === undefined) return null;
107
+ const plan = retainedPlan(stored.plan);
108
+ if (plan === undefined) return null;
59
109
  return {
60
110
  run: stored.run,
61
111
  session: stored.session,
@@ -65,7 +115,9 @@ function completeBinding(directory, stored) {
65
115
  conversation: stored.conversation === true,
66
116
  ownerStop: stored.owner_stop,
67
117
  sealed,
118
+ stopped,
68
119
  reported,
120
+ plan,
69
121
  };
70
122
  }
71
123