sneakoscope 0.7.46 → 0.7.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/cli/main.mjs +110 -51
- package/src/cli/maintenance-commands.mjs +2 -7
- package/src/core/auto-review.mjs +1 -1
- package/src/core/db-safety.mjs +11 -33
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +9 -2
- package/src/core/init.mjs +9 -5
- package/src/core/permission-gates.mjs +99 -0
- package/src/core/pipeline.mjs +24 -6
- package/src/core/routes.mjs +2 -1
- package/src/core/tmux-ui.mjs +24 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex`
|
|
|
79
79
|
- Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
|
|
80
80
|
- Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
|
|
81
81
|
- Requires tmux 3.x or newer before opening the session.
|
|
82
|
-
- Creates or reuses a named detached tmux session,
|
|
82
|
+
- Creates or reuses a named detached tmux session and prints only the session, gate, attach, and blocker details needed to act.
|
|
83
83
|
|
|
84
84
|
## Installation
|
|
85
85
|
|
|
@@ -205,7 +205,7 @@ sks --mad
|
|
|
205
205
|
sks --mad --yes
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal.
|
|
208
|
+
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and needed migrations are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session.
|
|
209
209
|
|
|
210
210
|
MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
|
|
211
211
|
|
|
@@ -230,9 +230,9 @@ sks team dashboard latest
|
|
|
230
230
|
sks team log latest
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
233
|
+
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
234
234
|
|
|
235
|
-
The tmux Team launch is a live orchestration screen: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while
|
|
235
|
+
The tmux Team launch is a live orchestration screen in one tmux window: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
|
|
236
236
|
|
|
237
237
|
Agent sessions communicate through the bounded Team transcript. Use `sks team message <mission-id|latest> --from <agent> --to <agent|all> --message "..."` to add direct or broadcast messages; lane panes show messages addressed to that agent plus the fallback global tail.
|
|
238
238
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.48",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
package/src/cli/main.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import { createSkillCandidate, decideSkillInjection, skillDreamFixture, writeSki
|
|
|
56
56
|
import { classifyToolError, harnessGrowthReport } from '../core/evaluation.mjs';
|
|
57
57
|
import { runWorkflowPerfBench, validateWorkflowPerfReport } from '../core/perf-bench.mjs';
|
|
58
58
|
import { buildProofField, proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
|
|
59
|
+
import { permissionGateSummary } from '../core/permission-gates.mjs';
|
|
59
60
|
import { recordMistake, writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
|
|
60
61
|
import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/mistake-recall.mjs';
|
|
61
62
|
import { buildPromptContext } from '../core/prompt-context-builder.mjs';
|
|
@@ -702,7 +703,11 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
|
|
|
702
703
|
permissions_deactivated: false,
|
|
703
704
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
704
705
|
direct_execute_sql_allowed: true,
|
|
706
|
+
normal_db_writes_allowed: true,
|
|
707
|
+
live_server_writes_allowed: true,
|
|
708
|
+
migration_apply_allowed: true,
|
|
705
709
|
catastrophic_safety_guard_active: true,
|
|
710
|
+
permission_profile: permissionGateSummary(),
|
|
706
711
|
contract_hash: contract.sealed_hash || null
|
|
707
712
|
});
|
|
708
713
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
@@ -721,6 +726,9 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
|
|
|
721
726
|
mad_sks_gate_ready: true,
|
|
722
727
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
723
728
|
direct_execute_sql_allowed: true,
|
|
729
|
+
normal_db_writes_allowed: true,
|
|
730
|
+
live_server_writes_allowed: true,
|
|
731
|
+
migration_apply_allowed: true,
|
|
724
732
|
catastrophic_safety_guard_active: true
|
|
725
733
|
}
|
|
726
734
|
};
|
|
@@ -813,7 +821,11 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
813
821
|
deactivates_when_gate_passed: gateFile,
|
|
814
822
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
815
823
|
direct_execute_sql_allowed: true,
|
|
824
|
+
normal_db_writes_allowed: true,
|
|
825
|
+
live_server_writes_allowed: true,
|
|
826
|
+
migration_apply_allowed: true,
|
|
816
827
|
catastrophic_safety_guard_active: true,
|
|
828
|
+
permission_profile: permissionGateSummary(),
|
|
817
829
|
contract_hash: contract.sealed_hash || null
|
|
818
830
|
};
|
|
819
831
|
await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
|
|
@@ -830,6 +842,9 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
830
842
|
mad_sks_gate_file: gateFile,
|
|
831
843
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
832
844
|
direct_execute_sql_allowed: true,
|
|
845
|
+
normal_db_writes_allowed: true,
|
|
846
|
+
live_server_writes_allowed: true,
|
|
847
|
+
migration_apply_allowed: true,
|
|
833
848
|
catastrophic_safety_guard_active: true
|
|
834
849
|
};
|
|
835
850
|
}
|
|
@@ -1071,8 +1086,9 @@ async function madHighCommand(args = []) {
|
|
|
1071
1086
|
return;
|
|
1072
1087
|
}
|
|
1073
1088
|
const profile = await enableMadHighProfile();
|
|
1074
|
-
|
|
1075
|
-
console.log(
|
|
1089
|
+
const madLaunch = await activateMadTmuxPermissionState(process.cwd());
|
|
1090
|
+
console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
|
|
1091
|
+
console.log('Live full-access active; catastrophic DB wipe/all-row/project-management guards remain.');
|
|
1076
1092
|
const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
|
|
1077
1093
|
return launchTmuxUi([...cleanArgs, '--workspace', workspace], {
|
|
1078
1094
|
codexArgs: profile.launch_args,
|
|
@@ -1081,6 +1097,67 @@ async function madHighCommand(args = []) {
|
|
|
1081
1097
|
});
|
|
1082
1098
|
}
|
|
1083
1099
|
|
|
1100
|
+
async function activateMadTmuxPermissionState(cwd = process.cwd()) {
|
|
1101
|
+
const root = await sksRoot();
|
|
1102
|
+
if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
|
|
1103
|
+
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad tmux live full-access session' });
|
|
1104
|
+
const gate = {
|
|
1105
|
+
schema_version: 1,
|
|
1106
|
+
passed: false,
|
|
1107
|
+
mad_sks_permission_active: true,
|
|
1108
|
+
permissions_deactivated: false,
|
|
1109
|
+
live_server_writes_allowed: true,
|
|
1110
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
1111
|
+
direct_execute_sql_allowed: true,
|
|
1112
|
+
normal_db_writes_allowed: true,
|
|
1113
|
+
migration_apply_allowed: true,
|
|
1114
|
+
catastrophic_safety_guard_active: true,
|
|
1115
|
+
permission_profile: permissionGateSummary(),
|
|
1116
|
+
activated_by: 'sks --mad',
|
|
1117
|
+
cwd: path.resolve(cwd || process.cwd())
|
|
1118
|
+
};
|
|
1119
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
|
|
1120
|
+
await writeJsonAtomic(path.join(dir, 'route-context.json'), {
|
|
1121
|
+
route: 'MadSKS',
|
|
1122
|
+
command: '$MAD-SKS',
|
|
1123
|
+
mode: 'MADSKS',
|
|
1124
|
+
task: gate.activated_by,
|
|
1125
|
+
mad_sks_authorization: true,
|
|
1126
|
+
tmux_launch: true,
|
|
1127
|
+
permission_profile: gate.permission_profile
|
|
1128
|
+
});
|
|
1129
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
1130
|
+
ts: nowIso(),
|
|
1131
|
+
type: 'mad_sks.tmux_permission_opened',
|
|
1132
|
+
route: 'MadSKS',
|
|
1133
|
+
live_server_writes_allowed: true,
|
|
1134
|
+
catastrophic_safety_guard_active: true
|
|
1135
|
+
});
|
|
1136
|
+
await setCurrent(root, {
|
|
1137
|
+
mission_id: id,
|
|
1138
|
+
route: 'MadSKS',
|
|
1139
|
+
route_command: '$MAD-SKS',
|
|
1140
|
+
mode: 'MADSKS',
|
|
1141
|
+
phase: 'MADSKS_TMUX_PERMISSION_ACTIVE',
|
|
1142
|
+
questions_allowed: false,
|
|
1143
|
+
implementation_allowed: true,
|
|
1144
|
+
mad_sks_active: true,
|
|
1145
|
+
mad_sks_modifier: true,
|
|
1146
|
+
mad_sks_gate_file: 'mad-sks-gate.json',
|
|
1147
|
+
mad_sks_gate_ready: true,
|
|
1148
|
+
live_server_writes_allowed: true,
|
|
1149
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
1150
|
+
direct_execute_sql_allowed: true,
|
|
1151
|
+
normal_db_writes_allowed: true,
|
|
1152
|
+
migration_apply_allowed: true,
|
|
1153
|
+
catastrophic_safety_guard_active: true,
|
|
1154
|
+
permission_profile: gate.permission_profile,
|
|
1155
|
+
stop_gate: 'mad-sks-gate.json',
|
|
1156
|
+
prompt: gate.activated_by
|
|
1157
|
+
});
|
|
1158
|
+
return { mission_id: id, dir, gate };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1084
1161
|
async function maybePromptSksUpdateForLaunch(args = [], opts = {}) {
|
|
1085
1162
|
if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
|
|
1086
1163
|
const latest = await npmPackageVersion('sneakoscope');
|
|
@@ -2348,7 +2425,7 @@ async function selftest() {
|
|
|
2348
2425
|
const madStandaloneResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madStandaloneTmp, input: madStandalonePayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2349
2426
|
if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
|
|
2350
2427
|
const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
|
|
2351
|
-
if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json') throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate scoped permissions');
|
|
2428
|
+
if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json' || madStandaloneState.normal_db_writes_allowed !== true || madStandaloneState.live_server_writes_allowed !== true || madStandaloneState.migration_apply_allowed !== true) throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate live full-access scoped permissions');
|
|
2352
2429
|
const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
|
|
2353
2430
|
const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
|
|
2354
2431
|
if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
|
|
@@ -2358,7 +2435,7 @@ async function selftest() {
|
|
|
2358
2435
|
const madModifierResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madModifierTmp, input: madModifierPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2359
2436
|
if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
|
|
2360
2437
|
const madModifierState = await readJson(stateFile(madModifierTmp), {});
|
|
2361
|
-
if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json') throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate scoped permissions');
|
|
2438
|
+
if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json' || madModifierState.normal_db_writes_allowed !== true || madModifierState.live_server_writes_allowed !== true || madModifierState.migration_apply_allowed !== true) throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate live full-access scoped permissions');
|
|
2362
2439
|
if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
|
|
2363
2440
|
const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
|
|
2364
2441
|
if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
|
|
@@ -2422,9 +2499,9 @@ async function selftest() {
|
|
|
2422
2499
|
const hookGoalDelegationContext = hookGoalDelegationJson.hookSpecificOutput?.additionalContext || '';
|
|
2423
2500
|
const hookGoalDelegationBridgeMatch = hookGoalDelegationContext.match(/Goal bridge mission: (M-[A-Za-z0-9-]+)/);
|
|
2424
2501
|
if (!hookGoalDelegationBridgeMatch || !hookGoalDelegationContext.includes('Delegated execution route: $Team')) throw new Error('selftest failed: $Goal implementation prompt did not prepare a bridge plus Team delegation');
|
|
2425
|
-
if (
|
|
2502
|
+
if (hookGoalDelegationContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalDelegationContext.includes('$Team route prepared')) throw new Error('selftest failed: $Goal implementation delegation did not prepare direct Team route');
|
|
2426
2503
|
const hookGoalDelegationState = await readJson(stateFile(hookGoalDelegationTmp), {});
|
|
2427
|
-
if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== '
|
|
2504
|
+
if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalDelegationState.implementation_allowed === false || !hookGoalDelegationState.team_plan_ready) throw new Error('selftest failed: $Goal implementation delegation did not leave direct Team ready');
|
|
2428
2505
|
if (!(await exists(path.join(missionDir(hookGoalDelegationTmp, hookGoalDelegationBridgeMatch[1]), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest failed: $Goal implementation delegation did not write bridge workflow artifact');
|
|
2429
2506
|
const activeGoalMissionId = hookState.mission_id;
|
|
2430
2507
|
const hookGoalOverlayPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Team 발표자료 만들어줘' });
|
|
@@ -2432,12 +2509,11 @@ async function selftest() {
|
|
|
2432
2509
|
if (hookGoalOverlayResult.code !== 0) throw new Error(`selftest failed: active Goal overlay hook exited ${hookGoalOverlayResult.code}: ${hookGoalOverlayResult.stderr}`);
|
|
2433
2510
|
const hookGoalOverlayJson = JSON.parse(hookGoalOverlayResult.stdout);
|
|
2434
2511
|
const hookGoalOverlayContext = hookGoalOverlayJson.hookSpecificOutput?.additionalContext || '';
|
|
2435
|
-
if (
|
|
2512
|
+
if (hookGoalOverlayContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalOverlayContext.includes('$Team route prepared')) throw new Error('selftest failed: active Goal hijacked a plain Korean implementation prompt instead of preparing direct Team');
|
|
2436
2513
|
if (!hookGoalOverlayContext.includes(`Active Goal overlay: existing Goal mission ${activeGoalMissionId}`) || !hookGoalOverlayContext.includes('goal-workflow.json')) throw new Error('selftest failed: active Goal overlay context was not included with the new route');
|
|
2437
|
-
if (hookGoalOverlayContext.indexOf('MANDATORY ambiguity-removal gate activated') > hookGoalOverlayContext.indexOf('Active Goal overlay:')) throw new Error('selftest failed: active Goal overlay appeared before the newly prepared Team gate');
|
|
2438
2514
|
const hookGoalOverlayState = await readJson(stateFile(hookGoalTmp), {});
|
|
2439
|
-
if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== '
|
|
2440
|
-
if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), '
|
|
2515
|
+
if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalOverlayState.implementation_allowed === false || !hookGoalOverlayState.team_plan_ready) throw new Error('selftest failed: active Goal overlay did not leave a new direct Team mission current');
|
|
2516
|
+
if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: active Goal overlay Team mission did not write team-plan.json');
|
|
2441
2517
|
const hookUpdateCurrentTmp = tmpdir();
|
|
2442
2518
|
await initProject(hookUpdateCurrentTmp, {});
|
|
2443
2519
|
const hookUpdateCurrentEnv = { SKS_DISABLE_UPDATE_CHECK: '0', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' };
|
|
@@ -2522,7 +2598,7 @@ async function selftest() {
|
|
|
2522
2598
|
if (hookKoreanSksResult.code !== 0) throw new Error(`selftest failed: Korean SKS hook exited ${hookKoreanSksResult.code}: ${hookKoreanSksResult.stderr}`);
|
|
2523
2599
|
const hookKoreanSksJson = JSON.parse(hookKoreanSksResult.stdout);
|
|
2524
2600
|
const hookKoreanSksContext = hookKoreanSksJson.hookSpecificOutput?.additionalContext || '';
|
|
2525
|
-
if (!hookKoreanSksContext.includes('
|
|
2601
|
+
if (!hookKoreanSksContext.includes('$Team route prepared') || hookKoreanSksContext.includes('GOAL_PRECISE: 이번 작업의 최종 목표') || hookKoreanSksContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: Korean prompt did not prepare direct Team route');
|
|
2526
2602
|
if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest failed: Korean implementation prompt did not promote to Team route');
|
|
2527
2603
|
if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest failed: Korean implementation prompt still used answer-only pipeline');
|
|
2528
2604
|
const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
|
|
@@ -2535,12 +2611,10 @@ async function selftest() {
|
|
|
2535
2611
|
if (hookPaymentTeamResult.code !== 0) throw new Error(`selftest failed: payment/auth Team hook exited ${hookPaymentTeamResult.code}: ${hookPaymentTeamResult.stderr}`);
|
|
2536
2612
|
const hookPaymentTeamJson = JSON.parse(hookPaymentTeamResult.stdout);
|
|
2537
2613
|
const hookPaymentTeamContext = hookPaymentTeamJson.hookSpecificOutput?.additionalContext || '';
|
|
2538
|
-
if (!hookPaymentTeamContext.includes('
|
|
2614
|
+
if (!hookPaymentTeamContext.includes('$Team route prepared') || hookPaymentTeamContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: predictable payment/auth Team prompt did not prepare direct Team route');
|
|
2539
2615
|
if (hookPaymentTeamContext.includes('PAYMENT_RETRY_POLICY') || hookPaymentTeamContext.includes('AUTH_PROTOCOL_CHANGE_ALLOWED')) throw new Error('selftest failed: predictable payment/auth policy defaults were asked instead of inferred');
|
|
2540
2616
|
const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
|
|
2541
2617
|
if (hookPaymentTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookPaymentTeamState.implementation_allowed !== true || !hookPaymentTeamState.ambiguity_gate_passed || !hookPaymentTeamState.team_plan_ready) throw new Error('selftest failed: predictable payment/auth Team did not materialize after auto-seal');
|
|
2542
|
-
const hookPaymentTeamSchema = await readJson(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'required-answers.schema.json'));
|
|
2543
|
-
if (hookPaymentTeamSchema.slots.length !== 0 || hookPaymentTeamSchema.inferred_answers?.PAYMENT_RETRY_POLICY === undefined || hookPaymentTeamSchema.inferred_answers?.AUTH_SESSION_EXPIRED_BEHAVIOR === undefined) throw new Error('selftest failed: predictable payment/auth defaults were not recorded as inferred answers');
|
|
2544
2618
|
if (!(await exists(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: predictable payment/auth Team auto-seal did not write team-plan.json');
|
|
2545
2619
|
const hookTeamTmp = tmpdir();
|
|
2546
2620
|
await initProject(hookTeamTmp, {});
|
|
@@ -2548,45 +2622,38 @@ async function selftest() {
|
|
|
2548
2622
|
const hookTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: hookTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2549
2623
|
if (hookTeamResult.code !== 0) throw new Error(`selftest failed: $Team hook exited ${hookTeamResult.code}: ${hookTeamResult.stderr}`);
|
|
2550
2624
|
const hookTeamJson = JSON.parse(hookTeamResult.stdout);
|
|
2551
|
-
if (
|
|
2552
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('
|
|
2553
|
-
if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team goal');
|
|
2554
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team presentation question');
|
|
2555
|
-
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('Codex plan-tool interaction')) throw new Error('selftest failed: $Team ambiguity gate did not inject plan-tool guidance');
|
|
2625
|
+
if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated') || hookTeamJson.hookSpecificOutput?.additionalContext?.includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: $Team hook still forced ambiguity questions');
|
|
2626
|
+
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: $Team hook did not prepare direct Team route');
|
|
2556
2627
|
const hookTeamState = await readJson(stateFile(hookTeamTmp), {});
|
|
2557
|
-
if (hookTeamState.phase !== '
|
|
2558
|
-
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a
|
|
2559
|
-
if (await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) throw new Error('selftest failed: Team plan was created
|
|
2628
|
+
if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest failed: $Team hook did not prepare direct Team mission');
|
|
2629
|
+
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pipeline plan');
|
|
2630
|
+
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Team plan was not created directly');
|
|
2560
2631
|
const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2561
2632
|
if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
|
|
2562
2633
|
const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
|
|
2563
2634
|
const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
|
|
2564
2635
|
const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2565
|
-
if (hookTeamPendingState.mission_id
|
|
2566
|
-
if (
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
const
|
|
2570
|
-
if (
|
|
2571
|
-
const
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
if (String(hookTeamStopJson.reason || '').includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team stop goal');
|
|
2575
|
-
if (!String(hookTeamStopJson.reason || '').includes('sks pipeline answer')) throw new Error('selftest failed: Stop hook did not provide pipeline answer command');
|
|
2576
|
-
if (String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction') || String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook reprinted verbose clarification guidance');
|
|
2577
|
-
const hookTeamSchema = await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'required-answers.schema.json'));
|
|
2636
|
+
if (hookTeamPendingState.mission_id === hookTeamState.mission_id || hookTeamPendingContext.includes('Required questions still pending') || hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: direct Team follow-up was blocked by stale clarification behavior');
|
|
2637
|
+
if (hookTeamPendingState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !hookTeamPendingState.team_plan_ready) throw new Error('selftest failed: direct Team follow-up did not prepare a fresh Team mission');
|
|
2638
|
+
const qaClarificationTmp = tmpdir();
|
|
2639
|
+
await initProject(qaClarificationTmp, {});
|
|
2640
|
+
const hookQaClarificationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, prompt: '$QA-LOOP 로그인 QA 해줘' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2641
|
+
if (hookQaClarificationResult.code !== 0) throw new Error(`selftest failed: QA clarification hook exited ${hookQaClarificationResult.code}: ${hookQaClarificationResult.stderr}`);
|
|
2642
|
+
const hookQaClarificationState = await readJson(stateFile(qaClarificationTmp), {});
|
|
2643
|
+
const hookQaClarificationSchema = await readJson(path.join(missionDir(qaClarificationTmp, hookQaClarificationState.mission_id), 'required-answers.schema.json'));
|
|
2644
|
+
const hookTeamSchema = hookQaClarificationSchema;
|
|
2578
2645
|
const visibleQuestionsBlock = [
|
|
2579
2646
|
'Required questions',
|
|
2580
2647
|
...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
|
|
2581
2648
|
'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
|
|
2582
2649
|
].join('\n');
|
|
2583
|
-
const visibleQuestionDecision = await evaluateStop(
|
|
2650
|
+
const visibleQuestionDecision = await evaluateStop(qaClarificationTmp, hookQaClarificationState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
|
|
2584
2651
|
if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
|
|
2585
|
-
const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd:
|
|
2652
|
+
const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'npm run selftest' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2586
2653
|
if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
|
|
2587
2654
|
const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
|
|
2588
2655
|
if (hookTeamPreToolBlockedJson.decision !== 'block' || !String(hookTeamPreToolBlockedJson.reason || '').includes('ambiguity gate is paused')) throw new Error('selftest failed: pending clarification allowed implementation tool use before answers');
|
|
2589
|
-
const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd:
|
|
2656
|
+
const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2590
2657
|
if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
|
|
2591
2658
|
const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
|
|
2592
2659
|
if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
|
|
@@ -2597,18 +2664,6 @@ async function selftest() {
|
|
|
2597
2664
|
if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
|
|
2598
2665
|
const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
|
|
2599
2666
|
if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not infer the only missing slot');
|
|
2600
|
-
const hookTeamAnswers = {};
|
|
2601
|
-
for (const s of hookTeamSchema.slots) hookTeamAnswers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : (s.id === 'DB_MAX_BLAST_RADIUS' ? 'no_live_dml' : 'selftest'));
|
|
2602
|
-
hookTeamAnswers.NON_GOALS = [];
|
|
2603
|
-
const hookTeamAnswersPath = path.join(hookTeamTmp, 'team-answers.json');
|
|
2604
|
-
await writeJsonAtomic(hookTeamAnswersPath, hookTeamAnswers);
|
|
2605
|
-
const pipelineAnswerResult = await runProcess(process.execPath, [hookBin, 'pipeline', 'answer', 'latest', hookTeamAnswersPath], { cwd: hookTeamTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2606
|
-
if (pipelineAnswerResult.code !== 0) throw new Error(`selftest failed: pipeline answer exited ${pipelineAnswerResult.code}: ${pipelineAnswerResult.stderr}`);
|
|
2607
|
-
const answeredTeamState = await readJson(stateFile(hookTeamTmp), {});
|
|
2608
|
-
if (answeredTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !answeredTeamState.ambiguity_gate_passed || answeredTeamState.implementation_allowed !== true || !answeredTeamState.team_plan_ready || !answeredTeamState.pipeline_plan_ready) throw new Error('selftest failed: pipeline answer did not materialize Team after ambiguity gate');
|
|
2609
|
-
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'decision-contract.json')))) throw new Error('selftest failed: pipeline answer did not seal decision contract');
|
|
2610
|
-
if (validatePipelinePlan(await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT))).ok !== true) throw new Error('selftest failed: pipeline answer did not refresh a valid pipeline plan');
|
|
2611
|
-
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-live.md')))) throw new Error('selftest failed: Team artifacts missing after ambiguity gate passed');
|
|
2612
2667
|
const honestLoopTmp = tmpdir();
|
|
2613
2668
|
await initProject(honestLoopTmp, {});
|
|
2614
2669
|
const { id: honestLoopId, dir: honestLoopDir } = await createMission(honestLoopTmp, { mode: 'sks', prompt: 'honest loopback selftest' });
|
|
@@ -3026,6 +3081,8 @@ async function selftest() {
|
|
|
3026
3081
|
const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
|
|
3027
3082
|
if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
|
|
3028
3083
|
if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
|
|
3084
|
+
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'tiled' || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: Team tmux view did not expose single-window split UI metadata');
|
|
3085
|
+
if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
|
|
3029
3086
|
if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
|
|
3030
3087
|
if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
|
|
3031
3088
|
if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team tmux session is not named for visibility');
|
|
@@ -3263,7 +3320,9 @@ async function selftest() {
|
|
|
3263
3320
|
const madState = { mission_id: madMission.id, mode: 'TEAM', route_command: '$Team', stop_gate: 'team-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'team-gate.json' };
|
|
3264
3321
|
const columnCleanupSql = 'alter table users ' + 'dr' + 'op column legacy_name;';
|
|
3265
3322
|
const madColumnCleanupDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: columnCleanupSql }, { duringNoQuestion: false });
|
|
3266
|
-
if (madColumnCleanupDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS column cleanup was not allowed');
|
|
3323
|
+
if (madColumnCleanupDecision.action !== 'allow' || !madColumnCleanupDecision.mad_sks?.permission_profile?.allowed?.includes('direct_execute_sql_writes')) throw new Error('selftest failed: MAD-SKS column cleanup was not allowed through the modular permission gate');
|
|
3324
|
+
const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
|
|
3325
|
+
if (madLiveDmlDecision.action !== 'allow' || !madLiveDmlDecision.mad_sks?.live_server_writes_allowed) throw new Error('selftest failed: MAD-SKS targeted live DML was not allowed');
|
|
3267
3326
|
const tableRemovalSql = 'dr' + 'op table users;';
|
|
3268
3327
|
const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
|
|
3269
3328
|
if (madTableRemovalDecision.action !== 'block') throw new Error('selftest failed: MAD-SKS catastrophic table removal was not blocked');
|
|
@@ -1542,21 +1542,16 @@ export async function team(args) {
|
|
|
1542
1542
|
result.tmux = await launchTmuxTeamView({ root, missionId: id, plan, promptFile: result.workflow, json: flag(args, '--json') || !openTmux });
|
|
1543
1543
|
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1544
1544
|
console.log(`Team mission created: ${id}`);
|
|
1545
|
-
console.log(`Plan: ${path.relative(root, result.plan)}`);
|
|
1546
|
-
console.log(`Pipeline plan: ${path.relative(root, result.pipeline_plan)}`);
|
|
1547
1545
|
console.log(`Agent sessions: ${agentSessions}`);
|
|
1548
1546
|
console.log(`Role counts: ${formatRoleCounts(roleCounts)}`);
|
|
1549
|
-
console.log(`Workflow: ${path.relative(root, result.workflow)}`);
|
|
1550
|
-
console.log(`Runtime graph: ${path.relative(root, result.team_graph)}`);
|
|
1551
|
-
console.log(`Worker inbox: ${path.relative(root, result.worker_inbox_dir)}`);
|
|
1552
|
-
console.log(`Live: ${path.relative(root, result.live)}`);
|
|
1553
1547
|
if (result.tmux.ready) {
|
|
1554
1548
|
const tmuxState = result.tmux.created ? 'opened' : 'not opened; use --open-tmux for a tmux session';
|
|
1555
1549
|
console.log(`tmux: ${tmuxState} ${result.tmux.opened_lane_count || result.tmux.agents.length} agent lane(s) in ${result.tmux.session || result.tmux.workspace}`);
|
|
1550
|
+
if (result.tmux.split_ui?.mode) console.log(`tmux UI: ${result.tmux.split_ui.mode} (${result.tmux.split_ui.layout})`);
|
|
1556
1551
|
}
|
|
1557
1552
|
else console.log(`tmux: blocked (${Array.from(new Set(result.tmux.blockers || [])).join('; ')})`);
|
|
1558
1553
|
console.log(`Watch: sks team watch ${id}`);
|
|
1559
|
-
console.log(
|
|
1554
|
+
console.log(`Artifacts: .sneakoscope/missions/${id}`);
|
|
1560
1555
|
}
|
|
1561
1556
|
|
|
1562
1557
|
export function parseTeamCreateArgs(args) {
|
package/src/core/auto-review.mjs
CHANGED
|
@@ -180,7 +180,7 @@ function upsertProfile(text, profile, effort, reviewer = AUTO_REVIEW_REVIEWER) {
|
|
|
180
180
|
function upsertAutoReviewPolicy(text) {
|
|
181
181
|
const policy = [
|
|
182
182
|
'[auto_review]',
|
|
183
|
-
'policy = "
|
|
183
|
+
'policy = "In MAD launches, allow live-server work, normal DB writes, Supabase MCP DB writes, direct execute SQL, schema cleanup, and migration application for the active invocation. Deny only catastrophic database wipes, all-row value deletion/update, dangerous project or branch management, credential exfiltration, persistent security weakening, broad unrelated file deletion, and unrequested fallback implementation code."'
|
|
184
184
|
].join('\n');
|
|
185
185
|
const existing = readTableString(text, 'auto_review', 'policy');
|
|
186
186
|
if (existing && /unrequested fallback implementation code/i.test(existing)) return text;
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { exists, readJson, writeJsonAtomic, readText, nowIso, appendJsonlBounded } from './fsx.mjs';
|
|
3
3
|
import { missionDir, setCurrent } from './mission.mjs';
|
|
4
|
+
import { evaluateMadSksPermissionGate, isMadSksRouteState } from './permission-gates.mjs';
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
|
|
6
7
|
schema_version: 1,
|
|
@@ -229,33 +230,6 @@ function hasTableRemovalRisk(cls = {}) {
|
|
|
229
230
|
return ['drop_table', 'truncate'].some((reason) => reasons.has(reason));
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
function hasMadSksCatastrophicDbRisk(cls = {}) {
|
|
233
|
-
const reasons = new Set([
|
|
234
|
-
...(cls.reasons || []),
|
|
235
|
-
...(cls.sql?.reasons || []),
|
|
236
|
-
...(cls.command?.reasons || [])
|
|
237
|
-
]);
|
|
238
|
-
return [
|
|
239
|
-
'drop_database',
|
|
240
|
-
'drop_schema',
|
|
241
|
-
'drop_table',
|
|
242
|
-
'truncate',
|
|
243
|
-
'delete_without_where',
|
|
244
|
-
'update_without_where',
|
|
245
|
-
'supabase_db_reset',
|
|
246
|
-
'prisma_migrate_reset',
|
|
247
|
-
'postgres_database_admin_command'
|
|
248
|
-
].some((reason) => reasons.has(reason))
|
|
249
|
-
|| cls.toolReasons?.includes?.('dangerous_supabase_management_tool');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function isMadSksRouteState(state = {}) {
|
|
253
|
-
return state.mad_sks_active === true
|
|
254
|
-
|| String(state.mode || '').toUpperCase() === 'MADSKS'
|
|
255
|
-
|| String(state.route_command || '').toUpperCase() === '$MAD-SKS'
|
|
256
|
-
|| String(state.route || '').toUpperCase() === 'MADSKS';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
233
|
async function madSksOverrideState(root, state = {}) {
|
|
260
234
|
if (!isMadSksRouteState(state) || !state.mission_id || state.mad_sks_active === false) return { active: false };
|
|
261
235
|
const gateFile = state.mad_sks_gate_file || state.stop_gate || MAD_SKS_GATE_FILE;
|
|
@@ -279,31 +253,35 @@ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_PO
|
|
|
279
253
|
if (cls.level === 'safe') return { allowed: true, action: 'allow', reasons: ['read_only_operation'], classification: cls };
|
|
280
254
|
if (cls.level === 'possible_db') return { allowed: !noQuestion, action: noQuestion ? 'block' : 'warn', reasons: noQuestion ? ['unknown_database_operation_blocked_during_no_question_run'] : ['unknown_database_operation'], classification: cls };
|
|
281
255
|
if (madSks?.active && (cls.level === 'write' || cls.level === 'destructive')) {
|
|
282
|
-
|
|
256
|
+
const madGate = evaluateMadSksPermissionGate({ classification: cls, active: true });
|
|
257
|
+
if (!madGate.allowed) {
|
|
283
258
|
return {
|
|
284
259
|
allowed: false,
|
|
285
260
|
action: 'block',
|
|
286
|
-
reasons:
|
|
261
|
+
reasons: madGate.reasons,
|
|
287
262
|
classification: cls,
|
|
288
263
|
effective,
|
|
289
264
|
mad_sks: {
|
|
290
265
|
active: true,
|
|
291
266
|
catastrophic_safety_guard_active: true,
|
|
292
|
-
blocked_categories:
|
|
267
|
+
blocked_categories: madGate.blocked_categories,
|
|
268
|
+
permission_profile: madGate.profile
|
|
293
269
|
}
|
|
294
270
|
};
|
|
295
271
|
}
|
|
296
272
|
return {
|
|
297
273
|
allowed: true,
|
|
298
274
|
action: 'allow',
|
|
299
|
-
reasons:
|
|
275
|
+
reasons: madGate.reasons,
|
|
300
276
|
classification: cls,
|
|
301
277
|
effective,
|
|
302
278
|
mad_sks: {
|
|
303
279
|
active: true,
|
|
304
280
|
sks_db_constraints_removed: true,
|
|
305
281
|
catastrophic_safety_guard_active: true,
|
|
306
|
-
supabase_mcp_schema_cleanup_allowed: true
|
|
282
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
283
|
+
live_server_writes_allowed: true,
|
|
284
|
+
permission_profile: madGate.profile
|
|
307
285
|
}
|
|
308
286
|
};
|
|
309
287
|
}
|
|
@@ -416,7 +394,7 @@ export function dbBlockReason(decision) {
|
|
|
416
394
|
if ((decision.reasons || []).includes('mad_sks_catastrophic_db_operation_blocked')) {
|
|
417
395
|
return [
|
|
418
396
|
'Sneakoscope Codex MAD-SKS catastrophic database safeguard blocked this operation.',
|
|
419
|
-
'MAD-SKS opens Supabase MCP column/schema cleanup, direct execute SQL, and normal DB writes only while the mission gate is active.',
|
|
397
|
+
'MAD-SKS opens live-server changes, Supabase MCP column/schema cleanup, direct execute SQL, migrations when required, and normal DB writes only while the mission gate is active.',
|
|
420
398
|
'Whole database/table removal, all-row value wipes, database reset, and dangerous project or branch management remain blocked.'
|
|
421
399
|
].join(' ');
|
|
422
400
|
}
|
package/src/core/fsx.mjs
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
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.48';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -113,7 +113,7 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
|
|
|
113
113
|
const route = routePrompt(prompt);
|
|
114
114
|
const bypassActiveRoute = route?.id === 'DFix' || route?.id === 'Answer';
|
|
115
115
|
const goalOverlay = activeGoalOverlayContext(state, route);
|
|
116
|
-
if (
|
|
116
|
+
if (isBlockingClarificationAwaiting(state) && !looksLikeClarificationCancel(prompt)) {
|
|
117
117
|
const activeContext = await activeRouteContext(root, state);
|
|
118
118
|
const teamDigest = await teamLiveDigest(root, state);
|
|
119
119
|
const additionalContext = [updateContext, activeContext, teamDigest?.context].filter(Boolean).join('\n\n');
|
|
@@ -142,6 +142,12 @@ function isClarificationAwaiting(state = {}) {
|
|
|
142
142
|
|| ['QALOOP_CLARIFICATION_AWAITING_ANSWERS'].includes(String(state.phase || ''));
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function isBlockingClarificationAwaiting(state = {}) {
|
|
146
|
+
if (!isClarificationAwaiting(state)) return false;
|
|
147
|
+
return ['QALoop', 'PPT'].includes(String(state.route || ''))
|
|
148
|
+
|| ['QALOOP', 'PPT'].includes(String(state.mode || ''));
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
function looksLikeClarificationCancel(prompt = '') {
|
|
146
152
|
return /^(cancel|reset|restart|new mission|새로|취소|중단|리셋|다시 시작)\b/i.test(String(prompt || '').trim());
|
|
147
153
|
}
|
|
@@ -241,12 +247,13 @@ async function hookPermission(root, state, payload, noQuestion) {
|
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
function clarificationGateLocked(state = {}) {
|
|
244
|
-
if (
|
|
250
|
+
if (isBlockingClarificationAwaiting(state)) return true;
|
|
245
251
|
return Boolean(
|
|
246
252
|
state?.mission_id
|
|
247
253
|
&& state.implementation_allowed === false
|
|
248
254
|
&& state.ambiguity_gate_required === true
|
|
249
255
|
&& state.ambiguity_gate_passed !== true
|
|
256
|
+
&& isBlockingClarificationAwaiting(state)
|
|
250
257
|
);
|
|
251
258
|
}
|
|
252
259
|
|
package/src/core/init.mjs
CHANGED
|
@@ -200,7 +200,7 @@ export async function initProject(root, opts = {}) {
|
|
|
200
200
|
state: 'git-common-dir/sks-version-state.json'
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
|
-
database_safety: '
|
|
203
|
+
database_safety: 'default_safe; $MAD-SKS live-full-access profile is centralized in src/core/permission-gates.mjs and keeps only catastrophic DB safeguards',
|
|
204
204
|
gx_renderer: 'deterministic_svg_html'
|
|
205
205
|
};
|
|
206
206
|
await writeJsonAtomic(manifestPath, manifest);
|
|
@@ -331,7 +331,11 @@ export async function initProject(root, opts = {}) {
|
|
|
331
331
|
required_before_final: true,
|
|
332
332
|
verify_goal_evidence_tests_gaps: true
|
|
333
333
|
},
|
|
334
|
-
database_safety:
|
|
334
|
+
database_safety: {
|
|
335
|
+
...DEFAULT_DB_SAFETY_POLICY,
|
|
336
|
+
mad_sks_live_full_access: true,
|
|
337
|
+
mad_sks_gate_module: 'src/core/permission-gates.mjs'
|
|
338
|
+
},
|
|
335
339
|
performance: {
|
|
336
340
|
max_parallel_sessions: 2,
|
|
337
341
|
process_tail_bytes: 262144,
|
|
@@ -511,7 +515,7 @@ function managedCodexConfigBlocks() {
|
|
|
511
515
|
{ table: 'profiles.sks-mad-high', text: profileConfigBlock('sks-mad-high', 'high', { approval: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'auto_review' }) },
|
|
512
516
|
{
|
|
513
517
|
table: 'auto_review',
|
|
514
|
-
text: '[auto_review]\npolicy = "
|
|
518
|
+
text: '[auto_review]\npolicy = "In MAD launches, allow live-server work, normal DB writes, Supabase MCP DB writes, direct execute SQL, schema cleanup, and migration application for the active invocation. Deny only catastrophic database wipes, all-row value deletion/update, dangerous project or branch management, credential exfiltration, persistent security weakening, broad unrelated file deletion, and unrequested fallback implementation code."'
|
|
515
519
|
},
|
|
516
520
|
{ table: 'profiles.sks-default', text: profileConfigBlock('sks-default', 'high') }
|
|
517
521
|
];
|
|
@@ -731,7 +735,7 @@ export async function installSkills(root) {
|
|
|
731
735
|
'answer': `---\nname: answer\ndescription: Answer-only research route for ordinary questions that should not start implementation.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer summary plus Honest Mode; do not create missions, subagents, or file edits.\n`,
|
|
732
736
|
'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline status, pipeline plan, skill-dream. Promote code-changing work to Team unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and require human-approved conflict cleanup. ${skillDreamPolicyText()}\n`,
|
|
733
737
|
'wiki': `---\nname: wiki\ndescription: Dollar-command route for $Wiki TriWiki refresh, pack, validate, and prune commands.\n---\n\nUse for $Wiki or Korean wiki-refresh requests. Refresh/update/갱신: run sks wiki refresh, then validate .sneakoscope/wiki/context-pack.json. Pack: run sks wiki pack, then validate. Prune/clean/정리: use sks wiki refresh --prune, or sks wiki prune --dry-run for inspection. Report claims, anchors, trust, attention.use_first/hydrate_first, validation, and blockers. Do not start ambiguity-gated implementation, subagents, or unrelated work.\n`,
|
|
734
|
-
'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first, but score goal, constraints, success criteria, and codebase context before asking; ask only the lowest-clarity scope/safety/behavior/acceptance question(s), otherwise auto-seal inferred answers. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
|
|
738
|
+
'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first, but score goal, constraints, success criteria, and codebase context before asking; ask only the lowest-clarity scope/safety/behavior/acceptance question(s), otherwise auto-seal inferred answers. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions in one tmux window using split panes when tmux is available. $Team/$team plus sks --mad uses the MAD-SKS permission gate module: live server work, normal DB writes, Supabase MCP writes, direct SQL, schema cleanup, and needed migrations are open for the active invocation; only catastrophic DB wipe/all-row/project-management guards remain. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
|
|
735
739
|
'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Team alias for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the normal Team pipeline. Treat uploads as chat screenshot plus originals. Use Codex Computer Use visual inspection when available, list requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Team gates, review, reflection, and Honest Mode. ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. team-gate.json cannot pass From-Chat-IMG completion until unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
|
|
736
740
|
'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Ask scope, target, mutation, login. Credentials are runtime-only; never save secrets. UI-level E2E needs Codex Computer Use evidence or must be marked unverified; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI/browser verification. Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
|
|
737
741
|
'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with STP, audience, pain-point, format, research, design-system, and verification questions.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, seal presentation-specific ambiguity answers: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use design.md as the only design decision SSOT. If design.md is missing, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. If generated image assets or slide visual critique are needed, use imagegen/gpt-image-2 only when that asset/review need is explicitly sealed in the $PPT contract; prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) and use the OpenAI Image API with OPENAI_API_KEY when CLI-side required image assets can be generated. Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, record verified claims in ppt-fact-ledger.json, record generated image asset plans/results/blockers in ppt-image-asset-ledger.json, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, run the bounded ppt-review-policy/ppt-review-ledger/ppt-iteration-report loop, and verify readability, overlap, format fit, source coverage, export state, unsupported-claim status, image-asset completion, review-loop termination, and temporary build files cleanup. Finish with reflection and Honest Mode; do not skip STP/audience questions for presentation artifacts.\n`,
|
|
@@ -742,7 +746,7 @@ export async function installSkills(root) {
|
|
|
742
746
|
'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. Keep the loop short: frame outcome, compare a few mechanisms, falsify, keep the smallest useful probe, and avoid adding background process unless it reduces net route weight. Do not use for ordinary code edits.\n`,
|
|
743
747
|
'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`,
|
|
744
748
|
'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`,
|
|
745
|
-
'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS. 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
|
|
749
|
+
'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\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 opens live server work, Supabase MCP database writes, column/schema cleanup, direct execute SQL, migration application when required, and normal targeted DB writes. Keep only catastrophic safeguards: 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. Do not carry MAD-SKS permission into later prompts or routes. The permission profile is centralized in src/core/permission-gates.mjs so skill/hook/MCP-style gates share one decision function.\n`,
|
|
746
750
|
'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`,
|
|
747
751
|
'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`,
|
|
748
752
|
'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Score ambiguity first using goal, constraints, success criteria, and codebase context; ask only the lowest-clarity scope/safety/behavior/acceptance-changing questions within a small question budget, otherwise seal inferred answers. Materialize pipeline-plan.json for the runtime lane, kept/skipped stages, no-fallback invariant, and verification; inspect with sks pipeline plan, adding --proof-field when changed files are known. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: non-PPT UI/UX reads design.md; if missing use design-system-builder; use imagegen for image/logo/raster, and imagegen must prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) before API generation. For $PPT, ${pptPipelineAllowlistPolicyText()} ${getdesignReferencePolicyText()} TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const PERMISSION_GATE_SCHEMA_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export const MAD_SKS_PERMISSION_PROFILE = Object.freeze({
|
|
4
|
+
schema_version: PERMISSION_GATE_SCHEMA_VERSION,
|
|
5
|
+
id: 'mad_sks_live_full_access',
|
|
6
|
+
command: '$MAD-SKS',
|
|
7
|
+
intent: 'explicit_live_server_intervention',
|
|
8
|
+
scope: 'active_invocation_only',
|
|
9
|
+
authority_surface: 'skill_or_mcp_gate_function',
|
|
10
|
+
allowed: Object.freeze([
|
|
11
|
+
'live_server_changes',
|
|
12
|
+
'supabase_mcp_database_writes',
|
|
13
|
+
'direct_execute_sql_writes',
|
|
14
|
+
'schema_cleanup',
|
|
15
|
+
'column_cleanup',
|
|
16
|
+
'migration_apply_when_required',
|
|
17
|
+
'normal_dml_with_targeted_scope'
|
|
18
|
+
]),
|
|
19
|
+
blocked: Object.freeze([
|
|
20
|
+
'drop_database',
|
|
21
|
+
'drop_schema',
|
|
22
|
+
'drop_table',
|
|
23
|
+
'truncate_table',
|
|
24
|
+
'delete_without_where',
|
|
25
|
+
'update_without_where',
|
|
26
|
+
'database_reset',
|
|
27
|
+
'dangerous_project_or_branch_management',
|
|
28
|
+
'credential_exfiltration',
|
|
29
|
+
'persistent_security_weakening',
|
|
30
|
+
'unrequested_fallback_implementation'
|
|
31
|
+
]),
|
|
32
|
+
deactivation: 'mission_gate_passed_or_permissions_deactivated'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export function permissionGateSummary(profile = MAD_SKS_PERMISSION_PROFILE) {
|
|
36
|
+
return {
|
|
37
|
+
schema_version: PERMISSION_GATE_SCHEMA_VERSION,
|
|
38
|
+
id: profile.id,
|
|
39
|
+
scope: profile.scope,
|
|
40
|
+
authority_surface: profile.authority_surface,
|
|
41
|
+
allowed: [...profile.allowed],
|
|
42
|
+
blocked: [...profile.blocked],
|
|
43
|
+
deactivation: profile.deactivation
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isMadSksRouteState(state = {}) {
|
|
48
|
+
return state.mad_sks_active === true
|
|
49
|
+
|| String(state.mode || '').toUpperCase() === 'MADSKS'
|
|
50
|
+
|| String(state.route_command || '').toUpperCase() === '$MAD-SKS'
|
|
51
|
+
|| String(state.route || '').toUpperCase() === 'MADSKS'
|
|
52
|
+
|| state.permission_profile?.id === MAD_SKS_PERMISSION_PROFILE.id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function madSksCatastrophicDbReasons(cls = {}) {
|
|
56
|
+
const reasons = new Set([
|
|
57
|
+
...(cls.reasons || []),
|
|
58
|
+
...(cls.sql?.reasons || []),
|
|
59
|
+
...(cls.command?.reasons || [])
|
|
60
|
+
]);
|
|
61
|
+
const blocked = [
|
|
62
|
+
'drop_database',
|
|
63
|
+
'drop_schema',
|
|
64
|
+
'drop_table',
|
|
65
|
+
'truncate',
|
|
66
|
+
'delete_without_where',
|
|
67
|
+
'update_without_where',
|
|
68
|
+
'supabase_db_reset',
|
|
69
|
+
'prisma_migrate_reset',
|
|
70
|
+
'postgres_database_admin_command'
|
|
71
|
+
].filter((reason) => reasons.has(reason));
|
|
72
|
+
if (cls.toolReasons?.includes?.('dangerous_supabase_management_tool')) blocked.push('dangerous_project_or_branch_management');
|
|
73
|
+
return [...new Set(blocked)];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function evaluateMadSksPermissionGate({ classification, active = false } = {}) {
|
|
77
|
+
const cls = classification || { level: 'none', reasons: [] };
|
|
78
|
+
if (!active || !['write', 'destructive'].includes(cls.level)) return { matched: false, active: Boolean(active), profile: permissionGateSummary() };
|
|
79
|
+
const catastrophic = madSksCatastrophicDbReasons(cls);
|
|
80
|
+
if (catastrophic.length) {
|
|
81
|
+
return {
|
|
82
|
+
matched: true,
|
|
83
|
+
active: true,
|
|
84
|
+
allowed: false,
|
|
85
|
+
action: 'block',
|
|
86
|
+
reasons: ['mad_sks_catastrophic_db_operation_blocked'],
|
|
87
|
+
blocked_categories: catastrophic,
|
|
88
|
+
profile: permissionGateSummary()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
matched: true,
|
|
93
|
+
active: true,
|
|
94
|
+
allowed: true,
|
|
95
|
+
action: 'allow',
|
|
96
|
+
reasons: ['mad_sks_scoped_live_full_access_active'],
|
|
97
|
+
profile: permissionGateSummary()
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } fr
|
|
|
15
15
|
import { writeResearchPlan } from './research.mjs';
|
|
16
16
|
import { PPT_REQUIRED_GATE_FIELDS } from './ppt.mjs';
|
|
17
17
|
import { SPEED_LANE_POLICY } from './proof-field.mjs';
|
|
18
|
+
import { permissionGateSummary } from './permission-gates.mjs';
|
|
18
19
|
import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
19
20
|
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
|
|
20
21
|
import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
|
|
@@ -31,6 +32,7 @@ const COMPLIANCE_LOOP_GUARD_ARTIFACT = 'compliance-loop-guard.json';
|
|
|
31
32
|
const HARD_BLOCKER_ARTIFACT = 'hard-blocker.json';
|
|
32
33
|
const DEFAULT_COMPLIANCE_LOOP_LIMIT = 3;
|
|
33
34
|
const CLARIFICATION_BYPASS_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki', 'ComputerUse', 'Goal']);
|
|
35
|
+
const QUESTION_GATE_ROUTES = new Set(['QALoop', 'PPT']);
|
|
34
36
|
const LIGHTWEIGHT_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki']);
|
|
35
37
|
const FULL_ROUTE_STAGES = Object.freeze([
|
|
36
38
|
'route_classification',
|
|
@@ -337,8 +339,8 @@ export async function prepareRoute(root, prompt, state = {}) {
|
|
|
337
339
|
const required = routeNeedsContext7(route, prompt);
|
|
338
340
|
const reasoning = routeReasoning(route, prompt);
|
|
339
341
|
const subagentsRequired = routeRequiresSubagents(route, prompt);
|
|
340
|
-
if (route.id
|
|
341
|
-
if (route.id === 'Team') return withSkillDreamContext(await prepareTeam(root, route, task, required), dreamContext);
|
|
342
|
+
if (QUESTION_GATE_ROUTES.has(route.id) || route.id === 'MadSKS') return withSkillDreamContext(await prepareClarificationGate(root, route, task, required, { madSksAuthorization }), dreamContext);
|
|
343
|
+
if (route.id === 'Team') return withSkillDreamContext(await prepareTeam(root, route, task, required, { madSksAuthorization }), dreamContext);
|
|
342
344
|
if (route.id === 'Research') return withSkillDreamContext(await prepareResearch(root, route, task, required), dreamContext);
|
|
343
345
|
if (route.id === 'AutoResearch') return withSkillDreamContext(await prepareAutoResearch(root, route, task, required), dreamContext);
|
|
344
346
|
if (route.id === 'DB') return withSkillDreamContext(await prepareDb(root, route, task, required), dreamContext);
|
|
@@ -434,7 +436,10 @@ export async function activeRouteContext(root, state) {
|
|
|
434
436
|
if (state.honest_loop_required || /HONEST_LOOPBACK_AFTER_CLARIFICATION/.test(String(state.phase || ''))) {
|
|
435
437
|
return `SKS Honest Mode found unresolved gaps for ${state.route_command || state.route || state.mode}. Do not ask ambiguity questions again. Continue from the sealed decision-contract.json, inspect .sneakoscope/missions/${state.mission_id}/honest-loopback.json, fix gaps, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.${reasoningNote}${planNote}`;
|
|
436
438
|
}
|
|
437
|
-
if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS'))
|
|
439
|
+
if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) {
|
|
440
|
+
if (['QALoop', 'PPT'].includes(String(state.route || '')) || ['QALOOP', 'PPT'].includes(String(state.mode || ''))) return clarificationAwaitingAnswersContext(root, state);
|
|
441
|
+
return `Previous ${state.route_command || state.route || state.mode || 'SKS'} clarification state is non-blocking. Do not reprint old question sheets; prepare the current prompt normally and replace stale route state when needed.`;
|
|
442
|
+
}
|
|
438
443
|
if (state.clarification_passed && String(state.phase || '').includes('CLARIFICATION_CONTRACT_SEALED')) {
|
|
439
444
|
return `Mandatory ambiguity-removal gate passed for ${state.route_command || state.route || state.mode}. Use the sealed decision-contract.json and ${PIPELINE_PLAN_ARTIFACT} before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the clarified goal, constraints, non-goals, risk boundary, and test scope.${planNote}`;
|
|
440
445
|
}
|
|
@@ -589,7 +594,7 @@ function applyMadSksAuthorizationToSchema(schema = {}) {
|
|
|
589
594
|
schema.inference_notes = {
|
|
590
595
|
...(schema.inference_notes || {}),
|
|
591
596
|
MAD_SKS_MODE: 'explicit dollar command modifier is the permission boundary',
|
|
592
|
-
DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS opens Supabase MCP
|
|
597
|
+
DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS opens live-server DB changes, Supabase MCP cleanup, direct SQL, and needed migrations while blocking only catastrophic database wipe operations'
|
|
593
598
|
};
|
|
594
599
|
schema.slots = (schema.slots || []).filter((slot) => !/^(DB_|DATABASE_|DESTRUCTIVE_DB_|SUPABASE_MCP_POLICY$)/.test(slot.id));
|
|
595
600
|
return schema;
|
|
@@ -604,7 +609,10 @@ async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, co
|
|
|
604
609
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
605
610
|
direct_execute_sql_allowed: true,
|
|
606
611
|
normal_db_writes_allowed: true,
|
|
612
|
+
live_server_writes_allowed: true,
|
|
613
|
+
migration_apply_allowed: true,
|
|
607
614
|
catastrophic_safety_guard_active: true,
|
|
615
|
+
permission_profile: permissionGateSummary(),
|
|
608
616
|
contract_hash: contract.sealed_hash || null
|
|
609
617
|
});
|
|
610
618
|
await appendJsonl(path.join(dir, 'events.jsonl'), {
|
|
@@ -624,6 +632,8 @@ async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, co
|
|
|
624
632
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
625
633
|
direct_execute_sql_allowed: true,
|
|
626
634
|
normal_db_writes_allowed: true,
|
|
635
|
+
live_server_writes_allowed: true,
|
|
636
|
+
migration_apply_allowed: true,
|
|
627
637
|
catastrophic_safety_guard_active: true
|
|
628
638
|
}
|
|
629
639
|
};
|
|
@@ -642,7 +652,10 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
642
652
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
643
653
|
direct_execute_sql_allowed: true,
|
|
644
654
|
normal_db_writes_allowed: true,
|
|
655
|
+
live_server_writes_allowed: true,
|
|
656
|
+
migration_apply_allowed: true,
|
|
645
657
|
catastrophic_safety_guard_active: true,
|
|
658
|
+
permission_profile: permissionGateSummary(),
|
|
646
659
|
contract_hash: contract.sealed_hash || null
|
|
647
660
|
});
|
|
648
661
|
await appendJsonl(path.join(dir, 'events.jsonl'), {
|
|
@@ -659,6 +672,8 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
|
|
|
659
672
|
supabase_mcp_schema_cleanup_allowed: true,
|
|
660
673
|
direct_execute_sql_allowed: true,
|
|
661
674
|
normal_db_writes_allowed: true,
|
|
675
|
+
live_server_writes_allowed: true,
|
|
676
|
+
migration_apply_allowed: true,
|
|
662
677
|
catastrophic_safety_guard_active: true
|
|
663
678
|
};
|
|
664
679
|
}
|
|
@@ -748,7 +763,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
|
|
|
748
763
|
};
|
|
749
764
|
}
|
|
750
765
|
|
|
751
|
-
async function prepareTeam(root, route, task, required) {
|
|
766
|
+
async function prepareTeam(root, route, task, required, opts = {}) {
|
|
752
767
|
const spec = parseTeamSpecText(task);
|
|
753
768
|
const cleanTask = spec.prompt || task;
|
|
754
769
|
const fromChatImgRequired = hasFromChatImgSignal(cleanTask);
|
|
@@ -803,8 +818,11 @@ async function prepareTeam(root, route, task, required) {
|
|
|
803
818
|
await writeMistakeMemoryReport(dir, { mission_id: id, route: 'team', task: cleanTask }).catch(() => null);
|
|
804
819
|
await writeCodeStructureReport(root, dir, { missionId: id, exception: 'Team prepare records split-review risk; extraction happens only when the mission scope includes the touched file.' }).catch(() => null);
|
|
805
820
|
await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
|
|
821
|
+
const madSksState = opts.madSksAuthorization
|
|
822
|
+
? await materializeMadSksAuthorization(dir, id, route, { mad_sks_authorization: true }, {})
|
|
823
|
+
: {};
|
|
806
824
|
const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: cleanTask, required, ambiguity: { required: false, status: 'direct_team_cli' } });
|
|
807
|
-
await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
|
|
825
|
+
await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, implementation_allowed: true, ambiguity_gate_required: false, ambiguity_gate_passed: true, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_plan_ready: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, ...madSksState }));
|
|
808
826
|
return routeContext(route, id, cleanTask, required, `Run scouts, refresh/validate TriWiki, debate, close debate agents, form a fresh ${roster.bundle_size}-person executor team, then close/clean Team sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection.`);
|
|
809
827
|
}
|
|
810
828
|
|
package/src/core/routes.mjs
CHANGED
|
@@ -378,7 +378,7 @@ export const ROUTES = [
|
|
|
378
378
|
command: '$MAD-SKS',
|
|
379
379
|
mode: 'MADSKS',
|
|
380
380
|
route: 'explicit scoped database authorization modifier',
|
|
381
|
-
description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily open Supabase MCP
|
|
381
|
+
description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily open live server work, Supabase MCP DB writes, direct execute SQL, schema cleanup, migration application, and normal targeted DB writes for the active invocation, while blocking only catastrophic database-wipe/all-row/project-management operations.',
|
|
382
382
|
requiredSkills: ['mad-sks', 'db-safety-guard', 'pipeline-runner', 'context7-docs', REFLECTION_SKILL_NAME, 'honest-mode'],
|
|
383
383
|
lifecycle: ['explicit_invocation', 'auto_sealed_permission_scope', 'scoped_db_cleanup_override', 'catastrophic_db_guard', 'permission_deactivation', 'post_route_reflection', 'honest_mode'],
|
|
384
384
|
context7Policy: 'required',
|
|
@@ -568,6 +568,7 @@ export function routePrompt(prompt) {
|
|
|
568
568
|
}
|
|
569
569
|
const route = routeByDollarCommand(command) || null;
|
|
570
570
|
if (route?.id === 'SKS' && looksLikeTeamDefaultWork(stripDollarCommand(text))) return routeById('Team');
|
|
571
|
+
if (route?.id === 'Team') return route;
|
|
571
572
|
return route;
|
|
572
573
|
}
|
|
573
574
|
if (hasFromChatImgSignal(text)) return routeById('Team');
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -254,6 +254,18 @@ function colorizedLaneBannerCommand(lines = [], color = '') {
|
|
|
254
254
|
return `printf '\\033[1;${code}m%s\\033[0m\\n' ${shellEscape(text)}`;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
function compactTeamPaneBanner({ missionId, agentId, phase, style, overview = false } = {}) {
|
|
258
|
+
const role = overview ? 'overview' : `${style.label} (${style.color_name})`;
|
|
259
|
+
return [
|
|
260
|
+
`SKS Team ${missionId}`,
|
|
261
|
+
overview ? 'Overview: live orchestration' : `Agent: ${agentId}`,
|
|
262
|
+
`Lane: ${role}${phase ? ` | Phase: ${phase}` : ''}`,
|
|
263
|
+
overview ? 'Follow: team watch' : `Follow: team lane --agent ${agentId}`,
|
|
264
|
+
`Cleanup: sks team cleanup-tmux ${missionId}`,
|
|
265
|
+
''
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
257
269
|
export const TMUX_TEAM_LANE_STYLES = Object.freeze({
|
|
258
270
|
overview: Object.freeze({ role: 'overview', label: 'overview', color_name: 'Blue', color: 'blue', icon: 'layout-dashboard' }),
|
|
259
271
|
scout: Object.freeze({ role: 'scout', label: 'scout', color_name: 'Cyan', color: 'cyan', icon: 'search' }),
|
|
@@ -285,7 +297,7 @@ export function teamAgentCommand(root, missionId, agentId, phase) {
|
|
|
285
297
|
return [
|
|
286
298
|
terminalTitleCommand(title),
|
|
287
299
|
'clear',
|
|
288
|
-
colorizedLaneBannerCommand(
|
|
300
|
+
colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId, phase, style }), style.color),
|
|
289
301
|
`cd ${shellEscape(root)}`,
|
|
290
302
|
`node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
|
|
291
303
|
].join('; ');
|
|
@@ -297,7 +309,7 @@ export function teamOverviewCommand(root, missionId) {
|
|
|
297
309
|
return [
|
|
298
310
|
terminalTitleCommand(title),
|
|
299
311
|
'clear',
|
|
300
|
-
colorizedLaneBannerCommand(
|
|
312
|
+
colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId: 'mission_overview', style, overview: true }), style.color),
|
|
301
313
|
`cd ${shellEscape(root)}`,
|
|
302
314
|
`node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`
|
|
303
315
|
].join('; ');
|
|
@@ -495,6 +507,14 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
495
507
|
}));
|
|
496
508
|
const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(launch.root, missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
|
|
497
509
|
const lanes = [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
|
|
510
|
+
const splitUi = {
|
|
511
|
+
mode: 'single_window_split_panes',
|
|
512
|
+
window: 'sks',
|
|
513
|
+
layout: 'tiled',
|
|
514
|
+
live_updates: true,
|
|
515
|
+
panes_show: ['overview', 'scout', 'planning', 'execution', 'review', 'safety'],
|
|
516
|
+
user_attach_command: launch.attach_command
|
|
517
|
+
};
|
|
498
518
|
const result = {
|
|
499
519
|
ready: launch.ready,
|
|
500
520
|
tmux: launch.tmux,
|
|
@@ -503,6 +523,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
503
523
|
overview,
|
|
504
524
|
agents: commands,
|
|
505
525
|
lanes,
|
|
526
|
+
split_ui: splitUi,
|
|
506
527
|
cleanup_policy: 'mark-complete; tmux panes remain user controlled',
|
|
507
528
|
blockers: launch.blockers,
|
|
508
529
|
attach_command: launch.attach_command
|
|
@@ -520,6 +541,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
520
541
|
mission_id: missionId,
|
|
521
542
|
session: result.session,
|
|
522
543
|
attach_command: created.attach_command || launch.attach_command,
|
|
544
|
+
split_ui: splitUi,
|
|
523
545
|
cleanup_policy: result.cleanup_policy,
|
|
524
546
|
panes: created.panes || [],
|
|
525
547
|
lanes: lanes.map((entry) => ({
|