@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +157 -0
  4. package/VERSION +1 -0
  5. package/bin/agent-collab-mcp-lite.js +7 -0
  6. package/dist/lib/branch_guard.d.ts +11 -0
  7. package/dist/lib/branch_guard.d.ts.map +1 -0
  8. package/dist/lib/branch_guard.js +30 -0
  9. package/dist/lib/branch_guard.js.map +1 -0
  10. package/dist/lib/git_runner.d.ts +13 -0
  11. package/dist/lib/git_runner.d.ts.map +1 -0
  12. package/dist/lib/git_runner.js +49 -0
  13. package/dist/lib/git_runner.js.map +1 -0
  14. package/dist/lib/lock_retry.d.ts +40 -0
  15. package/dist/lib/lock_retry.d.ts.map +1 -0
  16. package/dist/lib/lock_retry.js +76 -0
  17. package/dist/lib/lock_retry.js.map +1 -0
  18. package/dist/lib/post_state_verify.d.ts +7 -0
  19. package/dist/lib/post_state_verify.d.ts.map +1 -0
  20. package/dist/lib/post_state_verify.js +43 -0
  21. package/dist/lib/post_state_verify.js.map +1 -0
  22. package/dist/lib/ps_runner.d.ts +8 -0
  23. package/dist/lib/ps_runner.d.ts.map +1 -0
  24. package/dist/lib/ps_runner.js +118 -0
  25. package/dist/lib/ps_runner.js.map +1 -0
  26. package/dist/lib/repo_root.d.ts +3 -0
  27. package/dist/lib/repo_root.d.ts.map +1 -0
  28. package/dist/lib/repo_root.js +44 -0
  29. package/dist/lib/repo_root.js.map +1 -0
  30. package/dist/lib/result_envelope.d.ts +3 -0
  31. package/dist/lib/result_envelope.d.ts.map +1 -0
  32. package/dist/lib/result_envelope.js +25 -0
  33. package/dist/lib/result_envelope.js.map +1 -0
  34. package/dist/lib/schema_guard.d.ts +3 -0
  35. package/dist/lib/schema_guard.d.ts.map +1 -0
  36. package/dist/lib/schema_guard.js +43 -0
  37. package/dist/lib/schema_guard.js.map +1 -0
  38. package/dist/server.d.ts +3 -0
  39. package/dist/server.d.ts.map +1 -0
  40. package/dist/server.js +156 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/tools/claim_review.d.ts +30 -0
  43. package/dist/tools/claim_review.d.ts.map +1 -0
  44. package/dist/tools/claim_review.js +113 -0
  45. package/dist/tools/claim_review.js.map +1 -0
  46. package/dist/tools/close.d.ts +26 -0
  47. package/dist/tools/close.d.ts.map +1 -0
  48. package/dist/tools/close.js +60 -0
  49. package/dist/tools/close.js.map +1 -0
  50. package/dist/tools/fetch.d.ts +27 -0
  51. package/dist/tools/fetch.d.ts.map +1 -0
  52. package/dist/tools/fetch.js +72 -0
  53. package/dist/tools/fetch.js.map +1 -0
  54. package/dist/tools/push_verdict.d.ts +32 -0
  55. package/dist/tools/push_verdict.d.ts.map +1 -0
  56. package/dist/tools/push_verdict.js +123 -0
  57. package/dist/tools/push_verdict.js.map +1 -0
  58. package/dist/tools/read_report.d.ts +17 -0
  59. package/dist/tools/read_report.d.ts.map +1 -0
  60. package/dist/tools/read_report.js +37 -0
  61. package/dist/tools/read_report.js.map +1 -0
  62. package/dist/tools/status.d.ts +31 -0
  63. package/dist/tools/status.d.ts.map +1 -0
  64. package/dist/tools/status.js +41 -0
  65. package/dist/tools/status.js.map +1 -0
  66. package/dist/tools/write_verdict.d.ts +39 -0
  67. package/dist/tools/write_verdict.d.ts.map +1 -0
  68. package/dist/tools/write_verdict.js +92 -0
  69. package/dist/tools/write_verdict.js.map +1 -0
  70. package/dist/types.d.ts +52 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +24 -0
  73. package/dist/types.js.map +1 -0
  74. 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"}