sneakoscope 3.1.4 → 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 (44) 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-lb.js +12 -9
  9. package/dist/core/codex-app/codex-agent-role-sync.js +63 -11
  10. package/dist/core/codex-app/codex-agent-type-probe.js +202 -0
  11. package/dist/core/codex-app/codex-app-execution-profile.js +15 -8
  12. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -4
  13. package/dist/core/codex-app/codex-app-harness-matrix.js +78 -26
  14. package/dist/core/codex-app/codex-app-types.js +21 -0
  15. package/dist/core/codex-app/codex-hook-approval-probe.js +188 -0
  16. package/dist/core/codex-app/codex-hook-lifecycle.js +31 -10
  17. package/dist/core/codex-app/codex-init-deep.js +97 -4
  18. package/dist/core/codex-app/codex-skill-sync.js +75 -3
  19. package/dist/core/codex-app/lazycodex-analysis.js +31 -10
  20. package/dist/core/codex-app/lazycodex-interop-policy.js +13 -2
  21. package/dist/core/codex-app/lazycodex-live-analyzer.js +98 -0
  22. package/dist/core/commands/mad-sks-command.js +37 -2
  23. package/dist/core/commands/qa-loop-command.js +3 -2
  24. package/dist/core/commands/research-command.js +2 -2
  25. package/dist/core/doctor/doctor-zellij-repair.js +5 -1
  26. package/dist/core/feature-fixtures.js +1 -0
  27. package/dist/core/feature-registry.js +4 -1
  28. package/dist/core/fsx.js +1 -1
  29. package/dist/core/init.js +4 -1
  30. package/dist/core/loops/loop-continuation-enforcer.js +11 -3
  31. package/dist/core/loops/loop-planner.js +21 -4
  32. package/dist/core/loops/loop-worker-runtime.js +23 -7
  33. package/dist/core/naruto/naruto-loop-worker-router.js +11 -2
  34. package/dist/core/qa-loop.js +39 -4
  35. package/dist/core/research/research-cycle-runner.js +1 -0
  36. package/dist/core/research/research-stage-runner.js +9 -2
  37. package/dist/core/research.js +35 -1
  38. package/dist/core/version.js +1 -1
  39. package/dist/core/zellij/homebrew-policy.js +0 -1
  40. package/dist/core/zellij/zellij-self-heal-types.js +45 -0
  41. package/dist/core/zellij/zellij-self-heal.js +69 -8
  42. package/dist/core/zellij/zellij-update.js +9 -1
  43. package/dist/scripts/sks-3-1-5-directive-check-lib.js +347 -0
  44. package/package.json +26 -2
@@ -43,7 +43,8 @@ async function runLoopWorkerNative(input) {
43
43
  ? buildLoopMakerPrompt({ plan: input.plan, node: input.node, worktreePath: input.worktree?.path || null })
44
44
  : buildLoopCheckerPrompt({ plan: input.plan, node: input.node, makerArtifacts: input.makerArtifacts || [] });
45
45
  const workerCount = effectiveLoopWorkerCount(input);
46
- const workGraph = buildLoopNarutoWorkGraph(input, workerCount);
46
+ const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
47
+ const workGraph = buildLoopNarutoWorkGraph(input, workerCount, executionProfile);
47
48
  // Root-cause-1 fix: keep the ORCHESTRATOR root on the MAIN repo (input.root), not the
48
49
  // loop worktree. All zellij/right-column/slot-telemetry state derives from the orchestrator
49
50
  // root, so anchoring it on input.root makes the SLOTS snapshot land under
@@ -52,7 +53,6 @@ async function runLoopWorkerNative(input) {
52
53
  // The loop worktree is still where workers cwd + write: it is threaded through the per-worker
53
54
  // `worktree` opt below, which launchWorker reads as ctx.opts.worktree -> workerCwd.
54
55
  const insideZellij = Boolean(process.env.SKS_ZELLIJ_SESSION_NAME || process.env.ZELLIJ);
55
- const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
56
56
  const visiblePaneCap = Math.min(resolveLoopVisiblePaneCap(workerCount), Math.max(1, workerCount));
57
57
  const zellijPlacementOpts = insideZellij ? {
58
58
  workerPlacement: 'zellij-pane',
@@ -99,9 +99,9 @@ async function runLoopWorkerNative(input) {
99
99
  fallback_reason: null
100
100
  } : null
101
101
  });
102
- return normalizeNativeResult(input, orchestrator);
102
+ return normalizeNativeResult(input, orchestrator, executionProfile);
103
103
  }
104
- async function normalizeNativeResult(input, result) {
104
+ async function normalizeNativeResult(input, result, executionProfile) {
105
105
  const artifacts = collectArtifactPaths(result);
106
106
  const changedFiles = stringArray(result?.changed_files || result?.proof?.changed_files || result?.results?.flatMap?.((row) => row?.changed_files || []));
107
107
  const blockers = [
@@ -124,7 +124,15 @@ async function normalizeNativeResult(input, result) {
124
124
  blockers: [...new Set(blockers)],
125
125
  runtime_proof_path: proofPath,
126
126
  worker_ids: stringArray(result?.results?.map?.((row) => row?.agent_id || row?.id)),
127
- 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
+ } : {})
128
136
  };
129
137
  await writeJsonAtomic(proofPath, { ...normalized, native_result_summary: summarizeNativeResult(result), generated_at: nowIso() });
130
138
  return normalized;
@@ -189,7 +197,13 @@ async function runLoopWorkerFixture(input) {
189
197
  fixture_allowed_reason: fixturePolicy.allowed ? fixturePolicy.reason : null
190
198
  };
191
199
  }
192
- 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;
193
207
  const workItems = Array.from({ length: Math.max(1, workerCount) }, (_, index) => {
194
208
  const id = `${input.node.loop_id}-${input.phase}-${index + 1}`;
195
209
  const writeAllowed = input.phase === 'maker';
@@ -218,7 +232,8 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
218
232
  mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
219
233
  required: input.node.worktree.required,
220
234
  allocation_required: false
221
- }
235
+ },
236
+ ...(profilePayload ? { codex_app_execution_profile: profilePayload } : {})
222
237
  };
223
238
  });
224
239
  return {
@@ -239,6 +254,7 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
239
254
  worktree_root: null,
240
255
  fallback_reason: input.worktree?.path ? 'loop_worktree_already_allocated' : null
241
256
  },
257
+ ...(profilePayload ? { codex_app_execution_profile: profilePayload } : {}),
242
258
  blockers: [],
243
259
  ok: true
244
260
  };
@@ -1,7 +1,9 @@
1
- export function routeNarutoLoopWorker(node, role) {
1
+ export function routeNarutoLoopWorker(node, role, profile) {
2
2
  const domain = node.loop_id.replace(/^loop-/, '');
3
3
  const roles = roleLabels(domain);
4
4
  const gates = [...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final];
5
+ const roleName = role === 'maker' ? roles.maker : roles.checker;
6
+ const strategy = profile?.agent_role_strategy || 'message-role';
5
7
  return {
6
8
  schema: 'sks.naruto-loop-worker-route.v1',
7
9
  loop_id: node.loop_id,
@@ -9,7 +11,10 @@ export function routeNarutoLoopWorker(node, role) {
9
11
  checker_role: roles.checker,
10
12
  prompt: [
11
13
  `loop purpose: ${node.purpose}`,
12
- `role: ${role === 'maker' ? roles.maker : roles.checker}`,
14
+ `role: ${roleName}`,
15
+ `agent role strategy: ${strategy}`,
16
+ `agent_type: ${strategy === 'agent_type' ? roleName.replace(/\s+/g, '-') : '-'}`,
17
+ `message role prefix: ${strategy === 'message-role' ? `Role: ${roleName}.` : '-'}`,
13
18
  `owner files: ${node.owner_scope.files.join(', ') || '-'}`,
14
19
  `owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
15
20
  `gates: ${gates.join(', ') || '-'}`,
@@ -21,6 +26,10 @@ export function routeNarutoLoopWorker(node, role) {
21
26
  allowed_files: node.owner_scope.files,
22
27
  allowed_directories: node.owner_scope.directories,
23
28
  gates,
29
+ agent_role_strategy: strategy,
30
+ agent_type: strategy === 'agent_type' ? roleName.replace(/\s+/g, '-') : null,
31
+ message_role_prefix: strategy === 'message-role' ? `Role: ${roleName}.` : null,
32
+ execution_profile_artifact: profile?.artifact_path || null,
24
33
  mutation_outside_owner_scope_allowed: false
25
34
  };
26
35
  }
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { exists, nowIso, readJson, readText, writeJsonAtomic, writeTextAtomic, PACKAGE_VERSION } from './fsx.js';
3
3
  import { CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE, CODEX_WEB_VERIFICATION_POLICY, evidenceMentionsForbiddenBrowserAutomation, evidenceMentionsForbiddenWebComputerUseEvidence } from './routes.js';
4
4
  import { appendAgentLedgerEvent, initializeAgentCentralLedger } from './agents/agent-central-ledger.js';
5
+ import { resolveCodexAppExecutionProfile } from './codex-app/codex-app-execution-profile.js';
5
6
  export const QA_LOOP_ROUTE = 'QALoop';
6
7
  const QA_REPORT_SUFFIX = 'qa-report.md';
7
8
  const UI_CHROME_EXTENSION_FIRST_ACK = 'use_codex_chrome_extension_first_no_computer_use_for_web_ui_or_mark_unverified';
@@ -320,6 +321,10 @@ export function defaultQaGate(contract = {}, opts = {}) {
320
321
  image_artifact_path_contract_present: false,
321
322
  image_artifact_path_contract_artifact: null,
322
323
  image_artifact_path_contract_blockers: [],
324
+ codex_app_execution_profile: opts.executionProfile ? compactExecutionProfile(opts.executionProfile) : null,
325
+ codex_app_execution_profile_artifact: opts.executionProfile ? 'qa-loop/execution-profile.json' : null,
326
+ codex_app_hooks_approval_required: opts.executionProfile?.hooks_approval_required === true,
327
+ codex_app_agent_role_strategy: opts.executionProfile?.agent_role_strategy || null,
323
328
  api_e2e_required: apiRequired,
324
329
  unsafe_external_side_effects: false,
325
330
  corrective_loop_enabled: corrective,
@@ -338,16 +343,21 @@ export async function writeQaLoopArtifacts(dir, mission, contract) {
338
343
  const a = contract.answers || {};
339
344
  const checklist = qaChecklist(a);
340
345
  const reportFile = qaReportFilename();
346
+ const root = missionRootFromDir(dir);
347
+ const executionProfile = root ? await resolveCodexAppExecutionProfile({ root }).catch(() => null) : null;
348
+ if (executionProfile)
349
+ await writeJsonAtomic(path.join(dir, 'qa-loop', 'execution-profile.json'), executionProfile).catch(() => undefined);
341
350
  await writeJsonAtomic(path.join(dir, 'qa-ledger.json'), {
342
351
  schema_version: 1,
343
352
  generated_at: nowIso(),
344
353
  mission_id: mission.id,
345
354
  qa_report_file: reportFile,
355
+ codex_app_execution_profile: executionProfile ? compactExecutionProfile(executionProfile) : null,
346
356
  target: { scope: a.QA_SCOPE, environment: a.TARGET_ENVIRONMENT, base_url: a.TARGET_BASE_URL, api_base_url: a.API_BASE_URL },
347
357
  safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: 'codex_chrome_extension_first_required_for_web_ui_e2e' },
348
358
  checklist
349
359
  });
350
- await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile }));
360
+ await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile, executionProfile }));
351
361
  await writeTextAtomic(path.join(dir, reportFile), qaReportTemplate(mission, contract, checklist));
352
362
  return { checklist_count: checklist.length, report_file: reportFile };
353
363
  }
@@ -432,6 +442,10 @@ export async function writeMockQaResult(dir, mission, contract) {
432
442
  image_artifact_path_contract_present: previousGate.image_artifact_path_contract_present === true,
433
443
  image_artifact_path_contract_artifact: previousGate.image_artifact_path_contract_artifact || null,
434
444
  image_artifact_path_contract_blockers: previousGate.image_artifact_path_contract_blockers || [],
445
+ codex_app_execution_profile: previousGate.codex_app_execution_profile || null,
446
+ codex_app_execution_profile_artifact: previousGate.codex_app_execution_profile_artifact || null,
447
+ codex_app_hooks_approval_required: previousGate.codex_app_hooks_approval_required === true,
448
+ codex_app_agent_role_strategy: previousGate.codex_app_agent_role_strategy || null,
435
449
  blockers: previousGate.blockers || [],
436
450
  passed: !uiRequired,
437
451
  qa_report_written: true,
@@ -453,7 +467,7 @@ export async function writeMockQaResult(dir, mission, contract) {
453
467
  });
454
468
  return evaluateQaGate(dir);
455
469
  }
456
- export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
470
+ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff, executionProfile }) {
457
471
  const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
458
472
  const imageContractText = imagePathContract
459
473
  ? `\nIMAGE PATH CONTRACT:\n${JSON.stringify(imagePathContract, null, 2)}\nUse model_visible_path values for follow-up image edits; do not invent generated image paths.\n`
@@ -461,6 +475,9 @@ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, repo
461
475
  const appHandoffText = appHandoff
462
476
  ? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
463
477
  : '';
478
+ const executionProfileText = executionProfile
479
+ ? `\nCODEX APP EXECUTION PROFILE:\n${JSON.stringify(compactExecutionProfile(executionProfile), null, 2)}\nUse this routing profile for agent role strategy and app/headless assumptions.\n`
480
+ : '';
464
481
  return `SKS QA-LOOP
465
482
  MISSION: ${id}
466
483
  TASK: ${mission.prompt}
@@ -473,7 +490,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
473
490
  ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
474
491
  CONTRACT:
475
492
  ${JSON.stringify(contract, null, 2)}
476
- ${imageContractText}${appHandoffText}
493
+ ${imageContractText}${appHandoffText}${executionProfileText}
477
494
  Previous tail:
478
495
  ${String(previous || '').slice(-2500)}
479
496
  `;
@@ -486,7 +503,8 @@ export async function qaStatus(dir) {
486
503
  const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
487
504
  const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
488
505
  const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
489
- return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, desktop_app_confirmation: appConfirmation, desktop_review_complete: appConfirmation?.verdict === 'pass', image_path_contract: imagePathContract };
506
+ const executionProfile = await readJson(path.join(dir, 'qa-loop', 'execution-profile.json'), null);
507
+ return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, desktop_app_confirmation: appConfirmation, desktop_review_complete: appConfirmation?.verdict === 'pass', image_path_contract: imagePathContract, codex_app_execution_profile: executionProfile };
490
508
  }
491
509
  function qaChecklist(a) {
492
510
  const cases = [
@@ -504,6 +522,23 @@ function qaChecklist(a) {
504
522
  cases.push(['report.evidence', 'Record pass/fail/blocked/skipped with evidence.'], ['report.corrective_loop', 'Record fixes, rechecks, unresolved findings, deferred blockers.'], ['report.honest', 'Run Honest Mode.']);
505
523
  return cases.map(([id, title]) => ({ id, title, status: 'pending', evidence: [] }));
506
524
  }
525
+ function missionRootFromDir(dir) {
526
+ const normalized = path.resolve(String(dir || ''));
527
+ const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
528
+ const idx = normalized.indexOf(marker);
529
+ return idx > 0 ? normalized.slice(0, idx) : null;
530
+ }
531
+ function compactExecutionProfile(profile) {
532
+ return profile ? {
533
+ mode: profile.mode || 'unknown',
534
+ agent_role_strategy: profile.agent_role_strategy || 'message-role',
535
+ hooks_approval_required: profile.hooks_approval_required === true,
536
+ hook_approval_state: profile.hook_approval_state || 'unknown',
537
+ app_handoff_ready: profile.app_handoff_ready === true,
538
+ plugin_mcp_inventory_ready: profile.plugin_mcp_inventory_ready === true,
539
+ artifact_path: profile.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json'
540
+ } : null;
541
+ }
507
542
  function qaReportTemplate(mission, contract, checklist) {
508
543
  const a = contract.answers || {};
509
544
  return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence: ${CODEX_WEB_VERIFICATION_POLICY}\n\n## Checklist\n\n${checklist.map((item) => `- [ ] ${item.id}: ${item.title}`).join('\n')}\n\n## Findings\n\nTBD\n\n## Corrections And Rechecks\n\nTBD\n\n## Honest Mode\n\nTBD\n`;
@@ -63,6 +63,7 @@ export async function runResearchCycle(inputOrDir, legacyGraph = null, legacyOpt
63
63
  blockers: [...new Set(blockers)],
64
64
  stages: stageResults.map((stage) => stage.stage_id),
65
65
  stage_results: stageResults,
66
+ codex_app_execution_profile: input.plan?.codex_app_execution_profile || null,
66
67
  parallelism: {
67
68
  max_parallel_stages: maxParallel,
68
69
  max_observed_parallel: maxObservedParallel,
@@ -91,7 +91,7 @@ async function runSourceShardStage(input, startedAt) {
91
91
  const validation = validateResearchSourceShardOutput(output);
92
92
  await writeJsonAtomic(path.join(input.dir, artifact), output);
93
93
  await writeTextAtomic(path.join(input.dir, 'research', `cycle-${input.cycle}`, 'source-notes', `${layer.id}.md`), `# Source shard: ${layer.label}\n\n${output.sources.map((source) => `- ${source.id}: ${source.title}`).join('\n')}\n`);
94
- return baseResult(input, startedAt, 'source_shard', validation.ok ? 'passed' : 'blocked', [artifact, `research/cycle-${input.cycle}/source-notes/${layer.id}.md`], validation.blockers, { layer_id: layer.id, source_count: output.sources.length });
94
+ return baseResult(input, startedAt, 'source_shard', validation.ok ? 'passed' : 'blocked', [artifact, `research/cycle-${input.cycle}/source-notes/${layer.id}.md`], validation.blockers, { layer_id: layer.id, source_count: output.sources.length, source_tool_route: researchSourceToolRoute(input.plan) });
95
95
  }
96
96
  const codex = await runResearchCodexStage({
97
97
  root: input.root,
@@ -333,9 +333,16 @@ function baseResult(input, startedAt, stageKind, status, outputArtifacts, blocke
333
333
  backend: input.backend,
334
334
  worker_result_path: typeof metrics.worker_result_path === 'string' ? metrics.worker_result_path : null,
335
335
  blockers: [...new Set(blockers.map(String).filter(Boolean))],
336
- metrics
336
+ metrics: {
337
+ ...metrics,
338
+ codex_app_execution_profile: input.plan?.codex_app_execution_profile || null,
339
+ source_tool_route: metrics.source_tool_route || researchSourceToolRoute(input.plan)
340
+ }
337
341
  };
338
342
  }
343
+ function researchSourceToolRoute(plan) {
344
+ return plan?.web_research_policy?.source_tool_routing?.mode || (plan?.codex_app_execution_profile?.plugin_mcp_inventory_ready ? 'plugin-mcp-inventory-first' : 'codex-cli-or-web-fallback');
345
+ }
339
346
  async function buildResearchGateSeed(dir, plan) {
340
347
  const sourceLedger = await readJson(path.join(dir, 'source-ledger.json'), null);
341
348
  const claimMatrix = await readJson(path.join(dir, 'claim-evidence-matrix.json'), null);
@@ -17,6 +17,7 @@ import { writeResearchHandoffArtifacts } from './research/research-handoff.js';
17
17
  import { RESEARCH_WORK_GRAPH_ARTIFACT, writeResearchWorkGraph } from './research/research-work-graph.js';
18
18
  import { researchPromptContractText, validateResearchPromptContract } from './research/research-prompt-contract.js';
19
19
  import { buildRealisticResearchPaper, buildRealisticResearchReport } from './research/research-realistic-report.js';
20
+ import { resolveCodexAppExecutionProfile } from './codex-app/codex-app-execution-profile.js';
20
21
  export const RESEARCH_PAPER_ARTIFACT = 'research-paper.md';
21
22
  export const RESEARCH_SOURCE_SKILL_ARTIFACT = 'research-source-skill.md';
22
23
  export const RESEARCH_GENIUS_SUMMARY_ARTIFACT = 'genius-opinion-summary.md';
@@ -252,6 +253,7 @@ export function createResearchPlan(prompt, opts = {}) {
252
253
  const createdAt = nowIso();
253
254
  const paperArtifact = researchPaperArtifactName(prompt, createdAt, opts);
254
255
  const nativeAgentPlan = researchNativeAgentPlan(prompt, { paperArtifact, missionId: opts.missionId });
256
+ const executionProfile = opts.executionProfile || null;
255
257
  return {
256
258
  schema_version: 1,
257
259
  mission_id: opts.missionId || null,
@@ -262,6 +264,7 @@ export function createResearchPlan(prompt, opts = {}) {
262
264
  paper_artifact: paperArtifact,
263
265
  quality_contract: DEFAULT_RESEARCH_QUALITY_CONTRACT,
264
266
  native_agent_plan: nativeAgentPlan,
267
+ codex_app_execution_profile: executionProfile ? compactExecutionProfile(executionProfile) : null,
265
268
  agent_sessions: nativeAgentPlan.personas,
266
269
  agent_batches: nativeAgentPlan.batches,
267
270
  autoresearch_cycle_policy: nativeAgentPlan.autoresearch_cycle_policy,
@@ -318,6 +321,12 @@ export function createResearchPlan(prompt, opts = {}) {
318
321
  web_research_policy: {
319
322
  mode: 'layered_source_retrieval_and_triangulation',
320
323
  requirement: 'Use every safely available public web/source route before synthesis, separated into source layers so the final claim is not dominated by one corpus or platform.',
324
+ source_tool_routing: {
325
+ mode: executionProfile?.plugin_mcp_inventory_ready ? 'plugin-mcp-inventory-first' : 'codex-cli-or-web-fallback',
326
+ plugin_mcp_inventory_ready: executionProfile?.plugin_mcp_inventory_ready === true,
327
+ execution_profile_artifact: executionProfile?.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json',
328
+ rule: 'Prefer verified plugin/MCP inventory when available; otherwise record source-tool blockers instead of assuming live search coverage.'
329
+ },
321
330
  query_sets: [
322
331
  'first-principles and theory sources',
323
332
  'plain-language explanations and empirical examples',
@@ -417,6 +426,9 @@ export function researchPlanMarkdown(plan) {
417
426
  lines.push(`Depth: ${plan.depth}`);
418
427
  lines.push(`Methodology: ${plan.methodology}`);
419
428
  lines.push(`Research paper: ${researchPaperArtifactForPlan(plan)}`);
429
+ if (plan.codex_app_execution_profile) {
430
+ lines.push(`Execution profile: ${plan.codex_app_execution_profile.mode}; agent role strategy ${plan.codex_app_execution_profile.agent_role_strategy}`);
431
+ }
420
432
  if (plan.execution_policy) {
421
433
  lines.push(`Execution: ${plan.execution_policy.normal_run}; default cycle timeout ${plan.execution_policy.default_cycle_timeout_minutes} minutes`);
422
434
  if (plan.execution_policy.default_max_cycles)
@@ -468,6 +480,8 @@ export function researchPlanMarkdown(plan) {
468
480
  lines.push('## Web Research Policy');
469
481
  lines.push(`Mode: ${plan.web_research_policy.mode}`);
470
482
  lines.push(`Requirement: ${plan.web_research_policy.requirement}`);
483
+ if (plan.web_research_policy.source_tool_routing)
484
+ lines.push(`Source tool routing: ${plan.web_research_policy.source_tool_routing.mode}`);
471
485
  for (const querySet of plan.web_research_policy.query_sets || [])
472
486
  lines.push(`- query set: ${querySet}`);
473
487
  if (plan.web_research_policy.skill_creator?.artifact)
@@ -537,7 +551,9 @@ export function countGeniusOpinionSummaries(text = '') {
537
551
  }).length;
538
552
  }
539
553
  export async function writeResearchPlan(dir, prompt, opts = {}) {
540
- const plan = createResearchPlan(prompt, opts);
554
+ const root = opts.root || missionRootFromDir(String(dir || ''));
555
+ const executionProfile = opts.executionProfile || (root ? await resolveCodexAppExecutionProfile({ root }).catch(() => null) : null);
556
+ const plan = createResearchPlan(prompt, { ...opts, executionProfile });
541
557
  const noveltyLedger = {
542
558
  schema_version: 1,
543
559
  entries: [],
@@ -553,6 +569,8 @@ export async function writeResearchPlan(dir, prompt, opts = {}) {
553
569
  const experimentPlan = defaultExperimentPlan(plan);
554
570
  const replicationPack = defaultReplicationPack(plan);
555
571
  await writeJsonAtomic(path.join(dir, 'research-plan.json'), plan);
572
+ if (executionProfile)
573
+ await writeJsonAtomic(path.join(dir, 'research', 'execution-profile.json'), executionProfile).catch(() => undefined);
556
574
  await writeTextAtomic(path.join(dir, 'research-plan.md'), researchPlanMarkdown(plan));
557
575
  await writeTextAtomic(path.join(dir, RESEARCH_SOURCE_SKILL_ARTIFACT), researchSourceSkillMarkdown(plan));
558
576
  await writeResearchQualityContract(dir, plan.quality_contract);
@@ -575,6 +593,22 @@ export async function writeResearchPlan(dir, prompt, opts = {}) {
575
593
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.plan.created', depth: plan.depth });
576
594
  return plan;
577
595
  }
596
+ function missionRootFromDir(dir) {
597
+ const normalized = path.resolve(String(dir || ''));
598
+ const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
599
+ const idx = normalized.indexOf(marker);
600
+ return idx > 0 ? normalized.slice(0, idx) : null;
601
+ }
602
+ function compactExecutionProfile(profile) {
603
+ return profile ? {
604
+ mode: profile.mode || 'unknown',
605
+ agent_role_strategy: profile.agent_role_strategy || 'message-role',
606
+ hooks_approval_required: profile.hooks_approval_required === true,
607
+ hook_approval_state: profile.hook_approval_state || 'unknown',
608
+ plugin_mcp_inventory_ready: profile.plugin_mcp_inventory_ready === true,
609
+ artifact_path: profile.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json'
610
+ } : null;
611
+ }
578
612
  export async function writeResearchNativeAgentLedger(dir, plan, opts = {}) {
579
613
  const missionId = opts.missionId || plan?.mission_id;
580
614
  if (!missionId)
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '3.1.4';
1
+ export const PACKAGE_VERSION = '3.1.5';
2
2
  //# sourceMappingURL=version.js.map
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import readline from 'node:readline';
3
2
  export const HOMEBREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
4
3
  export function resolveHomebrewInstallPolicy(input = {}) {
@@ -0,0 +1,45 @@
1
+ import { nowIso } from '../fsx.js';
2
+ export function isZellijSelfHealResult(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4
+ return false;
5
+ const row = value;
6
+ return row.schema === 'sks.zellij-self-heal.v1'
7
+ && typeof row.ok === 'boolean'
8
+ && typeof row.requested_by === 'string'
9
+ && typeof row.strategy === 'string'
10
+ && row.before !== null
11
+ && typeof row.before === 'object'
12
+ && row.after !== null
13
+ && typeof row.after === 'object'
14
+ && Array.isArray(row.blockers)
15
+ && Array.isArray(row.warnings);
16
+ }
17
+ export function normalizeZellijSelfHealResult(value) {
18
+ if (isZellijSelfHealResult(value)) {
19
+ return {
20
+ ...value,
21
+ dry_run: value.dry_run === true,
22
+ planned_mutations: Array.isArray(value.planned_mutations) ? value.planned_mutations : []
23
+ };
24
+ }
25
+ return {
26
+ schema: 'sks.zellij-self-heal.v1',
27
+ ok: false,
28
+ requested_by: 'setup',
29
+ fix_requested: false,
30
+ auto_approved: false,
31
+ install_homebrew_allowed: false,
32
+ dry_run: false,
33
+ planned_mutations: [],
34
+ before: { status: 'unknown', version: null, bin: null },
35
+ latest_version: null,
36
+ strategy: 'failed',
37
+ command: null,
38
+ after: { status: 'unknown', version: null, bin: null },
39
+ mutation_guard_artifact: null,
40
+ homebrew: { present: false, bin: null, install_attempted: false, install_allowed: false },
41
+ blockers: ['invalid_zellij_self_heal_result'],
42
+ warnings: [`normalized_at:${nowIso()}`]
43
+ };
44
+ }
45
+ //# sourceMappingURL=zellij-self-heal-types.js.map
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import fs from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
  import { ensureDir, nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
@@ -12,6 +11,7 @@ export async function repairZellijForSks(input) {
12
11
  const root = path.resolve(input.root || process.cwd());
13
12
  const env = input.env || process.env;
14
13
  const autoApproved = input.autoApprove === true;
14
+ const dryRun = input.dryRun === true;
15
15
  const beforeReport = await capabilitySnapshot(root, env, 'before');
16
16
  const before = compactCapability(beforeReport);
17
17
  const latest = await latestZellijVersion(env);
@@ -41,6 +41,9 @@ export async function repairZellijForSks(input) {
41
41
  if (input.fixRequested !== true) {
42
42
  return manualResult(root, input, env, before, latest, brew, 'fix_not_requested');
43
43
  }
44
+ if (dryRun) {
45
+ return dryRunResult(root, input, env, before, latest, brew, mutationArtifact);
46
+ }
44
47
  if (!autoApproved && input.interactive !== true) {
45
48
  return input.allowHeadlessFallback === true
46
49
  ? headlessResult(root, input, before, latest, brew, 'noninteractive_without_auto_approval')
@@ -183,10 +186,10 @@ async function capabilitySnapshot(root, env, phase, fallbackVersion) {
183
186
  require_zellij: false,
184
187
  min_version: ZELLIJ_MIN_VERSION,
185
188
  version: null,
186
- bin: null,
189
+ bin: 'zellij',
187
190
  command: ['zellij', '--version'],
188
191
  docs_evidence: [],
189
- blockers: [`zellij_capability_check_failed:${tail(err?.message || String(err))}`],
192
+ blockers: [`zellij_capability_check_failed:${tail(errorMessage(err))}`],
190
193
  warnings: [],
191
194
  operator_actions: ['Resolve the Zellij capability check failure, then rerun `sks doctor --fix --yes`.']
192
195
  }));
@@ -261,7 +264,7 @@ async function runZellijBrew(root, env, brewBin, args, command) {
261
264
  env,
262
265
  timeoutMs: 180000,
263
266
  maxOutputBytes: 256 * 1024
264
- }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
267
+ }).catch((err) => ({ code: 1, stdout: '', stderr: errorMessage(err) }));
265
268
  }
266
269
  async function runHomebrewInstall(root, env) {
267
270
  if (env.SKS_ZELLIJ_SELF_HEAL_FAKE_RUN === '1') {
@@ -281,7 +284,7 @@ async function runHomebrewInstall(root, env) {
281
284
  env,
282
285
  timeoutMs: 600000,
283
286
  maxOutputBytes: 256 * 1024
284
- }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
287
+ }).catch((err) => ({ code: 1, stdout: '', stderr: errorMessage(err) }));
285
288
  }
286
289
  async function appendFakeBrewLog(env, args) {
287
290
  if (!env.SKS_FAKE_BREW_LOG)
@@ -289,6 +292,56 @@ async function appendFakeBrewLog(env, args) {
289
292
  await ensureDir(path.dirname(env.SKS_FAKE_BREW_LOG));
290
293
  await fs.appendFile(env.SKS_FAKE_BREW_LOG, `${args.join(' ')}\n`, 'utf8');
291
294
  }
295
+ async function dryRunResult(root, input, env, before, latest, brew, mutationArtifact) {
296
+ const policy = !brew.present
297
+ ? resolveHomebrewInstallPolicy({
298
+ env,
299
+ installHomebrew: input.installHomebrew === true,
300
+ autoApprove: input.autoApprove === true,
301
+ interactiveAccepted: false
302
+ })
303
+ : null;
304
+ if (!brew.present && policy?.allowed !== true) {
305
+ return input.allowHeadlessFallback === true
306
+ ? headlessResult(root, input, before, latest, brew, policy?.blockers[0] || 'homebrew_missing')
307
+ : manualResult(root, input, env, before, latest, brew, policy?.blockers[0] || 'homebrew_missing');
308
+ }
309
+ const planned = [];
310
+ if (!brew.present) {
311
+ planned.push({
312
+ command: HOMEBREW_INSTALL_COMMAND,
313
+ reason: 'homebrew_missing_for_zellij_repair'
314
+ });
315
+ }
316
+ const install = before.status === 'missing';
317
+ const zellijCommand = install ? 'brew install zellij' : 'brew upgrade zellij';
318
+ planned.push({
319
+ command: zellijCommand,
320
+ reason: install ? 'zellij_missing' : 'zellij_too_old_or_stale'
321
+ });
322
+ const strategy = !brew.present ? 'brew-install-homebrew-then-zellij'
323
+ : install ? 'brew-install-zellij'
324
+ : 'brew-upgrade-zellij';
325
+ return persistSelfHeal(root, input.missionDir, {
326
+ schema: 'sks.zellij-self-heal.v1',
327
+ ok: true,
328
+ requested_by: input.requestedBy,
329
+ fix_requested: true,
330
+ auto_approved: input.autoApprove === true,
331
+ install_homebrew_allowed: policy?.allowed === true,
332
+ dry_run: true,
333
+ planned_mutations: planned,
334
+ before,
335
+ latest_version: latest,
336
+ strategy,
337
+ command: planned.map((row) => row.command).join(' && '),
338
+ after: before,
339
+ mutation_guard_artifact: `${mutationArtifact}#planned`,
340
+ homebrew: { present: brew.present, bin: brew.bin, install_attempted: false, install_allowed: policy?.allowed === true },
341
+ blockers: [],
342
+ warnings: ['dry_run_no_mutation_performed']
343
+ });
344
+ }
292
345
  async function manualResult(root, input, env, before, latest, brew, reason) {
293
346
  const command = brew.present ? 'sks doctor --fix --yes' : 'sks doctor --fix --install-homebrew --yes';
294
347
  return persistSelfHeal(root, input.missionDir, {
@@ -329,10 +382,15 @@ async function headlessResult(root, input, before, latest, brew, reason) {
329
382
  });
330
383
  }
331
384
  async function persistSelfHeal(root, missionDir, result) {
332
- await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-self-heal.json'), result).catch(() => undefined);
385
+ const normalized = {
386
+ ...result,
387
+ dry_run: result.dry_run === true,
388
+ planned_mutations: result.planned_mutations || []
389
+ };
390
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-self-heal.json'), normalized).catch(() => undefined);
333
391
  if (missionDir)
334
- await writeJsonAtomic(path.join(missionDir, 'zellij-self-heal.json'), result).catch(() => undefined);
335
- return result;
392
+ await writeJsonAtomic(path.join(missionDir, 'zellij-self-heal.json'), normalized).catch(() => undefined);
393
+ return normalized;
336
394
  }
337
395
  async function askZellijRepairAllowed(question) {
338
396
  if (!(process.stdin.isTTY && process.stdout.isTTY))
@@ -347,6 +405,9 @@ async function askZellijRepairAllowed(question) {
347
405
  rl.close();
348
406
  }
349
407
  }
408
+ function errorMessage(err) {
409
+ return err instanceof Error ? err.message : String(err);
410
+ }
350
411
  function tail(value, limit = 1000) {
351
412
  return String(value || '').replace(/\s+/g, ' ').trim().slice(-limit);
352
413
  }
@@ -232,17 +232,25 @@ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
232
232
  interactive: mode === 'interactive-prompt',
233
233
  installHomebrew: opts.installHomebrew === true || list.includes('--install-homebrew'),
234
234
  allowHeadlessFallback: opts.allowHeadlessFallback === true,
235
+ dryRun: opts.dryRun === true || list.includes('--dry-run'),
235
236
  missionDir: opts.missionDir || null,
236
237
  env
237
238
  });
238
239
  if (repaired.strategy === 'headless-fallback')
239
240
  console.log('Zellij repair: headless fallback selected (live_panes=false).');
241
+ else if (repaired.dry_run)
242
+ console.log(`Zellij repair: dry_run planned ${repaired.command || 'none'}`);
240
243
  else if (repaired.ok && repaired.command)
241
244
  console.log(`Zellij repair: ${repaired.strategy} via ${repaired.command}`);
242
245
  else if (!repaired.ok)
243
246
  console.log(`Zellij repair required. Run: ${repaired.command || notice.upgrade_command}`);
247
+ const repairedStatus = repaired.strategy === 'headless-fallback' ? 'headless_fallback'
248
+ : !repaired.ok || repaired.strategy === 'manual-required' ? 'repair_required'
249
+ : repaired.strategy === 'brew-upgrade-zellij' ? 'upgraded'
250
+ : repaired.strategy === 'none-current' ? 'noop'
251
+ : 'installed';
244
252
  return {
245
- status: repaired.ok ? (repaired.strategy === 'headless-fallback' ? 'missing' : 'installed') : 'manual_required',
253
+ status: repairedStatus,
246
254
  current: repaired.after.version || repaired.before.version,
247
255
  latest: repaired.latest_version,
248
256
  command: repaired.command,