sneakoscope 2.0.1 → 2.0.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 (74) hide show
  1. package/README.md +26 -5
  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/build-manifest.json +28 -8
  8. package/dist/cli/command-registry.js +2 -0
  9. package/dist/commands/doctor.js +29 -3
  10. package/dist/core/agents/agent-command-surface.js +13 -3
  11. package/dist/core/agents/agent-orchestrator.js +92 -4
  12. package/dist/core/agents/agent-output-validator.js +2 -1
  13. package/dist/core/agents/agent-patch-proof.js +5 -0
  14. package/dist/core/agents/agent-patch-schema.js +2 -1
  15. package/dist/core/agents/agent-proof-evidence.js +26 -0
  16. package/dist/core/agents/agent-roster.js +1 -1
  17. package/dist/core/agents/agent-runner-ollama.js +411 -0
  18. package/dist/core/agents/agent-schema.js +1 -1
  19. package/dist/core/agents/intelligent-work-graph.js +45 -3
  20. package/dist/core/agents/native-cli-session-swarm.js +8 -1
  21. package/dist/core/agents/native-cli-worker.js +1 -1
  22. package/dist/core/agents/native-worker-backend-router.js +44 -2
  23. package/dist/core/agents/ollama-worker-config.js +118 -0
  24. package/dist/core/auto-review.js +39 -6
  25. package/dist/core/codex-app/codex-app-fast-ui-repair.js +42 -3
  26. package/dist/core/codex-control/codex-fake-sdk-adapter.js +20 -0
  27. package/dist/core/codex-control/codex-output-schemas.js +5 -1
  28. package/dist/core/codex-control/gpt-final-arbiter.js +160 -0
  29. package/dist/core/codex-control/gpt-final-context-compressor.js +17 -0
  30. package/dist/core/codex-control/gpt-final-proof-pack.js +120 -0
  31. package/dist/core/codex-control/gpt-final-review-schema.js +71 -0
  32. package/dist/core/commands/basic-cli.js +36 -1
  33. package/dist/core/commands/local-model-command.js +120 -0
  34. package/dist/core/commands/mad-sks-command.js +58 -9
  35. package/dist/core/commands/naruto-command.js +77 -5
  36. package/dist/core/commands/run-command.js +33 -1
  37. package/dist/core/commands/team-command.js +31 -2
  38. package/dist/core/doctor/doctor-readiness-matrix.js +19 -0
  39. package/dist/core/feature-fixtures.js +5 -0
  40. package/dist/core/fsx.js +1 -1
  41. package/dist/core/git-simple.js +143 -4
  42. package/dist/core/hooks-runtime.js +1 -1
  43. package/dist/core/init.js +2 -0
  44. package/dist/core/local-llm/local-collaboration-policy.js +93 -0
  45. package/dist/core/local-llm/local-llm-config.js +15 -0
  46. package/dist/core/pipeline/final-gpt-patch-stage.js +31 -0
  47. package/dist/core/pipeline/final-gpt-review-stage.js +5 -0
  48. package/dist/core/provider/provider-context.js +72 -9
  49. package/dist/core/retention.js +11 -0
  50. package/dist/core/routes.js +21 -1
  51. package/dist/core/safety/mutation-guard.js +2 -0
  52. package/dist/core/team-live.js +7 -1
  53. package/dist/core/update-check.js +215 -25
  54. package/dist/core/verification/verification-worker-pool.js +12 -0
  55. package/dist/core/version.js +1 -1
  56. package/dist/core/zellij/zellij-worker-pane-manager.js +19 -2
  57. package/dist/scripts/agent-ast-aware-work-graph-check.js +1 -1
  58. package/dist/scripts/codex-sdk-team-naruto-agent-pipeline-check.js +2 -1
  59. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -2
  60. package/dist/scripts/gpt-final-arbiter-check.js +63 -0
  61. package/dist/scripts/gpt-final-arbiter-performance-check.js +36 -0
  62. package/dist/scripts/local-collab-gpt-final-availability-check.js +58 -0
  63. package/dist/scripts/local-collab-no-local-only-final-check.js +27 -0
  64. package/dist/scripts/local-collab-policy-check.js +17 -0
  65. package/dist/scripts/mad-sks-app-ui-no-mutation-check.js +92 -0
  66. package/dist/scripts/mad-sks-zellij-default-pane-worker-check.js +37 -0
  67. package/dist/scripts/mad-sks-zellij-launch-check.js +2 -1
  68. package/dist/scripts/provider-context-config-toml-check.js +63 -0
  69. package/dist/scripts/release-gate-existence-audit.js +4 -0
  70. package/dist/scripts/runtime-no-mjs-scripts-check.js +3 -2
  71. package/dist/scripts/zellij-worker-pane-manager-check.js +3 -0
  72. package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +39 -0
  73. package/package.json +13 -4
  74. package/schemas/local-llm/local-collaboration-policy.schema.json +57 -0
@@ -0,0 +1,120 @@
1
+ import { nowIso, sha256 } from '../fsx.js';
2
+ export const GPT_FINAL_PROOF_PACK_SCHEMA = 'sks.gpt-final-proof-pack.v1';
3
+ export function buildGptFinalProofPack(input, opts = {}) {
4
+ const maxCriticalLogChars = Math.max(1000, Number(opts.maxCriticalLogChars || 6000));
5
+ const localOutputs = Array.isArray(input.local_outputs) ? input.local_outputs : [];
6
+ const candidatePatchEnvelopes = Array.isArray(input.candidate_patch_envelopes) ? input.candidate_patch_envelopes : [];
7
+ const verificationResults = Array.isArray(input.verification_results) ? input.verification_results : [];
8
+ const pack = {
9
+ schema: GPT_FINAL_PROOF_PACK_SCHEMA,
10
+ generated_at: nowIso(),
11
+ route: String(input.route || ''),
12
+ mission_id: String(input.mission_id || ''),
13
+ local_mode: String(input.local_mode || ''),
14
+ worker_count: localOutputs.length,
15
+ changed_files: sortedUnique([
16
+ ...candidatePatchEnvelopes.flatMap((envelope) => envelopeChangedFiles(envelope)),
17
+ ...localOutputs.flatMap((output) => Array.isArray(output?.changed_files) ? output.changed_files.map(String) : [])
18
+ ]),
19
+ local_output_summaries: localOutputs.map((output, index) => ({
20
+ worker_id: String(output?.worker_id || output?.agent_id || output?.id || `worker-${index + 1}`),
21
+ backend: String(output?.backend || 'local-llm'),
22
+ status: String(output?.status || 'unknown'),
23
+ summary: trim(String(output?.summary || ''), 600),
24
+ proof: trim(String(output?.proof || output?.verification?.status || ''), 600),
25
+ blocker_count: Array.isArray(output?.blockers) ? output.blockers.length : 0,
26
+ patch_envelope_count: Array.isArray(output?.patch_envelopes) ? output.patch_envelopes.length : output?.patch_envelope ? 1 : 0
27
+ })),
28
+ candidate_diff_sha256: sha256(String(input.candidate_diff || '')),
29
+ candidate_diff_tail: tail(String(input.candidate_diff || ''), maxCriticalLogChars),
30
+ candidate_patch_envelopes: candidatePatchEnvelopes.map((envelope, index) => summarizeEnvelope(envelope, index)),
31
+ verification_failures: verificationResults.filter((result) => result?.ok === false || result?.status === 'failed').map((result) => ({
32
+ id: String(result?.id || result?.patch_entry_id || result?.name || 'verification'),
33
+ status: String(result?.status || (result?.ok === false ? 'failed' : 'unknown')),
34
+ blockers: stringArray(result?.blockers || result?.violations)
35
+ })),
36
+ side_effect_ledger_summary: summarizeObject(input.side_effect_report),
37
+ mutation_ledger_summary: summarizeObject(input.mutation_ledger),
38
+ rollback_plan_summary: summarizeObject(input.rollback_plan),
39
+ conflict_map: buildConflictMap(candidatePatchEnvelopes),
40
+ critical_logs_tail: tail(JSON.stringify({
41
+ local_outputs: localOutputs.map((output) => ({
42
+ id: output?.worker_id || output?.agent_id || output?.id,
43
+ blockers: output?.blockers || [],
44
+ unverified: output?.unverified || []
45
+ })),
46
+ verification_results: verificationResults
47
+ }), maxCriticalLogChars),
48
+ token_budget_estimate: 0
49
+ };
50
+ return {
51
+ ...pack,
52
+ token_budget_estimate: estimateTokens(pack)
53
+ };
54
+ }
55
+ export function buildGptFinalLatencyBudgetReport(input) {
56
+ const workerCount = Math.max(0, Number(input.workerCount || 0));
57
+ const tokenBudgetEstimate = Math.max(0, Number(input.tokenBudgetEstimate || 0));
58
+ const capAdjustment = tokenBudgetEstimate > 8000 || workerCount > 20 ? 'reduce_local_parallelism_or_pack_size' : 'within_budget';
59
+ return {
60
+ schema: 'sks.gpt-final-latency-budget.v1',
61
+ generated_at: nowIso(),
62
+ worker_count: workerCount,
63
+ token_budget_estimate: tokenBudgetEstimate,
64
+ latency_ms: input.latencyMs ?? null,
65
+ cap_adjustment: capAdjustment,
66
+ ok: capAdjustment === 'within_budget'
67
+ };
68
+ }
69
+ function summarizeEnvelope(envelope, index) {
70
+ return {
71
+ id: String(envelope?.id || envelope?.patch_id || `patch-${index + 1}`),
72
+ agent_id: String(envelope?.agent_id || 'unknown'),
73
+ source: String(envelope?.source || 'local-llm'),
74
+ operations: Array.isArray(envelope?.operations) ? envelope.operations.length : 0,
75
+ write_paths: envelopeChangedFiles(envelope),
76
+ lease_id: envelope?.lease_id || envelope?.lease_proof?.lease_id || null,
77
+ rollback_ready: Boolean(envelope?.rollback_hint || envelope?.lease_proof?.rollback_node_id)
78
+ };
79
+ }
80
+ function envelopeChangedFiles(envelope) {
81
+ return Array.isArray(envelope?.operations) ? envelope.operations.map((operation) => String(operation?.path || '')).filter(Boolean) : [];
82
+ }
83
+ function buildConflictMap(envelopes) {
84
+ const owners = new Map();
85
+ for (const envelope of envelopes) {
86
+ const agent = String(envelope?.agent_id || 'unknown');
87
+ for (const file of envelopeChangedFiles(envelope))
88
+ owners.set(file, [...(owners.get(file) || []), agent]);
89
+ }
90
+ return [...owners.entries()]
91
+ .filter(([, agents]) => new Set(agents).size > 1)
92
+ .map(([file, agents]) => ({ file, agents: sortedUnique(agents) }));
93
+ }
94
+ function summarizeObject(value) {
95
+ if (!value || typeof value !== 'object')
96
+ return null;
97
+ return {
98
+ schema: value.schema || null,
99
+ ok: typeof value.ok === 'boolean' ? value.ok : null,
100
+ status: value.status || null,
101
+ blockers: stringArray(value.blockers).slice(0, 20),
102
+ keys: Object.keys(value).slice(0, 30)
103
+ };
104
+ }
105
+ function estimateTokens(value) {
106
+ return Math.ceil(JSON.stringify(value).length / 4);
107
+ }
108
+ function tail(value, max) {
109
+ return value.length <= max ? value : value.slice(value.length - max);
110
+ }
111
+ function trim(value, max) {
112
+ return value.length <= max ? value : value.slice(0, max);
113
+ }
114
+ function sortedUnique(values) {
115
+ return [...new Set(values.map(String).filter(Boolean))].sort();
116
+ }
117
+ function stringArray(value) {
118
+ return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
119
+ }
120
+ //# sourceMappingURL=gpt-final-proof-pack.js.map
@@ -0,0 +1,71 @@
1
+ export const GPT_FINAL_ARBITER_RESULT_SCHEMA_ID = 'sks.gpt-final-arbiter-result.v1';
2
+ export const GPT_FINAL_ARBITER_INPUT_SCHEMA = 'sks.gpt-final-arbiter-input.v1';
3
+ export const gptFinalArbiterResultSchema = {
4
+ type: 'object',
5
+ required: [
6
+ 'schema',
7
+ 'status',
8
+ 'summary',
9
+ 'gpt_review_findings',
10
+ 'accepted_patch_envelopes',
11
+ 'modified_patch_envelopes',
12
+ 'rejected_patch_envelopes',
13
+ 'required_followup_work',
14
+ 'verification_plan',
15
+ 'rollback_notes',
16
+ 'blockers',
17
+ 'confidence'
18
+ ],
19
+ properties: {
20
+ schema: { const: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID },
21
+ status: { enum: ['approved', 'modified', 'rejected', 'needs_more_work'] },
22
+ summary: { type: 'string' },
23
+ gpt_review_findings: { type: 'array', items: { type: 'object' } },
24
+ accepted_patch_envelopes: { type: 'array', items: { type: 'object' } },
25
+ modified_patch_envelopes: { type: 'array', items: { type: 'object' } },
26
+ rejected_patch_envelopes: { type: 'array', items: { type: 'object' } },
27
+ required_followup_work: { type: 'array', items: { type: 'object' } },
28
+ verification_plan: { type: 'array', items: { type: 'string' } },
29
+ rollback_notes: { type: 'array', items: { type: 'string' } },
30
+ blockers: { type: 'array', items: { type: 'string' } },
31
+ confidence: { enum: ['low', 'medium', 'high'] }
32
+ },
33
+ additionalProperties: true
34
+ };
35
+ export function normalizeGptFinalArbiterResult(value) {
36
+ const status = normalizeStatus(value?.status);
37
+ return {
38
+ schema: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID,
39
+ status,
40
+ summary: String(value?.summary || defaultSummary(status)),
41
+ gpt_review_findings: array(value?.gpt_review_findings),
42
+ accepted_patch_envelopes: array(value?.accepted_patch_envelopes),
43
+ modified_patch_envelopes: array(value?.modified_patch_envelopes),
44
+ rejected_patch_envelopes: array(value?.rejected_patch_envelopes),
45
+ required_followup_work: array(value?.required_followup_work),
46
+ verification_plan: stringArray(value?.verification_plan),
47
+ rollback_notes: stringArray(value?.rollback_notes),
48
+ blockers: stringArray(value?.blockers),
49
+ confidence: normalizeConfidence(value?.confidence)
50
+ };
51
+ }
52
+ function normalizeStatus(value) {
53
+ return value === 'approved' || value === 'modified' || value === 'rejected' || value === 'needs_more_work'
54
+ ? value
55
+ : 'needs_more_work';
56
+ }
57
+ function normalizeConfidence(value) {
58
+ return value === 'low' || value === 'medium' || value === 'high' ? value : 'medium';
59
+ }
60
+ function array(value) {
61
+ return Array.isArray(value) ? value : [];
62
+ }
63
+ function stringArray(value) {
64
+ return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
65
+ }
66
+ function defaultSummary(status) {
67
+ return status === 'approved' || status === 'modified'
68
+ ? 'GPT final arbiter accepted the candidate result.'
69
+ : 'GPT final arbiter did not approve the candidate result.';
70
+ }
71
+ //# sourceMappingURL=gpt-final-review-schema.js.map
@@ -10,7 +10,7 @@ import { buildFeatureRegistry, validateFeatureRegistry } from '../feature-regist
10
10
  import { hooksExplainReport } from '../../cli/feature-commands.js';
11
11
  import { writeSelftestRouteProof } from '../proof/selftest-proof-fixtures.js';
12
12
  import { createMission } from '../mission.js';
13
- import { formatSksUpdateCheckText, runSksUpdateCheck } from '../update-check.js';
13
+ import { formatSksUpdateCheckText, runSksUpdateCheck, runSksUpdateNow } from '../update-check.js';
14
14
  export async function helpCommand(args = []) {
15
15
  const topic = args[0];
16
16
  if (topic)
@@ -109,6 +109,34 @@ export async function updateCheckCommand(args = []) {
109
109
  return printJson(result);
110
110
  console.log(`${sksTextLogo()}\n\n${formatSksUpdateCheckText(result)}`);
111
111
  }
112
+ export async function updateCommand(sub = 'check', args = []) {
113
+ const action = String(sub || 'check').toLowerCase();
114
+ if (action === 'check' || action === 'status')
115
+ return updateCheckCommand(args);
116
+ if (action !== 'now') {
117
+ console.error('Usage: sks update check|now [--version <version>] [--json] [--dry-run]');
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ const result = await runSksUpdateNow({
122
+ version: valueAfter(args, '--version') || valueAfter(args, '-v'),
123
+ dryRun: flag(args, '--dry-run'),
124
+ timeoutMs: 10 * 60 * 1000,
125
+ maxOutputBytes: 128 * 1024
126
+ });
127
+ if (flag(args, '--json'))
128
+ return printJson(result);
129
+ console.log(`${sksTextLogo()}\n`);
130
+ console.log(`SKS update ${result.status}`);
131
+ if (result.command)
132
+ console.log(`Command: ${result.command}`);
133
+ if (result.global_root)
134
+ console.log(`Global root: ${result.global_root}`);
135
+ if (result.error)
136
+ console.log(`Error: ${result.error}`);
137
+ if (!result.ok)
138
+ process.exitCode = 1;
139
+ }
112
140
  export async function setupCommand(args = []) {
113
141
  const root = await projectRoot();
114
142
  const installScope = installScopeFromArgs(args);
@@ -327,4 +355,11 @@ function whichSync(command) {
327
355
  });
328
356
  return result.status === 0 ? String(result.stdout || '').trim().split(/\r?\n/)[0] : null;
329
357
  }
358
+ function valueAfter(args = [], name) {
359
+ const index = args.findIndex((arg) => String(arg) === name);
360
+ if (index < 0)
361
+ return null;
362
+ const value = args[index + 1];
363
+ return value === undefined ? null : String(value);
364
+ }
330
365
  //# sourceMappingURL=basic-cli.js.map
@@ -0,0 +1,120 @@
1
+ import { resolveOllamaWorkerConfig, writeLocalModelConfig, readLocalModelConfig } from '../agents/ollama-worker-config.js';
2
+ export async function localModelCommand(args = []) {
3
+ const action = normalizeLocalModelAction(args[0]);
4
+ if (action === 'enable')
5
+ return emit(await enable(args.slice(1)), args);
6
+ if (action === 'disable')
7
+ return emit(await disable(), args);
8
+ if (action === 'set-model')
9
+ return emit(await setModel(args.slice(1)), args);
10
+ if (action === 'status')
11
+ return emit(await status(), args);
12
+ const result = { schema: 'sks.local-model-command.v1', ok: false, action, blockers: ['unknown_local_model_action'] };
13
+ process.exitCode = 1;
14
+ return emit(result, args);
15
+ }
16
+ function normalizeLocalModelAction(value) {
17
+ const text = String(value || 'status').trim().toLowerCase();
18
+ if (['on', 'enable', 'enabled', 'with-local-llm-on', '켜', '켜기'].includes(text))
19
+ return 'enable';
20
+ if (['off', 'disable', 'disabled', 'with-local-llm-off', '꺼', '끄기'].includes(text))
21
+ return 'disable';
22
+ if (['model', 'set', 'set-model'].includes(text))
23
+ return 'set-model';
24
+ if (['', 'status', 'state', 'check'].includes(text))
25
+ return 'status';
26
+ return text;
27
+ }
28
+ async function enable(args) {
29
+ const model = readOption(args, '--model', firstPositional(args) || '');
30
+ const baseUrl = readOption(args, '--base-url', '');
31
+ const think = readBoolFlag(args, '--think', '--no-think');
32
+ const patch = { enabled: true, provider: 'ollama' };
33
+ if (model)
34
+ patch.model = model;
35
+ if (baseUrl)
36
+ patch.base_url = baseUrl;
37
+ if (think !== null)
38
+ patch.think = think;
39
+ const config = await writeLocalModelConfig(patch);
40
+ return { schema: 'sks.local-model-command.v1', ok: true, action: 'enable', config };
41
+ }
42
+ async function disable() {
43
+ const config = await writeLocalModelConfig({ enabled: false });
44
+ return { schema: 'sks.local-model-command.v1', ok: true, action: 'disable', config };
45
+ }
46
+ async function setModel(args) {
47
+ const model = String(args[0] || '').trim();
48
+ if (!model) {
49
+ process.exitCode = 1;
50
+ return { schema: 'sks.local-model-command.v1', ok: false, action: 'set-model', blockers: ['model_missing'] };
51
+ }
52
+ const config = await writeLocalModelConfig({ model });
53
+ return { schema: 'sks.local-model-command.v1', ok: true, action: 'set-model', config };
54
+ }
55
+ async function status() {
56
+ const config = await readLocalModelConfig();
57
+ const resolved = await resolveOllamaWorkerConfig();
58
+ const api = await probeOllama(resolved.base_url);
59
+ return { schema: 'sks.local-model-command.v1', ok: true, action: 'status', config, resolved, api };
60
+ }
61
+ async function probeOllama(baseUrl) {
62
+ try {
63
+ const response = await fetch(`${baseUrl}/api/version`, { signal: AbortSignal.timeout(3000) });
64
+ const text = await response.text();
65
+ return { ok: response.ok, status: response.status, data: response.ok ? JSON.parse(text) : null };
66
+ }
67
+ catch (error) {
68
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
69
+ }
70
+ }
71
+ function emit(result, args) {
72
+ if (args.includes('--json')) {
73
+ console.log(JSON.stringify(result, null, 2));
74
+ return result;
75
+ }
76
+ if (result.ok !== true) {
77
+ console.log(`Local model: blocked (${(result.blockers || []).join(', ') || 'unknown'})`);
78
+ return result;
79
+ }
80
+ const config = result.config || result.resolved || {};
81
+ console.log(`Local model: ${config.enabled ? 'enabled' : 'disabled'}`);
82
+ console.log(`Provider: ollama`);
83
+ console.log(`Model: ${config.model || 'unknown'}`);
84
+ console.log(`Base URL: ${config.base_url || config.baseUrl || 'unknown'}`);
85
+ if (typeof config.think === 'boolean')
86
+ console.log(`Think: ${config.think ? 'enabled' : 'disabled'}`);
87
+ if (result.api)
88
+ console.log(`Ollama API: ${result.api.ok ? 'ok' : 'not reachable'}`);
89
+ return result;
90
+ }
91
+ function readOption(args, name, fallback) {
92
+ const index = args.indexOf(name);
93
+ if (index >= 0 && args[index + 1] && !String(args[index + 1]).startsWith('--'))
94
+ return String(args[index + 1]);
95
+ const prefixed = args.find((arg) => String(arg).startsWith(`${name}=`));
96
+ return prefixed ? prefixed.slice(name.length + 1) : fallback;
97
+ }
98
+ function readBoolFlag(args, trueName, falseName) {
99
+ if (args.includes(trueName))
100
+ return true;
101
+ if (args.includes(falseName))
102
+ return false;
103
+ return null;
104
+ }
105
+ function firstPositional(args = []) {
106
+ for (let i = 0; i < args.length; i += 1) {
107
+ const arg = String(args[i] || '');
108
+ if (arg === '--model' || arg === '--base-url') {
109
+ if (args[i + 1] && !String(args[i + 1]).startsWith('--'))
110
+ i += 1;
111
+ continue;
112
+ }
113
+ if (arg.startsWith('--model=') || arg.startsWith('--base-url='))
114
+ continue;
115
+ if (!arg.startsWith('--'))
116
+ return arg;
117
+ }
118
+ return '';
119
+ }
120
+ //# sourceMappingURL=local-model-command.js.map
@@ -4,7 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { appendJsonlBounded, exists, nowIso, packageRoot, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
5
5
  import { initProject } from '../init.js';
6
6
  import { createMission, setCurrent } from '../mission.js';
7
- import { enableMadHighProfile, madHighProfileName } from '../auto-review.js';
7
+ import { buildMadHighLaunchProfileNoWrite, madHighProfileName } from '../auto-review.js';
8
8
  import { permissionGateSummary } from '../permission-gates.js';
9
9
  import { attachZellijSessionInteractive, launchMadZellijUi, sanitizeZellijSessionName } from '../zellij/zellij-launcher.js';
10
10
  import { createMadSksAuthorizationManifest, validateMadSksAuthorizationManifest } from '../mad-sks/authorization-manifest.js';
@@ -17,13 +17,14 @@ import { runMadSksExecutor } from '../mad-sks/executors/index.js';
17
17
  import { applyMadSksRollbackPlan } from '../mad-sks/rollback-apply.js';
18
18
  import { repairCodexConfigEperm } from '../codex/codex-config-eperm-repair.js';
19
19
  import { runCodexLaunchPreflight } from '../preflight/parallel-preflight-engine.js';
20
+ import { diffCodexAppUiSnapshots, writeCodexAppUiSnapshot } from '../codex-app/codex-app-ui-state-snapshot.js';
20
21
  export async function madHighCommand(args = [], deps = {}) {
21
22
  const subcommand = firstSubcommand(args);
22
23
  if (subcommand)
23
24
  return madSksSubcommand(subcommand, args.filter((arg) => String(arg) !== subcommand));
24
25
  const cleanArgs = stripMadLaunchOnlyArgs(args);
25
26
  if (args.includes('--json')) {
26
- const profile = await enableMadHighProfile();
27
+ const profile = buildMadHighLaunchProfileNoWrite();
27
28
  return console.log(JSON.stringify(profile, null, 2));
28
29
  }
29
30
  const update = deps.maybePromptSksUpdateForLaunch ? await deps.maybePromptSksUpdateForLaunch(args, { label: 'MAD launch' }) : { status: 'skipped' };
@@ -55,16 +56,28 @@ export async function madHighCommand(args = [], deps = {}) {
55
56
  process.exitCode = 1;
56
57
  return;
57
58
  }
58
- const profile = await enableMadHighProfile();
59
+ const profile = buildMadHighLaunchProfileNoWrite();
59
60
  const launchRoot = process.cwd();
60
61
  if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
61
62
  await initProject(launchRoot, {});
63
+ const uiSnapshotId = Date.now().toString(36);
64
+ const beforeUi = await writeCodexAppUiSnapshot(launchRoot, `mad-before-${uiSnapshotId}`).catch(() => null);
62
65
  // launchFast skips the redundant live-`codex exec` config probe (up to ~20s, run
63
66
  // up to 3x via repair re-inspections): the real codex profile is exercised moments
64
67
  // later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
65
68
  // readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
66
69
  // old behavior.
67
- const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: true, launchFast: process.env.SKS_LAUNCH_FULL_CODEX_PROBE !== '1', profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
70
+ const rawArgs = (args || []).map((arg) => String(arg));
71
+ const allowMadRepair = rawArgs.includes('--repair-config') || rawArgs.includes('--fix') || rawArgs.includes('--yes-repair');
72
+ const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: allowMadRepair, launchFast: process.env.SKS_LAUNCH_FULL_CODEX_PROBE !== '1', profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
73
+ const afterPreflightUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-preflight-${uiSnapshotId}`).catch(() => null) : null;
74
+ const preflightUiDiff = beforeUi && afterPreflightUi ? diffCodexAppUiSnapshots(beforeUi, afterPreflightUi) : null;
75
+ if (preflightUiDiff && !preflightUiDiff.ok) {
76
+ await writeJsonAtomic(path.join(launchRoot, '.sneakoscope', 'reports', 'mad-codex-app-ui-preflight-diff.json'), preflightUiDiff);
77
+ console.error('SKS MAD launch changed Codex App UI state during preflight. Run `sks doctor --fix`.');
78
+ process.exitCode = 1;
79
+ return preflightUiDiff;
80
+ }
68
81
  if (!launchPreflight.ok) {
69
82
  console.error('SKS MAD launch blocked by config preflight.');
70
83
  for (const blocker of launchPreflight.blockers || [])
@@ -83,16 +96,30 @@ export async function madHighCommand(args = [], deps = {}) {
83
96
  SKS_MAD_SKS_TARGET_ROOT: madLaunch.gate.cwd,
84
97
  SKS_MAD_SKS_PROTECTED_CORE_DIGEST: madLaunch.gate.protected_core_digest
85
98
  };
86
- const madNativeSwarm = await startMadNativeSwarm(madLaunch.root, madLaunch, args, profile, {
87
- env: madSksEnv
88
- });
89
99
  const launchOpts = codexLbImmediateLaunchOpts(cleanArgs, launchLb, { codexArgs: profile.launch_args, conciseBlockers: true, madSksEnv, launchEnv: madSksEnv });
90
100
  const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', launchOpts.session || `sks-mad-${sanitizeZellijSessionName(process.cwd())}`));
91
- 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: madNativeSwarm.lane_count || 1, requireZellij: process.env.SKS_REQUIRE_ZELLIJ === '1' });
101
+ 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' });
102
+ const afterLaunchUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-launch-${uiSnapshotId}`).catch(() => null) : null;
103
+ const launchUiDiff = beforeUi && afterLaunchUi ? diffCodexAppUiSnapshots(beforeUi, afterLaunchUi) : null;
104
+ if (launchUiDiff) {
105
+ await writeJsonAtomic(path.join(madLaunch.dir, 'codex-app-ui-diff.json'), launchUiDiff);
106
+ if (!launchUiDiff.ok) {
107
+ console.error('SKS MAD launch changed Codex App UI state. Run `sks doctor --fix`.');
108
+ process.exitCode = 1;
109
+ return launchUiDiff;
110
+ }
111
+ }
92
112
  if (!launch.ok) {
93
113
  console.log(`MAD Zellij action: ${formatMadZellijAction(launch)}`);
94
114
  return launch;
95
115
  }
116
+ const madNativeSwarm = await startMadNativeSwarm(madLaunch.root, madLaunch, args, profile, {
117
+ env: {
118
+ ...madSksEnv,
119
+ SKS_ZELLIJ_SESSION_NAME: launch.session_name
120
+ },
121
+ zellijSessionName: launch.session_name
122
+ });
96
123
  // The launcher only creates a detached background session. In an interactive
97
124
  // terminal, immediately attach so the session actually opens for the user
98
125
  // instead of leaving them to copy/paste the attach command by hand.
@@ -162,6 +189,11 @@ export async function startMadNativeSwarm(root, madLaunch, args = [], profile =
162
189
  '--fast',
163
190
  '--json'
164
191
  ];
192
+ if (swarm.backend === 'zellij') {
193
+ command.push('--real');
194
+ command.push('--zellij-session-name', opts.zellijSessionName || `sks-${madLaunch.mission_id}`);
195
+ command.push('--zellij-pane-worker');
196
+ }
165
197
  const baseReport = {
166
198
  schema: 'sks.mad-sks-native-swarm.v1',
167
199
  ok: true,
@@ -176,6 +208,7 @@ export async function startMadNativeSwarm(root, madLaunch, args = [], profile =
176
208
  target_active_slots: swarm.agents,
177
209
  work_items: swarm.workItems,
178
210
  backend: swarm.backend,
211
+ zellij_session_name: opts.zellijSessionName || null,
179
212
  readonly: true,
180
213
  command,
181
214
  stdout_log: path.relative(root, stdoutLog),
@@ -228,7 +261,7 @@ export function resolveMadNativeSwarmOptions(args = [], profile = {}, opts = {})
228
261
  const disabled = list.includes('--no-swarm') || list.includes('--no-mad-swarm') || process.env.SKS_MAD_NATIVE_SWARM === '0';
229
262
  const agents = clampInt(readOption(list, '--mad-agents', readOption(list, '--mad-swarm-agents', process.env.SKS_MAD_SWARM_AGENTS || opts.agents || 5)), 1, 20);
230
263
  const workItems = clampInt(readOption(list, '--mad-swarm-work-items', process.env.SKS_MAD_SWARM_WORK_ITEMS || opts.workItems || agents), agents, 100);
231
- const backend = String(readOption(list, '--mad-swarm-backend', process.env.SKS_MAD_SWARM_BACKEND || opts.backend || 'codex-sdk'));
264
+ const backend = defaultMadSwarmBackend(list, opts);
232
265
  return {
233
266
  enabled: !disabled,
234
267
  disabled_reason: disabled ? 'operator_disabled_mad_native_swarm' : null,
@@ -405,6 +438,9 @@ function madLaunchOnlyFlags() {
405
438
  '--mad-swarm-work-items',
406
439
  '--mad-swarm-backend',
407
440
  '--mad-swarm-prompt',
441
+ '--repair-config',
442
+ '--fix',
443
+ '--yes-repair',
408
444
  '--yes',
409
445
  '-y',
410
446
  '--dry-run',
@@ -420,6 +456,19 @@ function madLaunchValueFlags() {
420
456
  '--mad-swarm-prompt'
421
457
  ]);
422
458
  }
459
+ export function defaultMadSwarmBackend(args = [], opts = {}) {
460
+ const list = (args || []).map((arg) => String(arg));
461
+ const explicit = readOption(list, '--mad-swarm-backend', null);
462
+ if (explicit)
463
+ return String(explicit);
464
+ if (process.env.SKS_MAD_SWARM_BACKEND)
465
+ return String(process.env.SKS_MAD_SWARM_BACKEND);
466
+ if (opts.backend)
467
+ return String(opts.backend);
468
+ if (list.includes('--json') || list.includes('--no-attach') || opts.nonInteractive === true)
469
+ return 'codex-sdk';
470
+ return 'zellij';
471
+ }
423
472
  function stripMadLaunchOnlyArgs(args = []) {
424
473
  const flags = madLaunchOnlyFlags();
425
474
  const valueFlags = madLaunchValueFlags();