@ddtcorex/dsh-maestro-supervisor 0.6.2 → 0.6.4

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/lib/cli.js CHANGED
@@ -106,19 +106,79 @@ Commands:
106
106
  return writeReport({ reportsRoot, ts, health, gitDiff: diff, logTail: tail, action });
107
107
  },
108
108
  rollback: async () => {
109
- // Find latest LKG and restore
109
+ const { execSync } = await import('node:child_process');
110
110
  const entries = fs.existsSync(lkgRoot) ? fs.readdirSync(lkgRoot).sort() : [];
111
111
  if (!entries.length)
112
112
  throw new Error('no LKG to rollback to');
113
- const latest = entries[entries.length - 1];
114
- const src = path.join(lkgRoot, latest);
115
- // naive restore: copy files back
113
+ // Try newest to oldest (up to 3) to find a clean LKG for plugin failures
114
+ // Extract failing plugin from current log tail if possible
115
+ let failingPlugin;
116
+ try {
117
+ const tail = fs.readFileSync(path.join(os.homedir(), '.dsh/dsh-web.log'), 'utf8').slice(-5000);
118
+ const m = tail.match(/@ddtcorex\/dsh-maestro-[a-z0-9_-]+/i) ?? tail.match(/dsh-maestro-[a-z0-9_-]+/i);
119
+ if (m)
120
+ failingPlugin = m[0].replace(/^@ddtcorex\//, '');
121
+ }
122
+ catch { }
123
+ const candidates = [...entries].reverse().slice(0, 3);
124
+ let chosen;
125
+ for (const cand of candidates) {
126
+ if (!failingPlugin) {
127
+ chosen = cand;
128
+ break;
129
+ }
130
+ try {
131
+ const pkgPath = path.join(lkgRoot, cand, 'profiles/web/package.json');
132
+ if (!fs.existsSync(pkgPath)) {
133
+ chosen = cand;
134
+ break;
135
+ }
136
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
137
+ const bundles = pkg?.dsh?.profile?.bundles ?? [];
138
+ const deps = pkg?.dependencies ?? {};
139
+ const hasFailing = bundles.some((b) => b.includes(failingPlugin)) || Object.keys(deps).some(k => k.includes(failingPlugin));
140
+ if (!hasFailing) {
141
+ chosen = cand;
142
+ break;
143
+ }
144
+ console.log(`[supervisor] skipping LKG ${cand} still contains failing plugin ${failingPlugin}`);
145
+ }
146
+ catch {
147
+ chosen = cand;
148
+ break;
149
+ }
150
+ }
151
+ const target = chosen ?? entries[entries.length - 1];
152
+ const src = path.join(lkgRoot, target);
116
153
  for (const entry of fs.readdirSync(src)) {
117
154
  if (entry === 'manifest.json')
118
155
  continue;
119
- fs.cpSync(path.join(src, entry), path.join(dshHome, entry), { recursive: true, force: true });
156
+ const srcPath = path.join(src, entry);
157
+ const destPath = path.join(dshHome, entry);
158
+ try {
159
+ // Skip if src and dest are the same file (e.g. symlink to same target like ~/.dsh/AGENTS.md)
160
+ try {
161
+ if (fs.existsSync(srcPath) && fs.existsSync(destPath) && fs.realpathSync(srcPath) === fs.realpathSync(destPath))
162
+ continue;
163
+ }
164
+ catch { }
165
+ fs.cpSync(srcPath, destPath, { recursive: true, force: true });
166
+ }
167
+ catch (e) {
168
+ if (String(e?.message ?? '').includes('cannot be the same'))
169
+ continue;
170
+ throw e;
171
+ }
172
+ }
173
+ console.log(`[supervisor] rolled back to ${target}${failingPlugin ? ` (avoiding ${failingPlugin})` : ''}`);
174
+ // Reconcile node_modules from restored package.json (critical for link: deps)
175
+ try {
176
+ execSync('pnpm --dir ~/.dsh/profiles/web install --silent', { timeout: 30000, stdio: 'pipe' });
177
+ console.log('[supervisor] pnpm install reconciled profiles/web');
178
+ }
179
+ catch (e) {
180
+ console.log(`[supervisor] pnpm install failed: ${e?.message ?? String(e)}`);
120
181
  }
121
- console.log(`[supervisor] rolled back to ${latest}`);
122
182
  },
123
183
  restartWeb: async () => {
124
184
  const { execSync } = await import('node:child_process');
@@ -316,9 +316,9 @@ async function resolveLLMConfig() {
316
316
  process.env.OMNI_ROUTE_API_URL ??
317
317
  process.env.OPENCODE_API_URL ??
318
318
  null;
319
- // Model: AI_MODEL / DEEPSEEK_MODEL / settings.json domains.review.model / default
319
+ // Model: AI_MODEL / DEEPSEEK_MODEL / settings.json domains.supervisor.model -> domains.review.model / default
320
+ // Supervisor has its own picker; falls back to review model for backward compat, then DSH default.
320
321
  let model = process.env.AI_MODEL ?? process.env.DEEPSEEK_MODEL ?? process.env.OPENAI_MODEL ?? null;
321
- // Try settings.json first (review model is the user's current default model)
322
322
  if (!model) {
323
323
  try {
324
324
  const { readFileSync } = await import('node:fs');
@@ -326,11 +326,20 @@ async function resolveLLMConfig() {
326
326
  const settingsPath = `${homedir()}/.dsh/maestro/settings.json`;
327
327
  const raw = readFileSync(settingsPath, 'utf-8');
328
328
  const j = JSON.parse(raw);
329
- const m = j?.domains?.review?.model?.model ?? j?.domains?.review?.model;
330
- if (typeof m === 'string')
329
+ // Prefer supervisor model, fall back to review model (so old installs keep working)
330
+ const sup = j?.domains?.supervisor?.model?.model ?? j?.domains?.supervisor?.model;
331
+ const rev = j?.domains?.review?.model?.model ?? j?.domains?.review?.model;
332
+ let m = null;
333
+ if (typeof sup === 'string')
334
+ m = sup;
335
+ else if (sup?.model && typeof sup.model === 'string')
336
+ m = sup.model;
337
+ else if (typeof rev === 'string')
338
+ m = rev;
339
+ else if (rev?.model && typeof rev.model === 'string')
340
+ m = rev.model;
341
+ if (typeof m === 'string' && m.trim() !== '')
331
342
  model = m;
332
- else if (m?.model && typeof m.model === 'string')
333
- model = m.model;
334
343
  }
335
344
  catch { }
336
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-supervisor",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Supervisor daemon for DSH Web resilience — auto-detect crashes, rollback to LKG, report",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",