sneakoscope 0.6.69 → 0.6.72
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 +7 -3
- package/package.json +1 -1
- package/src/cli/main.mjs +21 -4
- package/src/core/auto-review.mjs +3 -2
- package/src/core/cmux-ui.mjs +207 -3
- package/src/core/decision-contract.mjs +3 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +84 -6
- package/src/core/init.mjs +4 -4
- package/src/core/pipeline.mjs +3 -2
- package/src/core/qa-loop.mjs +1 -1
- package/src/core/questions.mjs +3 -2
- package/src/core/routes.mjs +6 -1
- package/src/core/team-dag.mjs +1 -0
package/README.md
CHANGED
|
@@ -44,11 +44,12 @@ sks selftest --mock
|
|
|
44
44
|
| Area | What it does |
|
|
45
45
|
| --- | --- |
|
|
46
46
|
| CLI runtime | `sks`, `sks cmux`, and `sks --mad` open Codex CLI in a cmux workspace. |
|
|
47
|
-
| Codex App commands | Installs generated skills so `$Team`, `$DFix`, `$QA-LOOP`, `$Ralph`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
|
|
47
|
+
| Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$Ralph`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
|
|
48
48
|
| Team orchestration | Runs substantial work through ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode. |
|
|
49
|
+
| From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
|
|
49
50
|
| QA loop | Dogfoods UI/API behavior with safety gates, Browser/Computer evidence, safe fixes, and rechecks. |
|
|
50
51
|
| Ralph | Clarifies once, seals a decision contract, then continues without repeatedly asking the user. |
|
|
51
|
-
| TriWiki | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with `attention.use_first
|
|
52
|
+
| TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata, `attention.use_first`, and `attention.hydrate_first`. |
|
|
52
53
|
| Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
|
|
53
54
|
| DB safety | Treats SQL, migrations, Supabase, RLS, and destructive operations as high risk. |
|
|
54
55
|
| Release hygiene | Checks versioning, changelog, package contents, tarball size, syntax, selftests, and dry-run publishing. |
|
|
@@ -83,6 +84,7 @@ If the CLI is not on `PATH`, SKS also checks the app bundle path:
|
|
|
83
84
|
- Installs or upgrades the latest cmux cask through Homebrew when cmux is missing or not launchable.
|
|
84
85
|
- Re-probes the real cmux binary after install instead of trusting Homebrew's success text alone.
|
|
85
86
|
- Wakes cmux and retries the socket probe; if the socket is broken, SKS attempts a cmux app restart during that explicit launch.
|
|
87
|
+
- Reuses the named SKS MAD cmux workspace when it already exists and closes duplicate SKS-named MAD workspaces instead of increasing the workspace count on every launch.
|
|
86
88
|
|
|
87
89
|
## Installation
|
|
88
90
|
|
|
@@ -174,7 +176,9 @@ sks --mad
|
|
|
174
176
|
sks --mad --yes
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning cmux workspace with `approval_policy = "on-request"` and `approvals_reviewer = "auto_review"`. It is scoped to that explicit command and does not change normal SKS/DB safety defaults.
|
|
179
|
+
This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning cmux workspace with `approval_policy = "on-request"` and `approvals_reviewer = "auto_review"`. It is scoped to that explicit command and does not change normal SKS/DB safety defaults. Repeat launches select the existing named SKS MAD workspace and clean duplicate SKS-named MAD workspaces instead of creating an endless workspace list.
|
|
180
|
+
|
|
181
|
+
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.
|
|
178
182
|
|
|
179
183
|
Before launching, SKS checks whether a newer `sneakoscope` exists on npm. In an interactive terminal it prompts:
|
|
180
184
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.72",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Ralph, 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
|
@@ -25,12 +25,12 @@ import { DEFAULT_EVAL_THRESHOLDS, compareEvaluationReports, defaultEvaluationSce
|
|
|
25
25
|
import { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
|
|
26
26
|
import { contextCapsule } from '../core/triwiki-attention.mjs';
|
|
27
27
|
import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
|
|
28
|
-
import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, 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, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
|
|
28
|
+
import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, 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, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
|
|
29
29
|
import { context7Evidence, evaluateStop, recordContext7Evidence, recordSubagentEvidence } from '../core/pipeline.mjs';
|
|
30
30
|
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
|
|
31
31
|
import { appendTeamEvent, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
|
|
32
32
|
import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
33
|
-
import { CMUX_BREW_COMMAND, CMUX_BREW_UPGRADE_COMMAND, buildCmuxLaunchPlan, defaultCmuxWorkspaceName, ensureCmuxInstalled, formatCmuxBanner, launchCmuxTeamView, launchCmuxUi, platformCmuxInstallHint, runCmuxStatus, sanitizeCmuxWorkspaceName, cmuxAvailable } from '../core/cmux-ui.mjs';
|
|
33
|
+
import { CMUX_BREW_COMMAND, CMUX_BREW_UPGRADE_COMMAND, buildCmuxLaunchPlan, buildCmuxNewWorkspaceArgs, cmuxWorkspaceRef, cmuxWorkspaceRefFromText, defaultCmuxWorkspaceName, ensureCmuxInstalled, formatCmuxBanner, launchCmuxTeamView, launchCmuxUi, matchingCmuxWorkspaces, parseCmuxWorkspaceList, platformCmuxInstallHint, readCmuxWorkspaceRecord, runCmuxStatus, sanitizeCmuxWorkspaceName, cmuxAvailable, writeCmuxWorkspaceRecord } from '../core/cmux-ui.mjs';
|
|
34
34
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
35
35
|
|
|
36
36
|
const flag = (args, name) => args.includes(name);
|
|
@@ -492,7 +492,7 @@ async function updateCheck(args = []) {
|
|
|
492
492
|
if (result.update_available) console.log('Run: npm i -g sneakoscope');
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: questions -> $Answer, small design/content -> $DFix, code -> $Team. Use $From-Chat-IMG only for chat screenshot plus original attachments. Use $MAD-SKS only as an explicit scoped DB authorization modifier that can be combined with another $ route.';
|
|
495
|
+
const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: questions -> $Answer, small design/content -> $DFix, code -> $Team. Use $From-Chat-IMG only for chat screenshot plus original attachments. Use $MAD-SKS only as an explicit scoped DB authorization modifier that can be combined with another $ route. No route may invent unrequested fallback implementation code.';
|
|
496
496
|
|
|
497
497
|
function commands(args = []) {
|
|
498
498
|
if (flag(args, '--json')) return console.log(JSON.stringify({ aliases: ['sks', 'sneakoscope'], dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES, commands: COMMAND_CATALOG }, null, 2));
|
|
@@ -2367,8 +2367,21 @@ async function selftest() {
|
|
|
2367
2367
|
const madProfilePath = path.join(tmp, 'mad-codex-config.toml');
|
|
2368
2368
|
const madProfile = await enableMadHighProfile({ configPath: madProfilePath });
|
|
2369
2369
|
const madProfileText = await safeReadText(madProfilePath);
|
|
2370
|
-
if (madProfile.profile_name !== 'sks-mad-high' || !madProfileText.includes('sandbox_mode = "danger-full-access"') || !madProfileText.includes('approval_policy = "on-request"') || !madProfileText.includes('approvals_reviewer = "auto_review"') || !madProfileText.includes('model_reasoning_effort = "high"')) throw new Error('selftest failed: MAD high profile is not full-access auto-review high');
|
|
2370
|
+
if (madProfile.profile_name !== 'sks-mad-high' || !madProfileText.includes('sandbox_mode = "danger-full-access"') || !madProfileText.includes('approval_policy = "on-request"') || !madProfileText.includes('approvals_reviewer = "auto_review"') || !madProfileText.includes('model_reasoning_effort = "high"') || !madProfileText.includes('unrequested fallback implementation code')) throw new Error('selftest failed: MAD high profile is not full-access auto-review high with fallback-code guard');
|
|
2371
2371
|
if (!isMadHighLaunch(['--mad', '--high']) || isMadHighLaunch(['db', '--mad'])) throw new Error('selftest failed: MAD high launch flag parsing is not top-level only');
|
|
2372
|
+
const workspacePlan = { workspace: 'sks-mad-selftest', root: tmp, codexArgs: ['--profile', 'sks-mad-high'] };
|
|
2373
|
+
const workspaceArgs = buildCmuxNewWorkspaceArgs(workspacePlan, 'codex');
|
|
2374
|
+
if (!workspaceArgs.includes('--name') || !workspaceArgs.includes('sks-mad-selftest') || !workspaceArgs.includes('--description')) throw new Error('selftest failed: MAD cmux workspace is not named for reuse');
|
|
2375
|
+
const workspaceList = parseCmuxWorkspaceList(JSON.stringify({ workspaces: [
|
|
2376
|
+
{ id: 'keep-id', ref: 'workspace:1', name: 'sks-mad-selftest', cwd: tmp },
|
|
2377
|
+
{ id: 'other-id', ref: 'workspace:2', name: 'other', cwd: tmp }
|
|
2378
|
+
] }));
|
|
2379
|
+
const workspaceMatches = matchingCmuxWorkspaces(workspaceList, workspacePlan);
|
|
2380
|
+
if (workspaceMatches.length !== 1 || cmuxWorkspaceRef(workspaceMatches[0]) !== 'workspace:1') throw new Error('selftest failed: MAD cmux workspace reuse matching did not select the stable workspace');
|
|
2381
|
+
if (cmuxWorkspaceRefFromText('OK workspace:3') !== 'workspace:3') throw new Error('selftest failed: cmux workspace ref parser did not read cmux OK output');
|
|
2382
|
+
await writeCmuxWorkspaceRecord(workspacePlan, { ref: 'workspace:7', name: 'sks-mad-selftest', cwd: tmp });
|
|
2383
|
+
const workspaceRecord = await readCmuxWorkspaceRecord(workspacePlan);
|
|
2384
|
+
if (workspaceRecord?.ref !== 'workspace:7' || workspaceRecord.workspace !== 'sks-mad-selftest') throw new Error('selftest failed: MAD cmux workspace record was not persisted for stable reuse');
|
|
2372
2385
|
const guardBlocked = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
|
|
2373
2386
|
if (guardBlocked.action !== 'block') throw new Error('selftest failed: harness guard allowed skill tampering');
|
|
2374
2387
|
const setupBlocked = await checkHarnessModification(tmp, { command: 'sks setup --force' });
|
|
@@ -2656,6 +2669,10 @@ async function selftest() {
|
|
|
2656
2669
|
if (honestMissingSummaryResult.code !== 0) throw new Error(`selftest failed: missing-summary honest hook exited ${honestMissingSummaryResult.code}: ${honestMissingSummaryResult.stderr}`);
|
|
2657
2670
|
const honestMissingSummaryJson = JSON.parse(honestMissingSummaryResult.stdout);
|
|
2658
2671
|
if (honestMissingSummaryJson.decision !== 'block' || !String(honestMissingSummaryJson.reason || '').includes('completion summary')) throw new Error('selftest failed: Honest Mode without completion summary was accepted');
|
|
2672
|
+
const honestMissingSummaryRepeatResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**솔직모드**\n검증: selftest 통과\n남은 gap: 없음' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2673
|
+
if (honestMissingSummaryRepeatResult.code !== 0) throw new Error(`selftest failed: repeated missing-summary honest hook exited ${honestMissingSummaryRepeatResult.code}: ${honestMissingSummaryRepeatResult.stderr}`);
|
|
2674
|
+
const honestMissingSummaryRepeatJson = JSON.parse(honestMissingSummaryRepeatResult.stdout);
|
|
2675
|
+
if (honestMissingSummaryRepeatJson.decision === 'block' || !String(honestMissingSummaryRepeatJson.systemMessage || '').includes('repeat guard')) throw new Error('selftest failed: repeated completion-summary stop prompt was not suppressed');
|
|
2659
2676
|
const honestBlockedAsExpectedResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**작업 요약**\nlegacy QA report 차단 확인을 검증했습니다.\n**솔직모드**\n검증: selftest 통과, legacy `qa-report.md` 차단 확인\n제약: registry publish excluded' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2660
2677
|
if (honestBlockedAsExpectedResult.code !== 0) throw new Error(`selftest failed: blocked-as-expected honest hook exited ${honestBlockedAsExpectedResult.code}: ${honestBlockedAsExpectedResult.stderr}`);
|
|
2661
2678
|
const honestBlockedAsExpectedJson = JSON.parse(honestBlockedAsExpectedResult.stdout);
|
package/src/core/auto-review.mjs
CHANGED
|
@@ -180,9 +180,10 @@ function upsertProfile(text, profile, effort, reviewer = AUTO_REVIEW_REVIEWER) {
|
|
|
180
180
|
function upsertAutoReviewPolicy(text) {
|
|
181
181
|
const policy = [
|
|
182
182
|
'[auto_review]',
|
|
183
|
-
'policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion,
|
|
183
|
+
'policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."'
|
|
184
184
|
].join('\n');
|
|
185
|
-
|
|
185
|
+
const existing = readTableString(text, 'auto_review', 'policy');
|
|
186
|
+
if (existing && /unrequested fallback implementation code/i.test(existing)) return text;
|
|
186
187
|
return upsertTable(text, 'auto_review', policy);
|
|
187
188
|
}
|
|
188
189
|
|
package/src/core/cmux-ui.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { exists, packageRoot, runProcess, sha256, sksRoot, which } from './fsx.mjs';
|
|
4
|
+
import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
|
|
5
5
|
import { getCodexInfo } from './codex-adapter.mjs';
|
|
6
6
|
import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
|
|
7
7
|
|
|
@@ -28,6 +28,110 @@ export function defaultCmuxWorkspaceName(root) {
|
|
|
28
28
|
return sanitizeCmuxWorkspaceName(`sks-${base}-${hash}`);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function cmuxWorkspaceDescription(plan = {}) {
|
|
32
|
+
const profile = codexProfileFromArgs(plan.codexArgs);
|
|
33
|
+
return [
|
|
34
|
+
'managed-by=sneakoscope',
|
|
35
|
+
`workspace=${sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(plan.root))}`,
|
|
36
|
+
`root=${path.resolve(plan.root || process.cwd())}`,
|
|
37
|
+
`profile=${profile || 'default'}`
|
|
38
|
+
].join('; ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildCmuxNewWorkspaceArgs(plan = {}, command = '') {
|
|
42
|
+
return [
|
|
43
|
+
'new-workspace',
|
|
44
|
+
'--name', sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(plan.root)),
|
|
45
|
+
'--description', cmuxWorkspaceDescription(plan),
|
|
46
|
+
'--cwd', path.resolve(plan.root || process.cwd()),
|
|
47
|
+
'--command', command
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseCmuxWorkspaceList(text) {
|
|
52
|
+
const raw = String(text || '').trim();
|
|
53
|
+
if (!raw) return [];
|
|
54
|
+
try {
|
|
55
|
+
return normalizeWorkspacePayload(JSON.parse(raw));
|
|
56
|
+
} catch {}
|
|
57
|
+
return raw.split('\n').map(parseWorkspaceLine).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function matchingCmuxWorkspaces(workspaces = [], plan = {}) {
|
|
61
|
+
const targetName = sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(plan.root));
|
|
62
|
+
const root = path.resolve(plan.root || process.cwd());
|
|
63
|
+
return workspaces.filter((workspace) => {
|
|
64
|
+
const name = workspaceName(workspace);
|
|
65
|
+
const description = workspaceDescription(workspace);
|
|
66
|
+
const cwd = workspaceCwd(workspace);
|
|
67
|
+
if (name !== targetName && !description.includes(`workspace=${targetName}`)) return false;
|
|
68
|
+
if (!cwd && !description.includes(`root=${root}`)) return true;
|
|
69
|
+
return path.resolve(cwd || root) === root || description.includes(`root=${root}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function cmuxWorkspaceRef(workspace = {}) {
|
|
74
|
+
return String(workspace.ref || workspace.workspace_ref || workspace.handle || workspace.id || workspace.workspace_id || workspace.uuid || '').trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function cmuxWorkspaceRefFromText(text = '') {
|
|
78
|
+
return String(text || '').match(/\bworkspace:\d+\b/i)?.[0] || String(text || '').match(/[0-9a-f]{8}-[0-9a-f-]{27,}/i)?.[0] || '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function cmuxWorkspaceStatePath(plan = {}) {
|
|
82
|
+
return path.join(path.resolve(plan.root || process.cwd()), '.sneakoscope', 'state', 'cmux-workspaces.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function cmuxWorkspaceStateKey(plan = {}) {
|
|
86
|
+
const root = path.resolve(plan.root || process.cwd());
|
|
87
|
+
const workspace = sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(root));
|
|
88
|
+
const profile = codexProfileFromArgs(plan.codexArgs);
|
|
89
|
+
return sha256(`${root}\n${workspace}\n${profile || 'default'}`).slice(0, 16);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readCmuxWorkspaceRecord(plan = {}) {
|
|
93
|
+
const state = await readJson(cmuxWorkspaceStatePath(plan), {}).catch(() => ({}));
|
|
94
|
+
const record = state.workspaces?.[cmuxWorkspaceStateKey(plan)] || null;
|
|
95
|
+
return record && typeof record === 'object' ? record : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function writeCmuxWorkspaceRecord(plan = {}, workspace = {}) {
|
|
99
|
+
const statePath = cmuxWorkspaceStatePath(plan);
|
|
100
|
+
const state = await readJson(statePath, {}).catch(() => ({}));
|
|
101
|
+
const root = path.resolve(plan.root || process.cwd());
|
|
102
|
+
const name = workspaceName(workspace) || sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(root));
|
|
103
|
+
const record = {
|
|
104
|
+
workspace: name,
|
|
105
|
+
root,
|
|
106
|
+
ref: cmuxWorkspaceRef(workspace),
|
|
107
|
+
description: workspaceDescription(workspace) || cmuxWorkspaceDescription(plan),
|
|
108
|
+
cwd: workspaceCwd(workspace) || root,
|
|
109
|
+
profile: codexProfileFromArgs(plan.codexArgs) || 'default',
|
|
110
|
+
updated_at: nowIso()
|
|
111
|
+
};
|
|
112
|
+
if (!record.ref) return null;
|
|
113
|
+
const next = {
|
|
114
|
+
schema_version: 1,
|
|
115
|
+
updated_at: record.updated_at,
|
|
116
|
+
workspaces: {
|
|
117
|
+
...(state.workspaces && typeof state.workspaces === 'object' ? state.workspaces : {}),
|
|
118
|
+
[cmuxWorkspaceStateKey(plan)]: record
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
await writeJsonAtomic(statePath, next);
|
|
122
|
+
return record;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function forgetCmuxWorkspaceRecord(plan = {}) {
|
|
126
|
+
const statePath = cmuxWorkspaceStatePath(plan);
|
|
127
|
+
const state = await readJson(statePath, null).catch(() => null);
|
|
128
|
+
if (!state?.workspaces || typeof state.workspaces !== 'object') return;
|
|
129
|
+
const key = cmuxWorkspaceStateKey(plan);
|
|
130
|
+
if (!(key in state.workspaces)) return;
|
|
131
|
+
delete state.workspaces[key];
|
|
132
|
+
await writeJsonAtomic(statePath, { ...state, updated_at: nowIso(), workspaces: state.workspaces });
|
|
133
|
+
}
|
|
134
|
+
|
|
31
135
|
export function shellEscape(value) {
|
|
32
136
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
33
137
|
}
|
|
@@ -216,20 +320,86 @@ export async function launchCmuxUi(args = [], opts = {}) {
|
|
|
216
320
|
process.exitCode = 1;
|
|
217
321
|
return { plan: blocked };
|
|
218
322
|
}
|
|
323
|
+
if (args.includes('--status-only')) return { plan };
|
|
219
324
|
if (!args.includes('--no-open')) await openCmuxApp().catch(() => null);
|
|
220
325
|
const command = codexLaunchCommand(plan.root, plan.codex.bin, plan.codexArgs);
|
|
221
|
-
|
|
326
|
+
if (!args.includes('--status-only')) {
|
|
327
|
+
const reuse = await reuseExistingCmuxWorkspace(plan, { cleanup: opts.cleanupWorkspaces !== false });
|
|
328
|
+
if (reuse.reused) {
|
|
329
|
+
if (!args.includes('--quiet')) {
|
|
330
|
+
const suffix = reuse.closed_duplicates ? `; closed duplicate workspace(s): ${reuse.closed_duplicates}` : '';
|
|
331
|
+
console.log(`SKS cmux workspace reused: ${plan.workspace}${suffix}`);
|
|
332
|
+
}
|
|
333
|
+
return { plan, created: false, reused: true, workspace: reuse.workspace, cleanup: reuse };
|
|
334
|
+
}
|
|
335
|
+
if (!reuse.ok) {
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
console.error(`SKS cmux workspace check failed: ${reuse.error}`);
|
|
338
|
+
return { plan, workspace_reuse: reuse };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const created = spawnSync(plan.cmux.bin, buildCmuxNewWorkspaceArgs(plan, command), { encoding: 'utf8', stdio: 'pipe' });
|
|
342
|
+
if (!args.includes('--quiet')) {
|
|
343
|
+
if (created.stdout) process.stdout.write(created.stdout);
|
|
344
|
+
if (created.stderr) process.stderr.write(created.stderr);
|
|
345
|
+
}
|
|
222
346
|
if (created.status !== 0) {
|
|
223
347
|
process.exitCode = created.status || 1;
|
|
224
|
-
if (created.stderr) process.stderr.write(created.stderr);
|
|
348
|
+
if (args.includes('--quiet') && created.stderr) process.stderr.write(created.stderr);
|
|
225
349
|
return { plan };
|
|
226
350
|
}
|
|
351
|
+
const createdRef = cmuxWorkspaceRefFromText(`${created.stdout || ''}\n${created.stderr || ''}`);
|
|
352
|
+
if (createdRef) await writeCmuxWorkspaceRecord(plan, { ref: createdRef, name: plan.workspace, description: cmuxWorkspaceDescription(plan), cwd: plan.root }).catch(() => null);
|
|
227
353
|
if (args.includes('--no-open')) {
|
|
228
354
|
console.log(`SKS cmux workspace requested: ${plan.workspace}`);
|
|
229
355
|
}
|
|
230
356
|
return { plan, created: true };
|
|
231
357
|
}
|
|
232
358
|
|
|
359
|
+
async function reuseExistingCmuxWorkspace(plan = {}, opts = {}) {
|
|
360
|
+
const remembered = await reuseRecordedCmuxWorkspace(plan);
|
|
361
|
+
if (remembered.reused) return remembered;
|
|
362
|
+
const listed = await listCmuxWorkspaces(plan.cmux?.bin);
|
|
363
|
+
if (!listed.ok) return listed;
|
|
364
|
+
const matches = matchingCmuxWorkspaces(listed.workspaces, plan);
|
|
365
|
+
if (!matches.length) return { ok: true, reused: false, closed_duplicates: 0 };
|
|
366
|
+
const [keep, ...duplicates] = matches;
|
|
367
|
+
const ref = cmuxWorkspaceRef(keep);
|
|
368
|
+
if (!ref) return { ok: false, error: 'matching cmux workspace has no usable id/ref' };
|
|
369
|
+
const selected = await runProcess(plan.cmux.bin, ['select-workspace', '--workspace', ref], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
370
|
+
if (selected.code !== 0) return { ok: false, error: `${selected.stderr || selected.stdout || 'cmux select-workspace failed'}`.trim() };
|
|
371
|
+
let closed = 0;
|
|
372
|
+
if (opts.cleanup !== false) {
|
|
373
|
+
for (const duplicate of duplicates) {
|
|
374
|
+
const duplicateRef = cmuxWorkspaceRef(duplicate);
|
|
375
|
+
if (!duplicateRef || duplicateRef === ref) continue;
|
|
376
|
+
const close = await runProcess(plan.cmux.bin, ['close-workspace', '--workspace', duplicateRef], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
377
|
+
if (close.code === 0) closed += 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await writeCmuxWorkspaceRecord(plan, keep).catch(() => null);
|
|
381
|
+
return { ok: true, reused: true, workspace: keep, workspace_ref: ref, closed_duplicates: closed, total_matches: matches.length };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function reuseRecordedCmuxWorkspace(plan = {}) {
|
|
385
|
+
const record = await readCmuxWorkspaceRecord(plan);
|
|
386
|
+
const ref = cmuxWorkspaceRef(record || {});
|
|
387
|
+
if (!ref) return { ok: true, reused: false };
|
|
388
|
+
const selected = await runProcess(plan.cmux.bin, ['select-workspace', '--workspace', ref], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
389
|
+
if (selected.code === 0) return { ok: true, reused: true, workspace: record, workspace_ref: ref, remembered: true, closed_duplicates: 0 };
|
|
390
|
+
const error = `${selected.stderr || selected.stdout || 'cmux select-workspace failed'}`.trim();
|
|
391
|
+
if (isRecoverableCmuxSocketError(error)) return { ok: false, error, stale_ref: ref };
|
|
392
|
+
await forgetCmuxWorkspaceRecord(plan).catch(() => null);
|
|
393
|
+
return { ok: true, reused: false, stale_ref: ref, stale_error: error };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function listCmuxWorkspaces(bin) {
|
|
397
|
+
if (!bin) return { ok: false, error: 'cmux CLI not found' };
|
|
398
|
+
const run = await runProcess(bin, ['list-workspaces', '--json'], { timeoutMs: 5000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
399
|
+
if (run.code !== 0) return { ok: false, error: `${run.stderr || run.stdout || 'cmux list-workspaces failed'}`.trim() };
|
|
400
|
+
return { ok: true, workspaces: parseCmuxWorkspaceList(run.stdout || run.stderr || '') };
|
|
401
|
+
}
|
|
402
|
+
|
|
233
403
|
function printCmuxLaunchBlocked(plan, opts = {}) {
|
|
234
404
|
if (opts.concise) {
|
|
235
405
|
console.error('SKS cmux launch blocked.');
|
|
@@ -367,3 +537,37 @@ function readOption(args, name, fallback = null) {
|
|
|
367
537
|
const i = args.indexOf(name);
|
|
368
538
|
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
|
|
369
539
|
}
|
|
540
|
+
|
|
541
|
+
function codexProfileFromArgs(args = []) {
|
|
542
|
+
const i = Array.isArray(args) ? args.indexOf('--profile') : -1;
|
|
543
|
+
return i >= 0 && args[i + 1] ? String(args[i + 1]) : '';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function normalizeWorkspacePayload(payload) {
|
|
547
|
+
if (Array.isArray(payload)) return payload.filter((item) => item && typeof item === 'object');
|
|
548
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
549
|
+
for (const key of ['workspaces', 'workspace', 'items', 'data']) {
|
|
550
|
+
if (Array.isArray(payload[key])) return normalizeWorkspacePayload(payload[key]);
|
|
551
|
+
}
|
|
552
|
+
if (payload.result) return normalizeWorkspacePayload(payload.result);
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function parseWorkspaceLine(line) {
|
|
557
|
+
const text = String(line || '').trim();
|
|
558
|
+
if (!text) return null;
|
|
559
|
+
const ref = text.match(/\bworkspace:\d+\b/i)?.[0] || text.match(/[0-9a-f]{8}-[0-9a-f-]{27,}/i)?.[0] || '';
|
|
560
|
+
return ref ? { ref, title: text.replace(ref, '').trim(), raw: text } : null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function workspaceName(workspace = {}) {
|
|
564
|
+
return String(workspace.name || workspace.title || workspace.label || workspace.workspace_name || workspace.raw_name || '').trim();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function workspaceDescription(workspace = {}) {
|
|
568
|
+
return String(workspace.description || workspace.desc || workspace.subtitle || workspace.raw || '').trim();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function workspaceCwd(workspace = {}) {
|
|
572
|
+
return String(workspace.cwd || workspace.path || workspace.root || workspace.working_directory || workspace.workspace_cwd || '').trim();
|
|
573
|
+
}
|
|
@@ -45,6 +45,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
|
|
|
45
45
|
if_test_command_unknown: 'infer_from_repo_scripts_and_run_most_local_relevant_test',
|
|
46
46
|
if_e2e_unavailable: 'run_unit_or_integration_and_record_e2e_not_executed',
|
|
47
47
|
if_dependency_needed: 'avoid_new_dependency_unless_allowed_by_contract',
|
|
48
|
+
if_fallback_implementation_is_tempting: 'do_not_create_unrequested_substitute_code_block_with_evidence_instead',
|
|
48
49
|
if_existing_behavior_conflict: 'preserve_existing_public_behavior',
|
|
49
50
|
if_visual_cartridge_conflict: 'vgraph_json_wins_over_rendered_gx_artifact',
|
|
50
51
|
if_wiki_conflict: 'current_code_wins_over_wiki',
|
|
@@ -82,6 +83,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
|
|
|
82
83
|
qa_loop_mutation_policy: answers.QA_MUTATION_POLICY || null,
|
|
83
84
|
qa_loop_credentials_saved: false,
|
|
84
85
|
qa_loop_ui_requires_official_browser_or_computer_use: Boolean(answers.QA_SCOPE && answers.QA_SCOPE !== 'api_e2e_only'),
|
|
86
|
+
unrequested_fallback_code_allowed: false,
|
|
85
87
|
mad_sks_mode: madSks ? 'explicit_invocation_only' : false,
|
|
86
88
|
production_database_writes_allowed: madSks ? 'mad_sks_scoped' : false,
|
|
87
89
|
mcp_direct_execute_sql_writes_allowed: madSks ? 'mad_sks_scoped' : false,
|
|
@@ -113,6 +115,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
|
|
|
113
115
|
'explicit_user_answer',
|
|
114
116
|
'approved_defaults',
|
|
115
117
|
'database_safety_policy',
|
|
118
|
+
'no_unrequested_fallback_implementation_code',
|
|
116
119
|
'AGENTS.md',
|
|
117
120
|
'vgraph.json',
|
|
118
121
|
'current_code_and_tests',
|
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.6.
|
|
8
|
+
export const PACKAGE_VERSION = '0.6.72';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION } from './fsx.mjs';
|
|
2
|
+
import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256 } from './fsx.mjs';
|
|
3
3
|
import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.mjs';
|
|
4
4
|
import { missionDir, setCurrent, stateFile } from './mission.mjs';
|
|
5
5
|
import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.mjs';
|
|
@@ -10,6 +10,10 @@ const TEAM_DIGEST_MAX_EVENTS = 4;
|
|
|
10
10
|
const TEAM_DIGEST_MESSAGE_CHARS = 180;
|
|
11
11
|
const TEAM_DIGEST_CONTEXT_CHARS = 1600;
|
|
12
12
|
const TEAM_DIGEST_SYSTEM_CHARS = 260;
|
|
13
|
+
const STOP_REPEAT_GUARD_ARTIFACT = 'stop-hook-repeat-guard.json';
|
|
14
|
+
const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
|
|
15
|
+
const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
|
|
16
|
+
const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
|
|
13
17
|
|
|
14
18
|
async function loadHookPayload() {
|
|
15
19
|
const raw = await readStdin();
|
|
@@ -154,7 +158,7 @@ async function hookPostTool(root, state, payload, noQuestion) {
|
|
|
154
158
|
if (toolFailed(payload)) {
|
|
155
159
|
return {
|
|
156
160
|
additionalContext: [
|
|
157
|
-
'SKS no-question mode is active. Do not ask the user about this tool failure. Apply the active
|
|
161
|
+
'SKS no-question mode is active. Do not ask the user about this tool failure. Apply the active decision ladder, create a fix task only inside the sealed contract, and continue. Do not create unrequested fallback implementation code; block with evidence if the requested path is impossible.',
|
|
158
162
|
teamDigest?.context
|
|
159
163
|
].filter(Boolean).join('\n\n'),
|
|
160
164
|
systemMessage: joinSystemMessages(visibleHookMessage('post-tool'), teamDigest?.system)
|
|
@@ -188,15 +192,19 @@ async function hookStop(root, state, payload, noQuestion) {
|
|
|
188
192
|
if (!noQuestion) {
|
|
189
193
|
const last = extractLastMessage(payload);
|
|
190
194
|
if (!hasHonestMode(last)) {
|
|
191
|
-
|
|
195
|
+
const reason = 'SKS Honest Mode is required before finishing. Re-check the actual goal, verify evidence/tests, state gaps honestly, and only then provide the final answer. Include a short "SKS Honest Mode" or "솔직모드" section.';
|
|
196
|
+
const repeatDecision = await finalizationRepeatDecision(root, state, payload, reason, 'honest_mode_missing');
|
|
197
|
+
return repeatDecision || {
|
|
192
198
|
decision: 'block',
|
|
193
|
-
reason
|
|
199
|
+
reason
|
|
194
200
|
};
|
|
195
201
|
}
|
|
196
202
|
if (!hasCompletionSummary(last)) {
|
|
197
|
-
|
|
203
|
+
const reason = 'SKS final completion summary is required before finishing. Explain what was done, what changed for the user/repo, what was verified, and any remaining gaps before or alongside SKS Honest Mode.';
|
|
204
|
+
const repeatDecision = await finalizationRepeatDecision(root, state, payload, reason, 'completion_summary_missing');
|
|
205
|
+
return repeatDecision || {
|
|
198
206
|
decision: 'block',
|
|
199
|
-
reason
|
|
207
|
+
reason
|
|
200
208
|
};
|
|
201
209
|
}
|
|
202
210
|
if (shouldLoopBackAfterHonestMode(state) && hasHonestModeUnresolvedGap(last)) {
|
|
@@ -215,6 +223,76 @@ async function hookStop(root, state, payload, noQuestion) {
|
|
|
215
223
|
};
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
async function finalizationRepeatDecision(root, state = {}, payload = {}, reason = '', kind = 'finalization') {
|
|
227
|
+
const now = nowIso();
|
|
228
|
+
const guardPath = path.join(root, '.sneakoscope', 'state', STOP_REPEAT_GUARD_ARTIFACT);
|
|
229
|
+
const previous = await readJson(guardPath, {}).catch(() => ({}));
|
|
230
|
+
const limit = stopRepeatGuardLimit();
|
|
231
|
+
const entries = pruneStopRepeatEntries(previous.entries || {}, now);
|
|
232
|
+
const key = stopRepeatKey(state, payload, reason, kind);
|
|
233
|
+
const prior = entries[key] || {};
|
|
234
|
+
const repeatCount = stopRepeatInWindow(prior, now)
|
|
235
|
+
? Number(prior.repeat_count || 0) + 1
|
|
236
|
+
: 1;
|
|
237
|
+
const record = {
|
|
238
|
+
schema_version: 1,
|
|
239
|
+
updated_at: now,
|
|
240
|
+
window_ms: STOP_REPEAT_GUARD_WINDOW_MS,
|
|
241
|
+
limit,
|
|
242
|
+
entries: {
|
|
243
|
+
...entries,
|
|
244
|
+
[key]: {
|
|
245
|
+
kind,
|
|
246
|
+
route: state.route_command || state.route || state.mode || null,
|
|
247
|
+
mission_id: state.mission_id || null,
|
|
248
|
+
conversation_id: conversationId(payload),
|
|
249
|
+
first_seen: stopRepeatInWindow(prior, now) ? (prior.first_seen || now) : now,
|
|
250
|
+
last_seen: now,
|
|
251
|
+
repeat_count: repeatCount,
|
|
252
|
+
tripped: repeatCount >= limit,
|
|
253
|
+
reason
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
await writeJsonAtomic(guardPath, record).catch(() => null);
|
|
258
|
+
if (repeatCount < limit) return null;
|
|
259
|
+
return {
|
|
260
|
+
continue: true,
|
|
261
|
+
systemMessage: `SKS stop hook repeat guard suppressed repeated ${kind} prompt after ${repeatCount} identical block(s). No completion success is claimed by the hook.`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function stopRepeatKey(state = {}, payload = {}, reason = '', kind = '') {
|
|
266
|
+
return sha256(JSON.stringify({
|
|
267
|
+
kind,
|
|
268
|
+
reason,
|
|
269
|
+
conversation_id: conversationId(payload),
|
|
270
|
+
mission_id: state.mission_id || null,
|
|
271
|
+
route: state.route_command || state.route || state.mode || null,
|
|
272
|
+
gate: state.stop_gate || null
|
|
273
|
+
})).slice(0, 24);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function stopRepeatGuardLimit() {
|
|
277
|
+
const raw = Number.parseInt(process.env.SKS_STOP_REPEAT_GUARD_LIMIT || '', 10);
|
|
278
|
+
if (!Number.isFinite(raw)) return DEFAULT_STOP_REPEAT_GUARD_LIMIT;
|
|
279
|
+
return Math.max(1, Math.min(20, raw));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function stopRepeatInWindow(entry = {}, now = nowIso()) {
|
|
283
|
+
const last = Date.parse(entry.last_seen || '');
|
|
284
|
+
const current = Date.parse(now);
|
|
285
|
+
if (!Number.isFinite(last) || !Number.isFinite(current)) return false;
|
|
286
|
+
return current - last <= STOP_REPEAT_GUARD_WINDOW_MS;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function pruneStopRepeatEntries(entries = {}, now = nowIso()) {
|
|
290
|
+
return Object.fromEntries(Object.entries(entries)
|
|
291
|
+
.filter(([, entry]) => stopRepeatInWindow(entry, now))
|
|
292
|
+
.sort((a, b) => Date.parse(b[1]?.last_seen || '') - Date.parse(a[1]?.last_seen || ''))
|
|
293
|
+
.slice(0, STOP_REPEAT_GUARD_MAX_ENTRIES));
|
|
294
|
+
}
|
|
295
|
+
|
|
218
296
|
async function updateCheckContext(root, payload, prompt) {
|
|
219
297
|
if (process.env.SKS_DISABLE_UPDATE_CHECK === '1') return '';
|
|
220
298
|
const statePath = path.join(root, '.sneakoscope', 'state', 'update-check.json');
|
package/src/core/init.mjs
CHANGED
|
@@ -88,7 +88,7 @@ function isSksManagedHook(hook) {
|
|
|
88
88
|
return hook.type === 'command' && /\bhook\s+(?:user-prompt-submit|pre-tool|post-tool|permission-request|stop)\b/.test(command) && /\b(?:sks|sneakoscope|sks\.mjs)\b/.test(command);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Ralph asks only during prepare. After `decision-contract.json` is sealed, do not ask the user; resolve with the decision ladder.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Before substantive work, SKS checks npm for a newer package. If newer, ask update-now vs skip-for-this-conversation.\n- Versioning is managed by the SKS pre-commit hook; check `sks versioning status`. Bypass only with `SKS_DISABLE_VERSIONING=1`.\n- Installed harness files are immutable to LLM edits: `.codex/*`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/*policy*.json`, `AGENTS.md`, and `node_modules/sneakoscope`. The Sneakoscope engine source repo is the only automatic exception.\n- OMX/DCodex conflicts block setup/doctor. Show `sks conflicts prompt`; cleanup requires explicit human approval.\n- Do not stop at a plan when implementation was requested. Finish, verify, or report the hard blocker.\n\n## Routes\n\n- General execution/code-changing prompts default to `$Team`: analysis scouts, TriWiki refresh/validate, read-only debate, consensus, concrete runtime task graph/inboxes, fresh executor team, review, integration, Honest Mode.\n- `$DFix` is only for tiny design/content edits and bypasses the main pipeline. `$Answer`, `$Help`, and `$Wiki` stay lightweight.\n- For code work, surface route/guard/write scopes first, split independent worker scopes when available, and keep parent-owned integration and verification.\n- Design work reads `design.md`; if missing, use `design-system-builder`. Image/logo/raster assets use `imagegen`.\n- Research, AutoResearch, performance, token, accuracy, SEO/GEO, or workflow-improvement claims need experiment/eval evidence. Do not claim live model accuracy without a scored dataset.\n\n## Evidence And Context\n\n- Context7 is required for external libraries, APIs, MCPs, package managers, SDKs, and generated docs: resolve-library-id then query-docs.\n- When tech stack, framework, package, runtime, or deployment-platform versions change, use Context7 or official vendor web docs, record current syntax/security/limit guidance as high-priority TriWiki claims, then refresh and validate before coding.\n- TriWiki is the context-tracking SSOT for long-running missions, Team handoffs, and context-pressure recovery. Read `.sneakoscope/wiki/context-pack.json` before each stage, use `attention.use_first` for compact high-trust recall, hydrate `attention.hydrate_first` from source before risky or lower-trust decisions, refresh after findings or artifact changes, and validate before handoffs/final claims.\n- Source priority: current code/tests/config, decision contract, vgraph, beta, GX render/snapshot metadata, LLM Wiki coordinate index, then model knowledge only if allowed.\n- Final response before stop: summarize what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked; then run Honest Mode. Say what passed and what was not verified.\n\n## Safety\n\n- Database access is high risk. Use read-only inspection by default; live data mutation is out of scope unless a sealed contract allows local or branch-only migration files.\n- Task completion requires relevant tests or justification, zero unsupported critical claims, accepted visual/wiki drift, and final evidence.\n\n## Codex App\n\nUse `.codex/SNEAKOSCOPE.md`, generated `.agents/skills`, `.codex/hooks.json`, and SKS dollar commands (`$sks`, `$team`, `$dfix`, `$qa-loop`, etc.) as the app control surface.\n";
|
|
91
|
+
const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Ralph asks only during prepare. After `decision-contract.json` is sealed, do not ask the user; resolve with the decision ladder.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Before substantive work, SKS checks npm for a newer package. If newer, ask update-now vs skip-for-this-conversation.\n- Versioning is managed by the SKS pre-commit hook; check `sks versioning status`. Bypass only with `SKS_DISABLE_VERSIONING=1`.\n- Installed harness files are immutable to LLM edits: `.codex/*`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/*policy*.json`, `AGENTS.md`, and `node_modules/sneakoscope`. The Sneakoscope engine source repo is the only automatic exception.\n- OMX/DCodex conflicts block setup/doctor. Show `sks conflicts prompt`; cleanup requires explicit human approval.\n- Do not stop at a plan when implementation was requested. Finish, verify, or report the hard blocker.\n- Do not create unrequested fallback implementation code. If the requested path is impossible, block with evidence instead of inventing substitute behavior.\n\n## Routes\n\n- General execution/code-changing prompts default to `$Team`: analysis scouts, TriWiki refresh/validate, read-only debate, consensus, concrete runtime task graph/inboxes, fresh executor team, review, integration, Honest Mode.\n- `$DFix` is only for tiny design/content edits and bypasses the main pipeline. `$Answer`, `$Help`, and `$Wiki` stay lightweight.\n- For code work, surface route/guard/write scopes first, split independent worker scopes when available, and keep parent-owned integration and verification.\n- Design work reads `design.md`; if missing, use `design-system-builder`. Image/logo/raster assets use `imagegen`.\n- Research, AutoResearch, performance, token, accuracy, SEO/GEO, or workflow-improvement claims need experiment/eval evidence. Do not claim live model accuracy without a scored dataset.\n\n## Evidence And Context\n\n- Context7 is required for external libraries, APIs, MCPs, package managers, SDKs, and generated docs: resolve-library-id then query-docs.\n- When tech stack, framework, package, runtime, or deployment-platform versions change, use Context7 or official vendor web docs, record current syntax/security/limit guidance as high-priority TriWiki claims, then refresh and validate before coding.\n- TriWiki is the context-tracking SSOT for long-running missions, Team handoffs, and context-pressure recovery. Read `.sneakoscope/wiki/context-pack.json` before each stage, use `attention.use_first` for compact high-trust recall, hydrate `attention.hydrate_first` from source before risky or lower-trust decisions, refresh after findings or artifact changes, and validate before handoffs/final claims.\n- Source priority: current code/tests/config, decision contract, vgraph, beta, GX render/snapshot metadata, LLM Wiki coordinate index, then model knowledge only if allowed.\n- Final response before stop: summarize what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked; then run Honest Mode. Say what passed and what was not verified.\n\n## Safety\n\n- Database access is high risk. Use read-only inspection by default; live data mutation is out of scope unless a sealed contract allows local or branch-only migration files.\n- MAD and MAD-SKS widen only explicit scoped permissions; they still do not authorize unrequested fallback implementation code.\n- Task completion requires relevant tests or justification, zero unsupported critical claims, accepted visual/wiki drift, and final evidence.\n\n## Codex App\n\nUse `.codex/SNEAKOSCOPE.md`, generated `.agents/skills`, `.codex/hooks.json`, and SKS dollar commands (`$sks`, `$team`, `$dfix`, `$qa-loop`, etc.) as the app control surface.\n";
|
|
92
92
|
|
|
93
93
|
export async function initProject(root, opts = {}) {
|
|
94
94
|
const created = [];
|
|
@@ -410,7 +410,7 @@ sandbox_mode = "danger-full-access"
|
|
|
410
410
|
model_reasoning_effort = "high"
|
|
411
411
|
|
|
412
412
|
[auto_review]
|
|
413
|
-
policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion,
|
|
413
|
+
policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."
|
|
414
414
|
|
|
415
415
|
[profiles.sks-default]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "medium"\n`);
|
|
416
416
|
created.push('.codex/config.toml');
|
|
@@ -623,9 +623,9 @@ async function installCodexAgents(root) {
|
|
|
623
623
|
const agents = {
|
|
624
624
|
'analysis-scout.toml': `name = "analysis_scout"\ndescription = "Read-only Team analysis scout. Maps one independent repo/docs/tests/API/risk/user-friction slice and returns TriWiki-ready source-backed findings before debate starts."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are an SKS Team analysis scout.\nDo not edit files.\nOwn exactly one investigation slice assigned by the parent orchestrator.\nMap relevant source files, docs, tests, APIs, DB or safety risks, UX friction, and likely implementation boundaries.\nReturn concise source-backed claims suitable for team-analysis.md and TriWiki ingestion: claim, source path, evidence hash or quoted anchor, risk, confidence, and recommended implementation slice.\nDo not debate the final plan and do not implement code.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`,
|
|
625
625
|
'team-consensus.toml': `name = "team_consensus"\ndescription = "Planning and debate specialist for SKS Team mode. Maps options, constraints, role-persona risks, and proposes the agreed objective before implementation starts."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are the SKS Team consensus specialist.\nDo not edit files.\nMap the affected code paths, viable approaches, constraints, risks, and acceptance criteria.\nRun the debate as role-persona synthesis: final users are low-context, self-interested, stubborn, and inconvenience-averse; executors are capable developers; reviewers are strict.\nArgue for the smallest coherent objective that can be handed to a fresh executor_N development team.\nReturn: recommended objective, rejected alternatives, implementation slices, required reviewers, user-friction risks, and unresolved risks.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`,
|
|
626
|
-
'implementation-worker.toml': `name = "implementation_worker"\ndescription = "Implementation specialist for SKS Team mode. Owns one bounded write set and coordinates with other executor_N workers."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "workspace-write"\ndeveloper_instructions = """\nYou are an SKS Team executor/developer in the fresh development bundle.\nYou are not alone in the codebase. Other executor_N workers may be editing disjoint files.\nOnly edit the files or module slice assigned to you.\nDo not revert or overwrite edits made by others.\nRead local patterns first, make the smallest correct change, avoid adding user friction, run focused verification for your slice, and report changed paths plus evidence.\nRespect all SKS hooks, DB safety rules, no-question Ralph rules, and H-Proof completion gates.\nAlso return concise LIVE_EVENT lines for started, blocked, changed files, verification, and final result so the parent can record them.\n"""\n`,
|
|
626
|
+
'implementation-worker.toml': `name = "implementation_worker"\ndescription = "Implementation specialist for SKS Team mode. Owns one bounded write set and coordinates with other executor_N workers."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "workspace-write"\ndeveloper_instructions = """\nYou are an SKS Team executor/developer in the fresh development bundle.\nYou are not alone in the codebase. Other executor_N workers may be editing disjoint files.\nOnly edit the files or module slice assigned to you.\nDo not revert or overwrite edits made by others.\nRead local patterns first, make the smallest correct change, avoid adding user friction, run focused verification for your slice, and report changed paths plus evidence.\nDo not create fallback implementation code, substitute behavior, mock behavior, or compatibility shims unless the user or sealed decision contract explicitly requested them.\nRespect all SKS hooks, DB safety rules, no-question Ralph rules, and H-Proof completion gates.\nAlso return concise LIVE_EVENT lines for started, blocked, changed files, verification, and final result so the parent can record them.\n"""\n`,
|
|
627
627
|
'db-safety-reviewer.toml': `name = "db_safety_reviewer"\ndescription = "Read-only database safety reviewer for SQL, migrations, Supabase, RLS, destructive-operation risk, and rollback safety."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are a database safety reviewer.\nNever modify files or execute destructive commands.\nReview migrations, SQL, Supabase RLS, transaction boundaries, rollback safety, and MCP database tool usage.\nBlock DROP, TRUNCATE, mass DELETE/UPDATE, db reset, db push, project deletion, branch reset/merge/delete, RLS disabling, and live execute_sql writes.\nReturn concrete risks, exact file references, and required fixes.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`,
|
|
628
|
-
'qa-reviewer.toml': `name = "qa_reviewer"\ndescription = "Strict read-only verification reviewer for correctness, regressions, missing tests, user friction, and final evidence."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are an SKS Team strict reviewer.\nDo not edit files.\nReview correctness, edge cases, regression risk, missing tests, unsupported claims, and whether the final evidence proves the claimed outcome.\nAlso evaluate practical friction from the viewpoint of a stubborn, low-context final user who dislikes inconvenience.\nPrioritize concrete findings with file references and focused verification suggestions.\nReturn no findings if the implementation is sound, and clearly list residual test gaps.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`
|
|
628
|
+
'qa-reviewer.toml': `name = "qa_reviewer"\ndescription = "Strict read-only verification reviewer for correctness, regressions, missing tests, user friction, and final evidence."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are an SKS Team strict reviewer.\nDo not edit files.\nReview correctness, edge cases, regression risk, missing tests, unsupported claims, and whether the final evidence proves the claimed outcome.\nAlso evaluate practical friction from the viewpoint of a stubborn, low-context final user who dislikes inconvenience.\nPrioritize concrete findings with file references and focused verification suggestions.\nFlag any unrequested fallback implementation code, substitute behavior, mock behavior, or compatibility shim as a blocking finding unless the user or sealed decision contract explicitly requested it.\nReturn no findings if the implementation is sound, and clearly list residual test gaps.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`
|
|
629
629
|
};
|
|
630
630
|
const dir = path.join(root, '.codex', 'agents');
|
|
631
631
|
await ensureDir(dir);
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { buildQuestionSchemaForRoute, writeQuestions } from './questions.mjs';
|
|
|
7
7
|
import { sealContract } from './decision-contract.mjs';
|
|
8
8
|
import { scanDbSafety } from './db-safety.mjs';
|
|
9
9
|
import { writeResearchPlan } from './research.mjs';
|
|
10
|
-
import { 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, hasFromChatImgSignal, hasMadSksSignal, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
10
|
+
import { 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, hasFromChatImgSignal, hasMadSksSignal, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
11
11
|
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
|
|
12
12
|
import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
|
|
13
13
|
|
|
@@ -45,6 +45,7 @@ export function promptPipelineContext(prompt, route = routePrompt(prompt)) {
|
|
|
45
45
|
'Default execution routing: general implementation/code-changing prompts promote to Team so the normal path is parallel analysis, TriWiki refresh, debate/consensus, then fresh parallel executors. Answer, DFix, Help, Wiki maintenance, and safety-specific routes are intentional exceptions.',
|
|
46
46
|
'Stance: infer the user intent aggressively from rough wording and local context, but ask short ambiguity-removal questions before work when a missing answer can change the target, scope, safety boundary, or acceptance criteria.',
|
|
47
47
|
subagentExecutionPolicyText(route, prompt),
|
|
48
|
+
noUnrequestedFallbackCodePolicyText(),
|
|
48
49
|
'Design routing: UI/UX reads design.md first; if missing, use design-system-builder from docs/Design-Sys-Prompt.md with plan-tool clarification and a default font recommendation. Existing designs use design-ui-editor plus design-artifact-expert. Image/logo/raster assets use imagegen.',
|
|
49
50
|
triwikiContextTrackingText(),
|
|
50
51
|
triwikiStagePolicyText(),
|
|
@@ -312,7 +313,7 @@ async function prepareTeam(root, route, task, required) {
|
|
|
312
313
|
await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
|
|
313
314
|
await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: roleCounts, agent_sessions: agentSessions, bundle_size: roster.bundle_size, roster, confirmed: true, source: 'default_or_prompt_team_spec' });
|
|
314
315
|
const contextTracking = triwikiContextTracking();
|
|
315
|
-
await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Review, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team lane ${id} --agent analysis_scout_1 --follow\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
|
|
316
|
+
await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Review, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nNo unrequested fallback implementation code is allowed in any stage, executor lane, review lane, MAD route, or MAD-SKS route. If the requested path cannot be implemented inside the sealed contract, block with evidence instead of adding substitute behavior.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team lane ${id} --agent analysis_scout_1 --follow\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
|
|
316
317
|
await initTeamLive(id, dir, cleanTask, { agentSessions, roleCounts, roster });
|
|
317
318
|
const runtime = await writeTeamRuntimeArtifacts(dir, plan, {});
|
|
318
319
|
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 } : {}) });
|
package/src/core/qa-loop.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function buildQaLoopQuestionSchema(prompt) {
|
|
|
50
50
|
{ id: 'ACCEPTANCE_CRITERIA', question: 'List the QA completion criteria.', required: true, type: 'array_or_string' },
|
|
51
51
|
{ id: 'NON_GOALS', question: 'List anything QA-LOOP must not test.', required: true, type: 'array_or_string', allow_empty: true },
|
|
52
52
|
{ id: 'RISK_BOUNDARY', question: 'List hard safety boundaries for data, auth, permissions, money, messages, and third-party systems.', required: true, type: 'array_or_string' },
|
|
53
|
-
{ id: 'MID_RALPH_UNKNOWN_POLICY', question: 'If ambiguity appears during no-question QA, choose the
|
|
53
|
+
{ id: 'MID_RALPH_UNKNOWN_POLICY', question: 'If ambiguity appears during no-question QA, choose the resolution order. This does not authorize unrequested fallback implementation code.', required: true, type: 'array', options: ['preserve_existing_behavior', 'smallest_reversible_change', 'defer_optional_scope', 'block_only_if_no_safe_path'] }
|
|
54
54
|
]
|
|
55
55
|
};
|
|
56
56
|
}
|
package/src/core/questions.mjs
CHANGED
|
@@ -138,7 +138,8 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
|
|
|
138
138
|
addInferred(inferred, notes, 'RISK_BOUNDARY', [
|
|
139
139
|
'no npm publish unless explicitly requested',
|
|
140
140
|
'do not revert unrelated changes',
|
|
141
|
-
'no destructive commands or live data writes'
|
|
141
|
+
'no destructive commands or live data writes',
|
|
142
|
+
'no unrequested fallback implementation code'
|
|
142
143
|
], 'safety');
|
|
143
144
|
}
|
|
144
145
|
return { answers: inferred, notes };
|
|
@@ -159,7 +160,7 @@ export function buildQuestionSchema(prompt) {
|
|
|
159
160
|
{ id: 'DB_SCHEMA_CHANGE_ALLOWED', question: 'DB schema 또는 migration 변경을 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_needed', 'yes_with_migration'] },
|
|
160
161
|
{ id: 'DEPENDENCY_CHANGE_ALLOWED', question: '새 dependency 추가를 허용하나요?', required: true, type: 'enum', options: ['no', 'yes_if_already_approved', 'yes'] },
|
|
161
162
|
{ id: 'TEST_SCOPE', question: 'Ralph가 완료 전 실행 또는 정당화해야 할 테스트 범위를 지정해주세요.', required: true, type: 'array_or_string', examples: ['unit', 'integration', 'e2e', 'lint', 'typecheck'] },
|
|
162
|
-
{ id: 'MID_RALPH_UNKNOWN_POLICY', question: 'Ralph 중 새 모호성이 생기면 사용자에게 묻지 않고 어떤
|
|
163
|
+
{ id: 'MID_RALPH_UNKNOWN_POLICY', question: 'Ralph 중 새 모호성이 생기면 사용자에게 묻지 않고 어떤 해결 순서로 판단할까요? 이 항목은 대체 구현 또는 fallback 코드를 새로 만드는 허가가 아닙니다.', required: true, type: 'array', options: ['preserve_existing_behavior', 'smallest_reversible_change', 'defer_optional_scope', 'block_only_if_no_safe_path'] },
|
|
163
164
|
{ id: 'RISK_BOUNDARY', question: '보안, 결제, 데이터 손상, 권한, 인증 등 절대 넘으면 안 되는 위험 경계를 적어주세요.', required: true, type: 'array_or_string' },
|
|
164
165
|
|
|
165
166
|
{ id: 'DATABASE_TARGET_ENVIRONMENT', question: 'DB 관련 작업의 대상 환경을 지정해주세요. production write는 Sneakoscope Codex가 허용하지 않습니다.', required: true, type: 'enum', options: ['no_database', 'local_dev', 'preview_branch', 'supabase_branch', 'production_read_only'] },
|
package/src/core/routes.mjs
CHANGED
|
@@ -110,6 +110,10 @@ export function chatCaptureIntakeText() {
|
|
|
110
110
|
return `From-Chat-IMG intake: explicit signal only. Treat uploads as chat screenshot plus originals, use Computer Use/browser visual inspection when available, list requirements first in source order, match regions to attachments with confidence, and write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}. Preserve each visible customer request as source-bound text, account for every screenshot image region and separate attachment, map each item to work-order actions, perform the customer-request work, then run a scoped QA-LOOP over that exact work-order range before Team completion. Update checklist checkboxes as work proceeds until all boxes are checked, unresolved_items is empty, scoped_qa_loop_completed=true, and QA unresolved findings are zero. ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} is temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}, so it can be forgotten by retention after enough later sessions. Do not assume ordinary image prompts are chat captures.`;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export function noUnrequestedFallbackCodePolicyText() {
|
|
114
|
+
return 'No unrequested fallback implementation code: every pipeline stage, executor, reviewer, auto-review profile, and MAD/MAD-SKS invocation must implement only the requested contract. Do not invent alternate code paths, substitute features, compatibility shims, mock behavior, or hidden fallbacks unless the user explicitly requested them or the sealed decision contract names them; if the requested path is impossible, block with evidence instead.';
|
|
115
|
+
}
|
|
116
|
+
|
|
113
117
|
export function hasFromChatImgSignal(prompt = '') {
|
|
114
118
|
return /(?:^|\s)\$?from-chat-img(?:\s|:|$)/i.test(String(prompt || ''));
|
|
115
119
|
}
|
|
@@ -521,7 +525,8 @@ export function subagentExecutionPolicyText(route, prompt = '') {
|
|
|
521
525
|
'Subagent policy: REQUIRED for code-changing or execution work in this route.',
|
|
522
526
|
'Before editing, the parent orchestrator must visibly state the SKS route, split independent write scopes, and spawn worker/reviewer subagents whenever the tools are available.',
|
|
523
527
|
'Run workers in parallel only with disjoint ownership. The parent owns integration, verification, and final evidence.',
|
|
524
|
-
'If subagent tools are unavailable or the work cannot be safely split, record that as explicit subagent evidence before editing.'
|
|
528
|
+
'If subagent tools are unavailable or the work cannot be safely split, record that as explicit subagent evidence before editing.',
|
|
529
|
+
noUnrequestedFallbackCodePolicyText()
|
|
525
530
|
].join(' ');
|
|
526
531
|
}
|
|
527
532
|
|
package/src/core/team-dag.mjs
CHANGED
|
@@ -373,6 +373,7 @@ function inboxMarkdown(worker, tasks, compiled) {
|
|
|
373
373
|
'',
|
|
374
374
|
'Use concrete task ids for readiness and handoff. Dependencies below are runtime task ids, not plan-only symbolic ids.',
|
|
375
375
|
'Before task work, read `.sneakoscope/wiki/context-pack.json`: use `attention.use_first` for compact high-trust context and hydrate `attention.hydrate_first` from source before risky or lower-trust decisions.',
|
|
376
|
+
'Do not create fallback implementation code, substitute behavior, mock behavior, or compatibility shims unless the user or sealed decision contract explicitly requested them.',
|
|
376
377
|
''
|
|
377
378
|
];
|
|
378
379
|
for (const task of tasks) {
|