sneakoscope 3.1.3 → 3.1.5

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 (51) 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/cli/install-helpers.js +56 -4
  8. package/dist/commands/codex-app.js +45 -1
  9. package/dist/commands/codex-lb.js +12 -9
  10. package/dist/commands/doctor.js +44 -1
  11. package/dist/core/codex-app/codex-agent-role-sync.js +119 -0
  12. package/dist/core/codex-app/codex-agent-type-probe.js +202 -0
  13. package/dist/core/codex-app/codex-app-execution-profile.js +39 -0
  14. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -4
  15. package/dist/core/codex-app/codex-app-harness-matrix.js +127 -0
  16. package/dist/core/codex-app/codex-app-types.js +21 -0
  17. package/dist/core/codex-app/codex-hook-approval-probe.js +188 -0
  18. package/dist/core/codex-app/codex-hook-lifecycle.js +61 -0
  19. package/dist/core/codex-app/codex-init-deep.js +180 -0
  20. package/dist/core/codex-app/codex-skill-sync.js +164 -0
  21. package/dist/core/codex-app/lazycodex-analysis.js +72 -0
  22. package/dist/core/codex-app/lazycodex-interop-policy.js +60 -0
  23. package/dist/core/codex-app/lazycodex-live-analyzer.js +98 -0
  24. package/dist/core/commands/loop-command.js +11 -0
  25. package/dist/core/commands/mad-sks-command.js +113 -17
  26. package/dist/core/commands/qa-loop-command.js +3 -2
  27. package/dist/core/commands/research-command.js +2 -2
  28. package/dist/core/doctor/doctor-readiness-matrix.js +7 -0
  29. package/dist/core/doctor/doctor-zellij-repair.js +40 -0
  30. package/dist/core/feature-fixtures.js +1 -0
  31. package/dist/core/feature-registry.js +4 -1
  32. package/dist/core/fsx.js +1 -1
  33. package/dist/core/hooks-runtime.js +13 -0
  34. package/dist/core/init.js +4 -1
  35. package/dist/core/loops/loop-continuation-enforcer.js +40 -0
  36. package/dist/core/loops/loop-planner.js +29 -3
  37. package/dist/core/loops/loop-worker-runtime.js +27 -7
  38. package/dist/core/naruto/naruto-loop-worker-router.js +11 -2
  39. package/dist/core/qa-loop.js +39 -4
  40. package/dist/core/research/research-cycle-runner.js +1 -0
  41. package/dist/core/research/research-stage-runner.js +9 -2
  42. package/dist/core/research.js +35 -1
  43. package/dist/core/version.js +1 -1
  44. package/dist/core/zellij/homebrew-policy.js +44 -0
  45. package/dist/core/zellij/zellij-capability.js +32 -3
  46. package/dist/core/zellij/zellij-self-heal-types.js +45 -0
  47. package/dist/core/zellij/zellij-self-heal.js +414 -0
  48. package/dist/core/zellij/zellij-update.js +39 -6
  49. package/dist/scripts/sks-3-1-4-directive-check-lib.js +241 -0
  50. package/dist/scripts/sks-3-1-5-directive-check-lib.js +347 -0
  51. package/package.json +52 -2
@@ -22,29 +22,98 @@ 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)
28
29
  return madSksSubcommand(subcommand, args.filter((arg) => String(arg) !== subcommand));
29
30
  const cleanArgs = stripMadLaunchOnlyArgs(args);
30
- if (args.includes('--json')) {
31
+ const rawArgs = (args || []).map((arg) => String(arg));
32
+ const dryRun = rawArgs.includes('--dry-run');
33
+ if (args.includes('--json') && !dryRun) {
31
34
  const profile = buildMadHighLaunchProfileNoWrite();
32
35
  return console.log(JSON.stringify(profile, null, 2));
33
36
  }
34
37
  const update = { status: 'notice_only', non_blocking: true };
38
+ const headlessZellij = rawArgs.includes('--headless') || process.env.SKS_MAD_ALLOW_HEADLESS === '1';
39
+ const skipZellijRepair = rawArgs.includes('--skip-zellij-repair') || rawArgs.includes('--no-auto-install-zellij');
40
+ const launchRoot = process.cwd();
41
+ if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
42
+ await initProject(launchRoot, {});
43
+ if (dryRun) {
44
+ const zellijPlan = skipZellijRepair
45
+ ? { schema: 'sks.zellij-self-heal.v1', ok: true, status: 'skipped', dry_run: true, planned_mutations: [], command: null, blockers: [], warnings: ['zellij_repair_skipped'] }
46
+ : await repairZellijForSks({
47
+ root: launchRoot,
48
+ requestedBy: 'sks --mad',
49
+ fixRequested: true,
50
+ autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
51
+ interactive: false,
52
+ installHomebrew: rawArgs.includes('--install-homebrew'),
53
+ allowHeadlessFallback: headlessZellij,
54
+ dryRun: true
55
+ });
56
+ const report = {
57
+ schema: 'sks.mad-sks-zellij-dry-run.v1',
58
+ ok: zellijPlan.ok === true,
59
+ status: zellijPlan.ok === true ? 'dry_run' : 'repair_required',
60
+ generated_at: nowIso(),
61
+ launch_skipped: true,
62
+ zellij_repair: zellijPlan
63
+ };
64
+ await writeJsonAtomic(path.join(launchRoot, '.sneakoscope', 'reports', 'mad-sks-zellij-dry-run.json'), report);
65
+ if (rawArgs.includes('--json'))
66
+ console.log(JSON.stringify(report, null, 2));
67
+ else {
68
+ console.log(`SKS MAD dry-run: launch_skipped=true status=${report.status}`);
69
+ const planned = Array.isArray(zellijPlan.planned_mutations) ? zellijPlan.planned_mutations : [];
70
+ for (const row of planned)
71
+ console.log(`- plan: ${row.command}`);
72
+ if (zellijPlan.command && planned.length === 0)
73
+ console.log(`- run: ${zellijPlan.command}`);
74
+ }
75
+ return report;
76
+ }
35
77
  const codexUpdate = deps.maybePromptCodexUpdateForLaunch ? await deps.maybePromptCodexUpdateForLaunch(args, { label: 'MAD launch' }) : { status: 'skipped' };
36
78
  if (codexUpdate.status === 'failed' || codexUpdate.status === 'updated_not_reflected') {
37
79
  console.error(`Codex CLI update failed: ${codexUpdate.error || 'updated version was not visible on PATH'}`);
38
80
  process.exitCode = 1;
39
81
  return;
40
82
  }
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: [] };
83
+ const zellijUpdate = skipZellijRepair
84
+ ? { status: 'skipped', command: 'sks doctor --fix --yes' }
85
+ : deps.maybePromptZellijUpdateForLaunch
86
+ ? await deps.maybePromptZellijUpdateForLaunch(args, {
87
+ label: 'MAD launch',
88
+ root: launchRoot,
89
+ selfHealOnMissing: true,
90
+ autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
91
+ installHomebrew: rawArgs.includes('--install-homebrew'),
92
+ allowHeadlessFallback: headlessZellij
93
+ }).catch(() => ({ status: 'error', command: 'sks doctor --fix --yes' }))
94
+ : await repairZellijForSks({
95
+ root: launchRoot,
96
+ requestedBy: 'sks --mad',
97
+ fixRequested: true,
98
+ autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
99
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.SKS_NO_QUESTION !== '1'),
100
+ installHomebrew: rawArgs.includes('--install-homebrew'),
101
+ allowHeadlessFallback: headlessZellij
102
+ });
103
+ const zellijRepairBlocked = !headlessZellij && (zellijUpdate.status === 'manual_required'
104
+ || zellijUpdate.strategy === 'manual-required'
105
+ || zellijUpdate.ok === false);
106
+ if (zellijRepairBlocked) {
107
+ console.error('SKS MAD launch blocked by Zellij repair_required.');
108
+ console.error(`Run: ${zellijUpdate.command || 'sks doctor --fix --yes'}`);
109
+ process.exitCode = 1;
110
+ return { ok: false, status: 'repair_required', command: zellijUpdate.command || 'sks doctor --fix --yes', zellij_repair: zellijUpdate };
111
+ }
112
+ const depStatus = skipZellijRepair && deps.ensureMadLaunchDependencies
113
+ ? await deps.ensureMadLaunchDependencies(args)
114
+ : { ready: true, actions: [] };
46
115
  if (!depStatus.ready) {
47
- console.error('SKS MAD launch blocked by missing dependencies.');
116
+ console.error('SKS MAD launch blocked by required Zellij dependency.');
48
117
  for (const action of depStatus.actions)
49
118
  deps.printDepsInstallAction?.(action);
50
119
  process.exitCode = 1;
@@ -56,9 +125,6 @@ export async function madHighCommand(args = [], deps = {}) {
56
125
  return;
57
126
  }
58
127
  const profile = buildMadHighLaunchProfileNoWrite();
59
- const launchRoot = process.cwd();
60
- if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
61
- await initProject(launchRoot, {});
62
128
  const uiSnapshotId = Date.now().toString(36);
63
129
  const beforeUi = await writeCodexAppUiSnapshot(launchRoot, `mad-before-${uiSnapshotId}`).catch(() => null);
64
130
  // launchFast skips the redundant live-`codex exec` config probe (up to ~20s, run
@@ -66,7 +132,6 @@ export async function madHighCommand(args = [], deps = {}) {
66
132
  // later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
67
133
  // readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
68
134
  // old behavior.
69
- const rawArgs = (args || []).map((arg) => String(arg));
70
135
  const madDbRequested = rawArgs.includes('--mad-db');
71
136
  const madDbAck = readOption(rawArgs, '--ack', '');
72
137
  if (madDbRequested && madDbAck !== MAD_DB_ACK) {
@@ -138,7 +203,9 @@ export async function madHighCommand(args = [], deps = {}) {
138
203
  };
139
204
  const launchOpts = codexLbImmediateLaunchOpts(cleanArgs, launchLb, { codexArgs: profile.launch_args, conciseBlockers: true, madSksEnv, launchEnv: madSksEnv });
140
205
  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' });
206
+ const launch = headlessZellij
207
+ ? await writeMadHeadlessZellijFallback(madLaunch, workspace)
208
+ : 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
209
  const afterLaunchUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-launch-${uiSnapshotId}`).catch(() => null) : null;
143
210
  const launchUiDiff = beforeUi && afterLaunchUi ? diffCodexAppUiSnapshots(beforeUi, afterLaunchUi) : null;
144
211
  if (launchUiDiff) {
@@ -157,7 +224,8 @@ export async function madHighCommand(args = [], deps = {}) {
157
224
  schema: 'sks.zellij-initial-ui.v1',
158
225
  ok: true,
159
226
  mission_id: madLaunch.mission_id,
160
- session_name: launch.session_name,
227
+ session_name: launch.session_name || null,
228
+ live_panes: !headlessZellij,
161
229
  initial_panes: 'main-only',
162
230
  dashboard_created: false,
163
231
  worker_panes_created: 0,
@@ -166,16 +234,16 @@ export async function madHighCommand(args = [], deps = {}) {
166
234
  const madNativeSwarm = await startMadNativeSwarm(madLaunch.root, madLaunch, args, profile, {
167
235
  env: {
168
236
  ...madSksEnv,
169
- SKS_ZELLIJ_SESSION_NAME: launch.session_name
237
+ ...(launch.session_name ? { SKS_ZELLIJ_SESSION_NAME: launch.session_name } : {})
170
238
  },
171
- zellijSessionName: launch.session_name,
172
- workerPlacement: shouldAutoAttachZellij(args) ? 'zellij-pane' : 'process',
239
+ zellijSessionName: launch.session_name || null,
240
+ workerPlacement: headlessZellij ? 'process' : shouldAutoAttachZellij(args) ? 'zellij-pane' : 'process',
173
241
  zellijVisiblePaneCap: Number(process.env.SKS_ZELLIJ_VISIBLE_PANE_CAP || 8)
174
242
  });
175
243
  // The launcher only creates a detached background session. In an interactive
176
244
  // terminal, immediately attach so the session actually opens for the user
177
245
  // instead of leaving them to copy/paste the attach command by hand.
178
- if (shouldAutoAttachZellij(args)) {
246
+ if (!headlessZellij && shouldAutoAttachZellij(args)) {
179
247
  console.log(`Opening Zellij session: ${launch.session_name} (detach with Ctrl+q, re-attach later with: ${launch.attach_command_with_env})`);
180
248
  const attached = attachZellijSessionInteractive(launch.session_name, { cwd: process.cwd(), configPath: launch.clipboard_config_path });
181
249
  if (!attached.ok) {
@@ -187,6 +255,8 @@ export async function madHighCommand(args = [], deps = {}) {
187
255
  }
188
256
  if (launch.attach_command_with_env)
189
257
  console.log(`Attach with: ${launch.attach_command_with_env}`);
258
+ if (headlessZellij)
259
+ console.log('MAD launch running headless: live_panes=false.');
190
260
  return launch;
191
261
  }
192
262
  export async function startMadNativeSwarm(root, madLaunch, args = [], profile = {}, opts = {}) {
@@ -375,6 +445,27 @@ function formatMadZellijAction(launch) {
375
445
  const report = launch.report_path ? ` | report: ${launch.report_path}` : '';
376
446
  return `${blockers}${detail}${report}`;
377
447
  }
448
+ async function writeMadHeadlessZellijFallback(madLaunch, workspace) {
449
+ const report = {
450
+ schema: 'sks.zellij-session.v1',
451
+ generated_at: nowIso(),
452
+ ok: true,
453
+ kind: 'mad',
454
+ status: 'headless-fallback',
455
+ live_panes: false,
456
+ mission_id: madLaunch.mission_id,
457
+ session_name: null,
458
+ workspace,
459
+ root: madLaunch.root,
460
+ cwd: path.resolve(process.cwd()),
461
+ attach_command_with_env: null,
462
+ blockers: [],
463
+ warnings: ['zellij_headless_fallback_live_panes_false']
464
+ };
465
+ await writeJsonAtomic(path.join(madLaunch.dir, 'zellij-session.json'), report);
466
+ await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_sks.zellij_headless_fallback', live_panes: false });
467
+ return report;
468
+ }
378
469
  async function activateMadZellijPermissionState(cwd = process.cwd(), args = []) {
379
470
  const root = await sksRoot();
380
471
  if (!(await exists(path.join(root, '.sneakoscope'))))
@@ -473,6 +564,9 @@ function madLaunchOnlyFlags() {
473
564
  '--attach',
474
565
  '--no-attach',
475
566
  '--no-auto-install-zellij',
567
+ '--skip-zellij-repair',
568
+ '--install-homebrew',
569
+ '--headless',
476
570
  '--allow-system',
477
571
  '--allow-db-write',
478
572
  '--allow-package-install',
@@ -528,6 +622,8 @@ export function defaultMadSwarmBackend(args = [], opts = {}) {
528
622
  return String(opts.backend);
529
623
  if (list.includes('--json') || list.includes('--no-attach') || opts.nonInteractive === true)
530
624
  return 'codex-sdk';
625
+ if (list.includes('--headless') || process.env.SKS_MAD_ALLOW_HEADLESS === '1')
626
+ return 'codex-sdk';
531
627
  return 'zellij';
532
628
  }
533
629
  function stripMadLaunchOnlyArgs(args = []) {
@@ -146,6 +146,7 @@ async function qaLoopRun(args) {
146
146
  const mock = flag(args, '--mock');
147
147
  const qaGate = await readJson(path.join(dir, 'qa-gate.json'), {});
148
148
  const reportFile = qaGate.qa_report_file;
149
+ const executionProfile = await readJson(path.join(dir, 'qa-loop', 'execution-profile.json'), null);
149
150
  const uiRequired = qaUiRequired(contract.answers || {});
150
151
  const capabilityArtifact = await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), report: null }));
151
152
  const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
@@ -273,7 +274,7 @@ async function qaLoopRun(args) {
273
274
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.run.started', maxCycles, mock });
274
275
  const nativeAgentPlan = await readJson(path.join(dir, 'qa-agent-plan.json'), null);
275
276
  const nativeRoster = requestedAgents === 3 ? nativeAgentPlan : null;
276
- const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: '$QA-LOOP', prompt: mission.prompt || 'QA-LOOP run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: !(applyPatches && writeMode !== 'off'), profile, writeMode: writeMode, applyPatches, dryRunPatches, maxWriteAgents, roster: nativeRoster, routeCommand: 'sks qa-loop run', routeBlackboxKind: 'actual_qa_command' });
277
+ const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: '$QA-LOOP', prompt: mission.prompt || 'QA-LOOP run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: !(applyPatches && writeMode !== 'off'), profile, writeMode: writeMode, applyPatches, dryRunPatches, maxWriteAgents, roster: nativeRoster, routeCommand: 'sks qa-loop run', routeBlackboxKind: 'actual_qa_command', env: { SKS_CODEX_APP_EXECUTION_PROFILE: executionProfile?.mode || 'unknown', SKS_CODEX_AGENT_ROLE_STRATEGY: executionProfile?.agent_role_strategy || 'message-role' } });
277
278
  await writeJsonAtomic(path.join(dir, 'qa-native-agent-run.json'), nativeAgentRun);
278
279
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
279
280
  if (mock) {
@@ -343,7 +344,7 @@ async function qaLoopRun(args) {
343
344
  for (let cycle = 1; cycle <= maxCycles; cycle += 1) {
344
345
  const cycleDir = path.join(dir, 'qa-loop', `cycle-${cycle}`);
345
346
  const outputFile = path.join(cycleDir, 'final.md');
346
- const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile, imagePathContract: imagePathContract?.contract || null, appHandoff });
347
+ const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile, imagePathContract: imagePathContract?.contract || null, appHandoff, executionProfile });
347
348
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.cycle.start', cycle });
348
349
  const result = await runCodexExec({ root, prompt, outputFile, json: true, profile, logDir: cycleDir });
349
350
  await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
@@ -52,7 +52,7 @@ async function researchPrepare(args) {
52
52
  const context7Required = routeNeedsContext7(route, prompt);
53
53
  const reasoning = routeReasoning(route, prompt);
54
54
  const autoresearch = flag(args, '--autoresearch');
55
- const plan = await writeResearchPlan(dir, prompt, { depth: readFlagValue(args, '--depth', 'frontier'), missionId: id, autoresearch });
55
+ const plan = await writeResearchPlan(dir, prompt, { root, depth: readFlagValue(args, '--depth', 'frontier'), missionId: id, autoresearch });
56
56
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: prompt, required: context7Required, ambiguity: { required: false, status: 'direct_research_cli' } });
57
57
  await writeJsonAtomic(path.join(dir, 'route-context.json'), {
58
58
  route: route.id,
@@ -114,7 +114,7 @@ async function researchRun(args) {
114
114
  const { dir, mission } = await loadMission(root, id);
115
115
  const planPath = path.join(dir, 'research-plan.json');
116
116
  if (!(await exists(planPath)))
117
- await writeResearchPlan(dir, mission.prompt || '', { missionId: id, autoresearch: flag(args, '--autoresearch') });
117
+ await writeResearchPlan(dir, mission.prompt || '', { root, missionId: id, autoresearch: flag(args, '--autoresearch') });
118
118
  const plan = await readJson(planPath);
119
119
  const dbScan = await scanDbSafety(root);
120
120
  if (!dbScan.ok) {
@@ -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,40 @@
1
+ import { repairZellijForSks } from '../zellij/zellij-self-heal.js';
2
+ export async function runDoctorZellijRepair(input) {
3
+ const args = (input.args || []).map(String);
4
+ if (input.doctorFix !== true)
5
+ return null;
6
+ return repairZellijForSks({
7
+ root: input.root,
8
+ requestedBy: 'doctor --fix',
9
+ fixRequested: true,
10
+ autoApprove: args.includes('--yes') || args.includes('-y'),
11
+ installHomebrew: args.includes('--install-homebrew') || process.env.SKS_ALLOW_HOMEBREW_INSTALL === '1',
12
+ dryRun: args.includes('--dry-run'),
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.dry_run) {
22
+ const planned = result.planned_mutations.map((row) => row.command).join(' && ') || result.command || 'none';
23
+ return `Zellij repair: dry_run planned ${planned}`;
24
+ }
25
+ if (result.strategy === 'none-current')
26
+ return `Zellij repair: current ${result.after.version || ''}`.trim();
27
+ if (result.ok && (result.strategy === 'brew-install-zellij' || result.strategy === 'brew-install-homebrew-then-zellij')) {
28
+ return `Zellij repair: installed ${result.after.version || 'latest'} via ${result.command || 'brew install zellij'}`;
29
+ }
30
+ if (result.ok && result.strategy === 'brew-upgrade-zellij') {
31
+ return `Zellij repair: upgraded ${result.before.version || 'unknown'} -> ${result.after.version || 'latest'} via ${result.command || 'brew upgrade zellij'}`;
32
+ }
33
+ if (result.strategy === 'manual-required') {
34
+ return `Zellij repair: manual_required\nRun: ${result.command || 'sks doctor --fix --install-homebrew --yes'}`;
35
+ }
36
+ if (result.strategy === 'headless-fallback')
37
+ return 'Zellij repair: headless_fallback live_panes=false';
38
+ return `Zellij repair: failed\nRun: ${result.command || 'sks doctor --fix --yes'}`;
39
+ }
40
+ //# sourceMappingURL=doctor-zellij-repair.js.map
@@ -107,6 +107,7 @@ const FIXTURES = Object.freeze({
107
107
  'route-from-chat-img': fixture('mock', '$From-Chat-IMG visual work order route', ['from-chat-img-work-order.md', 'image-voxel-ledger.json', 'completion-proof.json'], 'pass'),
108
108
  'route-ux-review': fixture('mock', '$UX-Review image UX alias route', ['image-ux-generated-review-ledger.json', 'image-voxel-ledger.json'], 'pass'),
109
109
  'route-db': fixture('execute_and_validate_artifacts', 'sks db check --sql "SELECT 1" --json', ['completion-proof.json', 'db-operation-report.json'], 'pass'),
110
+ 'route-mad-db': fixture('mock', '$MAD-DB one-cycle DB break-glass route contract', ['mad-db-capability.json', 'mad-db-ledger.jsonl', 'completion-proof.json'], 'pass'),
110
111
  'route-wiki': fixture('execute_and_validate_artifacts', 'sks wiki image-ingest test/fixtures/images/one-by-one.png --json', [{ path: 'completion-proof.json', schema: 'sks.completion-proof.v1' }, { path: 'image-voxel-ledger.json', schema: 'sks.image-voxel-ledger.v1' }], 'pass'),
111
112
  'route-gx': fixture('execute_and_validate_artifacts', 'sks gx validate fixture --mock --json', ['completion-proof.json', { path: 'image-voxel-ledger.json', schema: 'sks.image-voxel-ledger.v1' }, 'gx-validation.json'], 'pass'),
112
113
  'route-sks': fixture('mock', '$SKS control-surface route', ['completion-proof.json'], 'pass'),
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
- import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS } from './routes.js';
5
+ import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, ROUTES } from './routes.js';
6
6
  import { FEATURE_QUALITY_LEVELS, fixtureForFeature, fixtureSummary, validateFeatureFixtures } from './feature-fixtures.js';
7
7
  import { runFeatureFixture, writeFeatureFixtureReports } from './feature-fixture-runner.js';
8
8
  import { PACKAGE_VERSION, exists, nowIso, packageRoot, readJson, readText, runProcess, writeJsonAtomic, writeTextAtomic } from './fsx.js';
@@ -46,6 +46,9 @@ export async function buildFeatureRegistry({ root = packageRoot(), generatedAt =
46
46
  }
47
47
  for (const route of DOLLAR_COMMANDS)
48
48
  features.push(routeFeature(route));
49
+ for (const route of ROUTES.filter((entry) => entry.hidden === true)) {
50
+ features.push(routeFeature(route));
51
+ }
49
52
  features.push(nativeAgentIntakeFeature());
50
53
  features.push(agentProofEvidenceFeature());
51
54
  for (const skillName of skillNames) {
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.5';
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,
package/dist/core/init.js CHANGED
@@ -639,7 +639,10 @@ export async function initProject(root, opts = {}) {
639
639
  const baseUrl = globalConfig.match(/(^|\n)\[model_providers\.codex-lb\][\s\S]*?\n\s*base_url\s*=\s*"([^"]+)"/)?.[2] || parseCodexLbEnvBaseUrl(envText);
640
640
  if (!parseCodexLbEnvKey(envText) || !baseUrl)
641
641
  return next;
642
- next = upsertTopLevelTomlString(next, 'model_provider', 'codex-lb');
642
+ const shouldSelectCodexLb = selectedRe.test(next) || selectedRe.test(globalConfig);
643
+ next = shouldSelectCodexLb
644
+ ? upsertTopLevelTomlString(next, 'model_provider', 'codex-lb')
645
+ : removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
643
646
  next = upsertTomlTable(next, 'model_providers.codex-lb', `[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = false`);
644
647
  return `${next.trim()}\n`;
645
648
  }
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { loopPlanPath } from './loop-artifacts.js';
4
+ export async function evaluateLoopContinuation(input) {
5
+ const root = path.resolve(input.root || process.cwd());
6
+ const plan = await readJson(loopPlanPath(root, input.missionId), null);
7
+ const blockers = [];
8
+ if (!plan)
9
+ blockers.push('loop_plan_missing');
10
+ const nodes = loopNodes(plan);
11
+ const proofs = await Promise.all(nodes.map((node) => readJson(path.join(root, '.sneakoscope', 'missions', input.missionId, 'loops', node.loop_id, 'loop-proof.json'), null)));
12
+ const completed = proofs.filter((proof) => isRecord(proof) && proof.status === 'completed').length;
13
+ const incomplete = Math.max(0, nodes.length - completed);
14
+ const shouldContinue = Boolean(plan && incomplete > 0 && blockers.length === 0);
15
+ const report = {
16
+ schema: 'sks.loop-continuation-enforcer.v1',
17
+ generated_at: nowIso(),
18
+ ok: blockers.length === 0,
19
+ mission_id: input.missionId,
20
+ nodes: nodes.length,
21
+ completed,
22
+ incomplete,
23
+ max_continuation_turns: input.maxContinuationTurns || 3,
24
+ should_continue: shouldContinue,
25
+ resume_instruction: shouldContinue ? `sks loop resume ${input.missionId}` : null,
26
+ blockers
27
+ };
28
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'missions', input.missionId, 'loop-continuation-enforcer.json'), report).catch(() => undefined);
29
+ return report;
30
+ }
31
+ function loopNodes(value) {
32
+ if (!isRecord(value) || !isRecord(value.graph) || !Array.isArray(value.graph.nodes))
33
+ return [];
34
+ return value.graph.nodes
35
+ .filter((node) => isRecord(node) && typeof node.loop_id === 'string');
36
+ }
37
+ function isRecord(value) {
38
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
39
+ }
40
+ //# 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, readInitDeepMemoryHints } 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));
@@ -52,6 +53,11 @@ export async function planLoopsFromRequest(input) {
52
53
  })
53
54
  };
54
55
  const nodes = [...actionNodes, integrationNode];
56
+ const memoryHints = await readInitDeepMemoryHints(input.root, scopePathsForNodes(nodes)).catch(() => []);
57
+ const nodesWithMemory = nodes.map((node) => {
58
+ const hints = memoryHints.filter((hint) => hintAppliesToNode(hint, node)).slice(0, 5);
59
+ return hints.length ? { ...node, memory_hints: hints } : node;
60
+ });
55
61
  const plan = {
56
62
  schema: 'sks.loop-plan.v1',
57
63
  mission_id: input.missionId,
@@ -63,12 +69,12 @@ export async function planLoopsFromRequest(input) {
63
69
  confidence: actionNodes.length ? 'high' : 'medium'
64
70
  },
65
71
  graph: {
66
- nodes,
72
+ nodes: nodesWithMemory,
67
73
  edges: actionNodes.map((node) => ({ from: node.loop_id, to: integrationNode.loop_id, reason: 'integration_after_loop_proof' }))
68
74
  },
69
75
  global_budget: defaultLoopBudget({
70
- max_iterations: Math.max(...nodes.map((node) => node.budget.max_iterations)),
71
- max_subagents: nodes.reduce((sum, node) => sum + node.budget.max_subagents, 0)
76
+ max_iterations: Math.max(...nodesWithMemory.map((node) => node.budget.max_iterations)),
77
+ max_subagents: nodesWithMemory.reduce((sum, node) => sum + node.budget.max_subagents, 0)
72
78
  }),
73
79
  safety: {
74
80
  no_unrequested_fallback_code: true,
@@ -83,6 +89,14 @@ export async function planLoopsFromRequest(input) {
83
89
  },
84
90
  blockers: []
85
91
  };
92
+ const projectMemory = await readInitDeepMemory(input.root).catch(() => null);
93
+ if (projectMemory) {
94
+ plan.project_memory = {
95
+ source: projectMemory.path,
96
+ injected: true,
97
+ summary: projectMemory.text.split(/\r?\n/).filter((line) => /^##\s+/.test(line)).slice(0, 8)
98
+ };
99
+ }
86
100
  const validation = validateLoopPlan(plan);
87
101
  plan.blockers = validation.blockers;
88
102
  await writeJsonAtomic(loopPlanPath(input.root, input.missionId), plan);
@@ -167,4 +181,16 @@ function dynamicCheckerWorkerCount(input) {
167
181
  function titleFromDomain(domainId) {
168
182
  return domainId === 'loop-general-coding' ? 'General coding loop' : `${domainId} loop`;
169
183
  }
184
+ function scopePathsForNodes(nodes) {
185
+ return nodes.flatMap((node) => [
186
+ ...node.owner_scope.files,
187
+ ...node.owner_scope.directories
188
+ ]).filter(Boolean);
189
+ }
190
+ function hintAppliesToNode(hint, node) {
191
+ if (hint.scope === '.')
192
+ return true;
193
+ const scopes = [...node.owner_scope.files, ...node.owner_scope.directories].map((value) => value.replace(/^\.?\//, ''));
194
+ return scopes.some((scope) => scope === hint.scope || scope.startsWith(`${hint.scope}/`) || hint.scope.startsWith(`${scope}/`));
195
+ }
170
196
  //# sourceMappingURL=loop-planner.js.map
@@ -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
  }
@@ -42,7 +43,8 @@ async function runLoopWorkerNative(input) {
42
43
  ? buildLoopMakerPrompt({ plan: input.plan, node: input.node, worktreePath: input.worktree?.path || null })
43
44
  : buildLoopCheckerPrompt({ plan: input.plan, node: input.node, makerArtifacts: input.makerArtifacts || [] });
44
45
  const workerCount = effectiveLoopWorkerCount(input);
45
- const workGraph = buildLoopNarutoWorkGraph(input, workerCount);
46
+ const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
47
+ const workGraph = buildLoopNarutoWorkGraph(input, workerCount, executionProfile);
46
48
  // Root-cause-1 fix: keep the ORCHESTRATOR root on the MAIN repo (input.root), not the
47
49
  // loop worktree. All zellij/right-column/slot-telemetry state derives from the orchestrator
48
50
  // root, so anchoring it on input.root makes the SLOTS snapshot land under
@@ -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: {
@@ -95,9 +99,9 @@ async function runLoopWorkerNative(input) {
95
99
  fallback_reason: null
96
100
  } : null
97
101
  });
98
- return normalizeNativeResult(input, orchestrator);
102
+ return normalizeNativeResult(input, orchestrator, executionProfile);
99
103
  }
100
- async function normalizeNativeResult(input, result) {
104
+ async function normalizeNativeResult(input, result, executionProfile) {
101
105
  const artifacts = collectArtifactPaths(result);
102
106
  const changedFiles = stringArray(result?.changed_files || result?.proof?.changed_files || result?.results?.flatMap?.((row) => row?.changed_files || []));
103
107
  const blockers = [
@@ -120,7 +124,15 @@ async function normalizeNativeResult(input, result) {
120
124
  blockers: [...new Set(blockers)],
121
125
  runtime_proof_path: proofPath,
122
126
  worker_ids: stringArray(result?.results?.map?.((row) => row?.agent_id || row?.id)),
123
- session_ids: stringArray(result?.results?.map?.((row) => row?.session_id))
127
+ session_ids: stringArray(result?.results?.map?.((row) => row?.session_id)),
128
+ ...(executionProfile ? {
129
+ codex_app_execution_profile: {
130
+ mode: executionProfile.mode,
131
+ agent_role_strategy: executionProfile.agent_role_strategy,
132
+ artifact_path: executionProfile.artifact_path,
133
+ agent_type_probe_artifact_path: executionProfile.agent_type_probe_artifact_path
134
+ }
135
+ } : {})
124
136
  };
125
137
  await writeJsonAtomic(proofPath, { ...normalized, native_result_summary: summarizeNativeResult(result), generated_at: nowIso() });
126
138
  return normalized;
@@ -185,7 +197,13 @@ async function runLoopWorkerFixture(input) {
185
197
  fixture_allowed_reason: fixturePolicy.allowed ? fixturePolicy.reason : null
186
198
  };
187
199
  }
188
- function buildLoopNarutoWorkGraph(input, workerCount) {
200
+ function buildLoopNarutoWorkGraph(input, workerCount, executionProfile) {
201
+ const profilePayload = executionProfile ? {
202
+ mode: executionProfile.mode,
203
+ agent_role_strategy: executionProfile.agent_role_strategy,
204
+ artifact_path: executionProfile.artifact_path,
205
+ agent_type_probe_artifact_path: executionProfile.agent_type_probe_artifact_path
206
+ } : undefined;
189
207
  const workItems = Array.from({ length: Math.max(1, workerCount) }, (_, index) => {
190
208
  const id = `${input.node.loop_id}-${input.phase}-${index + 1}`;
191
209
  const writeAllowed = input.phase === 'maker';
@@ -214,7 +232,8 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
214
232
  mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
215
233
  required: input.node.worktree.required,
216
234
  allocation_required: false
217
- }
235
+ },
236
+ ...(profilePayload ? { codex_app_execution_profile: profilePayload } : {})
218
237
  };
219
238
  });
220
239
  return {
@@ -235,6 +254,7 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
235
254
  worktree_root: null,
236
255
  fallback_reason: input.worktree?.path ? 'loop_worktree_already_allocated' : null
237
256
  },
257
+ ...(profilePayload ? { codex_app_execution_profile: profilePayload } : {}),
238
258
  blockers: [],
239
259
  ok: true
240
260
  };