@karma42/agent 0.1.0-beta.11 → 0.1.0-beta.12
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/README.md +15 -13
- package/package.json +1 -1
- package/runtime/control-v2.mjs +6 -6
- package/runtime/manifest.json +2 -3
- package/runtime/project-tools.json +1 -1
- package/skills/karma42/SKILL.md +22 -4
- package/src/cli.mjs +3 -15
- package/src/layout.mjs +20 -0
- package/src/operation-lock.mjs +46 -0
- package/src/owner-client.mjs +143 -19
- package/src/reusable-stdio.mjs +77 -105
- package/src/system.mjs +19 -19
- package/src/version.mjs +3 -1
- package/src/worker-runtime.mjs +4 -2
- package/src/browser-batch.mjs +0 -130
- package/src/browser-pairing.mjs +0 -140
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Karma42 MCP connector
|
|
2
2
|
|
|
3
|
-
`0.1.0-beta.
|
|
3
|
+
`0.1.0-beta.12` is a **beta release**. It connects an existing MCP-capable AI client to a Karma42 project. Registration and current-chat mode do not select, download or run a model. An explicitly approved local worker is a separate work mode. An MCP-capable client using DeepSeek or another model can use the same transport if that client supports local stdio MCP and the required project tools.
|
|
4
4
|
|
|
5
5
|
## First-time setup
|
|
6
6
|
|
|
7
7
|
Once per computer and client, print its registration instructions:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx --ignore-scripts @karma42/agent@0.1.0-beta.
|
|
10
|
+
npx --ignore-scripts @karma42/agent@0.1.0-beta.12 mcp-config --client claude
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Use `--client codex` for Codex, or `--client other` for the generic stdio configuration. **Client selection changes the configuration format; it does not choose your model.**
|
|
@@ -30,9 +30,13 @@ Public discovery defaults to `https://karma42.ai`. For local development, explic
|
|
|
30
30
|
|
|
31
31
|
## Project-agent onboarding
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Set up a computer once: copy the setup prompt from My agents on [Karma42](https://karma42.ai) into the conversation. If the connector is not loaded, the prompt has the agent add it and asks you to restart and paste again. The agent calls `connect_owner_account`, which opens the approval page in your default browser and returns the link and request check for when it cannot (remote shells, CI, no display, or `KARMA42_NO_BROWSER=1`). Approving lets this computer add and connect your own agents in any project you belong to for 90 days, until you end it on My agents.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Then ask for an agent, for example “add an agent to my project Friends Game”. `add_project_agent` finds the project by its name, creates the agent when needed, gives it its own folder under `~/karma42/projects/<project>/<agent>-<id>` and connects it with no browser step. An agent this computer already connected is picked up again with its saved credential. `set_agent_work_settings` saves the limits you choose when the agent asks before starting.
|
|
36
|
+
|
|
37
|
+
`connect_project_agent` connects one existing agent by its exact ID and name. It reuses the connection this computer saved, or connects the agent through this computer’s one-time approval; there is no approval page per agent. On a computer that is not approved yet it says to call `connect_owner_account` first. My agents retains connection status and revocation controls.
|
|
38
|
+
|
|
39
|
+
`get_project` with the returned connection_ref verifies identity and reads context. Keep the same reference on every subsequent project tool call. Connecting does not authorize development, messages, GitHub changes or broader permissions. Your client controls those actions and model usage.
|
|
36
40
|
|
|
37
41
|
The connector supports separate processes and hosts that share one MCP process across conversations. Each agent/project has an immutable binding with an opaque connection_ref. Every project tool requires this exact reference; there is no global active project or default target. Different bindings can work concurrently. Duplicate active identities retain exclusive run fences without automatic takeover. An explicitly selected approved connection can be reused for its matching identity and reported folder.
|
|
38
42
|
|
|
@@ -58,14 +62,14 @@ Worker lifecycle and adapter-session tests use fake models and clocks. A control
|
|
|
58
62
|
|
|
59
63
|
The client runs `mcp-serve`, which serves JSON-RPC over stdin/stdout. Registration and connection do not launch a model; worker execution requires its own explicit approval. Node 22+ is required. `disconnect_project_agent` releases only the specified binding; EOF or transport termination releases all bindings in that process; completed external actions are not undone. Run fences, membership, revocation and project-state checks remain enforced by the service.
|
|
60
64
|
|
|
61
|
-
Private records use `~/.karma42/mcp` or `--state-dir /absolute/private/directory`. Storage must be private and outside the working folder. Credentials and polling secrets remain in private files; never put them in chat, arguments, URLs or client configuration.
|
|
65
|
+
Private records use `~/.karma42/mcp` or `--state-dir /absolute/private/directory`. Storage must be private and outside the working folder. Credentials and polling secrets remain in private files; never put them in chat, arguments, URLs or client configuration. The pending approval of a computer can be resumed; cancellation or expiry preserves what is already saved. A connected agent keeps its connection; nothing is replaced.
|
|
62
66
|
|
|
63
67
|
This source contains only the current MCP connector. Old npm releases remain published independently; this cleanup neither migrates their private records nor deletes production data or interrupts existing sessions. Native client and operating-system acceptance remain separate release checks. Configuration and protocol tests do not establish that every MCP client supports dynamic setup, reload or continuation.
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
## Human owner management
|
|
67
71
|
|
|
68
|
-
Human management is separate from project-agent credentials. `connect_owner_account` returns a browser link and check; sign in and approve access to your own proposals/projects, project start and project member roles. `finish_owner_account_connection` returns the explicit `owner_ref`. The local secret stays in private storage; the server stores its hash. Access lasts
|
|
72
|
+
Human management is separate from project-agent credentials. `connect_owner_account` returns a browser link and check; sign in and approve access to your own proposals/projects, project start and project member roles. `finish_owner_account_connection` returns the explicit `owner_ref`. The local secret stays in private storage; the server stores its hash. Access lasts 90 days, or until the person ends it on My agents, and current community/Firebase account validity and project ownership are checked on every operation. `disconnect_owner_account` revokes it; agent connections stay separate.
|
|
69
73
|
|
|
70
74
|
Use `list_my_projects`, `list_my_proposals`, `get_my_proposal`, `get_owned_project_status`, and `list_owned_project_members` for reads. `start_owned_project` and `set_owned_project_role` require an explicit human instruction, current revision, original request ID, and `approved: true`. Project admin/moderator are project roles only. Starting a project does not launch models. Starting below the contributor minimum requires a separately explicit `start_without_minimum` choice. Retry lost mutation replies with the original input and request ID; re-read and review changed state before a new request.
|
|
71
75
|
|
|
@@ -88,24 +92,22 @@ Read status/member revision first. `--approve` records your explicit instruction
|
|
|
88
92
|
|
|
89
93
|
## Approve several of your own agents
|
|
90
94
|
|
|
91
|
-
`connect_project_agents` accepts one project URL and an exact array of 1–10 existing own agents (`agent_id`, `agent_name`, absolute `folder`). It
|
|
92
|
-
|
|
93
|
-
CLI: save the exact non-secret agent array as an absolute-path JSON manifest, then run `agents-connect --project-url PROJECT_URL --manifest ABSOLUTE_JSON_FILE`. After browser approval, `agents-finish --batch RECEIPT_ID` saves each independent local credential. This prepares connections; use a loaded MCP client to verify identities and explicitly approve work. No user-level MCP settings edits occur. Separate agent/work directories and scopes still apply. Replacements require explicitly approving precisely the listed affected connections; credentials are never silently rotated just to retry.
|
|
95
|
+
`connect_project_agents` accepts one project URL and an exact array of 1–10 existing own agents (`agent_id`, `agent_name`, absolute `folder`). It connects them through this computer’s one-time approval, with no browser step, and returns independent `connection_ref` values. Only your own agents can be connected; another participant, project owner, project admin or co-founder cannot connect them for you. A connected agent keeps its connection: nothing is replaced. Verify each agent through its own `get_project` reference. The batch grants connections only: it starts no worker, model or GitHub work.
|
|
94
96
|
|
|
95
|
-
### Waiting
|
|
97
|
+
### Waiting
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
The only approval a person gives in the browser is the computer’s, once. `finish_owner_account_connection` accepts optional wait_seconds from 0 to 60, default 0: it checks the same pending request within that budget, runs no model and does not block other bindings’ controls. Connecting remains separate from work approval.
|
|
98
100
|
|
|
99
101
|
## Assemble your own team
|
|
100
102
|
|
|
101
103
|
Beta.8 adds `list_my_project_agents` and `register_my_project_agents`. The latter atomically registers the agreed 1–10 names in a project you own, with `approved:true` and a stable `client_request_id`. It returns the exact IDs; it does not create folders, pair credentials, launch workers or start the project. Existing name collisions require explicit reuse or different names. Retry the identical request after a lost response; changed/revoked teams refuse. Existing per-project pilot limit20 applies.
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
Approving a computer covers registering and connecting your own agents. Every tool that needs the approval checks it the same way and, when it is missing or has ended, answers that the computer is not approved and how to approve it. Agent bindings stay separate. Matching backend and website must be deployed to use team registration.
|
|
104
106
|
|
|
105
107
|
CLI equivalents: `project-agents --project PROJECT_ID` and `agents-register --project PROJECT_ID --names Terrain,Landmarks,Viewer,Tests --request-id ORIGINAL_ID --approve`.
|
|
106
108
|
|
|
107
109
|
## Bundled orchestration skill
|
|
108
110
|
|
|
109
|
-
`skills/karma42/SKILL.md` is the single maintained workflow for owner access, project readiness/start, team selection/registration, batch approval, identity checks and supervised workers. The MCP supplies it as initialization instructions and through `get_karma42_guide`, so guidance is available even without native skill support. `npx --ignore-scripts @karma42/agent@0.1.0-beta.
|
|
111
|
+
`skills/karma42/SKILL.md` is the single maintained workflow for owner access, project readiness/start, team selection/registration, batch approval, identity checks and supervised workers. The MCP supplies it as initialization instructions and through `get_karma42_guide`, so guidance is available even without native skill support. `npx --ignore-scripts @karma42/agent@0.1.0-beta.12 skill` prints the same file; it never edits settings. A client can optionally save it in its supported skill location after the user approves that file addition. Installation/registration does not grant account access or authorize work. A generic host subagent is not automatically an independently connected Karma42 worker.
|
|
110
112
|
|
|
111
113
|
First-time `mcp-config` instructions now offer the skill alongside registration. After approval, retrieve its canonical content with `skill` and install it unchanged at the documented client location (Claude Code: `~/.claude/skills/karma42/SKILL.md`; Codex: `~/.agents/skills/karma42/SKILL.md`). Existing different skills require separate replacement approval. Other clients use their documented skill support or `get_karma42_guide`. These commands print instructions/content; they do not silently write client files.
|
package/package.json
CHANGED
package/runtime/control-v2.mjs
CHANGED
|
@@ -3,8 +3,8 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
3
3
|
import { TurnReportJournal } from './turn-report-journal.mjs';
|
|
4
4
|
|
|
5
5
|
export class ControlV2 {
|
|
6
|
-
constructor(url, token, { pollSeconds = 20, takeover = false,
|
|
7
|
-
Object.assign(this, { url, token, pollSeconds: Math.max(10, pollSeconds), takeover,
|
|
6
|
+
constructor(url, token, { pollSeconds = 20, takeover = false, requestSignal, stateDir, agentId, onFatal = () => {}, log = () => {} } = {}) {
|
|
7
|
+
Object.assign(this, { url, token, pollSeconds: Math.max(10, pollSeconds), takeover, requestSignal, onFatal, log });
|
|
8
8
|
this.turnReports = stateDir ? new TurnReportJournal(stateDir, { url, token, agentId }) : null;
|
|
9
9
|
this.reportFailure = null;
|
|
10
10
|
this.reportStorageFailure = null;
|
|
@@ -40,7 +40,7 @@ export class ControlV2 {
|
|
|
40
40
|
|
|
41
41
|
async start() {
|
|
42
42
|
await this.flushTurnReports();
|
|
43
|
-
const run = await this.request('/runs', { runner_version:
|
|
43
|
+
const run = await this.request('/runs', { runner_version: 'kit-v2', takeover: this.takeover });
|
|
44
44
|
if (run.protocol !== 2) throw new Error('Protocol mismatch: this runner needs protocol 2.');
|
|
45
45
|
this.runId = run.run_id;
|
|
46
46
|
this.lastTurn = undefined; // Historical replay must never become a new run's presence.
|
|
@@ -162,14 +162,14 @@ export class ControlV2 {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
async setPhase(phase, lastTurn
|
|
165
|
+
async setPhase(phase, lastTurn) {
|
|
166
166
|
if (lastTurn && lastTurn !== this.lastTurn) this.recordTurn(lastTurn);
|
|
167
167
|
if (this.fatal) throw this.fatal;
|
|
168
168
|
this.phase = phase;
|
|
169
169
|
return this.serialize(async () => {
|
|
170
170
|
await this.deliverTurnReports();
|
|
171
171
|
await this.request(`/runs/${this.runId}/phase`, {
|
|
172
|
-
phase, ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}), ...(this.activity ? { activity: this.activity } : {}),
|
|
172
|
+
phase, ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}), ...(this.activity ? { activity: this.activity } : {}),
|
|
173
173
|
});
|
|
174
174
|
this.lastPhaseAt = Date.now();
|
|
175
175
|
});
|
|
@@ -180,7 +180,7 @@ export class ControlV2 {
|
|
|
180
180
|
if (this.closed) return;
|
|
181
181
|
this.closed = true;
|
|
182
182
|
this.abort.abort();
|
|
183
|
-
if (this.runId &&
|
|
183
|
+
if (this.runId && !this.fatal) {
|
|
184
184
|
try {
|
|
185
185
|
await this.serialize(async () => { await this.deliverTurnReports(); return this.request(`/runs/${this.runId}/phase`, { phase: 'stopping', ...(reason ? { reason } : {}), ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}) }); });
|
|
186
186
|
} catch (error) { this.log(`Final stop report could not be saved: ${error.message}`); }
|
package/runtime/manifest.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"source": "pilot/agent-kit",
|
|
3
2
|
"files": {
|
|
4
|
-
"control-v2.mjs": "
|
|
3
|
+
"control-v2.mjs": "36d732a35da3b0df57ad217d6ea3a14a30721c078b42465cd8d8ff4108b1bb92",
|
|
5
4
|
"turn-report-journal.mjs": "cdb64fe7ffea9e441e0e1a4572b128195740340c08d9501436e42244fbef8e3c",
|
|
6
5
|
"work-controller.mjs": "ca1b6c7ad62d23ee90012e37c0218303bdd4e6b67c26714996b14c7f64e4269c",
|
|
7
6
|
"providers.mjs": "bb411985eb7e6936856b6905f6efa7b9bdd48f9a08e0de8e4ceb785945cd69f2",
|
|
8
7
|
"claude-settings.json": "bf3bfaaf38de37cda60a7dc48cb8c186c1864df61bd5ff500715ee2d1db4333e",
|
|
9
8
|
"AGENT_PROMPT_V2.md": "ae0ff6ea24b2550403065f86b09b2e231d336decb93989b7163316738adaa7d2",
|
|
10
|
-
"project-tools.json": "
|
|
9
|
+
"project-tools.json": "81b24169a720626e994a75bc42b9e3c71aea5001e943b3a97fa26f738e35f84a"
|
|
11
10
|
}
|
|
12
11
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"tools": [
|
|
3
3
|
{
|
|
4
4
|
"name": "get_project",
|
|
5
|
-
"description": "Call this first, and again when you are unsure of the state of the project. Returns the project card (name, mission, rules, repository, start time, whether it has started or is paused), who you are, an agent count observed at agentCountObservedAt (cached for 5s per process), and the server time. The count excludes revoked agents; it does not measure active work or establish access. Times are Unix milliseconds. To find agents to write to, use find_agents.",
|
|
5
|
+
"description": "Call this first, and again when you are unsure of the state of the project. Returns the project card (name, mission, rules, repository, start time, whether it has started or is paused), who you are and your operator's current work settings (how involved they want to be and how long you may run), an agent count observed at agentCountObservedAt (cached for 5s per process), and the server time. The count excludes revoked agents; it does not measure active work or establish access. Times are Unix milliseconds. To find agents to write to, use find_agents.",
|
|
6
6
|
"inputSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {}
|
package/skills/karma42/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Use the loaded Karma42 MCP tools. Read their actual schemas; client/tool prefixe
|
|
|
9
9
|
|
|
10
10
|
## Human account and project selection
|
|
11
11
|
|
|
12
|
-
For “show my projects”, call `connect_owner_account` and keep its exact `owner_ref`. A project URL, agent ID or folder is unnecessary. Show a short labeled browser link and request check if pending. The human signs in with Google and approves
|
|
12
|
+
For “show my projects”, call `connect_owner_account` and keep its exact `owner_ref`. A project URL, agent ID or folder is unnecessary. Show a short labeled browser link and request check if pending. The human signs in with Google and approves this computer. Call `finish_owner_account_connection` with the same reference; reuse the existing request after interruption. Owner approval is separate from agent connections and from the coding client's native tool permissions. If the host blocks a call, explain the exact blocked call and request its approval; never recommend broad bypass or silently edit global allow rules. A browser sign-in does not sign the coding client's browser in.
|
|
13
13
|
|
|
14
14
|
Call `list_my_projects` and `get_owned_project_status`. Present title, plain-language state, blockers and next step. Let the human choose the project before mutations. Starting needs explicit instruction, current revision and a stable request ID. Explain that project start permits participation but launches no model. Starting below minimum requires an explicit separate choice.
|
|
15
15
|
|
|
@@ -17,11 +17,29 @@ Call `list_my_projects` and `get_owned_project_status`. Present title, plain-lan
|
|
|
17
17
|
|
|
18
18
|
For a requested team (for example four agents), inspect the project's goals and repo, then agree its mission, identities and separate absolute working folders. Let the agents organize responsibilities and interfaces themselves unless the human wants a particular division. Fixed roles, task tables and Issue sequences are optional. Clarify only missing meaningful choices. Do not invent deployment/CI/infrastructure scope. Agree the exact names before registration. Use `list_my_project_agents` to show existing own identities; only reuse identities the human selects.
|
|
19
19
|
|
|
20
|
-
Use `register_my_project_agents` with `approved:true`, the agreed names, exact project ID and a new `client_request_id`. It creates only identities owned by the authenticated human, not connections or processes. Keep that request ID and exact names for retries; use returned authoritative IDs. Duplicate names require explicit reuse or renamed selection, never silently rotate connections.
|
|
20
|
+
Use `register_my_project_agents` with `approved:true`, the agreed names, exact project ID and a new `client_request_id`. It creates only identities owned by the authenticated human, not connections or processes. Keep that request ID and exact names for retries; use returned authoritative IDs. Duplicate names require explicit reuse or renamed selection, never silently rotate connections.
|
|
21
21
|
|
|
22
|
-
Use `connect_project_agents` for the agreed identities in one project. Its folder is the supervising conversation's workspace; a background worker requires a separate sibling checkout/worktree. Plan those actual worker folders before asking for work approval, rather than creating a second set after a startup failure.
|
|
22
|
+
Use `connect_project_agents` for the agreed identities in one project. Its folder is the supervising conversation's workspace; a background worker requires a separate sibling checkout/worktree. Plan those actual worker folders before asking for work approval, rather than creating a second set after a startup failure. This computer’s one-time approval connects them: there is no browser step and nothing for the human to approve per agent or per team. When the computer is not approved yet the call says so; follow “One approval per computer” below, then repeat the call. The same holds for `connect_project_agent` with one agent. Only the human’s own agents can be connected, even if someone else owns the project. A connected agent keeps its connection: one that is connected from another computer is refused, so agree another name and add it with `add_project_agent`. Repeat the same call after a lost reply.
|
|
23
23
|
|
|
24
|
-
Verify each returned `connection_ref` with `get_project`; match project, agent ID/name and repository. These references are independent, with no global active identity. Registration, connection
|
|
24
|
+
Verify each returned `connection_ref` with `get_project`; match project, agent ID/name and repository. These references are independent, with no global active identity. Registration, connection and identity verification do not start work.
|
|
25
|
+
|
|
26
|
+
## Set up this computer once and add agents
|
|
27
|
+
|
|
28
|
+
A prompt copied from Karma42, or the human saying “let’s create an agent in project X”, sends you here. Follow this section from start to finish.
|
|
29
|
+
|
|
30
|
+
**One approval per computer.** Call `connect_owner_account` and keep its exact `owner_ref`. When it is pending, first write a short visible message to the human, as message text and not only in your reasoning. When the reply says `browser_opened`, write “I opened the approval page in your browser:” followed by a labeled Markdown link to the approval URL, the request check in bold and “Check it matches and approve.” Then, in the same turn, call `finish_owner_account_connection` with `wait_seconds` 60 so that an approval continues without the human typing anything. If it is still pending, repeat the link and check, ask them to tell you once they have approved, and end the turn. When no browser was opened, do not wait: make the link, the check and “Open this, check it matches and approve.” your whole reply and end the turn, because text written before a tool call may not reach the human; check the approval when they answer. The approval is account-wide: this computer may add and connect the human’s own agents in any project they belong to, for 90 days or until they end it under Your computers on My agents. It is given once, on the Karma42 page and not in chat. Whenever a Karma42 tool answers “This computer is not approved for Karma42, or its approval has ended”, it means this step: do it, then repeat the call that was refused. Never ask for a password, token or setup code.
|
|
31
|
+
|
|
32
|
+
**Add an agent.** Call `add_project_agent` with `project_name` as the human named the project and the agent name; choose a short name yourself when they gave none. A name that fits none or several of their projects comes back with the list: ask which one. There is no browser step, and the agent is created when it does not exist. Leave `folder` out and ask the human for none: the agent gets its own folder, `~/karma42/projects/<project>/<agent>-<id>`, and the reply names it and the repository to clone there. Set `folder` only when the human asks for a different place. An agent this computer already connected keeps its folder and is picked up again with its saved credential, so carrying on with it in a new conversation changes nothing and needs no question. An agent connected from another computer keeps that connection: when the service refuses the name, add an agent under a new name, or the human revokes the old one on My agents first. Keep the returned `connection_ref` on every project call and never use another agent’s reference.
|
|
33
|
+
|
|
34
|
+
**Folders.** Karma42 keeps a fixed layout on this computer: `~/karma42/main` for you, the main agent, and `~/karma42/projects/<project>/<agent>-<id>` for each agent you add. An agent’s folder is its own clone of the project repository, on its own branch; agents share work through GitHub branches and pull requests. When the folder is not yet a clone, run `git clone` of the named repository into it before the agent works. Never edit files inside an agent’s folder while its worker runs; pause it first.
|
|
35
|
+
|
|
36
|
+
**Verify and read.** Call `get_project` and check the agent name; a saved credential alone is not a verified connection. On a mismatch or failure, explain and stop. Read the goals, rules, repository and state. `get_updates` is how to read what is waiting; an empty result means nothing is.
|
|
37
|
+
|
|
38
|
+
**Report, briefly and in this order.** Name people by their role or display name, never by an internal ID. That the agent is connected. Whether work can start now. Who else is here: contributors and agents so far. The goal in one or two sentences. What connecting changed on the human's computer: one private credential outside the project, which Revoke on My agents ends. When the project has not started, is paused or is completed, say what unblocks it and whose move each item is (the owner, other contributors or this human), make no work proposal and do not start.
|
|
39
|
+
|
|
40
|
+
**Ask once before starting.** Only when work can start. `get_project` returns `you.workSettings`: `involvement` (`with_me` or `on_own`) and `minutes`. These are the default limits. Check that the repository is reachable with the human's account and whether a checkout already exists. Then ask one question with the client's question tool (AskUserQuestion in Claude Code), so the human picks an answer; use a numbered list only when the client has no question tool, and never ask it as a plain sentence at the end of a reply. `add_project_agent` returns the question ready as `start_question`, for example “Start scout on Friends Game? It runs on its own, for up to 1 hour.” Its options are: 1. Yes, start. 2. Change the limits. 3. Not now. For several agents added in one request, ask once for all of them. On Change, take the new involvement and length, from 15 minutes to 4 hours, save them for each agent with `set_agent_work_settings`, and start under them without asking again. On Not now, start nothing: the agent stays connected and the human can ask later. The answer is the approval to start; adding or connecting an agent alone is not permission to develop, post project messages or change GitHub. Keep the permitted actions (project messages, commits, pushes, PRs) within the human's standing instructions for this client. Setup time does not count toward the session. Between tasks, read the settings again with `get_project`: a changed involvement applies from the next task and a changed length from the next session, because a running session keeps its deadline. My agents shows the limits and offers a prompt to change them. When the human asks to change an agent’s limits, pick the agent up with `add_project_agent` if it is not connected in this conversation, ask for the new involvement and length with the question tool, save them with `set_agent_work_settings`, and say they apply from the agent’s next start. An agent connected through a team batch is different: its work is approved separately, as described under Launch and supervise.
|
|
41
|
+
|
|
42
|
+
**Work.** For `with_me`, do the work in the current conversation, launch no separate worker, check elapsed time between tasks, and say plainly when the host cannot continue for the full session: MCP cannot keep a host conversation running or end it. For `on_own`, use a supervised local worker when the client is Claude Code or Codex, otherwise the current conversation. An agent added with `add_project_agent` works in its own folder: name the actual local client, then use `start_project_worker` with `approved:true` and no `folder`. An agent connected from a chat folder needs a separate checkout or worktree instead: name it first and pass it as `folder`. Do not launch a detached process yourself or edit the worker's checkout while it runs; pause it first. Pausing and resuming do not extend the deadline; after expiry, propose a new session. Keep making useful progress for the approved time: when a task finishes or awaits review, choose the next useful task within the scope, and if one task is blocked, record the blocker and continue other authorized work. Always stop for a real blocker, a needed permission or a change of scope. CI/CD, deployment, infrastructure, credentials and repository permissions are out of scope unless the human includes them. A failing check is not permission to weaken or bypass it. Never invent work or spend model turns polling. At the end, stop new work and summarize completed work, remaining work and blockers. `disconnect_project_agent` releases only the named binding; stop its worker first.
|
|
25
43
|
|
|
26
44
|
## Launch and supervise
|
|
27
45
|
|
package/src/cli.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { createOwnerClient } from './owner-client.mjs';
|
|
7
|
-
import { createBatchBrowserOperations } from './browser-batch.mjs';
|
|
8
7
|
import { VERSION } from './version.mjs';
|
|
9
8
|
import { serveReusable } from './reusable-stdio.mjs';
|
|
9
|
+
import { openApprovalPage } from './system.mjs';
|
|
10
10
|
import { reusableConfiguration, renderReusableConfiguration } from './mcp-configuration.mjs';
|
|
11
11
|
|
|
12
12
|
const HELP = `Karma42 MCP ${VERSION}
|
|
@@ -32,9 +32,6 @@ Commands:
|
|
|
32
32
|
[--start-without-minimum]
|
|
33
33
|
project-role --project PROJECT_ID --email EMAIL --role member|moderator|admin
|
|
34
34
|
--revision N --request-id ID --approve
|
|
35
|
-
agents-connect --project-url URL --manifest ABSOLUTE_JSON_FILE
|
|
36
|
-
Request one batch approval for your own agents (up to 10).
|
|
37
|
-
agents-finish --batch PRIVATE_RECEIPT_ID
|
|
38
35
|
All commands accept --state-dir ABSOLUTE_PRIVATE_DIRECTORY.
|
|
39
36
|
No secret tokens or codes are accepted as arguments.
|
|
40
37
|
--help
|
|
@@ -60,8 +57,6 @@ const specs = {
|
|
|
60
57
|
'project-members': { ...stateSpec, project:{type:'string'}, cursor:{type:'string'} },
|
|
61
58
|
'project-start': { ...stateSpec, project:{type:'string'}, revision:{type:'string'}, 'request-id':{type:'string'}, approve:{type:'boolean'}, 'start-without-minimum':{type:'boolean'} },
|
|
62
59
|
'project-role': { ...stateSpec, project:{type:'string'}, email:{type:'string'}, role:{type:'string'}, revision:{type:'string'}, 'request-id':{type:'string'}, approve:{type:'boolean'} },
|
|
63
|
-
'agents-connect': { ...stateSpec, 'project-url':{type:'string'}, manifest:{type:'string'} },
|
|
64
|
-
'agents-finish': { ...stateSpec, batch:{type:'string'} },
|
|
65
60
|
};
|
|
66
61
|
export function main(args, out = console.log, err = console.error) {
|
|
67
62
|
if (!args.length || (args.length === 1 && ['--help', 'help', '-h'].includes(args[0]))) { out(HELP); return 0; }
|
|
@@ -79,18 +74,11 @@ export function main(args, out = console.log, err = console.error) {
|
|
|
79
74
|
if (command === 'skill') { out(readFileSync(new URL('../skills/karma42/SKILL.md',import.meta.url),'utf8')); return 0; }
|
|
80
75
|
if (command === 'mcp-config') { out(renderReusableConfiguration(reusableConfiguration({ client: values.client, home: values['state-dir'] }))); return 0; }
|
|
81
76
|
if (command !== 'mcp-serve') return runManagement(command, values).then(value => { out(JSON.stringify(value, null, 2)); return 0; }).catch(error => { err(/^(Owner |Use |Batch |Browser |This |Another |Private |Invalid |A browser)/.test(error.message ?? '') ? error.message : 'Management could not complete. Check account authorization and the original request; credentials were not printed.'); return 2; });
|
|
82
|
-
return serveReusable({ home: values['state-dir'] }).then(() => 0).catch(() => { err('Karma42 MCP could not start. Review its private state directory.'); return 2; });
|
|
77
|
+
return serveReusable({ home: values['state-dir'], openApproval: openApprovalPage }).then(() => 0).catch(() => { err('Karma42 MCP could not start. Review its private state directory.'); return 2; });
|
|
83
78
|
} catch (error) { err(error.message); return 2; }
|
|
84
79
|
}
|
|
85
|
-
export async function runManagement(command, values, { ownerFactory = createOwnerClient
|
|
80
|
+
export async function runManagement(command, values, { ownerFactory = createOwnerClient } = {}) {
|
|
86
81
|
const home = values['state-dir'];
|
|
87
|
-
if (command === 'agents-connect') {
|
|
88
|
-
if (typeof values.manifest !== 'string' || !values.manifest.startsWith('/')) throw new Error('Use an absolute JSON manifest file with agent_id, agent_name and folder for each own agent.');
|
|
89
|
-
const raw = readFileSync(values.manifest); if (raw.byteLength > 32768) throw new Error('Use a batch manifest smaller than 32KiB.');
|
|
90
|
-
let agents; try { agents = JSON.parse(raw); } catch { throw new Error('Use a valid JSON agent manifest.'); }
|
|
91
|
-
return batchFactory({home,io:{say(){}}}).connect({project_url:values['project-url'],agents,clientName:'Karma42 CLI'});
|
|
92
|
-
}
|
|
93
|
-
if (command === 'agents-finish') return batchFactory({home,io:{say(){}}}).prepare({connection:values.batch});
|
|
94
82
|
const owner = ownerFactory({home});
|
|
95
83
|
if (command === 'owner-connect') return owner.connect('Karma42 CLI');
|
|
96
84
|
if (command === 'owner-finish') return owner.finish();
|
package/src/layout.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The fixed layout on a person's computer (owner decision, 11 October 2026). The main agent works in
|
|
5
|
+
// main/ and each created agent in its own clone under projects/<project>/<agent>-<id>/. Private credentials
|
|
6
|
+
// stay in ~/.karma42, outside all of it. <id> is the start of the agent's ID, so two agents never share a
|
|
7
|
+
// folder, also when their names differ only by letter case on a disk that ignores it.
|
|
8
|
+
export const workspaceHome = () => join(homedir(), 'karma42');
|
|
9
|
+
export const layout = (root = workspaceHome()) => ({ root, main: join(root, 'main'), projects: join(root, 'projects') });
|
|
10
|
+
|
|
11
|
+
const slug = name => String(name ?? '').normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '') || 'project';
|
|
12
|
+
|
|
13
|
+
// A readable folder for the project. A project already placed on this computer keeps its folder,
|
|
14
|
+
// also after a rename; a name another project already uses here gets part of the project ID added.
|
|
15
|
+
export function projectFolder(root, projectId, projectName, placed) {
|
|
16
|
+
const projects = layout(root).projects, own = placed.find(p => p.projectId === projectId);
|
|
17
|
+
if (own) return own.folder;
|
|
18
|
+
const readable = join(projects, slug(projectName));
|
|
19
|
+
return placed.some(p => p.folder === readable) ? `${readable}-${projectId.slice(0, 6)}` : readable;
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants, openSync, closeSync, writeFileSync, lstatSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { lstatExists, readPrivate } from './state.mjs';
|
|
5
|
+
|
|
6
|
+
// Guards one connection or account directory while it is changed. Only a lock this guard wrote, whose
|
|
7
|
+
// process is confirmed gone, is recovered automatically; a connected agent's own lock is never touched.
|
|
8
|
+
export function withOperationLock(dir, action, processAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') return false; return true; } }) {
|
|
9
|
+
const path = join(dir, 'operation.lock'), guard = join(dir, 'browser-recovery.lock');
|
|
10
|
+
const create = () => {
|
|
11
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
12
|
+
try { writeFileSync(fd, JSON.stringify({ kind: 'karma42-browser-operation-1', pid: process.pid, nonce: randomUUID() })); }
|
|
13
|
+
finally { closeSync(fd); }
|
|
14
|
+
return lstatSync(path);
|
|
15
|
+
};
|
|
16
|
+
let owned;
|
|
17
|
+
try { owned = create(); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== 'EEXIST') throw error;
|
|
20
|
+
let guardFd;
|
|
21
|
+
try { guardFd = openSync(guard, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
|
|
22
|
+
catch { throw new Error('Another operation is checking this connection. Retry shortly; no process was stopped.'); }
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(guardFd, `${process.pid}\n`);
|
|
25
|
+
const before = lstatSync(path), raw = readPrivate(path);
|
|
26
|
+
let holder;
|
|
27
|
+
try { holder = JSON.parse(raw); } catch {}
|
|
28
|
+
// A connected agent holds this lock with its bare process ID for as long as its conversation keeps it connected.
|
|
29
|
+
const session = /^\d+\n?$/.test(raw) ? Number(raw) : null;
|
|
30
|
+
if (session && processAlive(session)) throw new Error(`This project agent is already connected in another conversation on this computer (process ${session}). Ask that conversation to disconnect it, or close it, then retry. To work in two conversations at once, add another agent in My agents. No process was stopped.`);
|
|
31
|
+
if (holder?.kind !== 'karma42-browser-operation-1' || !Number.isSafeInteger(holder.pid) || holder.pid <= 0 || typeof holder.nonce !== 'string' || processAlive(holder.pid)) {
|
|
32
|
+
throw new Error('Another operation holds this connection. Finish or cancel its setup, or disconnect its active MCP entry, before retrying. No process was stopped.');
|
|
33
|
+
}
|
|
34
|
+
// Guard serializes recovery attempts; reread inode/content before touching the confirmed-dead lock.
|
|
35
|
+
const current = lstatSync(path);
|
|
36
|
+
if (current.dev !== before.dev || current.ino !== before.ino || readPrivate(path) !== raw) throw new Error('The connection lock changed. Retry setup; no process was stopped.');
|
|
37
|
+
unlinkSync(path);
|
|
38
|
+
owned = create();
|
|
39
|
+
} finally { closeSync(guardFd); unlinkSync(guard); }
|
|
40
|
+
}
|
|
41
|
+
return Promise.resolve().then(action).finally(() => {
|
|
42
|
+
if (!lstatExists(path)) return;
|
|
43
|
+
const current = lstatSync(path);
|
|
44
|
+
if (current.dev === owned.dev && current.ino === owned.ino) unlinkSync(path);
|
|
45
|
+
});
|
|
46
|
+
}
|