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
@@ -0,0 +1,182 @@
1
+ import path from 'node:path';
2
+ import { ARTIFACT_FILES } from '../artifact-schemas.js';
3
+ import { readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
4
+ import { loadMission } from '../mission.js';
5
+ import { MIN_TEAM_REVIEWER_LANES } from '../team-review-policy.js';
6
+ import { renderTeamDashboardState, writeTeamDashboardState } from '../team-dashboard-renderer.js';
7
+ import { appendTeamEvent, formatRoleCounts, isTerminalTeamAgentStatus, readTeamControl, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamCleanupSummary, renderTeamWatch, requestTeamSessionCleanup, teamCleanupRequested } from '../team-live.js';
8
+ import { attachZellijSessionInteractive, launchTeamZellijView } from '../zellij/zellij-launcher.js';
9
+ import { flag, readFlagValue } from './command-utils.js';
10
+ export const teamLegacySubcommands = new Set([
11
+ 'log',
12
+ 'tail',
13
+ 'watch',
14
+ 'lane',
15
+ 'status',
16
+ 'dashboard',
17
+ 'event',
18
+ 'message',
19
+ 'open-zellij',
20
+ 'attach-zellij',
21
+ 'cleanup-zellij',
22
+ 'open-tmux',
23
+ 'attach-tmux',
24
+ 'cleanup-tmux'
25
+ ]);
26
+ export async function teamLegacyObserveCommand(sub, args = []) {
27
+ const root = await sksRoot();
28
+ const missionArg = args[0] && !String(args[0]).startsWith('--') ? args[0] : 'latest';
29
+ const { resolveMissionId } = await import('./command-utils.js');
30
+ const id = await resolveMissionId(root, missionArg);
31
+ if (!id) {
32
+ console.error(`Usage: sks team ${sub} [mission-id|latest]`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const { dir } = await loadMission(root, id);
37
+ if (sub === 'open-tmux' || sub === 'attach-tmux' || sub === 'cleanup-tmux') {
38
+ const result = { ok: false, status: 'removed_runtime', runtime: 'tmux', replacement: 'zellij', operator_actions: ['Use `sks team open-zellij`, `attach-zellij`, or `cleanup-zellij`.'] };
39
+ if (flag(args, '--json'))
40
+ return console.log(JSON.stringify(result, null, 2));
41
+ console.error('tmux runtime has been removed from SKS Team. Use Zellij commands instead.');
42
+ process.exitCode = 2;
43
+ return;
44
+ }
45
+ if (sub === 'open-zellij' || sub === 'attach-zellij') {
46
+ const plan = await readJson(path.join(dir, 'team-plan.json'), null);
47
+ if (!plan) {
48
+ console.error(`Team plan missing for ${id}; cannot open Zellij Team view.`);
49
+ process.exitCode = 2;
50
+ return;
51
+ }
52
+ const slotCount = await inferTeamZellijSlotCount(dir, plan);
53
+ const zellij = await launchTeamZellijView({ root, missionId: id, ledgerRoot: path.join(dir, 'agents'), slotCount, dryRun: flag(args, '--json'), attach: false });
54
+ if (flag(args, '--json'))
55
+ return console.log(JSON.stringify(zellij, null, 2));
56
+ if (!zellij.ok) {
57
+ console.error(`Zellij Team view blocked for ${id}: ${(zellij.blockers || []).join('; ') || 'Zellij launch failed'}`);
58
+ process.exitCode = 2;
59
+ return;
60
+ }
61
+ if (zellij.capability?.status === 'ok')
62
+ console.log(`Zellij: prepared Team lane(s) in ${zellij.session_name}`);
63
+ else
64
+ console.log(`Zellij: optional live panes unavailable (${(zellij.warnings || []).join('; ') || zellij.capability?.status || 'unknown'})`);
65
+ if (zellij.capability?.status === 'ok' && (sub === 'attach-zellij' || shouldAutoAttachTeamZellij(args))) {
66
+ attachZellijSessionInteractive(zellij.session_name, { cwd: root, configPath: zellij.clipboard_config_path });
67
+ }
68
+ return;
69
+ }
70
+ if (sub === 'event') {
71
+ const message = readFlagValue(args, '--message', '');
72
+ if (!message) {
73
+ console.error('Usage: sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."');
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ const phase = readFlagValue(args, '--phase', 'general');
78
+ const record = await appendTeamEvent(dir, { agent: readFlagValue(args, '--agent', 'parent_orchestrator'), phase, type: readFlagValue(args, '--type', 'status'), artifact: readFlagValue(args, '--artifact', ''), message });
79
+ if (flag(args, '--json'))
80
+ return console.log(JSON.stringify(record, null, 2));
81
+ console.log(`${record.ts} [${record.phase}] ${record.agent}: ${record.message}`);
82
+ return;
83
+ }
84
+ if (sub === 'message') {
85
+ const message = readFlagValue(args, '--message', '');
86
+ if (!message) {
87
+ console.error('Usage: sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."');
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ const record = await appendTeamEvent(dir, { agent: readFlagValue(args, '--from', readFlagValue(args, '--agent', 'parent_orchestrator')), to: readFlagValue(args, '--to', 'all'), phase: readFlagValue(args, '--phase', 'communication'), type: 'message', message });
92
+ if (flag(args, '--json'))
93
+ return console.log(JSON.stringify(record, null, 2));
94
+ console.log(`${record.ts} [${record.phase}] ${record.agent} -> ${record.to}: ${record.message}`);
95
+ return;
96
+ }
97
+ if (sub === 'cleanup-zellij') {
98
+ const control = await requestTeamSessionCleanup(dir, { missionId: id, agent: readFlagValue(args, '--agent', 'parent_orchestrator'), reason: readFlagValue(args, '--reason', 'Team session ended; clean up live follow panes.'), finalMessage: 'Team session ended.' });
99
+ await appendTeamEvent(dir, { agent: readFlagValue(args, '--agent', 'parent_orchestrator'), phase: 'session_cleanup', type: 'cleanup', message: control.cleanup_reason || 'Team session cleanup requested.' });
100
+ const cleanup = { ok: true, runtime: 'zellij', mission_id: id, control, close_requested: flag(args, '--close-session') || flag(args, '--close') };
101
+ await writeJsonAtomic(path.join(dir, 'zellij-session-cleanup.json'), cleanup);
102
+ if (flag(args, '--json'))
103
+ return console.log(JSON.stringify(cleanup, null, 2));
104
+ console.log('Zellij cleanup: marked complete.');
105
+ console.log(renderTeamCleanupSummary(control));
106
+ return;
107
+ }
108
+ if (sub === 'status') {
109
+ const dashboard = await readTeamDashboard(dir);
110
+ if (flag(args, '--json'))
111
+ return console.log(JSON.stringify(dashboard || {}, null, 2));
112
+ if (!dashboard) {
113
+ console.error(`Team dashboard missing for ${id}.`);
114
+ process.exitCode = 2;
115
+ return;
116
+ }
117
+ console.log(`Team mission: ${id}`);
118
+ console.log(`Updated: ${dashboard.updated_at || 'unknown'}`);
119
+ console.log(`Agent sessions: ${dashboard.agent_session_count || MIN_TEAM_REVIEWER_LANES}`);
120
+ if (dashboard.role_counts)
121
+ console.log(`Role counts: ${formatRoleCounts(dashboard.role_counts)}`);
122
+ return;
123
+ }
124
+ if (sub === 'dashboard') {
125
+ await writeTeamDashboardState(dir, { missionId: id });
126
+ const state = await readJson(path.join(dir, ARTIFACT_FILES.team_dashboard_state), {});
127
+ if (flag(args, '--json'))
128
+ return console.log(JSON.stringify(state, null, 2));
129
+ console.log(renderTeamDashboardState(state));
130
+ return;
131
+ }
132
+ if (sub === 'log')
133
+ return console.log(await readTeamLive(dir));
134
+ if (sub === 'lane') {
135
+ const agent = readFlagValue(args, '--agent', 'parent_orchestrator');
136
+ const phase = readFlagValue(args, '--phase', '');
137
+ const lines = Number(readFlagValue(args, '--lines', '12'));
138
+ const text = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
139
+ if (flag(args, '--json'))
140
+ return console.log(JSON.stringify({ mission_id: id, agent, phase, lane: text }, null, 2));
141
+ console.log(text);
142
+ if (flag(args, '--follow') && !teamCleanupRequested(await readTeamControl(dir)) && !isTerminalTeamAgentStatus((await readTeamDashboard(dir).catch(() => null))?.agents?.[agent]?.status || '')) {
143
+ // Follow mode intentionally falls through only for interactive terminals in the full Zellij lane.
144
+ }
145
+ return;
146
+ }
147
+ if (sub === 'tail' || sub === 'watch') {
148
+ const lines = readFlagValue(args, '--lines', '20');
149
+ if (sub === 'watch' && !flag(args, '--raw'))
150
+ console.log(await renderTeamWatch(dir, { missionId: id, lines: Number(lines) }));
151
+ else
152
+ for (const line of await readTeamTranscriptTail(dir, Number(lines)))
153
+ console.log(line);
154
+ }
155
+ }
156
+ async function inferTeamZellijSlotCount(dir, plan = {}) {
157
+ const scheduler = await readJson(path.join(dir, 'agents', 'agent-scheduler-state.json'), null);
158
+ const lanes = await readJson(path.join(dir, 'agents', 'agent-zellij-lanes.json'), null);
159
+ const candidates = [
160
+ plan?.bundle_size,
161
+ plan?.agent_session_count,
162
+ lanes?.lane_count,
163
+ plan?.target_active_slots,
164
+ scheduler?.target_active_slots
165
+ ].map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0);
166
+ return Math.max(1, Math.min(100, Math.floor(candidates[0] || 5)));
167
+ }
168
+ function shouldAutoAttachTeamZellij(args = []) {
169
+ const list = (args || []).map((arg) => String(arg));
170
+ if (list.includes('--no-attach'))
171
+ return false;
172
+ if (list.includes('--json'))
173
+ return false;
174
+ if (process.env.SKS_NO_ZELLIJ_ATTACH === '1')
175
+ return false;
176
+ if (process.env.ZELLIJ)
177
+ return false;
178
+ if (list.includes('--attach'))
179
+ return true;
180
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
181
+ }
182
+ //# sourceMappingURL=team-legacy-observe-command.js.map
@@ -5,6 +5,7 @@ import { evaluateMadSksPermissionGate, isMadSksRouteState } from './permission-g
5
5
  import { resolveMadDbMutationPolicy } from './mad-db/mad-db-policy-resolver.js';
6
6
  import { recordMadDbOperation } from './mad-db/mad-db-capability.js';
7
7
  import { appendMadDbLedgerEvent, appendMadDbOperationLifecycle } from './mad-db/mad-db-ledger.js';
8
+ import { lifecycleHookFromUnknown, recordPendingMadDbLifecycleHook } from './mad-db/mad-db-result-lifecycle.js';
8
9
  export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
9
10
  schema_version: 1,
10
11
  mode: 'read_only_default',
@@ -520,6 +521,14 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
520
521
  sqlHash,
521
522
  destructive: classification.level === 'destructive'
522
523
  });
524
+ const lifecycleHook = {
525
+ mission_id: String(state.mission_id),
526
+ operation_id: operationId,
527
+ cycle_id: madDbDecision.cycle_id || null,
528
+ tool_name: classification.toolName || null,
529
+ sql_hash: sqlHash,
530
+ destructive: classification.level === 'destructive'
531
+ };
523
532
  const decision = {
524
533
  allowed: true,
525
534
  action: 'allow',
@@ -534,6 +543,8 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
534
543
  capability_file: 'mad-db-capability.json',
535
544
  consumed: false,
536
545
  operation_id: operationId,
546
+ lifecycle_result_pending: true,
547
+ ledger_result_hook: lifecycleHook,
537
548
  operation_count: Number(madDbDecision.operation_count || 0) + 1,
538
549
  max_operations: madDbDecision.max_operations || 20
539
550
  }
@@ -555,6 +566,7 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
555
566
  });
556
567
  decision.mad_db.consumed = updatedCapability?.consumed === true;
557
568
  decision.mad_db.operation_count = updatedCapability?.operation_count ?? decision.mad_db.operation_count;
569
+ await recordPendingMadDbLifecycleHook(root, state.mission_id, lifecycleHook);
558
570
  await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'db-safety.jsonl'), { ts: nowIso(), decision });
559
571
  return decision;
560
572
  }
@@ -567,6 +579,9 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
567
579
  }
568
580
  return decision;
569
581
  }
582
+ export function madDbLifecycleHookFromDecision(decision) {
583
+ return lifecycleHookFromUnknown(decision);
584
+ }
570
585
  export async function checkSqlFile(file) {
571
586
  const sql = await readText(file);
572
587
  return classifySql(sql);
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { detectCodex0138Capability } from '../codex-control/codex-0138-capability.js';
5
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
6
+ export async function runCodex0138Doctor(root, input = {}) {
7
+ const capability = await detectCodex0138Capability();
8
+ const fixed = [];
9
+ const checks = {
10
+ bash_fallback: await bashFallbackCheck(),
11
+ linux_proxy_socket_path: linuxProxySocketCheck(root),
12
+ oauth_mcp_prerefresh: oauthMcpPrerefreshCheck(capability),
13
+ agents_logical_path: await agentsLogicalPathCheck(root),
14
+ plugin_discovery_cache: await pluginDiscoveryCacheCheck(root, input.fix === true, fixed)
15
+ };
16
+ const warnings = [
17
+ ...(capability.ok ? [] : ['codex_0_138_not_detected']),
18
+ ...Object.values(checks).flatMap((check) => Array.isArray(check.warnings) ? check.warnings : [])
19
+ ];
20
+ const blockers = Object.values(checks).flatMap((check) => Array.isArray(check.blockers) ? check.blockers : []);
21
+ const report = {
22
+ schema: 'sks.codex-0138-doctor.v1',
23
+ generated_at: nowIso(),
24
+ ok: blockers.length === 0,
25
+ codex_0138_capability: capability,
26
+ checks,
27
+ fixed,
28
+ warnings,
29
+ blockers
30
+ };
31
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'codex-0138-doctor.json'), report);
32
+ return report;
33
+ }
34
+ async function bashFallbackCheck() {
35
+ const candidates = ['/bin/bash', '/usr/bin/bash'];
36
+ const existing = [];
37
+ for (const candidate of candidates) {
38
+ try {
39
+ await fs.access(candidate);
40
+ existing.push(candidate);
41
+ }
42
+ catch { }
43
+ }
44
+ return {
45
+ ok: existing.length > 0,
46
+ candidates,
47
+ existing,
48
+ blockers: existing.length ? [] : ['bash_fallback_missing'],
49
+ warnings: []
50
+ };
51
+ }
52
+ function linuxProxySocketCheck(root) {
53
+ if (process.platform !== 'linux')
54
+ return { ok: true, status: 'not_linux', warnings: [], blockers: [] };
55
+ const candidate = path.join(os.tmpdir(), 'sks-proxy', path.basename(root), 'proxy.sock');
56
+ return {
57
+ ok: candidate.length < 100,
58
+ candidate,
59
+ length: candidate.length,
60
+ warnings: candidate.length < 100 ? [] : ['linux_proxy_socket_path_long'],
61
+ blockers: []
62
+ };
63
+ }
64
+ function oauthMcpPrerefreshCheck(capability) {
65
+ return {
66
+ ok: true,
67
+ supported: capability.supports_oauth_mcp_prerefresh === true,
68
+ warnings: capability.supports_oauth_mcp_prerefresh ? [] : ['oauth_mcp_prerefresh_requires_codex_0_138'],
69
+ blockers: []
70
+ };
71
+ }
72
+ async function agentsLogicalPathCheck(root) {
73
+ const agents = path.join(root, 'AGENTS.md');
74
+ const realRoot = await fs.realpath(root).catch(() => root);
75
+ const exists = await fs.stat(agents).then((st) => st.isFile()).catch(() => false);
76
+ return {
77
+ ok: exists,
78
+ logical_path: agents,
79
+ real_root: realRoot,
80
+ warnings: exists ? [] : ['agents_md_missing_or_unreadable'],
81
+ blockers: []
82
+ };
83
+ }
84
+ async function pluginDiscoveryCacheCheck(root, fix, fixed) {
85
+ const cacheDir = path.join(root, '.sneakoscope', 'cache', 'codex-plugin-discovery');
86
+ const exists = await fs.stat(cacheDir).then((st) => st.isDirectory()).catch(() => false);
87
+ if (!exists && fix) {
88
+ await fs.mkdir(cacheDir, { recursive: true });
89
+ await writeJsonAtomic(path.join(cacheDir, 'README.json'), {
90
+ schema: 'sks.codex-plugin-discovery-cache.v1',
91
+ repaired_at: nowIso(),
92
+ purpose: 'Codex 0.138 plugin discovery cache placeholder; safe to refresh from codex plugin list --json.'
93
+ });
94
+ fixed.push('plugin_discovery_cache');
95
+ }
96
+ const after = exists || fix;
97
+ return {
98
+ ok: after,
99
+ path: cacheDir,
100
+ warnings: after ? [] : ['plugin_discovery_cache_missing_repair_available'],
101
+ blockers: []
102
+ };
103
+ }
104
+ //# sourceMappingURL=codex-0138-doctor.js.map
@@ -52,6 +52,14 @@ export function buildDoctorReadinessMatrix(input = {}) {
52
52
  blockers.add('codex_app_fast_ui_repair_requires_confirmation');
53
53
  if (input.codex_app_ui?.fast_selector === 'repaired')
54
54
  warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
55
+ const codex0138Doctor = input.codex_0138_doctor || null;
56
+ if (codex0138Doctor?.ok === false)
57
+ for (const blocker of normalizeList(codex0138Doctor.blockers))
58
+ warnings.add(blocker);
59
+ for (const warning of normalizeList(codex0138Doctor?.warnings))
60
+ warnings.add(warning);
61
+ for (const warning of normalizeList(input.codex_plugin_app_template_policy?.doctor_warnings))
62
+ warnings.add(warning);
55
63
  if (input.codex_lb?.ok === false)
56
64
  warnings.add(`codex_lb_${input.codex_lb?.circuit?.state || 'blocked'}`);
57
65
  const localModel = input.local_model || {};
@@ -107,6 +115,9 @@ export function buildDoctorReadinessMatrix(input = {}) {
107
115
  replacement: 'zellij'
108
116
  },
109
117
  codex_doctor: codexDoctor || null,
118
+ codex_0138_doctor: codex0138Doctor,
119
+ codex_plugin_inventory: input.codex_plugin_inventory || null,
120
+ codex_plugin_app_template_policy: input.codex_plugin_app_template_policy || null,
110
121
  fast_mode_ready: input.fast_mode_ready !== false,
111
122
  codex_app_ui: input.codex_app_ui || null,
112
123
  hooks_ready: input.hooks_ready !== false,
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, writeJsonAtomic } from './fsx.js';
3
3
  import { ARTIFACT_FILES } from './artifact-schemas.js';
4
+ import { codexModelEffortCapability, modelEffortAtLeast } from './codex-control/codex-model-capabilities.js';
4
5
  export const EFFORT_POLICY_VERSION = 1;
5
6
  export function selectEffort(task = {}) {
6
7
  const route = String(task.route || task.command || '').toLowerCase();
@@ -39,11 +40,19 @@ export function selectEffort(task = {}) {
39
40
  selected = 'medium';
40
41
  reasonCodes.push('default_medium');
41
42
  }
43
+ const modelCapability = codexModelEffortCapability({
44
+ model: task.model || task.model_id,
45
+ advertisedEfforts: task.advertised_efforts || task.model_advertised_efforts,
46
+ defaultEffort: task.model_reasoning_effort || task.default_effort
47
+ });
48
+ const modelReasoningEffort = modelEffortAtLeast(selected, modelCapability);
42
49
  return {
43
50
  schema_version: EFFORT_POLICY_VERSION,
44
51
  mission_id: task.mission_id || 'unassigned',
45
52
  task_id: task.task_id || 'TASK-001',
46
53
  selected_effort: selected,
54
+ model_reasoning_effort: modelReasoningEffort,
55
+ model_effort_capability: modelCapability,
47
56
  reason_codes: reasonCodes,
48
57
  risk_scores: risks,
49
58
  demotion_allowed_after: demotionPolicy(selected),
@@ -830,7 +830,7 @@ function commandMaturity(name) {
830
830
  function routeMaturity(command) {
831
831
  if (['$Answer', '$DFix', '$SKS', '$Fast-Mode', '$Wiki', '$Help'].includes(command))
832
832
  return 'stable';
833
- if (['$Team', '$Goal', '$DB', '$Computer-Use', '$CU', '$QA-LOOP', '$MAD-SKS'].includes(command))
833
+ if (['$Team', '$Goal', '$DB', '$Computer-Use', '$CU', '$QA-LOOP', '$MAD-SKS', '$MAD-DB'].includes(command))
834
834
  return 'beta';
835
835
  return 'labs';
836
836
  }
@@ -858,7 +858,7 @@ function knownGapsForCommand(name) {
858
858
  function routeVoxelContract(command) {
859
859
  if (['$Image-UX-Review', '$UX-Review', '$PPT', '$From-Chat-IMG', '$GX'].includes(command))
860
860
  return 'image/source/bbox voxel required';
861
- if (command === '$DB' || command === '$MAD-SKS')
861
+ if (command === '$DB' || command === '$MAD-SKS' || command === '$MAD-DB')
862
862
  return 'DB policy voxel required';
863
863
  return 'TriWiki anchors required';
864
864
  }
@@ -867,6 +867,8 @@ function routeKnownGaps(command) {
867
867
  return ['live imagegen/CU evidence required'];
868
868
  if (command === '$MAD-SKS')
869
869
  return ['permission closed by owning gate'];
870
+ if (command === '$MAD-DB')
871
+ return ['one-cycle capability must be explicitly enabled, consumed, or revoked'];
870
872
  return [];
871
873
  }
872
874
  function checkRow(id, ok, blockers = []) {
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.18';
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 { maybeRecordMadDbToolResultFromToolUse } 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';
@@ -162,6 +163,8 @@ function toolFailed(payload = {}) {
162
163
  if (Number.isFinite(n))
163
164
  return n !== 0;
164
165
  }
166
+ if (payload.isError === true || payload.tool_response?.isError === true || payload.toolResponse?.isError === true || payload.result?.isError === true)
167
+ return true;
165
168
  if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false)
166
169
  return true;
167
170
  if (payload.executed === false)
@@ -422,10 +425,7 @@ function agentWorkerHookContext(state = {}, payload = {}) {
422
425
  || payload.agentWorker === true);
423
426
  }
424
427
  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
- }
428
+ await recordMadDbPostToolLifecycle(root, state, payload).catch(() => null);
429
429
  await recordContext7Evidence(root, state, payload).catch(() => null);
430
430
  await recordSubagentEvidence(root, state, payload).catch(() => null);
431
431
  if (toolFailed(payload))
@@ -449,6 +449,40 @@ async function hookPostTool(root, state, payload, noQuestion) {
449
449
  ? { continue: true, additionalContext: teamDigest.context, systemMessage: joinSystemMessages(visibleHookMessage('post-tool'), teamDigest.system) }
450
450
  : { continue: true };
451
451
  }
452
+ async function recordMadDbPostToolLifecycle(root, state = {}, payload = {}) {
453
+ if (!state?.mission_id)
454
+ return null;
455
+ return maybeRecordMadDbToolResultFromToolUse({
456
+ root,
457
+ missionId: String(state.mission_id),
458
+ toolCallPayload: payload,
459
+ toolResult: payload
460
+ });
461
+ }
462
+ function extractRowCount(payload = {}) {
463
+ const candidates = [
464
+ payload.row_count,
465
+ payload.rowCount,
466
+ payload.tool_response?.row_count,
467
+ payload.tool_response?.rowCount,
468
+ payload.toolResponse?.rowCount,
469
+ payload.result?.row_count,
470
+ payload.result?.rowCount,
471
+ payload.result?.rows_affected,
472
+ payload.tool_response?.rows_affected
473
+ ];
474
+ for (const candidate of candidates) {
475
+ if (candidate === undefined || candidate === null || candidate === '')
476
+ continue;
477
+ const parsed = Number(candidate);
478
+ if (Number.isFinite(parsed))
479
+ return parsed;
480
+ }
481
+ return null;
482
+ }
483
+ function extractToolError(payload = {}) {
484
+ return String(payload.error || payload.message || payload.stderr || payload.tool_response?.stderr || payload.toolResponse?.stderr || payload.result?.stderr || payload.result?.error || 'tool_failed');
485
+ }
452
486
  async function recordToolErrorTaxonomy(root, state = {}, payload = {}) {
453
487
  if (!state?.mission_id)
454
488
  return null;
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { imageDimensions } from '../wiki-image/image-hash.js';
5
+ export async function buildImageArtifactPathContract(root, input) {
6
+ const images = [];
7
+ const blockers = [];
8
+ for (const [index, image] of input.images.entries()) {
9
+ const filePath = path.resolve(root, image.filePath || '');
10
+ const exists = await fileExists(filePath);
11
+ if (!exists)
12
+ blockers.push(`${image.kind}_file_path_missing:${image.id || index + 1}`);
13
+ const dims = exists ? await imageDimensions(filePath).catch(() => null) : null;
14
+ images.push({
15
+ id: image.id || `image-${index + 1}`,
16
+ kind: image.kind,
17
+ file_path: filePath,
18
+ relative_path: path.relative(root, filePath),
19
+ exists,
20
+ mime_type: mimeForPath(filePath),
21
+ width: dims?.width ?? null,
22
+ height: dims?.height ?? null,
23
+ model_visible_path: filePath,
24
+ followup_edit_hint: exists
25
+ ? `Use this saved local path for follow-up image edits: ${filePath}`
26
+ : 'Image file path missing; do not run visual QA until a real saved file path exists.'
27
+ });
28
+ }
29
+ if (images.some((image) => image.kind === 'generated_image' && !image.exists))
30
+ blockers.push('image_generated_file_path_missing');
31
+ return {
32
+ schema: 'sks.image-artifact-path-contract.v1',
33
+ mission_id: input.missionId,
34
+ generated_at: nowIso(),
35
+ images,
36
+ blockers: [...new Set(blockers)]
37
+ };
38
+ }
39
+ export async function writeImageArtifactPathContract(root, input) {
40
+ const contract = await buildImageArtifactPathContract(root, input);
41
+ const artifactPath = input.artifactPath || path.join(root, '.sneakoscope', 'missions', input.missionId, 'image-artifact-path-contract.json');
42
+ await writeJsonAtomic(artifactPath, contract);
43
+ return { contract, artifact_path: artifactPath };
44
+ }
45
+ export async function discoverImageArtifactsInDir(dir) {
46
+ const out = [];
47
+ await walk(dir, async (file) => {
48
+ if (!/\.(png|jpe?g|webp|gif)$/i.test(file))
49
+ return;
50
+ out.push({
51
+ id: path.basename(file).replace(/[^0-9A-Za-z._-]/g, '_'),
52
+ kind: /generated|gpt-image|callout/i.test(file) ? 'generated_image' : 'visual_qa_snapshot',
53
+ filePath: file
54
+ });
55
+ });
56
+ return out;
57
+ }
58
+ function mimeForPath(file) {
59
+ const ext = path.extname(file).toLowerCase();
60
+ if (ext === '.png')
61
+ return 'image/png';
62
+ if (ext === '.jpg' || ext === '.jpeg')
63
+ return 'image/jpeg';
64
+ if (ext === '.webp')
65
+ return 'image/webp';
66
+ if (ext === '.gif')
67
+ return 'image/gif';
68
+ return null;
69
+ }
70
+ async function fileExists(file) {
71
+ try {
72
+ const st = await fs.stat(file);
73
+ return st.isFile();
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ async function walk(dir, visit) {
80
+ let entries;
81
+ try {
82
+ entries = await fs.readdir(dir, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ for (const entry of entries) {
88
+ const full = path.join(dir, entry.name);
89
+ if (entry.isDirectory()) {
90
+ if (['node_modules', '.git', 'dist'].includes(entry.name))
91
+ continue;
92
+ await walk(full, visit);
93
+ }
94
+ else {
95
+ await visit(full);
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=image-artifact-path-contract.js.map