sneakoscope 5.5.2 → 5.6.0

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 (46) hide show
  1. package/README.md +4 -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/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +7 -1
  7. package/dist/cli/router.js +28 -0
  8. package/dist/commands/doctor.js +65 -3
  9. package/dist/config/skills-manifest.json +58 -58
  10. package/dist/core/agent-bridge/agent-manifest.js +64 -0
  11. package/dist/core/agent-bridge/agent-mode.js +29 -0
  12. package/dist/core/agent-bridge/mcp-server.js +156 -0
  13. package/dist/core/agents/agent-orchestrator.js +1 -11
  14. package/dist/core/commands/agent-bridge-command.js +86 -0
  15. package/dist/core/commands/image-ux-review-command.js +6 -6
  16. package/dist/core/commands/loop-command.js +2 -6
  17. package/dist/core/commands/mad-sks-command.js +3 -3
  18. package/dist/core/commands/mcp-server-command.js +13 -0
  19. package/dist/core/commands/naruto-command.js +4 -4
  20. package/dist/core/commands/ppt-command.js +6 -3
  21. package/dist/core/commands/qa-loop-command.js +64 -26
  22. package/dist/core/commands/team-command.js +1 -1
  23. package/dist/core/commands/wiki-command.js +71 -3
  24. package/dist/core/config/secret-preservation.js +43 -1
  25. package/dist/core/doctor/browser-use-repair.js +149 -0
  26. package/dist/core/doctor/computer-use-repair.js +166 -0
  27. package/dist/core/doctor/imagegen-repair.js +9 -0
  28. package/dist/core/doctor/mcp-transport-collision-repair.js +129 -0
  29. package/dist/core/doctor/supabase-mcp-repair.js +85 -16
  30. package/dist/core/feature-fixtures.js +8 -0
  31. package/dist/core/feature-registry.js +21 -0
  32. package/dist/core/fsx.js +1 -1
  33. package/dist/core/mad-db/mad-db-capability.js +1 -1
  34. package/dist/core/mission.js +19 -3
  35. package/dist/core/naruto/naruto-real-worker-child.js +5 -4
  36. package/dist/core/naruto/naruto-real-worker-runtime.js +5 -4
  37. package/dist/core/naruto/normalize-worker-prompt-text.js +12 -0
  38. package/dist/core/routes.js +3 -1
  39. package/dist/core/triwiki/code-index-scanner.js +313 -0
  40. package/dist/core/triwiki/code-pack.js +169 -0
  41. package/dist/core/triwiki/triwiki-cache-key.js +24 -2
  42. package/dist/core/triwiki-attention.js +34 -5
  43. package/dist/core/update-check.js +50 -0
  44. package/dist/core/version.js +1 -1
  45. package/dist/scripts/doctor-imagegen-repair-check.js +6 -1
  46. package/package.json +2 -2
@@ -24,6 +24,7 @@ import { writeCodexModelEffortCapabilityArtifact } from '../codex-control/codex-
24
24
  import { discoverImageArtifactsInDir, writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
25
25
  import { pluginAppTemplatePolicy } from '../codex-plugins/codex-plugin-json.js';
26
26
  import { confirmQaLoopAppHandoff } from '../qa-loop/qa-loop-app-handoff-confirmation.js';
27
+ import { emitStreamEvent } from '../agent-bridge/agent-mode.js';
27
28
  import fsp from 'node:fs/promises';
28
29
  export async function qaLoopCommand(sub, args = []) {
29
30
  const known = new Set(['prepare', 'answer', 'run', 'status', 'app-confirm', 'help', '--help', '-h']);
@@ -44,7 +45,7 @@ export async function qaLoopCommand(sub, args = []) {
44
45
  Usage:
45
46
  sks qa-loop prepare "target"
46
47
  sks qa-loop answer <mission-id|latest> <answers.json>
47
- sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N] [--surface auto|codex_in_app_browser|codex_chrome_extension|codex_computer_use] [--report-only] [--app-handoff] [--app-handoff-required] [--app-handoff-launch] [--app-handoff-artifact-only]
48
+ sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N] [--surface auto|codex_in_app_browser|codex_chrome_extension|codex_computer_use] [--report-only] [--app-handoff] [--app-handoff-required] [--app-handoff-launch] [--app-handoff-artifact-only] [--stream]
48
49
  sks qa-loop app-confirm <mission-id|latest> --verdict pass|fail --notes "..."
49
50
  sks qa-loop status <mission-id|latest> [--desktop]
50
51
  `);
@@ -124,7 +125,10 @@ async function qaLoopRun(args) {
124
125
  const id = await resolveMissionId(root, args[0]);
125
126
  if (!id)
126
127
  throw new Error('Usage: sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]');
128
+ const stream = flag(args, '--stream');
127
129
  const { dir, mission } = await loadMission(root, id);
130
+ if (stream)
131
+ emitStreamEvent('start', { schema: 'sks.qa-loop-run.v1', mission_id: id });
128
132
  const contractPath = path.join(dir, 'decision-contract.json');
129
133
  if (!(await exists(contractPath)))
130
134
  throw new Error('QA-LOOP cannot run: decision-contract.json is missing.');
@@ -149,6 +153,8 @@ async function qaLoopRun(args) {
149
153
  console.error('QA-LOOP cannot run: SKS safety scan found unsafe project data-tool configuration.');
150
154
  console.error(JSON.stringify(safetyScan.findings, null, 2));
151
155
  process.exitCode = 2;
156
+ if (stream)
157
+ emitStreamEvent('result', { schema: 'sks.qa-loop-run.v1', ok: false, mission_id: id, status: 'blocked', blocker: 'db_safety_scan_failed', findings: safetyScan.findings });
152
158
  return;
153
159
  }
154
160
  const fallbackCycles = Number.parseInt(contract.answers?.MAX_QA_CYCLES, 10) || DEFAULT_QA_MAX_CYCLES;
@@ -260,8 +266,11 @@ async function qaLoopRun(args) {
260
266
  if (appHandoffRequired && appHandoff && appHandoff.ok !== true) {
261
267
  await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: nextGate, artifacts: ['qa-gate.json', 'qa-ledger.json', reportFile, 'qa-loop/app-handoff.json', 'completion-proof.json'], statusHint: 'blocked', blockers: nextGate.blockers, command: { cmd: `sks qa-loop run ${id} --app-handoff-required`, status: 2 } });
262
268
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_DESKTOP_APP_HANDOFF', questions_allowed: true });
269
+ const handoffResult = { schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked_for_desktop_review', mission_id: id, app_handoff: appHandoff, gate: nextGate };
270
+ if (stream)
271
+ emitStreamEvent('result', handoffResult);
263
272
  if (flag(args, '--json'))
264
- return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked_for_desktop_review', mission_id: id, app_handoff: appHandoff, gate: nextGate }, null, 2));
273
+ return console.log(JSON.stringify(handoffResult, null, 2));
265
274
  console.error('QA-LOOP blocked: Codex Desktop /app handoff is required but unavailable or still pending.');
266
275
  process.exitCode = 2;
267
276
  return;
@@ -296,8 +305,11 @@ async function qaLoopRun(args) {
296
305
  await writeJsonAtomic(path.join(dir, 'qa-gate.json'), blockedGate);
297
306
  await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: blockedGate, artifacts: ['qa-gate.json', 'qa-ledger.json', reportFile, 'completion-proof.json'], statusHint: 'blocked', blockers: blockedGate.blockers, command: { cmd: `sks qa-loop run ${id}`, status: 2 } });
298
307
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_CHROME_EXTENSION_SETUP_REQUIRED', questions_allowed: true });
308
+ const chromeBlockedResult = { schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked', blocker: 'codex_chrome_extension_setup_required', mission_id: id, chrome_extension: chrome, gate: blockedGate };
309
+ if (stream)
310
+ emitStreamEvent('result', chromeBlockedResult);
299
311
  if (flag(args, '--json'))
300
- return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked', blocker: 'codex_chrome_extension_setup_required', mission_id: id, chrome_extension: chrome, gate: blockedGate }, null, 2));
312
+ return console.log(JSON.stringify(chromeBlockedResult, null, 2));
301
313
  console.error('QA-LOOP blocked: this journey was routed to @Chrome, but the Codex Chrome Extension is not connected. Install/enable it, then resume.');
302
314
  console.error(chrome.docs_url);
303
315
  process.exitCode = 2;
@@ -306,11 +318,15 @@ async function qaLoopRun(args) {
306
318
  }
307
319
  await setCurrent(root, { mission_id: id, route: 'QALoop', route_command: '$QA-LOOP', mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS', questions_allowed: false, stop_gate: 'qa-gate.json', reasoning_effort: 'high', reasoning_profile: 'sks-logic-high', reasoning_temporary: true });
308
320
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.run.started', maxCycles, mock });
321
+ if (stream)
322
+ emitStreamEvent('progress', { mission_id: id, type: 'qaloop.run.started', max_cycles: maxCycles, mock });
309
323
  const nativeAgentPlan = await readJson(path.join(dir, 'qa-agent-plan.json'), null);
310
324
  const nativeRoster = requestedAgents === 3 ? nativeAgentPlan : null;
311
325
  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' } });
312
326
  await writeJsonAtomic(path.join(dir, 'qa-native-agent-run.json'), nativeAgentRun);
313
327
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
328
+ if (stream)
329
+ emitStreamEvent('progress', { mission_id: id, type: 'qaloop.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
314
330
  if (flag(args, '--native-proof-only')) {
315
331
  const proofOnlyGate = {
316
332
  schema: 'sks.qa-native-proof-only-gate.v1',
@@ -319,17 +335,20 @@ async function qaLoopRun(args) {
319
335
  proof_status: nativeAgentRun.proof?.status || null,
320
336
  blockers: nativeAgentRun.proof?.blockers || []
321
337
  };
338
+ const proofOnlyResult = {
339
+ schema: 'sks.qa-loop-run.v1',
340
+ ok: proofOnlyGate.ok,
341
+ status: proofOnlyGate.ok ? 'native_proof_ready' : 'blocked',
342
+ mission_id: id,
343
+ gate: proofOnlyGate,
344
+ proof: nativeAgentRun.proof,
345
+ native_agent_run: nativeAgentRun,
346
+ native_proof_only: true
347
+ };
348
+ if (stream)
349
+ emitStreamEvent('result', proofOnlyResult);
322
350
  if (flag(args, '--json'))
323
- return console.log(JSON.stringify({
324
- schema: 'sks.qa-loop-run.v1',
325
- ok: proofOnlyGate.ok,
326
- status: proofOnlyGate.ok ? 'native_proof_ready' : 'blocked',
327
- mission_id: id,
328
- gate: proofOnlyGate,
329
- proof: nativeAgentRun.proof,
330
- native_agent_run: nativeAgentRun,
331
- native_proof_only: true
332
- }, null, 2));
351
+ return console.log(JSON.stringify(proofOnlyResult, null, 2));
333
352
  console.log(`QA-LOOP native proof ready: ${id}`);
334
353
  return;
335
354
  }
@@ -366,18 +385,21 @@ async function qaLoopRun(args) {
366
385
  command: { cmd: `sks qa-loop run ${id} --mock`, status: 0 }
367
386
  });
368
387
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: gate.passed ? 'QALOOP_DONE' : 'QALOOP_PAUSED', questions_allowed: true });
388
+ const mockResult = {
389
+ schema: 'sks.qa-loop-run.v1',
390
+ ok: proof.ok && (!needsVisual || gate.passed === true),
391
+ status: needsVisual && gate.passed !== true ? 'verified_partial_mock_no_live_web_evidence' : (gate.passed ? 'passed' : 'blocked'),
392
+ mission_id: id,
393
+ gate,
394
+ proof: proof.validation,
395
+ native_agent_run: nativeAgentRun,
396
+ mock_only: true,
397
+ live_web_evidence: !needsVisual || gate.passed === true
398
+ };
399
+ if (stream)
400
+ emitStreamEvent('result', mockResult);
369
401
  if (flag(args, '--json'))
370
- return console.log(JSON.stringify({
371
- schema: 'sks.qa-loop-run.v1',
372
- ok: proof.ok && (!needsVisual || gate.passed === true),
373
- status: needsVisual && gate.passed !== true ? 'verified_partial_mock_no_live_web_evidence' : (gate.passed ? 'passed' : 'blocked'),
374
- mission_id: id,
375
- gate,
376
- proof: proof.validation,
377
- native_agent_run: nativeAgentRun,
378
- mock_only: true,
379
- live_web_evidence: !needsVisual || gate.passed === true
380
- }, null, 2));
402
+ return console.log(JSON.stringify(mockResult, null, 2));
381
403
  console.log(`Mock QA-LOOP done: ${id}`);
382
404
  console.log(`Gate: ${gate.passed ? 'passed' : 'blocked'}`);
383
405
  return;
@@ -386,6 +408,8 @@ async function qaLoopRun(args) {
386
408
  await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: await readJson(path.join(dir, 'qa-gate.json'), null), artifacts: ['agents/agent-proof-evidence.json', 'qa-native-agent-run.json', 'completion-proof.json'], statusHint: 'blocked', blockers: nativeAgentRun.proof?.blockers || ['native_agent_backend_blocked'], command: { cmd: `sks qa-loop run ${id}`, status: 2 } });
387
409
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_NATIVE_AGENTS', questions_allowed: true });
388
410
  process.exitCode = 2;
411
+ if (stream)
412
+ emitStreamEvent('result', { schema: 'sks.qa-loop-run.v1', ok: false, mission_id: id, status: 'blocked', blocker: 'native_agent_backend_blocked', blockers: nativeAgentRun.proof?.blockers || ['native_agent_backend_blocked'] });
389
413
  return;
390
414
  }
391
415
  const codex = await getCodexInfo();
@@ -394,6 +418,8 @@ async function qaLoopRun(args) {
394
418
  await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: await readJson(path.join(dir, 'qa-gate.json'), null), artifacts: ['agents/agent-proof-evidence.json', 'qa-native-agent-run.json', 'completion-proof.json'], statusHint: 'blocked', blockers: ['codex_cli_missing'], command: { cmd: `sks qa-loop run ${id}`, status: 2 } });
395
419
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_REAL_RUN_REQUIRED', questions_allowed: true });
396
420
  process.exitCode = 2;
421
+ if (stream)
422
+ emitStreamEvent('result', { schema: 'sks.qa-loop-run.v1', ok: false, mission_id: id, status: 'blocked', blocker: 'codex_cli_missing' });
397
423
  return;
398
424
  }
399
425
  let last = '';
@@ -402,11 +428,15 @@ async function qaLoopRun(args) {
402
428
  const outputFile = path.join(cycleDir, 'final.md');
403
429
  const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile, imagePathContract: imagePathContract?.contract || null, appHandoff, executionProfile });
404
430
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.cycle.start', cycle });
431
+ if (stream)
432
+ emitStreamEvent('progress', { mission_id: id, type: 'qaloop.cycle.start', cycle, max_cycles: maxCycles });
405
433
  const result = await runCodexExec({ root, prompt, outputFile, json: true, profile, logDir: cycleDir });
406
434
  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 });
407
435
  last = await safeReadTextFile(fsp, outputFile, result.stdout || result.stderr || '');
408
436
  if (containsUserQuestion(last)) {
409
437
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.guard.question_blocked', cycle });
438
+ if (stream)
439
+ emitStreamEvent('progress', { mission_id: id, type: 'qaloop.guard.question_blocked', cycle });
410
440
  last = `${last}\n\n${noQuestionContinuationReason()}`;
411
441
  continue;
412
442
  }
@@ -415,18 +445,26 @@ async function qaLoopRun(args) {
415
445
  const proof = await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: gate.gate || gate, artifacts: ['agents/agent-proof-evidence.json', 'qa-native-agent-run.json', 'qa-gate.json', 'qa-ledger.json', reportFile, 'completion-proof.json'], visual: uiRequired, command: { cmd: `sks qa-loop run ${id}`, status: 0 } });
416
446
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_DONE', questions_allowed: true });
417
447
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.done', cycle });
448
+ const doneResult = { schema: 'sks.qa-loop-run.v1', ok: proof.ok, mission_id: id, gate, proof: proof.validation };
449
+ if (stream)
450
+ emitStreamEvent('result', doneResult);
418
451
  if (flag(args, '--json'))
419
- return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: proof.ok, mission_id: id, gate, proof: proof.validation }, null, 2));
452
+ return console.log(JSON.stringify(doneResult, null, 2));
420
453
  console.log(`QA-LOOP done: ${id}`);
421
454
  return;
422
455
  }
423
456
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.cycle.continue', cycle, reasons: gate.reasons });
457
+ if (stream)
458
+ emitStreamEvent('progress', { mission_id: id, type: 'qaloop.cycle.continue', cycle, reasons: gate.reasons });
424
459
  }
425
460
  const gate = await evaluateQaGate(dir);
426
461
  const proof = await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: gate.gate || gate, artifacts: ['agents/agent-proof-evidence.json', 'qa-native-agent-run.json', 'qa-gate.json', 'qa-ledger.json', reportFile, 'completion-proof.json'], mock: false, statusHint: 'blocked', reason: 'max_cycles', command: { cmd: `sks qa-loop run ${id}`, status: 2 } });
427
462
  await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_PAUSED_MAX_CYCLES', questions_allowed: true });
463
+ const pausedResult = { schema: 'sks.qa-loop-run.v1', ok: false, mission_id: id, gate, proof: proof.validation };
464
+ if (stream)
465
+ emitStreamEvent('result', pausedResult);
428
466
  if (flag(args, '--json'))
429
- return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: false, mission_id: id, gate, proof: proof.validation }, null, 2));
467
+ return console.log(JSON.stringify(pausedResult, null, 2));
430
468
  console.log(`QA-LOOP paused after max cycles: ${id}`);
431
469
  }
432
470
  async function qaLoopStatus(args) {
@@ -19,7 +19,7 @@ async function redirectTeamCreateToNaruto(args = []) {
19
19
  const result = jsonRequested
20
20
  ? await withSuppressedConsoleLog(() => narutoCommand(narutoArgs))
21
21
  : await narutoCommand(narutoArgs);
22
- const missionId = result?.mission_id || await findLatestMission(root);
22
+ const missionId = result?.mission_id || await findLatestMission(root, { mode: 'naruto' });
23
23
  const nativeAgentRun = missionId ? await buildTeamNativeAgentCompatibility(root, missionId, result) : null;
24
24
  if (missionId) {
25
25
  await writeJsonAtomic(path.join(root, '.sneakoscope', 'missions', missionId, 'team-alias-to-naruto.json'), {
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { appendJsonlBounded, ensureDir, exists, formatBytes, nowIso, PACKAGE_VERSION, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
3
+ import { appendJsonlBounded, ensureDir, exists, formatBytes, nowIso, PACKAGE_VERSION, readJson, runProcess, sksRoot, writeJsonAtomic } from '../fsx.js';
4
4
  import { contextCapsule } from '../triwiki-attention.js';
5
5
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../wiki-coordinate.js';
6
6
  import { pruneWikiArtifacts } from '../retention.js';
@@ -19,6 +19,8 @@ import { wikiWrongnessCommand } from '../triwiki-wrongness/wrongness-cli.js';
19
19
  import { wrongnessContextForRoute } from '../triwiki-wrongness/wrongness-retrieval.js';
20
20
  import { recordImageWrongnessFromValidation } from '../triwiki-wrongness/image-wrongness.js';
21
21
  import { publishSharedMemory, rebuildSharedIndexes, sharedMemorySummary, validateSharedMemory } from '../git-hygiene/shared-memory-publish.js';
22
+ import { scanCodebaseIndex } from '../triwiki/code-index-scanner.js';
23
+ import { buildCodePack, validateCodePack, writeCodePackAtomic } from '../triwiki/code-pack.js';
22
24
  import { flag, positionalArgs, readFlagValue, readOption, resolveMissionId } from './command-utils.js';
23
25
  export async function wikiCommand(sub, args = []) {
24
26
  if (!sub || sub === 'help' || sub === '--help') {
@@ -108,6 +110,9 @@ export async function wikiCommand(sub, args = []) {
108
110
  console.log(`Memory summary rebuilt: ${result.ok ? 'ok' : 'blocked'}`);
109
111
  return;
110
112
  }
113
+ if (sub === 'refresh' && flag(args, '--code')) {
114
+ return wikiRefreshCode(args);
115
+ }
111
116
  if (sub === 'refresh') {
112
117
  const root = await sksRoot();
113
118
  const dryRun = flag(args, '--dry-run');
@@ -194,19 +199,68 @@ export async function wikiCommand(sub, args = []) {
194
199
  const target = positionalArgs(args)[0] || path.join(root, '.sneakoscope', 'wiki', 'context-pack.json');
195
200
  const pack = await readJson(path.resolve(target));
196
201
  const { result, trustAnchors } = wikiValidationResult(pack);
202
+ const codePack = await codePackFreshness(root);
197
203
  if (flag(args, '--json'))
198
- return console.log(JSON.stringify(result, null, 2));
204
+ return console.log(JSON.stringify({ ...result, code_pack: codePack }, null, 2));
199
205
  console.log(`Wiki coordinate index: ${result.ok ? 'ok' : 'failed'}`);
200
206
  console.log(`Anchors checked: ${result.checked}`);
201
207
  console.log(`Trust anchors: ${trustAnchors}/${result.checked}`);
202
208
  for (const issue of result.issues)
203
209
  console.log(`- ${issue.severity}: ${issue.id}${issue.anchor ? ` ${issue.anchor}` : ''}`);
210
+ console.log(`Code pack: ${codePack.status}${codePack.status === 'stale' ? ' — run `sks wiki refresh --code`' : ''}`);
204
211
  process.exitCode = result.ok ? 0 : 2;
205
212
  return;
206
213
  }
207
214
  console.error('Usage: sks wiki coords|pack|refresh|publish|rebuild-index|rebuild-summary|validate|validate-shared|wrongness|image-ingest|anchor-add|relation-add|image-validate|image-summary');
208
215
  process.exitCode = 1;
209
216
  }
217
+ async function wikiRefreshCode(args = []) {
218
+ const root = await sksRoot();
219
+ const index = await scanCodebaseIndex(root);
220
+ const tokenBudgetRaw = readFlagValue(args, '--token-budget', null);
221
+ const tokenBudget = tokenBudgetRaw ? Number.parseInt(String(tokenBudgetRaw), 10) : undefined;
222
+ const pack = tokenBudget && Number.isFinite(tokenBudget) ? buildCodePack(root, index, tokenBudget) : buildCodePack(root, index);
223
+ const validation = await validateCodePack(pack, root);
224
+ const written = validation.ok ? await writeCodePackAtomic(root, pack) : null;
225
+ const result = {
226
+ schema: 'sks.wiki-refresh-code.v1',
227
+ ok: validation.ok,
228
+ issues: validation.issues,
229
+ modules: index.modules.length,
230
+ truncated: index.truncated,
231
+ scanned_file_count: index.scanned_file_count,
232
+ entries: pack.entries.length,
233
+ token_cost: pack.total_token_cost,
234
+ token_budget: pack.token_budget,
235
+ written: written ? written.path : null
236
+ };
237
+ process.exitCode = validation.ok ? 0 : 2;
238
+ // Always note the counts/issues on stderr (even in --json mode, where stdout must
239
+ // stay pure JSON) so a blocked run is diagnosable from stderr_tail alone without
240
+ // needing to reproduce it separately in an isolated environment.
241
+ process.stderr.write(`wiki-refresh-code: root=${root} modules=${index.modules.length} entries=${pack.entries.length} token_cost=${pack.total_token_cost}/${pack.token_budget} issues=${JSON.stringify(validation.issues)}\n`);
242
+ if (flag(args, '--json'))
243
+ return console.log(JSON.stringify(result, null, 2));
244
+ console.log('Sneakoscope TriWiki Code Pack Refresh');
245
+ console.log(`Code pack refresh: ${validation.ok ? 'ok' : 'blocked'} (${pack.entries.length} entries from ${index.modules.length} modules${index.truncated ? ', truncated' : ''})`);
246
+ for (const issue of validation.issues)
247
+ console.log(`- ${issue}`);
248
+ }
249
+ /** Cheap freshness check: compares the code pack's recorded git HEAD sha (at
250
+ * generation time) against the current HEAD. Any uncertainty (no pack, no git,
251
+ * spawn failure) resolves to 'stale' rather than 'fresh', never overclaiming. */
252
+ async function codePackFreshness(root) {
253
+ const packPath = path.join(root, '.sneakoscope', 'wiki', 'code-pack.json');
254
+ if (!(await exists(packPath)))
255
+ return { status: 'missing', git_head_sha: null, pack_sha: null };
256
+ const pack = await readJson(packPath, null).catch(() => null);
257
+ const packSha = pack?.git_head_sha || null;
258
+ if (!packSha)
259
+ return { status: 'missing', git_head_sha: null, pack_sha: null };
260
+ const head = await runProcess('git', ['rev-parse', 'HEAD'], { cwd: root, timeoutMs: 5000 }).catch(() => null);
261
+ const currentSha = head && head.code === 0 ? head.stdout.trim() : null;
262
+ return { status: currentSha && currentSha === packSha ? 'fresh' : 'stale', git_head_sha: currentSha, pack_sha: packSha };
263
+ }
210
264
  function wikiRefreshFailureAnalysis(validationResult, gate = {}) {
211
265
  if (validationResult?.ok) {
212
266
  return {
@@ -327,6 +381,8 @@ async function wikiImageLinkProof(args = []) {
327
381
  export async function writeWikiContextPack(root, args = [], opts = {}) {
328
382
  const role = readFlagValue(args, '--role', 'worker');
329
383
  const maxAnchors = Number(readFlagValue(args, '--max-anchors', role.includes('verifier') ? 48 : 32));
384
+ const codePackData = await readJson(path.join(root, '.sneakoscope', 'wiki', 'code-pack.json'), null).catch(() => null);
385
+ const codePackEntries = Array.isArray(codePackData?.entries) ? codePackData.entries : [];
330
386
  const pack = contextCapsule({
331
387
  mission: { id: 'project-wiki', coord: { rgba: { r: 48, g: 132, b: 212, a: 240 } } },
332
388
  role,
@@ -334,11 +390,23 @@ export async function writeWikiContextPack(root, args = [], opts = {}) {
334
390
  claims: await projectWikiClaims(root),
335
391
  q4: { mode: 'project-continuity', package: PACKAGE_VERSION, hydrate: 'anchor-first' },
336
392
  q3: ['sks', 'llm-wiki', 'wiki-coordinate', 'gx', 'skills'],
337
- budget: { maxWikiAnchors: maxAnchors, includeTrustSummary: true }
393
+ budget: { maxWikiAnchors: maxAnchors, includeTrustSummary: true },
394
+ codePackEntries
338
395
  });
339
396
  const wrongnessContext = await wrongnessContextForRoute(root, { route: '$Wiki', limit: 12 });
397
+ // buildTriWikiAttention's use_first/hydrate_first can reference code: ids that were
398
+ // never routed through selectClaims (no fabricated RGBA coordinate for them — see
399
+ // codePackAttentionRows), so append their real summary text here or a recallpulse
400
+ // consumer resolving pack.claims by id would only see the bare id string.
401
+ const codePackClaimRows = codePackEntries.map((entry) => ({
402
+ id: entry.id,
403
+ text: `${entry.text}${Array.isArray(entry.citations) && entry.citations.length ? ` (source: ${entry.citations.map((c) => c?.path).filter(Boolean).join(', ')})` : ''}`,
404
+ source: 'code-pack',
405
+ trust: entry.trust_score
406
+ }));
340
407
  const enrichedPack = {
341
408
  ...pack,
409
+ claims: [...(pack.claims || []), ...codePackClaimRows],
342
410
  wrongness_context: wrongnessContext,
343
411
  q3: Array.from(new Set([...(pack.q3 || []), 'wrongness-memory', 'negative-evidence']))
344
412
  };
@@ -56,7 +56,7 @@ export async function withSecretPreservationGuard(root, operationName, fn) {
56
56
  }
57
57
  try {
58
58
  const after = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: afterPath });
59
- const changedOrMissing = changedOrMissingProtectedSecrets(before, after);
59
+ const changedOrMissing = await withoutSecretsPreservedInComments(changedOrMissingProtectedSecrets(before, after));
60
60
  let rollbackAttempted = false;
61
61
  let rollbackOk = false;
62
62
  let restoredKeysCount = 0;
@@ -118,6 +118,48 @@ export function changedOrMissingProtectedSecrets(before, after) {
118
118
  })
119
119
  .filter((item) => Boolean(item));
120
120
  }
121
+ // A repair that intentionally deactivates a config block (e.g. commenting out
122
+ // a colliding stdio MCP server) makes the secret disappear from the *live*
123
+ // scan even though its value is still sitting right there in a comment, fully
124
+ // recoverable. Without this check the guard treats that as an accidental loss,
125
+ // rolls the whole file back (undoing the repair), and still throws — so the
126
+ // repair can never stick. Only 'missing' entries are reconsidered here: a
127
+ // 'changed' entry means a live value actually differs and must still trip the
128
+ // guard. Never reads the pre-image value itself — only re-hashes whatever text
129
+ // is currently on disk, so raw_values_recorded stays false throughout.
130
+ async function withoutSecretsPreservedInComments(entries) {
131
+ const textBySource = new Map();
132
+ const kept = [];
133
+ for (const entry of entries) {
134
+ if (entry.reason !== 'missing' || !entry.before_sha256) {
135
+ kept.push(entry);
136
+ continue;
137
+ }
138
+ let text = textBySource.get(entry.source);
139
+ if (text === undefined) {
140
+ text = await fs.readFile(entry.source, 'utf8').catch(() => '');
141
+ textBySource.set(entry.source, text);
142
+ }
143
+ const preserved = readAllAssignmentValues(text, entry.key).some((value) => sha256(value) === entry.before_sha256);
144
+ if (!preserved)
145
+ kept.push(entry);
146
+ }
147
+ return kept;
148
+ }
149
+ /** Like readAssignment, but also matches the same key commented out (`# KEY = ...`)
150
+ * and returns every candidate value found, not just the first. */
151
+ function readAllAssignmentValues(text, key) {
152
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\./g, '\\s*\\.\\s*');
153
+ const re = new RegExp(`^\\s*#?\\s*(?:export\\s+)?${escaped}\\s*=\\s*(.+?)\\s*$`, 'gm');
154
+ const values = [];
155
+ let match;
156
+ while ((match = re.exec(text))) {
157
+ const raw = match[1]?.trim();
158
+ if (raw)
159
+ values.push(unquote(raw));
160
+ }
161
+ return values;
162
+ }
121
163
  function secretSources(root) {
122
164
  const home = process.env.HOME || os.homedir();
123
165
  return [
@@ -0,0 +1,149 @@
1
+ import path from 'node:path';
2
+ import { codexChromeExtensionStatus, CODEX_CHROME_EXTENSION_SETUP_DOCS_URL } from '../codex-app.js';
3
+ import { runDoctorCodexStartupRepair } from './doctor-codex-startup-repair.js';
4
+ import { ensureDir, nowIso, runProcess, which, writeJsonAtomic } from '../fsx.js';
5
+ export const DOCTOR_BROWSER_USE_REPAIR_SCHEMA = 'sks.doctor-browser-use-repair.v1';
6
+ export async function repairBrowserUse(input) {
7
+ const root = path.resolve(input.root || process.cwd());
8
+ const apply = input.apply === true;
9
+ const detect = input.detectChromeExtensionStatus || codexChromeExtensionStatus;
10
+ const repairNodeReplEnv = input.nodeReplRepair || runDoctorCodexStartupRepair;
11
+ const steps = [];
12
+ const before = await detect({}).catch((err) => ({
13
+ ok: false,
14
+ status: 'setup_required',
15
+ blockers: [messageOf(err)],
16
+ plugin: { installed: false, enabled: false },
17
+ required_flags: ['browser_use_external', 'plugins', 'apps'],
18
+ guidance: []
19
+ }));
20
+ const codexBin = input.codexBin || await which('codex').catch(() => null);
21
+ steps.push({
22
+ id: 'codex_cli_present',
23
+ ok: Boolean(codexBin),
24
+ attempted: false,
25
+ command: 'which codex',
26
+ blocker: codexBin ? null : 'codex_binary_missing'
27
+ });
28
+ const flagsNeedingEnable = ['browser_use_external', 'plugins'];
29
+ for (const flag of flagsNeedingEnable) {
30
+ const alreadyOk = before?.ok === true || (before?.blockers || []).every((b) => !b.includes(`${flag}_feature_missing`));
31
+ if (codexBin && apply) {
32
+ const enable = await runProcess(codexBin, ['features', 'enable', flag], {
33
+ timeoutMs: input.timeoutMs || 10000,
34
+ maxOutputBytes: 32 * 1024
35
+ }).catch((err) => ({ code: 1, stdout: '', stderr: messageOf(err) }));
36
+ steps.push({
37
+ id: `${flag}_feature_enable`,
38
+ ok: enable.code === 0,
39
+ attempted: true,
40
+ command: `${codexBin} features enable ${flag}`,
41
+ exit_code: enable.code,
42
+ stdout_tail: tail(enable.stdout),
43
+ stderr_tail: tail(enable.stderr),
44
+ blocker: enable.code === 0 ? null : 'codex_feature_enable_unsupported_or_failed'
45
+ });
46
+ }
47
+ else {
48
+ steps.push({
49
+ id: `${flag}_feature_enable`,
50
+ ok: alreadyOk,
51
+ attempted: false,
52
+ command: codexBin ? `${codexBin} features enable ${flag}` : `codex features enable ${flag}`,
53
+ blocker: alreadyOk ? null : apply ? 'codex_cli_missing' : 'doctor_fix_not_requested'
54
+ });
55
+ }
56
+ }
57
+ // codex plugin enable/install has no documented CLI subcommand for the bundled chrome plugin
58
+ // in this repo's evidence (only `plugin list --json` / app-server plugin RPCs are attested); do not guess one.
59
+ steps.push({
60
+ id: 'chrome_plugin_enable',
61
+ ok: false,
62
+ attempted: false,
63
+ command: null,
64
+ status: 'needs_more_info',
65
+ blocker: 'chrome_plugin_enable_cli_subcommand_unknown'
66
+ });
67
+ let nodeReplStep;
68
+ if (apply) {
69
+ const nodeReplResult = await repairNodeReplEnv({ root, fix: true }).catch((err) => ({
70
+ ok: false,
71
+ blockers: [messageOf(err)]
72
+ }));
73
+ nodeReplStep = {
74
+ id: 'node_repl_env_block_repair',
75
+ ok: nodeReplResult?.ok !== false,
76
+ attempted: true,
77
+ command: 'runDoctorCodexStartupRepair({ fix: true })',
78
+ blocker: nodeReplResult?.ok === false ? (nodeReplResult?.blockers?.[0] || 'node_repl_env_block_repair_incomplete') : null
79
+ };
80
+ }
81
+ else {
82
+ nodeReplStep = {
83
+ id: 'node_repl_env_block_repair',
84
+ ok: true,
85
+ attempted: false,
86
+ command: 'runDoctorCodexStartupRepair({ fix: true })',
87
+ blocker: 'doctor_fix_not_requested'
88
+ };
89
+ }
90
+ steps.push(nodeReplStep);
91
+ const after = await detect({}).catch((err) => ({
92
+ ok: false,
93
+ status: 'setup_required',
94
+ blockers: [messageOf(err)],
95
+ plugin: { installed: false, enabled: false },
96
+ required_flags: ['browser_use_external', 'plugins', 'apps'],
97
+ guidance: []
98
+ }));
99
+ const extensionMissing = (after?.blockers || []).some((b) => b.startsWith('chrome_extension_plugin_missing')
100
+ || b.startsWith('chrome_extension_plugin_not_installed')
101
+ || b.startsWith('chrome_extension_plugin_cache_only_unverified'));
102
+ const blockers = [...(after?.blockers || [])];
103
+ if (extensionMissing && !blockers.includes('chrome_extension_manual_install_required')) {
104
+ blockers.push('chrome_extension_manual_install_required');
105
+ }
106
+ const recovered = after?.ok === true;
107
+ const nextActions = recovered ? [] : [
108
+ 'Open the Codex Desktop app and check its settings for the Chrome/Browser Use plugin entry (exact menu wording may vary by version; check Codex app settings generically if unsure).',
109
+ 'If a Chrome extension install/enable action is offered from within Codex App settings, follow it there rather than the Chrome Web Store directly, since Codex App is the source of truth for what "ready" means for this feature.',
110
+ `If no in-app action is available, consult the setup docs: ${CODEX_CHROME_EXTENSION_SETUP_DOCS_URL}`,
111
+ 'After installing/enabling the extension, tell SKS it is installed and rerun this repair to re-detect.',
112
+ 'Verify with: codex features list | rg "browser_use_external|plugins|apps"'
113
+ ];
114
+ let report = {
115
+ schema: DOCTOR_BROWSER_USE_REPAIR_SCHEMA,
116
+ generated_at: nowIso(),
117
+ ok: recovered,
118
+ attempted: before?.ok !== true,
119
+ apply,
120
+ recovered,
121
+ before,
122
+ after,
123
+ steps,
124
+ blockers: [...new Set(blockers)],
125
+ next_actions: nextActions,
126
+ manual_actions: nextActions,
127
+ docs_url: CODEX_CHROME_EXTENSION_SETUP_DOCS_URL
128
+ };
129
+ if (input.reportPath !== null) {
130
+ const reportPath = input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-browser-use-repair.json');
131
+ try {
132
+ await ensureDir(path.dirname(reportPath));
133
+ await writeJsonAtomic(reportPath, report);
134
+ report = { ...report, report_path: reportPath };
135
+ }
136
+ catch (err) {
137
+ report = { ...report, report_write_failed: true, report_write_error: messageOf(err) };
138
+ }
139
+ }
140
+ return report;
141
+ }
142
+ function tail(value, max = 2000) {
143
+ const text = String(value || '');
144
+ return text.length > max ? text.slice(-max) : text;
145
+ }
146
+ function messageOf(err) {
147
+ return err instanceof Error ? err.message : String(err);
148
+ }
149
+ //# sourceMappingURL=browser-use-repair.js.map