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,51 @@
1
+ // @ts-nocheck
2
+ import path from 'node:path';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ export function buildLazyCodexPatternAnalysis() {
5
+ return {
6
+ schema: 'sks.lazycodex-pattern-analysis.v1',
7
+ source_repo: 'code-yeongyu/lazycodex',
8
+ analyzed_at: nowIso(),
9
+ patterns: [
10
+ pattern('npx-no-global-install', 'npx no-global install alias', ['Directive: npx lazycodex-ai install aliases npx --yes --package oh-my-openagent omo install --platform=codex.'], 'adapt', 'Keep optional tooling no-global by default and record repair transactions.', ['src/cli/install-helpers.ts', 'src/core/zellij/zellij-self-heal.ts']),
11
+ pattern('codex-marketplace-plugin', 'Codex marketplace plugin add/upgrade', ['Directive: codex plugin marketplace add and codex plugin add omo@sisyphuslabs.'], 'adapt', 'Track marketplace/plugin inventory without assuming hooks are approved.', ['src/core/codex-app/codex-app-harness-matrix.ts']),
12
+ pattern('startup-review-hooks', 'Startup review hook approval', ['Directive: hooks require Codex startup review approval and re-approval after modifications.'], 'adopt', 'Separate installed hook files from approval state; unknown remains unknown.', ['src/core/codex-app/codex-hook-lifecycle.ts']),
13
+ pattern('background-bootstrap', 'Background bootstrap and restart notice', ['Directive: first approved session performs background bootstrap and upgrade may require restart.'], 'adapt', 'Report bootstrap proof as warning/blocker instead of silently assuming completion.', ['src/core/codex-app/codex-app-harness-matrix.ts']),
14
+ pattern('doctor-health-report', 'Doctor health report for plugin cache/hooks/MCP/agents/config', ['Directive: LazyCodex doctor reports plugin cache, hooks, MCP servers, agents, config state.'], 'adopt', 'Add Codex App Harness section to SKS doctor.', ['src/commands/doctor.ts']),
15
+ pattern('dollar-skill-picker', '$ skill picker and $command invocation', ['Directive: Codex composer $ browses installed skills.'], 'adapt', 'Keep SKS route skills synced without clobbering user or LazyCodex skills.', ['src/core/codex-app/codex-skill-sync.ts']),
16
+ pattern('init-deep-agents', '$init-deep hierarchical AGENTS.md', ['Directive: init-deep creates hierarchical AGENTS.md context.'], 'adapt', 'Generate SKS memory under .sneakoscope/context by default and preserve user AGENTS.md.', ['src/core/codex-app/codex-init-deep.ts']),
17
+ pattern('plan-start-loop', '$ulw-plan, $start-work, $ulw-loop command pillars', ['Directive: separate planning, durable work, and evidence loop.'], 'adapt', 'Map onto sks loop plan/run/proof without replacing existing Loop Mesh.', ['src/core/commands/loop-command.ts']),
18
+ pattern('specialist-skills', 'Specialist skills', ['Directive: specialist skills include review-work, LSP, AST-grep, programming, frontend UI/UX.'], 'watch', 'Keep checker profile selection explicit and evidence-backed.', ['src/core/loops/loop-gate-selector.ts']),
19
+ pattern('native-agent-type', 'Native spawn_agent agent_type with message fallback', ['Directive: LazyCodex probes agent_type and falls back to role in message.'], 'adopt', 'Expose native agent_type capability in execution profile.', ['src/core/codex-app/codex-agent-role-sync.ts', 'src/core/codex-app/codex-app-execution-profile.ts']),
20
+ pattern('multi-model-routing', 'Multi-model routing', ['Directive: LazyCodex/OmO route multiple models.'], 'watch', 'SKS keeps provider/profile policy separate from harness matrix.', ['src/core/provider/provider-context.ts']),
21
+ pattern('hook-continuation', 'Hook lifecycle and continuation enforcer', ['Directive: UserPromptSubmit, PreToolUse, PostToolUse, Stop, Notification map to pipeline actions.'], 'adopt', 'Map lifecycle and add Loop continuation proof adapter.', ['src/core/codex-app/codex-hook-lifecycle.ts', 'src/core/loops/loop-continuation-enforcer.ts']),
22
+ pattern('skill-mcp-slashcommand', 'Skill MCP and slashcommand tool', ['Directive: OmO exposes skill MCP and slashcommand tools.'], 'adapt', 'Report MCP candidates and SKS route skill availability without assuming external plugin behavior.', ['src/core/codex-app/codex-app-harness-matrix.ts']),
23
+ pattern('lsp-ast-grep', 'LSP/AST-grep optional tooling', ['Directive: LSP/AST-grep are optional-but-first-class loop gates/tools.'], 'watch', 'Use as future specialist gates; do not add unrequested fallback tooling now.', ['src/core/loops/loop-gate-selector.ts'])
24
+ ],
25
+ blockers: []
26
+ };
27
+ }
28
+ export async function writeLazyCodexPatternAnalysis(root) {
29
+ const report = buildLazyCodexPatternAnalysis();
30
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'lazycodex-analysis.json'), report);
31
+ return report;
32
+ }
33
+ export function renderLazyCodexAnalysisMarkdown(report) {
34
+ const rows = report.patterns.map((p) => `| ${p.id} | ${p.sks_adoption} | ${p.rationale.replace(/\|/g, '\\|')} |`).join('\n');
35
+ return [
36
+ '# LazyCodex / OmO Pattern Analysis',
37
+ '',
38
+ `Source repo: \`${report.source_repo}\``,
39
+ `Analyzed at: \`${report.analyzed_at}\``,
40
+ '',
41
+ '| Pattern | Adoption | Rationale |',
42
+ '|---|---|---|',
43
+ rows,
44
+ '',
45
+ 'This artifact is deterministic and based on the SKS 3.1.4 directive plus current SKS repository surfaces. Live LazyCodex runtime behavior remains a separate verification concern.'
46
+ ].join('\n');
47
+ }
48
+ function pattern(id, title, evidence, sks_adoption, rationale, target_modules) {
49
+ return { id, title, evidence, sks_adoption, rationale, target_modules };
50
+ }
51
+ //# sourceMappingURL=lazycodex-analysis.js.map
@@ -0,0 +1,49 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
6
+ import { buildCodexPluginInventory } from '../codex-plugins/codex-plugin-json.js';
7
+ export async function buildLazyCodexInteropPolicy(input) {
8
+ const root = path.resolve(input.root);
9
+ const inventory = input.inventory || await buildCodexPluginInventory().catch((err) => ({ plugins: [], blockers: [err?.message || String(err)] }));
10
+ const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
11
+ const skillNames = await discoverSkillNames([path.join(root, '.agents', 'skills'), path.join(codexHome, 'skills')]);
12
+ const pluginIds = (inventory.plugins || []).map((plugin) => `${plugin.id || ''} ${plugin.name || ''}`.toLowerCase());
13
+ const lazycodexInstalled = pluginIds.some((id) => id.includes('omo') || id.includes('lazycodex'))
14
+ || ['ulw-loop', 'ulw-plan', 'start-work'].some((name) => skillNames.includes(name));
15
+ const collisions = ['ulw-loop', 'ulw-plan', 'start-work'].filter((name) => skillNames.includes(name));
16
+ const report = {
17
+ schema: 'sks.lazycodex-interop-policy.v1',
18
+ generated_at: nowIso(),
19
+ ok: true,
20
+ mode: input.mode || 'coexist',
21
+ lazycodex_detected: lazycodexInstalled,
22
+ detection: {
23
+ plugin_inventory_ids: pluginIds,
24
+ skill_names: skillNames,
25
+ collisions
26
+ },
27
+ policy: {
28
+ clobber_lazycodex_skills: false,
29
+ clobber_user_skills: false,
30
+ default_mode: 'coexist',
31
+ explicit_handoff_required: true
32
+ },
33
+ actions: collisions.map((name) => `preserve_existing_skill:${name}`),
34
+ blockers: []
35
+ };
36
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'lazycodex-interop-policy.json'), report).catch(() => undefined);
37
+ return report;
38
+ }
39
+ async function discoverSkillNames(roots) {
40
+ const names = new Set();
41
+ for (const root of roots) {
42
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
43
+ for (const entry of entries)
44
+ if (entry.isDirectory())
45
+ names.add(entry.name);
46
+ }
47
+ return [...names].sort();
48
+ }
49
+ //# sourceMappingURL=lazycodex-interop-policy.js.map
@@ -11,6 +11,7 @@ import { runLoopNode, runLoopPlan } from '../loops/loop-runtime.js';
11
11
  import { scheduleLoopGraph } from '../loops/loop-scheduler.js';
12
12
  import { writeLoopKillRequest } from '../loops/loop-runtime-control.js';
13
13
  import { flag, promptOf, readFlagValue } from './command-utils.js';
14
+ import { runCodexInitDeep } from '../codex-app/codex-init-deep.js';
14
15
  export async function loopCommand(subcommand = 'help', args = []) {
15
16
  const action = subcommand || 'help';
16
17
  if (action === 'plan')
@@ -27,6 +28,8 @@ export async function loopCommand(subcommand = 'help', args = []) {
27
28
  return loopResume(args);
28
29
  if (action === 'graph')
29
30
  return loopGraph(args);
31
+ if (action === 'init-deep')
32
+ return loopInitDeep(args);
30
33
  console.log(`SKS Loop
31
34
 
32
35
  Usage:
@@ -37,8 +40,16 @@ Usage:
37
40
  sks loop kill <loop-id|all>
38
41
  sks loop resume latest [--rerun-completed]
39
42
  sks loop graph latest
43
+ sks loop init-deep [--json]
40
44
  `);
41
45
  }
46
+ async function loopInitDeep(args) {
47
+ const root = await sksRoot();
48
+ const result = await runCodexInitDeep({ root, apply: !flag(args, '--check-only') && !flag(args, '--dry-run') });
49
+ if (flag(args, '--json'))
50
+ return printJson(result);
51
+ console.log(`Loop init-deep: ${result.ok ? 'ok' : 'blocked'} ${result.generated_path || ''}`);
52
+ }
42
53
  async function loopPlan(args) {
43
54
  const root = await sksRoot();
44
55
  const request = promptOf(args);
@@ -22,6 +22,7 @@ import { checkSksUpdateNotice } from '../update/update-notice.js';
22
22
  import { createMadDbCapability, MAD_DB_ACK } from '../mad-db/mad-db-capability.js';
23
23
  import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
24
24
  import { writeCodex0139CapabilityArtifacts } from '../codex-control/codex-0139-capability.js';
25
+ import { repairZellijForSks } from '../zellij/zellij-self-heal.js';
25
26
  export async function madHighCommand(args = [], deps = {}) {
26
27
  const subcommand = firstSubcommand(args);
27
28
  if (subcommand)
@@ -32,19 +33,52 @@ export async function madHighCommand(args = [], deps = {}) {
32
33
  return console.log(JSON.stringify(profile, null, 2));
33
34
  }
34
35
  const update = { status: 'notice_only', non_blocking: true };
36
+ const rawArgs = (args || []).map((arg) => String(arg));
37
+ const headlessZellij = rawArgs.includes('--headless') || process.env.SKS_MAD_ALLOW_HEADLESS === '1';
38
+ const skipZellijRepair = rawArgs.includes('--skip-zellij-repair') || rawArgs.includes('--no-auto-install-zellij');
39
+ const launchRoot = process.cwd();
40
+ if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
41
+ await initProject(launchRoot, {});
35
42
  const codexUpdate = deps.maybePromptCodexUpdateForLaunch ? await deps.maybePromptCodexUpdateForLaunch(args, { label: 'MAD launch' }) : { status: 'skipped' };
36
43
  if (codexUpdate.status === 'failed' || codexUpdate.status === 'updated_not_reflected') {
37
44
  console.error(`Codex CLI update failed: ${codexUpdate.error || 'updated version was not visible on PATH'}`);
38
45
  process.exitCode = 1;
39
46
  return;
40
47
  }
41
- // Zellij is checked the same way Codex is, but it stays NON-blocking: a
42
- // failed or skipped zellij upgrade never prevents the MAD launch.
43
- const zellijUpdate = deps.maybePromptZellijUpdateForLaunch ? await deps.maybePromptZellijUpdateForLaunch(args, { label: 'MAD launch' }).catch(() => ({ status: 'error' })) : { status: 'skipped' };
44
- void zellijUpdate;
45
- const depStatus = deps.ensureMadLaunchDependencies ? await deps.ensureMadLaunchDependencies(args) : { ready: true, actions: [] };
48
+ const zellijUpdate = skipZellijRepair
49
+ ? { status: 'skipped', command: 'sks doctor --fix --yes' }
50
+ : deps.maybePromptZellijUpdateForLaunch
51
+ ? await deps.maybePromptZellijUpdateForLaunch(args, {
52
+ label: 'MAD launch',
53
+ root: launchRoot,
54
+ selfHealOnMissing: true,
55
+ autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
56
+ installHomebrew: rawArgs.includes('--install-homebrew'),
57
+ allowHeadlessFallback: headlessZellij
58
+ }).catch(() => ({ status: 'error', command: 'sks doctor --fix --yes' }))
59
+ : await repairZellijForSks({
60
+ root: launchRoot,
61
+ requestedBy: 'sks --mad',
62
+ fixRequested: true,
63
+ autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
64
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.SKS_NO_QUESTION !== '1'),
65
+ installHomebrew: rawArgs.includes('--install-homebrew'),
66
+ allowHeadlessFallback: headlessZellij
67
+ });
68
+ const zellijRepairBlocked = !headlessZellij && (zellijUpdate.status === 'manual_required'
69
+ || zellijUpdate.strategy === 'manual-required'
70
+ || zellijUpdate.ok === false);
71
+ if (zellijRepairBlocked) {
72
+ console.error('SKS MAD launch blocked by Zellij repair_required.');
73
+ console.error(`Run: ${zellijUpdate.command || 'sks doctor --fix --yes'}`);
74
+ process.exitCode = 1;
75
+ return { ok: false, status: 'repair_required', command: zellijUpdate.command || 'sks doctor --fix --yes', zellij_repair: zellijUpdate };
76
+ }
77
+ const depStatus = skipZellijRepair && deps.ensureMadLaunchDependencies
78
+ ? await deps.ensureMadLaunchDependencies(args)
79
+ : { ready: true, actions: [] };
46
80
  if (!depStatus.ready) {
47
- console.error('SKS MAD launch blocked by missing dependencies.');
81
+ console.error('SKS MAD launch blocked by required Zellij dependency.');
48
82
  for (const action of depStatus.actions)
49
83
  deps.printDepsInstallAction?.(action);
50
84
  process.exitCode = 1;
@@ -56,9 +90,6 @@ export async function madHighCommand(args = [], deps = {}) {
56
90
  return;
57
91
  }
58
92
  const profile = buildMadHighLaunchProfileNoWrite();
59
- const launchRoot = process.cwd();
60
- if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
61
- await initProject(launchRoot, {});
62
93
  const uiSnapshotId = Date.now().toString(36);
63
94
  const beforeUi = await writeCodexAppUiSnapshot(launchRoot, `mad-before-${uiSnapshotId}`).catch(() => null);
64
95
  // launchFast skips the redundant live-`codex exec` config probe (up to ~20s, run
@@ -66,7 +97,6 @@ export async function madHighCommand(args = [], deps = {}) {
66
97
  // later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
67
98
  // readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
68
99
  // old behavior.
69
- const rawArgs = (args || []).map((arg) => String(arg));
70
100
  const madDbRequested = rawArgs.includes('--mad-db');
71
101
  const madDbAck = readOption(rawArgs, '--ack', '');
72
102
  if (madDbRequested && madDbAck !== MAD_DB_ACK) {
@@ -138,7 +168,9 @@ export async function madHighCommand(args = [], deps = {}) {
138
168
  };
139
169
  const launchOpts = codexLbImmediateLaunchOpts(cleanArgs, launchLb, { codexArgs: profile.launch_args, conciseBlockers: true, madSksEnv, launchEnv: madSksEnv });
140
170
  const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', launchOpts.session || `sks-mad-${sanitizeZellijSessionName(process.cwd())}`));
141
- const launch = await launchMadZellijUi([...cleanArgs, '--workspace', workspace], { ...launchOpts, missionId: madLaunch.mission_id, root: madLaunch.root, cwd: process.cwd(), ledgerRoot: path.join(madLaunch.dir, 'agents'), slotCount: 0, requireZellij: process.env.SKS_REQUIRE_ZELLIJ === '1' });
171
+ const launch = headlessZellij
172
+ ? await writeMadHeadlessZellijFallback(madLaunch, workspace)
173
+ : await launchMadZellijUi([...cleanArgs, '--workspace', workspace], { ...launchOpts, missionId: madLaunch.mission_id, root: madLaunch.root, cwd: process.cwd(), ledgerRoot: path.join(madLaunch.dir, 'agents'), slotCount: 0, requireZellij: process.env.SKS_REQUIRE_ZELLIJ === '1' });
142
174
  const afterLaunchUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-launch-${uiSnapshotId}`).catch(() => null) : null;
143
175
  const launchUiDiff = beforeUi && afterLaunchUi ? diffCodexAppUiSnapshots(beforeUi, afterLaunchUi) : null;
144
176
  if (launchUiDiff) {
@@ -157,7 +189,8 @@ export async function madHighCommand(args = [], deps = {}) {
157
189
  schema: 'sks.zellij-initial-ui.v1',
158
190
  ok: true,
159
191
  mission_id: madLaunch.mission_id,
160
- session_name: launch.session_name,
192
+ session_name: launch.session_name || null,
193
+ live_panes: !headlessZellij,
161
194
  initial_panes: 'main-only',
162
195
  dashboard_created: false,
163
196
  worker_panes_created: 0,
@@ -166,16 +199,16 @@ export async function madHighCommand(args = [], deps = {}) {
166
199
  const madNativeSwarm = await startMadNativeSwarm(madLaunch.root, madLaunch, args, profile, {
167
200
  env: {
168
201
  ...madSksEnv,
169
- SKS_ZELLIJ_SESSION_NAME: launch.session_name
202
+ ...(launch.session_name ? { SKS_ZELLIJ_SESSION_NAME: launch.session_name } : {})
170
203
  },
171
- zellijSessionName: launch.session_name,
172
- workerPlacement: shouldAutoAttachZellij(args) ? 'zellij-pane' : 'process',
204
+ zellijSessionName: launch.session_name || null,
205
+ workerPlacement: headlessZellij ? 'process' : shouldAutoAttachZellij(args) ? 'zellij-pane' : 'process',
173
206
  zellijVisiblePaneCap: Number(process.env.SKS_ZELLIJ_VISIBLE_PANE_CAP || 8)
174
207
  });
175
208
  // The launcher only creates a detached background session. In an interactive
176
209
  // terminal, immediately attach so the session actually opens for the user
177
210
  // instead of leaving them to copy/paste the attach command by hand.
178
- if (shouldAutoAttachZellij(args)) {
211
+ if (!headlessZellij && shouldAutoAttachZellij(args)) {
179
212
  console.log(`Opening Zellij session: ${launch.session_name} (detach with Ctrl+q, re-attach later with: ${launch.attach_command_with_env})`);
180
213
  const attached = attachZellijSessionInteractive(launch.session_name, { cwd: process.cwd(), configPath: launch.clipboard_config_path });
181
214
  if (!attached.ok) {
@@ -187,6 +220,8 @@ export async function madHighCommand(args = [], deps = {}) {
187
220
  }
188
221
  if (launch.attach_command_with_env)
189
222
  console.log(`Attach with: ${launch.attach_command_with_env}`);
223
+ if (headlessZellij)
224
+ console.log('MAD launch running headless: live_panes=false.');
190
225
  return launch;
191
226
  }
192
227
  export async function startMadNativeSwarm(root, madLaunch, args = [], profile = {}, opts = {}) {
@@ -375,6 +410,27 @@ function formatMadZellijAction(launch) {
375
410
  const report = launch.report_path ? ` | report: ${launch.report_path}` : '';
376
411
  return `${blockers}${detail}${report}`;
377
412
  }
413
+ async function writeMadHeadlessZellijFallback(madLaunch, workspace) {
414
+ const report = {
415
+ schema: 'sks.zellij-session.v1',
416
+ generated_at: nowIso(),
417
+ ok: true,
418
+ kind: 'mad',
419
+ status: 'headless-fallback',
420
+ live_panes: false,
421
+ mission_id: madLaunch.mission_id,
422
+ session_name: null,
423
+ workspace,
424
+ root: madLaunch.root,
425
+ cwd: path.resolve(process.cwd()),
426
+ attach_command_with_env: null,
427
+ blockers: [],
428
+ warnings: ['zellij_headless_fallback_live_panes_false']
429
+ };
430
+ await writeJsonAtomic(path.join(madLaunch.dir, 'zellij-session.json'), report);
431
+ await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_sks.zellij_headless_fallback', live_panes: false });
432
+ return report;
433
+ }
378
434
  async function activateMadZellijPermissionState(cwd = process.cwd(), args = []) {
379
435
  const root = await sksRoot();
380
436
  if (!(await exists(path.join(root, '.sneakoscope'))))
@@ -473,6 +529,9 @@ function madLaunchOnlyFlags() {
473
529
  '--attach',
474
530
  '--no-attach',
475
531
  '--no-auto-install-zellij',
532
+ '--skip-zellij-repair',
533
+ '--install-homebrew',
534
+ '--headless',
476
535
  '--allow-system',
477
536
  '--allow-db-write',
478
537
  '--allow-package-install',
@@ -528,6 +587,8 @@ export function defaultMadSwarmBackend(args = [], opts = {}) {
528
587
  return String(opts.backend);
529
588
  if (list.includes('--json') || list.includes('--no-attach') || opts.nonInteractive === true)
530
589
  return 'codex-sdk';
590
+ if (list.includes('--headless') || process.env.SKS_MAD_ALLOW_HEADLESS === '1')
591
+ return 'codex-sdk';
531
592
  return 'zellij';
532
593
  }
533
594
  function stripMadLaunchOnlyArgs(args = []) {
@@ -65,6 +65,12 @@ export function buildDoctorReadinessMatrix(input = {}) {
65
65
  warnings.add('codex_0139_real_probes_not_run');
66
66
  for (const warning of normalizeList(input.codex_plugin_app_template_policy?.doctor_warnings))
67
67
  warnings.add(warning);
68
+ const codexAppHarness = input.codex_app_harness_matrix || null;
69
+ for (const warning of normalizeList(codexAppHarness?.warnings))
70
+ warnings.add(warning);
71
+ if (codexAppHarness?.ok === false)
72
+ for (const blocker of normalizeList(codexAppHarness.blockers))
73
+ warnings.add(`codex_app_harness:${blocker}`);
68
74
  if (input.codex_lb?.ok === false)
69
75
  warnings.add(`codex_lb_${input.codex_lb?.circuit?.state || 'blocked'}`);
70
76
  const localModel = input.local_model || {};
@@ -124,6 +130,7 @@ export function buildDoctorReadinessMatrix(input = {}) {
124
130
  codex_0139_real_probes: codex0139RealProbes,
125
131
  codex_plugin_inventory: input.codex_plugin_inventory || null,
126
132
  codex_plugin_app_template_policy: input.codex_plugin_app_template_policy || null,
133
+ codex_app_harness_matrix: codexAppHarness,
127
134
  fast_mode_ready: input.fast_mode_ready !== false,
128
135
  codex_app_ui: input.codex_app_ui || null,
129
136
  hooks_ready: input.hooks_ready !== false,
@@ -0,0 +1,36 @@
1
+ // @ts-nocheck
2
+ import { repairZellijForSks } from '../zellij/zellij-self-heal.js';
3
+ export async function runDoctorZellijRepair(input) {
4
+ const args = (input.args || []).map(String);
5
+ if (input.doctorFix !== true)
6
+ return null;
7
+ return repairZellijForSks({
8
+ root: input.root,
9
+ requestedBy: 'doctor --fix',
10
+ fixRequested: true,
11
+ autoApprove: args.includes('--yes') || args.includes('-y'),
12
+ installHomebrew: args.includes('--install-homebrew') || process.env.SKS_ALLOW_HOMEBREW_INSTALL === '1',
13
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.SKS_NO_QUESTION !== '1'),
14
+ allowHeadlessFallback: false,
15
+ env: process.env
16
+ });
17
+ }
18
+ export function doctorZellijRepairConsoleLine(result) {
19
+ if (!result)
20
+ return null;
21
+ if (result.strategy === 'none-current')
22
+ return `Zellij repair: current ${result.after.version || ''}`.trim();
23
+ if (result.ok && (result.strategy === 'brew-install-zellij' || result.strategy === 'brew-install-homebrew-then-zellij')) {
24
+ return `Zellij repair: installed ${result.after.version || 'latest'} via ${result.command || 'brew install zellij'}`;
25
+ }
26
+ if (result.ok && result.strategy === 'brew-upgrade-zellij') {
27
+ return `Zellij repair: upgraded ${result.before.version || 'unknown'} -> ${result.after.version || 'latest'} via ${result.command || 'brew upgrade zellij'}`;
28
+ }
29
+ if (result.strategy === 'manual-required') {
30
+ return `Zellij repair: manual_required\nRun: ${result.command || 'sks doctor --fix --install-homebrew --yes'}`;
31
+ }
32
+ if (result.strategy === 'headless-fallback')
33
+ return 'Zellij repair: headless_fallback live_panes=false';
34
+ return `Zellij repair: failed\nRun: ${result.command || 'sks doctor --fix --yes'}`;
35
+ }
36
+ //# sourceMappingURL=doctor-zellij-repair.js.map
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '3.1.3';
8
+ export const PACKAGE_VERSION = '3.1.4';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -14,6 +14,7 @@ import { REQUIRED_CODEX_MODEL, isForbiddenCodexModel } from './codex-model-guard
14
14
  import { dollarCommand, routeRequiresSubagents, stripVisibleDecisionAnswerBlocks } from './routes.js';
15
15
  import { appendMissionStatus } from './recallpulse.js';
16
16
  import { scanAgentTextForRecursion } from './agents/agent-recursion-guard.js';
17
+ import { evaluateLoopContinuation } from './loops/loop-continuation-enforcer.js';
17
18
  import { buildCompactContinue, buildPermissionRequestAllow, buildPermissionRequestDeny, buildPostToolUseBlock, buildPostToolUseContinue, buildPreToolUseContinue, buildPreToolUseDeny, buildSessionStartContinue, buildStopBlock, buildStopContinue, buildSubagentStartContinue, buildSubagentStopBlock, buildSubagentStopContinue, buildUserPromptSubmitBlock, buildUserPromptSubmitContinue } from './codex-compat/codex-hook-output-builders.js';
18
19
  const TEAM_DIGEST_MAX_EVENTS = 4;
19
20
  const TEAM_DIGEST_MESSAGE_CHARS = 180;
@@ -606,6 +607,18 @@ function clarificationPauseBlockReason(state = {}) {
606
607
  }
607
608
  async function hookStop(root, state, payload, noQuestion) {
608
609
  const last = extractLastMessage(payload);
610
+ if (state?.mode === 'LOOP' || state?.route === 'Loop' || state?.route_command === '$Loop') {
611
+ const missionId = state?.mission_id;
612
+ if (missionId) {
613
+ const continuation = await evaluateLoopContinuation({ root, missionId }).catch(() => null);
614
+ if (continuation?.should_continue) {
615
+ return {
616
+ decision: 'block',
617
+ reason: `SKS Loop continuation required. Resume with: ${continuation.resume_instruction}`
618
+ };
619
+ }
620
+ }
621
+ }
609
622
  if (await consumeCodexGitActionStopBypass(root, payload)) {
610
623
  return {
611
624
  continue: true,
@@ -0,0 +1,32 @@
1
+ // @ts-nocheck
2
+ import path from 'node:path';
3
+ import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
+ import { loopPlanPath } from './loop-artifacts.js';
5
+ export async function evaluateLoopContinuation(input) {
6
+ const root = path.resolve(input.root || process.cwd());
7
+ const plan = await readJson(loopPlanPath(root, input.missionId), null);
8
+ const blockers = [];
9
+ if (!plan)
10
+ blockers.push('loop_plan_missing');
11
+ const nodes = plan?.graph?.nodes || [];
12
+ const proofs = await Promise.all(nodes.map((node) => readJson(path.join(root, '.sneakoscope', 'missions', input.missionId, 'loops', node.loop_id, 'loop-proof.json'), null)));
13
+ const completed = proofs.filter((proof) => proof?.status === 'completed').length;
14
+ const incomplete = Math.max(0, nodes.length - completed);
15
+ const shouldContinue = Boolean(plan && incomplete > 0 && blockers.length === 0);
16
+ const report = {
17
+ schema: 'sks.loop-continuation-enforcer.v1',
18
+ generated_at: nowIso(),
19
+ ok: blockers.length === 0,
20
+ mission_id: input.missionId,
21
+ nodes: nodes.length,
22
+ completed,
23
+ incomplete,
24
+ max_continuation_turns: input.maxContinuationTurns || 3,
25
+ should_continue: shouldContinue,
26
+ resume_instruction: shouldContinue ? `sks loop resume ${input.missionId}` : null,
27
+ blockers
28
+ };
29
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'missions', input.missionId, 'loop-continuation-enforcer.json'), report).catch(() => undefined);
30
+ return report;
31
+ }
32
+ //# sourceMappingURL=loop-continuation-enforcer.js.map
@@ -5,6 +5,7 @@ import { selectLoopGates } from './loop-gate-selector.js';
5
5
  import { inferLoopOwnerScope } from './loop-owner-inference.js';
6
6
  import { classifyLoopRisk } from './loop-risk-classifier.js';
7
7
  import { defaultLoopBudget, validateLoopPlan } from './loop-schema.js';
8
+ import { readInitDeepMemory } from '../codex-app/codex-init-deep.js';
8
9
  export async function planLoopsFromRequest(input) {
9
10
  const parallelism = input.parallelism || 'balanced';
10
11
  const maxLoops = Math.max(1, Math.min(32, input.maxLoops || 8));
@@ -83,6 +84,14 @@ export async function planLoopsFromRequest(input) {
83
84
  },
84
85
  blockers: []
85
86
  };
87
+ const projectMemory = await readInitDeepMemory(input.root).catch(() => null);
88
+ if (projectMemory) {
89
+ plan.project_memory = {
90
+ source: projectMemory.path,
91
+ injected: true,
92
+ summary: projectMemory.text.split(/\r?\n/).filter((line) => /^##\s+/.test(line)).slice(0, 8)
93
+ };
94
+ }
86
95
  const validation = validateLoopPlan(plan);
87
96
  plan.blockers = validation.blockers;
88
97
  await writeJsonAtomic(loopPlanPath(input.root, input.missionId), plan);
@@ -6,6 +6,7 @@ import { loopNodeRoot } from './loop-artifacts.js';
6
6
  import { computeLoopConcurrencyBudget, loopWorkerBudgetFor } from './loop-concurrency-budget.js';
7
7
  import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
8
8
  import { buildLoopCheckerPrompt, buildLoopMakerPrompt } from './loop-worker-prompts.js';
9
+ import { resolveCodexAppExecutionProfile } from '../codex-app/codex-app-execution-profile.js';
9
10
  export async function runLoopMakerWorkers(input) {
10
11
  return runLoopWorkers({ ...input, phase: 'maker' });
11
12
  }
@@ -51,6 +52,7 @@ async function runLoopWorkerNative(input) {
51
52
  // The loop worktree is still where workers cwd + write: it is threaded through the per-worker
52
53
  // `worktree` opt below, which launchWorker reads as ctx.opts.worktree -> workerCwd.
53
54
  const insideZellij = Boolean(process.env.SKS_ZELLIJ_SESSION_NAME || process.env.ZELLIJ);
55
+ const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
54
56
  const visiblePaneCap = Math.min(resolveLoopVisiblePaneCap(workerCount), Math.max(1, workerCount));
55
57
  const zellijPlacementOpts = insideZellij ? {
56
58
  workerPlacement: 'zellij-pane',
@@ -77,7 +79,9 @@ async function runLoopWorkerNative(input) {
77
79
  SKS_LOOP_ID: input.node.loop_id,
78
80
  SKS_LOOP_PHASE: input.phase,
79
81
  SKS_LOOP_MAIN_ROOT: input.root,
80
- SKS_LOOP_WORKER_BUDGET: String(workerCount)
82
+ SKS_LOOP_WORKER_BUDGET: String(workerCount),
83
+ SKS_CODEX_APP_EXECUTION_PROFILE: executionProfile?.mode || 'unknown',
84
+ SKS_CODEX_AGENT_ROLE_STRATEGY: executionProfile?.agent_role_strategy || 'message-role'
81
85
  },
82
86
  ...(input.worktree?.path ? {
83
87
  worktree: {
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '3.1.3';
1
+ export const PACKAGE_VERSION = '3.1.4';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,45 @@
1
+ // @ts-nocheck
2
+ import readline from 'node:readline';
3
+ export const HOMEBREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
4
+ export function resolveHomebrewInstallPolicy(input = {}) {
5
+ const args = (input.args || []).map(String);
6
+ const env = input.env || process.env;
7
+ const hasFlag = input.installHomebrew === true || args.includes('--install-homebrew');
8
+ const hasYes = input.autoApprove === true || args.includes('--yes') || args.includes('-y');
9
+ const envAllowed = env.SKS_ALLOW_HOMEBREW_INSTALL === '1';
10
+ const interactiveAccepted = input.interactiveAccepted === true;
11
+ const allowed = envAllowed || interactiveAccepted || (hasFlag && hasYes);
12
+ const source = envAllowed ? 'env'
13
+ : interactiveAccepted ? 'interactive_confirmed'
14
+ : hasFlag && hasYes ? 'yes_install_homebrew'
15
+ : hasFlag ? 'flag'
16
+ : 'not_allowed';
17
+ return {
18
+ schema: 'sks.homebrew-policy.v1',
19
+ allowed,
20
+ source,
21
+ install_command: HOMEBREW_INSTALL_COMMAND,
22
+ blockers: allowed ? [] : ['homebrew_install_requires_explicit_approval']
23
+ };
24
+ }
25
+ export function homebrewMissingDoctorMessage() {
26
+ return [
27
+ 'Zellij repair: Homebrew missing. Run:',
28
+ ' sks doctor --fix --install-homebrew --yes',
29
+ 'or install Homebrew manually, then:',
30
+ ' sks doctor --fix --yes'
31
+ ].join('\n');
32
+ }
33
+ export async function askHomebrewInstallAllowed(question = 'Homebrew is missing. Install Homebrew now? [y/N] ') {
34
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
35
+ return false;
36
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
+ try {
38
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
39
+ return /^(y|yes|예|네|응)$/i.test(String(answer || '').trim());
40
+ }
41
+ finally {
42
+ rl.close();
43
+ }
44
+ }
45
+ //# sourceMappingURL=homebrew-policy.js.map
@@ -30,7 +30,10 @@ export function resolveZellijStackedPaneCapability(input = {}) {
30
30
  };
31
31
  }
32
32
  export async function checkZellijStackedPaneCapability(opts = {}) {
33
- const versionRun = await runZellij(['--version'], { optional: true, timeoutMs: 5000 });
33
+ const runOpts = { optional: true, timeoutMs: 5000 };
34
+ if (opts.env !== undefined)
35
+ runOpts.env = opts.env;
36
+ const versionRun = await runZellij(['--version'], runOpts);
34
37
  const versionText = `${versionRun.stdout_tail}\n${versionRun.stderr_tail}`.trim();
35
38
  const report = resolveZellijStackedPaneCapability({
36
39
  ok: versionRun.ok,
@@ -45,8 +48,34 @@ export async function checkZellijStackedPaneCapability(opts = {}) {
45
48
  return report;
46
49
  }
47
50
  export async function checkZellijCapability(opts = {}) {
48
- const requireZellij = opts.require === true || process.env.SKS_REQUIRE_ZELLIJ === '1';
49
- const versionRun = await runZellij(['--version'], { optional: !requireZellij, timeoutMs: 5000 });
51
+ const env = opts.env || process.env;
52
+ const requireZellij = opts.require === true || env.SKS_REQUIRE_ZELLIJ === '1';
53
+ if (env.SKS_ZELLIJ_CAPABILITY_FAKE_STATUS) {
54
+ const fakeStatus = String(env.SKS_ZELLIJ_CAPABILITY_FAKE_STATUS);
55
+ const status = fakeStatus === 'ok' || fakeStatus === 'missing' || fakeStatus === 'too_old' || fakeStatus === 'blocked' ? fakeStatus : 'blocked';
56
+ const report = {
57
+ schema: ZELLIJ_CAPABILITY_SCHEMA,
58
+ generated_at: nowIso(),
59
+ ok: status === 'ok',
60
+ status,
61
+ integration_optional: !requireZellij,
62
+ require_zellij: requireZellij,
63
+ min_version: ZELLIJ_MIN_VERSION,
64
+ version: status === 'missing' ? null : String(env.SKS_ZELLIJ_CAPABILITY_FAKE_VERSION || '0.40.0'),
65
+ bin: 'zellij',
66
+ command: ['zellij', '--version'],
67
+ docs_evidence: [],
68
+ blockers: requireZellij && status !== 'ok' ? [`zellij_${status}_required`] : [],
69
+ warnings: !requireZellij && status !== 'ok' ? [`zellij_${status}_optional_integration`] : [],
70
+ operator_actions: status === 'ok' ? [] : ['Install Zellij. On macOS: `brew install zellij`.']
71
+ };
72
+ if (opts.writeReport !== false) {
73
+ const root = opts.root || process.cwd();
74
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-capability.json'), report);
75
+ }
76
+ return report;
77
+ }
78
+ const versionRun = await runZellij(['--version'], { optional: !requireZellij, timeoutMs: 5000, env });
50
79
  const versionText = `${versionRun.stdout_tail}\n${versionRun.stderr_tail}`.trim();
51
80
  const version = parseZellijVersionText(versionText);
52
81
  const missing = !versionRun.ok && versionRun.blockers.includes('zellij_missing');