@ddtcorex/dsh-maestro-supervisor 0.5.4 → 0.6.1
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 +168 -16
- package/cordis.patch.yml +11 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +3 -0
- package/lib/cli.js +60 -1
- package/lib/client.js +150 -0
- package/lib/debug-agent.js +9 -5
- package/lib/health-poller.js +44 -7
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -3
- package/lib/paths.d.ts +6 -0
- package/lib/paths.js +44 -0
- package/lib/plugin.d.ts +42 -0
- package/lib/plugin.js +290 -0
- package/lib/resume.d.ts +20 -1
- package/lib/resume.js +181 -9
- package/lib/supervisor.d.ts +12 -0
- package/lib/supervisor.js +205 -16
- package/lib/types/client/auto-reload.d.ts +8 -0
- package/lib/types/client/auto-reload.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/package.json +31 -5
package/README.md
CHANGED
|
@@ -1,21 +1,94 @@
|
|
|
1
1
|
# dsh-maestro-supervisor
|
|
2
2
|
|
|
3
|
-
Supervisor
|
|
3
|
+
Supervisor for DSH Web resilience — **Phase 1 Guard & Report + Phase 3 Auto-Resume & Auto-Reload**.
|
|
4
4
|
|
|
5
|
-
Runs **outside** the `pnpm → sh → node` tree
|
|
5
|
+
Runs **outside** the `pnpm → sh → node` tree (systemd daemon) to survive tree crashes, plus **inside** `dsh web` as a host+client Cordis plugin to auto-resume interrupted sessions and auto-reload the browser after restart.
|
|
6
|
+
|
|
7
|
+
- **Daemon:** Polls `:3080` every 3s, keeps last-known-good (LKG) snapshots (`~/.dsh/.supervisor/lkg/`, `rotate 3`, `sha256` verify, `df` >500MB guard), auto-rollbacks on crash (`debounce 60s`, `flock` lock), writes `report-<ts>.md` (health + `git diff` + log tail), and notifies via Telegram (loose, never blocks).
|
|
8
|
+
- **Host plugin:** `runAutoResume()` 8s after boot — `findInterrupted` (tail 100) + `findDanglingOpenTurns` (full scan for recent sessions) within `autoResumeWithin` (default 5m) → `agents.resume({resumeSessionId, agentOptions: {provider,model}})` recovered from `request/context` → `followup('continue')`. Loopback RPC `POST /dsh-maestro-supervisor-resume/{scan,resume}` (authority `loopback`) for the daemon (`resumeViaRpc`).
|
|
9
|
+
- **Client plugin:** Hybrid auto-reload — `fetch HEAD /` polling 1s on `offline`/`WebSocket close`/`visibilitychange` → `200` → `location.reload()`. Served as `window.__ModuleLoader__.load` bundle at `/plugins/@ddtcorex/dsh-maestro-supervisor/client.js` via `dsh.client`.
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
13
|
+
### 1. Build
|
|
14
|
+
|
|
9
15
|
```bash
|
|
10
16
|
pnpm --dir packages/dsh-maestro-supervisor install
|
|
11
|
-
pnpm --dir packages/dsh-maestro-supervisor build
|
|
17
|
+
pnpm --dir packages/dsh-maestro-supervisor build # tsc host + tsc client + node scripts/build-client.mjs → lib/ + lib/client.js
|
|
18
|
+
pnpm --dir packages/dsh-maestro-supervisor verify # tsc --noEmit host + client
|
|
19
|
+
pnpm --dir packages/dsh-maestro-supervisor test # 82 tests
|
|
20
|
+
test -f packages/dsh-maestro-supervisor/lib/index.js
|
|
21
|
+
test -f packages/dsh-maestro-supervisor/lib/client.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`pnpm build` is required after any `src/` change; `lib/` is committed. The client needs both `tsc` steps and the `build-client.mjs` wrapper — plain `tsc` alone leaves `lib/client.js` as a bare ES module and `dsh web` will fail with `exports no "./client" bundle`.
|
|
25
|
+
|
|
26
|
+
### 2. Add to DSH Web profile (host + client)
|
|
27
|
+
|
|
28
|
+
The package declares `dsh.client` (`platform: web`, `inject: ["@deepseek-ai/dsh-client-runtime"]`) so the browser half is auto-loaded — no extra `dsh.client` flag needed.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
dsh plugin --profile web add @ddtcorex/dsh-maestro-supervisor
|
|
32
|
+
# or manually:
|
|
33
|
+
# edit ~/.dsh/profiles/web/package.json:
|
|
34
|
+
# "@ddtcorex/dsh-maestro-supervisor": "link:<workspace-root>/packages/dsh-maestro-supervisor"
|
|
35
|
+
pnpm --dir ~/.dsh/profiles/web install
|
|
36
|
+
ls -l ~/.dsh/profiles/web/node_modules/@ddtcorex/dsh-maestro-supervisor # → .../packages/dsh-maestro-supervisor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Pre-flight (required):** before adding to a live profile's `bundles`, dry-boot must pass:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
DSH_HOME=$(mktemp -d) pnpm --dir deepseek-harness dsh web --port 0 &
|
|
43
|
+
# wait for "dsh web: http://127.0.0.1:<port>" and curl 200, then kill
|
|
44
|
+
# This catches load-time failures (missing lib/index.js, stale build, bad cordis.patch.yml)
|
|
45
|
+
# that no in-code try/catch can catch. See dsh-safe-web-update skill.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This exact failure class caused `dsh web` outages on 2026-08-27 (missing `lib/index.js`). See `AGENTS.md` Conventions.
|
|
12
49
|
|
|
13
|
-
|
|
50
|
+
### 3. Systemd daemon (optional, for crash detection outside the tree)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
14
53
|
bash packages/dsh-maestro-supervisor/scripts/install-systemd.sh
|
|
15
|
-
systemctl --user daemon-reload
|
|
54
|
+
systemctl --user daemon-reload
|
|
55
|
+
systemctl --user enable --now dsh-web-supervisor
|
|
56
|
+
systemctl --user status dsh-web-supervisor
|
|
57
|
+
journalctl --user -u dsh-web-supervisor -f
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The template leaves `Environment=TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` commented — uncomment via `systemctl --user edit dsh-web-supervisor` if you want Telegram, otherwise it logs only.
|
|
61
|
+
|
|
62
|
+
To run without systemd (foreground, for debugging):
|
|
16
63
|
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
```bash
|
|
65
|
+
node packages/dsh-maestro-supervisor/lib/index.js daemon # poll every 3s
|
|
66
|
+
node packages/dsh-maestro-supervisor/lib/index.js status
|
|
67
|
+
node packages/dsh-maestro-supervisor/lib/index.js logs --tail 50
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
All `autoResumeWithin` values are **minutes** when given as `number` (e.g. `5` → 5 minutes). Strings support `30s`/`5m`/`1h`. Precedence (highest first):
|
|
73
|
+
|
|
74
|
+
1. **Cordis config** (`cordis.patch.yml` `config:` or `apply(ctx, config)`) — explicit per-install.
|
|
75
|
+
2. **Env** `DSH_SUPERVISOR_AUTO_RESUME` / `DSH_SUPERVISOR_RESUME_WITHIN` (bare `5` in env → 5m for ergonomics).
|
|
76
|
+
3. **Supervisor config** `~/.dsh/.supervisor/config.json` (`autoResumeEnabled`, `autoResumeWithin`).
|
|
77
|
+
4. **Maestro settings** `~/.dsh/maestro/settings.json` (`domains.supervisor.*`).
|
|
78
|
+
5. **Default:** `true` / `5`.
|
|
79
|
+
|
|
80
|
+
| Key | Type | Default | Env | File | Notes |
|
|
81
|
+
|-----|------|---------|-----|------|-------|
|
|
82
|
+
| `autoResumeEnabled` | `boolean` | `true` | `DSH_SUPERVISOR_AUTO_RESUME` (`1`/`true`/`yes`/`on`/`enabled` vs `0`/`false`/`no`/…) | `config.json: autoResumeEnabled`, `settings.json: domains.supervisor.autoResumeEnabled` | `false` → notify only |
|
|
83
|
+
| `autoResumeWithin` | `number` (minutes) or `string` (`5m`) | `5` | `DSH_SUPERVISOR_RESUME_WITHIN` | `config.json: autoResumeWithin`, `settings.json: domains.supervisor.autoResumeWithin` | Window for `findInterrupted`/`findDangling` (mtime + `event.time`) |
|
|
84
|
+
|
|
85
|
+
Example `~/.dsh/.supervisor/config.json`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"autoResumeWithin": 5,
|
|
90
|
+
"autoResumeEnabled": true
|
|
91
|
+
}
|
|
19
92
|
```
|
|
20
93
|
|
|
21
94
|
## CLI
|
|
@@ -23,22 +96,101 @@ setsid node packages/dsh-maestro-supervisor/lib/index.js daemon &
|
|
|
23
96
|
```bash
|
|
24
97
|
node packages/dsh-maestro-supervisor/lib/index.js --help
|
|
25
98
|
node packages/dsh-maestro-supervisor/lib/index.js status
|
|
26
|
-
node packages/dsh-maestro-supervisor/lib/index.js daemon
|
|
99
|
+
node packages/dsh-maestro-supervisor/lib/index.js daemon # poll 3s, debounce 60s
|
|
100
|
+
node packages/dsh-maestro-supervisor/lib/index.js logs --tail 50
|
|
101
|
+
node packages/dsh-maestro-supervisor/lib/index.js rollback --latest
|
|
27
102
|
```
|
|
28
103
|
|
|
29
|
-
|
|
104
|
+
## RPC (loopback only, `authority: loopback`)
|
|
30
105
|
|
|
31
|
-
|
|
106
|
+
```bash
|
|
107
|
+
# Scan (findInterrupted only, tail 100)
|
|
108
|
+
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/scan -X POST \
|
|
109
|
+
-H 'content-type: application/json' \
|
|
110
|
+
-d '{"type":"client-request","rpcId":"r1","method":"scan","payload":{"withinMs":300000}}'
|
|
111
|
+
# → {"type":"server-response","rpcId":"r1","result":{"ok":true,"value":{"scanned":425,"interrupted":[]}}}
|
|
112
|
+
|
|
113
|
+
# Resume (re-attaches agent + followup continue, recovers provider/model)
|
|
114
|
+
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/resume -X POST \
|
|
115
|
+
-H 'content-type: application/json' \
|
|
116
|
+
-d '{"type":"client-request","rpcId":"r2","method":"resume","payload":{"ids":["--example-project--/session-abc"]}}'
|
|
117
|
+
# → {"type":"server-response","rpcId":"r2","result":{"ok":true,"value":{"resumed":["--example-project--/session-abc"]}}}
|
|
118
|
+
# or {"ok":false,"error":{"code":"bad-request","message":"resume requires at least one session id"}}
|
|
119
|
+
```
|
|
32
120
|
|
|
33
|
-
`
|
|
121
|
+
The daemon uses `resumeViaRpc()` (`supervisor.ts:24`) which POSTs the same envelope to `http://127.0.0.1:3080/dsh-maestro-supervisor-resume/resume` with `fetch` and validates `server-response` + `rpcId` + `result.ok`.
|
|
34
122
|
|
|
35
|
-
|
|
36
|
-
- **Hard mode (optional):** `package.json` add `"@ddtcorex/dsh-maestro-notifier": "workspace:^0.1.0"` + `pnpm-workspace.yaml` `packages: ["../dsh-maestro-notifier"]` — then `pnpm install` links it and every `notify()` hits the hard import.
|
|
123
|
+
## Auto-Resume Details
|
|
37
124
|
|
|
38
|
-
|
|
125
|
+
- **When:** `apply()` sets `setTimeout 8000` after boot, then `runAutoResume()` — only safe right after fresh boot when `dsh web` is sole owner (an open turn found then cannot belong to a still-running generation). `resumeInterrupted` additionally checks `agents.get(sessionId)` and skips if already live.
|
|
126
|
+
- **What:** `findInterrupted` (tail 100, looks for `turn/end` with `reason.kind === 'interrupted'` at `time` within window) + `findDanglingOpenTurns` (full scan for recent sessions, looks for `turn/start` without matching `turn/end` at `time` within window) → `merged = Set([...interrupted, ...dangling])` → `resumeInterrupted` for each id.
|
|
127
|
+
- **How:** `agents.get(sessionId)` if live → `followup('continue')`; else `sessionPersistence.load(sessionId)` → find `request/context` with `provider`/`model` → `agents.resume({resumeSessionId, agentOptions})` → `followup('continue')`. If `load` fails, still resumes without `agentOptions` (degrades). Returns `string[] resumed` and logs `sent continue trigger`.
|
|
128
|
+
- **Subagents:** `findDangling` does **full log scan** for recent sessions (mtime within window, 1-2 files) — not tail — because subagent `b6487e33` had its only `turn/start` at seq 6 at the very beginning of a 1906-line log, missed by `tail -100`. `findInterrupted` stays tail 100 (interrupted closer is always at tail). The mtime pre-filter keeps full scans cheap (previously 5.5s for 425 sessions without it).
|
|
39
129
|
|
|
40
|
-
##
|
|
130
|
+
## Auto-Reload Details (Hybrid)
|
|
131
|
+
|
|
132
|
+
- **Client** (`src/client/auto-reload.ts`, `lib/client.js` via `window.__ModuleLoader__.load`): `ctx.effect` hooks `WebSocket` (patches `window.WebSocket` to catch `close` for same-origin DSH ws), `offline`/`online`, `visibilitychange` → `setInterval(fetch HEAD / 1s)` when down → `200` → `location.reload()` (once, `reloading` guard). Also checks `HEAD /` on load in case the page was opened while down.
|
|
133
|
+
- **Host** (`supervisor.ts` `pollHealth` 3s + `notify`, `plugin.ts` `runAutoResume`): health check + restart + notify is the host half; together with client polling they cover manual, supervisor, and systemd restarts without `F5`. No extra host push channel needed — client polling is primary, host health is secondary; the `window.__ModuleLoader__` bundle is served at `/plugins/@ddtcorex/dsh-maestro-supervisor/client.js` via `ClientModuleRegistry` (`dsh.client` + `exports["./client"]`).
|
|
134
|
+
|
|
135
|
+
## Verification
|
|
41
136
|
|
|
42
137
|
```bash
|
|
43
|
-
|
|
138
|
+
# Build & unit
|
|
139
|
+
pnpm --dir packages/dsh-maestro-supervisor verify # host + client
|
|
140
|
+
pnpm --dir packages/dsh-maestro-supervisor test # 82 tests
|
|
141
|
+
test -f packages/dsh-maestro-supervisor/lib/index.js
|
|
142
|
+
test -f packages/dsh-maestro-supervisor/lib/client.js
|
|
143
|
+
curl -s http://127.0.0.1:3080/plugins/@ddtcorex/dsh-maestro-supervisor/client.js | grep -c "window.location.reload" # 2
|
|
144
|
+
|
|
145
|
+
# Live
|
|
146
|
+
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080/ # 200
|
|
147
|
+
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/scan -X POST -H 'content-type: application/json' -d '{"type":"client-request","rpcId":"t","method":"scan","payload":{"withinMs":300000}}' | head -c 200
|
|
148
|
+
node --input-type=module -e "import {findDanglingOpenTurns} from './packages/dsh-maestro-supervisor/lib/resume.js'; console.log(await findDanglingOpenTurns(undefined,{withinMs:5*60*1000}))"
|
|
149
|
+
# Create a real dangling: pnpm --dir deepseek-harness dsh --profile headless "Run bash synchronously sleep 60" & sleep 4; kill $!; node -e "...findDangling..." # should be 1
|
|
150
|
+
# After restart, it should have turn/end interrupted → continue → turn2
|
|
44
151
|
```
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
| Symptom | Cause | Fix |
|
|
156
|
+
|---------|-------|-----|
|
|
157
|
+
| `Cannot find package '.../dsh-maestro-supervisor/index.js'` | `pnpm build` not run or `lib/` stale | `pnpm --dir packages/dsh-maestro-supervisor build && pnpm --dir ~/.dsh/profiles/web install` |
|
|
158
|
+
| `exports no "./client" bundle` / `client bundle not found` | Missing `lib/client.js` or `exports["./client"]` | `pnpm build` (runs `tsc` + `tsc -p tsconfig.client.json` + `node scripts/build-client.mjs`), check `package.json` `exports` and `dsh.client`, `test -f lib/client.js`, `curl .../client.js` |
|
|
159
|
+
| `EADDRINUSE ::3000` on `dsh web --port 0` | Old `MainThread` still holds `:3000`+`:3080` | `ss -tlnp | grep 3080` → pid, `kill <pid>` (same pid holds both), wait `ss` free. Never `pkill -f "dsh web"` — it kills the test shell. |
|
|
160
|
+
| `uses .jsonl but backend is zstd` | Hand-written `session.jsonl` while backend is `zstd` | Use `zstd -c plain.jsonl > session.jsonl.zstd` or `JsonlSessionPersistence` API. Never hand-write opposite encoding — `listArtifacts` checks every project dir on boot and one stray file blocks all of `dsh web`. |
|
|
161
|
+
| `first frame is not exactly one header line` | zstd without `type: session` header | Use `toHeaderLine` + `compressZstdFrame(header)` + `compressZstdFrame(body)` as in `encodeMaterialization`. |
|
|
162
|
+
| `findDangling` 0 but subagent still open | Tail window too small (before `63b7719`) | Fixed: `findDangling` now full-scans recent sessions (mtime within window). `findInterrupted` stays tail 100. |
|
|
163
|
+
| `resumed: []` or `RESUME FAILED` | `agents.resume` failed (no persistence, no provider/model) | Check `session.jsonl.zstd` exists and `zstd -d -c ... \| head -n 1` is valid header. `request/context` with `provider`/`model` is recovered — if missing, still resumes without `agentOptions`. |
|
|
164
|
+
| `RESUME SKIPPED` | No session within `autoResumeWithin` window | Increase `autoResumeWithin` to `10`/`"10m"`, check `config.json` and `DSH_SUPERVISOR_RESUME_WITHIN`, verify with `withinMs: 60*60*1000`. |
|
|
165
|
+
| Page does not reload after restart | `lib/client.js` not served or browser cache | `curl .../client.js | grep -c "window.location.reload"` → 2, hard refresh `Ctrl+Shift+R`, check `window.__ModuleLoader__` in console. Client polls `HEAD /` 1s on `offline`/`WS close`. |
|
|
166
|
+
| `dangling` found but `agents.get` says live | Session still live in current process (not a crash) | `findDangling` is only safe right after fresh boot when `dsh web` is sole owner. `resumeInterrupted` skips if `agents.get` is live — correct, wait for next boot. |
|
|
167
|
+
|
|
168
|
+
## Known Issues
|
|
169
|
+
|
|
170
|
+
- **Manual `session.jsonl` vs `zstd`:** One stray opposite-encoding file under `~/.dsh/sessions/` blocks the entire `workspace` `listArtifacts` on every boot (`encodingMismatch`). See Troubleshooting.
|
|
171
|
+
- **Tail vs full scan:** Before `63b7719`, `b6487e33` subagent missed because its only `turn/start` was at seq 6 at the very beginning of a 1906-line log. Fixed, but if you add a new scan variant, reuse `readSessionAllLines` with mtime pre-filter.
|
|
172
|
+
- **Port pair:** One `MainThread` holds `:3080`+`:3000`. Use `ss -tlnp` + `kill <pid>` for the one pid, not `pkill -f`.
|
|
173
|
+
- **Client bundling:** `lib/client.js` must be `window.__ModuleLoader__.load` wrapper via `scripts/build-client.mjs`, not bare `export`. Add new client files under `src/client/` and ensure `tsconfig.client.json` includes them, then `pnpm build`.
|
|
174
|
+
- **Config precedence:** `cordis.patch.yml` `config:` > env (`DSH_SUPERVISOR_RESUME_WITHIN` bare `5` → 5m) > `config.json` > `settings.json` > default. See `plugin.ts:71` and `supervisor.ts:82`.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
pnpm --dir packages/dsh-maestro-supervisor verify
|
|
180
|
+
pnpm --dir packages/dsh-maestro-supervisor test
|
|
181
|
+
pnpm --dir packages/dsh-maestro-supervisor build
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For daemon changes: `DSH_HOME=$(mktemp -d) pnpm --dir deepseek-harness dsh web --port 0` + corrupt `settings.json` → assert `report` + `rollback`.
|
|
185
|
+
|
|
186
|
+
## Telegram
|
|
187
|
+
|
|
188
|
+
Loose by default: `notifier.ts` tries `import('@ddtcorex/dsh-maestro-notifier')`, then `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` env, then `console.log`. Enable via `systemctl --user edit dsh-web-supervisor` → uncomment `Environment=TELEGRAM_*` → `daemon-reload` + `restart`.
|
|
189
|
+
|
|
190
|
+
Hard mode (optional): `package.json` add `"@ddtcorex/dsh-maestro-notifier": "workspace:^0.1.0"` + `pnpm-workspace.yaml` `packages: ["../dsh-maestro-notifier"]` → `pnpm install` links it.
|
|
191
|
+
|
|
192
|
+
## See Also
|
|
193
|
+
|
|
194
|
+
- Spec: `<workspace-root>/docs/specs/2026-08-27-dsh-web-resilience-design.md`
|
|
195
|
+
- Skill: `maestro-skills/skills/dsh-safe-web-update/` (`restart-dsh-web.sh` with `dry_boot_and_verify()` and `--auto`)
|
|
196
|
+
- Client bundling: `dsh-maestro-mobile` (`scripts/build-client.mjs` pattern)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# dsh-maestro-supervisor host plugin — auto-resume for interrupted sessions after DSH web restart.
|
|
2
|
+
# The standalone daemon (systemd) handles crash detection and web restart;
|
|
3
|
+
# this plugin runs inside DSH web and auto-resumes sessions within the
|
|
4
|
+
# configured window (default 5 minutes, enabled by default).
|
|
5
|
+
# Install with `dsh plugin --profile web add @ddtcorex/dsh-maestro-supervisor`
|
|
6
|
+
- insert:
|
|
7
|
+
- id: maestro-supervisor
|
|
8
|
+
name: '@ddtcorex/dsh-maestro-supervisor'
|
|
9
|
+
config:
|
|
10
|
+
autoResumeWithin: 5
|
|
11
|
+
autoResumeEnabled: true
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
package/lib/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { writeLKG, verifyLKG } from './snapshot.js';
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import * as os from 'node:os';
|
|
7
|
+
import { resolveHarnessRoot, resolveDeepseekHarnessDir } from './paths.js';
|
|
7
8
|
export async function runCli(args) {
|
|
8
9
|
const cmd = args[2] ?? '--help';
|
|
9
10
|
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
@@ -14,6 +15,7 @@ Commands:
|
|
|
14
15
|
status Show health + LKG status
|
|
15
16
|
logs Tail supervisor reports
|
|
16
17
|
rollback --to <ts> Rollback to LKG <ts>
|
|
18
|
+
resume [--within <dur>] List interrupted sessions (filter by time, e.g. 5m, 30s, 1h)
|
|
17
19
|
`);
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
@@ -34,6 +36,30 @@ Commands:
|
|
|
34
36
|
}
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
39
|
+
if (cmd === 'resume') {
|
|
40
|
+
const withinIdx = args.indexOf('--within');
|
|
41
|
+
let withinMs;
|
|
42
|
+
if (withinIdx !== -1) {
|
|
43
|
+
const raw = args[withinIdx + 1] ?? '';
|
|
44
|
+
const { parseDuration } = await import('./resume.js');
|
|
45
|
+
withinMs = parseDuration(raw);
|
|
46
|
+
if (withinMs === undefined) {
|
|
47
|
+
console.error(`invalid --within value: ${raw} (use e.g. 5m, 30s, 1h)`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const { findInterrupted } = await import('./resume.js');
|
|
52
|
+
const res = await findInterrupted(undefined, withinMs !== undefined ? { withinMs } : undefined);
|
|
53
|
+
if (withinMs !== undefined) {
|
|
54
|
+
console.log(`interrupted within ${args[withinIdx + 1]}: ${res.interrupted.length}/${res.scanned}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(`interrupted: ${res.interrupted.length}/${res.scanned}`);
|
|
58
|
+
}
|
|
59
|
+
for (const id of res.interrupted)
|
|
60
|
+
console.log(id);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
37
63
|
if (cmd === 'daemon') {
|
|
38
64
|
console.log('[supervisor] starting daemon — poll every 3s, Ctrl+C to stop');
|
|
39
65
|
const dshHome = path.join(os.homedir(), '.dsh');
|
|
@@ -60,7 +86,7 @@ Commands:
|
|
|
60
86
|
let diff = gitDiff ?? '';
|
|
61
87
|
if (!diff) {
|
|
62
88
|
try {
|
|
63
|
-
const harnessRoot =
|
|
89
|
+
const harnessRoot = resolveHarnessRoot();
|
|
64
90
|
diff = await collectGitDiff(harnessRoot).catch(() => '');
|
|
65
91
|
if (!diff) {
|
|
66
92
|
// fallback: try git diff in cwd
|
|
@@ -94,6 +120,39 @@ Commands:
|
|
|
94
120
|
}
|
|
95
121
|
console.log(`[supervisor] rolled back to ${latest}`);
|
|
96
122
|
},
|
|
123
|
+
restartWeb: async () => {
|
|
124
|
+
const { execSync } = await import('node:child_process');
|
|
125
|
+
// Kill stale MainThread holding 3080/3000 before any restart attempt
|
|
126
|
+
// (EADDRINUSE crash leaves old pid alive with http 200; new start would fail)
|
|
127
|
+
try {
|
|
128
|
+
execSync(`pids=$(ss -tlnp 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | sort -u); if [ -n "$pids" ]; then echo "[supervisor] killing stale pids $pids"; kill $pids 2>/dev/null || true; sleep 2; fi`, { timeout: 5000, stdio: 'pipe' });
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
// Prefer systemd — if dsh-web.service is installed, restart/start it
|
|
132
|
+
try {
|
|
133
|
+
execSync('systemctl --user is-active --quiet dsh-web.service && systemctl --user restart dsh-web.service || systemctl --user start dsh-web.service', { timeout: 15000, stdio: 'pipe' });
|
|
134
|
+
console.log('[supervisor] restarted dsh-web via systemd');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
// Check if unit exists but not active — try start
|
|
139
|
+
try {
|
|
140
|
+
execSync('systemctl --user start dsh-web.service', { timeout: 15000, stdio: 'pipe' });
|
|
141
|
+
console.log('[supervisor] started dsh-web via systemd (fallback)');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
// Last fallback: detached direct node (portable — sources nvm directly, falls back to system node)
|
|
146
|
+
try {
|
|
147
|
+
const harnessRoot = resolveDeepseekHarnessDir();
|
|
148
|
+
const logPath = path.join(os.homedir(), '.dsh/dsh-web.log');
|
|
149
|
+
execSync(`setsid nohup bash -c 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; cd ${JSON.stringify(harnessRoot)} && exec node --import tsx/esm apps/cli/src/bin.ts web --no-open >> ${JSON.stringify(logPath)} 2>&1' &`, { timeout: 5000 });
|
|
150
|
+
console.log('[supervisor] started dsh-web via nohup fallback (direct node, portable)');
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
throw new Error(`restartWeb failed: ${e?.message ?? String(e)}`);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
97
156
|
notify: async (msg) => console.log(`[notify] ${msg}`),
|
|
98
157
|
intervalMs: 3000,
|
|
99
158
|
});
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@ddtcorex/dsh-maestro-supervisor", factory: (require) => {
|
|
2
|
+
var __modules = {};
|
|
3
|
+
__modules["auto-reload.js"] = function (require, module, exports) {
|
|
4
|
+
"use strict";
|
|
5
|
+
/**
|
|
6
|
+
* dsh-maestro-supervisor — client auto-reload for DSH Web after restart.
|
|
7
|
+
* Hybrid: polls `HEAD /` when the server is down (offline/WebSocket close)
|
|
8
|
+
* and reloads as soon as it is back. The host also pushes a reload via
|
|
9
|
+
* `POST /dsh-maestro-supervisor-reload` (loopback) when it recovers.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.apply = apply;
|
|
13
|
+
function apply(ctx) {
|
|
14
|
+
try {
|
|
15
|
+
ctx.effect(() => {
|
|
16
|
+
let timer = null;
|
|
17
|
+
let reloading = false;
|
|
18
|
+
const checkAndReload = async () => {
|
|
19
|
+
if (reloading)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/', { method: 'HEAD', cache: 'no-store' });
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
reloading = true;
|
|
25
|
+
if (timer) {
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
}
|
|
29
|
+
window.location.reload();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// still down — keep polling
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const startPolling = () => {
|
|
37
|
+
if (timer || reloading)
|
|
38
|
+
return;
|
|
39
|
+
timer = setInterval(checkAndReload, 1000);
|
|
40
|
+
};
|
|
41
|
+
const stopPolling = () => {
|
|
42
|
+
if (timer) {
|
|
43
|
+
clearInterval(timer);
|
|
44
|
+
timer = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const onOffline = () => startPolling();
|
|
48
|
+
const onOnline = () => {
|
|
49
|
+
stopPolling();
|
|
50
|
+
void checkAndReload();
|
|
51
|
+
};
|
|
52
|
+
// DSH Web uses WebSocket for sessions; a close means the server went down.
|
|
53
|
+
// We hook the global WebSocket to detect closes without polling constantly.
|
|
54
|
+
const OriginalWebSocket = window.WebSocket;
|
|
55
|
+
let wsCloseHandler = null;
|
|
56
|
+
try {
|
|
57
|
+
if (OriginalWebSocket) {
|
|
58
|
+
const Patched = function (url, protocols) {
|
|
59
|
+
const ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url);
|
|
60
|
+
ws.addEventListener('close', () => {
|
|
61
|
+
// Only treat DSH WebSocket closes (same origin) as server-down
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(url, window.location.href);
|
|
64
|
+
if (u.host === window.location.host)
|
|
65
|
+
startPolling();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
startPolling();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
ws.addEventListener('open', () => {
|
|
72
|
+
// If we were polling and WS reopens, check immediately
|
|
73
|
+
if (timer)
|
|
74
|
+
void checkAndReload();
|
|
75
|
+
});
|
|
76
|
+
return ws;
|
|
77
|
+
};
|
|
78
|
+
Patched.prototype = OriginalWebSocket.prototype;
|
|
79
|
+
Object.setPrototypeOf(Patched, OriginalWebSocket);
|
|
80
|
+
window.WebSocket = Patched;
|
|
81
|
+
wsCloseHandler = () => { window.WebSocket = OriginalWebSocket; };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
window.addEventListener('offline', onOffline);
|
|
86
|
+
window.addEventListener('online', onOnline);
|
|
87
|
+
document.addEventListener('visibilitychange', () => {
|
|
88
|
+
if (document.visibilityState === 'visible')
|
|
89
|
+
void checkAndReload();
|
|
90
|
+
});
|
|
91
|
+
// Host push: POST /dsh-maestro-supervisor-reload/reload (loopback) -> reload
|
|
92
|
+
// The host plugin registers this RPC; the client also listens via fetch polling,
|
|
93
|
+
// but a direct push is instant and avoids the 1s poll delay.
|
|
94
|
+
// We poll for the push by listening to a custom event dispatched from the host's
|
|
95
|
+
// `fetch` response — the host's `notify` after `restartWeb` will trigger it.
|
|
96
|
+
const onHostPush = () => {
|
|
97
|
+
if (!reloading)
|
|
98
|
+
window.location.reload();
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener('dsh-maestro-supervisor-reload', onHostPush);
|
|
101
|
+
// If the page loads while the server is already down (e.g. hard refresh
|
|
102
|
+
// during restart), start polling immediately.
|
|
103
|
+
void fetch('/', { method: 'HEAD', cache: 'no-store' }).catch(() => startPolling());
|
|
104
|
+
return () => {
|
|
105
|
+
window.removeEventListener('offline', onOffline);
|
|
106
|
+
window.removeEventListener('online', onOnline);
|
|
107
|
+
window.removeEventListener('dsh-maestro-supervisor-reload', onHostPush);
|
|
108
|
+
if (timer)
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
if (wsCloseHandler)
|
|
111
|
+
wsCloseHandler();
|
|
112
|
+
};
|
|
113
|
+
}, 'supervisor:auto-reload');
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
__modules["index.js"] = function (require, module, exports) {
|
|
119
|
+
"use strict";
|
|
120
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
121
|
+
if (k2 === undefined) k2 = k;
|
|
122
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
123
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
124
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
125
|
+
}
|
|
126
|
+
Object.defineProperty(o, k2, desc);
|
|
127
|
+
}) : (function(o, m, k, k2) {
|
|
128
|
+
if (k2 === undefined) k2 = k;
|
|
129
|
+
o[k2] = m[k];
|
|
130
|
+
}));
|
|
131
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
132
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
133
|
+
};
|
|
134
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
135
|
+
__exportStar(require("./auto-reload.js"), exports);
|
|
136
|
+
};
|
|
137
|
+
var __cache = {};
|
|
138
|
+
function __localRequire(id) {
|
|
139
|
+
if (id.charCodeAt(0) !== 46) return require(id);
|
|
140
|
+
id = id.slice(2);
|
|
141
|
+
var cached = __cache[id];
|
|
142
|
+
if (cached) return cached.exports;
|
|
143
|
+
var module = { exports: {} };
|
|
144
|
+
__cache[id] = module;
|
|
145
|
+
__modules[id](__localRequire, module, module.exports);
|
|
146
|
+
return module.exports;
|
|
147
|
+
}
|
|
148
|
+
var module = { exports: {} };
|
|
149
|
+
__modules["index.js"](__localRequire, module, module.exports);
|
|
150
|
+
return module.exports; } });
|
package/lib/debug-agent.js
CHANGED
|
@@ -76,17 +76,19 @@ export async function runDebugAgent(opts) {
|
|
|
76
76
|
return { fixed: false, reason: `would debug ${opts.reportPath} (attempt ${attempts}) — LLM not wired, manual fix needed` };
|
|
77
77
|
}
|
|
78
78
|
async function autoFixKnownPatterns(err, exec, readFile, writeFile) {
|
|
79
|
+
const { resolveHarnessRoot } = await import('./paths.js');
|
|
80
|
+
const harnessRoot = resolveHarnessRoot();
|
|
79
81
|
const lower = err.toLowerCase();
|
|
80
82
|
// allowBuilds — ensure pnpm-workspace.yaml has allowBuilds.esbuild:true
|
|
81
83
|
if (lower.includes('allowbuilds') || lower.includes('allow_builds')) {
|
|
82
84
|
try {
|
|
83
|
-
exec(
|
|
85
|
+
exec(`pnpm --dir ${harnessRoot}/packages/dsh-maestro-supervisor verify --silent 2>&1 | head -5`, { timeout: 15000 });
|
|
84
86
|
}
|
|
85
87
|
catch { }
|
|
86
88
|
// Try to patch any pnpm-workspace.yaml missing allowBuilds by touching it (heuristic)
|
|
87
89
|
// Real fix would edit file; for test we just call exec to satisfy expectation
|
|
88
90
|
try {
|
|
89
|
-
const ws =
|
|
91
|
+
const ws = `${harnessRoot}/packages/dsh-maestro-supervisor/pnpm-workspace.yaml`;
|
|
90
92
|
const content = readFile(ws);
|
|
91
93
|
if (!content.includes('allowBuilds')) {
|
|
92
94
|
// writeFile patched content
|
|
@@ -100,10 +102,10 @@ async function autoFixKnownPatterns(err, exec, readFile, writeFile) {
|
|
|
100
102
|
const candidates = ['dsh-maestro-supervisor', 'dsh-maestro-observe', 'dsh-maestro-memory'];
|
|
101
103
|
for (const pkg of candidates) {
|
|
102
104
|
try {
|
|
103
|
-
const p =
|
|
105
|
+
const p = `${harnessRoot}/packages/${pkg}/lib/index.js`;
|
|
104
106
|
readFile(p);
|
|
105
107
|
try {
|
|
106
|
-
exec(`pnpm --dir /
|
|
108
|
+
exec(`pnpm --dir ${harnessRoot}/packages/${pkg} verify --silent 2>&1 | head -5`, { timeout: 15000 });
|
|
107
109
|
}
|
|
108
110
|
catch { }
|
|
109
111
|
}
|
|
@@ -217,10 +219,12 @@ function defaultWriteFile(p, c) {
|
|
|
217
219
|
async function defaultDryBoot() {
|
|
218
220
|
try {
|
|
219
221
|
const { execSync } = await import('node:child_process');
|
|
222
|
+
const { resolveDeepseekHarnessDir } = await import('./paths.js');
|
|
223
|
+
const deepseekDir = resolveDeepseekHarnessDir();
|
|
220
224
|
const tmp = execSync('mktemp -d', { encoding: 'utf-8' }).trim();
|
|
221
225
|
const port = Math.floor(19000 + Math.random() * 1000);
|
|
222
226
|
// Use spawn-based dry-boot to avoid nested quoting hell (prev \\$! / \\$(seq) caused syntax error)
|
|
223
|
-
const out = execSync(`timeout 8 bash -c 'DSH_HOME=${tmp} pnpm --dir
|
|
227
|
+
const out = execSync(`timeout 8 bash -c 'DSH_HOME=${tmp} pnpm --dir ${deepseekDir} dsh web --port ${port} --no-open >${tmp}/dsh.log 2>&1 & pid=$!; for i in $(seq 1 5); do sleep 1; if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${port}/ 2>&1 | grep -q 200; then kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true; echo ok; exit 0; fi; done; kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true; echo fail; exit 1'`, { encoding: 'utf-8', timeout: 12000 });
|
|
224
228
|
execSync(`rm -rf ${tmp}`);
|
|
225
229
|
return out.includes('ok');
|
|
226
230
|
}
|
package/lib/health-poller.js
CHANGED
|
@@ -12,9 +12,11 @@ const ERROR_PATTERNS = [
|
|
|
12
12
|
'allowBuilds',
|
|
13
13
|
'Cannot find module',
|
|
14
14
|
'Failed to load',
|
|
15
|
+
'EADDRINUSE',
|
|
16
|
+
'address already in use',
|
|
15
17
|
];
|
|
16
18
|
export async function pollHealth(opts = {}) {
|
|
17
|
-
const fetchFn = opts.fetch ?? defaultFetch(opts.url ?? 'http://127.0.0.1:3080/', opts.timeoutMs ??
|
|
19
|
+
const fetchFn = opts.fetch ?? defaultFetch(opts.url ?? 'http://127.0.0.1:3080/', opts.timeoutMs ?? 5000);
|
|
18
20
|
const psAliveFn = opts.psAlive ?? defaultPsAlive;
|
|
19
21
|
const logTailFn = opts.logTail ?? defaultLogTail;
|
|
20
22
|
let httpCode;
|
|
@@ -37,14 +39,36 @@ export async function pollHealth(opts = {}) {
|
|
|
37
39
|
// ignore log read errors
|
|
38
40
|
}
|
|
39
41
|
let logError;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// Find most recent error line (not first) and ignore stale errors that are
|
|
43
|
+
// followed by a successful boot (log tail is append-only, old EADDRINUSE stays forever).
|
|
44
|
+
// We check last occurrence and ensure no "dsh web: http" success after it.
|
|
45
|
+
const lines = logContent.split('\n');
|
|
46
|
+
const lowerLines = lines.map(l => l.toLowerCase());
|
|
47
|
+
let lastErrorIdx = -1;
|
|
48
|
+
let matchedLine = '';
|
|
49
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
50
|
+
const lower = lowerLines[i];
|
|
51
|
+
for (const pat of ERROR_PATTERNS) {
|
|
52
|
+
if (lower.includes(pat.toLowerCase())) {
|
|
53
|
+
lastErrorIdx = i;
|
|
54
|
+
matchedLine = lines[i].trim().slice(0, 500);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (lastErrorIdx !== -1)
|
|
46
59
|
break;
|
|
60
|
+
}
|
|
61
|
+
if (lastErrorIdx !== -1) {
|
|
62
|
+
// If a successful boot line appears after the last error, error is stale (already recovered)
|
|
63
|
+
let hasSuccessAfter = false;
|
|
64
|
+
for (let i = lastErrorIdx + 1; i < lines.length; i++) {
|
|
65
|
+
if (lowerLines[i].includes('dsh web: http')) {
|
|
66
|
+
hasSuccessAfter = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
47
69
|
}
|
|
70
|
+
if (!hasSuccessAfter)
|
|
71
|
+
logError = matchedLine;
|
|
48
72
|
}
|
|
49
73
|
// Distinguish FULL (http !=200) vs DEGRADED (http 200 but log has plugin error)
|
|
50
74
|
if (fetchError) {
|
|
@@ -57,6 +81,19 @@ export async function pollHealth(opts = {}) {
|
|
|
57
81
|
};
|
|
58
82
|
}
|
|
59
83
|
if (logError) {
|
|
84
|
+
// EADDRINUSE is fatal even with http 200 — old process still holds 3080/3000
|
|
85
|
+
// and new start failed; treat as FULL down so supervisor kills + restarts.
|
|
86
|
+
const lowerErr = logError.toLowerCase();
|
|
87
|
+
const isFatalPortError = lowerErr.includes('eaddrinuse') || lowerErr.includes('address already in use');
|
|
88
|
+
if (isFatalPortError) {
|
|
89
|
+
return {
|
|
90
|
+
up: false,
|
|
91
|
+
httpCode,
|
|
92
|
+
error: logError,
|
|
93
|
+
degraded: false,
|
|
94
|
+
logTail: logContent.slice(-5000),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
60
97
|
// http 200 but log error → DEGRADED (isolatable), not FULL
|
|
61
98
|
if (httpCode === 200) {
|
|
62
99
|
return {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-maestro-supervisor — Cordis host plugin entry.
|
|
3
|
+
* The daemon CLI lives in bin.ts (lib/bin.js); this file is the
|
|
4
|
+
* host plugin loaded by DSH web via cordis.patch.yml.
|
|
5
|
+
*/
|
|
6
|
+
export * from './plugin.js';
|
|
7
|
+
export { apply, inject } from './plugin.js';
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-maestro-supervisor — Cordis host plugin entry.
|
|
3
|
+
* The daemon CLI lives in bin.ts (lib/bin.js); this file is the
|
|
4
|
+
* host plugin loaded by DSH web via cordis.patch.yml.
|
|
5
|
+
*/
|
|
6
|
+
export * from './plugin.js';
|
|
7
|
+
export { apply, inject } from './plugin.js';
|
package/lib/paths.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Maestro Harness root without hardcoding a machine-specific home path.
|
|
3
|
+
* Priority: env > walk up from current file > cwd check > homedir fallback (last resort).
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveHarnessRoot(): string;
|
|
6
|
+
export declare function resolveDeepseekHarnessDir(): string;
|