sneakoscope 2.0.16 → 2.0.18

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 (63) hide show
  1. package/README.md +23 -30
  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/command-registry.js +1 -1
  8. package/dist/commands/doctor.js +39 -1
  9. package/dist/commands/proof.js +21 -0
  10. package/dist/commands/zellij-slot-pane.js +7 -1
  11. package/dist/core/agents/agent-effort-policy.js +7 -1
  12. package/dist/core/agents/agent-orchestrator.js +3 -1
  13. package/dist/core/agents/agent-scheduler.js +14 -1
  14. package/dist/core/agents/native-cli-session-swarm.js +11 -7
  15. package/dist/core/agents/native-cli-worker.js +56 -7
  16. package/dist/core/agents/parallel-runtime-proof.js +68 -9
  17. package/dist/core/agents/runtime-proof-summary.js +75 -0
  18. package/dist/core/codex-app/codex-app-handoff.js +77 -0
  19. package/dist/core/codex-control/codex-0138-capability.js +64 -0
  20. package/dist/core/codex-control/codex-model-capabilities.js +41 -0
  21. package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
  22. package/dist/core/codex-control/codex-task-runner.js +1 -1
  23. package/dist/core/codex-plugins/codex-plugin-json.js +152 -0
  24. package/dist/core/commands/mad-sks-command.js +4 -0
  25. package/dist/core/commands/naruto-command.js +20 -4
  26. package/dist/core/commands/qa-loop-command.js +111 -4
  27. package/dist/core/commands/team-command.js +6 -311
  28. package/dist/core/commands/team-legacy-observe-command.js +182 -0
  29. package/dist/core/db-safety.js +15 -0
  30. package/dist/core/doctor/codex-0138-doctor.js +104 -0
  31. package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
  32. package/dist/core/effort-orchestrator.js +9 -0
  33. package/dist/core/feature-registry.js +4 -2
  34. package/dist/core/fsx.js +1 -1
  35. package/dist/core/hooks-runtime.js +38 -4
  36. package/dist/core/image/image-artifact-path-contract.js +99 -0
  37. package/dist/core/image-ux-review/imagegen-adapter.js +24 -3
  38. package/dist/core/init.js +1 -0
  39. package/dist/core/mad-db/mad-db-capability.js +9 -1
  40. package/dist/core/mad-db/mad-db-result-lifecycle.js +207 -0
  41. package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
  42. package/dist/core/mcp/mcp-server-policy.js +24 -0
  43. package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
  44. package/dist/core/qa-loop.js +28 -2
  45. package/dist/core/release/release-gate-affected-selector.js +47 -5
  46. package/dist/core/release/release-gate-dag.js +5 -1
  47. package/dist/core/release/release-gate-scheduler.js +2 -1
  48. package/dist/core/routes.js +3 -1
  49. package/dist/core/usage/codex-account-usage.js +78 -0
  50. package/dist/core/version.js +1 -1
  51. package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
  52. package/dist/core/zellij/zellij-slot-pane-renderer.js +92 -1
  53. package/dist/core/zellij/zellij-slot-telemetry.js +29 -6
  54. package/dist/core/zellij/zellij-ui-mode.js +12 -2
  55. package/dist/scripts/prepublish-release-check-or-fast.js +3 -3
  56. package/dist/scripts/release-gate-existence-audit.js +5 -1
  57. package/dist/scripts/release-speed-summary.js +22 -2
  58. package/package.json +38 -4
  59. package/schemas/agents/parallel-runtime-proof.schema.json +31 -0
  60. package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
  61. package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
  62. package/schemas/image/image-artifact-path-contract.schema.json +32 -0
  63. package/schemas/usage/codex-account-usage.schema.json +27 -0
@@ -7,6 +7,7 @@ import { detectImagegenCapability } from '../imagegen/imagegen-capability.js';
7
7
  import { validateGptImage2Request } from '../imagegen/gpt-image-2-request-validator.js';
8
8
  import { withResponsesRetry } from '../responses-retry-policy.js';
9
9
  import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-discovery.js';
10
+ import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
10
11
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
11
12
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
12
13
  return [
@@ -124,6 +125,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
124
125
  real_generated: true
125
126
  });
126
127
  const outputSource = manualOutput ? 'manual_attach' : 'auto_discovered_generated_images';
128
+ const imageContract = await writeGeneratedImagePathContract(input, dest, 'codex_app_imagegen').catch(() => null);
127
129
  await writeJsonAtomic(responseArtifact, {
128
130
  schema: 'sks.image-ux-gpt-image-2-response.v1',
129
131
  created_at: nowIso(),
@@ -135,6 +137,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
135
137
  output_image_sha256: meta.sha256,
136
138
  output_id: meta.output_id,
137
139
  output_source: outputSource,
140
+ image_artifact_path_contract: imageContract?.artifact_path || null,
138
141
  discovered_from: discovery?.selected?.path || null,
139
142
  discovery: discovery ? { candidates_considered: discovery.candidates_considered, since_ms: discovery.since_ms, max_age_ms: discovery.max_age_ms } : null,
140
143
  local_only: true
@@ -149,6 +152,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
149
152
  output_source: outputSource,
150
153
  request_artifact: requestArtifact,
151
154
  response_artifact: responseArtifact,
155
+ image_artifact_path_contract: imageContract?.artifact_path || null,
152
156
  latency_ms: null
153
157
  };
154
158
  }
@@ -241,6 +245,7 @@ export function createFakeImagegenAdapter(opts = {}) {
241
245
  real_generated: false,
242
246
  mock: true
243
247
  });
248
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'fake_imagegen_adapter').catch(() => null);
244
249
  await writeJsonAtomic(responseArtifact, {
245
250
  schema: 'sks.image-ux-gpt-image-2-response.v1',
246
251
  created_at: nowIso(),
@@ -251,6 +256,7 @@ export function createFakeImagegenAdapter(opts = {}) {
251
256
  output_image_path: out,
252
257
  output_image_sha256: meta.sha256,
253
258
  output_id: meta.output_id,
259
+ image_artifact_path_contract: imageContract?.artifact_path || null,
254
260
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
255
261
  latency_ms: Date.now() - started,
256
262
  fake_adapter: true,
@@ -259,7 +265,7 @@ export function createFakeImagegenAdapter(opts = {}) {
259
265
  mock: true,
260
266
  local_only: true
261
267
  });
262
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
268
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
263
269
  }
264
270
  };
265
271
  }
@@ -385,6 +391,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
385
391
  output_id: generated.id || payload?.id || null,
386
392
  real_generated: true
387
393
  });
394
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_responses_image_generation').catch(() => null);
388
395
  await writeJsonAtomic(responseArtifact, {
389
396
  schema: 'sks.image-ux-gpt-image-2-response.v1',
390
397
  created_at: nowIso(),
@@ -397,12 +404,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
397
404
  output_image_path: out,
398
405
  output_image_sha256: meta.sha256,
399
406
  output_id: meta.output_id,
407
+ image_artifact_path_contract: imageContract?.artifact_path || null,
400
408
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
401
409
  latency_ms: Date.now() - started,
402
410
  token_cost_metadata: payload?.usage || null,
403
411
  local_only: true
404
412
  });
405
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
413
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
406
414
  }
407
415
  const sourceBytes = await fsp.readFile(sourcePath);
408
416
  const qualityParam = imagegenQualityParam(opts);
@@ -440,6 +448,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
440
448
  output_id: image?.id || payload?.id || null,
441
449
  real_generated: true
442
450
  });
451
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_images_api').catch(() => null);
443
452
  await writeJsonAtomic(responseArtifact, {
444
453
  schema: 'sks.image-ux-gpt-image-2-response.v1',
445
454
  created_at: nowIso(),
@@ -451,12 +460,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
451
460
  output_image_path: out,
452
461
  output_image_sha256: meta.sha256,
453
462
  output_id: meta.output_id,
463
+ image_artifact_path_contract: imageContract?.artifact_path || null,
454
464
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
455
465
  latency_ms: Date.now() - started,
456
466
  token_cost_metadata: payload?.usage || null,
457
467
  local_only: true
458
468
  });
459
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
469
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
460
470
  }
461
471
  catch (err) {
462
472
  const provider = useResponsesImageTool ? 'openai_responses_image_generation' : 'openai_images_api';
@@ -468,6 +478,17 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
468
478
  }
469
479
  };
470
480
  }
481
+ async function writeGeneratedImagePathContract(input, outputPath, provider) {
482
+ return writeImageArtifactPathContract(process.cwd(), {
483
+ missionId: input.mission_id || 'unassigned',
484
+ images: [{
485
+ id: `${provider}-${input.source_screen_id || 'screen'}`,
486
+ kind: 'generated_image',
487
+ filePath: outputPath
488
+ }],
489
+ artifactPath: path.join(input.output_dir, 'image-artifact-path-contract.json')
490
+ });
491
+ }
471
492
  export async function generateGptImage2CalloutReview(input, opts = {}) {
472
493
  if (opts.fake === true || process.env.SKS_TEST_FAKE_IMAGEGEN === '1') {
473
494
  return createFakeImagegenAdapter(opts.fakeAdapter || {}).generateCalloutReview(input);
package/dist/core/init.js CHANGED
@@ -1043,6 +1043,7 @@ export async function installSkills(root) {
1043
1043
  'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Research is not an implementation route: do not edit repository source, docs, package metadata, generated skills, or harness files; write only route-local mission artifacts under .sneakoscope/missions/<mission-id>/. Run the genius-lens agent council with named persona-inspired cognitive roles: Einstein Agent, Feynman Agent, Turing Agent, von Neumann Agent, and Skeptic Agent. These are lenses only; do not impersonate the historical people. Every Research agent ledger row must include display_name, persona, persona_boundary, effort=xhigh, reasoning_effort=xhigh, service_tier when available, one literal "Eureka!" idea, falsifiers, cheap_probes, and challenge_or_response before synthesis. This is not a fixed three-cycle route: repeat source gathering, Eureka ideas, evidence-bound debate, falsification, and synthesis pressure until every agent records final agreement, or until the explicit max-cycle safety cap pauses with an unpassed gate. Create research-source-skill.md as a route-local Skill Creator artifact, then maximize layered public web/source search across latest papers, official/government or leading-institution data, standards/primary docs, current news, public discourse, developer/practitioner sources, traditional background sources, and counterevidence before synthesis. Record research-source-skill.md, source-ledger.json, agent-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and research-gate.json. debate-ledger.json must include consensus_iterations, unanimous_consensus, and per-agent agreements; research-gate.json cannot pass until unanimous_consensus=true with every agent agreement recorded. Context7 is optional and only needed when the research topic depends on external package/API/framework docs; do not use it as the default research evidence layer. Normal Research may take one or two hours when needed; favor real source collection, cross-layer comparison, falsification, and a concise paper manuscript over speed. Do not use --mock except for selftests or dry harness checks; if live source execution is unavailable, record a blocker and keep the gate unpassed. Do not use for ordinary code edits.\n`,
1044
1044
  'autoresearch': `---\nname: autoresearch\ndescription: Dollar-command route for $AutoResearch or $autoresearch iterative experiment loops.\n---\n\nUse for $AutoResearch, iterative improvement, SEO/GEO, ranking, workflow, benchmark, or experiments. Define program, hypothesis, experiment, metric, keep/discard, falsification, next step, and Honest Mode. Load seo-geo-optimizer for README/npm/GitHub/schema/AI-search work.\n`,
1045
1045
  'db': `---\nname: db\ndescription: Dollar-command route for $DB or $db database and Supabase safety checks.\n---\n\nUse when the user invokes $DB/$db or the task touches SQL, Supabase, Postgres, migrations, Prisma, Drizzle, Knex, MCP database tools, or production data. Run or follow sks db policy, sks db scan, sks db classify, and sks db check. Destructive database operations remain forbidden.\n`,
1046
+ 'mad-db': `---\nname: mad-db\ndescription: One-cycle Mad-DB break-glass route alias for $MAD-DB and $mad-db database safety work.\n---\n\nUse only when the user explicitly invokes $MAD-DB/$mad-db or asks for Mad-DB visibility. Treat it as a DB safety route with one-cycle break-glass controls, not as a general permanent DB unlock. Prefer \`sks mad-db status --json\` for inspection, \`sks mad-db enable --ack "I AUTHORIZE ONE-CYCLE DB BREAK-GLASS" [--mission latest|new|M-...] --json\` only after explicit user authorization, and \`sks mad-db revoke --mission <id|latest> --json\` to close the cycle. Keep catastrophic safeguards active: whole database/schema/table removal, truncate, all-row delete/update, reset, dangerous project/branch management, credential exfiltration, persistent security weakening, and unrequested fallback implementation remain blocked. Pair with db-safety-guard, Context7 evidence when external DB/API docs are involved, route-local reflection, and Honest Mode.\n`,
1046
1047
  'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped permission widening across approved target-project surfaces.\n---\n\nUse only when the user explicitly invokes $MAD-SKS or top-level sks --mad. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened permission applies only while the active mission gate is open, must be deactivated when the task ends, and can open approved scopes such as target-project file writes, shell commands, package installs, local service control, network operations, browser/Computer Use workflows, generated assets, file permissions, migrations, Supabase MCP database writes, column/schema cleanup, direct execute SQL, and normal targeted DB writes. Keep catastrophic safeguards active: whole database/schema/table removal, truncate, all-row delete/update, reset, dangerous project/branch management, credential exfiltration, persistent security weakening, destructive delete without explicit confirmation, and unrequested fallback implementation remain blocked. Do not carry MAD-SKS permission into later prompts or routes. The permission profile source is centralized in src/core/permission-gates.ts and emitted as dist/core/permission-gates.js so skill/hook/MCP-style gates share one decision function.\n`,
1047
1048
  'gx': `---\nname: gx\ndescription: Dollar-command route for $GX or $gx deterministic GX visual context cartridges.\n---\n\nUse when the user invokes $GX/$gx or asks for architecture/context visualization through SKS. Prefer sks gx init, render, validate, drift, and snapshot. vgraph.json remains the source of truth.\n`,
1048
1049
  'help': `---\nname: help\ndescription: Dollar-command route for $Help or $help explaining installed SKS commands and workflows.\n---\n\nUse when the user invokes $Help/$help or asks what commands exist. Prefer concise output from sks commands, sks usage <topic>, sks quickstart, sks aliases, and sks codex-app.\n`,
@@ -89,7 +89,7 @@ export async function recordMadDbOperation(root, missionId, input = {}) {
89
89
  }
90
90
  export async function consumeMadDbCapability(root, missionId, input = {}) {
91
91
  const capability = await readMadDbCapability(root, missionId);
92
- if (!isMadDbCapabilityActive(capability))
92
+ if (!capability || capability.consumed === true)
93
93
  return capability;
94
94
  const consumed = {
95
95
  ...capability,
@@ -103,6 +103,14 @@ export async function consumeMadDbCapability(root, missionId, input = {}) {
103
103
  await appendJsonlBounded(path.join(dir, 'mad-db-ledger.jsonl'), { ts: nowIso(), type: 'capability.consumed', mission_id: missionId, cycle_id: consumed.cycle_id, consumed_by: consumed.consumed_by });
104
104
  return consumed;
105
105
  }
106
+ export async function closeMadDbCycle(root, missionId, cycleId) {
107
+ const capability = await readMadDbCapability(root, missionId);
108
+ if (!capability || capability.cycle_id !== cycleId)
109
+ return capability;
110
+ if (capability.consumed === true)
111
+ return capability;
112
+ return consumeMadDbCapability(root, missionId, { consumedBy: 'mad-db-cycle-close', reason: 'mad_db_cycle_closed' });
113
+ }
106
114
  export async function revokeMadDbCapability(root, missionId, reason = 'operator_revoked') {
107
115
  const capability = await readMadDbCapability(root, missionId);
108
116
  if (!capability)
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { appendJsonlBounded, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
4
+ import { missionDir } from '../mission.js';
5
+ import { appendMadDbOperationLifecycle } from './mad-db-ledger.js';
6
+ const PENDING_FILE = 'mad-db-lifecycle-pending.jsonl';
7
+ const PENDING_LATEST_FILE = 'mad-db-lifecycle-pending.latest.json';
8
+ export async function recordPendingMadDbLifecycleHook(root, missionId, hook) {
9
+ const dir = missionDir(root, missionId);
10
+ const row = {
11
+ schema: 'sks.mad-db-lifecycle-pending.v1',
12
+ ts: nowIso(),
13
+ mission_id: missionId,
14
+ hook
15
+ };
16
+ await appendJsonlBounded(path.join(dir, PENDING_FILE), row);
17
+ await writeJsonAtomic(path.join(dir, PENDING_LATEST_FILE), row).catch(() => undefined);
18
+ return row;
19
+ }
20
+ export async function readLatestPendingMadDbLifecycleHook(root, missionId, payload = {}) {
21
+ const dir = missionDir(root, missionId);
22
+ const embedded = lifecycleHookFromUnknown(payload);
23
+ if (embedded)
24
+ return embedded;
25
+ const latest = await readJson(path.join(dir, PENDING_LATEST_FILE), null).catch(() => null);
26
+ const latestHook = lifecycleHookFromUnknown(latest?.hook);
27
+ if (latestHook && hookMatchesPayload(latestHook, payload))
28
+ return latestHook;
29
+ const text = await readText(path.join(dir, PENDING_FILE), '').catch(() => '');
30
+ const rows = String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).reverse();
31
+ for (const line of rows.slice(0, 50)) {
32
+ try {
33
+ const row = JSON.parse(line);
34
+ const hook = lifecycleHookFromUnknown(row?.hook);
35
+ if (hook && hookMatchesPayload(hook, payload))
36
+ return hook;
37
+ }
38
+ catch {
39
+ // Ignore malformed pending rows.
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ export async function recordMadDbToolResult(input) {
45
+ const terminalType = input.ok ? 'db_operation.succeeded' : 'db_operation.failed';
46
+ if (await hasTerminalLifecycleEvent(input.root, input.missionId, input.hook.operation_id)) {
47
+ return {
48
+ schema: 'sks.mad-db-tool-result-lifecycle.v1',
49
+ ok: true,
50
+ skipped: true,
51
+ reason: 'mad_db_operation_terminal_event_already_recorded',
52
+ operation_id: input.hook.operation_id
53
+ };
54
+ }
55
+ const event = await appendMadDbOperationLifecycle(input.root, input.missionId, {
56
+ type: terminalType,
57
+ operationId: input.hook.operation_id,
58
+ cycleId: input.hook.cycle_id || null,
59
+ mcpServer: input.hook.mcp_server || null,
60
+ toolName: input.hook.tool_name || null,
61
+ sqlHash: input.hook.sql_hash || null,
62
+ destructive: input.hook.destructive === true,
63
+ resultStatus: input.ok ? 'succeeded' : 'failed',
64
+ rowCount: input.rowCount ?? null,
65
+ error: input.error || null
66
+ });
67
+ await markPendingHookResolved(input.root, input.missionId, input.hook, input.ok);
68
+ return {
69
+ schema: 'sks.mad-db-tool-result-lifecycle.v1',
70
+ ok: true,
71
+ skipped: false,
72
+ operation_id: input.hook.operation_id,
73
+ result_status: input.ok ? 'succeeded' : 'failed',
74
+ event
75
+ };
76
+ }
77
+ export async function maybeRecordMadDbToolResultFromToolUse(input) {
78
+ const payload = input.toolResult ?? input.toolCallPayload ?? {};
79
+ const hook = lifecycleHookFromUnknown(input.decision)
80
+ || lifecycleHookFromUnknown(input.toolCallPayload)
81
+ || lifecycleHookFromUnknown(input.toolResult)
82
+ || await readLatestPendingMadDbLifecycleHook(input.root, input.missionId, input.toolCallPayload || payload);
83
+ if (!hook)
84
+ return null;
85
+ const ok = !madDbToolUseFailed(payload);
86
+ return recordMadDbToolResult({
87
+ root: input.root,
88
+ missionId: input.missionId,
89
+ hook,
90
+ ok,
91
+ rowCount: extractRowCount(payload),
92
+ error: ok ? null : extractToolError(payload)
93
+ });
94
+ }
95
+ export function lifecycleHookFromUnknown(value) {
96
+ const candidate = value?.ledger_result_hook || value?.mad_db?.ledger_result_hook || value;
97
+ const missionId = stringOrNull(candidate?.mission_id || candidate?.missionId);
98
+ const operationId = stringOrNull(candidate?.operation_id || candidate?.operationId);
99
+ if (!missionId || !operationId)
100
+ return null;
101
+ return {
102
+ mission_id: missionId,
103
+ operation_id: operationId,
104
+ cycle_id: stringOrNull(candidate?.cycle_id || candidate?.cycleId),
105
+ tool_name: stringOrNull(candidate?.tool_name || candidate?.toolName),
106
+ sql_hash: stringOrNull(candidate?.sql_hash || candidate?.sqlHash),
107
+ mcp_server: stringOrNull(candidate?.mcp_server || candidate?.mcpServer),
108
+ destructive: candidate?.destructive === true
109
+ };
110
+ }
111
+ function hookMatchesPayload(hook, payload) {
112
+ if (!hook.tool_name)
113
+ return true;
114
+ const toolText = [
115
+ payload.tool_name,
116
+ payload.toolName,
117
+ payload.name,
118
+ payload.tool?.name,
119
+ payload.server,
120
+ payload.mcp_tool,
121
+ payload.tool,
122
+ payload.type
123
+ ].filter(Boolean).join(' ').toLowerCase();
124
+ if (!toolText)
125
+ return true;
126
+ return toolText.includes(String(hook.tool_name).toLowerCase()) || String(hook.tool_name).toLowerCase().includes(toolText);
127
+ }
128
+ function madDbToolUseFailed(payload = {}) {
129
+ if (payload?.isError === true || payload?.tool_response?.isError === true || payload?.toolResponse?.isError === true || payload?.result?.isError === true)
130
+ return true;
131
+ const candidates = [
132
+ payload.exit_code,
133
+ payload.exitCode,
134
+ payload.tool_response?.exit_code,
135
+ payload.toolResponse?.exitCode,
136
+ payload.result?.exit_code,
137
+ payload.result?.exitCode
138
+ ];
139
+ for (const candidate of candidates) {
140
+ if (candidate === undefined || candidate === null || candidate === '')
141
+ continue;
142
+ const n = Number(candidate);
143
+ if (Number.isFinite(n))
144
+ return n !== 0;
145
+ }
146
+ if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false)
147
+ return true;
148
+ if (payload.executed === false)
149
+ return true;
150
+ return false;
151
+ }
152
+ function extractRowCount(payload = {}) {
153
+ const candidates = [
154
+ payload.row_count,
155
+ payload.rowCount,
156
+ payload.tool_response?.row_count,
157
+ payload.tool_response?.rowCount,
158
+ payload.toolResponse?.rowCount,
159
+ payload.result?.row_count,
160
+ payload.result?.rowCount,
161
+ payload.result?.rows_affected,
162
+ payload.tool_response?.rows_affected
163
+ ];
164
+ for (const candidate of candidates) {
165
+ if (candidate === undefined || candidate === null || candidate === '')
166
+ continue;
167
+ const parsed = Number(candidate);
168
+ if (Number.isFinite(parsed))
169
+ return parsed;
170
+ }
171
+ return null;
172
+ }
173
+ function extractToolError(payload = {}) {
174
+ if (payload?.result?.isError === true && Array.isArray(payload.result.content)) {
175
+ const text = payload.result.content.map((entry) => entry?.text || entry?.message || '').filter(Boolean).join('\n');
176
+ if (text.trim())
177
+ return text.trim();
178
+ }
179
+ return String(payload.error || payload.message || payload.stderr || payload.tool_response?.stderr || payload.toolResponse?.stderr || payload.result?.stderr || payload.result?.error || 'tool_failed');
180
+ }
181
+ async function hasTerminalLifecycleEvent(root, missionId, operationId) {
182
+ const ledger = path.join(missionDir(root, missionId), 'mad-db-ledger.jsonl');
183
+ const text = await readText(ledger, '').catch(() => '');
184
+ return String(text).split(/\r?\n/).some((line) => {
185
+ if (!line.includes(operationId))
186
+ return false;
187
+ return line.includes('db_operation.succeeded') || line.includes('db_operation.failed');
188
+ });
189
+ }
190
+ async function markPendingHookResolved(root, missionId, hook, ok) {
191
+ const dir = missionDir(root, missionId);
192
+ const row = {
193
+ schema: 'sks.mad-db-lifecycle-pending-resolution.v1',
194
+ ts: nowIso(),
195
+ mission_id: missionId,
196
+ operation_id: hook.operation_id,
197
+ cycle_id: hook.cycle_id || null,
198
+ result_status: ok ? 'succeeded' : 'failed'
199
+ };
200
+ await appendJsonlBounded(path.join(dir, 'mad-db-lifecycle-resolved.jsonl'), row).catch(() => undefined);
201
+ await fs.rm(path.join(dir, PENDING_LATEST_FILE), { force: true }).catch(() => undefined);
202
+ }
203
+ function stringOrNull(value) {
204
+ const text = String(value || '').trim();
205
+ return text ? text : null;
206
+ }
207
+ //# sourceMappingURL=mad-db-result-lifecycle.js.map
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import { buildCodexPluginInventory } from '../codex-plugins/codex-plugin-json.js';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { policyForPluginMcpServer } from './mcp-server-policy.js';
5
+ export function buildMcpPluginServerCandidates(inventory) {
6
+ const candidates = inventory.plugins.flatMap((plugin) => plugin.remote_mcp_servers.map((server) => policyForPluginMcpServer({
7
+ pluginId: plugin.id,
8
+ name: server.name,
9
+ url: server.url,
10
+ authType: server.auth_type
11
+ })));
12
+ return {
13
+ schema: 'sks.mcp-plugin-server-candidates.v1',
14
+ generated_at: nowIso(),
15
+ candidates,
16
+ candidate_only: true,
17
+ blockers: []
18
+ };
19
+ }
20
+ export async function writeMcpPluginInventoryArtifacts(root, input = {}) {
21
+ const inventory = input.inventory || await buildCodexPluginInventory();
22
+ const candidates = buildMcpPluginServerCandidates(inventory);
23
+ const pluginArtifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.json');
24
+ const candidateArtifact = path.join(root, '.sneakoscope', 'mcp-plugin-server-candidates.json');
25
+ await writeJsonAtomic(pluginArtifact, inventory);
26
+ await writeJsonAtomic(candidateArtifact, candidates);
27
+ return { inventory, candidates, plugin_artifact: pluginArtifact, candidate_artifact: candidateArtifact };
28
+ }
29
+ //# sourceMappingURL=mcp-plugin-inventory.js.map
@@ -0,0 +1,24 @@
1
+ export function policyForPluginMcpServer(input) {
2
+ const haystack = `${input.name} ${input.url || ''}`.toLowerCase();
3
+ const dbRelated = /supabase|postgres|database|sql|db\b/.test(haystack);
4
+ const oauth = /oauth/i.test(String(input.authType || ''));
5
+ return {
6
+ name: input.name,
7
+ plugin_id: input.pluginId,
8
+ url: input.url || null,
9
+ auth_type: input.authType || null,
10
+ candidate_only: true,
11
+ auto_enable: false,
12
+ destructive_tools_auto_enabled: false,
13
+ db_safety_required: dbRelated,
14
+ mad_db_required_for_destructive: dbRelated,
15
+ oauth_prerefresh_recommended: oauth,
16
+ policy_notes: [
17
+ 'Remote MCP servers from plugin detail are candidate only.',
18
+ 'Do not auto-enable destructive MCP tools.',
19
+ dbRelated ? 'DB MCP servers require DB safety and Mad-DB for destructive operations.' : 'Non-DB MCP candidate still requires explicit operator enablement.',
20
+ oauth ? 'OAuth-backed MCP should trigger pre-refresh doctor check.' : ''
21
+ ].filter(Boolean)
22
+ };
23
+ }
24
+ //# sourceMappingURL=mcp-server-policy.js.map
@@ -0,0 +1,37 @@
1
+ import { defaultModelCallBudget } from '../codex-control/model-call-concurrency.js';
2
+ import { codexModelEffortCapability, nextAdvertisedEffort } from '../codex-control/codex-model-capabilities.js';
3
+ export function buildQaLoopBudgetPolicy(input = {}) {
4
+ const usage = input.usage || null;
5
+ const available = Boolean(usage?.token_usage);
6
+ const limit = Number(usage?.usage_limit_tokens || 0);
7
+ const total = Number(usage?.token_usage?.total_tokens || 0);
8
+ const nearLimit = Boolean(limit > 0 && total / limit >= 0.9);
9
+ const baseBudget = defaultModelCallBudget(String(input.provider || 'codex-sdk'));
10
+ return {
11
+ schema: 'sks.qa-loop-budget-policy.v1',
12
+ ok: true,
13
+ account_usage_source: usage?.source || 'unavailable',
14
+ token_usage_available: available,
15
+ near_limit: nearLimit,
16
+ remote_model_call_concurrency: nearLimit ? Math.max(1, Math.min(2, baseBudget)) : baseBudget,
17
+ local_llm_draft_preferred: nearLimit,
18
+ final_reviewer_gpt_backed: true,
19
+ warnings: available ? [] : ['codex_account_usage_unavailable_no_hard_block']
20
+ };
21
+ }
22
+ export function selectQaLoopEscalatedEffort(input = {}) {
23
+ const capability = input.capability || codexModelEffortCapability();
24
+ const current = input.currentEffort || capability.default_effort;
25
+ const failureCount = Number(input.failureCount || 0);
26
+ return {
27
+ schema: 'sks.qa-loop-effort-escalation.v1',
28
+ model: capability.model,
29
+ advertised_efforts: capability.advertised_efforts,
30
+ order_source: capability.order_source,
31
+ failure_count: failureCount,
32
+ current_effort: current,
33
+ next_effort: failureCount >= 2 ? nextAdvertisedEffort(current, capability) : current,
34
+ escalated: failureCount >= 2 && nextAdvertisedEffort(current, capability) !== current
35
+ };
36
+ }
37
+ //# sourceMappingURL=qa-loop-budget-policy.js.map
@@ -309,6 +309,14 @@ export function defaultQaGate(contract = {}, opts = {}) {
309
309
  ui_chrome_extension_evidence: !uiRequired,
310
310
  ui_computer_use_evidence: false,
311
311
  ui_evidence_source: uiRequired ? null : 'not_required',
312
+ desktop_app_handoff_required: false,
313
+ desktop_app_handoff_status: 'not_requested',
314
+ desktop_app_handoff_artifact: null,
315
+ desktop_app_handoff_supported: false,
316
+ desktop_app_handoff_is_web_ui_evidence: false,
317
+ image_artifact_path_contract_present: false,
318
+ image_artifact_path_contract_artifact: null,
319
+ image_artifact_path_contract_blockers: [],
312
320
  api_e2e_required: apiRequired,
313
321
  unsafe_external_side_effects: false,
314
322
  corrective_loop_enabled: corrective,
@@ -374,6 +382,15 @@ export async function evaluateQaGate(dir) {
374
382
  if (evidenceMentionsForbiddenWebComputerUseEvidence({ evidence: gate.evidence, ui_evidence_source: gate.ui_evidence_source }))
375
383
  reasons.push('computer_use_web_evidence_forbidden');
376
384
  }
385
+ if (gate.desktop_app_handoff_required === true) {
386
+ if (gate.desktop_app_handoff_status !== 'pending' && gate.desktop_app_handoff_status !== 'completed')
387
+ reasons.push('desktop_app_handoff_missing');
388
+ if (gate.desktop_app_handoff_is_web_ui_evidence === true)
389
+ reasons.push('desktop_app_handoff_misused_as_web_evidence');
390
+ }
391
+ const imageBlockers = Array.isArray(gate.image_artifact_path_contract_blockers) ? gate.image_artifact_path_contract_blockers : [];
392
+ if (imageBlockers.includes('image_generated_file_path_missing'))
393
+ reasons.push('image_generated_file_path_missing');
377
394
  if (!reportFile)
378
395
  reasons.push('qa_report_file_missing');
379
396
  else if (!isQaReportFilename(reportFile))
@@ -396,8 +413,14 @@ export async function writeMockQaResult(dir, mission, contract) {
396
413
  await writeJsonAtomic(path.join(dir, 'qa-gate.json'), { ...defaultQaGate(contract, { reportFile }), passed: !uiRequired, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, chrome_extension_preflight_passed: !uiRequired, ui_chrome_extension_evidence: !uiRequired, ui_computer_use_evidence: false, ui_evidence_source: uiRequired ? null : 'not_required', unresolved_findings: 0, unresolved_fixable_findings: 0, unsafe_or_deferred_findings: 0, post_fix_verification_complete: true, honest_mode_complete: true, evidence: ['mock QA-LOOP smoke completed'], notes: ['No live UI/API verification was claimed.'] });
397
414
  return evaluateQaGate(dir);
398
415
  }
399
- export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile }) {
416
+ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
400
417
  const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
418
+ const imageContractText = imagePathContract
419
+ ? `\nIMAGE PATH CONTRACT:\n${JSON.stringify(imagePathContract, null, 2)}\nUse model_visible_path values for follow-up image edits; do not invent generated image paths.\n`
420
+ : '';
421
+ const appHandoffText = appHandoff
422
+ ? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
423
+ : '';
401
424
  return `SKS QA-LOOP
402
425
  MISSION: ${id}
403
426
  TASK: ${mission.prompt}
@@ -410,6 +433,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
410
433
  ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
411
434
  CONTRACT:
412
435
  ${JSON.stringify(contract, null, 2)}
436
+ ${imageContractText}${appHandoffText}
413
437
  Previous tail:
414
438
  ${String(previous || '').slice(-2500)}
415
439
  `;
@@ -417,9 +441,11 @@ ${String(previous || '').slice(-2500)}
417
441
  export async function qaStatus(dir) {
418
442
  const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
419
443
  const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
444
+ const appHandoff = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
445
+ const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
420
446
  const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
421
447
  const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
422
- return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()) };
448
+ return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, image_path_contract: imagePathContract };
423
449
  }
424
450
  function qaChecklist(a) {
425
451
  const cases = [
@@ -10,7 +10,7 @@ export function selectAffectedReleaseGates(root, manifest, gates, input = {}) {
10
10
  if (input.full) {
11
11
  return selectionResult(gates, gates, [], 'full', {}, []);
12
12
  }
13
- const changedFiles = resolveChangedFiles(root, input.changedSince || 'auto');
13
+ const changedFiles = input.changedFiles ? [...new Set(input.changedFiles)].sort() : resolveChangedFiles(root, input.changedSince || 'auto');
14
14
  const selected = [];
15
15
  const reasons = {};
16
16
  for (const gate of gates) {
@@ -28,7 +28,9 @@ export function selectAffectedReleaseGates(root, manifest, gates, input = {}) {
28
28
  reasons[gate.id] = 'always_keep_core_release_safety';
29
29
  }
30
30
  }
31
- const expanded = expandWithDependencies(selected, manifest);
31
+ const expanded = input.preset === 'affected' || input.preset === 'fast'
32
+ ? selected
33
+ : expandWithDependencies(selected, manifest);
32
34
  const ordered = manifest.gates.filter((gate) => expanded.some((row) => row.id === gate.id));
33
35
  return selectionResult(gates, ordered, changedFiles, 'affected', reasons, gates.filter((gate) => !ordered.some((row) => row.id === gate.id)).map((gate) => gate.id));
34
36
  }
@@ -53,19 +55,32 @@ function gateSelectionReason(gate, changedFiles, preset) {
53
55
  return 'always_keep_core_release_safety';
54
56
  if (!changedFiles.length)
55
57
  return preset === 'fast' ? 'fast_no_diff_core_only_skip' : 'no_changed_files';
58
+ const releaseGate = /^(release:|publish:|prepublish)/.test(gate.id);
56
59
  if (changedFiles.some((file) => file === 'package.json' || file === 'package-lock.json')) {
57
60
  if (/^(release:|publish:|prepublish|runtime:|typecheck|schema:check)/.test(gate.id))
58
61
  return 'package_metadata_changed';
59
62
  }
60
- if (changedFiles.some((file) => file === 'release-gates.v2.json' || file.startsWith('src/core/release/') || file.startsWith('src/scripts/release-')))
61
- return 'release_gate_system_changed';
63
+ if (changedFiles.some((file) => file === 'release-gates.v2.json' || file.startsWith('src/core/release/'))) {
64
+ if (releaseGate)
65
+ return 'release_gate_system_changed';
66
+ }
67
+ const matchingReleaseScript = changedFiles.some((file) => releaseScriptGateCandidates(file).includes(gate.id));
68
+ if (matchingReleaseScript)
69
+ return 'release_script_changed';
70
+ if (changedFiles.some((file) => file.startsWith('src/scripts/prepublish-') || file.startsWith('src/scripts/publish-'))) {
71
+ if (releaseGate && gate.id === 'release:version-truth')
72
+ return 'publish_or_prepublish_script_changed';
73
+ }
74
+ if (changedFiles.some((file) => file.startsWith('src/scripts/scheduler-') || file.startsWith('src/core/scheduler/'))) {
75
+ return gate.id.startsWith('scheduler:') ? 'scheduler_source_changed' : null;
76
+ }
62
77
  if (changedFiles.some((file) => file.startsWith('src/core/research/')))
63
78
  return gate.id.startsWith('research:') ? 'research_source_changed' : null;
64
79
  if (changedFiles.some((file) => file.startsWith('src/core/zellij/') || file.startsWith('src/commands/zellij')))
65
80
  return gate.id.startsWith('zellij:') || gate.id.startsWith('agent:zellij') || gate.id.startsWith('naruto:zellij') ? 'zellij_source_changed' : null;
66
81
  if (changedFiles.some((file) => file.includes('/db') || file.includes('mad-db') || file.includes('mcp')))
67
82
  return /db|mcp|mad-db|mad-sks/.test(gate.id) ? 'db_mcp_or_mad_db_changed' : null;
68
- const inputs = gate.cache?.inputs || [];
83
+ const inputs = (gate.cache?.inputs || []).filter((pattern) => !isBroadAffectedInput(pattern));
69
84
  if (inputs.some((pattern) => changedFiles.some((file) => matchesGlobish(file, pattern))))
70
85
  return 'cache_input_changed';
71
86
  return null;
@@ -110,4 +125,31 @@ function matchesGlobish(file, pattern) {
110
125
  return file.startsWith(normalized.slice(0, -1));
111
126
  return false;
112
127
  }
128
+ function isBroadAffectedInput(pattern) {
129
+ const normalized = pattern.replace(/\\/g, '/');
130
+ return new Set([
131
+ '**',
132
+ '**/*',
133
+ 'src/**',
134
+ 'src/**/*',
135
+ 'schemas/**',
136
+ 'schemas/**/*',
137
+ 'package.json',
138
+ 'package-lock.json',
139
+ 'release-gates.v2.json'
140
+ ]).has(normalized);
141
+ }
142
+ function releaseScriptGateCandidates(file) {
143
+ const normalized = file.replace(/\\/g, '/');
144
+ const base = normalized.split('/').pop()?.replace(/\.(ts|js|mjs|cjs)$/, '') || '';
145
+ if (!base.startsWith('release-'))
146
+ return [];
147
+ const rest = base.slice('release-'.length);
148
+ const withoutCheck = rest.replace(/-check$/, '');
149
+ return [
150
+ `release:${rest}`,
151
+ `release:${withoutCheck}`,
152
+ `release:${withoutCheck}:check`
153
+ ];
154
+ }
113
155
  //# sourceMappingURL=release-gate-affected-selector.js.map
@@ -26,6 +26,10 @@ export async function runReleaseGateDag(input) {
26
26
  ? selectAffectedReleaseGates(root, manifest, presetGates, { changedSince: input.changedSince || 'auto', preset })
27
27
  : selectAffectedReleaseGates(root, manifest, presetGates, { full: true, preset });
28
28
  const selected = affected.gates;
29
+ const selectedIds = new Set(selected.map((gate) => gate.id));
30
+ const affectedExternalSatisfiedDeps = affected.selection.mode === 'affected'
31
+ ? new Set(selected.flatMap((gate) => gate.deps || []).filter((dep) => !selectedIds.has(dep)))
32
+ : new Set();
29
33
  const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
30
34
  const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
31
35
  fs.mkdirSync(reportDir, { recursive: true });
@@ -88,7 +92,7 @@ export async function runReleaseGateDag(input) {
88
92
  writeReleaseGateJson(path.join(reportDir, 'explain.json'), { schema: RELEASE_GATE_NODE_SCHEMA, preset, budget, gates: selected.map((gate) => ({ id: gate.id, deps: gate.deps, resource: gate.resource, command: gate.command })) });
89
93
  }
90
94
  while (pending.size || running.size) {
91
- const ready = findReadyReleaseGateNodes({ pending, completed, failed });
95
+ const ready = findReadyReleaseGateNodes({ pending, completed, failed, satisfiedDeps: affectedExternalSatisfiedDeps });
92
96
  const launchable = pickReadyLaunchableReleaseGates({ ready, running: [...running.values()].map((row) => row.gate) });
93
97
  let progressed = false;
94
98
  for (const gate of launchable) {
@@ -1,6 +1,7 @@
1
1
  import { defaultReleaseGateBudget, pickLaunchableReleaseGates } from './release-gate-resource-governor.js';
2
2
  export function findReadyReleaseGateNodes(input) {
3
- return [...input.pending.values()].filter((gate) => gate.deps.every((dep) => input.completed.has(dep)) && !gate.deps.some((dep) => input.failed.has(dep)));
3
+ const satisfiedDeps = input.satisfiedDeps || new Set();
4
+ return [...input.pending.values()].filter((gate) => gate.deps.every((dep) => input.completed.has(dep) || satisfiedDeps.has(dep)) && !gate.deps.some((dep) => input.failed.has(dep)));
4
5
  }
5
6
  export function findReleaseGatesBlockedByFailedDeps(input) {
6
7
  return [...input.pending.values()].filter((gate) => gate.deps.some((dep) => input.failed.has(dep)));