@annealessio647-toby/ai-agent-collab-mcp-lite 0.1.0
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/CHANGELOG.md +94 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/VERSION +1 -0
- package/bin/agent-collab-mcp-lite.js +7 -0
- package/dist/lib/branch_guard.d.ts +11 -0
- package/dist/lib/branch_guard.d.ts.map +1 -0
- package/dist/lib/branch_guard.js +30 -0
- package/dist/lib/branch_guard.js.map +1 -0
- package/dist/lib/git_runner.d.ts +13 -0
- package/dist/lib/git_runner.d.ts.map +1 -0
- package/dist/lib/git_runner.js +49 -0
- package/dist/lib/git_runner.js.map +1 -0
- package/dist/lib/lock_retry.d.ts +40 -0
- package/dist/lib/lock_retry.d.ts.map +1 -0
- package/dist/lib/lock_retry.js +76 -0
- package/dist/lib/lock_retry.js.map +1 -0
- package/dist/lib/post_state_verify.d.ts +7 -0
- package/dist/lib/post_state_verify.d.ts.map +1 -0
- package/dist/lib/post_state_verify.js +43 -0
- package/dist/lib/post_state_verify.js.map +1 -0
- package/dist/lib/ps_runner.d.ts +8 -0
- package/dist/lib/ps_runner.d.ts.map +1 -0
- package/dist/lib/ps_runner.js +118 -0
- package/dist/lib/ps_runner.js.map +1 -0
- package/dist/lib/repo_root.d.ts +3 -0
- package/dist/lib/repo_root.d.ts.map +1 -0
- package/dist/lib/repo_root.js +44 -0
- package/dist/lib/repo_root.js.map +1 -0
- package/dist/lib/result_envelope.d.ts +3 -0
- package/dist/lib/result_envelope.d.ts.map +1 -0
- package/dist/lib/result_envelope.js +25 -0
- package/dist/lib/result_envelope.js.map +1 -0
- package/dist/lib/schema_guard.d.ts +3 -0
- package/dist/lib/schema_guard.d.ts.map +1 -0
- package/dist/lib/schema_guard.js +43 -0
- package/dist/lib/schema_guard.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +156 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/claim_review.d.ts +30 -0
- package/dist/tools/claim_review.d.ts.map +1 -0
- package/dist/tools/claim_review.js +113 -0
- package/dist/tools/claim_review.js.map +1 -0
- package/dist/tools/close.d.ts +26 -0
- package/dist/tools/close.d.ts.map +1 -0
- package/dist/tools/close.js +60 -0
- package/dist/tools/close.js.map +1 -0
- package/dist/tools/fetch.d.ts +27 -0
- package/dist/tools/fetch.d.ts.map +1 -0
- package/dist/tools/fetch.js +72 -0
- package/dist/tools/fetch.js.map +1 -0
- package/dist/tools/push_verdict.d.ts +32 -0
- package/dist/tools/push_verdict.d.ts.map +1 -0
- package/dist/tools/push_verdict.js +123 -0
- package/dist/tools/push_verdict.js.map +1 -0
- package/dist/tools/read_report.d.ts +17 -0
- package/dist/tools/read_report.d.ts.map +1 -0
- package/dist/tools/read_report.js +37 -0
- package/dist/tools/read_report.js.map +1 -0
- package/dist/tools/status.d.ts +31 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +41 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/write_verdict.d.ts +39 -0
- package/dist/tools/write_verdict.d.ts.map +1 -0
- package/dist/tools/write_verdict.js +92 -0
- package/dist/tools/write_verdict.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ai-agent-collab-mcp-lite` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
Schema versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-06-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial v1 build per `MCP_LITE_v1_PLAN.md` Rev 2 (codex PASS).
|
|
12
|
+
- MCP stdio server (Node.js + TypeScript) registering 7 manual-trigger tools.
|
|
13
|
+
- **7 tools** (D-5): `status`, `fetch`, `read_report`, `claim_review`, `write_verdict`, `push_verdict`, `close`.
|
|
14
|
+
- **D-2** validate-before-action: `write_verdict` runs `agent-collab.ps1 validate -As codex` after write; `push_verdict` runs validate before the verb.
|
|
15
|
+
- **D-4** fail-fast: every tool throws `SchemaVersionMismatch` if `.agent/COLLAB_MODE.yaml: schema_version != 2`.
|
|
16
|
+
- **D-5** lease safety for `claim_review`: explicit branches (fresh / `LeaseConflict` / `StaleLease` + opt-in `forceSteal`). Never silently passes `-Force`.
|
|
17
|
+
- **D-6** branch handling first-class: mutable tools throw `BranchMismatch` if local checkout differs from `COLLAB_MODE.yaml: branch`. `fetch` does ref-only fetch always + conditional ff-pull when local matches.
|
|
18
|
+
- PS 5.1 / pwsh 7+ safety: `ps_runner` always uses `-File` mode to preserve exit codes (R-1).
|
|
19
|
+
- UTF-8 no BOM file writes (R-2).
|
|
20
|
+
- Distribution: npm package primary (`@annealessio647-toby/ai-agent-collab-mcp-lite`), submodule install supported.
|
|
21
|
+
- Examples: VS Code MCP configs (npm + submodule paths) + walkthrough.md.
|
|
22
|
+
- CI: tsc + jest (`ci.yml`); banned-words scanner (`banned-words.yml`); npm publish on tag (`publish.yml`).
|
|
23
|
+
- `.github/CODEOWNERS` locks `src/`, `tests/`, `.github/`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **RC1 (codex 41687a2):** Removed heartbeat semantics from `claim_review`. The
|
|
27
|
+
underlying `agent-collab.ps1 codex-reviewing` verb only accepts
|
|
28
|
+
`state=waiting_for_codex`, so a second `claim_review` call (post-claim, when
|
|
29
|
+
state is `codex_reviewing`) now throws `BadState` upfront. `ClaimType` is
|
|
30
|
+
reduced to `'fresh' | 'stolen-stale'`. Walkthrough + tests updated.
|
|
31
|
+
|
|
32
|
+
### CI
|
|
33
|
+
- **RC3 (codex 41687a2):** Added `package-lock.json` and switched CI + publish
|
|
34
|
+
workflows from `npm install` to `npm ci`. Restored `cache: 'npm'` under
|
|
35
|
+
`actions/setup-node@v4` in both workflows.
|
|
36
|
+
|
|
37
|
+
### Fixed (mcp-lite-v1-fix-pathext)
|
|
38
|
+
- **Windows PATHEXT silent failure (integrate phase smoke discovery):**
|
|
39
|
+
The `@modelcontextprotocol/sdk` `StdioClientTransport` on Windows uses
|
|
40
|
+
a curated env (`DEFAULT_INHERITED_ENV_VARS`) that includes `PATH` but
|
|
41
|
+
OMITS `PATHEXT`. Without `PATHEXT` a spawned PowerShell cannot resolve
|
|
42
|
+
extension-less command names like `git`, and the FROZEN `agent-collab.ps1`
|
|
43
|
+
dies on its first `git` call with `The term 'git' is not recognized` —
|
|
44
|
+
but PowerShell's `Write-Error` does not set a non-zero exit code by
|
|
45
|
+
default. The tools previously trusted `exitCode === 0` and returned
|
|
46
|
+
hardcoded post-state values, masking a complete no-op.
|
|
47
|
+
- **Defense layer 1 — `src/lib/ps_runner.ts buildPsEnv()`:** when running
|
|
48
|
+
on Windows, the spawn env is composed so a missing parent `PATHEXT` is
|
|
49
|
+
filled with `.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC`.
|
|
50
|
+
Parent-set `PATHEXT` (any casing — Windows env is case-insensitive)
|
|
51
|
+
is preserved. Non-Windows: passthrough.
|
|
52
|
+
- **Defense layer 2 — `src/lib/post_state_verify.ts assertPostState()`:**
|
|
53
|
+
every mutating tool (`claim_review`, `push_verdict`, `close`) now
|
|
54
|
+
re-reads `.agent/COLLAB_MODE.yaml` after the PS verb returns and
|
|
55
|
+
throws `PostStateMismatch` (new `ToolErrorName`) if the on-disk state
|
|
56
|
+
is not one of the expected post-states. The error includes the
|
|
57
|
+
expected set, actual state, exit code, and truncated stdout/stderr
|
|
58
|
+
(1000 chars each) for forensic diagnosis. Catches *any* silent no-op,
|
|
59
|
+
not just the `PATHEXT` cause.
|
|
60
|
+
- **`src/types.ts`:** `ToolErrorName` extended with `'PostStateMismatch'`.
|
|
61
|
+
- **Tests:** `tests/pathext_env.spec.ts` (5 cases — fill / preserve /
|
|
62
|
+
case-insensitive / non-Windows passthrough) + `tests/post_state_verify.spec.ts`
|
|
63
|
+
(4 unit + 2 integration cases proving `claim_review` and `close` throw
|
|
64
|
+
`PostStateMismatch` when a stub PS verb is a silent no-op).
|
|
65
|
+
- **Examples:** `examples/vscode-mcp.npm.json` + `examples/vscode-mcp.submodule.json`
|
|
66
|
+
now ship an explicit (optional) `PATHEXT` entry in the `env:` block,
|
|
67
|
+
self-documenting the dependency.
|
|
68
|
+
- **README + walkthrough:** Windows section documents the SDK quirk,
|
|
69
|
+
both defense layers, and the optional explicit override.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- **Codex r3 0e81196 — concurrency-unsafe lock unlink:** Replaced the
|
|
73
|
+
`clearStaleLock()` helper (which unconditionally `unlink`'d
|
|
74
|
+
`.agent/.lock` before every mutating PS spawn — and could race a sibling
|
|
75
|
+
in-flight mutation) with `runMutatingVerbWithLockRetry()`. New behavior:
|
|
76
|
+
invoke the verb normally; on `lock already held` stderr, back off (100ms,
|
|
77
|
+
200ms, 400ms; ~700ms total) and retry. If still held, throw a structured
|
|
78
|
+
`ToolError('LockHeld', ...)` with retry count and stderr; the lock file
|
|
79
|
+
is NEVER deleted by the helper. `claim_review`, `push_verdict`, `close`
|
|
80
|
+
all switched to the new helper; `write_verdict` was already safe (its
|
|
81
|
+
only PS spawn is read-only `validate -As codex`).
|
|
82
|
+
- Regression test `tests/lock_concurrency.spec.ts` plants a fresh
|
|
83
|
+
`.agent/.lock` with sentinel content and asserts both `claim_review` and
|
|
84
|
+
`close` throw `LockHeld` AND the lock file is byte-identical afterward.
|
|
85
|
+
- `src/types.ts` `ToolErrorName` extended with `'LockHeld'`.
|
|
86
|
+
- e2e setup-side sequential cleanup in `tests/e2e.spec.ts` (`runPwshSync`)
|
|
87
|
+
kept as-is per codex r3 guidance — tests are strictly sequential and
|
|
88
|
+
setup is canonical PS only.
|
|
89
|
+
|
|
90
|
+
### Spec notes
|
|
91
|
+
- Implements MCP_LITE_v1_PLAN.md Rev 2 from standalone-v1 collab phase `mcp-lite-plan-review-r2` (codex PASS 3b182a6).
|
|
92
|
+
- Does NOT implement Claude-side verbs (`on`, `claim`, `claude-done`); those remain direct PS calls per non-goal §1.
|
|
93
|
+
- Does NOT implement webhook, scheduler, file-watcher, auto-merge; per non-goal §1.
|
|
94
|
+
- Requires `ai-agent-collab-standard` schema_version 2 in the target project.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 annealessio647-toby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# ai-agent-collab-mcp-lite
|
|
2
|
+
|
|
3
|
+
**MCP Lite v1**: a tiny manual-trigger MCP stdio server that wraps the existing `agent-collab.ps1 + Git + .agent/` collaboration flow so the **codex review side** is end-to-end through MCP without a PowerShell call.
|
|
4
|
+
|
|
5
|
+
Wraps. Does not replace. Source of truth stays in `.agent/` and GitHub.
|
|
6
|
+
|
|
7
|
+
## 7 manual-trigger tools
|
|
8
|
+
|
|
9
|
+
| Tool | Maps to | Side effects |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `status` | `agent-collab.ps1 status -Json` | none (or `git fetch` if `fetchFirst`) |
|
|
12
|
+
| `fetch` | `git fetch origin <branch>` + conditional `git pull --ff-only` | remote ref always; local working tree only when branch matches |
|
|
13
|
+
| `read_report` | `fs.readFile(.agent/CLAUDE_REPORT.md)` | none |
|
|
14
|
+
| `claim_review` | `agent-collab.ps1 codex-reviewing` | one atomic git commit + push (lease-safe; never auto-`-Force`) |
|
|
15
|
+
| `write_verdict` | write `.agent/CODEX_VERDICT.md` → `validate -As codex` | local file write only; does NOT commit |
|
|
16
|
+
| `push_verdict` | `validate -As codex` → `codex-pass` / `codex-fail` | one atomic git commit + push |
|
|
17
|
+
| `close` | `agent-collab.ps1 off` | one atomic git commit + push |
|
|
18
|
+
|
|
19
|
+
Every mutable tool runs the same two guards first: **D-4 schema version check** (`schema_version != 2` → throw) and **D-6 branch guard** (`git symbolic-ref --short HEAD != COLLAB_MODE.branch` → throw, no side effects). See the inline TypeScript types under `src/types.ts` and the design rationale in [`CHANGELOG.md`](CHANGELOG.md).
|
|
20
|
+
|
|
21
|
+
## Install — VS Code Quick Start
|
|
22
|
+
|
|
23
|
+
### Option A: npm global install (D-3 primary path)
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
npm install -g @annealessio647-toby/ai-agent-collab-mcp-lite
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then in your project's `.vscode/mcp.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"servers": {
|
|
34
|
+
"collab-mcp-lite": {
|
|
35
|
+
"command": "agent-collab-mcp-lite",
|
|
36
|
+
"args": [],
|
|
37
|
+
"env": {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Option B: git submodule (D-3 supported path)
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
git submodule add https://github.com/annealessio647-toby/ai-agent-collab-mcp-lite.git .agent-mcp-lite
|
|
47
|
+
cd .agent-mcp-lite
|
|
48
|
+
npm install
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then in `.vscode/mcp.json`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"servers": {
|
|
57
|
+
"collab-mcp-lite": {
|
|
58
|
+
"command": "node",
|
|
59
|
+
"args": ["${workspaceFolder}/.agent-mcp-lite/dist/server.js"],
|
|
60
|
+
"env": {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See `examples/vscode-mcp.npm.json` and `examples/vscode-mcp.submodule.json` for ready-to-copy snippets.
|
|
67
|
+
|
|
68
|
+
## Windows note — PATHEXT and `git`
|
|
69
|
+
|
|
70
|
+
The `@modelcontextprotocol/sdk` `StdioClientTransport` on Windows uses a curated env (`DEFAULT_INHERITED_ENV_VARS`) that includes `PATH` but **OMITS `PATHEXT`**. Without `PATHEXT`, a spawned PowerShell cannot resolve extension-less command names (`git`, `npm`, …), and the FROZEN `agent-collab.ps1` dies on its first `git` call with `The term 'git' is not recognized` — but PowerShell's `Write-Error` does NOT set a non-zero exit code by default, so the underlying verb returns "successfully" and disk state is never advanced.
|
|
71
|
+
|
|
72
|
+
This MCP server defends in two layers:
|
|
73
|
+
|
|
74
|
+
1. **`src/lib/ps_runner.ts buildPsEnv()`** — when running on Windows, the spawn env is composed so that if the parent `process.env` has no `PATHEXT`, a canonical default (`.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC`) is supplied. Parent-set `PATHEXT` (in any casing) is preserved.
|
|
75
|
+
2. **`src/lib/post_state_verify.ts assertPostState()`** — every mutating tool re-reads `.agent/COLLAB_MODE.yaml` after the PS verb returns and asserts the on-disk state advanced to one of the expected values. If not, the tool throws `PostStateMismatch` with the actual state, exit code, and truncated stdout/stderr so the operator can investigate. This catches *any* silent no-op, not just the `PATHEXT` cause.
|
|
76
|
+
|
|
77
|
+
If you are a VS Code MCP / Codex CLI MCP user on Windows and want to be extra safe, you can still pass `PATHEXT` explicitly in the server's `env`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"servers": {
|
|
82
|
+
"collab-mcp-lite": {
|
|
83
|
+
"command": "agent-collab-mcp-lite",
|
|
84
|
+
"args": [],
|
|
85
|
+
"env": {
|
|
86
|
+
"PATHEXT": ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This is no longer required (the server compensates internally), but it does no harm and makes the dependency explicit.
|
|
94
|
+
|
|
95
|
+
## Prerequisites per project
|
|
96
|
+
|
|
97
|
+
The target project must contain a working `.agent/` directory and a `tools/agent-collab.ps1` stub (or `.agent-collab/tools/agent-collab.ps1` submodule path). See [`ai-agent-collab-standard`](https://github.com/annealessio647-toby/ai-agent-collab-standard) for the upstream protocol.
|
|
98
|
+
|
|
99
|
+
Specifically:
|
|
100
|
+
- `.agent/COLLAB_MODE.yaml` exists with `schema_version: 2`
|
|
101
|
+
- `.agent/policy.yaml` exists
|
|
102
|
+
- `agent-collab.ps1` is available via the path resolved from `repoPath` parameter on each tool call
|
|
103
|
+
|
|
104
|
+
## Tool call shape
|
|
105
|
+
|
|
106
|
+
Every tool call requires `repoPath` (absolute path to the project repo containing `.agent/`). One MCP server instance can drive multiple projects; no global "current repo" state.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
// example: write_verdict
|
|
110
|
+
{
|
|
111
|
+
"name": "write_verdict",
|
|
112
|
+
"arguments": {
|
|
113
|
+
"repoPath": "D:/projects/<project-root>",
|
|
114
|
+
"verdict": "PASS",
|
|
115
|
+
"accepted": "...",
|
|
116
|
+
"requiredChanges": "None.",
|
|
117
|
+
"risks": "None.",
|
|
118
|
+
"nextStep": "Run close to advance state.",
|
|
119
|
+
"expectedBranch": "collab/some-phase"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Acceptance test (manual two-machine round-trip)
|
|
125
|
+
|
|
126
|
+
| Step | Where | Action |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| 1 | Machine A (Claude, no MCP needed) | `agent-collab.ps1 on -Phase <p>`, `claim -As claude`, fill report, `claude-done` |
|
|
129
|
+
| 2 | Machine B (Codex, **using MCP Lite**) | `status({repoPath})` → `state=waiting_for_codex` |
|
|
130
|
+
| 3 | B | `fetch({repoPath})` → ref-only + ff-pull (local matches) |
|
|
131
|
+
| 4 | B | `read_report({repoPath})` |
|
|
132
|
+
| 5 | B | `claim_review({repoPath})` → state=`codex_reviewing` |
|
|
133
|
+
| 6 | B | `write_verdict({repoPath, verdict, ...})` → validate PASS |
|
|
134
|
+
| 7 | B | `push_verdict({repoPath, verdict})` → commit + push |
|
|
135
|
+
| 8 | A or B | `close({repoPath, reason})` → state=`closed` |
|
|
136
|
+
|
|
137
|
+
## Versioning + compatibility
|
|
138
|
+
|
|
139
|
+
- Requires `schema_version: 2` in the target project's `.agent/COLLAB_MODE.yaml`. Otherwise every tool throws `SchemaVersionMismatch`.
|
|
140
|
+
- v0.1.x line supports `ai-agent-collab-standard` v2.0.0 schema. A future v0.2.x may track v3.
|
|
141
|
+
|
|
142
|
+
## What this is NOT
|
|
143
|
+
|
|
144
|
+
- Not a webhook receiver.
|
|
145
|
+
- Not a background scheduler.
|
|
146
|
+
- Not an auto-PR-merger.
|
|
147
|
+
- Not a domain-aware service (no KiCad / DRC / firmware / report knowledge).
|
|
148
|
+
- Not a replacement for `agent-collab.ps1` — it shells out to the canonical script.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see [LICENSE](LICENSE).
|
|
153
|
+
|
|
154
|
+
## See also
|
|
155
|
+
|
|
156
|
+
- [`ai-agent-collab-standard`](https://github.com/annealessio647-toby/ai-agent-collab-standard) — upstream protocol + canonical `agent-collab.ps1`
|
|
157
|
+
- `MCP_LITE_v1_PLAN.md` (Rev 2) in `standalone-v1` collab phase `mcp-lite-plan-review-r2` — full spec this repo implements
|
package/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI shim — used by `npm install -g` to expose `agent-collab-mcp-lite`.
|
|
3
|
+
// All work lives in the compiled stdio server.
|
|
4
|
+
import('../dist/server.js').catch((err) => {
|
|
5
|
+
process.stderr.write(`agent-collab-mcp-lite: failed to load server: ${err}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CollabMode } from '../types.js';
|
|
2
|
+
export declare function assertBranchGuard(repoPath: string, collab: CollabMode, callerExpectedBranch?: string): Promise<{
|
|
3
|
+
actual: string;
|
|
4
|
+
expected: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function softBranchCheck(repoPath: string, collab: CollabMode, callerExpectedBranch?: string): Promise<{
|
|
7
|
+
actual: string;
|
|
8
|
+
expected: string;
|
|
9
|
+
matches: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=branch_guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch_guard.d.ts","sourceRoot":"","sources":["../../src/lib/branch_guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,aAAa,CAAC;AAMpD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,UAAU,EAClB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAW/C;AAGD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,UAAU,EAClB,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAIjE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ToolError } from '../types.js';
|
|
2
|
+
import { currentBranch } from './git_runner.js';
|
|
3
|
+
// D-6 branch guard. Mutable tools MUST call this before any side effect.
|
|
4
|
+
// Throws BranchMismatch if local checkout differs from expectedBranch (or
|
|
5
|
+
// COLLAB_MODE.branch if expectedBranch is omitted).
|
|
6
|
+
export async function assertBranchGuard(repoPath, collab, callerExpectedBranch) {
|
|
7
|
+
const expected = resolveExpected(collab, callerExpectedBranch);
|
|
8
|
+
const actual = await currentBranch(repoPath);
|
|
9
|
+
if (actual !== expected) {
|
|
10
|
+
throw new ToolError('BranchMismatch', `expected branch '${expected}', got '${actual || '(detached HEAD)'}'; checkout the collab branch before mutating`, { expected, actual });
|
|
11
|
+
}
|
|
12
|
+
return { actual, expected };
|
|
13
|
+
}
|
|
14
|
+
// D-6 soft check. Read-only tools (fetch) use this to populate result.
|
|
15
|
+
export async function softBranchCheck(repoPath, collab, callerExpectedBranch) {
|
|
16
|
+
const expected = resolveExpected(collab, callerExpectedBranch);
|
|
17
|
+
const actual = await currentBranch(repoPath);
|
|
18
|
+
return { actual, expected, matches: actual === expected };
|
|
19
|
+
}
|
|
20
|
+
function resolveExpected(collab, caller) {
|
|
21
|
+
if (caller && collab.branch && caller !== collab.branch) {
|
|
22
|
+
throw new ToolError('ToolUsage', `expectedBranch '${caller}' contradicts COLLAB_MODE.branch '${collab.branch}'`, { callerExpectedBranch: caller, collabBranch: collab.branch });
|
|
23
|
+
}
|
|
24
|
+
const expected = caller ?? collab.branch;
|
|
25
|
+
if (!expected) {
|
|
26
|
+
throw new ToolError('ToolUsage', 'no branch to check: COLLAB_MODE.branch is empty and no expectedBranch supplied');
|
|
27
|
+
}
|
|
28
|
+
return expected;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=branch_guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch_guard.js","sourceRoot":"","sources":["../../src/lib/branch_guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,yEAAyE;AACzE,0EAA0E;AAC1E,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAgB,EAChB,MAAkB,EAClB,oBAA6B;IAE7B,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,MAAM,IAAI,SAAS,CACjB,gBAAgB,EAChB,oBAAoB,QAAQ,WAAW,MAAM,IAAI,iBAAiB,+CAA+C,EACjH,EAAE,QAAQ,EAAE,MAAM,EAAE,CACrB,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,MAAkB,EAClB,oBAA6B;IAE7B,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC7C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,eAAe,CAAC,MAAkB,EAAE,MAAe;IAC1D,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QACxD,MAAM,IAAI,SAAS,CACjB,WAAW,EACX,mBAAmB,MAAM,qCAAqC,MAAM,CAAC,MAAM,GAAG,EAC9E,EAAE,oBAAoB,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,CAC9D,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;IACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,SAAS,CACjB,WAAW,EACX,gFAAgF,CACjF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GitResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runGit(cwd: string, args: string[]): Promise<GitResult>;
|
|
7
|
+
export declare function currentBranch(cwd: string): Promise<string>;
|
|
8
|
+
export declare function aheadBehind(cwd: string, branch: string): Promise<{
|
|
9
|
+
hasUpstream: boolean;
|
|
10
|
+
ahead: number;
|
|
11
|
+
behind: number;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=git_runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git_runner.d.ts","sourceRoot":"","sources":["../../src/lib/git_runner.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAkBtE;AAGD,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIhE;AAID,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACtE,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAaD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ToolError } from '../types.js';
|
|
3
|
+
// Spawn `git` with the given args under `cwd`. Captures all output and the exit
|
|
4
|
+
// code. Never throws on non-zero exit — callers inspect `exitCode`. Throws only
|
|
5
|
+
// on process-spawn failures (e.g., git not installed).
|
|
6
|
+
export function runGit(cwd, args) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
9
|
+
const stdoutChunks = [];
|
|
10
|
+
const stderrChunks = [];
|
|
11
|
+
child.stdout.on('data', (c) => stdoutChunks.push(c));
|
|
12
|
+
child.stderr.on('data', (c) => stderrChunks.push(c));
|
|
13
|
+
child.on('error', (err) => {
|
|
14
|
+
reject(new ToolError('IO', `failed to spawn git: ${err.message}`));
|
|
15
|
+
});
|
|
16
|
+
child.on('close', (code) => {
|
|
17
|
+
resolve({
|
|
18
|
+
exitCode: code ?? -1,
|
|
19
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
|
|
20
|
+
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Read the current branch name. Returns empty string when detached HEAD.
|
|
26
|
+
export async function currentBranch(cwd) {
|
|
27
|
+
const r = await runGit(cwd, ['symbolic-ref', '--short', 'HEAD']);
|
|
28
|
+
if (r.exitCode === 0)
|
|
29
|
+
return r.stdout.trim();
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
// Compare local HEAD against origin/<branch>. Returns ahead/behind counts; if
|
|
33
|
+
// the upstream is unknown both counts are 0 and `hasUpstream: false`.
|
|
34
|
+
export async function aheadBehind(cwd, branch) {
|
|
35
|
+
const ref = `origin/${branch}`;
|
|
36
|
+
const exists = await runGit(cwd, ['rev-parse', '--verify', '--quiet', ref]);
|
|
37
|
+
if (exists.exitCode !== 0) {
|
|
38
|
+
return { hasUpstream: false, ahead: 0, behind: 0 };
|
|
39
|
+
}
|
|
40
|
+
const r = await runGit(cwd, ['rev-list', '--left-right', '--count', `${ref}...HEAD`]);
|
|
41
|
+
if (r.exitCode !== 0) {
|
|
42
|
+
return { hasUpstream: false, ahead: 0, behind: 0 };
|
|
43
|
+
}
|
|
44
|
+
const m = r.stdout.trim().match(/^(\d+)\s+(\d+)$/);
|
|
45
|
+
if (!m)
|
|
46
|
+
return { hasUpstream: true, ahead: 0, behind: 0 };
|
|
47
|
+
return { hasUpstream: true, behind: parseInt(m[1], 10), ahead: parseInt(m[2], 10) };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=git_runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git_runner.js","sourceRoot":"","sources":["../../src/lib/git_runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface VerbRetryResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
retries: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Invoke a mutating agent-collab.ps1 verb with concurrency-safe lock-held
|
|
9
|
+
* retry semantics.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists (codex r3 review of `2c661e4`): unconditionally unlinking
|
|
12
|
+
* `.agent/.lock` before every mutating spawn breaks the script's per-machine
|
|
13
|
+
* concurrency protection. If a sibling MCP / VS Code / manual PS process is
|
|
14
|
+
* legitimately mid-mutation, deleting their lock races them and risks
|
|
15
|
+
* interleaved state writes.
|
|
16
|
+
*
|
|
17
|
+
* Strategy:
|
|
18
|
+
* 1. Invoke the verb. If exit 0, return immediately.
|
|
19
|
+
* 2. If exit != 0 AND stderr matches "lock already held:", back off briefly
|
|
20
|
+
* and retry. The FROZEN script's `finally { Remove-Item .lock }` is the
|
|
21
|
+
* release path; on Linux pwsh 7 that unlink occasionally trails the
|
|
22
|
+
* child's exit by tens of milliseconds. After ~100-700 ms one of three
|
|
23
|
+
* things has happened:
|
|
24
|
+
* a) the unlink landed -> next attempt acquires cleanly
|
|
25
|
+
* b) a real sibling is still working -> we keep seeing the lock
|
|
26
|
+
* c) the script crashed before unlink and left a truly stale file
|
|
27
|
+
* Cases (b) and (c) are externally indistinguishable from a single
|
|
28
|
+
* machine without consulting other processes; both surface the same
|
|
29
|
+
* `LockHeld` error to the caller, who decides how to react (wait, page
|
|
30
|
+
* a human, manually clear if confident no sibling exists).
|
|
31
|
+
* 3. After all retries are exhausted, throw `ToolError('LockHeld', ...)`
|
|
32
|
+
* with the retry count and the original stderr/stdout. The lock file
|
|
33
|
+
* is NEVER deleted by this helper.
|
|
34
|
+
*
|
|
35
|
+
* The FROZEN agent-collab.ps1's `Acquire-Lock` still catches truly concurrent
|
|
36
|
+
* holders via `[System.IO.File]::Open(...CreateNew...)`. We do not weaken
|
|
37
|
+
* that guarantee here.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runMutatingVerbWithLockRetry(ps: string, args: string[]): Promise<VerbRetryResult>;
|
|
40
|
+
//# sourceMappingURL=lock_retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock_retry.d.ts","sourceRoot":"","sources":["../../src/lib/lock_retry.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,eAAe,CAAC,CA+B1B"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { runPsScript } from './ps_runner.js';
|
|
2
|
+
import { ToolError } from '../types.js';
|
|
3
|
+
// Pattern emitted by FROZEN agent-collab.ps1 Acquire-Lock when .agent/.lock
|
|
4
|
+
// already exists. The exact message text:
|
|
5
|
+
// "lock already held: <path> -- another agent-collab process is running on
|
|
6
|
+
// this machine; remove file if stale"
|
|
7
|
+
const LOCK_HELD_PATTERN = /lock already held:/i;
|
|
8
|
+
// Backoff schedule for the retry loop. Total worst-case wait = 700 ms.
|
|
9
|
+
// Picked to be:
|
|
10
|
+
// - long enough to let a sibling PS process's `finally { Remove-Item .lock }`
|
|
11
|
+
// land its unlink on a slow Linux runner;
|
|
12
|
+
// - short enough that legitimately-stuck locks surface a structured error
|
|
13
|
+
// within the same MCP tool invocation rather than hanging.
|
|
14
|
+
const RETRY_DELAYS_MS = [100, 200, 400];
|
|
15
|
+
/**
|
|
16
|
+
* Invoke a mutating agent-collab.ps1 verb with concurrency-safe lock-held
|
|
17
|
+
* retry semantics.
|
|
18
|
+
*
|
|
19
|
+
* Why this exists (codex r3 review of `2c661e4`): unconditionally unlinking
|
|
20
|
+
* `.agent/.lock` before every mutating spawn breaks the script's per-machine
|
|
21
|
+
* concurrency protection. If a sibling MCP / VS Code / manual PS process is
|
|
22
|
+
* legitimately mid-mutation, deleting their lock races them and risks
|
|
23
|
+
* interleaved state writes.
|
|
24
|
+
*
|
|
25
|
+
* Strategy:
|
|
26
|
+
* 1. Invoke the verb. If exit 0, return immediately.
|
|
27
|
+
* 2. If exit != 0 AND stderr matches "lock already held:", back off briefly
|
|
28
|
+
* and retry. The FROZEN script's `finally { Remove-Item .lock }` is the
|
|
29
|
+
* release path; on Linux pwsh 7 that unlink occasionally trails the
|
|
30
|
+
* child's exit by tens of milliseconds. After ~100-700 ms one of three
|
|
31
|
+
* things has happened:
|
|
32
|
+
* a) the unlink landed -> next attempt acquires cleanly
|
|
33
|
+
* b) a real sibling is still working -> we keep seeing the lock
|
|
34
|
+
* c) the script crashed before unlink and left a truly stale file
|
|
35
|
+
* Cases (b) and (c) are externally indistinguishable from a single
|
|
36
|
+
* machine without consulting other processes; both surface the same
|
|
37
|
+
* `LockHeld` error to the caller, who decides how to react (wait, page
|
|
38
|
+
* a human, manually clear if confident no sibling exists).
|
|
39
|
+
* 3. After all retries are exhausted, throw `ToolError('LockHeld', ...)`
|
|
40
|
+
* with the retry count and the original stderr/stdout. The lock file
|
|
41
|
+
* is NEVER deleted by this helper.
|
|
42
|
+
*
|
|
43
|
+
* The FROZEN agent-collab.ps1's `Acquire-Lock` still catches truly concurrent
|
|
44
|
+
* holders via `[System.IO.File]::Open(...CreateNew...)`. We do not weaken
|
|
45
|
+
* that guarantee here.
|
|
46
|
+
*/
|
|
47
|
+
export async function runMutatingVerbWithLockRetry(ps, args) {
|
|
48
|
+
let retries = 0;
|
|
49
|
+
let res = await runPsScript(ps, args);
|
|
50
|
+
while (res.exitCode !== 0 &&
|
|
51
|
+
LOCK_HELD_PATTERN.test(res.stderr || '') &&
|
|
52
|
+
retries < RETRY_DELAYS_MS.length) {
|
|
53
|
+
await delay(RETRY_DELAYS_MS[retries]);
|
|
54
|
+
retries += 1;
|
|
55
|
+
res = await runPsScript(ps, args);
|
|
56
|
+
}
|
|
57
|
+
if (res.exitCode !== 0 && LOCK_HELD_PATTERN.test(res.stderr || '')) {
|
|
58
|
+
throw new ToolError('LockHeld', `agent-collab.ps1 reported lock held after ${retries} retries (${RETRY_DELAYS_MS.slice(0, retries).reduce((a, b) => a + b, 0)} ms). Another agent-collab process may be mid-mutation; inspect .agent/.lock manually before clearing.`, {
|
|
59
|
+
retries,
|
|
60
|
+
retryDelaysMs: RETRY_DELAYS_MS,
|
|
61
|
+
stderr: res.stderr,
|
|
62
|
+
stdout: res.stdout,
|
|
63
|
+
exitCode: res.exitCode,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
exitCode: res.exitCode,
|
|
68
|
+
stdout: res.stdout,
|
|
69
|
+
stderr: res.stderr,
|
|
70
|
+
retries,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function delay(ms) {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=lock_retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock_retry.js","sourceRoot":"","sources":["../../src/lib/lock_retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,4EAA4E;AAC5E,0CAA0C;AAC1C,6EAA6E;AAC7E,yCAAyC;AACzC,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEhD,uEAAuE;AACvE,gBAAgB;AAChB,gFAAgF;AAChF,8CAA8C;AAC9C,4EAA4E;AAC5E,+DAA+D;AAC/D,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AASxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,EAAU,EACV,IAAc;IAEd,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,GAAG,GAAa,MAAM,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChD,OACE,GAAG,CAAC,QAAQ,KAAK,CAAC;QAClB,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;QACxC,OAAO,GAAG,eAAe,CAAC,MAAM,EAChC,CAAC;QACD,MAAM,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,CAAC;QACb,GAAG,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,SAAS,CACjB,UAAU,EACV,6CAA6C,OAAO,aAAa,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,wGAAwG,EACrO;YACE,OAAO;YACP,aAAa,EAAE,eAAe;YAC9B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;SACvB,CACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO;KACR,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CollabState, CollabMode } from '../types.js';
|
|
2
|
+
export declare function assertPostState(repoPath: string, expected: ReadonlyArray<CollabState>, verbDescription: string, context?: {
|
|
3
|
+
exitCode?: number;
|
|
4
|
+
stdout?: string;
|
|
5
|
+
stderr?: string;
|
|
6
|
+
}): CollabMode;
|
|
7
|
+
//# sourceMappingURL=post_state_verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post_state_verify.d.ts","sourceRoot":"","sources":["../../src/lib/post_state_verify.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAwBjE,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,EACpC,eAAe,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACA,UAAU,CAsBZ"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readCollabModeOrThrow } from './schema_guard.js';
|
|
2
|
+
import { ToolError } from '../types.js';
|
|
3
|
+
// Post-state verification for mutating tools.
|
|
4
|
+
//
|
|
5
|
+
// Background (mcp-lite-v1-fix-pathext): the FROZEN agent-collab.ps1 calls
|
|
6
|
+
// `git` repeatedly. On a misconfigured Windows host where the spawned
|
|
7
|
+
// PowerShell cannot resolve `git` (the original repro: MCP SDK strips
|
|
8
|
+
// PATHEXT — see ps_runner.ts buildPsEnv), the script's `Write-Error` from
|
|
9
|
+
// the missing `git` does NOT cause a non-zero exit code by default. The
|
|
10
|
+
// MCP tool sees `exitCode: 0` and `stderr: <some PowerShell error spew>`
|
|
11
|
+
// and previously trusted it as "verb succeeded" — but on disk the state
|
|
12
|
+
// machine never advanced. The user then sees a phantom green tool call
|
|
13
|
+
// with no real Git commit and no real state change.
|
|
14
|
+
//
|
|
15
|
+
// This helper closes that hole: after any mutating verb returns exit=0
|
|
16
|
+
// and the lock-retry logic accepts it, we re-read `.agent/COLLAB_MODE.yaml`
|
|
17
|
+
// and assert the on-disk state is one of the values the caller expected.
|
|
18
|
+
// If not, we throw `PostStateMismatch` with both the expected set and the
|
|
19
|
+
// actual observed state so the operator can investigate.
|
|
20
|
+
//
|
|
21
|
+
// `assertPostState` returns the freshly-read CollabMode so callers can
|
|
22
|
+
// reuse the post-state fields (lease_expires_at, owner, etc.) without
|
|
23
|
+
// re-reading the file again themselves.
|
|
24
|
+
export function assertPostState(repoPath, expected, verbDescription, context) {
|
|
25
|
+
const collab = readCollabModeOrThrow(repoPath);
|
|
26
|
+
if (!expected.includes(collab.state)) {
|
|
27
|
+
const head = `${verbDescription} reported success but post-state is '${collab.state}' (expected one of: ${expected.join(', ')})`;
|
|
28
|
+
const hint = collab.state === 'waiting_for_codex' || collab.state === 'waiting_for_claude' || collab.state === 'idle'
|
|
29
|
+
? ' — the FROZEN agent-collab.ps1 likely failed silently (e.g. missing git on PATH inside the spawned PowerShell, see ps_runner buildPsEnv).'
|
|
30
|
+
: '';
|
|
31
|
+
throw new ToolError('PostStateMismatch', head + hint, {
|
|
32
|
+
expected: Array.from(expected),
|
|
33
|
+
actual: collab.state,
|
|
34
|
+
phase: collab.phase,
|
|
35
|
+
owner: collab.owner,
|
|
36
|
+
verbExitCode: context?.exitCode,
|
|
37
|
+
verbStdout: context?.stdout?.slice(0, 1000) ?? '',
|
|
38
|
+
verbStderr: context?.stderr?.slice(0, 1000) ?? '',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return collab;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=post_state_verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post_state_verify.js","sourceRoot":"","sources":["../../src/lib/post_state_verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAA2B,MAAM,aAAa,CAAC;AAEjE,8CAA8C;AAC9C,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,sEAAsE;AACtE,0EAA0E;AAC1E,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AACxE,uEAAuE;AACvE,oDAAoD;AACpD,EAAE;AACF,uEAAuE;AACvE,4EAA4E;AAC5E,yEAAyE;AACzE,0EAA0E;AAC1E,yDAAyD;AACzD,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,wCAAwC;AAExC,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,QAAoC,EACpC,eAAuB,EACvB,OAIC;IAED,MAAM,MAAM,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAC/C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,GAAG,eAAe,wCAAwC,MAAM,CAAC,KAAK,uBAAuB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QACjI,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,KAAK,mBAAmB,IAAI,MAAM,CAAC,KAAK,KAAK,oBAAoB,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM;YACnH,CAAC,CAAC,2IAA2I;YAC7I,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,SAAS,CACjB,mBAAmB,EACnB,IAAI,GAAG,IAAI,EACX;YACE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC9B,MAAM,EAAE,MAAM,CAAC,KAAK;YACpB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,YAAY,EAAE,OAAO,EAAE,QAAQ;YAC/B,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE;YACjD,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE;SAClD,CACF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PsResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildPsEnv(parentEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
7
|
+
export declare function runPsScript(scriptPath: string, scriptArgs: string[], cwd?: string): Promise<PsResult>;
|
|
8
|
+
//# sourceMappingURL=ps_runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ps_runner.d.ts","sourceRoot":"","sources":["../../src/lib/ps_runner.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAmCD,wBAAgB,UAAU,CACxB,SAAS,GAAE,MAAM,CAAC,UAAwB,GACzC,MAAM,CAAC,UAAU,CAgBnB;AAOD,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CAYnB"}
|