sneakoscope 5.5.3 → 5.6.1
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.
- package/README.md +4 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +7 -1
- package/dist/cli/router.js +28 -0
- package/dist/commands/doctor.js +65 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agent-bridge/agent-manifest.js +64 -0
- package/dist/core/agent-bridge/agent-mode.js +29 -0
- package/dist/core/agent-bridge/mcp-server.js +156 -0
- package/dist/core/agents/agent-orchestrator.js +1 -11
- package/dist/core/codex-app/sks-menubar.js +47 -2
- package/dist/core/commands/agent-bridge-command.js +86 -0
- package/dist/core/commands/image-ux-review-command.js +6 -6
- package/dist/core/commands/loop-command.js +2 -6
- package/dist/core/commands/mad-sks-command.js +3 -3
- package/dist/core/commands/mcp-server-command.js +13 -0
- package/dist/core/commands/naruto-command.js +4 -4
- package/dist/core/commands/ppt-command.js +6 -3
- package/dist/core/commands/qa-loop-command.js +64 -26
- package/dist/core/commands/team-command.js +1 -1
- package/dist/core/commands/wiki-command.js +89 -3
- package/dist/core/config/secret-preservation.js +43 -1
- package/dist/core/doctor/browser-use-repair.js +149 -0
- package/dist/core/doctor/computer-use-repair.js +166 -0
- package/dist/core/doctor/imagegen-repair.js +9 -0
- package/dist/core/doctor/mcp-transport-collision-repair.js +129 -0
- package/dist/core/feature-fixtures.js +19 -11
- package/dist/core/feature-registry.js +21 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +48 -0
- package/dist/core/hooks-runtime.js +4 -0
- package/dist/core/mad-db/mad-db-capability.js +1 -1
- package/dist/core/mission.js +19 -3
- package/dist/core/naruto/naruto-real-worker-child.js +5 -4
- package/dist/core/naruto/naruto-real-worker-runtime.js +5 -4
- package/dist/core/naruto/normalize-worker-prompt-text.js +12 -0
- package/dist/core/routes.js +3 -1
- package/dist/core/triwiki/code-index-scanner.js +313 -0
- package/dist/core/triwiki/code-pack.js +169 -0
- package/dist/core/triwiki/triwiki-cache-key.js +24 -2
- package/dist/core/triwiki-attention.js +42 -5
- package/dist/core/triwiki-wrongness/wrongness-schema.js +18 -1
- package/dist/core/update-check.js +50 -0
- package/dist/core/version.js +1 -1
- package/dist/scripts/doctor-imagegen-repair-check.js +6 -1
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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';
|
|
@@ -17,8 +17,11 @@ import { validateImageVoxelLedger } from '../wiki-image/validation.js';
|
|
|
17
17
|
import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
|
|
18
18
|
import { wikiWrongnessCommand } from '../triwiki-wrongness/wrongness-cli.js';
|
|
19
19
|
import { wrongnessContextForRoute } from '../triwiki-wrongness/wrongness-retrieval.js';
|
|
20
|
+
import { readCombinedWrongnessRecords } from '../triwiki-wrongness/wrongness-ledger.js';
|
|
20
21
|
import { recordImageWrongnessFromValidation } from '../triwiki-wrongness/image-wrongness.js';
|
|
21
22
|
import { publishSharedMemory, rebuildSharedIndexes, sharedMemorySummary, validateSharedMemory } from '../git-hygiene/shared-memory-publish.js';
|
|
23
|
+
import { scanCodebaseIndex } from '../triwiki/code-index-scanner.js';
|
|
24
|
+
import { buildCodePack, validateCodePack, writeCodePackAtomic } from '../triwiki/code-pack.js';
|
|
22
25
|
import { flag, positionalArgs, readFlagValue, readOption, resolveMissionId } from './command-utils.js';
|
|
23
26
|
export async function wikiCommand(sub, args = []) {
|
|
24
27
|
if (!sub || sub === 'help' || sub === '--help') {
|
|
@@ -108,6 +111,9 @@ export async function wikiCommand(sub, args = []) {
|
|
|
108
111
|
console.log(`Memory summary rebuilt: ${result.ok ? 'ok' : 'blocked'}`);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
114
|
+
if (sub === 'refresh' && flag(args, '--code')) {
|
|
115
|
+
return wikiRefreshCode(args);
|
|
116
|
+
}
|
|
111
117
|
if (sub === 'refresh') {
|
|
112
118
|
const root = await sksRoot();
|
|
113
119
|
const dryRun = flag(args, '--dry-run');
|
|
@@ -194,19 +200,83 @@ export async function wikiCommand(sub, args = []) {
|
|
|
194
200
|
const target = positionalArgs(args)[0] || path.join(root, '.sneakoscope', 'wiki', 'context-pack.json');
|
|
195
201
|
const pack = await readJson(path.resolve(target));
|
|
196
202
|
const { result, trustAnchors } = wikiValidationResult(pack);
|
|
203
|
+
const codePack = await codePackFreshness(root);
|
|
197
204
|
if (flag(args, '--json'))
|
|
198
|
-
return console.log(JSON.stringify(result, null, 2));
|
|
205
|
+
return console.log(JSON.stringify({ ...result, code_pack: codePack }, null, 2));
|
|
199
206
|
console.log(`Wiki coordinate index: ${result.ok ? 'ok' : 'failed'}`);
|
|
200
207
|
console.log(`Anchors checked: ${result.checked}`);
|
|
201
208
|
console.log(`Trust anchors: ${trustAnchors}/${result.checked}`);
|
|
202
209
|
for (const issue of result.issues)
|
|
203
210
|
console.log(`- ${issue.severity}: ${issue.id}${issue.anchor ? ` ${issue.anchor}` : ''}`);
|
|
211
|
+
console.log(`Code pack: ${codePack.status}${codePack.status === 'stale' ? ' — run `sks wiki refresh --code`' : ''}`);
|
|
204
212
|
process.exitCode = result.ok ? 0 : 2;
|
|
205
213
|
return;
|
|
206
214
|
}
|
|
207
215
|
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
216
|
process.exitCode = 1;
|
|
209
217
|
}
|
|
218
|
+
async function wikiRefreshCode(args = []) {
|
|
219
|
+
const root = await sksRoot();
|
|
220
|
+
const index = await scanCodebaseIndex(root);
|
|
221
|
+
const tokenBudgetRaw = readFlagValue(args, '--token-budget', null);
|
|
222
|
+
const tokenBudget = tokenBudgetRaw ? Number.parseInt(String(tokenBudgetRaw), 10) : undefined;
|
|
223
|
+
const pack = tokenBudget && Number.isFinite(tokenBudget) ? buildCodePack(root, index, tokenBudget) : buildCodePack(root, index);
|
|
224
|
+
const validation = await validateCodePack(pack, root);
|
|
225
|
+
const written = validation.ok ? await writeCodePackAtomic(root, pack) : null;
|
|
226
|
+
const result = {
|
|
227
|
+
schema: 'sks.wiki-refresh-code.v1',
|
|
228
|
+
ok: validation.ok,
|
|
229
|
+
issues: validation.issues,
|
|
230
|
+
modules: index.modules.length,
|
|
231
|
+
truncated: index.truncated,
|
|
232
|
+
scanned_file_count: index.scanned_file_count,
|
|
233
|
+
entries: pack.entries.length,
|
|
234
|
+
token_cost: pack.total_token_cost,
|
|
235
|
+
token_budget: pack.token_budget,
|
|
236
|
+
written: written ? written.path : null
|
|
237
|
+
};
|
|
238
|
+
process.exitCode = validation.ok ? 0 : 2;
|
|
239
|
+
// Always note the counts/issues on stderr (even in --json mode, where stdout must
|
|
240
|
+
// stay pure JSON) so a blocked run is diagnosable from stderr_tail alone without
|
|
241
|
+
// needing to reproduce it separately in an isolated environment.
|
|
242
|
+
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`);
|
|
243
|
+
if (flag(args, '--json'))
|
|
244
|
+
return console.log(JSON.stringify(result, null, 2));
|
|
245
|
+
console.log('Sneakoscope TriWiki Code Pack Refresh');
|
|
246
|
+
console.log(`Code pack refresh: ${validation.ok ? 'ok' : 'blocked'} (${pack.entries.length} entries from ${index.modules.length} modules${index.truncated ? ', truncated' : ''})`);
|
|
247
|
+
for (const issue of validation.issues)
|
|
248
|
+
console.log(`- ${issue}`);
|
|
249
|
+
}
|
|
250
|
+
/** Cheap freshness check: compares the code pack's recorded git HEAD sha (at
|
|
251
|
+
* generation time) against the current HEAD. Any uncertainty (no pack, no git,
|
|
252
|
+
* spawn failure) resolves to 'stale' rather than 'fresh', never overclaiming. */
|
|
253
|
+
/** Active-wrongness counts per TriWiki module id (from wrongness records' module_ids),
|
|
254
|
+
* so attention can hydrate frequently-wrong modules' code entries first. */
|
|
255
|
+
async function buildWrongnessByModule(root) {
|
|
256
|
+
const records = await readCombinedWrongnessRecords(root, null).catch(() => []);
|
|
257
|
+
const counts = {};
|
|
258
|
+
for (const record of records) {
|
|
259
|
+
if (record?.status && record.status !== 'active')
|
|
260
|
+
continue;
|
|
261
|
+
for (const moduleId of Array.isArray(record?.module_ids) ? record.module_ids : []) {
|
|
262
|
+
if (moduleId)
|
|
263
|
+
counts[moduleId] = (counts[moduleId] || 0) + 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return counts;
|
|
267
|
+
}
|
|
268
|
+
async function codePackFreshness(root) {
|
|
269
|
+
const packPath = path.join(root, '.sneakoscope', 'wiki', 'code-pack.json');
|
|
270
|
+
if (!(await exists(packPath)))
|
|
271
|
+
return { status: 'missing', git_head_sha: null, pack_sha: null };
|
|
272
|
+
const pack = await readJson(packPath, null).catch(() => null);
|
|
273
|
+
const packSha = pack?.git_head_sha || null;
|
|
274
|
+
if (!packSha)
|
|
275
|
+
return { status: 'missing', git_head_sha: null, pack_sha: null };
|
|
276
|
+
const head = await runProcess('git', ['rev-parse', 'HEAD'], { cwd: root, timeoutMs: 5000 }).catch(() => null);
|
|
277
|
+
const currentSha = head && head.code === 0 ? head.stdout.trim() : null;
|
|
278
|
+
return { status: currentSha && currentSha === packSha ? 'fresh' : 'stale', git_head_sha: currentSha, pack_sha: packSha };
|
|
279
|
+
}
|
|
210
280
|
function wikiRefreshFailureAnalysis(validationResult, gate = {}) {
|
|
211
281
|
if (validationResult?.ok) {
|
|
212
282
|
return {
|
|
@@ -327,6 +397,9 @@ async function wikiImageLinkProof(args = []) {
|
|
|
327
397
|
export async function writeWikiContextPack(root, args = [], opts = {}) {
|
|
328
398
|
const role = readFlagValue(args, '--role', 'worker');
|
|
329
399
|
const maxAnchors = Number(readFlagValue(args, '--max-anchors', role.includes('verifier') ? 48 : 32));
|
|
400
|
+
const codePackData = await readJson(path.join(root, '.sneakoscope', 'wiki', 'code-pack.json'), null).catch(() => null);
|
|
401
|
+
const codePackEntries = Array.isArray(codePackData?.entries) ? codePackData.entries : [];
|
|
402
|
+
const wrongnessByModule = await buildWrongnessByModule(root);
|
|
330
403
|
const pack = contextCapsule({
|
|
331
404
|
mission: { id: 'project-wiki', coord: { rgba: { r: 48, g: 132, b: 212, a: 240 } } },
|
|
332
405
|
role,
|
|
@@ -334,11 +407,24 @@ export async function writeWikiContextPack(root, args = [], opts = {}) {
|
|
|
334
407
|
claims: await projectWikiClaims(root),
|
|
335
408
|
q4: { mode: 'project-continuity', package: PACKAGE_VERSION, hydrate: 'anchor-first' },
|
|
336
409
|
q3: ['sks', 'llm-wiki', 'wiki-coordinate', 'gx', 'skills'],
|
|
337
|
-
budget: { maxWikiAnchors: maxAnchors, includeTrustSummary: true }
|
|
410
|
+
budget: { maxWikiAnchors: maxAnchors, includeTrustSummary: true },
|
|
411
|
+
codePackEntries,
|
|
412
|
+
wrongnessByModule
|
|
338
413
|
});
|
|
339
414
|
const wrongnessContext = await wrongnessContextForRoute(root, { route: '$Wiki', limit: 12 });
|
|
415
|
+
// buildTriWikiAttention's use_first/hydrate_first can reference code: ids that were
|
|
416
|
+
// never routed through selectClaims (no fabricated RGBA coordinate for them — see
|
|
417
|
+
// codePackAttentionRows), so append their real summary text here or a recallpulse
|
|
418
|
+
// consumer resolving pack.claims by id would only see the bare id string.
|
|
419
|
+
const codePackClaimRows = codePackEntries.map((entry) => ({
|
|
420
|
+
id: entry.id,
|
|
421
|
+
text: `${entry.text}${Array.isArray(entry.citations) && entry.citations.length ? ` (source: ${entry.citations.map((c) => c?.path).filter(Boolean).join(', ')})` : ''}`,
|
|
422
|
+
source: 'code-pack',
|
|
423
|
+
trust: entry.trust_score
|
|
424
|
+
}));
|
|
340
425
|
const enrichedPack = {
|
|
341
426
|
...pack,
|
|
427
|
+
claims: [...(pack.claims || []), ...codePackClaimRows],
|
|
342
428
|
wrongness_context: wrongnessContext,
|
|
343
429
|
q3: Array.from(new Set([...(pack.q3 || []), 'wrongness-memory', 'negative-evidence']))
|
|
344
430
|
};
|
|
@@ -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
|