sneakoscope 2.0.16 → 2.0.17

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 (37) hide show
  1. package/README.md +5 -3
  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/proof.js +21 -0
  9. package/dist/commands/zellij-slot-pane.js +7 -1
  10. package/dist/core/agents/agent-orchestrator.js +3 -1
  11. package/dist/core/agents/agent-scheduler.js +14 -1
  12. package/dist/core/agents/native-cli-session-swarm.js +11 -7
  13. package/dist/core/agents/native-cli-worker.js +56 -7
  14. package/dist/core/agents/parallel-runtime-proof.js +68 -9
  15. package/dist/core/agents/runtime-proof-summary.js +75 -0
  16. package/dist/core/commands/naruto-command.js +17 -3
  17. package/dist/core/commands/team-command.js +6 -311
  18. package/dist/core/commands/team-legacy-observe-command.js +182 -0
  19. package/dist/core/db-safety.js +15 -0
  20. package/dist/core/feature-registry.js +4 -2
  21. package/dist/core/fsx.js +1 -1
  22. package/dist/core/hooks-runtime.js +41 -4
  23. package/dist/core/init.js +1 -0
  24. package/dist/core/mad-db/mad-db-capability.js +9 -1
  25. package/dist/core/mad-db/mad-db-result-lifecycle.js +136 -0
  26. package/dist/core/release/release-gate-affected-selector.js +47 -5
  27. package/dist/core/release/release-gate-dag.js +5 -1
  28. package/dist/core/release/release-gate-scheduler.js +2 -1
  29. package/dist/core/routes.js +3 -1
  30. package/dist/core/version.js +1 -1
  31. package/dist/core/zellij/zellij-slot-pane-renderer.js +74 -1
  32. package/dist/core/zellij/zellij-slot-telemetry.js +29 -6
  33. package/dist/core/zellij/zellij-ui-mode.js +12 -2
  34. package/dist/scripts/prepublish-release-check-or-fast.js +3 -3
  35. package/dist/scripts/release-speed-summary.js +22 -2
  36. package/package.json +14 -3
  37. package/schemas/agents/parallel-runtime-proof.schema.json +31 -0
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '2.0.16';
8
+ export const PACKAGE_VERSION = '2.0.17';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -3,6 +3,7 @@ import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdi
3
3
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.js';
4
4
  import { missionDir, setCurrent, stateFile } from './mission.js';
5
5
  import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.js';
6
+ import { readLatestPendingMadDbLifecycleHook, recordMadDbToolResult } from './mad-db/mad-db-result-lifecycle.js';
6
7
  import { checkHarnessModification, harnessGuardBlockReason, isHarnessSourceProject } from './harness-guard.js';
7
8
  import { isMadSksRouteState } from './permission-gates.js';
8
9
  import { classifyMadSksShellCommand } from './mad-sks/write-guard.js';
@@ -422,10 +423,7 @@ function agentWorkerHookContext(state = {}, payload = {}) {
422
423
  || payload.agentWorker === true);
423
424
  }
424
425
  async function hookPostTool(root, state, payload, noQuestion) {
425
- const dbDecision = await checkDbOperation(root, state, payload, { duringNoQuestion: noQuestion });
426
- if (dbDecision.action === 'block' || dbDecision.action === 'confirm') {
427
- return { decision: 'block', reason: dbBlockReason(dbDecision) };
428
- }
426
+ await recordMadDbPostToolLifecycle(root, state, payload).catch(() => null);
429
427
  await recordContext7Evidence(root, state, payload).catch(() => null);
430
428
  await recordSubagentEvidence(root, state, payload).catch(() => null);
431
429
  if (toolFailed(payload))
@@ -449,6 +447,45 @@ async function hookPostTool(root, state, payload, noQuestion) {
449
447
  ? { continue: true, additionalContext: teamDigest.context, systemMessage: joinSystemMessages(visibleHookMessage('post-tool'), teamDigest.system) }
450
448
  : { continue: true };
451
449
  }
450
+ async function recordMadDbPostToolLifecycle(root, state = {}, payload = {}) {
451
+ if (!state?.mission_id)
452
+ return null;
453
+ const hook = await readLatestPendingMadDbLifecycleHook(root, String(state.mission_id), payload);
454
+ if (!hook)
455
+ return null;
456
+ return recordMadDbToolResult({
457
+ root,
458
+ missionId: String(state.mission_id),
459
+ hook,
460
+ ok: !toolFailed(payload),
461
+ rowCount: extractRowCount(payload),
462
+ error: toolFailed(payload) ? extractToolError(payload) : null
463
+ });
464
+ }
465
+ function extractRowCount(payload = {}) {
466
+ const candidates = [
467
+ payload.row_count,
468
+ payload.rowCount,
469
+ payload.tool_response?.row_count,
470
+ payload.tool_response?.rowCount,
471
+ payload.toolResponse?.rowCount,
472
+ payload.result?.row_count,
473
+ payload.result?.rowCount,
474
+ payload.result?.rows_affected,
475
+ payload.tool_response?.rows_affected
476
+ ];
477
+ for (const candidate of candidates) {
478
+ if (candidate === undefined || candidate === null || candidate === '')
479
+ continue;
480
+ const parsed = Number(candidate);
481
+ if (Number.isFinite(parsed))
482
+ return parsed;
483
+ }
484
+ return null;
485
+ }
486
+ function extractToolError(payload = {}) {
487
+ return String(payload.error || payload.message || payload.stderr || payload.tool_response?.stderr || payload.toolResponse?.stderr || payload.result?.stderr || payload.result?.error || 'tool_failed');
488
+ }
452
489
  async function recordToolErrorTaxonomy(root, state = {}, payload = {}) {
453
490
  if (!state?.mission_id)
454
491
  return null;
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,136 @@
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 function lifecycleHookFromUnknown(value) {
78
+ const candidate = value?.ledger_result_hook || value?.mad_db?.ledger_result_hook || value;
79
+ const missionId = stringOrNull(candidate?.mission_id || candidate?.missionId);
80
+ const operationId = stringOrNull(candidate?.operation_id || candidate?.operationId);
81
+ if (!missionId || !operationId)
82
+ return null;
83
+ return {
84
+ mission_id: missionId,
85
+ operation_id: operationId,
86
+ cycle_id: stringOrNull(candidate?.cycle_id || candidate?.cycleId),
87
+ tool_name: stringOrNull(candidate?.tool_name || candidate?.toolName),
88
+ sql_hash: stringOrNull(candidate?.sql_hash || candidate?.sqlHash),
89
+ mcp_server: stringOrNull(candidate?.mcp_server || candidate?.mcpServer),
90
+ destructive: candidate?.destructive === true
91
+ };
92
+ }
93
+ function hookMatchesPayload(hook, payload) {
94
+ if (!hook.tool_name)
95
+ return true;
96
+ const toolText = [
97
+ payload.tool_name,
98
+ payload.toolName,
99
+ payload.name,
100
+ payload.tool?.name,
101
+ payload.server,
102
+ payload.mcp_tool,
103
+ payload.tool,
104
+ payload.type
105
+ ].filter(Boolean).join(' ').toLowerCase();
106
+ if (!toolText)
107
+ return true;
108
+ return toolText.includes(String(hook.tool_name).toLowerCase()) || String(hook.tool_name).toLowerCase().includes(toolText);
109
+ }
110
+ async function hasTerminalLifecycleEvent(root, missionId, operationId) {
111
+ const ledger = path.join(missionDir(root, missionId), 'mad-db-ledger.jsonl');
112
+ const text = await readText(ledger, '').catch(() => '');
113
+ return String(text).split(/\r?\n/).some((line) => {
114
+ if (!line.includes(operationId))
115
+ return false;
116
+ return line.includes('db_operation.succeeded') || line.includes('db_operation.failed');
117
+ });
118
+ }
119
+ async function markPendingHookResolved(root, missionId, hook, ok) {
120
+ const dir = missionDir(root, missionId);
121
+ const row = {
122
+ schema: 'sks.mad-db-lifecycle-pending-resolution.v1',
123
+ ts: nowIso(),
124
+ mission_id: missionId,
125
+ operation_id: hook.operation_id,
126
+ cycle_id: hook.cycle_id || null,
127
+ result_status: ok ? 'succeeded' : 'failed'
128
+ };
129
+ await appendJsonlBounded(path.join(dir, 'mad-db-lifecycle-resolved.jsonl'), row).catch(() => undefined);
130
+ await fs.rm(path.join(dir, PENDING_LATEST_FILE), { force: true }).catch(() => undefined);
131
+ }
132
+ function stringOrNull(value) {
133
+ const text = String(value || '').trim();
134
+ return text ? text : null;
135
+ }
136
+ //# sourceMappingURL=mad-db-result-lifecycle.js.map
@@ -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)));
@@ -552,12 +552,14 @@ export const ROUTES = [
552
552
  route: 'explicit scoped permission-widening modifier',
553
553
  description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily open approved target-project scopes such as files, shell, package installs, services, network, Computer Use/browser workflows, generated assets, file permissions, migrations, Supabase MCP DB writes, direct execute SQL, schema cleanup, and normal targeted DB writes for the active invocation, while preserving catastrophic wipe/all-row/project-management, credential-exfiltration, persistent security-weakening, and unrequested fallback safeguards.',
554
554
  requiredSkills: ['mad-sks', 'db-safety-guard', 'pipeline-runner', 'context7-docs', REFLECTION_SKILL_NAME, 'honest-mode'],
555
+ dollarAliases: ['$MAD-DB'],
556
+ appSkillAliases: ['mad-db'],
555
557
  lifecycle: ['explicit_invocation', 'auto_sealed_permission_scope', 'scoped_permission_override', 'catastrophic_guard', 'permission_deactivation', 'post_route_reflection', 'honest_mode'],
556
558
  context7Policy: 'required',
557
559
  reasoningPolicy: 'xhigh',
558
560
  stopGate: 'mad-sks-gate.json',
559
561
  cliEntrypoint: 'Codex App prompt route only: $MAD-SKS <task>',
560
- examples: ['$MAD-SKS $Team target project maintenance with package/service/file and DB scopes', '$DB Supabase 점검 $MAD-SKS']
562
+ examples: ['$MAD-SKS $Team target project maintenance with package/service/file and DB scopes', '$DB Supabase 점검 $MAD-SKS', '$MAD-DB enable one-cycle DB break-glass only after explicit ack']
561
563
  },
562
564
  {
563
565
  id: 'GX',
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.16';
1
+ export const PACKAGE_VERSION = '2.0.17';
2
2
  //# sourceMappingURL=version.js.map
@@ -31,21 +31,41 @@ export function renderZellijSlotPane(input) {
31
31
  return frameSlotPane(`LIVE SLOT ${input.slotId}`, rows.slice(0, Math.max(1, maxLines - 2)));
32
32
  }
33
33
  export async function renderZellijSlotPaneFromArtifacts(input) {
34
+ const artifactRender = await renderZellijSlotPaneFromArtifactDir(input).catch(() => null);
34
35
  if (input.missionId && input.missionId !== 'latest') {
35
36
  const telemetry = await tryRenderTelemetrySlotPane({
36
37
  artifactRoot: input.artifactRoot || input.artifactDir,
37
38
  missionId: input.missionId,
38
39
  slotId: input.slotId,
39
- generationIndex: input.generationIndex
40
+ generationIndex: input.generationIndex,
41
+ artifactRender
40
42
  });
41
43
  if (telemetry)
42
44
  return telemetry;
45
+ if (artifactRender)
46
+ return artifactRender;
43
47
  return [
44
48
  `${input.slotId} gen-${Math.max(1, Math.floor(Number(input.generationIndex) || 1))}`,
45
49
  'waiting for telemetry...',
46
50
  `mission ${input.missionId}`
47
51
  ].join('\n');
48
52
  }
53
+ if (artifactRender)
54
+ return artifactRender;
55
+ const fallbackInput = {
56
+ slotId: input.slotId,
57
+ generationIndex: input.generationIndex,
58
+ status: 'launching',
59
+ currentTask: 'waiting for worker intake',
60
+ mode: input.mode || 'compact-slots'
61
+ };
62
+ if (input.role !== undefined)
63
+ fallbackInput.role = input.role;
64
+ if (input.backend !== undefined)
65
+ fallbackInput.backend = input.backend;
66
+ return renderZellijSlotPane(fallbackInput);
67
+ }
68
+ async function renderZellijSlotPaneFromArtifactDir(input) {
49
69
  const artifactDir = path.resolve(input.artifactDir);
50
70
  const result = await readJson(path.join(artifactDir, 'worker-result.json'));
51
71
  const intake = await readJson(path.join(artifactDir, 'worker-intake.json'));
@@ -77,6 +97,8 @@ export async function renderZellijSlotPaneFromArtifacts(input) {
77
97
  ...(Array.isArray(intake?.input_files) ? intake.input_files : [])
78
98
  ]);
79
99
  const now = Date.now();
100
+ if (!result && !intake && !backendReport && !fastReport && !paneReport && !codexProof && !localProof && !heartbeatMtime && !eventRows.length)
101
+ return null;
80
102
  return renderZellijSlotPane({
81
103
  slotId: input.slotId,
82
104
  generationIndex: input.generationIndex,
@@ -136,6 +158,20 @@ export async function renderZellijSlotPaneFromArtifacts(input) {
136
158
  mode: input.mode || 'compact-slots'
137
159
  });
138
160
  }
161
+ export async function renderZellijSlotPaneStatusFromArtifacts(input) {
162
+ const snapshot = input.missionId && input.missionId !== 'latest'
163
+ ? await readZellijSlotTelemetrySnapshot(path.resolve(input.artifactRoot || input.artifactDir), input.missionId).catch(() => null)
164
+ : null;
165
+ const status = telemetryStatus(snapshot);
166
+ return {
167
+ schema: 'sks.zellij-slot-pane-status.v1',
168
+ mission_id: input.missionId || null,
169
+ slot_id: input.slotId,
170
+ generation_index: Math.max(1, Math.floor(Number(input.generationIndex) || 1)),
171
+ telemetry_stale: status.telemetry_stale,
172
+ telemetry_age_ms: status.telemetry_age_ms
173
+ };
174
+ }
139
175
  export function buildZellijSlotPaneCommand(input) {
140
176
  const args = [
141
177
  input.cliPath,
@@ -159,9 +195,14 @@ async function tryRenderTelemetrySlotPane(input) {
159
195
  const slot = findTelemetrySlot(snapshot, input.slotId, input.generationIndex);
160
196
  if (!slot)
161
197
  return null;
198
+ const staleRows = staleTelemetryRows(telemetryStatus(snapshot).telemetry_age_ms);
199
+ const fallbackRows = artifactFallbackRows(input.artifactRender);
200
+ const liveRows = staleRows.length || !slot.progress ? fallbackRows : [];
162
201
  if (slot.status === 'failed') {
163
202
  return [
164
203
  `${slot.slot_id} gen-${slot.generation_index} · FAILED`,
204
+ ...staleRows,
205
+ ...liveRows,
165
206
  `blocker: ${trimInline(slot.blockers[0] || 'worker_failed', 78)}`,
166
207
  `artifact: ${trimInline(slot.artifact_paths[slot.artifact_paths.length - 1] || '-', 78)}`
167
208
  ].join('\n');
@@ -169,6 +210,8 @@ async function tryRenderTelemetrySlotPane(input) {
169
210
  if (slot.status === 'completed' || slot.status === 'drained') {
170
211
  return [
171
212
  `${slot.slot_id} gen-${slot.generation_index} · done`,
213
+ ...staleRows,
214
+ ...liveRows,
172
215
  `artifacts ${slot.artifact_paths.length} · ${slot.latest_event_type === 'verification_passed' ? 'verify passed' : 'verify queued'}`,
173
216
  'closing in 3s'
174
217
  ].join('\n');
@@ -177,12 +220,25 @@ async function tryRenderTelemetrySlotPane(input) {
177
220
  const heartbeat = slot.latest_ts ? `${Math.max(0, Math.round((Date.now() - Date.parse(slot.latest_ts)) / 1000))}s` : '?';
178
221
  return [
179
222
  `${slot.slot_id} gen-${slot.generation_index} · ${trimInline(slot.role || 'worker', 28)}`,
223
+ ...staleRows,
224
+ ...liveRows,
180
225
  trimInline(backend, 78),
181
226
  `${slot.status}: ${trimInline(slot.task_title || 'worker task', 68)}`,
182
227
  `${formatTelemetryProgress(slot.progress)} · latest ${slot.latest_event_type} ${heartbeat}`,
183
228
  `${slot.latest_event_type === 'patch_candidate' ? 'patch candidate' : 'patch'}: ${slot.latest_event_type === 'patch_candidate' ? 'queued' : trimInline(slot.current_file || '-', 42)}`
184
229
  ].join('\n');
185
230
  }
231
+ function artifactFallbackRows(text) {
232
+ if (!text)
233
+ return [];
234
+ return String(text)
235
+ .split(/\r?\n/)
236
+ .map((line) => line.replace(/^\|\s?/, '').replace(/\s?\|$/, '').trim())
237
+ .filter((line) => /^(heartbeat|doing|files|event|out|err):\s+/i.test(line))
238
+ .filter((line) => !/unknown|waiting for worker intake|no changed file yet/i.test(line))
239
+ .slice(-4)
240
+ .map((line) => `live: ${trimInline(line, 72)}`);
241
+ }
186
242
  function findTelemetrySlot(snapshot, slotId, generationIndex) {
187
243
  const generation = Math.max(1, Math.floor(Number(generationIndex) || 1));
188
244
  return Object.values(snapshot.slots || {}).find((row) => row.slot_id === slotId && Number(row.generation_index) === generation) || null;
@@ -192,6 +248,23 @@ function formatTelemetryProgress(progress) {
192
248
  return 'progress ?';
193
249
  return `progress ${progress.done}/${progress.total}${progress.label ? ` ${trimInline(progress.label, 24)}` : ''}`;
194
250
  }
251
+ function telemetryStatus(snapshot) {
252
+ const parsed = snapshot?.updated_at ? Date.parse(snapshot.updated_at) : NaN;
253
+ const telemetryAgeMs = Number.isFinite(parsed) ? Math.max(0, Date.now() - parsed) : Number.MAX_SAFE_INTEGER;
254
+ return {
255
+ telemetry_stale: telemetryAgeMs > 3000,
256
+ telemetry_age_ms: telemetryAgeMs
257
+ };
258
+ }
259
+ function staleTelemetryRows(ageMs) {
260
+ if (!Number.isFinite(ageMs))
261
+ return ['telemetry stale; worker may still be running'];
262
+ if (ageMs > 10000)
263
+ return ['telemetry stale; worker may still be running'];
264
+ if (ageMs > 3000)
265
+ return [`telemetry stale ${(ageMs / 1000).toFixed(1)}s`];
266
+ return [];
267
+ }
195
268
  async function readJson(file) {
196
269
  try {
197
270
  return JSON.parse(await fs.promises.readFile(file, 'utf8'));
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { appendJsonlBounded, ensureDir, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
3
+ import { appendJsonlBounded, ensureDir, nowIso, readJson, readText } from '../fsx.js';
4
4
  export const ZELLIJ_SLOT_TELEMETRY_EVENT_SCHEMA = 'sks.zellij-slot-telemetry-event.v1';
5
5
  export const ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA = 'sks.zellij-slot-telemetry-snapshot.v1';
6
6
  const telemetrySnapshotCache = new Map();
7
7
  const telemetrySnapshotWriteCounts = new Map();
8
+ const telemetrySnapshotFlushCounts = new Map();
9
+ const telemetrySnapshotLastFlushMs = new Map();
8
10
  export function slotTelemetryEventPath(root, missionId) {
9
11
  return path.join(inferMissionDir(root, missionId), 'zellij', 'slot-telemetry.events.jsonl');
10
12
  }
@@ -60,6 +62,7 @@ export function applyTelemetryEventToSnapshot(snapshot, event) {
60
62
  schema: ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA,
61
63
  mission_id: event.mission_id || snapshot.mission_id,
62
64
  updated_at: nowIso(),
65
+ flush_count: snapshot.flush_count || 0,
63
66
  slots,
64
67
  counts: countSlotTelemetry(slots)
65
68
  };
@@ -79,11 +82,11 @@ export async function rebuildZellijSlotTelemetrySnapshot(root, missionId) {
79
82
  schema: ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA,
80
83
  mission_id: missionId,
81
84
  updated_at: nowIso(),
85
+ flush_count: 0,
82
86
  slots,
83
87
  counts: countSlotTelemetry(slots)
84
88
  };
85
- await writeJsonAtomic(slotTelemetrySnapshotPath(root, missionId), snapshot);
86
- telemetrySnapshotCache.set(slotTelemetrySnapshotPath(root, missionId), snapshot);
89
+ await writeTelemetrySnapshotFast(slotTelemetrySnapshotPath(root, missionId), snapshot);
87
90
  return snapshot;
88
91
  }
89
92
  async function readTelemetryEvents(file) {
@@ -210,17 +213,37 @@ function tail(value, max) {
210
213
  }
211
214
  async function writeTelemetrySnapshotFast(file, snapshot) {
212
215
  await ensureDir(path.dirname(file));
213
- await fsp.writeFile(file, `${JSON.stringify(snapshot)}\n`, 'utf8');
216
+ const flushCount = Number(telemetrySnapshotFlushCounts.get(file) || 0) + 1;
217
+ telemetrySnapshotFlushCounts.set(file, flushCount);
218
+ telemetrySnapshotLastFlushMs.set(file, Date.now());
219
+ const next = { ...snapshot, flush_count: flushCount };
220
+ telemetrySnapshotCache.set(file, next);
221
+ await fsp.writeFile(file, `${JSON.stringify(next)}\n`, 'utf8');
214
222
  }
215
223
  function shouldFlushTelemetrySnapshot(file, event) {
216
224
  const next = (telemetrySnapshotWriteCounts.get(file) || 0) + 1;
217
225
  telemetrySnapshotWriteCounts.set(file, next);
218
- return next === 1
219
- || next % 100 === 0
226
+ const now = Date.now();
227
+ const last = telemetrySnapshotLastFlushMs.get(file) || 0;
228
+ const parsedFlushMs = Number(process.env.SKS_ZELLIJ_SLOT_TELEMETRY_FLUSH_MS || 1000);
229
+ const parsedFlushEvery = Number(process.env.SKS_ZELLIJ_SLOT_TELEMETRY_FLUSH_EVERY_N || 100);
230
+ const flushMs = Math.max(250, Number.isFinite(parsedFlushMs) ? parsedFlushMs : 1000);
231
+ const flushEvery = Math.max(1, Number.isFinite(parsedFlushEvery) ? Math.floor(parsedFlushEvery) : 100);
232
+ const important = event.event_type === 'task_started'
233
+ || event.event_type === 'task_progress'
234
+ || event.event_type === 'artifact_written'
235
+ || event.event_type === 'patch_candidate'
220
236
  || event.event_type === 'worker_completed'
221
237
  || event.event_type === 'worker_failed'
222
238
  || event.status === 'completed'
223
239
  || event.status === 'failed';
240
+ const should = next === 1
241
+ || important
242
+ || now - last >= flushMs
243
+ || next % flushEvery === 0;
244
+ if (should)
245
+ telemetrySnapshotLastFlushMs.set(file, now);
246
+ return should;
224
247
  }
225
248
  function inferMissionDir(root, missionId) {
226
249
  const resolved = path.resolve(root);
@@ -1,16 +1,26 @@
1
1
  export function resolveZellijUiMode(args = [], env = process.env) {
2
+ return resolveExplicitZellijUiMode(args, env) || 'compact-slots';
3
+ }
4
+ export function resolveZellijWorkerPaneUiMode(args = [], env = process.env) {
5
+ return resolveExplicitZellijUiMode(args, env) || 'full-debug';
6
+ }
7
+ function resolveExplicitZellijUiMode(args = [], env = process.env) {
2
8
  const fromEnv = String(env.SKS_ZELLIJ_UI_MODE || '').trim();
9
+ if (fromEnv === 'compact-slots')
10
+ return 'compact-slots';
3
11
  if (fromEnv === 'full-debug')
4
12
  return 'full-debug';
5
13
  if (fromEnv === 'dashboard-plus-slots')
6
14
  return 'dashboard-plus-slots';
15
+ if (args.includes('--zellij-compact-slots'))
16
+ return 'compact-slots';
7
17
  if (args.includes('--zellij-dashboard'))
8
18
  return 'dashboard-plus-slots';
9
19
  if (args.includes('--zellij-full-debug'))
10
20
  return 'full-debug';
11
- return 'compact-slots';
21
+ return null;
12
22
  }
13
23
  export function zellijUiModeCreatesDashboard(mode) {
14
- return mode !== 'compact-slots';
24
+ return mode === 'dashboard-plus-slots';
15
25
  }
16
26
  //# sourceMappingURL=zellij-ui-mode.js.map