sneakoscope 3.1.8 → 3.1.10

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 (42) hide show
  1. package/README.md +6 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/cli/args.js +17 -0
  8. package/dist/cli/command-registry.js +16 -13
  9. package/dist/cli/router.js +8 -5
  10. package/dist/commands/doctor.js +54 -4
  11. package/dist/core/codex-app/codex-skill-sync.js +37 -2
  12. package/dist/core/codex-native/core-skill-integrity.js +6 -1
  13. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  14. package/dist/core/codex-native/native-capability-postcheck.js +143 -15
  15. package/dist/core/codex-native/native-capability-repair-matrix.js +1 -1
  16. package/dist/core/codex-native/project-skill-dedupe.js +18 -3
  17. package/dist/core/codex-native/skill-registry-ledger.js +9 -2
  18. package/dist/core/commands/basic-cli.js +7 -2
  19. package/dist/core/commands/mad-sks-command.js +36 -13
  20. package/dist/core/commands/naruto-command.js +4 -1
  21. package/dist/core/commands/pipeline-command.js +3 -4
  22. package/dist/core/commands/qa-loop-command.js +36 -1
  23. package/dist/core/commands/research-command.js +61 -1
  24. package/dist/core/commands/team-command.js +63 -3
  25. package/dist/core/config/managed-config-merge.js +59 -10
  26. package/dist/core/config/secret-preservation.js +145 -37
  27. package/dist/core/decision-contract.js +28 -4
  28. package/dist/core/doctor/command-alias-cleanup.js +64 -0
  29. package/dist/core/feature-fixtures.js +2 -0
  30. package/dist/core/feature-registry.js +2 -2
  31. package/dist/core/fsx.js +1 -1
  32. package/dist/core/init.js +31 -6
  33. package/dist/core/naruto/naruto-work-graph.js +4 -1
  34. package/dist/core/pipeline-internals/runtime-core.js +50 -4
  35. package/dist/core/pipeline-internals/runtime-gates.js +10 -1
  36. package/dist/core/proof/route-proof-gate.js +1 -1
  37. package/dist/core/qa-loop.js +227 -11
  38. package/dist/core/questions.js +239 -2
  39. package/dist/core/routes.js +3 -4
  40. package/dist/core/version.js +1 -1
  41. package/dist/scripts/agent-native-release-gate.js +13 -4
  42. package/package.json +5 -1
@@ -17,8 +17,16 @@ export async function dedupeProjectSkills(input) {
17
17
  const userEntries = group.filter((entry) => !entry.managed_by_sks);
18
18
  const managedEntries = group.filter((entry) => entry.managed_by_sks);
19
19
  if (userEntries.length > 0 && managedEntries.length > 0) {
20
- for (const user of userEntries)
21
- actions.push(actionRow(canonical, 'kept', user, null, 'user-authored skill preserved'));
20
+ const keepUser = userEntries[0];
21
+ if (keepUser)
22
+ actions.push(actionRow(canonical, 'kept', keepUser, null, 'user-authored skill preserved'));
23
+ const shouldMoveUserDuplicates = fix && yes && input.quarantineUserDuplicates === true;
24
+ for (const duplicateUser of userEntries.slice(1)) {
25
+ const quarantine = shouldMoveUserDuplicates ? await quarantineSkill(root, canonical, duplicateUser, 'user-authored duplicate skill') : null;
26
+ actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicateUser, quarantine, 'user-authored duplicate skill requires --quarantine-user-duplicates --yes'));
27
+ }
28
+ if (userEntries.length > 1 && !shouldMoveUserDuplicates)
29
+ unresolvedUserDuplicates.push(canonical);
22
30
  for (const managed of managedEntries) {
23
31
  const quarantine = await maybeQuarantine(root, canonical, managed, fix, 'managed collision with user-authored skill');
24
32
  actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', managed, quarantine, 'managed collision with user-authored skill'));
@@ -49,7 +57,11 @@ export async function dedupeProjectSkills(input) {
49
57
  }
50
58
  }
51
59
  const duplicateNames = [...new Set(actions.filter((action) => action.action !== 'kept').map((action) => action.canonical_name))].sort();
52
- const blockers = unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`);
60
+ const afterLedger = await buildSkillRegistryLedger({ root, reportPath: null });
61
+ const blockers = [
62
+ ...unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`),
63
+ ...afterLedger.duplicate_active_canonical_names.map((name) => `duplicate_active_skill_name:${name}`)
64
+ ];
53
65
  const report = {
54
66
  schema: 'sks.project-skill-dedupe.v1',
55
67
  generated_at: nowIso(),
@@ -58,6 +70,9 @@ export async function dedupeProjectSkills(input) {
58
70
  fix,
59
71
  yes,
60
72
  actions,
73
+ active_unique_by_canonical_name: afterLedger.active_unique_by_canonical_name,
74
+ active_entries: afterLedger.active_entries,
75
+ duplicate_active_canonical_names: afterLedger.duplicate_active_canonical_names,
61
76
  duplicate_canonical_names: duplicateNames,
62
77
  unresolved_user_duplicates: unresolvedUserDuplicates,
63
78
  blockers
@@ -56,13 +56,20 @@ export async function buildSkillRegistryLedger(input) {
56
56
  entry.status = entry.status === 'user-owned' ? 'duplicate' : 'duplicate';
57
57
  });
58
58
  }
59
- const blockers = duplicates.map((name) => `duplicate_skill_name:${name}`);
59
+ const activeEntries = entries.filter((entry) => entry.status !== 'quarantined');
60
+ const activeGrouped = groupByCanonical(activeEntries);
61
+ const duplicateActiveNames = [...activeGrouped.entries()].filter(([, group]) => group.length > 1).map(([name]) => name).sort();
62
+ const activeUnique = duplicateActiveNames.length === 0;
63
+ const blockers = duplicateActiveNames.map((name) => `duplicate_active_skill_name:${name}`);
60
64
  const ledger = {
61
65
  schema: 'sks.skill-registry-ledger.v1',
62
66
  generated_at: nowIso(),
63
- ok: blockers.length === 0,
67
+ ok: activeUnique,
64
68
  root,
65
69
  entries,
70
+ active_unique_by_canonical_name: activeUnique,
71
+ active_entries: activeEntries,
72
+ duplicate_active_canonical_names: duplicateActiveNames,
66
73
  duplicate_canonical_names: duplicates,
67
74
  blockers
68
75
  };
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { spawnSync } from 'node:child_process';
3
- import { COMMANDS } from '../../cli/command-registry.js';
3
+ import { COMMANDS, LEGACY_COMMAND_ALIASES } from '../../cli/command-registry.js';
4
4
  import { flag } from '../../cli/args.js';
5
5
  import { printJson, sksTextLogo } from '../../cli/output.js';
6
6
  import { PACKAGE_VERSION, ensureDir, exists, nowIso, projectRoot, readJson, sksRoot, tmpdir, writeJsonAtomic } from '../fsx.js';
@@ -55,6 +55,9 @@ export function dollarCommandsCommand(args = []) {
55
55
  export function aliasesCommand() {
56
56
  console.log('Aliases');
57
57
  console.log('- sks, sneakoscope');
58
+ console.log('- CLI compatibility aliases:');
59
+ for (const [alias, canonical] of Object.entries(LEGACY_COMMAND_ALIASES))
60
+ console.log(` sks ${alias} -> sks ${canonical}`);
58
61
  console.log('- $ aliases:');
59
62
  for (const entry of DOLLAR_COMMAND_ALIASES)
60
63
  console.log(` ${entry.app_skill} -> ${entry.canonical}`);
@@ -182,7 +185,9 @@ export async function initCommand(args = []) {
182
185
  export async function fixPathCommand(args = []) {
183
186
  const root = await projectRoot();
184
187
  const installScope = installScopeFromArgs(args);
185
- await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
188
+ await withSecretPreservationGuard(root, 'fix-path-command', async () => {
189
+ await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
190
+ });
186
191
  const result = {
187
192
  schema: 'sks.fix-path.v1',
188
193
  ok: true,
@@ -30,6 +30,7 @@ export async function madHighCommand(args = [], deps = {}) {
30
30
  return madSksSubcommand(subcommand, args.filter((arg) => String(arg) !== subcommand));
31
31
  const cleanArgs = stripMadLaunchOnlyArgs(args);
32
32
  const rawArgs = (args || []).map((arg) => String(arg));
33
+ const madDbGrant = resolveMadLaunchMadDbGrant(rawArgs);
33
34
  const dryRun = rawArgs.includes('--dry-run');
34
35
  if (args.includes('--json') && !dryRun) {
35
36
  const profile = buildMadHighLaunchProfileNoWrite();
@@ -133,13 +134,6 @@ export async function madHighCommand(args = [], deps = {}) {
133
134
  // later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
134
135
  // readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
135
136
  // old behavior.
136
- const madDbRequested = rawArgs.includes('--mad-db');
137
- const madDbAck = readOption(rawArgs, '--ack', '');
138
- if (madDbRequested && madDbAck !== MAD_DB_ACK) {
139
- console.error(`SKS MAD-DB launch blocked. Required --ack ${JSON.stringify(MAD_DB_ACK)}`);
140
- process.exitCode = 2;
141
- return { ok: false, status: 'blocked', reason: 'mad_db_ack_phrase_required', required_ack: MAD_DB_ACK };
142
- }
143
137
  const allowMadRepair = rawArgs.includes('--repair-config') || rawArgs.includes('--fix') || rawArgs.includes('--yes-repair');
144
138
  const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: allowMadRepair, launchFast: process.env.SKS_LAUNCH_FULL_CODEX_PROBE !== '1', profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
145
139
  const afterPreflightUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-preflight-${uiSnapshotId}`).catch(() => null) : null;
@@ -160,18 +154,38 @@ export async function madHighCommand(args = [], deps = {}) {
160
154
  return launchPreflight;
161
155
  }
162
156
  const madLaunch = await activateMadZellijPermissionState(process.cwd(), args);
163
- const madDbCapability = madDbRequested
164
- ? await createMadDbCapability(madLaunch.root, { missionId: madLaunch.mission_id, ack: madDbAck, cwd: process.cwd() })
157
+ const madDbCapability = madDbGrant.enabled
158
+ ? await createMadDbCapability(madLaunch.root, { missionId: madLaunch.mission_id, ack: madDbGrant.ack, cwd: process.cwd() })
165
159
  : null;
166
160
  if (madDbCapability) {
161
+ const grantReport = {
162
+ schema: 'sks.mad-sks-launch-grants.v1',
163
+ generated_at: nowIso(),
164
+ mission_id: madLaunch.mission_id,
165
+ mad_sks_active: true,
166
+ mad_db_active: true,
167
+ mad_db_default_grant: madDbGrant.source === 'sks_mad_default',
168
+ mad_db_grant_source: madDbGrant.source,
169
+ mad_db_one_cycle_only: true,
170
+ mad_db_capability_file: 'mad-db-capability.json',
171
+ mad_db_cycle_id: madDbCapability.cycle_id,
172
+ mad_db_expires_at: madDbCapability.expires_at,
173
+ standalone_mad_db_enable_still_requires_ack: true
174
+ };
175
+ await writeJsonAtomic(path.join(madLaunch.dir, 'mad-sks-launch-grants.json'), grantReport);
167
176
  await setCurrent(madLaunch.root, {
168
177
  mission_id: madLaunch.mission_id,
169
178
  mad_db_active: true,
170
179
  mad_db_cycle_id: madDbCapability.cycle_id,
171
180
  mad_db_capability_file: 'mad-db-capability.json',
172
- mad_db_break_glass: true
181
+ mad_db_break_glass: true,
182
+ mad_db_default_grant: madDbGrant.source === 'sks_mad_default',
183
+ mad_db_grant_source: madDbGrant.source,
184
+ mad_db_one_cycle_only: true,
185
+ mad_db_priority_override_active: true,
186
+ mad_sks_launch_grants_file: 'mad-sks-launch-grants.json'
173
187
  });
174
- await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_db.capability_created', cycle_id: madDbCapability.cycle_id, expires_at: madDbCapability.expires_at });
188
+ await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_db.capability_created', grant_source: madDbGrant.source, cycle_id: madDbCapability.cycle_id, expires_at: madDbCapability.expires_at });
175
189
  }
176
190
  const updateNotice = await checkSksUpdateNotice({
177
191
  packageName: deps.packageName || 'sneakoscope',
@@ -192,10 +206,10 @@ export async function madHighCommand(args = [], deps = {}) {
192
206
  await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_sks.update_notice_checked', non_blocking: true, update_available: updateNotice.update_available === true, source: updateNotice.source });
193
207
  console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
194
208
  if (madDbCapability)
195
- console.log(`MAD-DB one-cycle capability active; expires ${madDbCapability.expires_at}.`);
209
+ console.log(`MAD-DB one-cycle capability active (${madDbGrant.source}); expires ${madDbCapability.expires_at}.`);
196
210
  if (updateNotice.update_available === true)
197
211
  console.log(`SKS update notice: ${updateNotice.latest_version} available (non-blocking).`);
198
- console.log('Scoped high-power maintenance authority active; add explicit --allow-* flags for packages, services, network, browser/Computer Use, generated assets, file permissions, DB writes, or system/admin scopes. Catastrophic guards remain.');
212
+ console.log('Scoped high-power maintenance authority active; add explicit --allow-* flags for packages, services, network, browser/Computer Use, generated assets, file permissions, or system/admin scopes. MAD-DB one-cycle DB break-glass is already active for this launch; protected-core, audit, and one-cycle bounds remain.');
199
213
  const launchLb = lb.status === 'present' ? { ...lb, status: 'configured' } : lb;
200
214
  const madSksEnv = {
201
215
  SKS_PROTECTED_CORE_POLICY: madLaunch.gate.protected_core_policy,
@@ -260,6 +274,15 @@ export async function madHighCommand(args = [], deps = {}) {
260
274
  console.log('MAD launch running headless: live_panes=false.');
261
275
  return launch;
262
276
  }
277
+ export function resolveMadLaunchMadDbGrant(args = []) {
278
+ const list = (args || []).map((arg) => String(arg));
279
+ return {
280
+ enabled: true,
281
+ source: list.includes('--mad-db') ? 'sks_mad_explicit_redundant_flag' : 'sks_mad_default',
282
+ ack: MAD_DB_ACK,
283
+ one_cycle_only: true
284
+ };
285
+ }
263
286
  export async function startMadNativeSwarm(root, madLaunch, args = [], profile = {}, opts = {}) {
264
287
  const swarm = resolveMadNativeSwarmOptions(args, profile, opts);
265
288
  const dir = madLaunch.dir || missionDirLike(root, madLaunch.mission_id);
@@ -114,6 +114,7 @@ async function narutoRun(parsed) {
114
114
  prompt: parsed.prompt,
115
115
  requestedClones: roster.agent_count,
116
116
  totalWorkItems: parsed.workItems,
117
+ honorExplicitTotalWorkItems: parsed.workItemsExplicit,
117
118
  readonly: parsed.readonly,
118
119
  writeCapable,
119
120
  leaseBasePath: patchEnvelopeBasePath,
@@ -127,6 +128,7 @@ async function narutoRun(parsed) {
127
128
  prompt: parsed.prompt,
128
129
  requestedClones: roster.agent_count,
129
130
  totalWorkItems: parsed.workItems,
131
+ honorExplicitTotalWorkItems: parsed.workItemsExplicit,
130
132
  readonly: parsed.readonly,
131
133
  writeCapable,
132
134
  leaseBasePath: patchEnvelopeBasePath,
@@ -791,6 +793,7 @@ function parseNarutoArgs(args = []) {
791
793
  const json = hasFlag(args, '--json');
792
794
  const requestedClones = Number(readOption(args, '--clones', readOption(args, '--agents', DEFAULT_NARUTO_CLONES)));
793
795
  const clones = clampClones(requestedClones);
796
+ const workItemsExplicit = hasOption(args, '--work-items');
794
797
  const workItems = clampWorkItems(Number(readOption(args, '--work-items', clones * 2)), clones);
795
798
  const concurrency = normalizeConcurrency(readOption(args, '--concurrency', readOption(args, '--target-active-slots', null)), clones);
796
799
  const useOllama = hasFlag(args, '--ollama') || hasFlag(args, '--local-model');
@@ -831,7 +834,7 @@ function parseNarutoArgs(args = []) {
831
834
  const messages = normalizeMessages(readOption(args, '--messages', '8'));
832
835
  const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--service-tier', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
833
836
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
834
- return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
837
+ return { action, prompt, clones, workItems, workItemsExplicit, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
835
838
  }
836
839
  function normalizeParallelism(value) {
837
840
  const text = String(value || 'extreme').toLowerCase();
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import { projectRoot, readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { projectRoot, readJson } from '../fsx.js';
3
3
  import { missionDir, stateFile } from '../mission.js';
4
- import { buildPipelinePlan, PIPELINE_PLAN_ARTIFACT, projectGateStatus } from '../pipeline.js';
4
+ import { PIPELINE_PLAN_ARTIFACT, projectGateStatus, writePipelinePlan } from '../pipeline.js';
5
5
  import { routePrompt } from '../routes.js';
6
6
  import { flag, positionalArgs, readFlagValue, resolveMissionId } from './command-utils.js';
7
7
  export async function pipelineCommand(args = []) {
@@ -33,14 +33,13 @@ export async function pipelineCommand(args = []) {
33
33
  force: flag(args, '--force-agents'),
34
34
  noAgents: flag(args, '--no-agents')
35
35
  };
36
- const plan = buildPipelinePlan({
36
+ const plan = await writePipelinePlan(dir, {
37
37
  missionId: id,
38
38
  route,
39
39
  task: routeContext.task || mission.prompt || state.prompt || '',
40
40
  required: Boolean(routeContext.context7_required || state.context7_required),
41
41
  agents
42
42
  });
43
- await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
44
43
  if (flag(args, '--json'))
45
44
  return console.log(JSON.stringify({ schema: 'sks.pipeline-plan.v1', ok: true, mission_id: id, plan }, null, 2));
46
45
  console.log(`Pipeline plan written: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}`);
@@ -5,7 +5,7 @@ import { getCodexInfo, runCodexExec } from '../codex-adapter.js';
5
5
  import { createMission, loadMission, setCurrent, stateFile } from '../mission.js';
6
6
  import { writeQuestions } from '../questions.js';
7
7
  import { sealContract } from '../decision-contract.js';
8
- import { buildQaLoopQuestionSchema, buildQaLoopPrompt, evaluateQaGate, qaStatus, qaUiRequired, writeMockQaResult, writeQaLoopArtifacts, writeQaNativeAgentLedger } from '../qa-loop.js';
8
+ import { buildQaLoopQuestionSchema, buildQaLoopPrompt, ensureQaLoopVisualEvidenceContract, evaluateQaGate, qaGptImage2AnnotatedReviewRequired, qaStatus, qaUiRequired, writeMockQaResult, writeQaLoopArtifacts, writeQaNativeAgentLedger } from '../qa-loop.js';
9
9
  import { containsUserQuestion, noQuestionContinuationReason } from '../no-question-guard.js';
10
10
  import { ROUTES, routePrompt, stripVisibleDecisionAnswerBlocks } from '../routes.js';
11
11
  import { codexChromeExtensionStatus } from '../codex-app.js';
@@ -124,6 +124,8 @@ async function qaLoopRun(args) {
124
124
  const contract = await readJson(contractPath, {});
125
125
  if (!(await exists(path.join(dir, 'qa-ledger.json'))))
126
126
  await writeQaLoopArtifacts(dir, mission, contract);
127
+ else
128
+ await ensureQaLoopVisualEvidenceContract(dir, mission, contract);
127
129
  const safetyScan = await scanDbSafety(root);
128
130
  if (!safetyScan.ok) {
129
131
  console.error('QA-LOOP cannot run: SKS safety scan found unsafe project data-tool configuration.');
@@ -148,6 +150,7 @@ async function qaLoopRun(args) {
148
150
  const reportFile = qaGate.qa_report_file;
149
151
  const executionProfile = await readJson(path.join(dir, 'qa-loop', 'execution-profile.json'), null);
150
152
  const uiRequired = qaUiRequired(contract.answers || {});
153
+ const gptImage2ReviewRequired = qaGptImage2AnnotatedReviewRequired(contract, mission.prompt);
151
154
  const capabilityArtifact = await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), report: null }));
152
155
  const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
153
156
  const budgetPolicy = buildQaLoopBudgetPolicy({ usage: usageArtifact?.snapshot || null, provider: 'codex-sdk' });
@@ -253,6 +256,16 @@ async function qaLoopRun(args) {
253
256
  ui_chrome_extension_evidence: false,
254
257
  ui_computer_use_evidence: false,
255
258
  ui_evidence_source: 'blocked_chrome_extension_setup_required',
259
+ ui_chrome_extension_screenshot_required: true,
260
+ ui_chrome_extension_screenshot_captured: false,
261
+ ui_chrome_extension_screenshot_artifact: null,
262
+ ui_chrome_extension_screenshot_sha256: null,
263
+ gpt_image_2_annotated_review_required: gptImage2ReviewRequired,
264
+ gpt_image_2_annotated_review_generated: false,
265
+ gpt_image_2_annotated_review_artifact: null,
266
+ gpt_image_2_annotated_review_sha256: null,
267
+ gpt_image_2_annotated_review_model: gptImage2ReviewRequired ? null : 'not_required',
268
+ gpt_image_2_annotated_review_provider: gptImage2ReviewRequired ? null : 'not_required',
256
269
  blocker: 'codex_chrome_extension_setup_required',
257
270
  blockers: Array.from(new Set([...(qaGate.blockers || []), 'codex_chrome_extension_setup_required', ...(chrome.blockers || [])])),
258
271
  evidence: [...(qaGate.evidence || []), 'Codex Chrome Extension preflight failed before web QA execution.'],
@@ -277,6 +290,28 @@ async function qaLoopRun(args) {
277
290
  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' } });
278
291
  await writeJsonAtomic(path.join(dir, 'qa-native-agent-run.json'), nativeAgentRun);
279
292
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
293
+ if (flag(args, '--native-proof-only')) {
294
+ const proofOnlyGate = {
295
+ schema: 'sks.qa-native-proof-only-gate.v1',
296
+ ok: nativeAgentRun.proof?.ok === true,
297
+ native_agent_proof: nativeAgentRun.proof?.ok === true,
298
+ proof_status: nativeAgentRun.proof?.status || null,
299
+ blockers: nativeAgentRun.proof?.blockers || []
300
+ };
301
+ if (flag(args, '--json'))
302
+ return console.log(JSON.stringify({
303
+ schema: 'sks.qa-loop-run.v1',
304
+ ok: proofOnlyGate.ok,
305
+ status: proofOnlyGate.ok ? 'native_proof_ready' : 'blocked',
306
+ mission_id: id,
307
+ gate: proofOnlyGate,
308
+ proof: nativeAgentRun.proof,
309
+ native_agent_run: nativeAgentRun,
310
+ native_proof_only: true
311
+ }, null, 2));
312
+ console.log(`QA-LOOP native proof ready: ${id}`);
313
+ return;
314
+ }
280
315
  if (mock) {
281
316
  let gate = await writeMockQaResult(dir, mission, contract);
282
317
  const needsVisual = uiRequired;
@@ -40,6 +40,38 @@ export async function researchCommand(sub, args = []) {
40
40
  export async function autoresearchCommand(sub, args = []) {
41
41
  return researchCommand(sub || 'status', args);
42
42
  }
43
+ function hasFlagOption(args = [], name) {
44
+ return args.includes(name) || args.some((arg) => String(arg).startsWith(`${name}=`));
45
+ }
46
+ function limitResearchNativeWorkGraph(graph, limit) {
47
+ const count = Math.max(1, Math.min(Number(graph?.work_items?.length || 0) || 1, Math.floor(Number(limit) || 1)));
48
+ const workItems = (graph.work_items || []).slice(0, count).map((item) => {
49
+ const selectedIds = new Set((graph.work_items || []).slice(0, count).map((row) => String(row.id || '')));
50
+ return {
51
+ ...item,
52
+ dependencies: (item.dependencies || []).map(String).filter((id) => selectedIds.has(id)),
53
+ can_run_in_parallel_with: (item.can_run_in_parallel_with || []).map(String).filter((id) => selectedIds.has(id))
54
+ };
55
+ });
56
+ const selectedIds = new Set(workItems.map((item) => String(item.id || '')));
57
+ const activeWaves = (graph.active_waves || [])
58
+ .map((wave) => ({
59
+ ...wave,
60
+ work_item_ids: (wave.work_item_ids || []).map(String).filter((id) => selectedIds.has(id)),
61
+ write_paths: (wave.write_paths || []).map(String),
62
+ conflict_count: Number(wave.conflict_count || 0)
63
+ }))
64
+ .filter((wave) => wave.work_item_ids.length > 0);
65
+ return {
66
+ ...graph,
67
+ requested_clones: Math.min(Number(graph.requested_clones || count), count),
68
+ total_work_items: workItems.length,
69
+ work_items: workItems,
70
+ active_waves: activeWaves,
71
+ mixed_work_kinds: [...new Set(workItems.map((item) => item.kind))],
72
+ write_allowed_count: workItems.filter((item) => item.write_allowed === true).length
73
+ };
74
+ }
43
75
  async function researchPrepare(args) {
44
76
  const root = await sksRoot();
45
77
  if (!(await exists(path.join(root, '.sneakoscope'))))
@@ -139,10 +171,16 @@ async function researchRun(args) {
139
171
  const mock = flag(args, '--mock');
140
172
  const researchWorkGraph = await writeResearchWorkGraph(dir, plan);
141
173
  const graphWorkItemCount = Math.max(1, Number(researchWorkGraph.total_work_items || researchWorkGraph.work_items?.length || 0));
174
+ const explicitWorkItems = hasFlagOption(args, '--work-items');
175
+ const effectiveDesiredWorkItemCount = explicitWorkItems ? desiredWorkItemCount : Math.max(desiredWorkItemCount, graphWorkItemCount);
176
+ const effectiveMinimumWorkItems = Math.min(effectiveDesiredWorkItemCount, explicitWorkItems ? minimumWorkItems : Math.max(minimumWorkItems, Math.min(graphWorkItemCount, targetActiveSlots)));
177
+ const nativeResearchWorkGraph = explicitWorkItems
178
+ ? limitResearchNativeWorkGraph(researchWorkGraph, effectiveDesiredWorkItemCount)
179
+ : researchWorkGraph;
142
180
  await runResearchCycle(dir, researchWorkGraph, { cycle: 0, status: mock ? 'mock_native_orchestrator_planned' : 'native_orchestrator_planned' });
143
181
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
144
182
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
145
- const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: Math.max(desiredWorkItemCount, graphWorkItemCount), minimumWorkItems: Math.max(minimumWorkItems, Math.min(graphWorkItemCount, targetActiveSlots)), maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
183
+ const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
146
184
  await writeJsonAtomic(path.join(dir, 'research-native-agent-run.json'), nativeAgentRun);
147
185
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
148
186
  if (!nativeAgentRun.ok) {
@@ -151,6 +189,28 @@ async function researchRun(args) {
151
189
  process.exitCode = 2;
152
190
  return;
153
191
  }
192
+ if (flag(args, '--native-proof-only')) {
193
+ const proofOnlyGate = {
194
+ schema: 'sks.research-native-proof-only-gate.v1',
195
+ ok: nativeAgentRun.proof?.ok === true,
196
+ native_agent_proof: nativeAgentRun.proof?.ok === true,
197
+ proof_status: nativeAgentRun.proof?.status || null,
198
+ blockers: nativeAgentRun.proof?.blockers || []
199
+ };
200
+ if (flag(args, '--json'))
201
+ return console.log(JSON.stringify({
202
+ schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1',
203
+ ok: proofOnlyGate.ok,
204
+ mission_id: id,
205
+ gate: proofOnlyGate,
206
+ proof: nativeAgentRun.proof,
207
+ native_agent_run: nativeAgentRun,
208
+ research_work_graph: nativeResearchWorkGraph,
209
+ native_proof_only: true
210
+ }, null, 2));
211
+ console.log(`Research native proof ready: ${id}`);
212
+ return;
213
+ }
154
214
  const legacyResearchCycle = flag(args, '--legacy-research-cycle') || process.env.SKS_RESEARCH_LEGACY_CYCLE === '1';
155
215
  const sourceMutationBaseline = await researchCodeMutationSnapshot(root, id);
156
216
  if (!legacyResearchCycle) {
@@ -1,8 +1,9 @@
1
1
  import path from 'node:path';
2
- import { nowIso, sksRoot, writeJsonAtomic } from '../fsx.js';
2
+ import { nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
3
3
  import { findLatestMission } from '../mission.js';
4
4
  import { narutoCommand } from './naruto-command.js';
5
5
  import { teamLegacyObserveCommand, teamLegacySubcommands } from './team-legacy-observe-command.js';
6
+ import { SSOT_GUARD_ARTIFACT } from '../safety/ssot-guard.js';
6
7
  export async function team(args = []) {
7
8
  if (teamLegacySubcommands.has(String(args[0] || ''))) {
8
9
  return teamLegacyObserveCommand(String(args[0]), args.slice(1));
@@ -13,9 +14,13 @@ async function redirectTeamCreateToNaruto(args = []) {
13
14
  const root = await sksRoot();
14
15
  const list = (args || []).map((arg) => String(arg));
15
16
  const narutoArgs = list[0] === 'run' ? list : ['run', ...list];
17
+ const jsonRequested = list.includes('--json');
16
18
  console.warn('SKS Team is deprecated for new execution missions; redirecting to $Naruto.');
17
- const result = await narutoCommand(narutoArgs);
19
+ const result = jsonRequested
20
+ ? await withSuppressedConsoleLog(() => narutoCommand(narutoArgs))
21
+ : await narutoCommand(narutoArgs);
18
22
  const missionId = result?.mission_id || await findLatestMission(root);
23
+ const nativeAgentRun = missionId ? await buildTeamNativeAgentCompatibility(root, missionId, result) : null;
19
24
  if (missionId) {
20
25
  await writeJsonAtomic(path.join(root, '.sneakoscope', 'missions', missionId, 'team-alias-to-naruto.json'), {
21
26
  schema: 'sks.team-alias-to-naruto.v1',
@@ -26,10 +31,65 @@ async function redirectTeamCreateToNaruto(args = []) {
26
31
  route_command: '$Naruto',
27
32
  deprecated_route: '$Team',
28
33
  parallel_write_policy: result?.parallel_write_policy || result?.run?.parallel_write_policy || null,
34
+ ssot_guard_artifact: SSOT_GUARD_ARTIFACT,
29
35
  created_at: nowIso(),
30
36
  args: list
31
37
  });
32
38
  }
33
- return result;
39
+ const finalResult = {
40
+ ...result,
41
+ mock: result?.mock === true || result?.backend === 'fake',
42
+ ...(nativeAgentRun ? { native_agent_run: nativeAgentRun } : {})
43
+ };
44
+ if (jsonRequested)
45
+ console.log(JSON.stringify(finalResult, null, 2));
46
+ return finalResult;
47
+ }
48
+ async function withSuppressedConsoleLog(fn) {
49
+ const originalLog = console.log;
50
+ console.log = () => undefined;
51
+ try {
52
+ return await fn();
53
+ }
54
+ finally {
55
+ console.log = originalLog;
56
+ }
57
+ }
58
+ async function buildTeamNativeAgentCompatibility(root, missionId, result) {
59
+ const ledgerRoot = path.join(root, '.sneakoscope', 'missions', missionId, 'agents');
60
+ const [schedulerState, proof, parallelWritePolicy] = await Promise.all([
61
+ readJson(path.join(ledgerRoot, 'agent-scheduler-state.json'), null),
62
+ readJson(path.join(ledgerRoot, 'agent-proof-evidence.json'), null),
63
+ readJson(path.join(ledgerRoot, 'agent-parallel-write-policy.json'), null)
64
+ ]);
65
+ if (!schedulerState || !proof)
66
+ return null;
67
+ return {
68
+ schema: result?.run?.schema || 'sks.agent-run.v1',
69
+ ok: result?.run?.ok === true || result?.ok === true,
70
+ mission_id: missionId,
71
+ route: '$Team',
72
+ backend: result?.backend || result?.run?.backend || proof.backend || null,
73
+ ledger_root: path.relative(root, ledgerRoot),
74
+ target_active_slots: schedulerState.target_active_slots ?? result?.target_active_slots ?? null,
75
+ scheduler: {
76
+ state: schedulerState
77
+ },
78
+ proof: {
79
+ ...proof,
80
+ route: '$Team',
81
+ route_command: 'sks team',
82
+ route_blackbox_kind: 'actual_team_command',
83
+ real_route_command_used: true
84
+ },
85
+ parallel_write_policy: parallelWritePolicy || result?.parallel_write_policy || result?.run?.parallel_write_policy || null,
86
+ redirected_to: '$Naruto',
87
+ compatibility: {
88
+ schema: 'sks.team-native-agent-compatibility.v1',
89
+ ok: true,
90
+ source: 'team-alias-to-naruto',
91
+ ledger_root: path.relative(root, ledgerRoot)
92
+ }
93
+ };
34
94
  }
35
95
  //# sourceMappingURL=team-command.js.map
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
3
+ import { ensureDir, nowIso, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
4
  import { isProtectedSecretKey, PROTECTED_SECRET_KEYS } from './supabase-secret-preservation.js';
5
5
  export async function writeManagedJsonConfig(file, current, managed) {
6
6
  const next = safeMergeObject(current, managed);
@@ -23,13 +23,14 @@ export async function writeManagedEnvConfig(file, currentText, managedLines) {
23
23
  const next = additions.length ? `${String(currentText || '').replace(/\s*$/, '\n')}${additions.join('\n')}\n` : String(currentText || '');
24
24
  return writeMergedText(file, currentText, next, 'env', protectedKeysInText(currentText));
25
25
  }
26
- export function safeMergeObject(current, managed) {
26
+ export function safeMergeObject(current, managed, prefix = '') {
27
27
  const out = { ...current };
28
28
  for (const [key, value] of Object.entries(managed)) {
29
- if (isProtectedSecretKey(key) && current[key] != null)
29
+ const dotted = prefix ? `${prefix}.${key}` : key;
30
+ if (isProtectedSecretKey(dotted) && current[key] != null)
30
31
  continue;
31
32
  if (isPlainObject(value) && isPlainObject(current[key]))
32
- out[key] = safeMergeObject(current[key], value);
33
+ out[key] = safeMergeObject(current[key], value, dotted);
33
34
  else
34
35
  out[key] = value;
35
36
  }
@@ -51,11 +52,23 @@ function upsertTomlBlockPreservingSecrets(text, block) {
51
52
  break;
52
53
  }
53
54
  }
54
- const existingSecretLines = lines.slice(start + 1, end).filter((line) => {
55
- const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
56
- return isProtectedSecretKey(`${header}.${key}`) || isProtectedSecretKey(key);
57
- });
58
- lines.splice(start, end - start, ...blockLines, ...existingSecretLines.filter((line) => !blockLines.includes(line)));
55
+ const existingBody = lines.slice(start + 1, end);
56
+ const nextBody = [...existingBody];
57
+ for (const managedLine of blockLines.slice(1)) {
58
+ const managedKey = managedLine.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
59
+ if (!managedKey) {
60
+ if (!nextBody.includes(managedLine))
61
+ nextBody.push(managedLine);
62
+ continue;
63
+ }
64
+ const existingIndex = nextBody.findIndex((line) => (line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '') === managedKey);
65
+ const protectedLine = isProtectedSecretKey(`${header}.${managedKey}`) || isProtectedSecretKey(managedKey);
66
+ if (existingIndex === -1)
67
+ nextBody.push(managedLine);
68
+ else if (!protectedLine)
69
+ nextBody[existingIndex] = managedLine;
70
+ }
71
+ lines.splice(start, end - start, blockLines[0] || `[${header}]`, ...nextBody);
59
72
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
60
73
  }
61
74
  async function writeMergedText(file, before, after, format, preserved) {
@@ -68,6 +81,7 @@ async function writeMergedText(file, before, after, format, preserved) {
68
81
  }
69
82
  await writeTextAtomic(file, after);
70
83
  }
84
+ const preservedSecretLineHashes = protectedSecretLineHashes(before);
71
85
  return {
72
86
  schema: 'sks.managed-config-merge.v1',
73
87
  generated_at: nowIso(),
@@ -77,6 +91,8 @@ async function writeMergedText(file, before, after, format, preserved) {
77
91
  changed: before !== after,
78
92
  backup_path: backupPath,
79
93
  protected_keys_preserved: preserved,
94
+ preserved_secret_lines_sha256: preservedSecretLineHashes,
95
+ idempotent: before === after || protectedSecretLineHashes(after).every((hash) => preservedSecretLineHashes.includes(hash)),
80
96
  blockers: []
81
97
  };
82
98
  }
@@ -88,7 +104,40 @@ function protectedKeysPresent(value) {
88
104
  return found;
89
105
  }
90
106
  function protectedKeysInText(text) {
91
- return PROTECTED_SECRET_KEYS.filter((key) => new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text)).map(String);
107
+ const found = new Set();
108
+ for (const key of PROTECTED_SECRET_KEYS) {
109
+ if (new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text))
110
+ found.add(String(key));
111
+ }
112
+ let section = '';
113
+ for (const line of String(text || '').split(/\r?\n/)) {
114
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
115
+ if (sectionMatch) {
116
+ section = String(sectionMatch[1] || '');
117
+ continue;
118
+ }
119
+ const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
120
+ if (key && section && isProtectedSecretKey(`${section}.${key}`))
121
+ found.add(`${section}.${key}`);
122
+ }
123
+ return [...found].sort();
124
+ }
125
+ function protectedSecretLineHashes(text) {
126
+ const hashes = [];
127
+ let section = '';
128
+ for (const line of String(text || '').split(/\r?\n/)) {
129
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
130
+ if (sectionMatch) {
131
+ section = String(sectionMatch[1] || '');
132
+ continue;
133
+ }
134
+ const key = line.match(/^\s*(?:export\s+)?([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
135
+ if (!key)
136
+ continue;
137
+ if (isProtectedSecretKey(key) || (section && isProtectedSecretKey(`${section}.${key}`)))
138
+ hashes.push(sha256(line));
139
+ }
140
+ return hashes.sort();
92
141
  }
93
142
  function lookupPath(value, dotted) {
94
143
  let current = value;