sneakoscope 0.6.78 → 0.6.80
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 +2 -2
- package/package.json +1 -1
- package/src/cli/main.mjs +99 -7
- package/src/cli/maintenance-commands.mjs +3 -0
- package/src/core/codex-app.mjs +2 -2
- package/src/core/decision-contract.mjs +1 -1
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +84 -5
- package/src/core/init.mjs +1 -1
- package/src/core/qa-loop.mjs +8 -7
- package/src/core/questions.mjs +1 -1
- package/src/core/routes.mjs +2 -2
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ sks selftest --mock
|
|
|
47
47
|
| Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$Goal`, `$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
49
|
| From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
|
|
50
|
-
| QA loop | Dogfoods UI/API behavior with safety gates,
|
|
50
|
+
| QA loop | Dogfoods UI/API behavior with safety gates, Codex Computer Use-only UI evidence, safe fixes, and rechecks. |
|
|
51
51
|
| Goal | Bridges SKS pipeline state to Codex native persisted `/goal` create, pause, resume, and clear workflows. |
|
|
52
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`. |
|
|
53
53
|
| Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
|
|
@@ -328,7 +328,7 @@ sks qa-loop run latest --max-cycles 2
|
|
|
328
328
|
sks qa-loop status latest
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
-
Use `$QA-LOOP` in Codex App when
|
|
331
|
+
Use `$QA-LOOP` in Codex App when UI-level E2E needs verification. UI verification must use Codex Computer Use evidence only; Chrome MCP, Browser Use, Playwright, and other browser automation do not satisfy UI-level E2E verification.
|
|
332
332
|
|
|
333
333
|
### Refresh Context Before Risky Work
|
|
334
334
|
|
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.80",
|
|
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
|
@@ -440,10 +440,11 @@ async function wizard(args = []) {
|
|
|
440
440
|
const rl = readline.createInterface({ input, output });
|
|
441
441
|
try {
|
|
442
442
|
console.log('ㅅㅋㅅ Setup UI\n');
|
|
443
|
-
|
|
443
|
+
const currentPackage = await effectivePackageVersion();
|
|
444
|
+
console.log(`Current package: ${currentPackage}`);
|
|
444
445
|
const latest = await npmPackageVersion('sneakoscope');
|
|
445
446
|
if (latest.version) {
|
|
446
|
-
const needsUpdate = compareVersions(latest.version,
|
|
447
|
+
const needsUpdate = compareVersions(latest.version, currentPackage) > 0;
|
|
447
448
|
console.log(`Latest on npm: ${latest.version}${needsUpdate ? ' (update available)' : ''}`);
|
|
448
449
|
if (needsUpdate) {
|
|
449
450
|
const update = await askChoice(rl, 'Update SKS before setup?', ['yes', 'no'], 'yes');
|
|
@@ -496,11 +497,13 @@ async function askChoice(rl, question, choices, fallback) {
|
|
|
496
497
|
|
|
497
498
|
async function updateCheck(args = []) {
|
|
498
499
|
const latest = await npmPackageVersion('sneakoscope');
|
|
500
|
+
const currentPackage = await effectivePackageVersion();
|
|
499
501
|
const result = {
|
|
500
502
|
package: 'sneakoscope',
|
|
501
|
-
current:
|
|
503
|
+
current: currentPackage,
|
|
504
|
+
runtime_current: PACKAGE_VERSION,
|
|
502
505
|
latest: latest.version,
|
|
503
|
-
update_available: latest.version ? compareVersions(latest.version,
|
|
506
|
+
update_available: latest.version ? compareVersions(latest.version, currentPackage) > 0 : false,
|
|
504
507
|
error: latest.error || null
|
|
505
508
|
};
|
|
506
509
|
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
@@ -1124,14 +1127,15 @@ async function madHighCommand(args = []) {
|
|
|
1124
1127
|
async function maybePromptSksUpdateForMad(args = []) {
|
|
1125
1128
|
if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
|
|
1126
1129
|
const latest = await npmPackageVersion('sneakoscope');
|
|
1127
|
-
|
|
1130
|
+
const currentPackage = await effectivePackageVersion();
|
|
1131
|
+
if (!latest.version || compareVersions(latest.version, currentPackage) <= 0) return { status: 'current', latest: latest.version || null, error: latest.error || null };
|
|
1128
1132
|
const command = 'npm i -g sneakoscope@latest';
|
|
1129
1133
|
if (flag(args, '--yes') || flag(args, '-y')) return installSksLatest(command, latest.version);
|
|
1130
1134
|
if (!canAskYesNo()) {
|
|
1131
|
-
console.log(`SKS update available: ${
|
|
1135
|
+
console.log(`SKS update available: ${currentPackage} -> ${latest.version}. Run: ${command}`);
|
|
1132
1136
|
return { status: 'available', latest: latest.version, command };
|
|
1133
1137
|
}
|
|
1134
|
-
const answer = (await askPostinstallQuestion(`SKS ${
|
|
1138
|
+
const answer = (await askPostinstallQuestion(`SKS ${currentPackage} -> ${latest.version} update before MAD launch? [Y/n] `)).trim();
|
|
1135
1139
|
const yes = answer === '' || /^(y|yes|예|네|응)$/i.test(answer);
|
|
1136
1140
|
if (!yes) return { status: 'skipped_by_user', latest: latest.version, command };
|
|
1137
1141
|
return installSksLatest(command, latest.version);
|
|
@@ -1899,6 +1903,15 @@ async function npmPackageVersion(name) {
|
|
|
1899
1903
|
return { version: result.stdout.trim().split(/\s+/).pop() };
|
|
1900
1904
|
}
|
|
1901
1905
|
|
|
1906
|
+
async function effectivePackageVersion() {
|
|
1907
|
+
const pkg = await readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
|
|
1908
|
+
return highestVersion([PACKAGE_VERSION, pkg.version]);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function highestVersion(versions = []) {
|
|
1912
|
+
return versions.filter(Boolean).reduce((best, candidate) => compareVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1902
1915
|
function compareVersions(a, b) {
|
|
1903
1916
|
const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
|
1904
1917
|
const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
|
@@ -2263,6 +2276,81 @@ async function selftest() {
|
|
|
2263
2276
|
const hookState = await readJson(stateFile(hookGoalTmp), {});
|
|
2264
2277
|
if (hookState.phase !== 'GOAL_READY' || hookState.mode !== 'GOAL') throw new Error('selftest failed: $Goal hook did not set ready state');
|
|
2265
2278
|
if (!(await exists(path.join(missionDir(hookGoalTmp, hookState.mission_id), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest failed: $Goal hook did not write goal workflow artifact');
|
|
2279
|
+
const hookUpdateCurrentTmp = tmpdir();
|
|
2280
|
+
await initProject(hookUpdateCurrentTmp, {});
|
|
2281
|
+
const hookUpdateCurrentPayload = JSON.stringify({ cwd: hookUpdateCurrentTmp, prompt: '상태 확인해줘' });
|
|
2282
|
+
const hookUpdateCurrentResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2283
|
+
cwd: hookUpdateCurrentTmp,
|
|
2284
|
+
input: hookUpdateCurrentPayload,
|
|
2285
|
+
env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
|
|
2286
|
+
timeoutMs: 15000,
|
|
2287
|
+
maxOutputBytes: 256 * 1024
|
|
2288
|
+
});
|
|
2289
|
+
if (hookUpdateCurrentResult.code !== 0) throw new Error(`selftest failed: current update hook exited ${hookUpdateCurrentResult.code}: ${hookUpdateCurrentResult.stderr}`);
|
|
2290
|
+
const hookUpdateCurrentJson = JSON.parse(hookUpdateCurrentResult.stdout);
|
|
2291
|
+
const hookUpdateCurrentContext = hookUpdateCurrentJson.hookSpecificOutput?.additionalContext || '';
|
|
2292
|
+
if (String(hookUpdateCurrentContext).includes('Update SKS now') || String(hookUpdateCurrentContext).includes('Skip update for this conversation')) throw new Error('selftest failed: hook prompted for update even though installed SKS is current');
|
|
2293
|
+
const hookUpdateCurrentState = await readJson(path.join(hookUpdateCurrentTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2294
|
+
if (hookUpdateCurrentState.pending_offer) throw new Error('selftest failed: current installed SKS left a pending update offer');
|
|
2295
|
+
if (hookUpdateCurrentState.current !== '9.9.9' || hookUpdateCurrentState.runtime_current !== PACKAGE_VERSION || hookUpdateCurrentState.installed_current !== '9.9.9') throw new Error('selftest failed: hook did not record effective installed SKS version');
|
|
2296
|
+
const hookUpdatePendingTmp = tmpdir();
|
|
2297
|
+
await initProject(hookUpdatePendingTmp, {});
|
|
2298
|
+
await writeJsonAtomic(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {
|
|
2299
|
+
current: PACKAGE_VERSION,
|
|
2300
|
+
latest: '9.9.9',
|
|
2301
|
+
pending_offer: { conversation_id: hookUpdatePendingTmp, latest: '9.9.9', offered_at: nowIso() }
|
|
2302
|
+
});
|
|
2303
|
+
const hookUpdatePendingPayload = JSON.stringify({ cwd: hookUpdatePendingTmp, prompt: 'Update SKS now' });
|
|
2304
|
+
const hookUpdatePendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2305
|
+
cwd: hookUpdatePendingTmp,
|
|
2306
|
+
input: hookUpdatePendingPayload,
|
|
2307
|
+
env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
|
|
2308
|
+
timeoutMs: 15000,
|
|
2309
|
+
maxOutputBytes: 256 * 1024
|
|
2310
|
+
});
|
|
2311
|
+
if (hookUpdatePendingResult.code !== 0) throw new Error(`selftest failed: stale pending update hook exited ${hookUpdatePendingResult.code}: ${hookUpdatePendingResult.stderr}`);
|
|
2312
|
+
const hookUpdatePendingJson = JSON.parse(hookUpdatePendingResult.stdout);
|
|
2313
|
+
const hookUpdatePendingContext = hookUpdatePendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2314
|
+
if (String(hookUpdatePendingContext).includes('user accepted update') || String(hookUpdatePendingContext).includes('Before doing other work')) throw new Error('selftest failed: current installed SKS accepted a stale pending update offer');
|
|
2315
|
+
const hookUpdatePendingState = await readJson(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2316
|
+
if (hookUpdatePendingState.pending_offer) throw new Error('selftest failed: stale pending update offer was not cleared after installed SKS became current');
|
|
2317
|
+
const hookUpdateSkippedTmp = tmpdir();
|
|
2318
|
+
await initProject(hookUpdateSkippedTmp, {});
|
|
2319
|
+
await writeJsonAtomic(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {
|
|
2320
|
+
current: PACKAGE_VERSION,
|
|
2321
|
+
latest: '9.9.9',
|
|
2322
|
+
skipped: { conversation_id: hookUpdateSkippedTmp, latest: '9.9.9', skipped_at: nowIso() }
|
|
2323
|
+
});
|
|
2324
|
+
const hookUpdateSkippedPayload = JSON.stringify({ cwd: hookUpdateSkippedTmp, prompt: '상태 확인해줘' });
|
|
2325
|
+
const hookUpdateSkippedResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2326
|
+
cwd: hookUpdateSkippedTmp,
|
|
2327
|
+
input: hookUpdateSkippedPayload,
|
|
2328
|
+
env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
|
|
2329
|
+
timeoutMs: 15000,
|
|
2330
|
+
maxOutputBytes: 256 * 1024
|
|
2331
|
+
});
|
|
2332
|
+
if (hookUpdateSkippedResult.code !== 0) throw new Error(`selftest failed: stale skipped update hook exited ${hookUpdateSkippedResult.code}: ${hookUpdateSkippedResult.stderr}`);
|
|
2333
|
+
const hookUpdateSkippedJson = JSON.parse(hookUpdateSkippedResult.stdout);
|
|
2334
|
+
const hookUpdateSkippedContext = hookUpdateSkippedJson.hookSpecificOutput?.additionalContext || '';
|
|
2335
|
+
if (String(hookUpdateSkippedContext).includes('was skipped for this conversation')) throw new Error('selftest failed: current installed SKS kept stale skipped update context');
|
|
2336
|
+
const hookUpdateSkippedState = await readJson(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2337
|
+
if (hookUpdateSkippedState.skipped) throw new Error('selftest failed: stale skipped update state was not cleared after installed SKS became current');
|
|
2338
|
+
const hookUpdateOldTmp = tmpdir();
|
|
2339
|
+
await initProject(hookUpdateOldTmp, {});
|
|
2340
|
+
const hookUpdateOldPayload = JSON.stringify({ cwd: hookUpdateOldTmp, prompt: '상태 확인해줘' });
|
|
2341
|
+
const hookUpdateOldResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2342
|
+
cwd: hookUpdateOldTmp,
|
|
2343
|
+
input: hookUpdateOldPayload,
|
|
2344
|
+
env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '0.0.0' },
|
|
2345
|
+
timeoutMs: 15000,
|
|
2346
|
+
maxOutputBytes: 256 * 1024
|
|
2347
|
+
});
|
|
2348
|
+
if (hookUpdateOldResult.code !== 0) throw new Error(`selftest failed: stale update hook exited ${hookUpdateOldResult.code}: ${hookUpdateOldResult.stderr}`);
|
|
2349
|
+
const hookUpdateOldJson = JSON.parse(hookUpdateOldResult.stdout);
|
|
2350
|
+
const hookUpdateOldContext = hookUpdateOldJson.hookSpecificOutput?.additionalContext || '';
|
|
2351
|
+
if (!String(hookUpdateOldContext).includes('Update SKS now') || !String(hookUpdateOldContext).includes('Skip update for this conversation')) throw new Error('selftest failed: hook did not prompt when installed SKS is stale');
|
|
2352
|
+
const hookUpdateOldState = await readJson(path.join(hookUpdateOldTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2353
|
+
if (hookUpdateOldState.pending_offer?.latest !== '9.9.9') throw new Error('selftest failed: stale installed SKS did not persist pending update offer');
|
|
2266
2354
|
const hookKoreanSksTmp = tmpdir();
|
|
2267
2355
|
await initProject(hookKoreanSksTmp, {});
|
|
2268
2356
|
const hookKoreanSksPayload = JSON.stringify({ cwd: hookKoreanSksTmp, prompt: koreanReadmeInstallPrompt });
|
|
@@ -2378,6 +2466,8 @@ async function selftest() {
|
|
|
2378
2466
|
const hookQaJson = JSON.parse(hookQaResult.stdout);
|
|
2379
2467
|
const hookQaContext = hookQaJson.hookSpecificOutput?.additionalContext || '';
|
|
2380
2468
|
if (!hookQaContext.includes('MANDATORY ambiguity-removal gate activated') || !hookQaContext.includes('QA_SCOPE') || !hookQaContext.includes('UI_COMPUTER_USE_ACK')) throw new Error('selftest failed: $QA-LOOP hook did not provide QA-specific questions');
|
|
2469
|
+
if (!hookQaContext.includes('Codex Computer Use') || !hookQaContext.includes('Playwright') || !hookQaContext.includes('Chrome MCP')) throw new Error('selftest failed: $QA-LOOP hook did not state Computer Use-only UI policy');
|
|
2470
|
+
if (hookQaContext.includes('Browser Use 또는 Computer Use') || hookQaContext.includes('Browser/Computer Use evidence')) throw new Error('selftest failed: $QA-LOOP hook still allows Browser Use as UI evidence');
|
|
2381
2471
|
const hookQaState = await readJson(stateFile(hookQaTmp), {});
|
|
2382
2472
|
if (hookQaState.phase !== 'QALOOP_CLARIFICATION_AWAITING_ANSWERS' || hookQaState.implementation_allowed !== false) throw new Error('selftest failed: $QA-LOOP hook did not lock execution behind ambiguity gate');
|
|
2383
2473
|
const hookQaSchema = await readJson(path.join(missionDir(hookQaTmp, hookQaState.mission_id), 'required-answers.schema.json'));
|
|
@@ -2408,6 +2498,8 @@ async function selftest() {
|
|
|
2408
2498
|
if (unresolvedQaGate.passed || !unresolvedQaGate.reasons.includes('unresolved_fixable_findings_remaining')) throw new Error('selftest failed: unresolved fixable QA finding was accepted');
|
|
2409
2499
|
const promptQa = buildQaLoopPrompt({ id: 'selftest', mission: { prompt: 'QA and fix' }, contract: { answers: { QA_CORRECTIVE_POLICY: 'apply_safe_fixes_and_reverify' } }, cycle: 1, previous: '', reportFile: qaReportFile });
|
|
2410
2500
|
if (!promptQa.includes('dogfood as human proxy') || !promptQa.includes('fix safe code/test/docs now') || !promptQa.includes('post_fix_verification_complete')) throw new Error('selftest failed: QA-LOOP dogfood prompt');
|
|
2501
|
+
if (!promptQa.includes('Codex Computer Use evidence only') || !promptQa.includes('Chrome MCP') || !promptQa.includes('Playwright')) throw new Error('selftest failed: QA-LOOP prompt did not enforce Computer Use-only UI evidence');
|
|
2502
|
+
if (promptQa.includes('Browser/Computer Use evidence')) throw new Error('selftest failed: QA-LOOP prompt still allows Browser/Computer UI evidence');
|
|
2411
2503
|
const pkgQa = defaultQaGate({ sealed_hash: 'selftest', answers: { QA_SCOPE: 'all_available', TARGET_BASE_URL: 'none', API_BASE_URL: 'same_as_target', TARGET_ENVIRONMENT: 'local_dev_server', DESTRUCTIVE_DEPLOYED_TESTS_ALLOWED: 'never' } });
|
|
2412
2504
|
if (pkgQa.ui_e2e_required || pkgQa.api_e2e_required || !pkgQa.ui_computer_use_evidence) throw new Error('selftest failed: package QA target gate');
|
|
2413
2505
|
const qaRunResult = await runProcess(process.execPath, [hookBin, 'qa-loop', 'run', 'latest', '--mock'], { cwd: hookQaTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
@@ -65,6 +65,9 @@ Usage:
|
|
|
65
65
|
|
|
66
66
|
Prompt route:
|
|
67
67
|
$QA-LOOP dogfood UI/API, fix safe issues, reverify
|
|
68
|
+
|
|
69
|
+
UI evidence:
|
|
70
|
+
Codex Computer Use only for UI-level E2E; do not use Chrome MCP, Browser Use, Playwright, or other browser automation as UI verification evidence.
|
|
68
71
|
`);
|
|
69
72
|
}
|
|
70
73
|
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -128,10 +128,10 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, computerUseRead
|
|
|
128
128
|
}
|
|
129
129
|
if (appInstalled && (!computerUseReady || !browserUseReady)) {
|
|
130
130
|
lines.push('Open Codex App settings, enable recommended MCP/plugin tools, then restart Codex CLI sessions.');
|
|
131
|
-
lines.push('Required for SKS QA-LOOP
|
|
131
|
+
lines.push('Required for SKS QA-LOOP UI evidence: Codex Computer Use only. Browser Use can support non-UI browser context, but it does not satisfy UI-level E2E verification.');
|
|
132
132
|
lines.push('Verify with: codex mcp list');
|
|
133
133
|
}
|
|
134
|
-
if (!lines.length) lines.push('Codex App, Codex CLI, Computer Use, and Browser Use checks look ready.');
|
|
134
|
+
if (!lines.length) lines.push('Codex App, Codex CLI, Computer Use, and Browser Use checks look ready. UI-level E2E verification still requires Codex Computer Use evidence.');
|
|
135
135
|
return lines;
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -82,7 +82,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
|
|
|
82
82
|
qa_loop_target_environment: answers.TARGET_ENVIRONMENT || null,
|
|
83
83
|
qa_loop_mutation_policy: answers.QA_MUTATION_POLICY || null,
|
|
84
84
|
qa_loop_credentials_saved: false,
|
|
85
|
-
|
|
85
|
+
qa_loop_ui_requires_codex_computer_use_only: Boolean(answers.QA_SCOPE && answers.QA_SCOPE !== 'api_e2e_only'),
|
|
86
86
|
unrequested_fallback_code_allowed: false,
|
|
87
87
|
mad_sks_mode: madSks ? 'explicit_invocation_only' : false,
|
|
88
88
|
production_database_writes_allowed: madSks ? 'mad_sks_scoped' : false,
|
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.80';
|
|
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, sha256 } from './fsx.mjs';
|
|
2
|
+
import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256, packageRoot } 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';
|
|
@@ -334,6 +334,50 @@ async function updateCheckContext(root, payload, prompt) {
|
|
|
334
334
|
const updateState = await readJson(statePath, {});
|
|
335
335
|
const conv = conversationId(payload);
|
|
336
336
|
const pending = updateState.pending_offer;
|
|
337
|
+
let effective = null;
|
|
338
|
+
async function effectiveVersion() {
|
|
339
|
+
if (!effective) {
|
|
340
|
+
const installed = await detectInstalledSksVersion();
|
|
341
|
+
effective = {
|
|
342
|
+
installed,
|
|
343
|
+
current: highestVersion([PACKAGE_VERSION, installed.version])
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return effective;
|
|
347
|
+
}
|
|
348
|
+
if (pending?.latest) {
|
|
349
|
+
const currentCheck = await effectiveVersion();
|
|
350
|
+
if (compareVersions(pending.latest, currentCheck.current) <= 0) {
|
|
351
|
+
await writeJsonAtomic(statePath, {
|
|
352
|
+
...updateState,
|
|
353
|
+
current: currentCheck.current,
|
|
354
|
+
runtime_current: PACKAGE_VERSION,
|
|
355
|
+
installed_current: currentCheck.installed.version || null,
|
|
356
|
+
latest: pending.latest,
|
|
357
|
+
checked_at: nowIso(),
|
|
358
|
+
pending_offer: null,
|
|
359
|
+
check_error: null
|
|
360
|
+
});
|
|
361
|
+
return '';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (updateState.skipped?.latest) {
|
|
365
|
+
const currentCheck = await effectiveVersion();
|
|
366
|
+
if (compareVersions(updateState.skipped.latest, currentCheck.current) <= 0) {
|
|
367
|
+
await writeJsonAtomic(statePath, {
|
|
368
|
+
...updateState,
|
|
369
|
+
current: currentCheck.current,
|
|
370
|
+
runtime_current: PACKAGE_VERSION,
|
|
371
|
+
installed_current: currentCheck.installed.version || null,
|
|
372
|
+
latest: updateState.skipped.latest,
|
|
373
|
+
checked_at: nowIso(),
|
|
374
|
+
pending_offer: null,
|
|
375
|
+
skipped: null,
|
|
376
|
+
check_error: null
|
|
377
|
+
});
|
|
378
|
+
return '';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
337
381
|
if (pending?.conversation_id === conv && pending?.latest && looksLikeUpdateDecline(prompt)) {
|
|
338
382
|
await writeJsonAtomic(statePath, {
|
|
339
383
|
...updateState,
|
|
@@ -354,26 +398,34 @@ async function updateCheckContext(root, payload, prompt) {
|
|
|
354
398
|
return `SKS update check: update ${updateState.skipped.latest} was skipped for this conversation only. Do not ask again in this conversation; check again next conversation.`;
|
|
355
399
|
}
|
|
356
400
|
const check = await checkLatestVersion();
|
|
401
|
+
const { installed, current } = await effectiveVersion();
|
|
402
|
+
const isCurrent = check.latest && compareVersions(check.latest, current) <= 0;
|
|
357
403
|
await writeJsonAtomic(statePath, {
|
|
358
404
|
...updateState,
|
|
359
|
-
current
|
|
405
|
+
current,
|
|
406
|
+
runtime_current: PACKAGE_VERSION,
|
|
407
|
+
installed_current: installed.version || null,
|
|
360
408
|
latest: check.latest || null,
|
|
361
409
|
checked_at: nowIso(),
|
|
410
|
+
pending_offer: isCurrent ? null : updateState.pending_offer || null,
|
|
362
411
|
check_error: check.error || null
|
|
363
412
|
});
|
|
364
|
-
if (!check.latest || check.error ||
|
|
413
|
+
if (!check.latest || check.error || isCurrent) return '';
|
|
365
414
|
await writeJsonAtomic(statePath, {
|
|
366
415
|
...updateState,
|
|
367
|
-
current
|
|
416
|
+
current,
|
|
417
|
+
runtime_current: PACKAGE_VERSION,
|
|
418
|
+
installed_current: installed.version || null,
|
|
368
419
|
latest: check.latest,
|
|
369
420
|
checked_at: nowIso(),
|
|
370
421
|
pending_offer: { conversation_id: conv, latest: check.latest, offered_at: nowIso() },
|
|
371
422
|
skipped: updateState.skipped?.conversation_id === conv ? null : updateState.skipped || null
|
|
372
423
|
});
|
|
373
|
-
return `SKS update check: installed ${
|
|
424
|
+
return `SKS update check: installed ${current}, latest ${check.latest}. Before any other work, ask the user to choose: "Update SKS now" or "Skip update for this conversation". If they choose update, run npm i -g sneakoscope for global installs, or npm i -D sneakoscope && npx sks setup --install-scope project for project installs, then run sks setup and sks doctor --fix. If they skip, do not ask again in this conversation, but check again next conversation.`;
|
|
374
425
|
}
|
|
375
426
|
|
|
376
427
|
async function checkLatestVersion() {
|
|
428
|
+
if (process.env.SKS_NPM_VIEW_SNEAKOSCOPE_VERSION) return { latest: process.env.SKS_NPM_VIEW_SNEAKOSCOPE_VERSION };
|
|
377
429
|
const npm = await which('npm').catch(() => null);
|
|
378
430
|
if (!npm) return { error: 'npm not found' };
|
|
379
431
|
const result = await runProcess(npm, ['view', 'sneakoscope', 'version'], { timeoutMs: 3500, maxOutputBytes: 4096 });
|
|
@@ -381,6 +433,33 @@ async function checkLatestVersion() {
|
|
|
381
433
|
return { latest: result.stdout.trim().split(/\s+/).pop() };
|
|
382
434
|
}
|
|
383
435
|
|
|
436
|
+
async function detectInstalledSksVersion() {
|
|
437
|
+
const override = parseVersionText(process.env.SKS_INSTALLED_SKS_VERSION || '');
|
|
438
|
+
if (override) return { version: override, source: 'env' };
|
|
439
|
+
const candidates = [];
|
|
440
|
+
const pkg = await readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
|
|
441
|
+
if (parseVersionText(pkg.version)) candidates.push({ version: parseVersionText(pkg.version), source: 'package.json' });
|
|
442
|
+
const sks = await which('sks').catch(() => null);
|
|
443
|
+
if (!sks) return candidates[0] || { version: null, source: null };
|
|
444
|
+
const result = await runProcess(sks, ['--version'], {
|
|
445
|
+
timeoutMs: 2000,
|
|
446
|
+
maxOutputBytes: 4096,
|
|
447
|
+
env: { SKS_DISABLE_UPDATE_CHECK: '1' }
|
|
448
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
449
|
+
if (result.code === 0 && parseVersionText(result.stdout)) candidates.push({ version: parseVersionText(result.stdout), source: sks });
|
|
450
|
+
if (candidates.length) return candidates.reduce((best, candidate) => compareVersions(candidate.version, best.version) > 0 ? candidate : best);
|
|
451
|
+
return { version: null, source: sks, error: `${result.stderr || result.stdout || 'sks --version failed'}`.trim() };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseVersionText(text) {
|
|
455
|
+
const match = String(text || '').match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
|
|
456
|
+
return match ? match[0] : null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function highestVersion(versions = []) {
|
|
460
|
+
return versions.filter(Boolean).reduce((best, candidate) => compareVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
|
|
461
|
+
}
|
|
462
|
+
|
|
384
463
|
function compareVersions(a, b) {
|
|
385
464
|
const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
|
386
465
|
const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
package/src/core/init.mjs
CHANGED
|
@@ -500,7 +500,7 @@ export async function installSkills(root) {
|
|
|
500
500
|
'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`,
|
|
501
501
|
'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. 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. Log events, close sessions, pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
|
|
502
502
|
'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 Computer Use/browser 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. 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`,
|
|
503
|
-
'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates,
|
|
503
|
+
'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, and other browser automation do not satisfy UI 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`,
|
|
504
504
|
'goal': `---\nname: goal\ndescription: Dollar-command route for $Goal or $goal Codex native persisted /goal workflows.\n---\n\nUse when the user invokes $Goal/$goal or asks to persist a workflow with Codex native /goal continuation. Prepare with sks goal create or the $Goal route, then use native Codex /goal create, pause, resume, and clear controls where available. Do not recreate the old no-question loop.\n`,
|
|
505
505
|
'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. Do not use for ordinary code edits.\n`,
|
|
506
506
|
'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`,
|
package/src/core/qa-loop.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { exists, nowIso, readJson, readText, writeJsonAtomic, writeTextAtomic, P
|
|
|
3
3
|
|
|
4
4
|
export const QA_LOOP_ROUTE = 'QALoop';
|
|
5
5
|
const QA_REPORT_SUFFIX = 'qa-report.md';
|
|
6
|
+
const UI_COMPUTER_USE_ONLY_ACK = 'use_codex_computer_use_only_no_chrome_mcp_no_browser_use_no_playwright_or_mark_ui_not_verified';
|
|
6
7
|
|
|
7
8
|
function qaReportDateStamp(date = new Date()) {
|
|
8
9
|
return date.toISOString().slice(0, 10);
|
|
@@ -28,7 +29,7 @@ export function buildQaLoopQuestionSchema(prompt) {
|
|
|
28
29
|
return {
|
|
29
30
|
schema_version: 1,
|
|
30
31
|
route: QA_LOOP_ROUTE,
|
|
31
|
-
description: 'QA-LOOP questions must be answered before execution. Login secrets and browser auth state are runtime-only and must not be saved to mission files or TriWiki. UI evidence must
|
|
32
|
+
description: 'QA-LOOP questions must be answered before execution. Login secrets and browser auth state are runtime-only and must not be saved to mission files or TriWiki. UI-level E2E evidence must use Codex Computer Use only; Chrome MCP, Browser Use, Playwright, and other browser automation do not satisfy UI verification.',
|
|
32
33
|
prompt,
|
|
33
34
|
slots: [
|
|
34
35
|
{ id: 'GOAL_PRECISE', question: 'Define the QA objective in one sentence.', required: true, type: 'string' },
|
|
@@ -44,7 +45,7 @@ export function buildQaLoopQuestionSchema(prompt) {
|
|
|
44
45
|
{ id: 'TEMP_TEST_CREDENTIALS_READY', question: 'If login is required, are test-only credentials ready to provide ephemerally during the run?', required: true, type: 'enum', options: ['not_required', 'yes_temp_only', 'no_block_authenticated_tests'] },
|
|
45
46
|
{ id: 'TEST_CREDENTIALS_RUNTIME_SOURCE', question: 'If login is required, how will test-only credentials be provided without saving the values?', required: true, type: 'enum', options: ['not_required', 'ephemeral_chat_only', 'environment_variables', 'secret_manager'] },
|
|
46
47
|
{ id: 'CREDENTIAL_STORAGE_ACK', question: 'Acknowledge credential handling policy.', required: true, type: 'enum', options: ['never_store_credentials_in_artifacts_or_wiki'] },
|
|
47
|
-
{ id: 'UI_COMPUTER_USE_ACK', question: 'Acknowledge UI E2E evidence policy.', required: true, type: 'enum', options: [
|
|
48
|
+
{ id: 'UI_COMPUTER_USE_ACK', question: 'Acknowledge UI E2E evidence policy: Codex Computer Use only; no Chrome MCP, Browser Use, Playwright, or other browser automation.', required: true, type: 'enum', options: [UI_COMPUTER_USE_ONLY_ACK] },
|
|
48
49
|
{ id: 'TEAM_MODE_ALLOWED', question: 'May QA-LOOP use Team/subagents where useful?', required: true, type: 'enum', options: ['yes_parallel_where_safe', 'no_parent_only'] },
|
|
49
50
|
{ id: 'MAX_QA_CYCLES', question: 'How many no-question QA cycles are allowed before pausing?', required: true, type: 'string' },
|
|
50
51
|
{ id: 'ACCEPTANCE_CRITERIA', question: 'List the QA completion criteria.', required: true, type: 'array_or_string' },
|
|
@@ -65,7 +66,7 @@ export function validateQaLoopAnswers(schema, answers = {}) {
|
|
|
65
66
|
if (env !== 'local_dev_server' && mutation === 'seeded_create_change_remove_local_only') errors.push({ slot: 'QA_MUTATION_POLICY', error: 'destructive_removal_tests_are_local_dev_only' });
|
|
66
67
|
if (env === 'deployed_production_domain' && mutation !== 'read_only_smoke_only') errors.push({ slot: 'QA_MUTATION_POLICY', error: 'production_deployed_qa_is_read_only_smoke_only' });
|
|
67
68
|
if (answers.DESTRUCTIVE_DEPLOYED_TESTS_ALLOWED !== 'never') errors.push({ slot: 'DESTRUCTIVE_DEPLOYED_TESTS_ALLOWED', error: 'destructive_deployed_tests_never_allowed' });
|
|
68
|
-
if (isUiScope(answers.QA_SCOPE) && answers.UI_COMPUTER_USE_ACK !==
|
|
69
|
+
if (isUiScope(answers.QA_SCOPE) && answers.UI_COMPUTER_USE_ACK !== UI_COMPUTER_USE_ONLY_ACK) errors.push({ slot: 'UI_COMPUTER_USE_ACK', error: 'ui_e2e_requires_codex_computer_use_only_ack' });
|
|
69
70
|
if (answers.LOGIN_REQUIRED === 'yes' && answers.TEMP_TEST_CREDENTIALS_READY !== 'yes_temp_only') errors.push({ slot: 'TEMP_TEST_CREDENTIALS_READY', error: 'authenticated_tests_require_ephemeral_test_credentials_or_must_be_blocked' });
|
|
70
71
|
if (answers.LOGIN_REQUIRED === 'yes' && answers.TEST_CREDENTIALS_RUNTIME_SOURCE === 'not_required') errors.push({ slot: 'TEST_CREDENTIALS_RUNTIME_SOURCE', error: 'credential_runtime_source_required' });
|
|
71
72
|
if (answers.CREDENTIAL_STORAGE_ACK !== 'never_store_credentials_in_artifacts_or_wiki') errors.push({ slot: 'CREDENTIAL_STORAGE_ACK', error: 'credential_temp_only_ack_required' });
|
|
@@ -146,7 +147,7 @@ export async function writeQaLoopArtifacts(dir, mission, contract) {
|
|
|
146
147
|
mission_id: mission.id,
|
|
147
148
|
qa_report_file: reportFile,
|
|
148
149
|
target: { scope: a.QA_SCOPE, environment: a.TARGET_ENVIRONMENT, base_url: a.TARGET_BASE_URL, api_base_url: a.API_BASE_URL },
|
|
149
|
-
safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: '
|
|
150
|
+
safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: 'codex_computer_use_only_required_for_ui_e2e' },
|
|
150
151
|
checklist
|
|
151
152
|
});
|
|
152
153
|
await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile }));
|
|
@@ -195,7 +196,7 @@ TASK: ${mission.prompt}
|
|
|
195
196
|
CYCLE: ${cycle}
|
|
196
197
|
NO QUESTIONS: use decision-contract.json.
|
|
197
198
|
MODE: dogfood as human proxy; use real flows, fix safe code/test/docs now, then recheck.
|
|
198
|
-
UI:
|
|
199
|
+
UI: Codex Computer Use evidence only, or mark UI unverified. Chrome MCP, Browser Use, Playwright, and other browser automation do not satisfy UI-level E2E verification. Secrets runtime-only.
|
|
199
200
|
SAFETY: deployed read-only smoke; no destructive, billing, message, webhook, admin, bulk-write, global-config, or live-data edits unless contract allows.
|
|
200
201
|
GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0, or post_fix_verification_complete is not true.
|
|
201
202
|
ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
|
|
@@ -224,7 +225,7 @@ function qaChecklist(a) {
|
|
|
224
225
|
['preflight.roles', 'Map roles, permissions, protected areas.']
|
|
225
226
|
];
|
|
226
227
|
if (qaUiRequired(a)) cases.push(
|
|
227
|
-
['ui.
|
|
228
|
+
['ui.computer_use_only', 'Use Codex Computer Use evidence only, or mark UI unverified. Do not use Chrome MCP, Browser Use, Playwright, or other browser automation as UI verification evidence.'],
|
|
228
229
|
['ui.navigation', 'Check primary navigation, deep links, back/forward, refresh, and protected routes.'],
|
|
229
230
|
['ui.auth', 'Check login, logout, session expiry, unauthorized access, and role-specific visibility.'],
|
|
230
231
|
['ui.forms', 'Check required fields, validation, disabled states, success, and failure.'],
|
|
@@ -252,7 +253,7 @@ function qaChecklist(a) {
|
|
|
252
253
|
|
|
253
254
|
function qaReportTemplate(mission, contract, checklist) {
|
|
254
255
|
const a = contract.answers || {};
|
|
255
|
-
return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence:
|
|
256
|
+
return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence: Codex Computer Use only when runnable; Chrome MCP, Browser Use, Playwright, and other browser automation do not satisfy UI-level E2E verification\n\n## Checklist\n\n${checklist.map((item) => `- [ ] ${item.id}: ${item.title}`).join('\n')}\n\n## Findings\n\nTBD\n\n## Corrections And Rechecks\n\nTBD\n\n## Honest Mode\n\nTBD\n`;
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
function positiveCount(value) {
|
package/src/core/questions.mjs
CHANGED
|
@@ -217,7 +217,7 @@ export function questionsMarkdown(schema) {
|
|
|
217
217
|
if (isQaLoop) {
|
|
218
218
|
lines.push('QA-LOOP는 이 질문들에 모두 답변하고 Decision Contract가 봉인된 뒤에만 실행됩니다.');
|
|
219
219
|
lines.push('로그인이 필요하면 테스트 전용 계정 정보만 임시 런타임 입력으로 제공해야 하며, answers.json/리포트/로그/wiki에는 절대 저장하지 않습니다.');
|
|
220
|
-
lines.push('UI E2E는
|
|
220
|
+
lines.push('UI 수준 E2E는 Codex Computer Use 증거가 없으면 검증 완료로 주장할 수 없습니다. Chrome MCP, Browser Use, Playwright, 기타 브라우저 자동화는 UI 검증 증거로 인정하지 않습니다.');
|
|
221
221
|
lines.push('개발 서버가 아닌 배포/스테이징 도메인에서는 삭제성 테스트를 절대 실행하지 않습니다.');
|
|
222
222
|
} else {
|
|
223
223
|
lines.push('이 질문들에 모두 답변하고 Decision Contract가 봉인된 뒤에만 실행됩니다.');
|
package/src/core/routes.mjs
CHANGED
|
@@ -193,7 +193,7 @@ export const ROUTES = [
|
|
|
193
193
|
command: '$QA-LOOP',
|
|
194
194
|
mode: 'QALOOP',
|
|
195
195
|
route: 'QA loop',
|
|
196
|
-
description: 'Dogfood UI/API as human proxy with safety gates,
|
|
196
|
+
description: 'Dogfood UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, Honest Mode.',
|
|
197
197
|
requiredSkills: ['qa-loop', 'pipeline-runner', REFLECTION_SKILL_NAME, 'honest-mode'],
|
|
198
198
|
lifecycle: ['qa_questions_answered', 'contract_sealed', 'qa_checklist', 'qa_loop_cycles', 'safe_remediation', 'focused_reverification', 'qa_report_md', 'qa_gate', 'post_route_reflection', 'honest_mode'],
|
|
199
199
|
context7Policy: 'optional',
|
|
@@ -346,7 +346,7 @@ export const COMMAND_CATALOG = [
|
|
|
346
346
|
{ name: 'auto-review', usage: 'sks auto-review status|enable|start [--high] | sks --Auto-review --high', description: 'Enable Codex automatic approval review and launch SKS cmux with the auto-review profile.' },
|
|
347
347
|
{ name: 'dollar-commands', usage: 'sks dollar-commands [--json]', description: 'List Codex App $ commands such as $DFix and $Team.' },
|
|
348
348
|
{ name: 'dfix', usage: 'sks dfix', description: 'Explain $DFix ultralight design/content fix mode.' },
|
|
349
|
-
{ name: 'qa-loop', usage: 'sks qa-loop prepare|answer|run|status ...', description: 'Dogfood UI/API as human proxy with safety gates, safe fixes, rechecks,
|
|
349
|
+
{ name: 'qa-loop', usage: 'sks qa-loop prepare|answer|run|status ...', description: 'Dogfood UI/API as human proxy with safety gates, safe fixes, rechecks, Codex Computer Use-only UI evidence, report.' },
|
|
350
350
|
{ name: 'context7', usage: 'sks context7 check|setup|tools|resolve|docs|evidence ...', description: 'Check, configure, and call the local Context7 MCP requirement.' },
|
|
351
351
|
{ name: 'pipeline', usage: 'sks pipeline status|resume|answer ...', description: 'Inspect the active skill-first route, pass mandatory ambiguity gates, and inspect completion gates.' },
|
|
352
352
|
{ name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
|