sneakoscope 3.1.3 → 3.1.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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/codex-app.js +45 -1
  8. package/dist/commands/doctor.js +44 -1
  9. package/dist/core/codex-app/codex-agent-role-sync.js +67 -0
  10. package/dist/core/codex-app/codex-app-execution-profile.js +32 -0
  11. package/dist/core/codex-app/codex-app-harness-matrix.js +75 -0
  12. package/dist/core/codex-app/codex-hook-lifecycle.js +40 -0
  13. package/dist/core/codex-app/codex-init-deep.js +87 -0
  14. package/dist/core/codex-app/codex-skill-sync.js +92 -0
  15. package/dist/core/codex-app/lazycodex-analysis.js +51 -0
  16. package/dist/core/codex-app/lazycodex-interop-policy.js +49 -0
  17. package/dist/core/commands/loop-command.js +11 -0
  18. package/dist/core/commands/mad-sks-command.js +77 -16
  19. package/dist/core/doctor/doctor-readiness-matrix.js +7 -0
  20. package/dist/core/doctor/doctor-zellij-repair.js +36 -0
  21. package/dist/core/fsx.js +1 -1
  22. package/dist/core/hooks-runtime.js +13 -0
  23. package/dist/core/loops/loop-continuation-enforcer.js +32 -0
  24. package/dist/core/loops/loop-planner.js +9 -0
  25. package/dist/core/loops/loop-worker-runtime.js +5 -1
  26. package/dist/core/version.js +1 -1
  27. package/dist/core/zellij/homebrew-policy.js +45 -0
  28. package/dist/core/zellij/zellij-capability.js +32 -3
  29. package/dist/core/zellij/zellij-self-heal.js +353 -0
  30. package/dist/core/zellij/zellij-update.js +31 -6
  31. package/dist/scripts/sks-3-1-4-directive-check-lib.js +241 -0
  32. package/package.json +28 -2
@@ -0,0 +1,353 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { ensureDir, nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
5
+ import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
6
+ import { guardContextForRoute, guardedPackageInstall } from '../safety/mutation-guard.js';
7
+ import { mutationLedgerPath } from '../safety/mutation-ledger.js';
8
+ import { checkZellijCapability, ZELLIJ_MIN_VERSION } from './zellij-capability.js';
9
+ import { compareVersionLike } from './zellij-command.js';
10
+ import { askHomebrewInstallAllowed, HOMEBREW_INSTALL_COMMAND, resolveHomebrewInstallPolicy } from './homebrew-policy.js';
11
+ export async function repairZellijForSks(input) {
12
+ const root = path.resolve(input.root || process.cwd());
13
+ const env = input.env || process.env;
14
+ const autoApproved = input.autoApprove === true;
15
+ const beforeReport = await capabilitySnapshot(root, env, 'before');
16
+ const before = compactCapability(beforeReport);
17
+ const latest = await latestZellijVersion(env);
18
+ const brew = await findBrew(env);
19
+ const stale = Boolean(before.version && latest && compareVersionLike(latest, before.version) > 0);
20
+ const needsRepair = before.status === 'missing' || before.status === 'too_old' || stale;
21
+ const mutationArtifact = mutationLedgerPath(root);
22
+ if (!needsRepair) {
23
+ return persistSelfHeal(root, input.missionDir, {
24
+ schema: 'sks.zellij-self-heal.v1',
25
+ ok: true,
26
+ requested_by: input.requestedBy,
27
+ fix_requested: input.fixRequested === true,
28
+ auto_approved: autoApproved,
29
+ install_homebrew_allowed: false,
30
+ before,
31
+ latest_version: latest,
32
+ strategy: 'none-current',
33
+ command: null,
34
+ after: before,
35
+ mutation_guard_artifact: null,
36
+ homebrew: { present: brew.present, bin: brew.bin, install_attempted: false, install_allowed: false },
37
+ blockers: [],
38
+ warnings: beforeReport.warnings || []
39
+ });
40
+ }
41
+ if (input.fixRequested !== true) {
42
+ return manualResult(root, input, env, before, latest, brew, 'fix_not_requested');
43
+ }
44
+ if (!autoApproved && input.interactive !== true) {
45
+ return input.allowHeadlessFallback === true
46
+ ? headlessResult(root, input, before, latest, brew, 'noninteractive_without_auto_approval')
47
+ : manualResult(root, input, env, before, latest, brew, 'noninteractive_without_auto_approval');
48
+ }
49
+ if (!autoApproved && input.interactive === true) {
50
+ const accepted = await askZellijRepairAllowed(before.status === 'missing'
51
+ ? 'Zellij is missing. Install it with Homebrew now? [Y/n] '
52
+ : `Zellij ${before.version || 'unknown'} needs repair. Upgrade with Homebrew now? [Y/n] `);
53
+ if (!accepted)
54
+ return manualResult(root, input, env, before, latest, brew, 'operator_declined_zellij_repair');
55
+ }
56
+ let brewBin = brew.bin;
57
+ let homebrewInstallAttempted = false;
58
+ let homebrewInstallAllowed = false;
59
+ if (!brew.present) {
60
+ const interactiveAccepted = input.interactive === true && !autoApproved
61
+ ? await askHomebrewInstallAllowed()
62
+ : false;
63
+ const policy = resolveHomebrewInstallPolicy({
64
+ env,
65
+ installHomebrew: input.installHomebrew === true,
66
+ autoApprove: autoApproved,
67
+ interactiveAccepted
68
+ });
69
+ homebrewInstallAllowed = policy.allowed;
70
+ if (!policy.allowed) {
71
+ return input.allowHeadlessFallback === true
72
+ ? headlessResult(root, input, before, latest, brew, policy.blockers[0] || 'homebrew_missing')
73
+ : manualResult(root, input, env, before, latest, brew, policy.blockers[0] || 'homebrew_missing');
74
+ }
75
+ homebrewInstallAttempted = true;
76
+ const homebrewRun = await runHomebrewInstall(root, env);
77
+ if (homebrewRun.code !== 0) {
78
+ return persistSelfHeal(root, input.missionDir, {
79
+ schema: 'sks.zellij-self-heal.v1',
80
+ ok: false,
81
+ requested_by: input.requestedBy,
82
+ fix_requested: true,
83
+ auto_approved: autoApproved,
84
+ install_homebrew_allowed: true,
85
+ before,
86
+ latest_version: latest,
87
+ strategy: 'failed',
88
+ command: HOMEBREW_INSTALL_COMMAND,
89
+ after: before,
90
+ mutation_guard_artifact: mutationArtifact,
91
+ homebrew: { present: false, bin: null, install_attempted: true, install_allowed: true },
92
+ blockers: [`homebrew_install_failed:${tail(homebrewRun.stderr || homebrewRun.stdout || 'unknown')}`],
93
+ warnings: []
94
+ });
95
+ }
96
+ const afterBrew = await findBrew(env);
97
+ brewBin = afterBrew.bin || brew.bin || 'brew';
98
+ }
99
+ if (!brewBin) {
100
+ return manualResult(root, input, env, before, latest, brew, 'homebrew_missing');
101
+ }
102
+ const install = before.status === 'missing';
103
+ const brewArgs = install ? ['install', 'zellij'] : ['upgrade', 'zellij'];
104
+ const strategy = !brew.present && homebrewInstallAttempted ? 'brew-install-homebrew-then-zellij'
105
+ : install ? 'brew-install-zellij'
106
+ : 'brew-upgrade-zellij';
107
+ const command = `brew ${brewArgs.join(' ')}`;
108
+ const run = await runZellijBrew(root, env, brewBin, brewArgs, command);
109
+ if (run.code !== 0 && /already installed|already up-to-date/i.test(`${run.stdout}\n${run.stderr}`)) {
110
+ const after = await capabilitySnapshot(root, env, 'after-noop');
111
+ return persistSelfHeal(root, input.missionDir, {
112
+ schema: 'sks.zellij-self-heal.v1',
113
+ ok: true,
114
+ requested_by: input.requestedBy,
115
+ fix_requested: true,
116
+ auto_approved: autoApproved,
117
+ install_homebrew_allowed: homebrewInstallAllowed,
118
+ before,
119
+ latest_version: latest,
120
+ strategy: 'none-current',
121
+ command,
122
+ after: compactCapability(after, latest || before.version),
123
+ mutation_guard_artifact: mutationArtifact,
124
+ homebrew: { present: true, bin: brewBin, install_attempted: homebrewInstallAttempted, install_allowed: homebrewInstallAllowed },
125
+ blockers: [],
126
+ warnings: ['brew_reported_already_current']
127
+ });
128
+ }
129
+ if (run.code !== 0) {
130
+ return persistSelfHeal(root, input.missionDir, {
131
+ schema: 'sks.zellij-self-heal.v1',
132
+ ok: false,
133
+ requested_by: input.requestedBy,
134
+ fix_requested: true,
135
+ auto_approved: autoApproved,
136
+ install_homebrew_allowed: homebrewInstallAllowed,
137
+ before,
138
+ latest_version: latest,
139
+ strategy: 'failed',
140
+ command,
141
+ after: before,
142
+ mutation_guard_artifact: mutationArtifact,
143
+ homebrew: { present: true, bin: brewBin, install_attempted: homebrewInstallAttempted, install_allowed: homebrewInstallAllowed },
144
+ blockers: [`zellij_brew_repair_failed:${tail(run.stderr || run.stdout || 'unknown')}`],
145
+ warnings: []
146
+ });
147
+ }
148
+ const afterReport = await capabilitySnapshot(root, env, 'after', latest || env.SKS_ZELLIJ_SELF_HEAL_AFTER_VERSION || '0.44.0');
149
+ const after = compactCapability(afterReport, latest || before.version || '0.44.0');
150
+ const ok = after.status === 'ok';
151
+ return persistSelfHeal(root, input.missionDir, {
152
+ schema: 'sks.zellij-self-heal.v1',
153
+ ok,
154
+ requested_by: input.requestedBy,
155
+ fix_requested: true,
156
+ auto_approved: autoApproved,
157
+ install_homebrew_allowed: homebrewInstallAllowed,
158
+ before,
159
+ latest_version: latest,
160
+ strategy,
161
+ command,
162
+ after,
163
+ mutation_guard_artifact: mutationArtifact,
164
+ homebrew: { present: true, bin: brewBin, install_attempted: homebrewInstallAttempted, install_allowed: homebrewInstallAllowed },
165
+ blockers: ok ? [] : ['zellij_repair_completed_but_capability_not_ok'],
166
+ warnings: []
167
+ });
168
+ }
169
+ async function capabilitySnapshot(root, env, phase, fallbackVersion) {
170
+ const fakeStatus = phase === 'before' ? env.SKS_ZELLIJ_SELF_HEAL_BEFORE_STATUS : env.SKS_ZELLIJ_SELF_HEAL_AFTER_STATUS;
171
+ if (fakeStatus) {
172
+ const version = phase === 'before'
173
+ ? (env.SKS_ZELLIJ_SELF_HEAL_BEFORE_VERSION || (fakeStatus === 'missing' ? null : '0.40.0'))
174
+ : (env.SKS_ZELLIJ_SELF_HEAL_AFTER_VERSION || fallbackVersion || '0.44.0');
175
+ return fakeCapability(String(fakeStatus), version);
176
+ }
177
+ return checkZellijCapability({ root, require: false, writeReport: false, env }).catch((err) => ({
178
+ schema: 'sks.zellij-capability.v1',
179
+ generated_at: nowIso(),
180
+ ok: false,
181
+ status: 'blocked',
182
+ integration_optional: true,
183
+ require_zellij: false,
184
+ min_version: ZELLIJ_MIN_VERSION,
185
+ version: null,
186
+ bin: null,
187
+ command: ['zellij', '--version'],
188
+ docs_evidence: [],
189
+ blockers: [`zellij_capability_check_failed:${tail(err?.message || String(err))}`],
190
+ warnings: [],
191
+ operator_actions: ['Resolve the Zellij capability check failure, then rerun `sks doctor --fix --yes`.']
192
+ }));
193
+ }
194
+ function fakeCapability(status, version) {
195
+ const normalized = status === 'ok' || status === 'missing' || status === 'too_old' || status === 'blocked' ? status : 'blocked';
196
+ return {
197
+ schema: 'sks.zellij-capability.v1',
198
+ generated_at: nowIso(),
199
+ ok: normalized === 'ok',
200
+ status: normalized,
201
+ integration_optional: true,
202
+ require_zellij: false,
203
+ min_version: ZELLIJ_MIN_VERSION,
204
+ version,
205
+ bin: 'zellij',
206
+ command: ['zellij', '--version'],
207
+ docs_evidence: [],
208
+ blockers: normalized === 'ok' ? [] : [`zellij_${normalized}`],
209
+ warnings: normalized === 'ok' ? [] : [`zellij_${normalized}_fixture`],
210
+ operator_actions: normalized === 'ok' ? [] : ['Install Zellij. On macOS: `brew install zellij`.']
211
+ };
212
+ }
213
+ function compactCapability(report, fallbackVersion) {
214
+ return {
215
+ status: report.status,
216
+ version: report.version || fallbackVersion || null,
217
+ bin: report.status === 'missing' ? null : report.bin || 'zellij'
218
+ };
219
+ }
220
+ async function latestZellijVersion(env) {
221
+ if (env.SKS_ZELLIJ_LATEST_VERSION)
222
+ return String(env.SKS_ZELLIJ_LATEST_VERSION).replace(/^v(?=\d)/, '');
223
+ const mod = await import('./zellij-update.js').catch(() => null);
224
+ if (!mod?.fetchLatestZellijVersion)
225
+ return null;
226
+ const result = await mod.fetchLatestZellijVersion({ env, timeoutMs: 1500 }).catch(() => null);
227
+ return result?.version || null;
228
+ }
229
+ async function findBrew(env) {
230
+ if (env.SKS_ZELLIJ_SELF_HEAL_BREW_PRESENT === '0')
231
+ return { present: false, bin: null };
232
+ if (env.SKS_FAKE_BREW_BIN)
233
+ return { present: true, bin: String(env.SKS_FAKE_BREW_BIN) };
234
+ for (const dir of String(env.PATH || process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
235
+ const candidate = path.join(dir, process.platform === 'win32' ? 'brew.cmd' : 'brew');
236
+ try {
237
+ await fs.access(candidate);
238
+ return { present: true, bin: candidate };
239
+ }
240
+ catch { }
241
+ }
242
+ if (env.SKS_ZELLIJ_SELF_HEAL_BREW_PRESENT === '1')
243
+ return { present: true, bin: 'brew' };
244
+ return { present: false, bin: null };
245
+ }
246
+ async function runZellijBrew(root, env, brewBin, args, command) {
247
+ if (env.SKS_ZELLIJ_SELF_HEAL_FAKE_RUN === '1') {
248
+ await appendFakeBrewLog(env, args);
249
+ return { code: Number(env.SKS_ZELLIJ_SELF_HEAL_FAKE_RUN_CODE || 0), stdout: 'fake brew ok', stderr: '' };
250
+ }
251
+ const contract = createRequestedScopeContract({
252
+ route: 'zellij-self-heal',
253
+ userRequest: command,
254
+ projectRoot: root,
255
+ overrides: { package_install: true, zellij_install: true }
256
+ });
257
+ return guardedPackageInstall(guardContextForRoute(root, contract, command), 'zellij', {
258
+ confirmed: true,
259
+ command: brewBin,
260
+ args,
261
+ env,
262
+ timeoutMs: 180000,
263
+ maxOutputBytes: 256 * 1024
264
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
265
+ }
266
+ async function runHomebrewInstall(root, env) {
267
+ if (env.SKS_ZELLIJ_SELF_HEAL_FAKE_RUN === '1') {
268
+ await appendFakeBrewLog(env, ['install-homebrew']);
269
+ return { code: Number(env.SKS_ZELLIJ_SELF_HEAL_FAKE_HOMEBREW_CODE || 0), stdout: 'fake homebrew install ok', stderr: '' };
270
+ }
271
+ const contract = createRequestedScopeContract({
272
+ route: 'zellij-self-heal',
273
+ userRequest: HOMEBREW_INSTALL_COMMAND,
274
+ projectRoot: root,
275
+ overrides: { package_install: true, zellij_install: true }
276
+ });
277
+ return guardedPackageInstall(guardContextForRoute(root, contract, HOMEBREW_INSTALL_COMMAND), 'homebrew', {
278
+ confirmed: true,
279
+ command: '/bin/bash',
280
+ args: ['-c', 'curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | /bin/bash'],
281
+ env,
282
+ timeoutMs: 600000,
283
+ maxOutputBytes: 256 * 1024
284
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
285
+ }
286
+ async function appendFakeBrewLog(env, args) {
287
+ if (!env.SKS_FAKE_BREW_LOG)
288
+ return;
289
+ await ensureDir(path.dirname(env.SKS_FAKE_BREW_LOG));
290
+ await fs.appendFile(env.SKS_FAKE_BREW_LOG, `${args.join(' ')}\n`, 'utf8');
291
+ }
292
+ async function manualResult(root, input, env, before, latest, brew, reason) {
293
+ const command = brew.present ? 'sks doctor --fix --yes' : 'sks doctor --fix --install-homebrew --yes';
294
+ return persistSelfHeal(root, input.missionDir, {
295
+ schema: 'sks.zellij-self-heal.v1',
296
+ ok: false,
297
+ requested_by: input.requestedBy,
298
+ fix_requested: input.fixRequested === true,
299
+ auto_approved: input.autoApprove === true,
300
+ install_homebrew_allowed: false,
301
+ before,
302
+ latest_version: latest,
303
+ strategy: 'manual-required',
304
+ command,
305
+ after: before,
306
+ mutation_guard_artifact: null,
307
+ homebrew: { present: brew.present, bin: brew.bin, install_attempted: false, install_allowed: false },
308
+ blockers: [reason],
309
+ warnings: []
310
+ });
311
+ }
312
+ async function headlessResult(root, input, before, latest, brew, reason) {
313
+ return persistSelfHeal(root, input.missionDir, {
314
+ schema: 'sks.zellij-self-heal.v1',
315
+ ok: true,
316
+ requested_by: input.requestedBy,
317
+ fix_requested: input.fixRequested === true,
318
+ auto_approved: input.autoApprove === true,
319
+ install_homebrew_allowed: false,
320
+ before,
321
+ latest_version: latest,
322
+ strategy: 'headless-fallback',
323
+ command: 'sks --mad --headless',
324
+ after: before,
325
+ mutation_guard_artifact: null,
326
+ homebrew: { present: brew.present, bin: brew.bin, install_attempted: false, install_allowed: false },
327
+ blockers: [],
328
+ warnings: [reason, 'live_panes=false']
329
+ });
330
+ }
331
+ async function persistSelfHeal(root, missionDir, result) {
332
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-self-heal.json'), result).catch(() => undefined);
333
+ if (missionDir)
334
+ await writeJsonAtomic(path.join(missionDir, 'zellij-self-heal.json'), result).catch(() => undefined);
335
+ return result;
336
+ }
337
+ async function askZellijRepairAllowed(question) {
338
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
339
+ return false;
340
+ const rl = (await import('node:readline')).createInterface({ input: process.stdin, output: process.stdout });
341
+ try {
342
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
343
+ const trimmed = String(answer || '').trim();
344
+ return trimmed === '' || /^(y|yes|예|네|응)$/i.test(trimmed);
345
+ }
346
+ finally {
347
+ rl.close();
348
+ }
349
+ }
350
+ function tail(value, limit = 1000) {
351
+ return String(value || '').replace(/\s+/g, ' ').trim().slice(-limit);
352
+ }
353
+ //# sourceMappingURL=zellij-self-heal.js.map
@@ -80,7 +80,7 @@ export async function checkZellijUpdateNotice(input = {}) {
80
80
  message: 'Zellij update notice disabled by environment.'
81
81
  });
82
82
  }
83
- const capability = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
83
+ const capability = await checkZellijCapability({ require: false, writeReport: false, env }).catch(() => null);
84
84
  const current = capability?.version || null;
85
85
  const missing = capability?.status === 'missing' || !capability;
86
86
  const fetchInput = { env };
@@ -117,7 +117,7 @@ export async function checkZellijUpdateNotice(input = {}) {
117
117
  * the exact operator command, mirroring how the Codex CLI update flow behaves.
118
118
  */
119
119
  export async function upgradeZellijToLatest(input = {}) {
120
- const before = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
120
+ const before = await checkZellijCapability({ require: false, writeReport: false, env: input.env || process.env }).catch(() => null);
121
121
  const beforeVersion = before?.version || null;
122
122
  const missing = before?.status === 'missing' || !before;
123
123
  const latest = await fetchLatestZellijVersion({ env: input.env || process.env });
@@ -185,7 +185,7 @@ export async function upgradeZellijToLatest(input = {}) {
185
185
  error: `${run.stderr || run.stdout || 'brew upgrade failed'}`.trim().slice(-1000)
186
186
  };
187
187
  }
188
- const after = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
188
+ const after = await checkZellijCapability({ require: false, writeReport: false, env: input.env || process.env }).catch(() => null);
189
189
  return {
190
190
  status: missing ? 'installed' : 'upgraded',
191
191
  before_version: beforeVersion,
@@ -222,9 +222,34 @@ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
222
222
  if (!notice)
223
223
  return { status: 'skipped', current: null, latest: null, command: null };
224
224
  if (notice.zellij_missing) {
225
- // Zellij is an optional integration; installation is owned by
226
- // `sks deps check --yes` / `sks zellij update --yes`. Just surface the hint.
227
- console.log(`Zellij not found (optional live panes disabled). Install with: ${notice.upgrade_command}`);
225
+ if (opts.selfHealOnMissing === true) {
226
+ const { repairZellijForSks } = await import('./zellij-self-heal.js');
227
+ const repaired = await repairZellijForSks({
228
+ root: opts.root || process.cwd(),
229
+ requestedBy: opts.label === 'MAD launch' ? 'sks --mad' : 'sks zellij update',
230
+ fixRequested: true,
231
+ autoApprove: opts.autoApprove === true || list.includes('--yes') || list.includes('-y'),
232
+ interactive: mode === 'interactive-prompt',
233
+ installHomebrew: opts.installHomebrew === true || list.includes('--install-homebrew'),
234
+ allowHeadlessFallback: opts.allowHeadlessFallback === true,
235
+ missionDir: opts.missionDir || null,
236
+ env
237
+ });
238
+ if (repaired.strategy === 'headless-fallback')
239
+ console.log('Zellij repair: headless fallback selected (live_panes=false).');
240
+ else if (repaired.ok && repaired.command)
241
+ console.log(`Zellij repair: ${repaired.strategy} via ${repaired.command}`);
242
+ else if (!repaired.ok)
243
+ console.log(`Zellij repair required. Run: ${repaired.command || notice.upgrade_command}`);
244
+ return {
245
+ status: repaired.ok ? (repaired.strategy === 'headless-fallback' ? 'missing' : 'installed') : 'manual_required',
246
+ current: repaired.after.version || repaired.before.version,
247
+ latest: repaired.latest_version,
248
+ command: repaired.command,
249
+ error: repaired.blockers[0] || null
250
+ };
251
+ }
252
+ console.log(`Zellij missing, required for sks --mad. Repairable with: sks doctor --fix --yes`);
228
253
  return { status: 'missing', current: null, latest: notice.latest_version, command: notice.upgrade_command };
229
254
  }
230
255
  if (!notice.update_available) {
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import fs from 'node:fs';
4
+ import fsp from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { assertGate, emitGate, importDist, root } from './sks-1-18-gate-lib.js';
9
+ export async function runDirective314Gate(id) {
10
+ if (id.startsWith('zellij:'))
11
+ return zellijGate(id);
12
+ if (id.startsWith('doctor:zellij'))
13
+ return doctorZellijGate(id);
14
+ if (id.startsWith('mad:zellij'))
15
+ return madZellijGate(id);
16
+ if (id === 'lazycodex:analysis')
17
+ return lazycodexAnalysisGate(id);
18
+ if (id.startsWith('codex-app:') || id === 'doctor:codex-app-harness')
19
+ return codexAppGate(id);
20
+ if (id.startsWith('loop:'))
21
+ return loopGate(id);
22
+ if (id === 'lazycodex:interop-policy' || id === 'lazycodex:pattern-adoption-blackbox')
23
+ return lazycodexInteropGate(id);
24
+ throw new Error(`unknown_gate:${id}`);
25
+ }
26
+ async function zellijGate(id) {
27
+ const rootDir = await tempRoot(`sks-${id.replace(/[:/]/g, '-')}-`);
28
+ const selfHeal = await importDist('core/zellij/zellij-self-heal.js');
29
+ const policy = await importDist('core/zellij/homebrew-policy.js');
30
+ if (id === 'zellij:homebrew-policy') {
31
+ assertGate(policy.resolveHomebrewInstallPolicy({ env: {} }).allowed === false, 'Homebrew install must not be silent by default');
32
+ assertGate(policy.resolveHomebrewInstallPolicy({ installHomebrew: true }).allowed === false, 'Homebrew install flag alone must still require --yes or interactive/env approval');
33
+ assertGate(policy.resolveHomebrewInstallPolicy({ installHomebrew: true, autoApprove: true }).allowed === true, 'Homebrew install should be allowed with explicit flag+yes');
34
+ assertGate(policy.homebrewMissingDoctorMessage().includes('sks doctor --fix --install-homebrew --yes'), 'Homebrew policy must expose one-shot doctor command');
35
+ return emitGate(id, { fixtures: 3 });
36
+ }
37
+ if (id === 'zellij:update-missing-self-heal') {
38
+ const update = await importDist('core/zellij/zellij-update.js');
39
+ const result = await update.maybePromptZellijUpdateForLaunch(['--yes'], {
40
+ label: 'MAD launch',
41
+ root: rootDir,
42
+ selfHealOnMissing: true,
43
+ autoApprove: true,
44
+ env: fakeZellijEnv('missing', { brew: true })
45
+ });
46
+ assertGate(result.status === 'installed', 'missing update path must self-heal when requested', result);
47
+ return emitGate(id, { status: result.status });
48
+ }
49
+ const result = await selfHeal.repairZellijForSks({
50
+ root: rootDir,
51
+ requestedBy: 'doctor --fix',
52
+ fixRequested: true,
53
+ autoApprove: true,
54
+ installHomebrew: false,
55
+ env: fakeZellijEnv('missing', { brew: true })
56
+ });
57
+ assertGate(result.ok === true, 'zellij self-heal must succeed with fake brew present', result);
58
+ assertGate(result.strategy === 'brew-install-zellij', 'missing zellij must select brew-install-zellij', result);
59
+ assertGate(fs.existsSync(path.join(rootDir, '.sneakoscope', 'reports', 'zellij-self-heal.json')), 'self-heal artifact missing');
60
+ emitGate(id, { strategy: result.strategy });
61
+ }
62
+ async function doctorZellijGate(id) {
63
+ const rootDir = await tempRoot(`sks-${id.replace(/[:/]/g, '-')}-`);
64
+ const mod = await importDist('core/doctor/doctor-zellij-repair.js');
65
+ const env = fakeZellijEnv(id.includes('upgrade') ? 'too_old' : 'missing', { brew: !id.includes('no-homebrew') });
66
+ const previous = swapEnv(env);
67
+ try {
68
+ const result = await mod.runDoctorZellijRepair({ root: rootDir, args: ['--fix', '--yes'], doctorFix: true });
69
+ if (id.includes('no-homebrew')) {
70
+ assertGate(result.strategy === 'manual-required', 'no-homebrew doctor repair must be manual-required', result);
71
+ assertGate(String(result.command).includes('--install-homebrew'), 'manual no-homebrew path must show install-homebrew command', result);
72
+ }
73
+ else if (id.includes('upgrade')) {
74
+ assertGate(result.strategy === 'brew-upgrade-zellij', 'stale zellij must upgrade', result);
75
+ }
76
+ else {
77
+ assertGate(result.strategy === 'brew-install-zellij', 'missing zellij must install', result);
78
+ }
79
+ if (id === 'doctor:zellij-fix-output') {
80
+ const line = mod.doctorZellijRepairConsoleLine(result);
81
+ assertGate(!/optional live panes disabled/i.test(line), 'doctor repair output must not use optional/blocking wording', { line });
82
+ }
83
+ emitGate(id, { strategy: result.strategy });
84
+ }
85
+ finally {
86
+ restoreEnv(previous);
87
+ }
88
+ }
89
+ async function madZellijGate(id) {
90
+ const source = fs.readFileSync(path.join(root, 'src/core/commands/mad-sks-command.ts'), 'utf8');
91
+ assertGate(source.includes("requestedBy: 'sks --mad'"), 'MAD must request zellij self-heal as sks --mad');
92
+ assertGate(source.includes('--headless') && source.includes('live_panes: false'), 'MAD must support headless live_panes=false');
93
+ assertGate(!/optional live panes disabled/.test(source), 'MAD source must not print optional live panes disabled');
94
+ if (id === 'mad:zellij-no-contradictory-output') {
95
+ const update = fs.readFileSync(path.join(root, 'src/core/zellij/zellij-update.ts'), 'utf8');
96
+ assertGate(!/Zellij not found \(optional live panes disabled\)/.test(update), 'Zellij missing output must not be contradictory optional wording');
97
+ }
98
+ emitGate(id, { source_checked: true });
99
+ }
100
+ async function lazycodexAnalysisGate(id) {
101
+ const mod = await importDist('core/codex-app/lazycodex-analysis.js');
102
+ const report = await mod.writeLazyCodexPatternAnalysis(root);
103
+ assertGate(report.patterns.length >= 14, 'LazyCodex analysis must include required patterns', report);
104
+ const docs = mod.renderLazyCodexAnalysisMarkdown(report);
105
+ await fsp.writeFile(path.join(root, 'docs', 'lazycodex-analysis.md'), `${docs}\n`, 'utf8');
106
+ emitGate(id, { patterns: report.patterns.length });
107
+ }
108
+ async function codexAppGate(id) {
109
+ const rootDir = await tempRoot(`sks-${id.replace(/[:/]/g, '-')}-`);
110
+ const previous = swapEnv({
111
+ SKS_CODEX_0138_FAKE: '1',
112
+ SKS_CODEX_0139_FAKE: '1',
113
+ SKS_CODEX_PLUGIN_JSON_FAKE: '1',
114
+ SKS_CODEX_AGENT_TYPE_SUPPORTED: id.includes('blackbox') ? '1' : ''
115
+ });
116
+ try {
117
+ if (id === 'codex-app:harness-matrix' || id === 'doctor:codex-app-harness' || id === 'codex-app:harness-blackbox') {
118
+ const mod = await importDist('core/codex-app/codex-app-harness-matrix.js');
119
+ const matrix = await mod.buildCodexAppHarnessMatrix({ root: rootDir });
120
+ assertGate(matrix.schema === 'sks.codex-app-harness-matrix.v1', 'harness matrix schema mismatch', matrix);
121
+ assertGate(matrix.app_features.plugin_json === true, 'fixture should expose plugin_json', matrix);
122
+ if (id === 'doctor:codex-app-harness') {
123
+ const doctor = fs.readFileSync(path.join(root, 'src/commands/doctor.ts'), 'utf8');
124
+ assertGate(doctor.includes('Codex App Harness:'), 'doctor output must include Codex App Harness section');
125
+ assertGate(doctor.includes('codex_app_harness_matrix'), 'doctor JSON must include codex_app_harness_matrix');
126
+ }
127
+ return emitGate(id, { ok: matrix.ok, warnings: matrix.warnings.length });
128
+ }
129
+ if (id === 'codex-app:skill-sync' || id === 'codex-app:skill-agent-blackbox') {
130
+ const mod = await importDist('core/codex-app/codex-skill-sync.js');
131
+ const skillsRoot = path.join(rootDir, 'skills');
132
+ await fsp.mkdir(path.join(skillsRoot, 'ulw-loop'), { recursive: true });
133
+ const report = await mod.syncCodexSksSkills({ root: rootDir, skillsRoot, apply: true });
134
+ assertGate(report.interop.clobbered_lazycodex === false && report.lazycodex_reserved_present.includes('ulw-loop'), 'skill sync must preserve LazyCodex skills', report);
135
+ return emitGate(id, { desired: report.desired_skills.length });
136
+ }
137
+ if (id === 'codex-app:agent-role-sync') {
138
+ const mod = await importDist('core/codex-app/codex-agent-role-sync.js');
139
+ const report = await mod.syncCodexAgentRoles({ root: rootDir, codexHome: path.join(rootDir, 'codex-home'), apply: true, agentTypeSupported: true });
140
+ assertGate(report.fallback === 'agent_type', 'agent role sync should use agent_type when supported', report);
141
+ return emitGate(id, { roles: report.directive_roles.length });
142
+ }
143
+ if (id === 'codex-app:init-deep') {
144
+ const mod = await importDist('core/codex-app/codex-init-deep.js');
145
+ await fsp.mkdir(path.join(rootDir, 'src/core/zellij'), { recursive: true });
146
+ await fsp.writeFile(path.join(rootDir, 'src/core/zellij/a.ts'), 'export {}\n');
147
+ const report = await mod.runCodexInitDeep({ root: rootDir, apply: true });
148
+ assertGate(report.root_agents_preserved === true, 'init-deep must preserve user AGENTS.md', report);
149
+ return emitGate(id, { guidance: report.directory_guidance.length });
150
+ }
151
+ if (id === 'codex-app:hook-lifecycle') {
152
+ const mod = await importDist('core/codex-app/codex-hook-lifecycle.js');
153
+ const report = await mod.buildCodexHookLifecycle({ root: rootDir });
154
+ assertGate(report.approval_state === 'unknown', 'hook lifecycle must report unknown approval when not detectable', report);
155
+ return emitGate(id, { lifecycle: Object.keys(report.lifecycle).length });
156
+ }
157
+ if (id === 'codex-app:execution-profile') {
158
+ const mod = await importDist('core/codex-app/codex-app-execution-profile.js');
159
+ const profile = await mod.resolveCodexAppExecutionProfile({ root: rootDir });
160
+ assertGate(['codex-app-native', 'codex-cli-headless', 'sks-loop-headless', 'degraded-no-app'].includes(profile.mode), 'execution profile mode invalid', profile);
161
+ return emitGate(id, { mode: profile.mode });
162
+ }
163
+ }
164
+ finally {
165
+ restoreEnv(previous);
166
+ }
167
+ }
168
+ async function loopGate(id) {
169
+ const rootDir = await tempRoot(`sks-${id.replace(/[:/]/g, '-')}-`);
170
+ if (id === 'loop:planner-project-memory') {
171
+ const init = await importDist('core/codex-app/codex-init-deep.js');
172
+ const planner = await importDist('core/loops/loop-planner.js');
173
+ await fsp.mkdir(path.join(rootDir, 'src/core/loops'), { recursive: true });
174
+ await fsp.writeFile(path.join(rootDir, 'src/core/loops/a.ts'), 'export {}\n');
175
+ await init.runCodexInitDeep({ root: rootDir, apply: true });
176
+ const plan = await planner.planLoopsFromRequest({ root: rootDir, missionId: 'M-loop-memory', request: 'update loop planner project memory', sourceCommand: 'loop' });
177
+ assertGate(plan.project_memory?.injected === true, 'loop planner must consume init-deep memory hints', plan);
178
+ return emitGate(id, { injected: true });
179
+ }
180
+ const planDir = path.join(rootDir, '.sneakoscope', 'missions', 'M-loop-cont', 'loops');
181
+ await fsp.mkdir(planDir, { recursive: true });
182
+ await fsp.writeFile(path.join(rootDir, '.sneakoscope', 'missions', 'M-loop-cont', 'loops', 'loop-plan.json'), JSON.stringify({ graph: { nodes: [{ loop_id: 'loop-a' }] } }));
183
+ const mod = await importDist('core/loops/loop-continuation-enforcer.js');
184
+ const report = await mod.evaluateLoopContinuation({ root: rootDir, missionId: 'M-loop-cont' });
185
+ assertGate(report.should_continue === true, 'loop continuation should request resume when proof missing', report);
186
+ emitGate(id, { should_continue: report.should_continue });
187
+ }
188
+ async function lazycodexInteropGate(id) {
189
+ const rootDir = await tempRoot(`sks-${id.replace(/[:/]/g, '-')}-`);
190
+ const previous = swapEnv({ SKS_CODEX_PLUGIN_JSON_FAKE: '1' });
191
+ try {
192
+ const mod = await importDist('core/codex-app/lazycodex-interop-policy.js');
193
+ const skillsRoot = path.join(rootDir, '.codex', 'skills');
194
+ await fsp.mkdir(path.join(skillsRoot, 'start-work'), { recursive: true });
195
+ const report = await mod.buildLazyCodexInteropPolicy({ root: rootDir, codexHome: path.join(rootDir, '.codex') });
196
+ assertGate(report.policy.clobber_lazycodex_skills === false, 'interop policy must not clobber LazyCodex skills', report);
197
+ emitGate(id, { detected: report.lazycodex_detected, collisions: report.detection.collisions.length });
198
+ }
199
+ finally {
200
+ restoreEnv(previous);
201
+ }
202
+ }
203
+ function fakeZellijEnv(status, opts = {}) {
204
+ return {
205
+ ...process.env,
206
+ SKS_ZELLIJ_CAPABILITY_FAKE_STATUS: status,
207
+ SKS_ZELLIJ_CAPABILITY_FAKE_VERSION: status === 'too_old' ? '0.40.0' : '0.44.0',
208
+ SKS_ZELLIJ_SELF_HEAL_BEFORE_STATUS: status,
209
+ SKS_ZELLIJ_SELF_HEAL_BEFORE_VERSION: status === 'too_old' ? '0.40.0' : '',
210
+ SKS_ZELLIJ_SELF_HEAL_AFTER_STATUS: 'ok',
211
+ SKS_ZELLIJ_SELF_HEAL_AFTER_VERSION: '0.44.3',
212
+ SKS_ZELLIJ_LATEST_VERSION: '0.44.3',
213
+ SKS_ZELLIJ_SELF_HEAL_FAKE_RUN: '1',
214
+ SKS_ZELLIJ_SELF_HEAL_BREW_PRESENT: opts.brew ? '1' : '0'
215
+ };
216
+ }
217
+ async function tempRoot(prefix) {
218
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
219
+ await fsp.mkdir(path.join(dir, '.sneakoscope', 'reports'), { recursive: true });
220
+ return dir;
221
+ }
222
+ function swapEnv(next) {
223
+ const previous = {};
224
+ for (const [key, value] of Object.entries(next)) {
225
+ previous[key] = process.env[key];
226
+ if (value === '')
227
+ delete process.env[key];
228
+ else
229
+ process.env[key] = value;
230
+ }
231
+ return previous;
232
+ }
233
+ function restoreEnv(previous) {
234
+ for (const [key, value] of Object.entries(previous)) {
235
+ if (value === undefined)
236
+ delete process.env[key];
237
+ else
238
+ process.env[key] = value;
239
+ }
240
+ }
241
+ //# sourceMappingURL=sks-3-1-4-directive-check-lib.js.map