sneakoscope 0.9.11 → 0.9.13
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 +28 -2
- package/crates/sks-core/Cargo.lock +7 -0
- package/crates/sks-core/Cargo.toml +10 -0
- package/crates/sks-core/src/main.rs +202 -0
- package/package.json +15 -3
- package/src/cli/args.mjs +49 -0
- package/src/cli/command-registry.mjs +128 -0
- package/src/cli/feature-commands.mjs +112 -6
- package/src/cli/install-helpers.mjs +14 -7
- package/src/cli/legacy-main.mjs +4147 -0
- package/src/cli/main.mjs +7 -4138
- package/src/cli/output.mjs +9 -0
- package/src/cli/router.mjs +30 -0
- package/src/commands/all-features.mjs +6 -0
- package/src/commands/codex-app.mjs +30 -0
- package/src/commands/codex-lb.mjs +47 -0
- package/src/commands/db.mjs +6 -0
- package/src/commands/doctor.mjs +46 -0
- package/src/commands/features.mjs +6 -0
- package/src/commands/help.mjs +77 -0
- package/src/commands/hooks.mjs +6 -0
- package/src/commands/perf.mjs +91 -0
- package/src/commands/proof.mjs +103 -0
- package/src/commands/root.mjs +24 -0
- package/src/commands/version.mjs +5 -0
- package/src/commands/wiki.mjs +95 -0
- package/src/core/codex-lb-circuit.mjs +130 -0
- package/src/core/db-safety.mjs +18 -3
- package/src/core/feature-fixtures.mjs +103 -0
- package/src/core/feature-registry.mjs +117 -11
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +17 -6
- package/src/core/language-preference.mjs +106 -0
- package/src/core/pipeline.mjs +24 -0
- package/src/core/proof/claim-ledger.mjs +9 -0
- package/src/core/proof/command-ledger.mjs +17 -0
- package/src/core/proof/evidence-collector.mjs +33 -0
- package/src/core/proof/file-change-ledger.mjs +6 -0
- package/src/core/proof/proof-reader.mjs +30 -0
- package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
- package/src/core/proof/proof-schema.mjs +42 -0
- package/src/core/proof/proof-writer.mjs +81 -0
- package/src/core/proof/route-adapter.mjs +74 -0
- package/src/core/proof/route-proof-gate.mjs +33 -0
- package/src/core/proof/route-proof-policy.mjs +96 -0
- package/src/core/proof/selftest-proof-fixtures.mjs +54 -0
- package/src/core/proof/validation.mjs +20 -0
- package/src/core/routes.mjs +4 -3
- package/src/core/rust-accelerator.mjs +55 -3
- package/src/core/secret-redaction.mjs +69 -0
- package/src/core/version-manager.mjs +11 -7
- package/src/core/version.mjs +1 -0
- package/src/core/wiki-image/bbox.mjs +10 -0
- package/src/core/wiki-image/callout-parser.mjs +16 -0
- package/src/core/wiki-image/computer-use-ledger.mjs +38 -0
- package/src/core/wiki-image/image-hash.mjs +42 -0
- package/src/core/wiki-image/image-relation.mjs +2 -0
- package/src/core/wiki-image/image-voxel-ledger.mjs +147 -0
- package/src/core/wiki-image/image-voxel-schema.mjs +16 -0
- package/src/core/wiki-image/proof-linker.mjs +12 -0
- package/src/core/wiki-image/validation.mjs +53 -0
- package/src/core/wiki-image/visual-anchor.mjs +42 -0
|
@@ -0,0 +1,4147 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import readline from 'node:readline/promises';
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
+
|
|
7
|
+
export const IS_LEGACY_CLI = true;
|
|
8
|
+
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
|
|
9
|
+
import { assertCodexWarningSuppressed as assertCodexWarn, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, initProject, installSkills, missingGeneratedCodexAppFeatureFlags, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
10
|
+
import { buildCodexExecArgs, getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
11
|
+
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
12
|
+
import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
|
|
13
|
+
import { sealContract, validateAnswers } from '../core/decision-contract.mjs';
|
|
14
|
+
import { buildQaLoopQuestionSchema, buildQaLoopPrompt, defaultQaGate, evaluateQaGate, isQaReportFilename, qaStatus, writeMockQaResult, writeQaLoopArtifacts } from '../core/qa-loop.mjs';
|
|
15
|
+
import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-question-guard.mjs';
|
|
16
|
+
import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
|
|
17
|
+
import { emitHook, selftestCodexCommitHooks } from '../core/hooks-runtime.mjs';
|
|
18
|
+
import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
|
|
19
|
+
import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
|
|
20
|
+
import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
|
|
21
|
+
import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
|
|
22
|
+
import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
|
|
23
|
+
import { bumpProjectVersion, disableVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
|
|
24
|
+
import { rustInfo } from '../core/rust-accelerator.mjs';
|
|
25
|
+
import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
|
|
26
|
+
import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
|
|
27
|
+
import { buildResearchPrompt, evaluateResearchGate, isDatedResearchPaperArtifact, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
|
|
28
|
+
import { evaluateRecallPulseFixtures, readMissionStatusLedger, writeRecallPulseArtifacts } from '../core/recallpulse.mjs';
|
|
29
|
+
import {
|
|
30
|
+
PPT_AUDIENCE_STRATEGY_ARTIFACT,
|
|
31
|
+
PPT_CLEANUP_REPORT_ARTIFACT,
|
|
32
|
+
PPT_FACT_LEDGER_ARTIFACT,
|
|
33
|
+
PPT_GATE_ARTIFACT,
|
|
34
|
+
PPT_HTML_ARTIFACT,
|
|
35
|
+
PPT_IMAGE_ASSET_LEDGER_ARTIFACT,
|
|
36
|
+
PPT_ITERATION_REPORT_ARTIFACT,
|
|
37
|
+
PPT_PARALLEL_REPORT_ARTIFACT,
|
|
38
|
+
PPT_PDF_ARTIFACT,
|
|
39
|
+
PPT_REVIEW_LEDGER_ARTIFACT,
|
|
40
|
+
PPT_REVIEW_POLICY_ARTIFACT,
|
|
41
|
+
PPT_RENDER_REPORT_ARTIFACT,
|
|
42
|
+
PPT_SOURCE_HTML_DIR,
|
|
43
|
+
PPT_TEMP_DIR,
|
|
44
|
+
writePptBuildArtifacts,
|
|
45
|
+
writePptRouteArtifacts
|
|
46
|
+
} from '../core/ppt.mjs';
|
|
47
|
+
import {
|
|
48
|
+
IMAGE_UX_REVIEW_GATE_ARTIFACT,
|
|
49
|
+
IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT,
|
|
50
|
+
IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT,
|
|
51
|
+
IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT,
|
|
52
|
+
IMAGE_UX_REVIEW_POLICY_ARTIFACT,
|
|
53
|
+
IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT,
|
|
54
|
+
writeImageUxReviewRouteArtifacts
|
|
55
|
+
} from '../core/image-ux-review.mjs';
|
|
56
|
+
import { contextCapsule } from '../core/triwiki-attention.mjs';
|
|
57
|
+
import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
|
|
58
|
+
import { ALLOWED_REASONING_EFFORTS, AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, COMMAND_CATALOG, DESIGN_SYSTEM_SSOT, 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_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, GETDESIGN_REFERENCE, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
|
|
59
|
+
import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, projectGateStatus, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
|
|
60
|
+
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
|
|
61
|
+
import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamWatch } from '../core/team-live.mjs';
|
|
62
|
+
import { evaluateTeamReviewPolicyGate } from '../core/team-review-policy.mjs';
|
|
63
|
+
import { ARTIFACT_FILES, validateDogfoodReport, validateEffortDecision, validateFromChatImgVisualMap, validateSkillCandidate, validateSkillInjectionDecision, validateTeamDashboardState, validateWorkOrderLedger } from '../core/artifact-schemas.mjs';
|
|
64
|
+
import { selectEffort, writeEffortDecision } from '../core/effort-orchestrator.mjs';
|
|
65
|
+
import { createWorkOrderLedger } from '../core/work-order-ledger.mjs';
|
|
66
|
+
import { buildFromChatImgVisualMap } from '../core/from-chat-img-forensics.mjs';
|
|
67
|
+
import { classifyDogfoodFinding, createDogfoodReport, writeDogfoodReport } from '../core/dogfood-loop.mjs';
|
|
68
|
+
import { createSkillCandidate, decideSkillInjection, skillDreamFixture, writeSkillCandidate, writeSkillForgeReport, writeSkillInjectionDecision } from '../core/skill-forge.mjs';
|
|
69
|
+
import { classifyToolError, harnessGrowthReport } from '../core/evaluation.mjs';
|
|
70
|
+
import { runWorkflowPerfBench, validateWorkflowPerfReport } from '../core/perf-bench.mjs';
|
|
71
|
+
import { buildProofField, proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
|
|
72
|
+
import { permissionGateSummary } from '../core/permission-gates.mjs';
|
|
73
|
+
import { recordMistake, writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
|
|
74
|
+
import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/mistake-recall.mjs';
|
|
75
|
+
import { buildPromptContext } from '../core/prompt-context-builder.mjs';
|
|
76
|
+
import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
|
|
77
|
+
import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
78
|
+
import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, findCodexAppUpgradeRepairTargets, formatCodexAppStatus, parseProcessRows } from '../core/codex-app.mjs';
|
|
79
|
+
import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry } from '../core/feature-registry.mjs';
|
|
80
|
+
import { writeSelftestRouteProof } from '../core/proof/selftest-proof-fixtures.mjs';
|
|
81
|
+
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
82
|
+
import { allFeaturesCommand, featuresCommand, hooksCommand, hooksExplainReport } from './feature-commands.mjs';
|
|
83
|
+
import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
|
|
84
|
+
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
85
|
+
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
86
|
+
import { context7Command } from './context7-command.mjs';
|
|
87
|
+
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, formatCodexLbRepairResultText, formatCodexLbStatusText, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, selftestSksShimRepair, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
|
|
88
|
+
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
89
|
+
import { openClawCommand } from './openclaw-command.mjs';
|
|
90
|
+
import { recallPulseCommand } from './recallpulse-command.mjs';
|
|
91
|
+
|
|
92
|
+
const flag = (args, name) => args.includes(name);
|
|
93
|
+
const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
|
|
94
|
+
const REFLECTION_ARTIFACT = 'reflection.md';
|
|
95
|
+
const REFLECTION_GATE = 'reflection-gate.json';
|
|
96
|
+
const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
|
|
97
|
+
|
|
98
|
+
function installScopeFromArgs(args = [], fallback = 'global') {
|
|
99
|
+
if (flag(args, '--project')) return 'project';
|
|
100
|
+
if (flag(args, '--global')) return 'global';
|
|
101
|
+
const i = args.indexOf('--install-scope');
|
|
102
|
+
return normalizeInstallScope(i >= 0 && args[i + 1] ? args[i + 1] : fallback);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function main(args) {
|
|
106
|
+
if (isMadHighLaunch(args)) return runMadHighCommand(args, { maybePromptSksUpdateForLaunch, maybePromptCodexUpdateForLaunch, ensureMadLaunchDependencies, printDepsInstallAction, maybePromptCodexLbSetupForLaunch, packageVersion: PACKAGE_VERSION });
|
|
107
|
+
if (isAutoReviewFlag(args[0])) return autoReviewCommand('start', args.slice(1));
|
|
108
|
+
const [cmd, sub, ...rest] = args;
|
|
109
|
+
const tail = sub === undefined ? [] : [sub, ...rest];
|
|
110
|
+
if (!cmd) return defaultTmuxCommand();
|
|
111
|
+
if (cmd === '--help' || cmd === '-h') return help();
|
|
112
|
+
if (cmd === '--version' || cmd === '-v' || cmd === 'version') return version();
|
|
113
|
+
if (cmd === 'tmux') return !sub || String(sub).startsWith('--') ? tmuxCommand('check', tail) : tmuxCommand(sub, rest);
|
|
114
|
+
if (cmd === 'auto-review' || cmd === 'autoreview') return autoReviewCommand(sub, rest);
|
|
115
|
+
if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
|
|
116
|
+
if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
|
|
117
|
+
const handlers = {
|
|
118
|
+
postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), auth: () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
|
|
119
|
+
'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), 'image-ux-review': () => imageUxReviewCommand(sub, rest), 'ux-review': () => imageUxReviewCommand(sub, rest), 'visual-review': () => imageUxReviewCommand(sub, rest), 'ui-ux-review': () => imageUxReviewCommand(sub, rest), context7: () => context7Command(sub, rest), recallpulse: () => recallPulseCommand(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), features: () => featuresCommand(sub, rest), 'all-features': () => allFeaturesCommand(sub, rest), hooks: () => hooksCommand(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
|
|
120
|
+
goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
|
|
121
|
+
team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
|
|
122
|
+
};
|
|
123
|
+
if (handlers[cmd]) return handlers[cmd]();
|
|
124
|
+
console.error(`Unknown command: ${cmd}`);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function defaultTmuxCommand(args = []) {
|
|
129
|
+
const update = await maybePromptSksUpdateForLaunch(args, { label: 'default tmux launch' });
|
|
130
|
+
if (update.status === 'updated') {
|
|
131
|
+
console.log(`SKS updated from ${PACKAGE_VERSION} to ${update.latest}. Rerun: sks`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (update.status === 'failed') {
|
|
135
|
+
console.error(`SKS update failed: ${update.error}`);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const codexUpdate = await maybePromptCodexUpdateForLaunch(args, { label: 'default tmux launch' });
|
|
140
|
+
if (codexUpdate.status === 'failed' || codexUpdate.status === 'updated_not_reflected') {
|
|
141
|
+
console.error(`Codex CLI update failed: ${codexUpdate.error || 'updated version was not visible on PATH'}`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const lb = await maybePromptCodexLbSetupForLaunch(args);
|
|
146
|
+
if (lb.status === 'missing_api_key') {
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
return launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb, { conciseBlockers: true }));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
|
|
154
|
+
const root = readOption(args, '--root', process.cwd());
|
|
155
|
+
const explicitSession = readOption(args, '--session', null) || readOption(args, '--workspace', null);
|
|
156
|
+
if (lb?.bypass_codex_lb) {
|
|
157
|
+
const session = explicitSession || sanitizeTmuxSessionName(`sks-openai-fallback-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
|
|
158
|
+
console.log(`codex-lb bypass active for this launch: ${lb.chain_health?.status || lb.status}`);
|
|
159
|
+
console.log(`Using fresh OpenAI fallback tmux session: ${session}`);
|
|
160
|
+
return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
|
|
161
|
+
}
|
|
162
|
+
if (!lb?.ok) return opts;
|
|
163
|
+
const nextOpts = withCodexLbProviderArgs(opts);
|
|
164
|
+
if (explicitSession) return nextOpts;
|
|
165
|
+
const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
|
|
166
|
+
console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
|
|
167
|
+
console.log(`Using fresh tmux session: ${session}`);
|
|
168
|
+
return { ...nextOpts, session, codexLbFreshSession: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function withCodexLbProviderArgs(opts = {}) {
|
|
172
|
+
const codexArgs = [...(opts.codexArgs || [])];
|
|
173
|
+
const hasProviderOverride = codexArgs.some((arg) => /model_provider\s*=/.test(String(arg || '')));
|
|
174
|
+
if (!hasProviderOverride) codexArgs.push('-c', 'model_provider="codex-lb"');
|
|
175
|
+
return { ...opts, codexArgs };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function help(args = []) {
|
|
179
|
+
const topic = args[0];
|
|
180
|
+
if (topic) return usage([topic]);
|
|
181
|
+
console.log(`${sksAsciiLogo()}
|
|
182
|
+
|
|
183
|
+
Usage:
|
|
184
|
+
sks
|
|
185
|
+
sks help [topic]
|
|
186
|
+
sks version
|
|
187
|
+
sks update-check [--json]
|
|
188
|
+
sks wizard
|
|
189
|
+
sks commands [--json]
|
|
190
|
+
sks usage [${USAGE_TOPICS}]
|
|
191
|
+
sks root [--json]
|
|
192
|
+
sks quickstart
|
|
193
|
+
sks bootstrap [--install-scope global|project] [--local-only] [--json]
|
|
194
|
+
sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
|
|
195
|
+
sks codex-app
|
|
196
|
+
sks codex-app pat status [--json]
|
|
197
|
+
sks hooks explain [--json]
|
|
198
|
+
sks codex-lb status|health|repair|release|unselect|setup --host <domain> --api-key <key>
|
|
199
|
+
sks auth status|health|repair|release|unselect|setup --host <domain> --api-key <key>
|
|
200
|
+
sks openclaw install|path|print [--dir path] [--force] [--json]
|
|
201
|
+
sks --mad [--high]
|
|
202
|
+
sks auto-review status|enable|start [--high]
|
|
203
|
+
sks --Auto-review [--high]
|
|
204
|
+
sks tmux open [--workspace name]
|
|
205
|
+
sks tmux status [--once]
|
|
206
|
+
sks dollar-commands [--json]
|
|
207
|
+
sks dfix
|
|
208
|
+
sks qa-loop prepare "target"
|
|
209
|
+
sks qa-loop answer <mission-id|latest> <answers.json>
|
|
210
|
+
sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]
|
|
211
|
+
sks qa-loop status <mission-id|latest>
|
|
212
|
+
sks ppt build <mission-id|latest> [--json]
|
|
213
|
+
sks ppt status <mission-id|latest> [--json]
|
|
214
|
+
sks context7 check|setup|tools|resolve|docs|evidence ...
|
|
215
|
+
sks recallpulse run|status|eval|governance|checklist <mission-id|latest>
|
|
216
|
+
sks pipeline status|resume|plan [--json] [--proof-field]
|
|
217
|
+
sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
|
|
218
|
+
sks guard check [--json]
|
|
219
|
+
sks conflicts check|prompt [--json]
|
|
220
|
+
sks versioning status|bump|disable [--json]
|
|
221
|
+
sks reasoning ["prompt"] [--json]
|
|
222
|
+
sks aliases
|
|
223
|
+
sks setup [--bootstrap] [--install-scope global|project] [--local-only] [--force] [--json]
|
|
224
|
+
sks fix-path [--install-scope global|project] [--json]
|
|
225
|
+
sks doctor [--fix] [--local-only] [--json] [--install-scope global|project]
|
|
226
|
+
sks init [--install-scope global|project] [--local-only]
|
|
227
|
+
sks selftest [--mock]
|
|
228
|
+
sks features list|check|inventory [--json] [--write-docs]
|
|
229
|
+
sks all-features selftest --mock [--json]
|
|
230
|
+
sks goal create "task"
|
|
231
|
+
sks goal pause|resume|clear <mission-id|latest>
|
|
232
|
+
sks goal status <mission-id|latest>
|
|
233
|
+
sks team "task" [executor:5 reviewer:6 user:1] [--json]
|
|
234
|
+
sks team log|tail|watch|lane|status|dashboard|open-tmux|attach-tmux [mission-id|latest]
|
|
235
|
+
sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
|
|
236
|
+
sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."
|
|
237
|
+
sks team open-tmux [mission-id|latest] [--no-attach|--separate-session]
|
|
238
|
+
sks team attach-tmux [mission-id|latest]
|
|
239
|
+
sks team cleanup-tmux [mission-id|latest]
|
|
240
|
+
sks research prepare "topic" [--depth frontier]
|
|
241
|
+
sks research run <mission-id|latest> [--mock] [--max-cycles N] [--cycle-timeout-minutes N]
|
|
242
|
+
sks research status <mission-id|latest>
|
|
243
|
+
sks db policy
|
|
244
|
+
sks db scan [--migrations] [--json]
|
|
245
|
+
sks db mcp-config --project-ref <ref>
|
|
246
|
+
sks db check --sql "DROP TABLE users"
|
|
247
|
+
sks db check --command "supabase db reset"
|
|
248
|
+
sks hproof check [mission-id|latest]
|
|
249
|
+
sks validate-artifacts [mission-id|latest] [--json]
|
|
250
|
+
sks eval run [--json] [--out report.json]
|
|
251
|
+
sks eval compare --baseline old.json --candidate new.json [--json]
|
|
252
|
+
sks perf run|workflow [--json] [--intent "task"] [--changed file1,file2]
|
|
253
|
+
sks proof-field scan [--json] [--intent "task"]
|
|
254
|
+
sks skill-dream status|run|record [--json]
|
|
255
|
+
sks harness fixture [--json]
|
|
256
|
+
sks code-structure scan [--json]
|
|
257
|
+
sks wiki coords --rgba 12,34,56,255
|
|
258
|
+
sks wiki pack [--json] [--role worker|verifier] [--max-anchors N]
|
|
259
|
+
sks wiki refresh [--json] [--role worker|verifier] [--max-anchors N] [--prune] [--dry-run]
|
|
260
|
+
sks wiki prune [--json] [--dry-run]
|
|
261
|
+
sks wiki validate [context-pack.json]
|
|
262
|
+
sks gx init [name]
|
|
263
|
+
sks gx render [name] [--format svg|html|all]
|
|
264
|
+
sks gx validate [name]
|
|
265
|
+
sks gx drift [name]
|
|
266
|
+
sks gx snapshot [name]
|
|
267
|
+
sks profile show
|
|
268
|
+
sks profile set <model>
|
|
269
|
+
sks gc [--dry-run] [--json]
|
|
270
|
+
sks memory [--dry-run] [--json]
|
|
271
|
+
sks stats [--json]
|
|
272
|
+
|
|
273
|
+
Codex App prompt commands:
|
|
274
|
+
${formatDollarCommandsCompact(' ')}
|
|
275
|
+
|
|
276
|
+
Discovery:
|
|
277
|
+
sks commands Full command list with descriptions
|
|
278
|
+
sks usage goal Workflow examples for one topic
|
|
279
|
+
sks dollar-commands Codex App $ commands: ${dollarCommandNames()}
|
|
280
|
+
`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function version() {
|
|
284
|
+
console.log(`sneakoscope ${PACKAGE_VERSION}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function shouldShowWizard() {
|
|
288
|
+
return Boolean(input.isTTY && output.isTTY && process.env.SKS_NO_WIZARD !== '1' && process.env.CI !== 'true');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isAutoReviewFlag(value) {
|
|
292
|
+
return /^--?auto[-_]?review$/i.test(String(value || ''));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isMadHighLaunch(args = []) {
|
|
296
|
+
return /^--(?:mad|MAD|mad-sks)$/i.test(String(args[0] || ''));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function wizard(args = []) {
|
|
300
|
+
if (!shouldShowWizard() && !flag(args, '--force')) return help();
|
|
301
|
+
const rl = readline.createInterface({ input, output });
|
|
302
|
+
try {
|
|
303
|
+
console.log(`${sksAsciiLogo()}\nSetup UI\n`);
|
|
304
|
+
const currentPackage = await effectivePackageVersion();
|
|
305
|
+
console.log(`Current package: ${currentPackage}`);
|
|
306
|
+
const latest = await npmPackageVersion('sneakoscope');
|
|
307
|
+
if (latest.version) {
|
|
308
|
+
const needsUpdate = compareVersions(latest.version, currentPackage) > 0;
|
|
309
|
+
console.log(`Latest on npm: ${latest.version}${needsUpdate ? ' (update available)' : ''}`);
|
|
310
|
+
if (needsUpdate) {
|
|
311
|
+
const update = await askChoice(rl, 'Update SKS before setup?', ['yes', 'no'], 'yes');
|
|
312
|
+
if (update === 'yes') {
|
|
313
|
+
console.log('\nRun this update command, then rerun `sks`:');
|
|
314
|
+
console.log(' npm i -g sneakoscope@latest\n');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log('Skipping update for this setup run.\n');
|
|
318
|
+
}
|
|
319
|
+
} else if (latest.error) {
|
|
320
|
+
console.log(`Latest on npm: unknown (${latest.error})`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const scope = await askChoice(rl, 'Install scope for this project?', ['global', 'project', 'commands', 'quit'], 'global');
|
|
324
|
+
if (scope === 'quit') return;
|
|
325
|
+
if (scope === 'commands') {
|
|
326
|
+
quickstart();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (scope === 'project') {
|
|
330
|
+
console.log('\nProject-only setup needs the package installed in this project:');
|
|
331
|
+
console.log(' npm i -D sneakoscope');
|
|
332
|
+
const proceed = await askChoice(rl, 'Continue with project setup after that dependency exists?', ['yes', 'no'], 'no');
|
|
333
|
+
if (proceed !== 'yes') return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const runSetup = await askChoice(rl, `Run sks bootstrap with ${scope} scope now?`, ['yes', 'no'], 'yes');
|
|
337
|
+
if (runSetup === 'yes') await bootstrap(['--install-scope', scope]);
|
|
338
|
+
const runDoctor = await askChoice(rl, 'Run sks doctor --fix now?', ['yes', 'no'], 'yes');
|
|
339
|
+
if (runDoctor === 'yes') await doctor(['--fix', '--install-scope', scope]);
|
|
340
|
+
const runSelftest = await askChoice(rl, 'Run sks selftest --mock now?', ['yes', 'no'], 'yes');
|
|
341
|
+
if (runSelftest === 'yes') await selftest(['--mock']);
|
|
342
|
+
console.log('\nSetup UI complete. Useful next commands:');
|
|
343
|
+
console.log(' sks commands');
|
|
344
|
+
console.log(' sks dollar-commands');
|
|
345
|
+
console.log(' sks codex-app');
|
|
346
|
+
} finally {
|
|
347
|
+
rl.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function askChoice(rl, question, choices, fallback) {
|
|
352
|
+
const suffix = choices.map((c) => c === fallback ? c.toUpperCase() : c).join('/');
|
|
353
|
+
const raw = (await rl.question(`${question} [${suffix}] `)).trim().toLowerCase();
|
|
354
|
+
const value = raw || fallback;
|
|
355
|
+
const hit = choices.find((c) => c.toLowerCase() === value || c[0].toLowerCase() === value);
|
|
356
|
+
return hit || fallback;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function updateCheck(args = []) {
|
|
360
|
+
const latest = await npmPackageVersion('sneakoscope');
|
|
361
|
+
const currentPackage = await effectivePackageVersion();
|
|
362
|
+
const result = {
|
|
363
|
+
package: 'sneakoscope',
|
|
364
|
+
current: currentPackage,
|
|
365
|
+
runtime_current: PACKAGE_VERSION,
|
|
366
|
+
latest: latest.version,
|
|
367
|
+
update_available: latest.version ? compareVersions(latest.version, currentPackage) > 0 : false,
|
|
368
|
+
error: latest.error || null
|
|
369
|
+
};
|
|
370
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
371
|
+
console.log(`${sksAsciiLogo()}\nUpdate Check`);
|
|
372
|
+
console.log(`Current: ${result.current}`);
|
|
373
|
+
console.log(`Latest: ${result.latest || 'unknown'}`);
|
|
374
|
+
console.log(`Update: ${result.update_available ? 'available' : 'not needed'}`);
|
|
375
|
+
if (result.error) console.log(`Error: ${result.error}`);
|
|
376
|
+
if (result.update_available) console.log('Run: npm i -g sneakoscope@latest');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: direct answers -> $Answer, tiny Direct Fix edits -> $DFix, presentation/PDF artifacts -> $PPT, image-generation UI/UX reviews -> $Image-UX-Review/$UX-Review, Computer Use UI/browser speed work -> $Computer-Use, code -> $Team. Execution routes infer their contract from prompt, TriWiki/current-code defaults, and conservative policy instead of surfacing prequestion sheets. 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.';
|
|
380
|
+
|
|
381
|
+
function commands(args = []) {
|
|
382
|
+
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));
|
|
383
|
+
console.log(`${sksAsciiLogo()}\nCommands\n`);
|
|
384
|
+
console.log('Aliases: sks, sneakoscope\n');
|
|
385
|
+
const width = Math.max(...COMMAND_CATALOG.map((c) => c.usage.length));
|
|
386
|
+
for (const c of COMMAND_CATALOG) console.log(`${c.usage.padEnd(width)} ${c.description}`);
|
|
387
|
+
console.log('\nCodex App $ Commands\n');
|
|
388
|
+
console.log('Use these inside Codex App or another agent prompt. They are prompt routes, not terminal commands.\n');
|
|
389
|
+
console.log(formatDollarCommandsDetailed());
|
|
390
|
+
console.log(`\nCanonical Codex App picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function rootCommand(args = []) {
|
|
394
|
+
const project = await findProjectRoot();
|
|
395
|
+
const global = globalSksRoot();
|
|
396
|
+
const active = await sksRoot();
|
|
397
|
+
const result = {
|
|
398
|
+
cwd: process.cwd(),
|
|
399
|
+
mode: project ? 'project' : 'global',
|
|
400
|
+
active_root: active,
|
|
401
|
+
project_root: project,
|
|
402
|
+
global_root: global,
|
|
403
|
+
using_global_root: !project
|
|
404
|
+
};
|
|
405
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
406
|
+
console.log(`${sksAsciiLogo()}\nRoot\n`);
|
|
407
|
+
console.log(`Mode: ${result.mode}`);
|
|
408
|
+
console.log(`Active root: ${active}`);
|
|
409
|
+
console.log(`Project: ${project || 'none'}`);
|
|
410
|
+
console.log(`Global root: ${global}`);
|
|
411
|
+
if (!project) console.log('\nNo project marker was found here, so SKS will use the per-user global runtime root. Run `sks bootstrap` to initialize the current directory as a project.');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function dollarCommands(args = []) {
|
|
415
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES }, null, 2));
|
|
416
|
+
console.log(`${sksAsciiLogo()}\n$ Commands\n`);
|
|
417
|
+
console.log('Use these inside Codex App or another agent prompt. Shells treat $ as variable syntax, so these are prompt commands, not terminal commands.\n');
|
|
418
|
+
console.log(formatDollarCommandsDetailed());
|
|
419
|
+
console.log(`\nCanonical Codex App picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}`);
|
|
420
|
+
console.log(`\n${DOLLAR_DEFAULT_PIPELINE_TEXT}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function formatDollarCommandsDetailed(indent = '') {
|
|
424
|
+
const width = Math.max(...DOLLAR_COMMANDS.map((c) => c.command.length));
|
|
425
|
+
return DOLLAR_COMMANDS.map((c) => `${indent}${c.command.padEnd(width)} ${c.route}: ${c.description}`).join('\n');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function formatDollarCommandsCompact(indent = '') {
|
|
429
|
+
const width = Math.max(...DOLLAR_COMMANDS.map((c) => c.command.length));
|
|
430
|
+
return DOLLAR_COMMANDS.map((c) => `${indent}${c.command.padEnd(width)} ${c.route}`).join('\n');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function dollarCommandNames() {
|
|
434
|
+
return DOLLAR_COMMANDS.map((c) => c.command).join(', ');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function dfixHelp() {
|
|
438
|
+
console.log(`SKS Direct Fix Mode
|
|
439
|
+
|
|
440
|
+
Prompt command:
|
|
441
|
+
$DFix <tiny direct fix request>
|
|
442
|
+
|
|
443
|
+
Examples:
|
|
444
|
+
$DFix 글자 색 파란색으로 바꿔줘
|
|
445
|
+
$DFix 내용을 영어로 바꿔줘
|
|
446
|
+
$DFix Change the CTA label to "Start"
|
|
447
|
+
$DFix Fix the README typo
|
|
448
|
+
$DFix Update the package version
|
|
449
|
+
|
|
450
|
+
Purpose:
|
|
451
|
+
Fast tiny direct edits only. Direct Fix bypasses the general SKS prompt pipeline and uses an ultralight, no-record task list.
|
|
452
|
+
|
|
453
|
+
Rules:
|
|
454
|
+
List the exact micro-edits, inspect only needed files, apply only those edits.
|
|
455
|
+
Do not run mission state, ambiguity gates, TriWiki/TriFix/reflection/state recording, Context7 routing, subagents, Goal, Research, eval, or broad redesign.
|
|
456
|
+
Run only cheap verification when useful.
|
|
457
|
+
Start the final answer with "DFix 완료 요약:" and include one "DFix 솔직모드:" line for verified, not verified, and remaining issues.
|
|
458
|
+
`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function pptCommand(sub = 'status', args = []) {
|
|
462
|
+
const root = await sksRoot();
|
|
463
|
+
const action = sub || 'status';
|
|
464
|
+
const missionArg = args.find((arg) => !String(arg).startsWith('--')) || 'latest';
|
|
465
|
+
const id = await resolveMissionId(root, missionArg);
|
|
466
|
+
if (!id) throw new Error('Usage: sks ppt build|status <mission-id|latest> [--json]');
|
|
467
|
+
const { dir } = await loadMission(root, id);
|
|
468
|
+
if (action === 'build') {
|
|
469
|
+
const contract = await readJson(path.join(dir, 'decision-contract.json'), null);
|
|
470
|
+
if (!contract) throw new Error(`PPT build requires a sealed decision-contract.json for ${id}`);
|
|
471
|
+
const result = await writePptBuildArtifacts(dir, contract);
|
|
472
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ppt.build.completed', ok: result.ok, files: result.files });
|
|
473
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ ok: result.ok, mission_id: id, files: result.files, gate: result.gate, report: result.report }, null, 2));
|
|
474
|
+
console.log('SKS PPT build\n');
|
|
475
|
+
console.log(`Mission: ${id}`);
|
|
476
|
+
console.log(`HTML: ${path.relative(root, result.files.html)}`);
|
|
477
|
+
console.log(`PDF: ${path.relative(root, result.files.pdf)}`);
|
|
478
|
+
console.log(`Facts: ${path.relative(root, result.files.fact_ledger)}`);
|
|
479
|
+
console.log(`Images: ${path.relative(root, result.files.image_asset_ledger)}`);
|
|
480
|
+
console.log(`Review: ${path.relative(root, result.files.review_ledger)}`);
|
|
481
|
+
console.log(`Loop: ${path.relative(root, result.files.iteration_report)}`);
|
|
482
|
+
console.log(`Report: ${path.relative(root, result.files.render_report)}`);
|
|
483
|
+
console.log(`Cleanup: ${path.relative(root, result.files.cleanup_report)}`);
|
|
484
|
+
console.log(`Parallel:${' '.repeat(1)}${path.relative(root, result.files.parallel_report)}`);
|
|
485
|
+
console.log(`Gate: ${result.ok ? 'passed' : 'blocked'} (${path.relative(root, result.files.gate)})`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (action === 'status') {
|
|
489
|
+
const gate = await readJson(path.join(dir, PPT_GATE_ARTIFACT), null);
|
|
490
|
+
const report = await readJson(path.join(dir, PPT_RENDER_REPORT_ARTIFACT), null);
|
|
491
|
+
const status = {
|
|
492
|
+
ok: Boolean(gate?.passed),
|
|
493
|
+
mission_id: id,
|
|
494
|
+
gate,
|
|
495
|
+
report,
|
|
496
|
+
files: {
|
|
497
|
+
html: path.join(dir, PPT_HTML_ARTIFACT),
|
|
498
|
+
source_html: path.join(dir, PPT_HTML_ARTIFACT),
|
|
499
|
+
pdf: path.join(dir, PPT_PDF_ARTIFACT),
|
|
500
|
+
fact_ledger: path.join(dir, PPT_FACT_LEDGER_ARTIFACT),
|
|
501
|
+
image_asset_ledger: path.join(dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT),
|
|
502
|
+
review_policy: path.join(dir, PPT_REVIEW_POLICY_ARTIFACT),
|
|
503
|
+
review_ledger: path.join(dir, PPT_REVIEW_LEDGER_ARTIFACT),
|
|
504
|
+
iteration_report: path.join(dir, PPT_ITERATION_REPORT_ARTIFACT),
|
|
505
|
+
render_report: path.join(dir, PPT_RENDER_REPORT_ARTIFACT),
|
|
506
|
+
cleanup_report: path.join(dir, PPT_CLEANUP_REPORT_ARTIFACT),
|
|
507
|
+
parallel_report: path.join(dir, PPT_PARALLEL_REPORT_ARTIFACT),
|
|
508
|
+
gate: path.join(dir, PPT_GATE_ARTIFACT)
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
512
|
+
console.log('SKS PPT status\n');
|
|
513
|
+
console.log(`Mission: ${id}`);
|
|
514
|
+
console.log(`Gate: ${status.ok ? 'passed' : 'not passed'}`);
|
|
515
|
+
console.log(`HTML: ${path.relative(root, status.files.html)}`);
|
|
516
|
+
console.log(`PDF: ${path.relative(root, status.files.pdf)}`);
|
|
517
|
+
console.log(`Facts: ${path.relative(root, status.files.fact_ledger)}`);
|
|
518
|
+
console.log(`Images: ${path.relative(root, status.files.image_asset_ledger)}`);
|
|
519
|
+
console.log(`Review: ${path.relative(root, status.files.review_ledger)}`);
|
|
520
|
+
console.log(`Loop: ${path.relative(root, status.files.iteration_report)}`);
|
|
521
|
+
console.log(`Report: ${path.relative(root, status.files.render_report)}`);
|
|
522
|
+
console.log(`Cleanup: ${path.relative(root, status.files.cleanup_report)}`);
|
|
523
|
+
console.log(`Parallel:${' '.repeat(1)}${path.relative(root, status.files.parallel_report)}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
throw new Error(`Unknown ppt command: ${action}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function imageUxReviewCommand(sub = 'status', args = []) {
|
|
530
|
+
const root = await sksRoot();
|
|
531
|
+
const action = sub || 'status';
|
|
532
|
+
if (action === 'help' || action === '--help' || action === '-h') {
|
|
533
|
+
console.log(`SKS Image UX Review
|
|
534
|
+
|
|
535
|
+
Prompt commands:
|
|
536
|
+
$Image-UX-Review <target>
|
|
537
|
+
$UX-Review <target>
|
|
538
|
+
|
|
539
|
+
Inspect artifacts:
|
|
540
|
+
sks image-ux-review status latest --json
|
|
541
|
+
|
|
542
|
+
Core loop:
|
|
543
|
+
source UI screenshot -> $imagegen/gpt-image-2 generated annotated review image -> image-ux-issue-ledger.json -> optional requested fixes -> changed-screen recheck
|
|
544
|
+
`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (action !== 'status') throw new Error(`Unknown image-ux-review command: ${action}`);
|
|
548
|
+
const missionArg = args.find((arg) => !String(arg).startsWith('--')) || 'latest';
|
|
549
|
+
const id = await resolveMissionId(root, missionArg);
|
|
550
|
+
if (!id) throw new Error('Usage: sks image-ux-review status <mission-id|latest> [--json]');
|
|
551
|
+
const { dir } = await loadMission(root, id);
|
|
552
|
+
const gate = await readJson(path.join(dir, IMAGE_UX_REVIEW_GATE_ARTIFACT), null);
|
|
553
|
+
const policy = await readJson(path.join(dir, IMAGE_UX_REVIEW_POLICY_ARTIFACT), null);
|
|
554
|
+
const inventory = await readJson(path.join(dir, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT), null);
|
|
555
|
+
const generatedReviewLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), null);
|
|
556
|
+
const issueLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), null);
|
|
557
|
+
const iterationReport = await readJson(path.join(dir, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT), null);
|
|
558
|
+
const status = {
|
|
559
|
+
ok: Boolean(gate?.passed),
|
|
560
|
+
mission_id: id,
|
|
561
|
+
gate,
|
|
562
|
+
policy,
|
|
563
|
+
inventory,
|
|
564
|
+
generated_review_ledger: generatedReviewLedger,
|
|
565
|
+
issue_ledger: issueLedger,
|
|
566
|
+
iteration_report: iterationReport,
|
|
567
|
+
files: {
|
|
568
|
+
policy: path.join(dir, IMAGE_UX_REVIEW_POLICY_ARTIFACT),
|
|
569
|
+
inventory: path.join(dir, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT),
|
|
570
|
+
generated_review_ledger: path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT),
|
|
571
|
+
issue_ledger: path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT),
|
|
572
|
+
iteration_report: path.join(dir, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT),
|
|
573
|
+
gate: path.join(dir, IMAGE_UX_REVIEW_GATE_ARTIFACT)
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
577
|
+
console.log('SKS Image UX Review status\n');
|
|
578
|
+
console.log(`Mission: ${id}`);
|
|
579
|
+
console.log(`Gate: ${status.ok ? 'passed' : 'not passed'}`);
|
|
580
|
+
console.log(`Policy: ${path.relative(root, status.files.policy)}`);
|
|
581
|
+
console.log(`Screens: ${path.relative(root, status.files.inventory)}`);
|
|
582
|
+
console.log(`Images: ${path.relative(root, status.files.generated_review_ledger)}`);
|
|
583
|
+
console.log(`Issues: ${path.relative(root, status.files.issue_ledger)}`);
|
|
584
|
+
console.log(`Loop: ${path.relative(root, status.files.iteration_report)}`);
|
|
585
|
+
console.log(`Gate: ${path.relative(root, status.files.gate)}`);
|
|
586
|
+
if (gate?.blockers?.length) console.log(`Blockers:${' '.repeat(1)}${gate.blockers.join(', ')}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function pipeline(sub = 'status', args = []) {
|
|
590
|
+
const root = await sksRoot();
|
|
591
|
+
const action = sub || 'status';
|
|
592
|
+
if (action === 'answer') return pipelineAnswer(root, args);
|
|
593
|
+
if (action === 'plan') return pipelinePlan(root, args);
|
|
594
|
+
const state = await readJson(stateFile(root), {});
|
|
595
|
+
const evidence = await context7Evidence(root, state);
|
|
596
|
+
const plan = state.mission_id ? await readJson(path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT), null) : null;
|
|
597
|
+
const gateProjection = await projectGateStatus(root, state);
|
|
598
|
+
const stop = await evaluateStop(root, state, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
599
|
+
const result = {
|
|
600
|
+
root,
|
|
601
|
+
state,
|
|
602
|
+
context7: evidence,
|
|
603
|
+
gate_projection: gateProjection,
|
|
604
|
+
plan: plan ? pipelinePlanSummary(plan, root, state.mission_id) : null,
|
|
605
|
+
stop_gate: state.stop_gate || null,
|
|
606
|
+
next_action: stop?.reason || 'No active blocking route gate detected.'
|
|
607
|
+
};
|
|
608
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
609
|
+
if (action !== 'status' && action !== 'resume') throw new Error(`Unknown pipeline command: ${action}`);
|
|
610
|
+
console.log('SKS Pipeline\n');
|
|
611
|
+
console.log(`Mode: ${state.mode || 'IDLE'}`);
|
|
612
|
+
console.log(`Route: ${state.route_command || state.route || 'none'}`);
|
|
613
|
+
console.log(`Phase: ${state.phase || 'IDLE'}`);
|
|
614
|
+
console.log(`Mission: ${state.mission_id || 'none'}`);
|
|
615
|
+
if (plan) {
|
|
616
|
+
console.log(`Plan: ${path.relative(root, path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT))}`);
|
|
617
|
+
console.log(`Lane: ${plan.runtime_lane?.lane || 'unknown'} (${plan.runtime_lane?.source || 'unknown'})`);
|
|
618
|
+
console.log(`Stages: keep ${plan.stage_summary?.kept ?? '?'} / skip ${plan.stage_summary?.skipped ?? '?'}`);
|
|
619
|
+
}
|
|
620
|
+
console.log(`Reasoning: ${state.reasoning_effort || 'medium'}${state.reasoning_profile ? ` (${state.reasoning_profile})` : ''}${state.reasoning_temporary ? ' temporary' : ''}`);
|
|
621
|
+
console.log(`Stop gate: ${state.stop_gate || 'none'}`);
|
|
622
|
+
console.log(`Gate projection: ${gateProjection.ok ? 'ok' : `blocked (${gateProjection.blockers.join(', ')})`}`);
|
|
623
|
+
console.log(`Context7: ${state.context7_required ? (evidence.ok ? 'ok' : 'required-missing') : 'optional'} (${evidence.count || 0} event(s))`);
|
|
624
|
+
console.log(`Next: ${result.next_action}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function pipelinePlan(root, args = []) {
|
|
628
|
+
const state = await readJson(stateFile(root), {});
|
|
629
|
+
const missionArg = pipelineMissionArg(args);
|
|
630
|
+
const id = await resolveMissionId(root, missionArg);
|
|
631
|
+
let dir = null;
|
|
632
|
+
let mission = {};
|
|
633
|
+
let routeContext = {};
|
|
634
|
+
if (id) {
|
|
635
|
+
const loaded = await loadMission(root, id);
|
|
636
|
+
dir = loaded.dir;
|
|
637
|
+
mission = loaded.mission || {};
|
|
638
|
+
routeContext = await readJson(path.join(dir, 'route-context.json'), {});
|
|
639
|
+
const existing = await readJson(path.join(dir, PIPELINE_PLAN_ARTIFACT), null);
|
|
640
|
+
if (existing && !flag(args, '--refresh') && !flag(args, '--proof-field')) {
|
|
641
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ ok: validatePipelinePlan(existing).ok, plan_path: path.join(dir, PIPELINE_PLAN_ARTIFACT), plan: existing }, null, 2));
|
|
642
|
+
return printPipelinePlan(root, id, existing);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const intent = readOption(args, '--intent', routeContext.task || mission.prompt || state.prompt || '');
|
|
646
|
+
const route = ROUTES.find((candidate) => candidate.id === routeContext.route || candidate.command === routeContext.command || candidate.id === state.route || candidate.command === state.route_command)
|
|
647
|
+
|| routePrompt(routeContext.command || state.route_command || intent || '$SKS');
|
|
648
|
+
const changedRaw = readOption(args, '--changed', '');
|
|
649
|
+
const proofField = flag(args, '--proof-field') ? await buildProofField(root, { intent, changedFiles: changedRaw ? changedRaw.split(',') : undefined }) : null;
|
|
650
|
+
const contract = dir ? await readJson(path.join(dir, 'decision-contract.json'), {}) : {};
|
|
651
|
+
const contractSealed = contract?.status === 'sealed' || Boolean(contract?.sealed_at || contract?.sealed_hash);
|
|
652
|
+
const ambiguity = {
|
|
653
|
+
required: Boolean(routeContext.clarification_gate || state.ambiguity_gate_required || contractSealed),
|
|
654
|
+
passed: Boolean(state.ambiguity_gate_passed || state.clarification_passed || contractSealed),
|
|
655
|
+
status: state.clarification_required ? 'awaiting_answers' : ((state.ambiguity_gate_passed || contractSealed) ? 'contract_sealed' : undefined),
|
|
656
|
+
contract_hash: contract?.sealed_hash || null
|
|
657
|
+
};
|
|
658
|
+
const planInput = { missionId: id || null, route, task: intent, required: Boolean(routeContext.context7_required || state.context7_required), ambiguity, proofField };
|
|
659
|
+
const plan = dir ? await writePipelinePlan(dir, planInput) : buildPipelinePlan(planInput);
|
|
660
|
+
const validation = validatePipelinePlan(plan);
|
|
661
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ ok: validation.ok, validation, plan_path: dir ? path.join(dir, PIPELINE_PLAN_ARTIFACT) : null, plan }, null, 2));
|
|
662
|
+
printPipelinePlan(root, id || 'none', plan);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function pipelineMissionArg(args = []) {
|
|
666
|
+
const valueFlags = new Set(['--intent', '--changed']);
|
|
667
|
+
for (let i = 0; i < args.length; i++) {
|
|
668
|
+
const arg = String(args[i]);
|
|
669
|
+
if (valueFlags.has(arg)) {
|
|
670
|
+
i++;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (!arg.startsWith('--')) return arg;
|
|
674
|
+
}
|
|
675
|
+
return 'latest';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function pipelinePlanSummary(plan, root, id) {
|
|
679
|
+
return {
|
|
680
|
+
path: id ? path.join(missionDir(root, id), PIPELINE_PLAN_ARTIFACT) : null,
|
|
681
|
+
validation: validatePipelinePlan(plan),
|
|
682
|
+
lane: plan.runtime_lane?.lane || null,
|
|
683
|
+
source: plan.runtime_lane?.source || null,
|
|
684
|
+
kept: plan.stage_summary?.kept ?? null,
|
|
685
|
+
skipped: plan.stage_summary?.skipped ?? null,
|
|
686
|
+
next_actions: plan.next_actions || []
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function printPipelinePlan(root, id, plan) {
|
|
691
|
+
const validation = validatePipelinePlan(plan);
|
|
692
|
+
console.log('SKS Pipeline Plan\n');
|
|
693
|
+
console.log(`Mission: ${id}`);
|
|
694
|
+
console.log(`Route: ${plan.route?.command || plan.route?.id || 'unknown'}`);
|
|
695
|
+
console.log(`Lane: ${plan.runtime_lane?.lane || 'unknown'} (${plan.runtime_lane?.source || 'unknown'})`);
|
|
696
|
+
console.log(`Valid: ${validation.ok ? 'yes' : `no (${validation.issues.join(', ')})`}`);
|
|
697
|
+
if (id && id !== 'none') console.log(`Artifact: ${path.relative(root, path.join(missionDir(root, id), PIPELINE_PLAN_ARTIFACT))}`);
|
|
698
|
+
console.log(`Stages: keep ${plan.stage_summary?.kept ?? 0}, skip ${plan.stage_summary?.skipped ?? 0}, n/a ${plan.stage_summary?.not_applicable ?? 0}`);
|
|
699
|
+
console.log(`Verify: ${(plan.verification || []).join('; ')}`);
|
|
700
|
+
console.log(`Next: ${(plan.next_actions || []).join(' -> ')}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function pipelineAnswer(root, args = []) {
|
|
704
|
+
const [missionArg, answerFile] = args;
|
|
705
|
+
const id = await resolveMissionId(root, missionArg);
|
|
706
|
+
if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">');
|
|
707
|
+
const { dir, mission } = await loadMission(root, id);
|
|
708
|
+
const schema = await readJson(path.join(dir, 'required-answers.schema.json'));
|
|
709
|
+
const answers = answerFile === '--stdin'
|
|
710
|
+
? parseAnswersText(schema, await readStdin())
|
|
711
|
+
: answerFile === '--text'
|
|
712
|
+
? parseAnswersText(schema, args.slice(2).join(' '))
|
|
713
|
+
: await readJson(path.resolve(answerFile));
|
|
714
|
+
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
715
|
+
const result = await sealContract(dir, mission);
|
|
716
|
+
if (!result.ok) {
|
|
717
|
+
console.error('Answer validation failed. SKS ambiguity gate remains locked.');
|
|
718
|
+
console.error(JSON.stringify(result.validation, null, 2));
|
|
719
|
+
process.exitCode = 2;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const routeContext = await readJson(path.join(dir, 'route-context.json'), {});
|
|
723
|
+
const route = ROUTES.find((candidate) => candidate.id === routeContext.route || candidate.command === routeContext.command)
|
|
724
|
+
|| routePrompt(routeContext.command || routeContext.route || '$SKS');
|
|
725
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'pipeline.clarification.contract_sealed', route: route?.id || routeContext.route, hash: result.contract.sealed_hash });
|
|
726
|
+
const materialized = await materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext, result.contract);
|
|
727
|
+
const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: materialized.prompt || routeContext.task || mission.prompt || '', required: Boolean(routeContext.context7_required), ambiguity: { required: true, passed: true, status: 'contract_sealed', contract_hash: result.contract.sealed_hash } });
|
|
728
|
+
if (route?.id === 'QALoop') await writeQaLoopArtifacts(dir, mission, result.contract);
|
|
729
|
+
await setCurrent(root, {
|
|
730
|
+
mission_id: id,
|
|
731
|
+
route: route?.id || routeContext.route || 'SKS',
|
|
732
|
+
route_command: route?.command || routeContext.command || '$SKS',
|
|
733
|
+
mode: route?.mode || routeContext.mode || 'SKS',
|
|
734
|
+
phase: materialized.phase || `${route?.mode || routeContext.mode || 'SKS'}_CLARIFICATION_CONTRACT_SEALED`,
|
|
735
|
+
context7_required: Boolean(routeContext.context7_required),
|
|
736
|
+
context7_verified: false,
|
|
737
|
+
subagents_required: route ? routeRequiresSubagents(route, routeContext.task || mission.prompt || '') : false,
|
|
738
|
+
subagents_verified: false,
|
|
739
|
+
reflection_required: route ? reflectionRequiredForRoute(route) : false,
|
|
740
|
+
visible_progress_required: true,
|
|
741
|
+
context_tracking: 'triwiki',
|
|
742
|
+
required_skills: route?.requiredSkills || [],
|
|
743
|
+
stop_gate: route?.stopGate || routeContext.original_stop_gate || 'honest_mode',
|
|
744
|
+
clarification_required: false,
|
|
745
|
+
clarification_passed: true,
|
|
746
|
+
ambiguity_gate_required: true,
|
|
747
|
+
ambiguity_gate_passed: true,
|
|
748
|
+
implementation_allowed: true,
|
|
749
|
+
pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok,
|
|
750
|
+
pipeline_plan_path: PIPELINE_PLAN_ARTIFACT,
|
|
751
|
+
reasoning_effort: route ? routeReasoning(route, routeContext.task || mission.prompt || '').effort : 'medium',
|
|
752
|
+
reasoning_profile: route ? routeReasoning(route, routeContext.task || mission.prompt || '').profile : 'sks-task-medium',
|
|
753
|
+
reasoning_temporary: true,
|
|
754
|
+
prompt: materialized.prompt || routeContext.task || mission.prompt || '',
|
|
755
|
+
...materialized.state
|
|
756
|
+
});
|
|
757
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ ok: true, mission_id: id, route: route?.id || routeContext.route, hash: result.contract.sealed_hash, validation: result.validation, pipeline_plan: path.join(dir, PIPELINE_PLAN_ARTIFACT) }, null, 2));
|
|
758
|
+
console.log(`SKS ambiguity gate passed for ${id}`);
|
|
759
|
+
console.log(`Route: ${route?.command || routeContext.command || '$SKS'}`);
|
|
760
|
+
console.log(`Hash: ${result.contract.sealed_hash}`);
|
|
761
|
+
console.log(`Plan: ${path.relative(root, path.join(dir, PIPELINE_PLAN_ARTIFACT))}`);
|
|
762
|
+
console.log('Next: continue the original route lifecycle using decision-contract.json.');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function parseAnswersText(schema = {}, text = '') {
|
|
766
|
+
const body = String(text || '').trim();
|
|
767
|
+
const slots = Array.isArray(schema.slots) ? schema.slots : [];
|
|
768
|
+
const slotById = new Map(slots.map((slot) => [slot.id, slot]));
|
|
769
|
+
const answers = {};
|
|
770
|
+
let currentId = null;
|
|
771
|
+
let currentLines = [];
|
|
772
|
+
const flush = () => {
|
|
773
|
+
if (!currentId) return;
|
|
774
|
+
answers[currentId] = normalizeTextAnswerValue(slotById.get(currentId), currentLines.join('\n').trim());
|
|
775
|
+
currentId = null;
|
|
776
|
+
currentLines = [];
|
|
777
|
+
};
|
|
778
|
+
for (const line of body.split(/\r?\n/)) {
|
|
779
|
+
const match = line.match(/^\s*([A-Z][A-Z0-9_]{2,})\s*[::]\s*(.*)$/);
|
|
780
|
+
if (match && slotById.has(match[1])) {
|
|
781
|
+
flush();
|
|
782
|
+
currentId = match[1];
|
|
783
|
+
currentLines = [match[2] || ''];
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (currentId) currentLines.push(line);
|
|
787
|
+
}
|
|
788
|
+
flush();
|
|
789
|
+
if (!Object.keys(answers).length && slots.length === 1 && body) {
|
|
790
|
+
answers[slots[0].id] = normalizeTextAnswerValue(slots[0], body.replace(new RegExp(`^\\s*${slots[0].id}\\s*`, 'i'), '').trim());
|
|
791
|
+
}
|
|
792
|
+
return answers;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizeTextAnswerValue(slot = {}, raw = '') {
|
|
796
|
+
const value = String(raw || '').trim();
|
|
797
|
+
if (slot.type === 'array') {
|
|
798
|
+
return value.split(/\r?\n|,/).map((x) => x.replace(/^\s*[-*]\s*/, '').trim()).filter(Boolean);
|
|
799
|
+
}
|
|
800
|
+
if (slot.type === 'array_or_string') {
|
|
801
|
+
const bulletLines = value.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
802
|
+
if (bulletLines.length > 1 && bulletLines.every((line) => /^[-*]\s+/.test(line))) return bulletLines.map((line) => line.replace(/^[-*]\s+/, '').trim());
|
|
803
|
+
}
|
|
804
|
+
return value;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext = {}, contract = {}) {
|
|
808
|
+
const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, contract);
|
|
809
|
+
if (route?.id === 'MadSKS') {
|
|
810
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), {
|
|
811
|
+
schema_version: 1,
|
|
812
|
+
passed: false,
|
|
813
|
+
mad_sks_permission_active: true,
|
|
814
|
+
permissions_deactivated: false,
|
|
815
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
816
|
+
direct_execute_sql_allowed: true,
|
|
817
|
+
normal_db_writes_allowed: true,
|
|
818
|
+
live_server_writes_allowed: true,
|
|
819
|
+
migration_apply_allowed: true,
|
|
820
|
+
catastrophic_safety_guard_active: true,
|
|
821
|
+
permission_profile: permissionGateSummary(),
|
|
822
|
+
contract_hash: contract.sealed_hash || null
|
|
823
|
+
});
|
|
824
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
825
|
+
ts: nowIso(),
|
|
826
|
+
type: 'mad_sks.scoped_permission_opened',
|
|
827
|
+
route: route.id,
|
|
828
|
+
catastrophic_safety_guard_active: true
|
|
829
|
+
});
|
|
830
|
+
return {
|
|
831
|
+
phase: 'MADSKS_SCOPED_PERMISSION_ACTIVE',
|
|
832
|
+
prompt: routeContext.task || mission.prompt || '',
|
|
833
|
+
state: {
|
|
834
|
+
mad_sks_active: true,
|
|
835
|
+
mad_sks_modifier: true,
|
|
836
|
+
mad_sks_gate_file: 'mad-sks-gate.json',
|
|
837
|
+
mad_sks_gate_ready: true,
|
|
838
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
839
|
+
direct_execute_sql_allowed: true,
|
|
840
|
+
normal_db_writes_allowed: true,
|
|
841
|
+
live_server_writes_allowed: true,
|
|
842
|
+
migration_apply_allowed: true,
|
|
843
|
+
catastrophic_safety_guard_active: true
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (route?.id === 'PPT') {
|
|
848
|
+
await writePptRouteArtifacts(dir, contract);
|
|
849
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
850
|
+
ts: nowIso(),
|
|
851
|
+
type: 'ppt.materialized_after_ambiguity_gate',
|
|
852
|
+
route: route.id,
|
|
853
|
+
audience_strategy_artifact: PPT_AUDIENCE_STRATEGY_ARTIFACT,
|
|
854
|
+
gate: PPT_GATE_ARTIFACT
|
|
855
|
+
});
|
|
856
|
+
return {
|
|
857
|
+
phase: 'PPT_AUDIENCE_STRATEGY_READY',
|
|
858
|
+
prompt: routeContext.task || mission.prompt || '',
|
|
859
|
+
state: {
|
|
860
|
+
ppt_audience_strategy_ready: true,
|
|
861
|
+
ppt_gate_ready: true,
|
|
862
|
+
...madSksState
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (route?.id === 'ImageUXReview') {
|
|
867
|
+
await writeImageUxReviewRouteArtifacts(dir, contract);
|
|
868
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
869
|
+
ts: nowIso(),
|
|
870
|
+
type: 'image_ux_review.materialized',
|
|
871
|
+
route: route.id,
|
|
872
|
+
gate: IMAGE_UX_REVIEW_GATE_ARTIFACT,
|
|
873
|
+
generated_review_ledger: IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
phase: 'IMAGE_UX_REVIEW_READY',
|
|
877
|
+
prompt: routeContext.task || mission.prompt || '',
|
|
878
|
+
state: {
|
|
879
|
+
image_ux_review_gate_ready: true,
|
|
880
|
+
image_ux_review_policy_ready: true,
|
|
881
|
+
...madSksState
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (route?.id !== 'Team') return Object.keys(madSksState).length ? { state: madSksState } : {};
|
|
886
|
+
const spec = parseTeamSpecText(routeContext.task || mission.prompt || '');
|
|
887
|
+
const prompt = spec.prompt || routeContext.task || mission.prompt || '';
|
|
888
|
+
const fromChatImgRequired = hasFromChatImgSignal(prompt);
|
|
889
|
+
const plan = buildTeamPlan(id, prompt, {
|
|
890
|
+
agentSessions: spec.agentSessions,
|
|
891
|
+
roleCounts: spec.roleCounts,
|
|
892
|
+
roster: spec.roster
|
|
893
|
+
});
|
|
894
|
+
await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
|
|
895
|
+
await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: spec.roleCounts, agent_sessions: spec.agentSessions, bundle_size: spec.roster.bundle_size, roster: spec.roster, confirmed: true, source: 'default_or_prompt_team_spec' });
|
|
896
|
+
await writeTextAtomic(path.join(dir, 'team-workflow.md'), teamWorkflowMarkdown(plan));
|
|
897
|
+
await initTeamLive(id, dir, prompt, {
|
|
898
|
+
agentSessions: spec.agentSessions,
|
|
899
|
+
roleCounts: spec.roleCounts,
|
|
900
|
+
roster: spec.roster
|
|
901
|
+
});
|
|
902
|
+
const runtime = await writeTeamRuntimeArtifacts(dir, plan, { contractHash: contract.sealed_hash || null });
|
|
903
|
+
await writeJsonAtomic(path.join(dir, 'team-gate.json'), {
|
|
904
|
+
passed: false,
|
|
905
|
+
team_roster_confirmed: true,
|
|
906
|
+
analysis_artifact: false,
|
|
907
|
+
triwiki_refreshed: false,
|
|
908
|
+
triwiki_validated: false,
|
|
909
|
+
consensus_artifact: false,
|
|
910
|
+
...runtime.gate_fields,
|
|
911
|
+
implementation_team_fresh: false,
|
|
912
|
+
review_artifact: false,
|
|
913
|
+
integration_evidence: false,
|
|
914
|
+
session_cleanup: false,
|
|
915
|
+
context7_evidence: false,
|
|
916
|
+
...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}),
|
|
917
|
+
contract_hash: contract.sealed_hash || null
|
|
918
|
+
});
|
|
919
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
920
|
+
ts: nowIso(),
|
|
921
|
+
type: 'team.materialized_after_ambiguity_gate',
|
|
922
|
+
route: route.id,
|
|
923
|
+
bundle_size: spec.roster.bundle_size,
|
|
924
|
+
agent_sessions: spec.agentSessions
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
phase: 'TEAM_PARALLEL_ANALYSIS_SCOUTING',
|
|
928
|
+
prompt,
|
|
929
|
+
state: {
|
|
930
|
+
agent_sessions: spec.agentSessions,
|
|
931
|
+
role_counts: spec.roleCounts,
|
|
932
|
+
team_roster_confirmed: true,
|
|
933
|
+
team_plan_ready: true,
|
|
934
|
+
team_graph_ready: runtime.ok,
|
|
935
|
+
team_live_ready: true,
|
|
936
|
+
from_chat_img_required: fromChatImgRequired,
|
|
937
|
+
...madSksState
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function materializeMadSksAuthorization(dir, id, route, routeContext = {}, contract = {}) {
|
|
943
|
+
if (!routeContext.mad_sks_authorization || route?.id === 'MadSKS') return {};
|
|
944
|
+
const gateFile = route?.stopGate || 'done-gate.json';
|
|
945
|
+
const artifact = {
|
|
946
|
+
schema_version: 1,
|
|
947
|
+
mission_id: id,
|
|
948
|
+
route: route?.command || route?.id || null,
|
|
949
|
+
status: 'active',
|
|
950
|
+
active_only_for_current_route: true,
|
|
951
|
+
deactivates_when_gate_passed: gateFile,
|
|
952
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
953
|
+
direct_execute_sql_allowed: true,
|
|
954
|
+
normal_db_writes_allowed: true,
|
|
955
|
+
live_server_writes_allowed: true,
|
|
956
|
+
migration_apply_allowed: true,
|
|
957
|
+
catastrophic_safety_guard_active: true,
|
|
958
|
+
permission_profile: permissionGateSummary(),
|
|
959
|
+
contract_hash: contract.sealed_hash || null
|
|
960
|
+
};
|
|
961
|
+
await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
|
|
962
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
|
|
963
|
+
ts: nowIso(),
|
|
964
|
+
type: 'mad_sks.modifier_authorization_opened',
|
|
965
|
+
route: route?.id || null,
|
|
966
|
+
gate: gateFile,
|
|
967
|
+
catastrophic_safety_guard_active: true
|
|
968
|
+
});
|
|
969
|
+
return {
|
|
970
|
+
mad_sks_active: true,
|
|
971
|
+
mad_sks_modifier: true,
|
|
972
|
+
mad_sks_gate_file: gateFile,
|
|
973
|
+
supabase_mcp_schema_cleanup_allowed: true,
|
|
974
|
+
direct_execute_sql_allowed: true,
|
|
975
|
+
normal_db_writes_allowed: true,
|
|
976
|
+
live_server_writes_allowed: true,
|
|
977
|
+
migration_apply_allowed: true,
|
|
978
|
+
catastrophic_safety_guard_active: true
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function guard(sub = 'check', args = []) {
|
|
983
|
+
const root = await projectRoot();
|
|
984
|
+
const action = sub || 'check';
|
|
985
|
+
if (action !== 'check' && action !== 'status') throw new Error(`Unknown guard command: ${action}`);
|
|
986
|
+
const status = await harnessGuardStatus(root);
|
|
987
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
988
|
+
console.log('SKS Harness Guard\n');
|
|
989
|
+
console.log(`Status: ${status.ok ? 'ok' : 'blocked'}`);
|
|
990
|
+
console.log(`Locked: ${status.locked ? 'yes' : 'no'}`);
|
|
991
|
+
console.log(`Exception: ${status.source_exception ? 'Sneakoscope engine source repo' : 'none'}`);
|
|
992
|
+
console.log(`Policy: ${status.policy_path}${status.policy_exists ? '' : ' (missing)'}`);
|
|
993
|
+
console.log(`Checked: ${status.fingerprints_checked} fingerprint(s)`);
|
|
994
|
+
if (status.missing.length) console.log(`Missing: ${status.missing.join(', ')}`);
|
|
995
|
+
if (status.changed.length) console.log(`Changed: ${status.changed.join(', ')}`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function conflicts(sub = 'check', args = []) {
|
|
999
|
+
const root = await projectRoot();
|
|
1000
|
+
const action = sub || 'check';
|
|
1001
|
+
if (action !== 'check' && action !== 'prompt') throw new Error(`Unknown conflicts command: ${action}`);
|
|
1002
|
+
const scan = await scanHarnessConflicts(root);
|
|
1003
|
+
const result = { ...scan, cleanup_prompt: scan.hard_block ? llmHarnessCleanupPrompt(scan) : null };
|
|
1004
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1005
|
+
if (action === 'prompt') return console.log(llmHarnessCleanupPrompt(scan));
|
|
1006
|
+
console.log('SKS Harness Conflict Check\n');
|
|
1007
|
+
console.log(`Status: ${scan.hard_block ? 'blocked' : 'ok'}`);
|
|
1008
|
+
console.log(`Conflicts: ${scan.conflicts.length}`);
|
|
1009
|
+
if (scan.conflicts.length) console.log(formatHarnessConflictReport(scan));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function versioning(sub = 'status', args = []) {
|
|
1013
|
+
const root = await projectRoot();
|
|
1014
|
+
const action = sub || 'status';
|
|
1015
|
+
if (action === 'status' || action === 'check') {
|
|
1016
|
+
const status = await versioningStatus(root);
|
|
1017
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1018
|
+
console.log('SKS Project Versioning\n');
|
|
1019
|
+
console.log(`Enabled: ${status.enabled ? 'yes' : 'no'}${status.reason ? ` (${status.reason})` : ''}`);
|
|
1020
|
+
console.log(`Version: ${status.package_version || 'none'}`);
|
|
1021
|
+
console.log(`Bump: ${status.bump || 'patch'}`);
|
|
1022
|
+
console.log(`Hook: ${status.hook_installed ? 'installed' : 'missing'}${status.hook_path ? ` ${status.hook_path}` : ''}`);
|
|
1023
|
+
console.log(`Last seen: ${status.last_version || 'none'}`);
|
|
1024
|
+
if (status.runtime_drift?.checked) {
|
|
1025
|
+
const drift = status.runtime_drift;
|
|
1026
|
+
console.log(`Runtime: ${drift.runtime_version || 'unknown'} (${drift.relation || 'unknown'})`);
|
|
1027
|
+
if (!drift.ok) console.log(`Warning: source package is ${drift.package_version}, but bare sks resolves to ${drift.runtime_version}. Use node ./bin/sks.mjs in this repo or reinstall/update the global package before trusting runtime behavior.`);
|
|
1028
|
+
}
|
|
1029
|
+
if (!status.ok) console.log('Run: sks doctor --fix');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (action === 'hook' || action === 'install-hook' || action === 'enable') {
|
|
1033
|
+
const res = await disableVersionGitHook(root);
|
|
1034
|
+
const blocked = { ...res, ok: false, installed: false, reason: 'pre_commit_hooks_unsupported' };
|
|
1035
|
+
process.exitCode = 2;
|
|
1036
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(blocked, null, 2));
|
|
1037
|
+
console.error('SKS no longer installs Git pre-commit hooks. Use `sks versioning bump` and release checks explicitly.');
|
|
1038
|
+
if (res.hook_removed) console.error(`Removed existing SKS version hook: ${res.hook_path}`);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (action === 'disable' || action === 'off' || action === 'remove-hook' || action === 'unhook') {
|
|
1042
|
+
const res = await disableVersionGitHook(root);
|
|
1043
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
1044
|
+
console.log(res.hook_removed ? `Version hook removed: ${res.hook_path}` : `Version hook disabled: ${res.reason || 'policy updated'}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (action === 'bump') {
|
|
1048
|
+
const res = await bumpProjectVersion(root, { force: true });
|
|
1049
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
1050
|
+
if (!res.ok) {
|
|
1051
|
+
console.error(`Version bump failed: ${res.reason || 'unknown'}`);
|
|
1052
|
+
process.exitCode = 2;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
console.log(res.changed ? `Project version bumped: ${res.previous_version} -> ${res.version}` : `Project version already advanced: ${res.version}`);
|
|
1056
|
+
console.log(`Staged: ${res.staged_files?.join(', ') || 'none'}`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (action === 'pre-commit') {
|
|
1060
|
+
const res = await runVersionPreCommit(root);
|
|
1061
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
1062
|
+
if (!res.ok) {
|
|
1063
|
+
console.error(`SKS versioning failed: ${res.reason || 'unknown'}`);
|
|
1064
|
+
process.exitCode = 2;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (res.skipped) return;
|
|
1068
|
+
console.log(res.changed ? `SKS versioning synced: ${res.version}` : `SKS versioning: ${res.version} verified`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
console.error('Usage: sks versioning status|bump|disable [--json]');
|
|
1072
|
+
process.exitCode = 1;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function reasoningCommand(args = []) {
|
|
1076
|
+
const prompt = promptOf(args);
|
|
1077
|
+
const route = routePrompt(prompt || '$SKS');
|
|
1078
|
+
const info = routeReasoning(route, prompt);
|
|
1079
|
+
const result = { route: route?.command || '$SKS', effort: info.effort, profile: info.profile, reason: info.reason, temporary: true, instruction: reasoningInstruction(info) };
|
|
1080
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1081
|
+
console.log('SKS Reasoning Route\n');
|
|
1082
|
+
console.log(`Route: ${result.route}`);
|
|
1083
|
+
console.log(`Effort: ${result.effort}`);
|
|
1084
|
+
console.log(`Profile: ${result.profile}`);
|
|
1085
|
+
console.log(`Reason: ${result.reason}`);
|
|
1086
|
+
console.log('Lifecycle: temporary; return to default/user-selected profile after the route gate passes');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function readOption(args, name, fallback) {
|
|
1090
|
+
const i = args.indexOf(name);
|
|
1091
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function readNumberOption(args, name, fallback) {
|
|
1095
|
+
const raw = readOption(args, name, null);
|
|
1096
|
+
if (raw === null || raw === undefined) return fallback;
|
|
1097
|
+
const value = Number(raw);
|
|
1098
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function tmuxCommand(sub = 'start', args = []) {
|
|
1102
|
+
const action = sub || 'start';
|
|
1103
|
+
if (action === 'status' || action === 'banner') {
|
|
1104
|
+
if (flag(args, '--json')) {
|
|
1105
|
+
const status = await codexAppIntegrationStatus();
|
|
1106
|
+
return console.log(JSON.stringify(status, null, 2));
|
|
1107
|
+
}
|
|
1108
|
+
await runTmuxStatus(action === 'banner' ? ['--once', ...args] : args);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (action === 'check') {
|
|
1112
|
+
const root = await sksRoot();
|
|
1113
|
+
const plan = await buildTmuxLaunchPlan({ root, session: readOption(args, '--session', null) });
|
|
1114
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(plan, null, 2));
|
|
1115
|
+
console.log(formatTmuxBanner(plan.app));
|
|
1116
|
+
console.log('');
|
|
1117
|
+
console.log(`tmux: ${plan.tmux.ok ? 'ok' : 'missing'} ${plan.tmux.version || ''}`.trim());
|
|
1118
|
+
console.log(`Workspace: ${plan.workspace}`);
|
|
1119
|
+
console.log(`Project: ${plan.root}`);
|
|
1120
|
+
console.log(`Ready: ${plan.ready ? 'yes' : 'no'}`);
|
|
1121
|
+
if (!plan.ready) {
|
|
1122
|
+
console.log('\nBlockers:');
|
|
1123
|
+
for (const blocker of Array.from(new Set(plan.blockers))) console.log(`- ${blocker}`);
|
|
1124
|
+
process.exitCode = 1;
|
|
1125
|
+
}
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (['start', 'attach', 'connect', 'open'].includes(action)) {
|
|
1129
|
+
const codexUpdate = await maybePromptCodexUpdateForLaunch(args, { label: 'tmux launch' });
|
|
1130
|
+
if (codexUpdate.status === 'failed' || codexUpdate.status === 'updated_not_reflected') {
|
|
1131
|
+
console.error(`Codex CLI update failed: ${codexUpdate.error || 'updated version was not visible on PATH'}`);
|
|
1132
|
+
process.exitCode = 1;
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const lb = await maybePromptCodexLbSetupForLaunch(args);
|
|
1136
|
+
if (lb.status === 'missing_api_key') {
|
|
1137
|
+
process.exitCode = 1;
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const result = await launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb));
|
|
1141
|
+
if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
console.error('Usage: sks tmux open|start|check|status|banner [--workspace name]');
|
|
1145
|
+
process.exitCode = 1;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function codexLbCommand(action = 'status', args = []) {
|
|
1149
|
+
const sub = action || 'status';
|
|
1150
|
+
const json = flag(args, '--json');
|
|
1151
|
+
if (sub === 'status' || sub === 'check') {
|
|
1152
|
+
const status = await codexLbStatus();
|
|
1153
|
+
const backupPath = codexLbChatgptBackupPath();
|
|
1154
|
+
const backupPresent = await exists(backupPath);
|
|
1155
|
+
if (json) return console.log(JSON.stringify({ ...status, chatgpt_backup_present: backupPresent, chatgpt_backup_path: backupPath }, null, 2));
|
|
1156
|
+
process.stdout.write(formatCodexLbStatusText(status, { backupPresent, backupPath }));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (sub === 'release') {
|
|
1160
|
+
const result = await releaseCodexLbAuthHold({
|
|
1161
|
+
keepProvider: flag(args, '--keep-provider'),
|
|
1162
|
+
deleteBackup: flag(args, '--delete-backup'),
|
|
1163
|
+
force: flag(args, '--force')
|
|
1164
|
+
});
|
|
1165
|
+
if (result.status === 'no_backup' || result.status === 'auth_in_use' || result.status === 'failed') process.exitCode = 1;
|
|
1166
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1167
|
+
if (result.status === 'released') {
|
|
1168
|
+
console.log('codex-lb auth released: ChatGPT OAuth blob restored.');
|
|
1169
|
+
console.log(`Auth: ${result.auth_path}`);
|
|
1170
|
+
console.log(`Backup: ${result.backup_removed ? 'removed' : result.backup_path}`);
|
|
1171
|
+
console.log(`Provider unselected: ${result.provider_unselected ? 'yes' : 'no'}`);
|
|
1172
|
+
if (result.provider_error) console.log(`Provider unselect warning: ${result.provider_error}`);
|
|
1173
|
+
console.log('\nLaunch Codex App / `codex` and complete the ChatGPT browser login if prompted.');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (result.status === 'already_chatgpt') {
|
|
1177
|
+
console.log('codex-lb auth release: auth.json already carries ChatGPT OAuth tokens — nothing to restore.');
|
|
1178
|
+
console.log(`Auth: ${result.auth_path}`);
|
|
1179
|
+
console.log(`Backup: ${result.backup_path}`);
|
|
1180
|
+
console.log(`Provider unselected: ${result.provider_unselected ? 'yes' : 'no'}`);
|
|
1181
|
+
if (result.provider_error) console.log(`Provider unselect warning: ${result.provider_error}`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (result.status === 'no_backup') {
|
|
1185
|
+
console.error(`codex-lb auth release: no ChatGPT OAuth backup found at ${result.backup_path}.`);
|
|
1186
|
+
if (result.reason === 'backup_not_oauth') console.error('The backup file is present but does not contain a ChatGPT OAuth token blob — refusing to clobber auth.json.');
|
|
1187
|
+
else console.error('Run `sks codex-lb repair` after a fresh ChatGPT login to recreate a backup, or `sks codex-lb unselect` to leave codex-lb off without touching auth.json.');
|
|
1188
|
+
process.exitCode = 1;
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (result.status === 'auth_in_use') {
|
|
1192
|
+
console.error(`codex-lb auth release refused: ${result.auth_path} does not look like the codex-lb apikey shape. Re-run with --force to overwrite, or back up auth.json yourself first.`);
|
|
1193
|
+
process.exitCode = 1;
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
console.error(`codex-lb auth release failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
1197
|
+
process.exitCode = 1;
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (sub === 'unselect') {
|
|
1201
|
+
const result = await unselectCodexLbProvider();
|
|
1202
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
1203
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1204
|
+
if (result.status === 'unselected') {
|
|
1205
|
+
console.log('codex-lb unselected. Codex CLI/App will fall back to the default OpenAI provider.');
|
|
1206
|
+
console.log(`Config: ${result.config_path}`);
|
|
1207
|
+
console.log('Re-engage codex-lb with: sks codex-lb repair');
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (result.status === 'not_selected') {
|
|
1211
|
+
console.log('codex-lb is not selected — nothing to do.');
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
console.error(`codex-lb unselect failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
1215
|
+
process.exitCode = 1;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
|
|
1219
|
+
const status = await codexLbStatus();
|
|
1220
|
+
const result = status.ok
|
|
1221
|
+
? await checkCodexLbResponseChain(status, { force: true })
|
|
1222
|
+
: { ok: false, status: 'not_configured', codex_lb: status };
|
|
1223
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1224
|
+
if (result.ok) {
|
|
1225
|
+
console.log('codex-lb response chain: ok');
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
console.error(`codex-lb response chain: failed (${result.status})`);
|
|
1229
|
+
if (result.error) console.error(result.error);
|
|
1230
|
+
process.exitCode = 1;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (sub === 'repair' || sub === 'resync' || sub === 'login') {
|
|
1234
|
+
const result = await repairCodexLbAuth();
|
|
1235
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1236
|
+
if (!result.ok) {
|
|
1237
|
+
if (result.status === 'not_configured') console.error('codex-lb auth repair failed: codex-lb is not fully configured. Run: sks codex-lb setup --host <domain> --api-key <key>');
|
|
1238
|
+
else console.error(`codex-lb auth repair failed: ${result.status}${result.codex_login?.error ? `: ${result.codex_login.error}` : ''}`);
|
|
1239
|
+
process.exitCode = 1;
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
process.stdout.write(formatCodexLbRepairResultText(result));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (sub === 'setup' || sub === 'reconfigure') {
|
|
1246
|
+
let host = readOption(args, '--host', readOption(args, '--domain', null));
|
|
1247
|
+
let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
|
|
1248
|
+
if (!host || !apiKey) {
|
|
1249
|
+
if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
|
|
1250
|
+
if (!canAskYesNo()) {
|
|
1251
|
+
console.error('Usage: sks codex-lb setup|reconfigure --host <domain> --api-key <key>');
|
|
1252
|
+
process.exitCode = 1;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
console.log('codex-lb setup — configure your Codex load balancer connection.\n');
|
|
1256
|
+
if (!host) host = (await askPostinstallQuestion('Your codex-lb domain (e.g. https://codex.example.com/backend-api/codex): ')).trim();
|
|
1257
|
+
if (!host) { console.error('Setup cancelled: no domain provided.'); process.exitCode = 1; return; }
|
|
1258
|
+
if (!apiKey) apiKey = (await askPostinstallQuestion('Your codex-lb API key (sk-clb-...): ')).trim();
|
|
1259
|
+
if (!apiKey) { console.error('Setup cancelled: no API key provided.'); process.exitCode = 1; return; }
|
|
1260
|
+
}
|
|
1261
|
+
const result = await configureCodexLb({ host, apiKey });
|
|
1262
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1263
|
+
if (!result.ok) {
|
|
1264
|
+
console.error(`codex-lb setup failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
1265
|
+
process.exitCode = 1;
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
console.log(`codex-lb configured: ${result.base_url}`);
|
|
1269
|
+
console.log(`Config: ${result.config_path}`);
|
|
1270
|
+
console.log(`Key env: ${result.env_path}`);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
console.error('Usage: sks codex-lb status|health|repair|release [--keep-provider] [--delete-backup] [--force]|unselect|setup --host <domain> --api-key <key> [--json]');
|
|
1274
|
+
process.exitCode = 1;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function maybePromptSksUpdateForLaunch(args = [], opts = {}) {
|
|
1278
|
+
if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
|
|
1279
|
+
const latest = await npmPackageVersion('sneakoscope');
|
|
1280
|
+
const currentPackage = await effectivePackageVersion();
|
|
1281
|
+
if (!latest.version || compareVersions(latest.version, currentPackage) <= 0) return { status: 'current', latest: latest.version || null, error: latest.error || null };
|
|
1282
|
+
const command = `npm i -g sneakoscope@${latest.version} --registry https://registry.npmjs.org/`;
|
|
1283
|
+
if (shouldAutoApproveInstall(args)) return installSksLatest(command, latest.version);
|
|
1284
|
+
if (!canAskYesNo()) {
|
|
1285
|
+
console.log(`SKS update available: ${currentPackage} -> ${latest.version}. Run: ${command}`);
|
|
1286
|
+
return { status: 'available', latest: latest.version, command };
|
|
1287
|
+
}
|
|
1288
|
+
const label = opts.label || 'launch';
|
|
1289
|
+
const answer = (await askPostinstallQuestion(`SKS ${currentPackage} -> ${latest.version} update before ${label}? [Y/n] `)).trim();
|
|
1290
|
+
const yes = answer === '' || /^(y|yes|예|네|응)$/i.test(answer);
|
|
1291
|
+
if (!yes) return { status: 'skipped_by_user', latest: latest.version, command };
|
|
1292
|
+
return installSksLatest(command, latest.version);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async function installSksLatest(command, latestVersion) {
|
|
1296
|
+
const npm = await which('npm').catch(() => null);
|
|
1297
|
+
if (!npm) return { status: 'failed', latest: latestVersion, command, error: 'npm not found on PATH' };
|
|
1298
|
+
const install = await runProcess(npm, ['i', '-g', `sneakoscope@${latestVersion}`, '--registry', 'https://registry.npmjs.org/'], { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
1299
|
+
if (install.code !== 0) return { status: 'failed', latest: latestVersion, command, error: `${install.stderr || install.stdout || command + ' failed'}`.trim() };
|
|
1300
|
+
return { status: 'updated', latest: latestVersion, command };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async function ensureMadLaunchDependencies(args = []) {
|
|
1304
|
+
const actions = [];
|
|
1305
|
+
if (!flag(args, '--skip-cli-tools')) {
|
|
1306
|
+
const codex = await getCodexInfo().catch(() => ({}));
|
|
1307
|
+
if (!codex.bin) actions.push(await installCodexDependency(args, { prompt: 'Codex CLI missing. Install latest Codex CLI with npm i -g @openai/codex@latest?' }));
|
|
1308
|
+
}
|
|
1309
|
+
if (!flag(args, '--no-auto-install-tmux')) {
|
|
1310
|
+
const tmux = await tmuxReadiness().catch(() => ({ ok: false }));
|
|
1311
|
+
if (!tmux.ok) actions.push(await installTmuxDependency(args));
|
|
1312
|
+
}
|
|
1313
|
+
const status = await depsStatus(await sksRoot());
|
|
1314
|
+
return { ready: Boolean(status.codex_cli.ok && status.tmux.ok), actions, status };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function deps(sub = 'check', args = []) {
|
|
1318
|
+
const action = sub || 'check';
|
|
1319
|
+
if (action === 'check' || action === 'status') {
|
|
1320
|
+
const root = await sksRoot();
|
|
1321
|
+
const status = await depsStatus(root);
|
|
1322
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1323
|
+
printDepsStatus(status);
|
|
1324
|
+
if (!status.ready) process.exitCode = 1;
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (action === 'install') return depsInstall(args);
|
|
1328
|
+
console.error('Usage: sks deps check|install [tmux|codex|context7|all] [--yes] [--json]');
|
|
1329
|
+
process.exitCode = 1;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
async function depsStatus(root = null, opts = {}) {
|
|
1333
|
+
root ||= await sksRoot();
|
|
1334
|
+
const npmBin = await which('npm').catch(() => null);
|
|
1335
|
+
const codex = opts.codex || await getCodexInfo().catch(() => ({}));
|
|
1336
|
+
const app = opts.codexApp || await codexAppIntegrationStatus({ codex });
|
|
1337
|
+
const context7 = opts.context7 || await checkContext7(root);
|
|
1338
|
+
const tmux = opts.tmux || await tmuxReadiness().catch((err) => ({ ok: false, version: null, error: err.message }));
|
|
1339
|
+
const brew = process.platform === 'darwin' ? await which('brew').catch(() => null) : null;
|
|
1340
|
+
const globalBin = await discoverGlobalSksCommand();
|
|
1341
|
+
const npmPrefix = npmBin ? await runProcess(npmBin, ['prefix', '-g'], { timeoutMs: 8000, maxOutputBytes: 4096 }).catch(() => null) : null;
|
|
1342
|
+
const pathText = process.env.PATH || '';
|
|
1343
|
+
const npmPrefixDir = npmPrefix?.code === 0 ? npmPrefix.stdout.trim().split(/\r?\n/).pop() : null;
|
|
1344
|
+
const npmBinDir = npmPrefixDir ? (process.platform === 'win32' ? npmPrefixDir : path.join(npmPrefixDir, 'bin')) : null;
|
|
1345
|
+
const nodeOk = Number(process.versions.node.split('.')[0]) >= 20;
|
|
1346
|
+
const homebrewNeeded = process.platform === 'darwin' && !tmux.ok;
|
|
1347
|
+
return {
|
|
1348
|
+
root,
|
|
1349
|
+
ready: Boolean(nodeOk && npmBin && globalBin && codex.bin && context7.ok && tmux.ok),
|
|
1350
|
+
node: { ok: nodeOk, version: process.version },
|
|
1351
|
+
npm: { ok: Boolean(npmBin), bin: npmBin, global_bin_dir: npmBinDir, global_bin_on_path: npmBinDir ? pathText.split(path.delimiter).includes(npmBinDir) : null },
|
|
1352
|
+
sneakoscope: { ok: Boolean(globalBin), bin: globalBin },
|
|
1353
|
+
codex_cli: { ok: Boolean(codex.bin), bin: codex.bin || null, version: codex.version || null },
|
|
1354
|
+
codex_app: app,
|
|
1355
|
+
context7,
|
|
1356
|
+
browser_use: { ok: Boolean(app.features?.browser_tool_ready || app.mcp.has_browser_use), cache: app.plugins.browser_use_cache, source: app.features?.browser_tool_source || app.mcp.browser_use_source || null },
|
|
1357
|
+
computer_use: { ok: app.mcp.has_computer_use, cache: app.plugins.computer_use_cache },
|
|
1358
|
+
tmux: { ok: Boolean(tmux.ok), bin: tmux.bin || null, version: tmux.version || null, min_version: tmux.min_version || '3.0', current_session: Boolean(tmux.current_session), install_hint: tmux.ok ? null : platformTmuxInstallHint(), error: tmux.error || null },
|
|
1359
|
+
homebrew: process.platform === 'darwin' ? { ok: Boolean(brew), bin: brew, required_for_tmux_install: homebrewNeeded } : { ok: null, bin: null, required_for_tmux_install: false },
|
|
1360
|
+
next_actions: depsNextActions({ npmBin, globalBin, codex, app, context7, tmux, brew, nodeOk })
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function depsNextActions({ npmBin, globalBin, codex, app, context7, tmux, brew, nodeOk }) {
|
|
1365
|
+
const out = [];
|
|
1366
|
+
if (!nodeOk) out.push('Install Node.js 20.11+.');
|
|
1367
|
+
if (!npmBin) out.push('Install npm or use a Node.js distribution that includes npm.');
|
|
1368
|
+
if (!globalBin) out.push('Run: npm i -g sneakoscope');
|
|
1369
|
+
if (!codex.bin) out.push('Run: sks deps install codex');
|
|
1370
|
+
if (!context7.ok) out.push('Run: sks deps install context7');
|
|
1371
|
+
if (!app.ok) out.push('Run: sks codex-app check');
|
|
1372
|
+
if (!tmux.ok) out.push(process.platform === 'darwin' && !brew ? 'Install tmux from https://www.tmux.dev/download, or install Homebrew then run: sks deps install tmux' : 'Run: sks deps install tmux');
|
|
1373
|
+
return out;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function printDepsStatus(status) {
|
|
1377
|
+
console.log('SKS Dependencies\n');
|
|
1378
|
+
console.log(`Node: ${status.node.ok ? 'ok' : 'missing'} ${status.node.version}`);
|
|
1379
|
+
console.log(`npm: ${status.npm.ok ? 'ok' : 'missing'} ${status.npm.bin || ''}`.trimEnd());
|
|
1380
|
+
console.log(`npm bin PATH:${status.npm.global_bin_on_path === null ? ' unknown' : status.npm.global_bin_on_path ? ' ok' : ' missing'} ${status.npm.global_bin_dir || ''}`.trimEnd());
|
|
1381
|
+
console.log(`SKS bin: ${status.sneakoscope.ok ? 'ok' : 'missing'} ${status.sneakoscope.bin || ''}`.trimEnd());
|
|
1382
|
+
console.log(`Codex CLI: ${status.codex_cli.ok ? 'ok' : 'missing'} ${status.codex_cli.version || status.codex_cli.bin || ''}`.trimEnd());
|
|
1383
|
+
console.log(`Codex App: ${status.codex_app.app.installed ? 'ok' : 'missing'}`);
|
|
1384
|
+
console.log(`Image Gen: ${status.codex_app.features?.image_generation ? 'ok' : 'missing'}`);
|
|
1385
|
+
console.log(`Context7: ${status.context7.ok ? 'ok' : 'missing'}`);
|
|
1386
|
+
console.log(`Browser: ${status.browser_use.ok ? `ok${status.browser_use.source ? ` (${status.browser_use.source})` : ''}` : 'missing'}`);
|
|
1387
|
+
console.log(`Computer Use:${status.computer_use.ok ? ' ok' : ' missing'}`);
|
|
1388
|
+
console.log(`tmux: ${tmuxStatusKind(status.tmux)} ${status.tmux.version || status.tmux.error || ''}`.trimEnd());
|
|
1389
|
+
if (process.platform === 'darwin') console.log(`Homebrew: ${status.homebrew.ok ? 'ok' : 'missing'} ${status.homebrew.bin || ''}`.trimEnd());
|
|
1390
|
+
console.log(`Ready: ${status.ready ? 'true' : 'false'}`);
|
|
1391
|
+
if (status.next_actions.length) {
|
|
1392
|
+
console.log('\nNext:');
|
|
1393
|
+
for (const action of status.next_actions) console.log(` ${action}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
async function depsInstall(args = []) {
|
|
1398
|
+
const root = await sksRoot();
|
|
1399
|
+
const target = positionalArgs(args)[0] || 'all';
|
|
1400
|
+
const wants = target === 'all' ? ['codex', 'context7', 'tmux'] : [target];
|
|
1401
|
+
const actions = [];
|
|
1402
|
+
if (wants.includes('codex')) actions.push(await installCodexDependency(args));
|
|
1403
|
+
if (wants.includes('context7')) actions.push(await installContext7Dependency(root));
|
|
1404
|
+
if (wants.includes('tmux')) actions.push(await installTmuxDependency(args));
|
|
1405
|
+
const status = await depsStatus(root);
|
|
1406
|
+
const result = { target, actions, status };
|
|
1407
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1408
|
+
for (const action of actions) printDepsInstallAction(action);
|
|
1409
|
+
console.log('');
|
|
1410
|
+
printDepsStatus(status);
|
|
1411
|
+
if (!status.ready) process.exitCode = 1;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async function installCodexDependency(args = [], opts = {}) {
|
|
1415
|
+
const before = await getCodexInfo().catch(() => ({}));
|
|
1416
|
+
if (before.bin) return { target: 'codex', status: 'present', bin: before.bin, version: before.version || null };
|
|
1417
|
+
const command = 'npm i -g @openai/codex@latest';
|
|
1418
|
+
if (!await confirmInstall(opts.prompt || `Install Codex CLI with ${command}?`, args)) return { target: 'codex', status: 'needs_approval', command };
|
|
1419
|
+
return { target: 'codex', ...(await ensureCodexCliTool()) };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function installContext7Dependency(root) {
|
|
1423
|
+
const before = await checkContext7(root);
|
|
1424
|
+
if (before.ok) return { target: 'context7', status: 'present' };
|
|
1425
|
+
const changed = await ensureProjectContext7Config(root);
|
|
1426
|
+
return { target: 'context7', status: changed ? 'project_configured' : 'already_configured', command: 'sks context7 check' };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function installTmuxDependency(args = []) {
|
|
1430
|
+
return ensureTmuxCliTool(args, { dryRun: flag(args, '--dry-run') });
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function confirmInstall(question, args = []) {
|
|
1434
|
+
if (shouldAutoApproveInstall(args)) return true;
|
|
1435
|
+
if (!canAskYesNo()) return false;
|
|
1436
|
+
return /^(y|yes|예|네|응)$/i.test((await askPostinstallQuestion(`${question} [y/N] `)).trim());
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function canAskYesNo() {
|
|
1440
|
+
return Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true');
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function printDepsInstallAction(action) {
|
|
1444
|
+
if (!action) return;
|
|
1445
|
+
console.log(`${action.target}: ${action.status}${action.version ? ` ${action.version}` : ''}`);
|
|
1446
|
+
if (action.command) console.log(` command: ${action.command}`);
|
|
1447
|
+
if (action.error) console.log(` error: ${action.error}`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function autoReviewCommand(sub = 'status', args = []) {
|
|
1451
|
+
const action = sub || 'status';
|
|
1452
|
+
const high = flag(args, '--high') || action === '--high';
|
|
1453
|
+
const cleanArgs = args.filter((arg) => arg !== '--high');
|
|
1454
|
+
if (action === 'status' || action === 'check') {
|
|
1455
|
+
const status = await autoReviewStatus();
|
|
1456
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1457
|
+
console.log(autoReviewSummary(status));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (action === 'disable') {
|
|
1461
|
+
const status = await disableAutoReview();
|
|
1462
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1463
|
+
console.log(autoReviewSummary(status));
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (action === 'enable') {
|
|
1467
|
+
const status = await enableAutoReview({ high });
|
|
1468
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1469
|
+
console.log(autoReviewSummary(status));
|
|
1470
|
+
console.log(`\nProfile ready: ${status.profile_name}`);
|
|
1471
|
+
console.log(`Launch: codex --profile ${status.profile_name}`);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (['start', 'open', 'attach', '--high'].includes(action)) {
|
|
1475
|
+
const profile = autoReviewProfileName({ high });
|
|
1476
|
+
const status = await enableAutoReview({ high });
|
|
1477
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1478
|
+
console.log(`SKS Auto-Review enabled: ${profile}`);
|
|
1479
|
+
const codexUpdate = await maybePromptCodexUpdateForLaunch(args, { label: 'auto-review tmux launch' });
|
|
1480
|
+
if (codexUpdate.status === 'failed' || codexUpdate.status === 'updated_not_reflected') {
|
|
1481
|
+
console.error(`Codex CLI update failed: ${codexUpdate.error || 'updated version was not visible on PATH'}`);
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const sessionArg = readOption(cleanArgs, '--session', null);
|
|
1486
|
+
const session = sessionArg || sanitizeTmuxSessionName(`${profile}-${defaultTmuxSessionName(process.cwd())}`);
|
|
1487
|
+
return launchTmuxUi([...cleanArgs, '--session', session], { codexArgs: ['--profile', profile] });
|
|
1488
|
+
}
|
|
1489
|
+
console.error('Usage: sks auto-review status|enable|disable|start [--high] [--json]');
|
|
1490
|
+
console.error('Alias: sks --Auto-review [--high]');
|
|
1491
|
+
process.exitCode = 1;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function codexAppHelp(args = []) {
|
|
1495
|
+
const action = args[0] || 'help';
|
|
1496
|
+
if (action === 'remote-control' || action === 'remote') return codexAppRemoteControlCommand(args.slice(1));
|
|
1497
|
+
if (action === 'pat') {
|
|
1498
|
+
const patAction = args[1] || 'status';
|
|
1499
|
+
if (patAction !== 'status' && patAction !== 'check') {
|
|
1500
|
+
console.error('Usage: sks codex-app pat status [--json]');
|
|
1501
|
+
process.exitCode = 1;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const status = codexAccessTokenStatus();
|
|
1505
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
|
|
1506
|
+
console.log('Codex App PAT status\n');
|
|
1507
|
+
console.log(`Status: ${status.status}`);
|
|
1508
|
+
console.log(`Docs: ${status.docs_url}`);
|
|
1509
|
+
console.log(`Policy: ${status.storage_policy}`);
|
|
1510
|
+
for (const entry of status.access_token_env_vars) console.log(`${entry.name}: ${entry.present ? entry.value : 'missing'}`);
|
|
1511
|
+
for (const warning of status.warnings) console.log(`- ${warning}`);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (action === 'check' || action === 'status') {
|
|
1515
|
+
const status = await codexAppIntegrationStatus();
|
|
1516
|
+
const skills = await codexAppSkillReadiness();
|
|
1517
|
+
const readiness = { ...status, ok: status.ok && skills.ok, runtime_ok: status.ok, skills };
|
|
1518
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(readiness, null, 2));
|
|
1519
|
+
console.log(formatCodexAppStatus(status, { includeRaw: flag(args, '--verbose') }));
|
|
1520
|
+
console.log('');
|
|
1521
|
+
console.log(`Project $ skills: ${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} ${skills.project.root}`);
|
|
1522
|
+
console.log(`Global $ skills: ${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`} ${skills.global.root}`);
|
|
1523
|
+
if (!skills.ok) console.log('Run: sks bootstrap, or sks doctor --fix');
|
|
1524
|
+
if (!readiness.ok) process.exitCode = 1;
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (action === 'open') {
|
|
1528
|
+
const status = await codexAppIntegrationStatus();
|
|
1529
|
+
if (status.app.installed && process.platform === 'darwin') await runProcess('open', ['-a', 'Codex'], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch(() => null);
|
|
1530
|
+
else if (process.platform === 'darwin') await runProcess('open', [CODEX_APP_DOCS_URL], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch(() => null);
|
|
1531
|
+
console.log(formatCodexAppStatus(status));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const status = await codexAppIntegrationStatus();
|
|
1535
|
+
const skills = await codexAppSkillReadiness();
|
|
1536
|
+
console.log([
|
|
1537
|
+
sksAsciiLogo(), '',
|
|
1538
|
+
'Codex App', '',
|
|
1539
|
+
formatCodexAppStatus(status), '',
|
|
1540
|
+
`Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
|
|
1541
|
+
'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks codex-app pat status', ' sks codex-app remote-control --status', ' sks tmux check', '',
|
|
1542
|
+
'Generated files:', ' .codex/config.toml', ' .codex/hooks.json', ' .agents/skills/', ' .codex/agents/', ' .codex/SNEAKOSCOPE.md', ' AGENTS.md', '',
|
|
1543
|
+
'Git ignore:', ' default setup writes .gitignore entries for .sneakoscope/, .codex/, .agents/, AGENTS.md', ' --local-only writes those patterns to .git/info/exclude instead', '',
|
|
1544
|
+
'Prompt routes:', formatDollarCommandsCompact(' ')
|
|
1545
|
+
].join('\n'));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function aliases() {
|
|
1549
|
+
console.log(`${sksAsciiLogo()}
|
|
1550
|
+
|
|
1551
|
+
Aliases
|
|
1552
|
+
|
|
1553
|
+
Binary aliases:
|
|
1554
|
+
sks
|
|
1555
|
+
sneakoscope
|
|
1556
|
+
|
|
1557
|
+
Command aliases:
|
|
1558
|
+
sks memory -> sks gc
|
|
1559
|
+
sks --help -> sks help
|
|
1560
|
+
sks -h -> sks help
|
|
1561
|
+
|
|
1562
|
+
Codex App prompt commands:
|
|
1563
|
+
${formatDollarCommandsCompact(' ')}
|
|
1564
|
+
|
|
1565
|
+
Examples:
|
|
1566
|
+
sks
|
|
1567
|
+
sks setup
|
|
1568
|
+
sneakoscope setup
|
|
1569
|
+
sks commands
|
|
1570
|
+
sneakoscope commands
|
|
1571
|
+
`);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function usage(args = []) {
|
|
1575
|
+
const topic = String(args[0] || 'overview').toLowerCase();
|
|
1576
|
+
const blocks = {
|
|
1577
|
+
overview: [sksAsciiLogo(), '', 'Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
|
|
1578
|
+
install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb health', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
|
|
1579
|
+
bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
|
|
1580
|
+
root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
|
|
1581
|
+
deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
|
|
1582
|
+
tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". SKS always forces gpt-5.5; SKS_CODEX_MODEL and SKS_CODEX_FAST_HIGH=0 cannot downgrade or remove that model pin. Use SKS_CODEX_REASONING only for reasoning effort. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
|
|
1583
|
+
openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
|
|
1584
|
+
team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team open-tmux latest', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
|
|
1585
|
+
'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
|
|
1586
|
+
ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; imagegen is a required PPT skill so any needed raster assets or generated slide visual critique must invoke Codex App $imagegen/gpt-image-2 and save real outputs into the mission assets/review evidence paths. Context7 is conditional only when the sealed PPT contract needs current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
|
|
1587
|
+
'image-ux-review': ['Image UX Review', '', ' $Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', ' $UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘', ' sks image-ux-review status latest --json', '', '$Image-UX-Review captures or receives source UI screenshots, runs Codex App $imagegen/gpt-image-2 to create generated annotated review images with numbered callouts, then extracts those generated images into image-ux-issue-ledger.json. Text-only screenshot critique cannot pass image-ux-review-gate.json; missing generated review images remain an explicit blocker.'],
|
|
1588
|
+
goal: ['Goal', '', ' sks goal create "task"', ' sks goal status latest', ' sks goal pause latest', ' sks goal resume latest', ' sks goal clear latest'],
|
|
1589
|
+
'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
|
|
1590
|
+
dollar: ['Dollar Commands', '', formatDollarCommandsCompact(' '), '', 'Terminal: sks dollar-commands [--json]'],
|
|
1591
|
+
wiki: ['TriWiki', '', ' sks wiki pack', ' sks wiki refresh [--prune]', ' sks wiki sweep latest --json', ' sks wiki validate .sneakoscope/wiki/context-pack.json', ' sks wiki prune --dry-run --json', '', 'Packs include attention.use_first and attention.hydrate_first for compact recall plus source hydration. Sweep records intentional forgetting and promotion candidates.'],
|
|
1592
|
+
harness: ['Harness Growth', '', ' sks harness fixture --json', ' sks harness review --json', '', 'Runs deterministic fixtures for deliberate forgetting, skill cards, harness experiments, tool error taxonomy, permission profiles, MultiAgentV2, and tmux cockpit views.'],
|
|
1593
|
+
'skill-dream': ['Skill Dreaming', '', ' sks skill-dream status', ' sks skill-dream run --json', ' sks skill-dream record --route team --skills team,prompt-pipeline', '', 'Records cheap JSON usage counters in .sneakoscope/skills/dream-state.json and periodically writes recommendation-only keep/merge/prune/improve reports. It never deletes or merges skills automatically.'],
|
|
1594
|
+
'code-structure': ['Code Structure', '', ' sks code-structure scan', ' sks code-structure scan --json', '', 'Flags handwritten source files above 1000/2000/3000-line thresholds and records split-review exceptions.'],
|
|
1595
|
+
gx: ['GX', '', ' sks gx init architecture-atlas', ' sks gx render architecture-atlas --format all', ' sks gx validate architecture-atlas']
|
|
1596
|
+
};
|
|
1597
|
+
const catalog = COMMAND_CATALOG.find((c) => c.name === topic);
|
|
1598
|
+
const fallback = catalog ? [catalog.name, '', `Usage: ${catalog.usage}`, catalog.description, '', 'Run sks commands for the full catalog.'] : blocks.overview;
|
|
1599
|
+
console.log((blocks[topic] || fallback).join('\n'));
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async function bootstrap(args = []) {
|
|
1603
|
+
const root = await projectRoot();
|
|
1604
|
+
const conflicts = await scanHarnessConflicts(root);
|
|
1605
|
+
if (conflicts.hard_block) return blockForHarnessConflicts(conflicts, args);
|
|
1606
|
+
const installScope = installScopeFromArgs(args);
|
|
1607
|
+
const localOnly = flag(args, '--local-only');
|
|
1608
|
+
const globalCommand = await globalSksCommand();
|
|
1609
|
+
const initRes = await initProject(root, { force: flag(args, '--force'), installScope, globalCommand, localOnly, repair: true });
|
|
1610
|
+
const wikiMigration = await migrateWikiContextPack(root);
|
|
1611
|
+
const globalSkills = localOnly
|
|
1612
|
+
? { status: 'skipped', reason: '--local-only', root: globalCodexSkillsRoot() }
|
|
1613
|
+
: await ensureGlobalCodexSkillsDuringInstall({ force: flag(args, '--force') });
|
|
1614
|
+
const cliTools = await ensureRelatedCliTools(args);
|
|
1615
|
+
const context7Status = await checkContext7(root);
|
|
1616
|
+
const appRuntime = await codexAppIntegrationStatus({ codex: await getCodexInfo().catch(() => ({})) });
|
|
1617
|
+
const deps = await depsStatus(root, { context7: context7Status, codexApp: appRuntime, tmux: cliTools.tmux });
|
|
1618
|
+
const install = await installStatus(root, installScope, { globalCommand });
|
|
1619
|
+
const versioningInfo = await versioningStatus(root);
|
|
1620
|
+
const skills = await checkRequiredSkills(root);
|
|
1621
|
+
const guard = await harnessGuardStatus(root);
|
|
1622
|
+
const files = await codexAppFilesStatus(root, skills, versioningInfo);
|
|
1623
|
+
const ready = Boolean(!conflicts.hard_block && install.ok && files.ok && skills.ok && guard.ok && context7Status.ok && appRuntime.ok && deps.tmux.ok);
|
|
1624
|
+
const result = {
|
|
1625
|
+
root,
|
|
1626
|
+
ready,
|
|
1627
|
+
project_setup: { ok: files.ok, files, created: initRes.created },
|
|
1628
|
+
triwiki: { migrated: wikiMigration },
|
|
1629
|
+
install,
|
|
1630
|
+
cli_tools: cliTools,
|
|
1631
|
+
codex_app: appRuntime,
|
|
1632
|
+
global_skills: globalSkills,
|
|
1633
|
+
context7: context7Status,
|
|
1634
|
+
tmux: deps.tmux,
|
|
1635
|
+
harness_guard: guard,
|
|
1636
|
+
deps,
|
|
1637
|
+
next: ready ? ['sks', '$Team implement ...', '$QA-LOOP run ...'] : deps.next_actions
|
|
1638
|
+
};
|
|
1639
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1640
|
+
console.log('SKS Ready\n');
|
|
1641
|
+
console.log(`Project setup: ${files.ok ? 'ok' : 'missing'}`);
|
|
1642
|
+
console.log(`Codex App: ${appRuntime.ok ? 'ok' : 'needs setup'}`);
|
|
1643
|
+
console.log(`Skills: ${skills.ok ? 'ok' : `missing ${skills.missing.length}`}`);
|
|
1644
|
+
console.log(`Hooks: ${files.hooks.ok ? 'ok' : 'missing'}`);
|
|
1645
|
+
console.log(`Harness guard: ${guard.ok ? 'ok' : 'blocked'}`);
|
|
1646
|
+
console.log(`Context7: ${context7Status.ok ? 'ok' : 'missing'}`);
|
|
1647
|
+
console.log(`tmux: ${deps.tmux.ok ? 'ok' : 'missing'}${deps.tmux.version ? ` ${deps.tmux.version}` : ''}`);
|
|
1648
|
+
console.log(`ready: ${ready ? 'true' : 'false'}`);
|
|
1649
|
+
if (!ready) {
|
|
1650
|
+
console.log('\nNext:');
|
|
1651
|
+
const actions = Array.from(new Set([
|
|
1652
|
+
...deps.next_actions,
|
|
1653
|
+
...(!install.ok ? [install.scope === 'project' ? 'npm i -D sneakoscope' : 'npm i -g sneakoscope'] : []),
|
|
1654
|
+
...(!files.ok || !skills.ok || !guard.ok ? ['sks doctor --fix'] : [])
|
|
1655
|
+
]));
|
|
1656
|
+
for (const action of actions) console.log(` ${action}`);
|
|
1657
|
+
if (!flag(args, '--from-postinstall')) process.exitCode = 1;
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
console.log('\nNext:');
|
|
1661
|
+
console.log(' sks');
|
|
1662
|
+
console.log(' $Team implement ...');
|
|
1663
|
+
console.log(' $QA-LOOP run ...');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
async function codexAppFilesStatus(root, skills = null, versioningInfo = null) {
|
|
1667
|
+
skills ||= await checkRequiredSkills(root);
|
|
1668
|
+
versioningInfo ||= await versioningStatus(root);
|
|
1669
|
+
const status = {
|
|
1670
|
+
config: { ok: await exists(path.join(root, '.codex', 'config.toml')) },
|
|
1671
|
+
hooks: { ok: await exists(path.join(root, '.codex', 'hooks.json')) },
|
|
1672
|
+
skills,
|
|
1673
|
+
agents: { ok: await exists(path.join(root, '.codex', 'agents')) },
|
|
1674
|
+
quick_reference: { ok: await exists(path.join(root, '.codex', 'SNEAKOSCOPE.md')) },
|
|
1675
|
+
agents_rules: { ok: await exists(path.join(root, 'AGENTS.md')) },
|
|
1676
|
+
versioning: versioningInfo
|
|
1677
|
+
};
|
|
1678
|
+
status.ok = status.config.ok && status.hooks.ok && status.skills.ok && status.agents.ok && status.quick_reference.ok && status.agents_rules.ok;
|
|
1679
|
+
return status;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async function setup(args) {
|
|
1683
|
+
if (flag(args, '--bootstrap')) return bootstrap(args.filter((arg) => arg !== '--bootstrap'));
|
|
1684
|
+
const root = await projectRoot();
|
|
1685
|
+
const conflicts = await scanHarnessConflicts(root);
|
|
1686
|
+
if (conflicts.hard_block) return blockForHarnessConflicts(conflicts, args);
|
|
1687
|
+
const installScope = installScopeFromArgs(args);
|
|
1688
|
+
const localOnly = flag(args, '--local-only');
|
|
1689
|
+
const cliTools = await ensureRelatedCliTools(args);
|
|
1690
|
+
const globalCommand = await globalSksCommand();
|
|
1691
|
+
const res = await initProject(root, { force: flag(args, '--force'), installScope, globalCommand, localOnly });
|
|
1692
|
+
const wikiMigration = await migrateWikiContextPack(root);
|
|
1693
|
+
const globalSkills = localOnly
|
|
1694
|
+
? { status: 'skipped', reason: '--local-only', root: globalCodexSkillsRoot() }
|
|
1695
|
+
: await ensureGlobalCodexSkillsDuringInstall({ force: flag(args, '--force') });
|
|
1696
|
+
const install = await installStatus(root, installScope, { globalCommand });
|
|
1697
|
+
const versioningInfo = await versioningStatus(root);
|
|
1698
|
+
const appRuntime = await codexAppIntegrationStatus();
|
|
1699
|
+
const hooksPath = path.join(root, '.codex', 'hooks.json');
|
|
1700
|
+
const result = {
|
|
1701
|
+
root,
|
|
1702
|
+
cli_tools: cliTools,
|
|
1703
|
+
install,
|
|
1704
|
+
hooks: hooksPath,
|
|
1705
|
+
codex_app: {
|
|
1706
|
+
config: path.join(root, '.codex', 'config.toml'),
|
|
1707
|
+
hooks: hooksPath,
|
|
1708
|
+
skills: path.join(root, '.agents', 'skills'),
|
|
1709
|
+
global_skills: globalSkills.root,
|
|
1710
|
+
agents: path.join(root, '.codex', 'agents'),
|
|
1711
|
+
quick_reference: path.join(root, '.codex', 'SNEAKOSCOPE.md'),
|
|
1712
|
+
agents_rules: path.join(root, 'AGENTS.md')
|
|
1713
|
+
},
|
|
1714
|
+
codex_app_runtime: appRuntime,
|
|
1715
|
+
global_skills: globalSkills,
|
|
1716
|
+
triwiki: { migrated: wikiMigration },
|
|
1717
|
+
created: res.created,
|
|
1718
|
+
versioning: versioningInfo,
|
|
1719
|
+
local_only: localOnly,
|
|
1720
|
+
next: ['sks context7 check', 'sks selftest --mock', 'sks doctor', 'sks commands']
|
|
1721
|
+
};
|
|
1722
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1723
|
+
console.log(`${sksAsciiLogo()}\nSetup\n`);
|
|
1724
|
+
console.log(`Project: ${root}`);
|
|
1725
|
+
console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
|
|
1726
|
+
console.log(`CLI tools: Codex ${formatCodexCliToolStatus(cliTools.codex)}; tmux ${tmuxStatusKind(cliTools.tmux)} ${cliTools.tmux.version || cliTools.tmux.error || ''}`.trimEnd());
|
|
1727
|
+
console.log(`Hooks: ${path.relative(root, hooksPath)}`);
|
|
1728
|
+
console.log(`Version: explicit bump only${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
|
|
1729
|
+
if (localOnly) console.log('Git: local-only (.git/info/exclude; user AGENTS preserved, SKS managed block refreshed)');
|
|
1730
|
+
else console.log('Git: .gitignore ignores SKS generated files');
|
|
1731
|
+
console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
|
|
1732
|
+
console.log(`Global $: ${globalSkills.status === 'installed' ? 'ok' : globalSkills.status} ${globalSkills.root || ''}`.trimEnd());
|
|
1733
|
+
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'} Git Actions=${appRuntime.features?.git_actions?.ok ? 'ok' : 'missing'}`);
|
|
1734
|
+
console.log(`Prompt: intent-first routing, $Answer fact-check route, $DFix ultralight Direct Fix route, $PPT HTML/PDF presentation route, Context7 gate`);
|
|
1735
|
+
console.log(`Skills: .agents/skills`);
|
|
1736
|
+
console.log(`Next: sks context7 check; sks selftest --mock; sks commands; sks dollar-commands`);
|
|
1737
|
+
if (cliTools.codex.status === 'failed') console.log(`\nCodex CLI install failed. Run manually: npm i -g @openai/codex. ${cliTools.codex.error || ''}`.trim());
|
|
1738
|
+
if (cliTools.codex.status === 'installed_not_on_path') console.log(`\nCodex CLI installed but not on PATH. ${cliTools.codex.hint}`);
|
|
1739
|
+
if (!cliTools.tmux.ok) console.log(`\ntmux ${tmuxStatusKind(cliTools.tmux)}. Install: ${cliTools.tmux.install_hint}`);
|
|
1740
|
+
if (!install.ok && install.scope === 'global') console.log('\nGlobal command missing. Run: npm i -g sneakoscope');
|
|
1741
|
+
if (!install.ok && install.scope === 'project') console.log('\nProject package missing. Run: npm i -D sneakoscope');
|
|
1742
|
+
if (!appRuntime.ok) console.log('\nCodex App, first-party Codex Computer Use, and $imagegen/gpt-image-2 are required for SKS visual evidence; Browser Use is not a UI verification substitute. Run: sks codex-app check');
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function formatCodexCliToolStatus(status = {}) {
|
|
1746
|
+
if (status.status === 'present') return `ok ${status.version || status.bin || ''}`.trim();
|
|
1747
|
+
if (status.status === 'installed') return `installed ${status.version || status.bin || ''}`.trim();
|
|
1748
|
+
if (status.status === 'installed_not_on_path') return 'installed, not on PATH';
|
|
1749
|
+
if (status.status === 'skipped') return `skipped (${status.reason})`;
|
|
1750
|
+
if (status.status === 'failed') return 'install failed';
|
|
1751
|
+
return status.status || 'unknown';
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
async function fixPath(args) {
|
|
1755
|
+
const root = await projectRoot();
|
|
1756
|
+
const conflicts = await scanHarnessConflicts(root);
|
|
1757
|
+
if (conflicts.hard_block) return blockForHarnessConflicts(conflicts, args);
|
|
1758
|
+
const manifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
|
|
1759
|
+
const installScope = args.includes('--install-scope') || flag(args, '--project') || flag(args, '--global')
|
|
1760
|
+
? installScopeFromArgs(args)
|
|
1761
|
+
: normalizeInstallScope(manifest?.installation?.scope || 'global');
|
|
1762
|
+
const globalCommand = await globalSksCommand();
|
|
1763
|
+
await initProject(root, { installScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(manifest?.git?.local_only) });
|
|
1764
|
+
const install = await installStatus(root, installScope, { globalCommand });
|
|
1765
|
+
const result = {
|
|
1766
|
+
root,
|
|
1767
|
+
install_scope: installScope,
|
|
1768
|
+
hook_command_prefix: sksCommandPrefix(installScope, { globalCommand }),
|
|
1769
|
+
hooks: path.join(root, '.codex', 'hooks.json'),
|
|
1770
|
+
install
|
|
1771
|
+
};
|
|
1772
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1773
|
+
console.log('SKS hook path refreshed\n');
|
|
1774
|
+
console.log(`Project: ${root}`);
|
|
1775
|
+
console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
|
|
1776
|
+
console.log(`Hooks: .codex/hooks.json`);
|
|
1777
|
+
if (!install.ok && install.scope === 'global') console.log('\nGlobal command missing. Run: npm i -g sneakoscope');
|
|
1778
|
+
if (!install.ok && install.scope === 'project') console.log('\nProject package missing. Run: npm i -D sneakoscope');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async function doctor(args) {
|
|
1782
|
+
const root = await sksRoot();
|
|
1783
|
+
const requestedScope = args.includes('--install-scope') || flag(args, '--project') || flag(args, '--global')
|
|
1784
|
+
? installScopeFromArgs(args)
|
|
1785
|
+
: null;
|
|
1786
|
+
let conflictScan = await scanHarnessConflicts(root);
|
|
1787
|
+
let repairApplied = false;
|
|
1788
|
+
let globalSkillsRepair = null;
|
|
1789
|
+
let globalCodexConfigRepair = null;
|
|
1790
|
+
let projectRepair = null;
|
|
1791
|
+
let codexLbRepair = null;
|
|
1792
|
+
const globalCommand = await globalSksCommand();
|
|
1793
|
+
if (flag(args, '--fix') && !conflictScan.hard_block) {
|
|
1794
|
+
const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
|
|
1795
|
+
const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
|
|
1796
|
+
projectRepair = await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
|
|
1797
|
+
if (!flag(args, '--local-only')) globalCodexConfigRepair = await ensureGlobalCodexFastModeDuringInstall();
|
|
1798
|
+
if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
|
|
1799
|
+
codexLbRepair = await repairCodexLbAuth();
|
|
1800
|
+
repairApplied = true;
|
|
1801
|
+
conflictScan = await scanHarnessConflicts(root);
|
|
1802
|
+
}
|
|
1803
|
+
const codex = await getCodexInfo();
|
|
1804
|
+
const rust = await rustInfo();
|
|
1805
|
+
const nodeOk = Number(process.versions.node.split('.')[0]) >= 20;
|
|
1806
|
+
const storage = await storageReport(root);
|
|
1807
|
+
const pkgBytes = await dirSize(packageRoot()).catch(() => 0);
|
|
1808
|
+
const manifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
|
|
1809
|
+
const installScope = requestedScope || normalizeInstallScope(manifest?.installation?.scope || 'global');
|
|
1810
|
+
const install = await installStatus(root, installScope, { globalCommand });
|
|
1811
|
+
const dbPolicyExists = await exists(path.join(root, '.sneakoscope', 'db-safety.json'));
|
|
1812
|
+
const dbScan = await scanDbSafety(root).catch((err) => ({ ok: false, findings: [{ id: 'db_safety_scan_failed', severity: 'high', reason: err.message }] }));
|
|
1813
|
+
const context7Status = await checkContext7(root);
|
|
1814
|
+
const appRuntime = await codexAppIntegrationStatus({ codex });
|
|
1815
|
+
const tmuxStatus = await tmuxReadiness().catch((err) => ({ ok: false, version: null, error: err.message }));
|
|
1816
|
+
const skillStatus = await checkRequiredSkills(root);
|
|
1817
|
+
const globalSkillStatus = await checkRequiredSkills(null, globalCodexSkillsRoot());
|
|
1818
|
+
const codexLb = await codexLbStatus();
|
|
1819
|
+
const codexLbReady = (!codexLb.selected && !codexLb.provider_configured && !codexLb.env_file) || (codexLb.ok && codexLb.selected);
|
|
1820
|
+
const guardStatus = await harnessGuardStatus(root);
|
|
1821
|
+
const versioningInfo = await versioningStatus(root);
|
|
1822
|
+
const codexApp = await codexAppFilesStatus(root, skillStatus, versioningInfo);
|
|
1823
|
+
codexApp.global_skills = globalSkillStatus;
|
|
1824
|
+
const result = {
|
|
1825
|
+
node: { ok: nodeOk, version: process.version }, root, codex, rust,
|
|
1826
|
+
install,
|
|
1827
|
+
repair: { applied: repairApplied, project: projectRepair, global_codex_config: globalCodexConfigRepair, global_skills: globalSkillsRepair, codex_lb: codexLbRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
|
|
1828
|
+
harness_conflicts: {
|
|
1829
|
+
ok: conflictScan.ok,
|
|
1830
|
+
hard_block: conflictScan.hard_block,
|
|
1831
|
+
requires_human_approval: conflictScan.requires_human_approval,
|
|
1832
|
+
conflicts: conflictScan.conflicts,
|
|
1833
|
+
cleanup_prompt: conflictScan.hard_block ? llmHarnessCleanupPrompt(conflictScan) : null
|
|
1834
|
+
},
|
|
1835
|
+
sneakoscope: { ok: await exists(path.join(root, '.sneakoscope')) },
|
|
1836
|
+
context7: context7Status,
|
|
1837
|
+
codex_app_runtime: appRuntime,
|
|
1838
|
+
runtime: { tmux: { ok: Boolean(tmuxStatus.ok), bin: tmuxStatus.bin || null, version: tmuxStatus.version || null, min_version: tmuxStatus.min_version || '3.0', current_session: Boolean(tmuxStatus.current_session), install_hint: tmuxStatus.ok ? null : platformTmuxInstallHint(), error: tmuxStatus.error || null } },
|
|
1839
|
+
harness_guard: guardStatus,
|
|
1840
|
+
versioning: versioningInfo,
|
|
1841
|
+
db_guard: { ok: dbPolicyExists && dbScan.ok, policy: dbPolicyExists ? await loadDbSafetyPolicy(root) : null, scan: dbScan },
|
|
1842
|
+
hooks: { ok: await exists(path.join(root, '.codex', 'hooks.json')) },
|
|
1843
|
+
skills: skillStatus,
|
|
1844
|
+
global_skills: globalSkillStatus,
|
|
1845
|
+
codex_lb: { ...codexLb, ready: codexLbReady },
|
|
1846
|
+
codex_app: {
|
|
1847
|
+
...codexApp
|
|
1848
|
+
},
|
|
1849
|
+
package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }, storage
|
|
1850
|
+
};
|
|
1851
|
+
result.ready = !result.harness_conflicts.hard_block && nodeOk && Boolean(codex.bin) && install.ok && result.sneakoscope.ok && result.context7.ok && appRuntime.ok && result.runtime.tmux.ok && result.harness_guard.ok && result.versioning.ok && result.db_guard.ok && result.codex_lb.ready && result.codex_app.ok && result.skills.ok && result.global_skills.ok;
|
|
1852
|
+
if (result.harness_conflicts.hard_block) process.exitCode = 1;
|
|
1853
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1854
|
+
console.log(`${sksAsciiLogo()}\nDoctor\n`);
|
|
1855
|
+
console.log(`Node: ${nodeOk ? 'ok' : 'fail'} ${process.version}`);
|
|
1856
|
+
console.log(`Project: ${root}`);
|
|
1857
|
+
console.log(`Codex: ${codex.bin ? 'ok' : 'missing'} ${codex.version || ''}`);
|
|
1858
|
+
console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
|
|
1859
|
+
console.log(`Conflicts: ${result.harness_conflicts.hard_block ? 'blocked' : 'ok'} ${result.harness_conflicts.conflicts.length} finding(s)`);
|
|
1860
|
+
if (repairApplied) console.log('Repair: regenerated SKS managed files from the installed package template');
|
|
1861
|
+
if (globalCodexConfigRepair) console.log(`Global Codex config: ${globalCodexConfigRepair.status} ${globalCodexConfigRepair.config_path || ''}`.trimEnd());
|
|
1862
|
+
if (globalSkillsRepair) {
|
|
1863
|
+
const removed = globalSkillsRepair.removed_stale_generated_skills || [];
|
|
1864
|
+
const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
|
|
1865
|
+
console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}${cleanup}`.trimEnd());
|
|
1866
|
+
}
|
|
1867
|
+
if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+provider auth resynced' : 'provider auth resynced'} from stored env`);
|
|
1868
|
+
else if (codexLbRepair && codexLbRepair.status !== 'missing_env_key') console.log(`codex-lb repair: skipped (${codexLbRepair.status})`);
|
|
1869
|
+
if (flag(args, '--fix') && result.harness_conflicts.hard_block) console.log('Repair: skipped because another Codex harness needs human-approved removal first');
|
|
1870
|
+
console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
|
|
1871
|
+
console.log(`State: ${result.sneakoscope.ok ? 'ok' : 'missing .sneakoscope'}`);
|
|
1872
|
+
console.log(`Context7: ${result.context7.ok ? 'ok' : 'missing MCP config'} project=${result.context7.project.ok ? 'ok' : 'missing'} global=${result.context7.global.ok ? 'ok' : 'missing'}`);
|
|
1873
|
+
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'}`);
|
|
1874
|
+
console.log(`tmux: ${tmuxStatusKind(result.runtime.tmux)} ${result.runtime.tmux.version || result.runtime.tmux.error || ''}`.trimEnd());
|
|
1875
|
+
console.log(`Guard: ${result.harness_guard.ok ? 'ok' : 'blocked'}${result.harness_guard.source_exception ? ' source-exception' : ''}`);
|
|
1876
|
+
console.log(`Version: ${result.versioning.ok ? 'ok' : 'missing'}${result.versioning.enabled ? ` ${result.versioning.package_version || ''}` : ` ${result.versioning.reason || 'disabled'}`}`);
|
|
1877
|
+
console.log(`DB Guard: ${result.db_guard.ok ? 'ok' : 'blocked'} ${dbScan.findings?.length || 0} finding(s)`);
|
|
1878
|
+
console.log(`Hooks: ${result.hooks.ok ? 'ok' : 'missing .codex/hooks.json'}`);
|
|
1879
|
+
console.log(`codex-lb: ${result.codex_lb.ok ? 'ok' : result.codex_lb.ready ? 'not configured' : 'needs repair'}${result.codex_lb.base_url ? ` ${result.codex_lb.base_url}` : ''}`);
|
|
1880
|
+
console.log(`Codex App: ${result.codex_app.ok ? 'ok' : 'missing app files'} .codex/config.toml .codex/hooks.json .agents/skills .codex/agents .codex/SNEAKOSCOPE.md`);
|
|
1881
|
+
console.log(`Skills: ${result.skills.ok ? 'ok' : `missing ${result.skills.missing.length} skill(s)`}`);
|
|
1882
|
+
console.log(`Global $: ${result.global_skills.ok ? 'ok' : `missing ${result.global_skills.missing.length} skill(s)`} ${result.global_skills.root}`);
|
|
1883
|
+
console.log(`Package: ${result.package.human}`);
|
|
1884
|
+
console.log(`Storage: ${storage.total_human || '0 B'}`);
|
|
1885
|
+
console.log(`Ready: ${result.ready ? 'yes' : 'no'}`);
|
|
1886
|
+
if (!codex.bin) console.log('\nCodex CLI missing. Install separately: npm i -g @openai/codex, or set SKS_CODEX_BIN.');
|
|
1887
|
+
if (!install.ok && install.scope === 'global') console.log('SKS global command missing. Install: npm i -g sneakoscope');
|
|
1888
|
+
if (!install.ok && install.scope === 'project') console.log('SKS project package missing. Install in this project: npm i -D sneakoscope');
|
|
1889
|
+
if (result.harness_conflicts.hard_block) console.log(`\n${formatHarnessConflictReport(conflictScan)}`);
|
|
1890
|
+
if (!result.context7.ok) console.log('Context7 MCP missing. Run: sks context7 setup --scope project');
|
|
1891
|
+
if (!appRuntime.ok) console.log('Codex App or first-party MCP/plugin tools missing. Run: sks codex-app check');
|
|
1892
|
+
if (!result.runtime.tmux.ok) console.log('tmux missing. Run: sks deps install tmux');
|
|
1893
|
+
if (!result.harness_guard.ok) console.log('Harness guard failed. Run: sks setup from a real terminal, then sks guard check.');
|
|
1894
|
+
if (!result.versioning.ok) console.log('Versioning metadata drift detected. Run: sks versioning status, then sks versioning bump if release metadata should change.');
|
|
1895
|
+
if (!result.codex_lb.ready) console.log('codex-lb config/auth drift detected. Run: sks doctor --fix, or reconfigure once with sks codex-lb reconfigure --host <domain> --api-key <key>.');
|
|
1896
|
+
if (!result.skills.ok) console.log(`Missing skills: ${result.skills.missing.join(', ')}. Run: sks setup`);
|
|
1897
|
+
if (!result.global_skills.ok) console.log(`Missing global $ skills: ${result.global_skills.missing.join(', ')}. Run: npm i -g sneakoscope, or sks setup from a non-local-only run.`);
|
|
1898
|
+
const blocked = [];
|
|
1899
|
+
if (!result.runtime.tmux.ok) blocked.push(['tmux is missing', 'sks deps install tmux']);
|
|
1900
|
+
if (!appRuntime.ok) blocked.push(['Codex App or first-party MCP/plugin tools need setup', 'sks codex-app check']);
|
|
1901
|
+
if (blocked.length) {
|
|
1902
|
+
console.log('\nBlocked:');
|
|
1903
|
+
for (const [reason] of blocked) console.log(`- ${reason}`);
|
|
1904
|
+
console.log('\nRun:');
|
|
1905
|
+
for (const [, command] of blocked) console.log(` ${command}`);
|
|
1906
|
+
}
|
|
1907
|
+
if (!result.ready && !flag(args, '--fix')) console.log('Run: sks doctor --fix');
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
async function codexAppSkillReadiness(root = null) {
|
|
1911
|
+
root ||= await sksRoot();
|
|
1912
|
+
const project = await checkRequiredSkills(root);
|
|
1913
|
+
const global = await checkRequiredSkills(null, globalCodexSkillsRoot());
|
|
1914
|
+
return { ok: project.ok || global.ok, project, global };
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function init(args) {
|
|
1918
|
+
const root = await projectRoot();
|
|
1919
|
+
const conflicts = await scanHarnessConflicts(root);
|
|
1920
|
+
if (conflicts.hard_block) return blockForHarnessConflicts(conflicts, args);
|
|
1921
|
+
const installScope = installScopeFromArgs(args);
|
|
1922
|
+
const localOnly = flag(args, '--local-only');
|
|
1923
|
+
const globalCommand = await globalSksCommand();
|
|
1924
|
+
const res = await initProject(root, { force: flag(args, '--force'), installScope, globalCommand, localOnly });
|
|
1925
|
+
console.log(`Initialized SKS in ${root}`);
|
|
1926
|
+
console.log(`Install scope: ${installScope} (${sksCommandPrefix(installScope, { globalCommand })})`);
|
|
1927
|
+
if (localOnly) console.log('Git mode: local-only (.git/info/exclude)');
|
|
1928
|
+
else console.log('Git mode: shared .gitignore');
|
|
1929
|
+
for (const x of res.created) console.log(`- ${x}`);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function blockForHarnessConflicts(scan, args = []) {
|
|
1933
|
+
const result = { ready: false, install_blocked: true, harness_conflicts: scan, cleanup_prompt: llmHarnessCleanupPrompt(scan) };
|
|
1934
|
+
process.exitCode = 1;
|
|
1935
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
1936
|
+
console.error(formatHarnessConflictReport(scan));
|
|
1937
|
+
console.error('\nSKS setup cannot continue while another Codex harness is present.');
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
async function globalSksCommand() {
|
|
1941
|
+
return await discoverGlobalSksCommand() || 'sks';
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function installStatus(root, scope, opts = {}) {
|
|
1945
|
+
const discoveredGlobalBin = await discoverGlobalSksCommand();
|
|
1946
|
+
const configuredGlobalBin = await configuredSksBin(opts.globalCommand);
|
|
1947
|
+
const globalBin = configuredGlobalBin || discoveredGlobalBin;
|
|
1948
|
+
const sourceProject = await isHarnessSourceProject(root).catch(() => false);
|
|
1949
|
+
const sourceBin = path.join(root, 'bin', 'sks.mjs');
|
|
1950
|
+
const sourceBinExists = sourceProject && await exists(sourceBin);
|
|
1951
|
+
const commandPrefix = sourceBinExists ? 'node ./bin/sks.mjs' : sksCommandPrefix(scope, { globalCommand: globalBin || undefined });
|
|
1952
|
+
const projectBin = path.join(root, 'node_modules', 'sneakoscope', 'bin', 'sks.mjs');
|
|
1953
|
+
const projectBinExists = await exists(projectBin);
|
|
1954
|
+
return {
|
|
1955
|
+
scope,
|
|
1956
|
+
default_scope: 'global',
|
|
1957
|
+
command_prefix: commandPrefix,
|
|
1958
|
+
global_bin: globalBin,
|
|
1959
|
+
project_bin: projectBin,
|
|
1960
|
+
source_project: sourceProject,
|
|
1961
|
+
source_bin: sourceBinExists ? sourceBin : null,
|
|
1962
|
+
ok: sourceBinExists || (scope === 'project' ? projectBinExists : Boolean(globalBin))
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
async function discoverGlobalSksCommand() {
|
|
1967
|
+
const configured = await configuredSksBin(process.env.SKS_BIN);
|
|
1968
|
+
if (configured) return configured;
|
|
1969
|
+
for (const name of ['sks', 'sneakoscope']) {
|
|
1970
|
+
const found = await which(name).catch(() => null);
|
|
1971
|
+
if (isStableSksBin(found)) return found;
|
|
1972
|
+
}
|
|
1973
|
+
return await npmGlobalSksBin();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
async function configuredSksBin(candidate) {
|
|
1977
|
+
if (!candidate || candidate === 'sks') return null;
|
|
1978
|
+
return isStableSksBin(candidate) && await exists(candidate) ? candidate : null;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function isStableSksBin(candidate) {
|
|
1982
|
+
return Boolean(candidate) && !isTransientNpmBinPath(candidate);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function isTransientNpmBinPath(candidate) {
|
|
1986
|
+
const normalized = String(candidate || '').split(path.sep).join('/');
|
|
1987
|
+
return normalized.includes('/_npx/')
|
|
1988
|
+
|| normalized.includes('/_cacache/tmp/')
|
|
1989
|
+
|| /\/npm-cache\/_npx\//.test(normalized)
|
|
1990
|
+
|| (/\/node_modules\/\.bin\/sks$/.test(normalized) && normalized.includes('/.npm-cache/'));
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
async function npmGlobalSksBin() {
|
|
1994
|
+
const npm = await which('npm').catch(() => null);
|
|
1995
|
+
if (!npm) return null;
|
|
1996
|
+
const result = await runProcess(npm, ['prefix', '-g'], { timeoutMs: 10000, maxOutputBytes: 4096 });
|
|
1997
|
+
if (result.code !== 0) return null;
|
|
1998
|
+
const prefix = result.stdout.trim().split(/\r?\n/).pop();
|
|
1999
|
+
if (!prefix) return null;
|
|
2000
|
+
const binDir = process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
|
|
2001
|
+
const suffixes = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
|
|
2002
|
+
for (const name of ['sks', 'sneakoscope']) {
|
|
2003
|
+
for (const suffix of suffixes) {
|
|
2004
|
+
const candidate = path.join(binDir, `${name}${suffix}`);
|
|
2005
|
+
if (isStableSksBin(candidate) && await exists(candidate)) return candidate;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
async function npmPackageVersion(name) {
|
|
2012
|
+
const envName = `SKS_NPM_VIEW_${String(name || '').replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_VERSION`;
|
|
2013
|
+
if (process.env[envName]) return { version: process.env[envName] };
|
|
2014
|
+
const npm = await which('npm').catch(() => null);
|
|
2015
|
+
if (!npm) return { error: 'npm not found' };
|
|
2016
|
+
const result = await runProcess(npm, ['view', name, 'version'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
2017
|
+
if (result.code !== 0) return { error: `${result.stderr || result.stdout || 'npm view failed'}`.trim() };
|
|
2018
|
+
return { version: result.stdout.trim().split(/\s+/).pop() };
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function effectivePackageVersion() {
|
|
2022
|
+
const pkg = await readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
|
|
2023
|
+
return highestVersion([PACKAGE_VERSION, pkg.version]);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
async function selftestRuntimeVersion() {
|
|
2027
|
+
const source = await safeReadText(path.join(packageRoot(), 'src', 'core', 'fsx.mjs'));
|
|
2028
|
+
const sourceVersion = source.match(/export const PACKAGE_VERSION = ['"]([^'"]+)['"];/)?.[1] || null;
|
|
2029
|
+
return sourceVersion || PACKAGE_VERSION;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function highestVersion(versions = []) {
|
|
2033
|
+
return versions.filter(Boolean).reduce((best, candidate) => compareVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function compareVersions(a, b) {
|
|
2037
|
+
const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
|
2038
|
+
const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
|
|
2039
|
+
for (let i = 0; i < Math.max(pa.length, pb.length, 3); i++) {
|
|
2040
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
2041
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
2042
|
+
}
|
|
2043
|
+
return 0;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function formatQuestionsForCli(schema) {
|
|
2047
|
+
return schema.slots.map((s, i) => {
|
|
2048
|
+
const options = s.options ? ` Options: ${s.options.join(', ')}.` : '';
|
|
2049
|
+
const examples = s.examples ? ` Examples: ${s.examples.join(', ')}.` : '';
|
|
2050
|
+
return `${i + 1}. ${s.id}: ${s.question}${options}${examples}`;
|
|
2051
|
+
}).join('\n');
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
async function safeReadText(file, fallback = '') {
|
|
2055
|
+
try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
2059
|
+
|
|
2060
|
+
function hasResearchProfileConfig(text = '') {
|
|
2061
|
+
return /\[profiles\.sks-research-xhigh\][\s\S]*?model = "gpt-5\.5"[\s\S]*?model_reasoning_effort = "xhigh"/.test(text)
|
|
2062
|
+
&& /\[profiles\.sks-research\][\s\S]*?model = "gpt-5\.5"[\s\S]*?approval_policy = "never"[\s\S]*?model_reasoning_effort = "xhigh"/.test(text);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
function readMaxCycles(args, fallback) {
|
|
2066
|
+
const i = args.indexOf('--max-cycles');
|
|
2067
|
+
const raw = i >= 0 && args[i + 1] ? Number(args[i + 1]) : Number(fallback);
|
|
2068
|
+
if (!Number.isFinite(raw)) return Math.max(1, Number.parseInt(fallback, 10) || 1);
|
|
2069
|
+
return Math.max(1, Math.min(50, Math.floor(raw)));
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function positionalArgs(args = []) {
|
|
2073
|
+
const out = [];
|
|
2074
|
+
const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--cycle-timeout-minutes', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines', '--dir']);
|
|
2075
|
+
for (let i = 0; i < args.length; i++) {
|
|
2076
|
+
const arg = String(args[i]);
|
|
2077
|
+
if (valueFlags.has(arg)) {
|
|
2078
|
+
i++;
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (!arg.startsWith('--')) out.push(arg);
|
|
2082
|
+
}
|
|
2083
|
+
return out;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function readFlagValue(args, name, fallback) {
|
|
2087
|
+
const i = args.indexOf(name);
|
|
2088
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
async function selftest() {
|
|
2092
|
+
// Keep selftest non-interactive even when install helpers would normally prompt.
|
|
2093
|
+
process.env.CI = 'true';
|
|
2094
|
+
const tmp = tmpdir();
|
|
2095
|
+
process.chdir(tmp);
|
|
2096
|
+
await initProject(tmp, {});
|
|
2097
|
+
const latestMissionTmp = tmpdir();
|
|
2098
|
+
await ensureDir(path.join(latestMissionTmp, '.sneakoscope', 'missions', 'M-20260509-193839-6917'));
|
|
2099
|
+
await ensureDir(path.join(latestMissionTmp, '.sneakoscope', 'missions', 'M-20260509-193839-0551'));
|
|
2100
|
+
await writeJsonAtomic(path.join(latestMissionTmp, '.sneakoscope', 'missions', 'M-20260509-193839-6917', 'mission.json'), { id: 'M-20260509-193839-6917', created_at: '2026-05-09T10:38:39.362Z' });
|
|
2101
|
+
await writeJsonAtomic(path.join(latestMissionTmp, '.sneakoscope', 'missions', 'M-20260509-193839-0551', 'mission.json'), { id: 'M-20260509-193839-0551', created_at: '2026-05-09T10:38:39.363Z' });
|
|
2102
|
+
if (await findLatestMission(latestMissionTmp) !== 'M-20260509-193839-0551') throw new Error('selftest: latest mission should use mission metadata time, not lexicographic id order');
|
|
2103
|
+
if (readMaxCycles(['--max-cycles', 'Infinity'], 8) !== 8) throw new Error('selftest: non-finite max cycles not sanitized');
|
|
2104
|
+
if (readMaxCycles(['--max-cycles', '0'], 8) !== 1) throw new Error('selftest: zero max cycles not bounded');
|
|
2105
|
+
const loopMission = await createMission(tmp, { mode: 'team', prompt: 'compliance loop guard selftest' });
|
|
2106
|
+
const loopState = { mission_id: loopMission.id, mode: 'TEAM', route_command: '$Team', stop_gate: 'team-gate.json' };
|
|
2107
|
+
await writeJsonAtomic(path.join(loopMission.dir, 'team-gate.json'), { passed: false });
|
|
2108
|
+
for (let i = 0; i < 2; i++) {
|
|
2109
|
+
const stop = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
|
|
2110
|
+
if (stop?.decision !== 'block') throw new Error('selftest: compliance loop guard blocked too early');
|
|
2111
|
+
}
|
|
2112
|
+
const trippedStop = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
|
|
2113
|
+
if (trippedStop) throw new Error('selftest: compliance loop guard did not terminally trip');
|
|
2114
|
+
const loopBlocker = await readJson(path.join(loopMission.dir, 'hard-blocker.json'), null);
|
|
2115
|
+
if (loopBlocker?.reason !== 'compliance_loop_guard_tripped') throw new Error('selftest: compliance loop guard did not write hard blocker');
|
|
2116
|
+
await writeSelftestRouteProof(tmp, { missionId: loopMission.id, kind: 'hard_blocker' });
|
|
2117
|
+
const hardBlockerUnblocked = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
|
|
2118
|
+
if (hardBlockerUnblocked?.decision === 'block' && !String(hardBlockerUnblocked.reason || '').includes('reflection')) throw new Error('selftest: hard blocker did not unblock incomplete active gate');
|
|
2119
|
+
const clarificationMission = await createMission(tmp, { mode: 'team', prompt: 'visible question gate selftest' });
|
|
2120
|
+
await writeTextAtomic(path.join(clarificationMission.dir, 'questions.md'), '# Questions\n\n1. GOAL_PRECISE: What should be changed?\n');
|
|
2121
|
+
await writeJsonAtomic(path.join(clarificationMission.dir, 'required-answers.schema.json'), { slots: [{ id: 'GOAL_PRECISE', question: 'What should be changed?' }] });
|
|
2122
|
+
const clarificationState = {
|
|
2123
|
+
mission_id: clarificationMission.id,
|
|
2124
|
+
mode: 'TEAM',
|
|
2125
|
+
route_command: '$Team',
|
|
2126
|
+
phase: 'TEAM_CLARIFICATION_AWAITING_ANSWERS',
|
|
2127
|
+
clarification_required: true,
|
|
2128
|
+
implementation_allowed: false,
|
|
2129
|
+
ambiguity_gate_required: true,
|
|
2130
|
+
ambiguity_gate_passed: false,
|
|
2131
|
+
stop_gate: 'clarification-gate'
|
|
2132
|
+
};
|
|
2133
|
+
for (let i = 0; i < 5; i++) {
|
|
2134
|
+
const stop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'continuing implementation without visible questions' });
|
|
2135
|
+
if (stop?.decision !== 'block' || stop?.gate !== 'clarification' || !/paused|answers|pipeline answer/i.test(String(stop?.reason || ''))) throw new Error('selftest: clarification not paused');
|
|
2136
|
+
}
|
|
2137
|
+
if (await exists(path.join(clarificationMission.dir, 'hard-blocker.json'))) throw new Error('selftest: clarification wrote hard-blocker');
|
|
2138
|
+
const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions\n1. GOAL_PRECISE\nsks pipeline answer latest --stdin' });
|
|
2139
|
+
if (visibleQuestionStop?.continue !== true) throw new Error('selftest: visible clarification did not wait');
|
|
2140
|
+
const cg = await projectGateStatus(tmp, clarificationState);
|
|
2141
|
+
if (!cg.blockers.includes('clarification-gate:explicit_user_answers') || !cg.blockers.includes('clarification-gate:pipeline_answer')) throw new Error('selftest: missing clarification blockers');
|
|
2142
|
+
await setCurrent(tmp, clarificationState);
|
|
2143
|
+
const hookPath = path.join(packageRoot(), 'bin', 'sks.mjs');
|
|
2144
|
+
const blockedPre = await runProcess(process.execPath, [hookPath, 'hook', 'pre-tool'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, tool_name: 'Bash', tool_input: { command: 'npm run selftest' } }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2145
|
+
if (blockedPre.code !== 0) throw new Error(`selftest: pre-tool exit ${blockedPre.code}: ${blockedPre.stderr}`);
|
|
2146
|
+
const bp = JSON.parse(blockedPre.stdout || '{}');
|
|
2147
|
+
if (bp.decision !== 'block' || !String(bp.reason || '').includes('waiting for explicit user answers')) throw new Error('selftest: pre-tool not blocked');
|
|
2148
|
+
const deniedPermission = await runProcess(process.execPath, [hookPath, 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'npm run selftest', action: 'Run command' }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2149
|
+
if (deniedPermission.code !== 0) throw new Error(`selftest: permission exit ${deniedPermission.code}: ${deniedPermission.stderr}`);
|
|
2150
|
+
const dp = JSON.parse(deniedPermission.stdout || '{}');
|
|
2151
|
+
if (dp.hookSpecificOutput?.decision?.behavior !== 'deny' || !String(dp.hookSpecificOutput?.decision?.message || '').includes('waiting for explicit user answers')) throw new Error('selftest: permission not denied');
|
|
2152
|
+
const answerTool = await runProcess(process.execPath, [hookPath, 'hook', 'pre-tool'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, tool_name: 'Bash', tool_input: { command: `sks pipeline answer ${clarificationMission.id} --stdin` } }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2153
|
+
if (answerTool.code !== 0) throw new Error(`selftest: answer hook exit ${answerTool.code}: ${answerTool.stderr}`);
|
|
2154
|
+
if (JSON.parse(answerTool.stdout || '{}').decision === 'block') throw new Error('selftest: answer command blocked');
|
|
2155
|
+
await setCurrent(tmp, loopState);
|
|
2156
|
+
const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
|
|
2157
|
+
cwd: tmp,
|
|
2158
|
+
input: JSON.stringify({ cwd: tmp, prompt: '$DFix Change the CTA label only' }),
|
|
2159
|
+
timeoutMs: 15000,
|
|
2160
|
+
maxOutputBytes: 64 * 1024
|
|
2161
|
+
});
|
|
2162
|
+
if (dfixPromptHook.code !== 0) throw new Error(`selftest: DFix prompt hook exited ${dfixPromptHook.code}: ${dfixPromptHook.stderr}`);
|
|
2163
|
+
if (await exists(path.join(tmp, '.sneakoscope', 'state', 'light-route-stop.json'))) throw new Error('selftest: DFix prompt hook created persistent light-route state');
|
|
2164
|
+
const dfixStopHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'stop'], {
|
|
2165
|
+
cwd: tmp,
|
|
2166
|
+
input: JSON.stringify({ cwd: tmp, last_assistant_message: 'DFix 완료 요약: CTA 라벨만 변경했습니다.\nDFix 솔직모드: 검증=대상 파일 확인 통과, 미검증=없음, 남은 문제=없음.' }),
|
|
2167
|
+
timeoutMs: 15000,
|
|
2168
|
+
maxOutputBytes: 64 * 1024
|
|
2169
|
+
});
|
|
2170
|
+
if (dfixStopHook.code !== 0) throw new Error(`selftest: DFix stop hook exited ${dfixStopHook.code}: ${dfixStopHook.stderr}`);
|
|
2171
|
+
const dfixStop = JSON.parse(dfixStopHook.stdout || '{}');
|
|
2172
|
+
if (dfixStop.decision === 'block' || dfixStop.continue === false) throw new Error(`selftest: DFix stop hook was blocked: ${dfixStopHook.stdout}`);
|
|
2173
|
+
if (!String(dfixStop.systemMessage || '').includes('DFix ultralight finalization accepted')) throw new Error('selftest: DFix stop hook did not use the ultralight finalization bypass');
|
|
2174
|
+
await writeJsonAtomic(path.join(loopMission.dir, 'team-roster.json'), { schema_version: 1, mission_id: loopMission.id, confirmed: true });
|
|
2175
|
+
await writeJsonAtomic(path.join(loopMission.dir, 'team-session-cleanup.json'), { schema_version: 1, passed: true, all_sessions_closed: true, outstanding_sessions: 0, live_transcript_finalized: true });
|
|
2176
|
+
await writeJsonAtomic(path.join(loopMission.dir, 'team-gate.json'), { passed: true, team_roster_confirmed: true, analysis_artifact: true, triwiki_refreshed: true, triwiki_validated: true, consensus_artifact: true, implementation_team_fresh: true, review_artifact: true, integration_evidence: true, session_cleanup: true });
|
|
2177
|
+
const afterGateFixStop = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
|
|
2178
|
+
if (afterGateFixStop?.decision !== 'block' || !String(afterGateFixStop.reason || '').includes('reflection')) throw new Error('selftest: hard blocker masked later gate progress');
|
|
2179
|
+
const guardStatus = await harnessGuardStatus(tmp);
|
|
2180
|
+
if (!guardStatus.ok || !guardStatus.locked || guardStatus.source_exception) throw new Error('selftest: harness guard not locked in installed project');
|
|
2181
|
+
const repairTmp = tmpdir();
|
|
2182
|
+
await writeJsonAtomic(path.join(repairTmp, 'package.json'), { name: 'sneakoscope', version: '0.0.0', type: 'module' });
|
|
2183
|
+
await ensureDir(path.join(repairTmp, 'bin'));
|
|
2184
|
+
await writeTextAtomic(path.join(repairTmp, 'bin', 'sks.mjs'), '#!/usr/bin/env node\n');
|
|
2185
|
+
await ensureDir(path.join(repairTmp, 'src', 'core'));
|
|
2186
|
+
await writeTextAtomic(path.join(repairTmp, 'src', 'core', 'init.mjs'), '// source-project marker\n');
|
|
2187
|
+
await writeTextAtomic(path.join(repairTmp, 'src', 'core', 'hooks-runtime.mjs'), '// source-project marker\n');
|
|
2188
|
+
await initProject(repairTmp, { installScope: 'project', localOnly: true });
|
|
2189
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'), 'tampered\n');
|
|
2190
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
|
|
2191
|
+
await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
|
|
2192
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'), '---\nname: stale-sks-generated\ndescription: Old SKS generated skill that should disappear on update.\n---\n');
|
|
2193
|
+
const stalePluginSkillNames = ['browser', 'browser-use', 'computer-use', 'chrome', 'documents', 'presentations', 'spreadsheets', 'latex'];
|
|
2194
|
+
const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
|
|
2195
|
+
for (const name of stalePluginSkillNames) {
|
|
2196
|
+
await ensureDir(path.join(repairTmp, '.agents', 'skills', name));
|
|
2197
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
2198
|
+
}
|
|
2199
|
+
await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
|
|
2200
|
+
schema_version: 1,
|
|
2201
|
+
generated_by: 'sneakoscope',
|
|
2202
|
+
version: '0.0.1',
|
|
2203
|
+
skills: ['team', 'stale-sks-generated', ...stalePluginSkillNames],
|
|
2204
|
+
files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md', ...stalePluginSkillNames.map((name) => `.agents/skills/${name}/SKILL.md`)]
|
|
2205
|
+
});
|
|
2206
|
+
const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
|
|
2207
|
+
await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
|
|
2208
|
+
const staleManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
|
|
2209
|
+
staleManifest.version = '0.0.1';
|
|
2210
|
+
staleManifest.generated_files = {
|
|
2211
|
+
schema_version: 1,
|
|
2212
|
+
generated_by: 'sneakoscope',
|
|
2213
|
+
prune_policy: 'remove_previous_sks_generated_paths_absent_from_current_manifest',
|
|
2214
|
+
files: [...(staleManifest.generated_files?.files || []), '.agents/skills/stale-sks-generated/SKILL.md', staleCodexAgentRel]
|
|
2215
|
+
};
|
|
2216
|
+
await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'manifest.json'), staleManifest);
|
|
2217
|
+
await ensureDir(path.join(repairTmp, '.agents', 'skills', 'custom-keep'));
|
|
2218
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md'), '---\nname: custom-keep\ndescription: User custom skill, not generated by SKS.\n---\n');
|
|
2219
|
+
await writeTextAtomic(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'), 'legacy mirror\n');
|
|
2220
|
+
await writeTextAtomic(path.join(repairTmp, '.codex', 'hooks.json'), '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "tampered hook" }] }] } }\n');
|
|
2221
|
+
await writeTextAtomic(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'), 'tampered quick reference\n');
|
|
2222
|
+
await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'policy.json'), { broken: true });
|
|
2223
|
+
const existingAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
|
|
2224
|
+
await writeTextAtomic(path.join(repairTmp, 'AGENTS.md'), existingAgentsMd.replace(/<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->[\s\S]*?<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n?/, '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\ntampered managed block\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n'));
|
|
2225
|
+
const doctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--local-only', '--json'], {
|
|
2226
|
+
cwd: repairTmp,
|
|
2227
|
+
env: { HOME: path.join(repairTmp, 'home'), SKS_DISABLE_UPDATE_CHECK: '1' },
|
|
2228
|
+
timeoutMs: 30000,
|
|
2229
|
+
maxOutputBytes: 1024 * 1024
|
|
2230
|
+
});
|
|
2231
|
+
if (doctorRepair.code !== 0) throw new Error(`selftest: doctor --fix exited ${doctorRepair.code}: ${doctorRepair.stderr}`);
|
|
2232
|
+
const doctorRepairJson = JSON.parse(doctorRepair.stdout || '{}');
|
|
2233
|
+
if (!doctorRepairJson.repair?.applied || doctorRepairJson.install?.scope !== 'project' || !doctorRepairJson.install?.ok || !doctorRepairJson.install?.source_project) throw new Error('selftest: doctor scope');
|
|
2234
|
+
const repairedManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
|
|
2235
|
+
if (repairedManifest.installation?.scope !== 'project' || repairedManifest.installation?.hook_command_prefix !== 'node ./bin/sks.mjs') throw new Error('selftest: manifest scope');
|
|
2236
|
+
const repairedCodexConfig = await safeReadText(path.join(repairTmp, '.codex', 'config.toml'));
|
|
2237
|
+
assertCodexWarn(repairedCodexConfig, 'doctor project config');
|
|
2238
|
+
const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
|
|
2239
|
+
if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest: doctor repair did not regenerate team skill');
|
|
2240
|
+
if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove deprecated agent-team alias skill');
|
|
2241
|
+
if (await exists(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'))) throw new Error('selftest: doctor repair did not prune stale generated skill from previous SKS manifest');
|
|
2242
|
+
for (const name of stalePluginSkillNames) {
|
|
2243
|
+
if (await exists(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor repair left stale generated ${name} plugin shadow skill`);
|
|
2244
|
+
}
|
|
2245
|
+
if (await exists(path.join(repairTmp, staleCodexAgentRel))) throw new Error('selftest: doctor repair did not prune stale generated agent file from previous SKS manifest');
|
|
2246
|
+
if (!doctorRepairJson.repair?.project?.skill_install?.removed_stale_generated_skills?.includes('.agents/skills/stale-sks-generated')) throw new Error('selftest: stale skill report');
|
|
2247
|
+
const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
|
|
2248
|
+
if (![...(generatedCleanupReport.pruned || []), ...(generatedCleanupReport.already_absent || [])].includes(staleCodexAgentRel)) throw new Error('selftest: stale file report');
|
|
2249
|
+
if (!(await exists(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md')))) throw new Error('selftest: doctor repair removed a user-owned custom skill');
|
|
2250
|
+
if (await exists(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove legacy .codex/skills');
|
|
2251
|
+
const repairedQuickReference = await safeReadText(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'));
|
|
2252
|
+
if (!repairedQuickReference.includes('Install scope: `project`') || repairedQuickReference.includes('tampered')) throw new Error('selftest: doctor --fix did not regenerate quick reference');
|
|
2253
|
+
const repairedHooks = await safeReadText(path.join(repairTmp, '.codex', 'hooks.json'));
|
|
2254
|
+
if (!repairedHooks.includes('node ./bin/sks.mjs hook stop') || repairedHooks.includes('tampered hook')) throw new Error('selftest: doctor --fix did not regenerate Codex hooks');
|
|
2255
|
+
const repairedPolicy = await readJson(path.join(repairTmp, '.sneakoscope', 'policy.json'));
|
|
2256
|
+
if (repairedPolicy.broken || repairedPolicy.installation?.scope !== 'project' || !repairedPolicy.prompt_pipeline?.dollar_commands?.includes('$Team')) throw new Error('selftest: policy regen');
|
|
2257
|
+
const repairedAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
|
|
2258
|
+
if (!repairedAgentsMd.includes('Do not create unrequested fallback implementation code') || repairedAgentsMd.includes('tampered managed block')) throw new Error('selftest: AGENTS regen');
|
|
2259
|
+
const doctorGlobalTmp = tmpdir();
|
|
2260
|
+
await writeJsonAtomic(path.join(doctorGlobalTmp, 'package.json'), { name: 'doctor-global-skill-repair-smoke', version: '0.0.0' });
|
|
2261
|
+
await initProject(doctorGlobalTmp, { installScope: 'global' });
|
|
2262
|
+
const doctorGlobalHome = path.join(doctorGlobalTmp, 'home');
|
|
2263
|
+
await ensureDir(path.join(doctorGlobalHome, '.codex'));
|
|
2264
|
+
await writeTextAtomic(path.join(doctorGlobalHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[features]\nplugins = false\napps = false\n');
|
|
2265
|
+
for (const name of stalePluginSkillNames) {
|
|
2266
|
+
await ensureDir(path.join(doctorGlobalHome, '.agents', 'skills', name));
|
|
2267
|
+
await writeTextAtomic(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
2268
|
+
}
|
|
2269
|
+
const doctorGlobalRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
|
|
2270
|
+
cwd: doctorGlobalTmp,
|
|
2271
|
+
env: { HOME: doctorGlobalHome, SKS_DISABLE_UPDATE_CHECK: '1' },
|
|
2272
|
+
timeoutMs: 30000,
|
|
2273
|
+
maxOutputBytes: 1024 * 1024
|
|
2274
|
+
});
|
|
2275
|
+
if (doctorGlobalRepair.code !== 0) throw new Error(`selftest: doctor --fix global skill repair exited ${doctorGlobalRepair.code}: ${doctorGlobalRepair.stderr}`);
|
|
2276
|
+
const doctorGlobalRepairJson = JSON.parse(doctorGlobalRepair.stdout || '{}');
|
|
2277
|
+
const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
|
|
2278
|
+
if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
|
|
2279
|
+
assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
|
|
2280
|
+
if (hasTopLevelCodexModeLock(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair left top-level model_reasoning_effort lock that can hide Codex App plugin UI');
|
|
2281
|
+
if (missingGeneratedCodexAppFeatureFlags(doctorGlobalCodexConfig).length || hasDeprecatedCodexHooksFeatureFlag(doctorGlobalCodexConfig) || !hasResearchProfileConfig(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair did not restore Codex App feature flags and Research xhigh profiles');
|
|
2282
|
+
for (const name of stalePluginSkillNames) {
|
|
2283
|
+
if (await exists(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor --fix did not remove global generated ${name} plugin shadow skill`);
|
|
2284
|
+
}
|
|
2285
|
+
const doctorGlobalRemoved = doctorGlobalRepairJson.repair?.global_skills?.removed_stale_generated_skills || [];
|
|
2286
|
+
for (const name of stalePluginSkillNames) {
|
|
2287
|
+
if (!doctorGlobalRemoved.includes(`.agents/skills/${name}`)) throw new Error(`selftest: doctor --fix did not report global ${name} plugin shadow cleanup`);
|
|
2288
|
+
}
|
|
2289
|
+
const conflictTmp = tmpdir();
|
|
2290
|
+
await ensureDir(path.join(conflictTmp, '.omx'));
|
|
2291
|
+
const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
|
|
2292
|
+
if (!conflictScan.hard_block || !formatHarnessConflictReport(conflictScan).includes('GPT-5.5')) throw new Error('selftest: OMX conflict did not block with cleanup prompt');
|
|
2293
|
+
const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2294
|
+
if (postinstallConflict.code !== 0) throw new Error('selftest: postinstall conflict notice should not make npm install fail');
|
|
2295
|
+
const postinstallConflictOutput = String(`${postinstallConflict.stdout}\n${postinstallConflict.stderr}`);
|
|
2296
|
+
if (!postinstallConflictOutput.includes('SKS setup is blocked') || postinstallConflictOutput.includes('Cleanup prompt:')) throw new Error('selftest: postinstall conflict notice did not stay informational');
|
|
2297
|
+
const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2298
|
+
if (postinstallConflictPrompt.code !== 0 || !String(postinstallConflictPrompt.stdout || '').includes('Goal: completely remove the conflicting Codex harnesses')) throw new Error('selftest: conflict prompt');
|
|
2299
|
+
const postinstallSetupTmp = tmpdir();
|
|
2300
|
+
await writeJsonAtomic(path.join(postinstallSetupTmp, 'package.json'), { name: 'postinstall-setup-smoke', version: '0.0.0' });
|
|
2301
|
+
await ensureDir(path.join(postinstallSetupTmp, '.git'));
|
|
2302
|
+
const postinstallSetupHome = path.join(postinstallSetupTmp, 'home');
|
|
2303
|
+
for (const name of stalePluginSkillNames) {
|
|
2304
|
+
await ensureDir(path.join(postinstallSetupHome, '.agents', 'skills', name));
|
|
2305
|
+
await writeTextAtomic(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
2306
|
+
}
|
|
2307
|
+
const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2308
|
+
if (postinstallSetup.code !== 0) throw new Error(`selftest: postinstall setup exited ${postinstallSetup.code}: ${postinstallSetup.stderr}`);
|
|
2309
|
+
if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: postinstall installed deprecated agent-team fallback skill');
|
|
2310
|
+
if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest: postinstall bootstrap');
|
|
2311
|
+
if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest: postinstall did not create project hooks during automatic bootstrap');
|
|
2312
|
+
const postinstallSetupConfig = await safeReadText(path.join(postinstallSetupTmp, '.codex', 'config.toml'));
|
|
2313
|
+
if (missingGeneratedCodexAppFeatureFlags(postinstallSetupConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallSetupConfig)) throw new Error('selftest: postinstall flags');
|
|
2314
|
+
assertCodexWarn(postinstallSetupConfig, 'postinstall project config');
|
|
2315
|
+
if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest: postinstall did not report automatic global Codex App skills');
|
|
2316
|
+
if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest: postinstall did not report stale first-party plugin shadow cleanup');
|
|
2317
|
+
const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
|
|
2318
|
+
if (postinstallSetupManifest.installation?.scope !== 'global') throw new Error('selftest: postinstall automatic bootstrap did not use global install scope');
|
|
2319
|
+
if (postinstallSetupManifest.design_system_ssot?.authority_file !== DESIGN_SYSTEM_SSOT.authority_file || postinstallSetupManifest.design_system_ssot?.builder_prompt !== DESIGN_SYSTEM_SSOT.builder_prompt) throw new Error('selftest: design SSOT');
|
|
2320
|
+
if (!postinstallSetupManifest.recommended_design_references?.some((entry) => entry.id === 'getdesign' && entry.codex_skill_install === GETDESIGN_REFERENCE.codex_skill_install)) throw new Error('selftest: getdesign ref');
|
|
2321
|
+
if (!postinstallSetupManifest.recommended_design_references?.some((entry) => entry.id === AWESOME_DESIGN_MD_REFERENCE.id && entry.url === AWESOME_DESIGN_MD_REFERENCE.url)) throw new Error('selftest: design refs');
|
|
2322
|
+
for (const rel of ['.agents/skills/team/SKILL.md', '.codex/config.toml', '.codex/hooks.json', '.sneakoscope/harness-guard.json', '.codex/SNEAKOSCOPE.md', 'AGENTS.md', '.gitignore']) {
|
|
2323
|
+
if (!(await exists(path.join(postinstallSetupTmp, rel)))) throw new Error(`selftest: automatic postinstall bootstrap did not create ${rel}`);
|
|
2324
|
+
}
|
|
2325
|
+
const postinstallSetupGitignore = await safeReadText(path.join(postinstallSetupTmp, '.gitignore'));
|
|
2326
|
+
if (!postinstallSetupGitignore.includes('.sneakoscope/') || !postinstallSetupGitignore.includes('.codex/') || !postinstallSetupGitignore.includes('.agents/') || !postinstallSetupGitignore.includes('AGENTS.md')) throw new Error('selftest: postinstall gitignore');
|
|
2327
|
+
for (const skillName of new Set(DOLLAR_SKILL_NAMES)) {
|
|
2328
|
+
if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', skillName, 'SKILL.md')))) throw new Error(`selftest: postinstall global ${skillName} skill not installed`);
|
|
2329
|
+
}
|
|
2330
|
+
for (const name of stalePluginSkillNames) {
|
|
2331
|
+
if (await exists(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: postinstall global skills shadow the first-party ${name} plugin`);
|
|
2332
|
+
}
|
|
2333
|
+
if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', 'getdesign-reference', 'SKILL.md')))) throw new Error('selftest: postinstall global getdesign-reference skill not installed');
|
|
2334
|
+
await selftestSksShimRepair();
|
|
2335
|
+
const oldNoBootstrap = process.env.SKS_POSTINSTALL_NO_BOOTSTRAP;
|
|
2336
|
+
process.env.SKS_POSTINSTALL_NO_BOOTSTRAP = '1';
|
|
2337
|
+
const noBootstrapDecision = await postinstallBootstrapDecision(postinstallSetupTmp);
|
|
2338
|
+
if (oldNoBootstrap === undefined) delete process.env.SKS_POSTINSTALL_NO_BOOTSTRAP;
|
|
2339
|
+
else process.env.SKS_POSTINSTALL_NO_BOOTSTRAP = oldNoBootstrap;
|
|
2340
|
+
if (noBootstrapDecision.run || noBootstrapDecision.reason !== 'SKS_POSTINSTALL_NO_BOOTSTRAP=1') throw new Error('selftest: postinstall bootstrap opt-out decision');
|
|
2341
|
+
const postinstallNoMarkerTmp = tmpdir();
|
|
2342
|
+
const postinstallNoMarkerHome = path.join(postinstallNoMarkerTmp, 'home');
|
|
2343
|
+
const postinstallNoMarkerCwd = path.join(postinstallNoMarkerTmp, 'cwd');
|
|
2344
|
+
const postinstallNoMarkerGlobalRoot = path.join(postinstallNoMarkerTmp, 'global-root');
|
|
2345
|
+
await ensureDir(postinstallNoMarkerCwd);
|
|
2346
|
+
const postinstallNoMarker = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallNoMarkerCwd, env: { INIT_CWD: postinstallNoMarkerCwd, HOME: postinstallNoMarkerHome, SKS_GLOBAL_ROOT: postinstallNoMarkerGlobalRoot, SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2347
|
+
if (postinstallNoMarker.code !== 0) throw new Error(`selftest: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
|
|
2348
|
+
if (!String(postinstallNoMarker.stdout || '').includes('no project marker found; auto-running global SKS runtime bootstrap')) throw new Error('selftest: no-marker bootstrap');
|
|
2349
|
+
if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: no-marker postinstall did not bootstrap global runtime root');
|
|
2350
|
+
const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
|
|
2351
|
+
if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest: no-marker flags');
|
|
2352
|
+
assertCodexWarn(postinstallNoMarkerConfig, 'postinstall global runtime config');
|
|
2353
|
+
if (!hasResearchProfileConfig(postinstallNoMarkerConfig)) throw new Error('selftest: postinstall global runtime config did not restore Research xhigh profiles');
|
|
2354
|
+
if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest: no-marker postinstall polluted install cwd');
|
|
2355
|
+
if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest: global runtime bootstrap without project git wrote shared .gitignore');
|
|
2356
|
+
const bootstrapJsonTmp = tmpdir();
|
|
2357
|
+
await writeJsonAtomic(path.join(bootstrapJsonTmp, 'package.json'), { name: 'bootstrap-json-smoke', version: '0.0.0' });
|
|
2358
|
+
const bootstrapJson = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'bootstrap', '--json'], { cwd: bootstrapJsonTmp, env: { HOME: path.join(bootstrapJsonTmp, 'home'), SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2359
|
+
const bootstrapResult = JSON.parse(bootstrapJson.stdout);
|
|
2360
|
+
if (!bootstrapResult.project_setup?.ok || typeof bootstrapResult.ready !== 'boolean') throw new Error('selftest: bootstrap json did not report project setup and ready boolean');
|
|
2361
|
+
const depsCheck = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'deps', 'check', '--json'], { cwd: bootstrapJsonTmp, env: { HOME: path.join(bootstrapJsonTmp, 'home') }, timeoutMs: 20000, maxOutputBytes: 256 * 1024 });
|
|
2362
|
+
const depsResult = JSON.parse(depsCheck.stdout);
|
|
2363
|
+
if (!depsResult.node?.ok || !('tmux' in depsResult) || !('homebrew' in depsResult)) throw new Error('selftest: deps check json missing expected fields');
|
|
2364
|
+
const globalCwd = tmpdir();
|
|
2365
|
+
const globalRuntimeRoot = path.join(tmpdir(), 'sks-global-root');
|
|
2366
|
+
const globalRootProbe = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'root', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2367
|
+
const globalRootResult = JSON.parse(globalRootProbe.stdout);
|
|
2368
|
+
if (globalRootResult.mode !== 'global' || globalRootResult.active_root !== globalRuntimeRoot || globalRootResult.project_root !== null) throw new Error('selftest: global root');
|
|
2369
|
+
const globalPipeline = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'pipeline', 'status', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2370
|
+
const globalPipelineResult = JSON.parse(globalPipeline.stdout);
|
|
2371
|
+
if (globalPipelineResult.root !== globalRuntimeRoot) throw new Error('selftest: global pipeline root');
|
|
2372
|
+
const globalTeam = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'team', 'global path smoke', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
|
|
2373
|
+
const globalTeamResult = JSON.parse(globalTeam.stdout);
|
|
2374
|
+
if (!String(globalTeamResult.mission_dir || '').startsWith(path.join(globalRuntimeRoot, '.sneakoscope', 'missions')) || !(await exists(path.join(globalRuntimeRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: global team root');
|
|
2375
|
+
if (await exists(path.join(globalCwd, '.sneakoscope'))) throw new Error('selftest: global runtime command polluted the caller cwd with .sneakoscope');
|
|
2376
|
+
const madProfilePath = path.join(tmp, 'mad-codex-config.toml');
|
|
2377
|
+
const madProfile = await enableMadHighProfile({ configPath: madProfilePath });
|
|
2378
|
+
const madProfileText = await safeReadText(madProfilePath);
|
|
2379
|
+
if (madProfile.profile_name !== 'sks-mad-high' || !madProfileText.includes('sandbox_mode = "danger-full-access"') || !madProfileText.includes('approval_policy = "never"') || !madProfileText.includes('approvals_reviewer = "auto_review"') || !madProfileText.includes('service_tier = "fast"') || !madProfile.launch_args.includes('--sandbox') || !madProfile.launch_args.includes('danger-full-access') || !madProfile.launch_args.includes('--ask-for-approval') || !madProfile.launch_args.includes('never') || !madProfileText.includes('model_reasoning_effort = "high"') || !madProfileText.includes('unrequested fallback implementation code')) throw new Error('selftest: MAD high profile is not Codex full-access high with fallback-code guard');
|
|
2380
|
+
if (!isMadHighLaunch(['--mad', '--high']) || isMadHighLaunch(['db', '--mad'])) throw new Error('selftest: MAD high launch flag parsing is not top-level only');
|
|
2381
|
+
const workspacePlan = { session: 'sks-mad-selftest', root: tmp, codexArgs: madProfile.launch_args };
|
|
2382
|
+
const tmuxSyntax = runTmuxLaunchPlanSyntaxCheck(workspacePlan);
|
|
2383
|
+
if (!tmuxSyntax.ok || !tmuxSyntax.command.includes('tmux attach-session -t sks-mad-selftest')) throw new Error('selftest: MAD tmux attach plan is not stable by session name');
|
|
2384
|
+
const tmuxOpenArgs = buildTmuxOpenArgs(workspacePlan);
|
|
2385
|
+
if (tmuxOpenArgs.join(' ') !== 'attach-session -t sks-mad-selftest') throw new Error('selftest: MAD tmux attach args are not stable by session name');
|
|
2386
|
+
const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2387
|
+
if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" -c model_reasoning_effort="high"') throw new Error('selftest: default sks tmux launch is not fast-high');
|
|
2388
|
+
const forcedModelPlan = await buildTmuxLaunchPlan({ root: tmp, env: { SKS_CODEX_MODEL: 'gpt-5.0-forbidden', SKS_CODEX_FAST_HIGH: '0', SKS_CODEX_REASONING: 'medium' }, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2389
|
+
if (forcedModelPlan.codexArgs.includes('gpt-5.0-forbidden') || forcedModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" -c model_reasoning_effort="medium"') throw new Error('selftest: sks tmux launch allowed a non-GPT-5.5 model override');
|
|
2390
|
+
const explicitBadModelPlan = await buildTmuxLaunchPlan({ root: tmp, codexArgs: ['--profile', 'legacy-forbidden-model', '--model', 'gpt-5.0-forbidden', '-c', 'model="gpt-5.0-forbidden"', '-c', 'model_reasoning_effort="low"'], tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2391
|
+
if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.0-forbidden') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" --profile legacy-forbidden-model -c model_reasoning_effort="low"') throw new Error('selftest: explicit tmux model override was not forced back to GPT-5.5');
|
|
2392
|
+
const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-forbidden-model', extraArgs: ['--model=gpt-5.0-forbidden', '--config', 'model = "gpt-5.0-forbidden"', '-c', 'model_reasoning_effort="medium"'] });
|
|
2393
|
+
if (codexExecArgs.join(' ').includes('gpt-5.0-forbidden') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.0-forbidden')) throw new Error('selftest: codex exec args allowed a non-GPT-5.5 model override');
|
|
2394
|
+
const researchExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'research exec selftest', profile: 'sks-research', extraArgs: ['-c', 'service_tier="fast"', '-c', 'model_reasoning_effort="xhigh"'] });
|
|
2395
|
+
const researchExecJoined = researchExecArgs.join(' ');
|
|
2396
|
+
if (!researchExecJoined.includes('--profile sks-research') || !researchExecJoined.includes('--model gpt-5.5') || !researchExecJoined.includes('service_tier="fast"') || !researchExecJoined.includes('model_reasoning_effort="xhigh"')) throw new Error('selftest: research exec args did not force GPT-5.5 fast xhigh execution');
|
|
2397
|
+
await selftestCodexLb(tmp);
|
|
2398
|
+
if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: MAD tmux launch does not auto-attach in an interactive terminal');
|
|
2399
|
+
if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: MAD tmux json mode should not auto-attach');
|
|
2400
|
+
if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: MAD tmux --no-attach should remain print-only');
|
|
2401
|
+
if (shouldAutoAttachTmux(['--mad'], { SKS_TMUX_NO_AUTO_ATTACH: '1' }, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: SKS_TMUX_NO_AUTO_ATTACH should disable tmux auto-attach');
|
|
2402
|
+
if (!isTmuxShellSession({ TMUX: '/tmp/tmux-501/default,1,0' })) throw new Error('selftest: tmux shell session env was not detected');
|
|
2403
|
+
if (tmuxStatusKind({ ok: false, bin: null }) !== 'missing') throw new Error('selftest: missing tmux was not labeled missing');
|
|
2404
|
+
const bareDefault = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs')], {
|
|
2405
|
+
cwd: globalCwd,
|
|
2406
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: PACKAGE_VERSION, PATH: '' },
|
|
2407
|
+
timeoutMs: 15000,
|
|
2408
|
+
maxOutputBytes: 64 * 1024
|
|
2409
|
+
});
|
|
2410
|
+
if (bareDefault.code !== 1 || !String(bareDefault.stderr || '').includes('SKS tmux launch blocked') || String(bareDefault.stdout || '').includes('Usage:')) throw new Error('selftest: bare sks did not route to default tmux launch');
|
|
2411
|
+
const fakeCodexBin = path.join(tmp, 'fake-codex-bin');
|
|
2412
|
+
await ensureDir(fakeCodexBin);
|
|
2413
|
+
const fakeCodexPath = path.join(fakeCodexBin, 'codex');
|
|
2414
|
+
await writeTextAtomic(fakeCodexPath, '#!/bin/sh\necho "codex-cli 0.1.0"\n');
|
|
2415
|
+
await fsp.chmod(fakeCodexPath, 0o755);
|
|
2416
|
+
const codexUpdatePrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs')], {
|
|
2417
|
+
cwd: globalCwd,
|
|
2418
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: PACKAGE_VERSION, SKS_NPM_VIEW__OPENAI_CODEX_VERSION: '99.0.0', PATH: fakeCodexBin },
|
|
2419
|
+
timeoutMs: 15000,
|
|
2420
|
+
maxOutputBytes: 64 * 1024
|
|
2421
|
+
});
|
|
2422
|
+
if (!String(codexUpdatePrompt.stdout || '').includes('Codex CLI update available: 0.1.0 -> 99.0.0') || String(codexUpdatePrompt.stdout || '').includes('Usage:')) throw new Error('selftest: bare sks did not recommend Codex CLI update before tmux launch');
|
|
2423
|
+
const openClawAutoBin = path.join(tmp, 'openclaw-auto-bin');
|
|
2424
|
+
await ensureDir(openClawAutoBin);
|
|
2425
|
+
const openClawCodexPath = path.join(openClawAutoBin, 'codex');
|
|
2426
|
+
await writeTextAtomic(openClawCodexPath, '#!/bin/sh\necho "codex-cli 0.1.0"\n');
|
|
2427
|
+
await writeTextAtomic(path.join(openClawAutoBin, 'npm'), '#!/bin/sh\nDIR="${0%/*}"\nif [ "$1" = "i" ]; then\n printf \'#!/bin/sh\\necho "codex-cli 99.0.0"\\n\' > "$DIR/codex"\n chmod +x "$DIR/codex"\n exit 0\nfi\necho "unexpected npm $*" >&2\nexit 1\n');
|
|
2428
|
+
await fsp.chmod(openClawCodexPath, 0o755);
|
|
2429
|
+
await fsp.chmod(path.join(openClawAutoBin, 'npm'), 0o755);
|
|
2430
|
+
const openClawAutoUpdate = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs')], {
|
|
2431
|
+
cwd: globalCwd,
|
|
2432
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, SKS_OPENCLAW: '1', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: PACKAGE_VERSION, SKS_NPM_VIEW__OPENAI_CODEX_VERSION: '99.0.0', PATH: openClawAutoBin },
|
|
2433
|
+
timeoutMs: 15000,
|
|
2434
|
+
maxOutputBytes: 64 * 1024
|
|
2435
|
+
});
|
|
2436
|
+
if (!String(openClawAutoUpdate.stdout || '').includes('Codex CLI ready: 0.1.0 -> codex-cli 99.0.0')) throw new Error('selftest: OpenClaw mode did not auto-approve Codex CLI update before tmux launch');
|
|
2437
|
+
const remoteControlBin = path.join(tmp, 'remote-control-bin');
|
|
2438
|
+
await ensureDir(remoteControlBin);
|
|
2439
|
+
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then shift; for arg in "$@"; do if [ "$arg" = "--model" ]; then echo "remote-control rejects --model" >&2; exit 64; fi; done; echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2440
|
+
await fsp.chmod(path.join(remoteControlBin, 'codex'), 0o755);
|
|
2441
|
+
const remoteControlStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run', '--json'], {
|
|
2442
|
+
cwd: globalCwd,
|
|
2443
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
|
|
2444
|
+
timeoutMs: 15000,
|
|
2445
|
+
maxOutputBytes: 64 * 1024
|
|
2446
|
+
});
|
|
2447
|
+
if (remoteControlStatus.code !== 0) throw new Error(`selftest: Codex remote-control status exited ${remoteControlStatus.code}: ${remoteControlStatus.stderr}`);
|
|
2448
|
+
const remoteControlJson = JSON.parse(remoteControlStatus.stdout);
|
|
2449
|
+
if (!remoteControlJson.ok || remoteControlJson.min_version !== '0.130.0' || !String(remoteControlJson.command || '').includes('remote-control')) throw new Error('selftest: Codex remote-control status did not report 0.130.0 readiness');
|
|
2450
|
+
const remoteControlLaunch = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--', '--model', 'gpt-5.0-forbidden', '-c', 'model="gpt-5.0-forbidden"', '--example'], {
|
|
2451
|
+
cwd: globalCwd,
|
|
2452
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
|
|
2453
|
+
timeoutMs: 15000,
|
|
2454
|
+
maxOutputBytes: 64 * 1024
|
|
2455
|
+
});
|
|
2456
|
+
const remoteControlLaunchText = `${remoteControlLaunch.stdout}\n${remoteControlLaunch.stderr}`;
|
|
2457
|
+
if (remoteControlLaunch.code !== 0 || remoteControlLaunchText.includes('gpt-5.0-forbidden') || remoteControlLaunchText.includes('--model') || !remoteControlLaunchText.includes('-c model="gpt-5.5"')) throw new Error('selftest: Codex remote-control passthrough did not force GPT-5.5 with config syntax');
|
|
2458
|
+
const remoteControlOldBin = path.join(tmp, 'remote-control-old-bin');
|
|
2459
|
+
await ensureDir(remoteControlOldBin);
|
|
2460
|
+
await writeTextAtomic(path.join(remoteControlOldBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.129.0"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2461
|
+
await fsp.chmod(path.join(remoteControlOldBin, 'codex'), 0o755);
|
|
2462
|
+
const remoteControlOldStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run'], {
|
|
2463
|
+
cwd: globalCwd,
|
|
2464
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlOldBin },
|
|
2465
|
+
timeoutMs: 15000,
|
|
2466
|
+
maxOutputBytes: 64 * 1024
|
|
2467
|
+
});
|
|
2468
|
+
if (remoteControlOldStatus.code !== 1 || !String(`${remoteControlOldStatus.stdout}\n${remoteControlOldStatus.stderr}`).includes('Codex CLI 0.130.0+')) throw new Error('selftest: Codex remote-control did not block older Codex CLI versions');
|
|
2469
|
+
if (!COMMAND_CATALOG.find((entry) => entry.name === 'codex-app')?.usage.includes('remote-control')) throw new Error('selftest: codex-app command catalog does not advertise remote-control');
|
|
2470
|
+
const guardBlocked = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
|
|
2471
|
+
if (guardBlocked.action !== 'block') throw new Error('selftest: harness guard allowed skill tampering');
|
|
2472
|
+
const setupBlocked = await checkHarnessModification(tmp, { command: 'sks setup --force' });
|
|
2473
|
+
if (setupBlocked.action !== 'block') throw new Error('selftest: harness guard allowed setup maintenance command');
|
|
2474
|
+
const appEditAllowed = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: src/app.js\n+ok\n' });
|
|
2475
|
+
if (appEditAllowed.action === 'block') throw new Error('selftest: harness guard blocked app source edit');
|
|
2476
|
+
const sourceEditAllowed = await checkHarnessModification(packageRoot(), { tool_name: 'apply_patch', command: '*** Update File: src/core/init.mjs\n+ok\n' });
|
|
2477
|
+
if (sourceEditAllowed.action === 'block' || !(await isHarnessSourceProject(packageRoot()))) throw new Error('selftest: harness source exception not honored');
|
|
2478
|
+
const defaultHooks = await readJson(path.join(tmp, '.codex', 'hooks.json'));
|
|
2479
|
+
if (defaultHooks.hooks.PreToolUse[0].hooks[0].command !== 'sks hook pre-tool') throw new Error('selftest: global install hook command changed');
|
|
2480
|
+
const sharedHooksTmp = tmpdir();
|
|
2481
|
+
await ensureDir(path.join(sharedHooksTmp, '.codex'));
|
|
2482
|
+
await writeJsonAtomic(path.join(sharedHooksTmp, '.codex', 'hooks.json'), {
|
|
2483
|
+
hooks: {
|
|
2484
|
+
UserPromptSubmit: [
|
|
2485
|
+
{ hooks: [{ type: 'command', command: 'node ./old/sks.mjs hook user-prompt-submit' }] },
|
|
2486
|
+
{ hooks: [{ type: 'command', command: 'node ./user-hook.mjs' }] }
|
|
2487
|
+
],
|
|
2488
|
+
Stop: [{ hooks: [{ type: 'command', command: 'node ./user-stop.mjs' }] }]
|
|
2489
|
+
},
|
|
2490
|
+
user_key: true
|
|
2491
|
+
});
|
|
2492
|
+
await initProject(sharedHooksTmp, {});
|
|
2493
|
+
const sharedHooks = await readJson(path.join(sharedHooksTmp, '.codex', 'hooks.json'));
|
|
2494
|
+
if (!sharedHooks.user_key) throw new Error('selftest: hooks merge dropped root metadata');
|
|
2495
|
+
if (!sharedHooks.hooks.UserPromptSubmit.some((entry) => entry.hooks?.some((hook) => hook.command === 'node ./user-hook.mjs'))) throw new Error('selftest: hooks merge dropped user hook');
|
|
2496
|
+
if (JSON.stringify(sharedHooks).includes('node ./old/sks.mjs hook user-prompt-submit')) throw new Error('selftest: hooks merge kept stale SKS hook');
|
|
2497
|
+
if (sharedHooks.hooks.UserPromptSubmit.filter((entry) => entry.hooks?.some((hook) => hook.command === 'sks hook user-prompt-submit')).length !== 1) throw new Error('selftest: hooks merge did not install exactly one SKS prompt hook');
|
|
2498
|
+
const absoluteHookTmp = tmpdir();
|
|
2499
|
+
await initProject(absoluteHookTmp, { globalCommand: '/usr/local/bin/sks' });
|
|
2500
|
+
const absoluteHooks = await readJson(path.join(absoluteHookTmp, '.codex', 'hooks.json'));
|
|
2501
|
+
if (absoluteHooks.hooks.PreToolUse[0].hooks[0].command !== '/usr/local/bin/sks hook pre-tool') throw new Error('selftest: absolute global hook command missing');
|
|
2502
|
+
const projectScopeTmp = tmpdir();
|
|
2503
|
+
await initProject(projectScopeTmp, { installScope: 'project' });
|
|
2504
|
+
const projectHooks = await readJson(path.join(projectScopeTmp, '.codex', 'hooks.json'));
|
|
2505
|
+
if (projectHooks.hooks.PreToolUse[0].hooks[0].command !== 'node ./node_modules/sneakoscope/bin/sks.mjs hook pre-tool') throw new Error('selftest: project install hook command missing');
|
|
2506
|
+
const sourceHookTmp = tmpdir();
|
|
2507
|
+
await writeJsonAtomic(path.join(sourceHookTmp, 'package.json'), { name: 'sneakoscope', version: '0.0.0' });
|
|
2508
|
+
await ensureDir(path.join(sourceHookTmp, 'bin'));
|
|
2509
|
+
await ensureDir(path.join(sourceHookTmp, 'src', 'core'));
|
|
2510
|
+
await writeTextAtomic(path.join(sourceHookTmp, 'bin', 'sks.mjs'), '#!/usr/bin/env node\n');
|
|
2511
|
+
await writeTextAtomic(path.join(sourceHookTmp, 'src', 'core', 'init.mjs'), '');
|
|
2512
|
+
await writeTextAtomic(path.join(sourceHookTmp, 'src', 'core', 'hooks-runtime.mjs'), '');
|
|
2513
|
+
await initProject(sourceHookTmp, { installScope: 'global', globalCommand: '/usr/local/bin/sks' });
|
|
2514
|
+
const sourceHooks = await readJson(path.join(sourceHookTmp, '.codex', 'hooks.json'));
|
|
2515
|
+
if (sourceHooks.hooks.PreToolUse[0].hooks[0].command !== 'node ./bin/sks.mjs hook pre-tool') throw new Error('selftest: source repo hook command should use local bin');
|
|
2516
|
+
const versionTmp = tmpdir();
|
|
2517
|
+
await runProcess('git', ['init'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2518
|
+
await runProcess('git', ['config', 'user.email', 'sks-selftest@example.invalid'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2519
|
+
await runProcess('git', ['config', 'user.name', 'SKS Selftest'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2520
|
+
await writeJsonAtomic(path.join(versionTmp, 'package.json'), { name: 'sks-version-selftest', version: '0.1.0' });
|
|
2521
|
+
await writeJsonAtomic(path.join(versionTmp, 'package-lock.json'), { name: 'sks-version-selftest', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'sks-version-selftest', version: '0.1.0' } } });
|
|
2522
|
+
await runProcess('git', ['add', 'package.json', 'package-lock.json'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2523
|
+
await runProcess('git', ['commit', '--no-verify', '-m', 'initial'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2524
|
+
await writeTextAtomic(path.join(versionTmp, '.git', 'hooks', 'pre-commit'), '#!/bin/sh\nexit 0\n');
|
|
2525
|
+
await initProject(versionTmp, {});
|
|
2526
|
+
const versionStatus = await versioningStatus(versionTmp);
|
|
2527
|
+
if (!versionStatus.ok || versionStatus.enabled || versionStatus.hook_installed) throw new Error('selftest: versioning hook should stay disabled after init');
|
|
2528
|
+
let versionHookText = await safeReadText(versionStatus.hook_path);
|
|
2529
|
+
if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: init installed versioning pre-commit');
|
|
2530
|
+
const versionHookAttempt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'versioning', 'hook', '--json'], { cwd: versionTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2531
|
+
if (versionHookAttempt.code === 0 || !versionHookAttempt.stdout.includes('pre_commit_hooks_unsupported')) throw new Error('selftest: versioning hook command should be blocked');
|
|
2532
|
+
const versionBlockedStatus = await versioningStatus(versionTmp);
|
|
2533
|
+
if (versionBlockedStatus.enabled || versionBlockedStatus.hook_installed) throw new Error('selftest: blocked versioning hook changed status');
|
|
2534
|
+
versionHookText = await safeReadText(versionBlockedStatus.hook_path);
|
|
2535
|
+
if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: blocked versioning hook installed pre-commit command');
|
|
2536
|
+
await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), '# Changelog\n\n## [Unreleased]\n\n## [0.1.0] - 2026-05-08\n\n### Fixed\n\n- Initial version selftest fixture.\n');
|
|
2537
|
+
await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
|
|
2538
|
+
await runProcess('git', ['add', 'README.md', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2539
|
+
const preCommitVerify = await runVersionPreCommit(versionTmp);
|
|
2540
|
+
if (!preCommitVerify.ok || !preCommitVerify.skipped || preCommitVerify.reason !== 'disabled_by_policy') throw new Error('selftest: pre-commit path should stay disabled by policy');
|
|
2541
|
+
const firstVersionBump = await bumpProjectVersion(versionTmp);
|
|
2542
|
+
if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest: first version bump did not advance patch version');
|
|
2543
|
+
const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
|
|
2544
|
+
const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
|
|
2545
|
+
const bumpedChangelog = await safeReadText(path.join(versionTmp, 'CHANGELOG.md'));
|
|
2546
|
+
if (bumpedPackage.version !== '0.1.1' || bumpedLock.version !== '0.1.1' || bumpedLock.packages[''].version !== '0.1.1') throw new Error('selftest: package lock versions not synced');
|
|
2547
|
+
if (!bumpedChangelog.includes('## [0.1.1]') || !bumpedChangelog.includes('explicit SKS version bump')) throw new Error('selftest: version bump did not sync changelog section');
|
|
2548
|
+
const firstCached = await runProcess('git', ['diff', '--cached', '--name-only'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2549
|
+
if (!firstCached.stdout.includes('package.json') || !firstCached.stdout.includes('package-lock.json') || !firstCached.stdout.includes('CHANGELOG.md')) throw new Error('selftest: version files not staged');
|
|
2550
|
+
await runProcess('git', ['commit', '--no-verify', '-m', 'first versioned commit'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2551
|
+
await writeJsonAtomic(versionStatus.state_path, { schema_version: 1, last_version: '0.1.5', updated_at: nowIso(), pid: process.pid, changed: true });
|
|
2552
|
+
await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), 'collision selftest\n');
|
|
2553
|
+
await runProcess('git', ['add', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2554
|
+
const collisionBump = await bumpProjectVersion(versionTmp);
|
|
2555
|
+
if (!collisionBump.ok || collisionBump.version !== '0.1.6') throw new Error('selftest: version collision state did not bump above last seen version');
|
|
2556
|
+
const localOnlyTmp = tmpdir();
|
|
2557
|
+
await ensureDir(path.join(localOnlyTmp, '.git'));
|
|
2558
|
+
await writeTextAtomic(path.join(localOnlyTmp, 'AGENTS.md'), 'existing local rules\n');
|
|
2559
|
+
await initProject(localOnlyTmp, { localOnly: true });
|
|
2560
|
+
const localExclude = await safeReadText(path.join(localOnlyTmp, '.git', 'info', 'exclude'));
|
|
2561
|
+
if (!localExclude.includes('.codex/') || !localExclude.includes('AGENTS.md')) throw new Error('selftest: local-only git excludes missing');
|
|
2562
|
+
if (await exists(path.join(localOnlyTmp, '.gitignore'))) throw new Error('selftest: local-only wrote shared .gitignore');
|
|
2563
|
+
const localAgents = await safeReadText(path.join(localOnlyTmp, 'AGENTS.md'));
|
|
2564
|
+
if (localAgents.trim() !== 'existing local rules') throw new Error('selftest: local-only modified existing AGENTS.md');
|
|
2565
|
+
const localManifest = await readJson(path.join(localOnlyTmp, '.sneakoscope', 'manifest.json'));
|
|
2566
|
+
if (!localManifest.git?.local_only) throw new Error('selftest: local-only manifest missing');
|
|
2567
|
+
const gitignoreTmp = tmpdir();
|
|
2568
|
+
await writeTextAtomic(path.join(gitignoreTmp, '.gitignore'), 'node_modules/\n.sneakoscope/\n');
|
|
2569
|
+
await initProject(gitignoreTmp, {});
|
|
2570
|
+
const gitignoreText = await safeReadText(path.join(gitignoreTmp, '.gitignore'));
|
|
2571
|
+
if (!gitignoreText.includes('node_modules/') || !gitignoreText.includes('# BEGIN Sneakoscope Codex generated files') || !gitignoreText.includes('.codex/') || !gitignoreText.includes('.agents/') || !gitignoreText.includes('AGENTS.md')) throw new Error('selftest: shared .gitignore did not preserve existing entries and add SKS patterns');
|
|
2572
|
+
await initProject(gitignoreTmp, {});
|
|
2573
|
+
const gitignoreTextSecond = await safeReadText(path.join(gitignoreTmp, '.gitignore'));
|
|
2574
|
+
if ((gitignoreTextSecond.match(/BEGIN Sneakoscope Codex generated files/g) || []).length !== 1) throw new Error('selftest: shared .gitignore managed block duplicated');
|
|
2575
|
+
const managedAgentsTmp = tmpdir();
|
|
2576
|
+
await ensureDir(path.join(managedAgentsTmp, '.git'));
|
|
2577
|
+
await writeTextAtomic(path.join(managedAgentsTmp, 'AGENTS.md'), '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\nold managed rules\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n');
|
|
2578
|
+
await initProject(managedAgentsTmp, { localOnly: true });
|
|
2579
|
+
const managedAgents = await safeReadText(path.join(managedAgentsTmp, 'AGENTS.md'));
|
|
2580
|
+
if (!managedAgents.includes('TriWiki is the context-tracking SSOT') || managedAgents.includes('old managed rules')) throw new Error('selftest: local-only did not refresh managed AGENTS.md block');
|
|
2581
|
+
if (!isTransientNpmBinPath('/tmp/.npm/_npx/abc/node_modules/.bin/sks')) throw new Error('selftest: npx bin path not recognized as transient');
|
|
2582
|
+
if (!isTransientNpmBinPath('/tmp/.npm-cache/_cacache/tmp/git-cloneabc/bin/sks.mjs')) throw new Error('selftest: npm cache git clone path not recognized as transient');
|
|
2583
|
+
if (isTransientNpmBinPath('/usr/local/bin/sks')) throw new Error('selftest: stable global bin marked transient');
|
|
2584
|
+
const oldPath = process.env.PATH;
|
|
2585
|
+
const oldSksBin = process.env.SKS_BIN;
|
|
2586
|
+
const fakeNpxBin = path.join(tmp, '.npm', '_npx', 'abc', 'node_modules', '.bin');
|
|
2587
|
+
await ensureDir(fakeNpxBin);
|
|
2588
|
+
await writeJsonAtomic(path.join(fakeNpxBin, 'sks'), { fake: true });
|
|
2589
|
+
try {
|
|
2590
|
+
process.env.PATH = fakeNpxBin;
|
|
2591
|
+
delete process.env.SKS_BIN;
|
|
2592
|
+
const discovered = await discoverGlobalSksCommand();
|
|
2593
|
+
if (isTransientNpmBinPath(discovered)) throw new Error('selftest: transient npx bin selected as global command');
|
|
2594
|
+
} finally {
|
|
2595
|
+
if (oldPath === undefined) delete process.env.PATH;
|
|
2596
|
+
else process.env.PATH = oldPath;
|
|
2597
|
+
if (oldSksBin === undefined) delete process.env.SKS_BIN;
|
|
2598
|
+
else process.env.SKS_BIN = oldSksBin;
|
|
2599
|
+
}
|
|
2600
|
+
const shimTmp = tmpdir();
|
|
2601
|
+
const shimDir = path.join(shimTmp, 'bin');
|
|
2602
|
+
const shimResult = await ensureSksCommandDuringInstall({ force: true, pathEnv: shimDir, home: shimTmp, target: path.join(packageRoot(), 'bin', 'sks.mjs'), nodeBin: process.execPath });
|
|
2603
|
+
if (shimResult.status !== 'created' || !(await exists(path.join(shimDir, process.platform === 'win32' ? 'sks.cmd' : 'sks')))) throw new Error('selftest: sks command shim not created');
|
|
2604
|
+
const globalSkillsTmp = tmpdir();
|
|
2605
|
+
const globalSkillsResult = await ensureGlobalCodexSkillsDuringInstall({ force: true, home: globalSkillsTmp });
|
|
2606
|
+
if (globalSkillsResult.status !== 'installed') throw new Error(`selftest: global Codex App skills not installed: ${globalSkillsResult.status}`);
|
|
2607
|
+
const globalSkillStatus = await checkRequiredSkills(globalSkillsTmp, path.join(globalSkillsTmp, '.agents', 'skills'));
|
|
2608
|
+
if (!globalSkillStatus.ok) throw new Error(`selftest: global Codex App skills missing: ${globalSkillStatus.missing.join(', ')}`);
|
|
2609
|
+
if (await exists(path.join(globalSkillsTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest: global generated skills shadow the first-party computer-use plugin');
|
|
2610
|
+
const codexSkillMirrorExists = await exists(path.join(tmp, '.codex', 'skills', 'research-discovery', 'SKILL.md'));
|
|
2611
|
+
if (codexSkillMirrorExists) throw new Error('selftest: generated .codex/skills mirror still installed');
|
|
2612
|
+
const codexAppSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'research-discovery', 'SKILL.md'));
|
|
2613
|
+
if (!codexAppSkillExists) throw new Error('selftest: Codex App skill not installed');
|
|
2614
|
+
for (const skillName of new Set(DOLLAR_SKILL_NAMES)) {
|
|
2615
|
+
const dollarSkillExists = await exists(path.join(tmp, '.agents', 'skills', skillName, 'SKILL.md'));
|
|
2616
|
+
if (!dollarSkillExists) throw new Error(`selftest: ${skillName} skill not installed`);
|
|
2617
|
+
}
|
|
2618
|
+
if (await exists(path.join(tmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest: project generated skills shadow the first-party computer-use plugin');
|
|
2619
|
+
const promptPipelineSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'prompt-pipeline', 'SKILL.md'));
|
|
2620
|
+
if (!promptPipelineSkillExists) throw new Error('selftest: prompt pipeline skill not installed');
|
|
2621
|
+
const promptPipelineText = await safeReadText(path.join(tmp, '.agents', 'skills', 'prompt-pipeline', 'SKILL.md'));
|
|
2622
|
+
if (!promptPipelineText.includes('TriWiki context-tracking SSOT')) throw new Error('selftest: prompt pipeline missing TriWiki context-tracking SSOT');
|
|
2623
|
+
if (!promptPipelineText.includes('Codex App pipeline activation:') || !promptPipelineText.includes('sks hook user-prompt-submit') || !promptPipelineText.includes('hookSpecificOutput.additionalContext')) throw new Error('selftest: prompt pipeline missing Codex App pipeline activation fallback');
|
|
2624
|
+
const teamSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'team', 'SKILL.md'));
|
|
2625
|
+
if (!teamSkillText.includes('Codex App pipeline activation:') || !teamSkillText.includes('sks pipeline status') || !teamSkillText.includes('mission/pipeline artifacts')) throw new Error('selftest: Team skill missing pipeline activation fallback');
|
|
2626
|
+
if (!promptPipelineText.includes('before every route stage') || !promptPipelineText.includes('sks wiki refresh')) throw new Error('selftest: prompt pipeline missing per-stage TriWiki policy');
|
|
2627
|
+
if (!promptPipelineText.includes('single design decision authority') || !promptPipelineText.includes('imagegen') || !promptPipelineText.includes('getdesign-reference') || !promptPipelineText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !promptPipelineText.includes('not parallel authorities')) throw new Error('selftest: prompt pipeline missing design SSOT/source-input routing');
|
|
2628
|
+
if (!promptPipelineText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !promptPipelineText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest: prompt pipeline missing Codex App image generation policy');
|
|
2629
|
+
if (!promptPipelineText.includes('From-Chat-IMG') || !promptPipelineText.includes('Do not assume ordinary image prompts are chat captures')) throw new Error('selftest: prompt pipeline missing explicit From-Chat-IMG gating');
|
|
2630
|
+
const fromChatImgSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'from-chat-img', 'SKILL.md'));
|
|
2631
|
+
if (!fromChatImgSkillText.includes('normal Team pipeline') || !fromChatImgSkillText.includes('Codex Computer Use visual inspection') || !fromChatImgSkillText.includes(CODEX_COMPUTER_USE_ONLY_POLICY) || !fromChatImgSkillText.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT) || !fromChatImgSkillText.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT) || !fromChatImgSkillText.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT)) throw new Error('selftest: from-chat-img skill missing Team/Computer Use-only inspection checklist guidance');
|
|
2632
|
+
if (fromChatImgSkillText.includes('Computer Use/browser visual inspection')) throw new Error('selftest: from-chat-img skill still allows browser visual inspection wording');
|
|
2633
|
+
const fromChatImgSkillMeta = await safeReadText(path.join(tmp, '.agents', 'skills', 'from-chat-img', 'agents', 'openai.yaml'));
|
|
2634
|
+
if (!fromChatImgSkillMeta.includes('model_reasoning_effort: xhigh')) throw new Error('selftest: from-chat-img skill metadata is not xhigh');
|
|
2635
|
+
for (const supportSkill of ['reasoning-router', 'pipeline-runner', 'context7-docs', 'seo-geo-optimizer', 'reflection', 'design-system-builder', 'design-ui-editor', 'getdesign-reference', 'imagegen', 'image-ux-review', 'visual-review', 'ui-ux-review']) {
|
|
2636
|
+
if (!(await exists(path.join(tmp, '.agents', 'skills', supportSkill, 'SKILL.md')))) throw new Error(`selftest: ${supportSkill} skill not installed`);
|
|
2637
|
+
}
|
|
2638
|
+
const imagegenSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'imagegen', 'SKILL.md'));
|
|
2639
|
+
if (!imagegenSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !imagegenSkillText.includes('$imagegen') || !imagegenSkillText.includes('gpt-image-2') || !imagegenSkillText.includes('Direct API fallback does not satisfy SKS route evidence') || !imagegenSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest: imagegen skill missing official Codex App image generation priority');
|
|
2640
|
+
const imageUxReviewSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'image-ux-review', 'SKILL.md'));
|
|
2641
|
+
if (!imageUxReviewSkillText.includes('gpt-image-2') || !imageUxReviewSkillText.includes('$imagegen') || !imageUxReviewSkillText.includes('generated annotated review image') || !imageUxReviewSkillText.includes('Text-only screenshot critique cannot satisfy this route') || !imageUxReviewSkillText.includes(IMAGE_UX_REVIEW_GATE_ARTIFACT) || !imageUxReviewSkillText.includes(IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT) || !imageUxReviewSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY)) throw new Error('selftest: image-ux-review skill missing gpt-image-2 generated-image review gate guidance');
|
|
2642
|
+
const getdesignSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'getdesign-reference', 'SKILL.md')); if (!getdesignSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !getdesignSkillText.includes('only design decision SSOT') || !getdesignSkillText.includes('source inputs')) throw new Error('selftest: getdesign-reference skill missing design SSOT source-input guidance');
|
|
2643
|
+
const designSystemBuilderSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'design-system-builder', 'SKILL.md')); if (!designSystemBuilderSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !designSystemBuilderSkillText.includes('Fuse those inputs into one design.md SSOT') || !designSystemBuilderSkillText.includes('competing authorities')) throw new Error('selftest: design-system-builder skill missing fused design SSOT guidance');
|
|
2644
|
+
const designSysPromptText = await safeReadText(path.join(packageRoot(), 'docs', 'Design-Sys-Prompt.md')); if (!designSysPromptText.includes('Design SSOT contract') || !designSysPromptText.includes('builder prompt') || !designSysPromptText.includes('not a competing design authority')) throw new Error('selftest: Design-Sys-Prompt missing design SSOT contract');
|
|
2645
|
+
if (!(await exists(path.join(tmp, '.agents', 'skills', 'reasoning-router', 'agents', 'openai.yaml')))) throw new Error('selftest: skill metadata missing');
|
|
2646
|
+
const hookGuardPayload = JSON.stringify({ cwd: tmp, tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
|
|
2647
|
+
const hookGuardResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'pre-tool'], { cwd: tmp, input: hookGuardPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2648
|
+
const hookGuardJson = JSON.parse(hookGuardResult.stdout);
|
|
2649
|
+
if (hookGuardJson.decision !== 'block' || !String(hookGuardJson.reason || '').includes('harness guard')) throw new Error('selftest: hook did not block harness tampering');
|
|
2650
|
+
const camelHookGuardPayload = JSON.stringify({ cwd: tmp, toolName: 'apply_patch', toolInput: { command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' } });
|
|
2651
|
+
const camelHookGuardResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'pre-tool'], { cwd: tmp, input: camelHookGuardPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2652
|
+
const camelHookGuardJson = JSON.parse(camelHookGuardResult.stdout);
|
|
2653
|
+
if (camelHookGuardJson.decision !== 'block') throw new Error('selftest: hook did not block camelCase Codex tool payload');
|
|
2654
|
+
await setCurrent(tmp, { mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS', route: 'QALoop', implementation_allowed: true });
|
|
2655
|
+
const codexGitPermissionResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'git push origin dev', action: 'Codex App Git Actions Push' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2656
|
+
const codexGitPermissionJson = JSON.parse(codexGitPermissionResult.stdout);
|
|
2657
|
+
if (codexGitPermissionJson.hookSpecificOutput?.decision?.behavior === 'deny') throw new Error('selftest: Codex App git push permission was denied during no-question mode');
|
|
2658
|
+
const codexGitForcePermissionResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'git push --force origin dev', action: 'Codex App Git Actions Push' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2659
|
+
const codexGitForcePermissionJson = JSON.parse(codexGitForcePermissionResult.stdout);
|
|
2660
|
+
if (codexGitForcePermissionJson.hookSpecificOutput?.decision?.behavior !== 'deny') throw new Error('selftest: force-push permission should stay denied during no-question mode');
|
|
2661
|
+
if (new Set(DOLLAR_COMMANDS.map((c) => c.command)).size !== DOLLAR_COMMANDS.length) throw new Error('selftest: duplicate dollar commands');
|
|
2662
|
+
if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$QA-LOOP' && alias.app_skill === '$qa-loop')) throw new Error('selftest: $QA-LOOP picker skill missing');
|
|
2663
|
+
if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$Team' && alias.app_skill === '$from-chat-img')) throw new Error('selftest: $From-Chat-IMG picker skill missing');
|
|
2664
|
+
if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$Image-UX-Review' && alias.app_skill === '$visual-review')) throw new Error('selftest: $Image-UX-Review picker alias missing');
|
|
2665
|
+
if (!DOLLAR_COMMANDS.some((entry) => entry.command === '$From-Chat-IMG')) throw new Error('selftest: $From-Chat-IMG missing from dollar command list');
|
|
2666
|
+
if (!DOLLAR_COMMANDS.some((entry) => entry.command === '$Image-UX-Review') || !DOLLAR_COMMANDS.some((entry) => entry.command === '$UX-Review')) throw new Error('selftest: Image UX Review missing from dollar command list');
|
|
2667
|
+
if (DOLLAR_COMMAND_ALIASES.some((alias) => ['$agent-team', '$qaloop', '$wiki-refresh', '$wikirefresh'].includes(alias.app_skill))) throw new Error('selftest: duplicate picker aliases still present');
|
|
2668
|
+
if (routePrompt('$agent-team run specialists')) throw new Error('selftest: deprecated $agent-team route still resolved');
|
|
2669
|
+
if (routePrompt('$QA-LOOP run UI E2E')?.id !== 'QALoop' || routePrompt('$QALoop deployed smoke')) throw new Error('selftest: QA-LOOP route is not standardized to $QA-LOOP');
|
|
2670
|
+
if (routePrompt('[$qa-loop](/tmp/qa-loop/SKILL.md) localhost UI 검증, Codex Computer Use만 사용')?.id !== 'QALoop') throw new Error('selftest: markdown-linked $QA-LOOP was hijacked by heuristic routing');
|
|
2671
|
+
if (stripVisibleDecisionAnswerBlocks('qa-loop [GOAL_PRECISE: local QA QA_SCOPE: ui_e2e_only TARGET_BASE_URL: http://localhost:3000] 다시 실행').includes('GOAL_PRECISE')) throw new Error('selftest: visible decision answer block sanitizer did not remove slot payload');
|
|
2672
|
+
if (routePrompt('[$research](/tmp/research/SKILL.md) Codex Computer Use 도구 노출 문제를 QA루프 관점에서 연구')?.id !== 'Research') throw new Error('selftest: markdown-linked $Research was not treated as explicit route');
|
|
2673
|
+
if (routePrompt('$WikiRefresh 갱신')) throw new Error('selftest: deprecated $WikiRefresh route still resolved');
|
|
2674
|
+
if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest: $MAD-SKS route did not resolve');
|
|
2675
|
+
if (routePrompt('$MAD-SKS 버튼 라벨만 바꿔줘')?.id === 'DFix') throw new Error('selftest: $MAD-SKS tiny label fix incorrectly routed to DFix');
|
|
2676
|
+
if (routePrompt('$MAD-SKS $Team Supabase MCP main 작업')?.id !== 'Team') throw new Error('selftest: $MAD-SKS did not compose with $Team');
|
|
2677
|
+
if (routePrompt('$MAD-SKS $Team 버튼 라벨만 바꿔줘')?.id !== 'Team') throw new Error('selftest: $MAD-SKS $Team tiny fix did not stay on Team route');
|
|
2678
|
+
if (routePrompt('$DB Supabase 점검 $MAD-SKS')?.id !== 'DB') throw new Error('selftest: trailing $MAD-SKS changed primary route');
|
|
2679
|
+
if (routePrompt('Fix the typo in README')?.id !== 'DFix') throw new Error('selftest: inferred typo Direct Fix did not route to DFix');
|
|
2680
|
+
if (routePrompt('Update the package version to 1.2.3')?.id !== 'DFix') throw new Error('selftest: inferred package-version Direct Fix did not route to DFix');
|
|
2681
|
+
if (routePrompt('package.json version만 1.2.3으로 바꿔줘')?.id !== 'DFix') throw new Error('selftest: inferred package.json version Direct Fix did not route to DFix');
|
|
2682
|
+
if (routePrompt('How do I fix the typo in README?')?.id !== 'Answer') throw new Error('selftest: how-to Direct Fix question did not route to Answer');
|
|
2683
|
+
if (routePrompt('How do I change README title?')?.id !== 'Answer') throw new Error('selftest: how-to README title question did not route to Answer');
|
|
2684
|
+
if (routePrompt('How do I make a settings page?')?.id !== 'Answer') throw new Error('selftest: how-to create question did not route to Answer');
|
|
2685
|
+
if (routePrompt('How to create a new form component?')?.id !== 'Answer') throw new Error('selftest: how-to form component question did not route to Answer');
|
|
2686
|
+
if (routePrompt('How can I build a modal?')?.id !== 'Answer') throw new Error('selftest: how-can-I build question did not route to Answer');
|
|
2687
|
+
if (routePrompt('Make a button')?.id !== 'Team') throw new Error('selftest: create-style button work did not route to Team');
|
|
2688
|
+
if (routePrompt('Make a button that submits the form')?.id !== 'Team') throw new Error('selftest: form button creation did not route to Team');
|
|
2689
|
+
if (routePrompt('Change button to submit the form')?.id !== 'Team') throw new Error('selftest: form button behavior change did not route to Team');
|
|
2690
|
+
if (routePrompt('버튼이 폼 제출하게 바꿔줘')?.id !== 'Team') throw new Error('selftest: Korean form button behavior change did not route to Team');
|
|
2691
|
+
if (routePrompt('Can you change the button to submit the form?')?.id !== 'Team') throw new Error('selftest: polite form button behavior request did not route to Team');
|
|
2692
|
+
if (routePrompt('Change button label to Submit')?.id !== 'DFix') throw new Error('selftest: button label Direct Fix did not route to DFix');
|
|
2693
|
+
if (routePrompt('Change button text to Submit')?.id !== 'DFix') throw new Error('selftest: button text Direct Fix did not route to DFix');
|
|
2694
|
+
if (routePrompt('Can you change the button label to Save?')?.id !== 'DFix') throw new Error('selftest: polite button label Direct Fix did not route to DFix');
|
|
2695
|
+
if (routePrompt('Make README generator work')?.id !== 'Team') throw new Error('selftest: README generator implementation did not route to Team');
|
|
2696
|
+
const imageUxRoute = routePrompt('$Image-UX-Review localhost 화면 검수');
|
|
2697
|
+
if (imageUxRoute?.id !== 'ImageUXReview') throw new Error('selftest: $Image-UX-Review did not route to ImageUXReview');
|
|
2698
|
+
if (routePrompt('$UX-Review 스크린샷 gpt-image-2 콜아웃 리뷰')?.id !== 'ImageUXReview') throw new Error('selftest: $UX-Review did not route to ImageUXReview');
|
|
2699
|
+
if (routePrompt('UI UX를 gpt-image-2 이미지 생성 콜아웃으로 리뷰해줘')?.id !== 'ImageUXReview') throw new Error('selftest: image-generation UI/UX review prompt did not route to ImageUXReview');
|
|
2700
|
+
if (routeRequiresSubagents(imageUxRoute, '$Image-UX-Review localhost 화면 검수')) throw new Error('selftest: ImageUXReview route should not require subagents');
|
|
2701
|
+
if (!reflectionRequiredForRoute(imageUxRoute)) throw new Error('selftest: ImageUXReview route should require reflection');
|
|
2702
|
+
const madStandaloneTmp = tmpdir();
|
|
2703
|
+
await initProject(madStandaloneTmp, {});
|
|
2704
|
+
const madStandalonePayload = JSON.stringify({ cwd: madStandaloneTmp, prompt: '$MAD-SKS main 권한 열어줘' });
|
|
2705
|
+
const madStandaloneResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madStandaloneTmp, input: madStandalonePayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2706
|
+
if (madStandaloneResult.code !== 0) throw new Error(`selftest: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
|
|
2707
|
+
const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
|
|
2708
|
+
if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json' || madStandaloneState.normal_db_writes_allowed !== true || madStandaloneState.live_server_writes_allowed !== true || madStandaloneState.migration_apply_allowed !== true) throw new Error('selftest: standalone MAD-SKS auto-seal did not activate live full-access scoped permissions');
|
|
2709
|
+
const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
|
|
2710
|
+
const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
|
|
2711
|
+
if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest: standalone MAD-SKS did not allow ordinary DDL');
|
|
2712
|
+
const madModifierTmp = tmpdir();
|
|
2713
|
+
await initProject(madModifierTmp, {});
|
|
2714
|
+
const madModifierPayload = JSON.stringify({ cwd: madModifierTmp, prompt: '$MAD-SKS $Team 회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘' });
|
|
2715
|
+
const madModifierResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madModifierTmp, input: madModifierPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2716
|
+
if (madModifierResult.code !== 0) throw new Error(`selftest: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
|
|
2717
|
+
const madModifierState = await readJson(stateFile(madModifierTmp), {});
|
|
2718
|
+
if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json' || madModifierState.normal_db_writes_allowed !== true || madModifierState.live_server_writes_allowed !== true || madModifierState.migration_apply_allowed !== true) throw new Error('selftest: MAD-SKS Team auto-seal did not activate live full-access scoped permissions');
|
|
2719
|
+
if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest: wiki refresh text did not route to Wiki');
|
|
2720
|
+
const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
|
|
2721
|
+
if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest: Korean README implementation prompt did not route to Team by default');
|
|
2722
|
+
if (looksLikeAnswerOnlyRequest(koreanReadmeInstallPrompt)) throw new Error('selftest: Korean README implementation prompt still looked answer-only');
|
|
2723
|
+
if (routePrompt('왜 팀 커맨드 없어졌어 병렬처리까지 제대로 작업해줘')?.id !== 'Team') throw new Error('selftest: Korean Team/parallel implementation prompt did not route to Team');
|
|
2724
|
+
if (routePrompt('$From-Chat-IMG 채팅내역 이미지와 첨부 원본 이미지로 수정 작업 지시서 작성')?.id !== 'Team') throw new Error('selftest: $From-Chat-IMG did not route to Team');
|
|
2725
|
+
if (routePrompt('From-Chat-IMG 채팅내역 이미지와 원본 첨부 이미지 분석해서 작업 지시서 만들어줘')?.id !== 'Team') throw new Error('selftest: bare From-Chat-IMG signal did not route to Team');
|
|
2726
|
+
if (routePrompt('채팅 이미지랑 첨부 이미지 분석 방식 설명해줘')?.id === 'Team') throw new Error('selftest: ordinary chat-image question activated Team without From-Chat-IMG');
|
|
2727
|
+
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Team')) throw new Error('selftest: dollar-commands missing Team default routing guidance');
|
|
2728
|
+
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$From-Chat-IMG')) throw new Error('selftest: dollar-commands missing From-Chat-IMG guidance');
|
|
2729
|
+
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$MAD-SKS')) throw new Error('selftest: dollar-commands missing MAD-SKS scoped override guidance');
|
|
2730
|
+
if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Image-UX-Review')) throw new Error('selftest: dollar-commands missing Image UX Review guidance');
|
|
2731
|
+
for (const name of ['context7', 'pipeline', 'qa-loop', 'image-ux-review', 'root', 'openclaw', 'hooks', 'features', 'all-features']) {
|
|
2732
|
+
if (!COMMAND_CATALOG.some((c) => c.name === name)) throw new Error(`selftest: catalog missing ${name}`);
|
|
2733
|
+
}
|
|
2734
|
+
const featureRegistry = await buildFeatureRegistry({ root: packageRoot() });
|
|
2735
|
+
const featureCoverage = validateFeatureRegistry(featureRegistry);
|
|
2736
|
+
if (!featureCoverage.ok) throw new Error(`selftest: feature registry coverage blocked: ${featureCoverage.blockers.join(', ')}`);
|
|
2737
|
+
const allFeaturesResult = buildAllFeaturesSelftest(featureRegistry);
|
|
2738
|
+
if (!allFeaturesResult.ok) throw new Error(`selftest: all-features contract blocked: ${allFeaturesResult.checks.filter((check) => !check.ok).map((check) => check.id).join(', ')}`);
|
|
2739
|
+
const patRedactionProbe = codexAccessTokenStatus({ CODEX_ACCESS_TOKEN: 'secret-probe-value', CODEX_LB_API_KEY: 'lb-secret-probe' });
|
|
2740
|
+
if (patRedactionProbe.status !== 'present_redacted' || JSON.stringify(patRedactionProbe).includes('secret-probe-value') || JSON.stringify(patRedactionProbe).includes('lb-secret-probe')) throw new Error('selftest: Codex access token status leaked a token value');
|
|
2741
|
+
const hooksReport = hooksExplainReport();
|
|
2742
|
+
if (!hooksReport.events.includes('UserPromptSubmit') || !hooksReport.events.includes('Stop') || !hooksReport.sources.some((source) => source.url.includes('/access-tokens'))) throw new Error('selftest: hooks explain coverage');
|
|
2743
|
+
const openClawTmp = tmpdir();
|
|
2744
|
+
const openClawResult = await installOpenClawSkill({ targetDir: path.join(openClawTmp, 'skills', OPENCLAW_SKILL_NAME) });
|
|
2745
|
+
if (!openClawResult.ok) throw new Error(`selftest: OpenClaw skill install blocked: ${openClawResult.reason}`);
|
|
2746
|
+
const openClawSkillText = await safeReadText(path.join(openClawResult.target_dir, 'SKILL.md'));
|
|
2747
|
+
const openClawManifestText = await safeReadText(path.join(openClawResult.target_dir, 'manifest.yaml'));
|
|
2748
|
+
const openClawConfigText = await safeReadText(path.join(openClawResult.target_dir, 'openclaw-agent-config.example.yaml'));
|
|
2749
|
+
if (!openClawSkillText.includes('sks root') || !openClawSkillText.includes('$Team') || !openClawSkillText.includes('OpenClaw agent must have the built-in `shell` tool enabled') || !openClawSkillText.includes('SKS_OPENCLAW=1')) throw new Error('selftest: OpenClaw skill missing SKS agent guidance');
|
|
2750
|
+
if (!openClawManifestText.includes('generated_by: sneakoscope') || !openClawManifestText.includes(`version: ${PACKAGE_VERSION}`)) throw new Error('selftest: OpenClaw manifest missing generated marker or version');
|
|
2751
|
+
if (!openClawConfigText.includes(`- ${OPENCLAW_SKILL_NAME}`) || !openClawConfigText.includes('- shell') || !openClawConfigText.includes('SKS_OPENCLAW')) throw new Error('selftest: OpenClaw agent config example missing skill, shell tool, or OpenClaw env');
|
|
2752
|
+
const registryDollarCommands = DOLLAR_COMMANDS.map((c) => c.command);
|
|
2753
|
+
const manifest = await readJson(path.join(tmp, '.sneakoscope', 'manifest.json'));
|
|
2754
|
+
const policy = await readJson(path.join(tmp, '.sneakoscope', 'policy.json'));
|
|
2755
|
+
const manifestDollarCommands = manifest.prompt_pipeline?.dollar_commands || [];
|
|
2756
|
+
const policyDollarCommands = policy.prompt_pipeline?.dollar_commands || [];
|
|
2757
|
+
if (JSON.stringify(manifestDollarCommands) !== JSON.stringify(registryDollarCommands)) throw new Error('selftest: manifest dollar command drift');
|
|
2758
|
+
if (JSON.stringify(policyDollarCommands) !== JSON.stringify(registryDollarCommands)) throw new Error('selftest: policy dollar command drift');
|
|
2759
|
+
if (!manifest.harness_guard?.immutable_to_llm_edits || !policy.harness_guard?.immutable_to_llm_edits) throw new Error('selftest: harness guard missing from manifest/policy');
|
|
2760
|
+
if (manifest.llm_wiki?.ssot !== 'triwiki' || policy.llm_wiki?.ssot !== 'triwiki') throw new Error('selftest: TriWiki context tracking not recorded in manifest/policy');
|
|
2761
|
+
const codexAppQuickRefExists = await exists(path.join(tmp, '.codex', 'SNEAKOSCOPE.md'));
|
|
2762
|
+
if (!codexAppQuickRefExists) throw new Error('selftest: Codex App quick reference missing');
|
|
2763
|
+
const codexAppQuickRefText = await safeReadText(path.join(tmp, '.codex', 'SNEAKOSCOPE.md'));
|
|
2764
|
+
if (!codexAppQuickRefText.includes('dollar-commands')) throw new Error('selftest: quickref commands');
|
|
2765
|
+
if (!codexAppQuickRefText.includes('Context Tracking') || !codexAppQuickRefText.includes('TriWiki')) throw new Error('selftest: quickref TriWiki');
|
|
2766
|
+
if (!codexAppQuickRefText.includes('Before each route phase') || !codexAppQuickRefText.includes('every stage')) throw new Error('selftest: quickref stage policy');
|
|
2767
|
+
for (const { command } of DOLLAR_COMMANDS) {
|
|
2768
|
+
if (!codexAppQuickRefText.includes(command)) throw new Error(`selftest: Codex App quick reference missing ${command}`);
|
|
2769
|
+
}
|
|
2770
|
+
const hookGoalTmp = tmpdir();
|
|
2771
|
+
await initProject(hookGoalTmp, {});
|
|
2772
|
+
const hookBin = path.join(packageRoot(), 'bin', 'sks.mjs');
|
|
2773
|
+
await selftestCodexCommitHooks();
|
|
2774
|
+
const hookImageUxTmp = tmpdir();
|
|
2775
|
+
await initProject(hookImageUxTmp, {});
|
|
2776
|
+
const hookImageUxPayload = JSON.stringify({ cwd: hookImageUxTmp, prompt: '$Image-UX-Review localhost 화면을 gpt-image-2 콜아웃 리뷰로 검수해줘' });
|
|
2777
|
+
const hookImageUxResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookImageUxTmp, input: hookImageUxPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2778
|
+
if (hookImageUxResult.code !== 0) throw new Error(`selftest: $Image-UX-Review hook exited ${hookImageUxResult.code}: ${hookImageUxResult.stderr}`);
|
|
2779
|
+
const hookImageUxJson = JSON.parse(hookImageUxResult.stdout);
|
|
2780
|
+
const imageUxContext = hookImageUxJson.hookSpecificOutput?.additionalContext || '';
|
|
2781
|
+
if (!imageUxContext.includes('$Image-UX-Review route prepared') || !imageUxContext.includes('Codex App $imagegen/gpt-image-2')) throw new Error('selftest: $Image-UX-Review hook did not prepare imagegen loop context');
|
|
2782
|
+
const hookImageUxState = await readJson(stateFile(hookImageUxTmp), {});
|
|
2783
|
+
if (hookImageUxState.mode !== 'IMAGE_UX_REVIEW' || hookImageUxState.stop_gate !== IMAGE_UX_REVIEW_GATE_ARTIFACT || hookImageUxState.subagents_required !== false || hookImageUxState.reflection_required !== true) throw new Error('selftest: $Image-UX-Review hook did not set direct image UX review state');
|
|
2784
|
+
const imageUxMissionDir = missionDir(hookImageUxTmp, hookImageUxState.mission_id);
|
|
2785
|
+
const imageUxGate = await readJson(path.join(imageUxMissionDir, IMAGE_UX_REVIEW_GATE_ARTIFACT));
|
|
2786
|
+
const imageUxGeneratedLedger = await readJson(path.join(imageUxMissionDir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT));
|
|
2787
|
+
if (imageUxGate.passed || imageUxGate.imagegen_review_images_generated || !imageUxGate.blockers?.includes('source_screenshots_not_captured_yet') || !imageUxGate.blockers?.includes('no_source_screenshots_for_imagegen_review')) throw new Error('selftest: Image UX review gate did not block missing source/generated review images');
|
|
2788
|
+
if (imageUxGeneratedLedger.provider?.model !== 'gpt-image-2' || imageUxGeneratedLedger.passed) throw new Error('selftest: Image UX generated review ledger did not record required gpt-image-2 blocker state');
|
|
2789
|
+
const imageUxStatusResult = await runProcess(process.execPath, [hookBin, 'image-ux-review', 'status', 'latest', '--json'], { cwd: hookImageUxTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2790
|
+
if (imageUxStatusResult.code !== 0) throw new Error(`selftest: sks image-ux-review status failed: ${imageUxStatusResult.stderr || imageUxStatusResult.stdout}`);
|
|
2791
|
+
const imageUxStatus = JSON.parse(imageUxStatusResult.stdout);
|
|
2792
|
+
if (imageUxStatus.ok || imageUxStatus.generated_review_ledger?.provider?.model !== 'gpt-image-2' || !imageUxStatus.files?.gate?.endsWith(IMAGE_UX_REVIEW_GATE_ARTIFACT)) throw new Error('selftest: sks image-ux-review status did not report gpt-image-2 gate blockers');
|
|
2793
|
+
const hookResearchMarkdownTmp = tmpdir();
|
|
2794
|
+
await initProject(hookResearchMarkdownTmp, {});
|
|
2795
|
+
const hookResearchTeamPayload = JSON.stringify({ cwd: hookResearchMarkdownTmp, prompt: '$Team existing active work' });
|
|
2796
|
+
const hookResearchTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookResearchMarkdownTmp, input: hookResearchTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2797
|
+
if (hookResearchTeamResult.code !== 0) throw new Error(`selftest: active Team setup before markdown $Research hook exited ${hookResearchTeamResult.code}: ${hookResearchTeamResult.stderr}`);
|
|
2798
|
+
const hookResearchTeamState = await readJson(stateFile(hookResearchMarkdownTmp), {});
|
|
2799
|
+
const hookResearchMarkdownPayload = JSON.stringify({ cwd: hookResearchMarkdownTmp, prompt: '논문 [$research](x) 팀 커밋 푸쉬 연구' });
|
|
2800
|
+
const hookResearchMarkdownResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookResearchMarkdownTmp, input: hookResearchMarkdownPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2801
|
+
if (hookResearchMarkdownResult.code !== 0) throw new Error(`selftest: markdown $Research hook exited ${hookResearchMarkdownResult.code}: ${hookResearchMarkdownResult.stderr}`);
|
|
2802
|
+
const hookResearchMarkdownJson = JSON.parse(hookResearchMarkdownResult.stdout);
|
|
2803
|
+
const hookResearchMarkdownContext = hookResearchMarkdownJson.hookSpecificOutput?.additionalContext || '';
|
|
2804
|
+
if (!hookResearchMarkdownContext.includes('$Research route prepared')) throw new Error('selftest: markdown research hook');
|
|
2805
|
+
if (hookResearchMarkdownContext.includes(`Active Team mission ${hookResearchTeamState.mission_id}`)) throw new Error('selftest: stale Team context');
|
|
2806
|
+
if (!String(hookResearchMarkdownJson.systemMessage || '').includes('Research route') || String(hookResearchMarkdownJson.systemMessage || '').includes('QA-LOOP route')) throw new Error('selftest: research hook message');
|
|
2807
|
+
const hookResearchMarkdownState = await readJson(stateFile(hookResearchMarkdownTmp), {});
|
|
2808
|
+
if (hookResearchMarkdownState.mode !== 'RESEARCH' || hookResearchMarkdownState.route !== 'Research' || hookResearchMarkdownState.mission_id === hookResearchTeamState.mission_id || hookResearchMarkdownState.stop_gate !== 'research-gate.json' || !hookResearchMarkdownState.pipeline_plan_ready) throw new Error('selftest: research hook state');
|
|
2809
|
+
const hookResearchMissionDir = missionDir(hookResearchMarkdownTmp, hookResearchMarkdownState.mission_id);
|
|
2810
|
+
if (!(await exists(path.join(hookResearchMissionDir, PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest: research hook plan');
|
|
2811
|
+
const rss = 'research-source-skill.md';
|
|
2812
|
+
const gos = 'genius-opinion-summary.md';
|
|
2813
|
+
for (const artifact of [rss, 'source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) {
|
|
2814
|
+
if (!(await exists(path.join(hookResearchMissionDir, artifact)))) throw new Error(`selftest: hook research ${artifact}`);
|
|
2815
|
+
}
|
|
2816
|
+
const hookPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Goal 로그인 세션 만료 UX 개선' });
|
|
2817
|
+
const hookResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookGoalTmp, input: hookPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2818
|
+
if (hookResult.code !== 0) throw new Error(`selftest: $Goal hook exited ${hookResult.code}: ${hookResult.stderr}`);
|
|
2819
|
+
const hookJson = JSON.parse(hookResult.stdout);
|
|
2820
|
+
if ('statusMessage' in hookJson || 'additionalContext' in hookJson) throw new Error('selftest: hook emitted Codex schema-invalid top-level fields');
|
|
2821
|
+
const goalContext = hookJson.hookSpecificOutput?.additionalContext || '';
|
|
2822
|
+
if (!goalContext.includes('$Goal route prepared') || !goalContext.includes('/goal create')) throw new Error('selftest: $Goal hook did not prepare native goal bridge');
|
|
2823
|
+
if (hookJson.hookSpecificOutput?.hookEventName !== 'UserPromptSubmit') throw new Error('selftest: $Goal hook did not emit official UserPromptSubmit additionalContext');
|
|
2824
|
+
if (!String(hookJson.systemMessage || '').includes('Goal workflow bridge')) throw new Error('selftest: $Goal hook missing visible status message');
|
|
2825
|
+
const hookState = await readJson(stateFile(hookGoalTmp), {});
|
|
2826
|
+
if (hookState.phase !== 'GOAL_READY' || hookState.mode !== 'GOAL') throw new Error('selftest: $Goal hook did not set ready state');
|
|
2827
|
+
if (!(await exists(path.join(missionDir(hookGoalTmp, hookState.mission_id), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest: $Goal hook did not write goal workflow artifact');
|
|
2828
|
+
const hookGoalDelegationTmp = tmpdir();
|
|
2829
|
+
await initProject(hookGoalDelegationTmp, {});
|
|
2830
|
+
const hookGoalDelegationPayload = JSON.stringify({ cwd: hookGoalDelegationTmp, prompt: '$Goal $Team 발표자료 만들어줘' });
|
|
2831
|
+
const hookGoalDelegationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookGoalDelegationTmp, input: hookGoalDelegationPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2832
|
+
if (hookGoalDelegationResult.code !== 0) throw new Error(`selftest: $Goal implementation delegation hook exited ${hookGoalDelegationResult.code}: ${hookGoalDelegationResult.stderr}`);
|
|
2833
|
+
const hookGoalDelegationJson = JSON.parse(hookGoalDelegationResult.stdout);
|
|
2834
|
+
const hookGoalDelegationContext = hookGoalDelegationJson.hookSpecificOutput?.additionalContext || '';
|
|
2835
|
+
const hookGoalDelegationBridgeMatch = hookGoalDelegationContext.match(/Goal bridge mission: (M-[A-Za-z0-9-]+)/);
|
|
2836
|
+
if (!hookGoalDelegationBridgeMatch || !hookGoalDelegationContext.includes('Delegated execution route: $Team')) throw new Error('selftest: $Goal implementation prompt did not prepare a bridge plus Team delegation');
|
|
2837
|
+
if (hookGoalDelegationContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalDelegationContext.includes('$Team route prepared')) throw new Error('selftest: $Goal implementation delegation did not prepare direct Team route');
|
|
2838
|
+
const hookGoalDelegationState = await readJson(stateFile(hookGoalDelegationTmp), {});
|
|
2839
|
+
if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalDelegationState.implementation_allowed === false || !hookGoalDelegationState.team_plan_ready) throw new Error('selftest: $Goal implementation delegation did not leave direct Team ready');
|
|
2840
|
+
if (!(await exists(path.join(missionDir(hookGoalDelegationTmp, hookGoalDelegationBridgeMatch[1]), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest: $Goal implementation delegation did not write bridge workflow artifact');
|
|
2841
|
+
const activeGoalMissionId = hookState.mission_id;
|
|
2842
|
+
const hookGoalOverlayPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Team 발표자료 만들어줘' });
|
|
2843
|
+
const hookGoalOverlayResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookGoalTmp, input: hookGoalOverlayPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2844
|
+
if (hookGoalOverlayResult.code !== 0) throw new Error(`selftest: active Goal overlay hook exited ${hookGoalOverlayResult.code}: ${hookGoalOverlayResult.stderr}`);
|
|
2845
|
+
const hookGoalOverlayJson = JSON.parse(hookGoalOverlayResult.stdout);
|
|
2846
|
+
const hookGoalOverlayContext = hookGoalOverlayJson.hookSpecificOutput?.additionalContext || '';
|
|
2847
|
+
if (hookGoalOverlayContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalOverlayContext.includes('$Team route prepared')) throw new Error('selftest: active Goal hijacked a plain Korean implementation prompt instead of preparing direct Team');
|
|
2848
|
+
if (!hookGoalOverlayContext.includes(`Active Goal overlay: existing Goal mission ${activeGoalMissionId}`) || !hookGoalOverlayContext.includes('goal-workflow.json')) throw new Error('selftest: active Goal overlay context was not included with the new route');
|
|
2849
|
+
const hookGoalOverlayState = await readJson(stateFile(hookGoalTmp), {});
|
|
2850
|
+
if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalOverlayState.implementation_allowed === false || !hookGoalOverlayState.team_plan_ready) throw new Error('selftest: active Goal overlay did not leave a new direct Team mission current');
|
|
2851
|
+
if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), 'team-plan.json')))) throw new Error('selftest: active Goal overlay Team mission did not write team-plan.json');
|
|
2852
|
+
const hookUpdateCurrentTmp = tmpdir();
|
|
2853
|
+
await initProject(hookUpdateCurrentTmp, {});
|
|
2854
|
+
const hookUpdateCurrentEnv = { SKS_DISABLE_UPDATE_CHECK: '0', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' };
|
|
2855
|
+
const hookUpdateCurrentPayload = JSON.stringify({ cwd: hookUpdateCurrentTmp, prompt: '상태 확인해줘' });
|
|
2856
|
+
const hookUpdateCurrentResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2857
|
+
cwd: hookUpdateCurrentTmp,
|
|
2858
|
+
input: hookUpdateCurrentPayload,
|
|
2859
|
+
env: hookUpdateCurrentEnv,
|
|
2860
|
+
timeoutMs: 15000,
|
|
2861
|
+
maxOutputBytes: 256 * 1024
|
|
2862
|
+
});
|
|
2863
|
+
if (hookUpdateCurrentResult.code !== 0) throw new Error(`selftest: current update hook exited ${hookUpdateCurrentResult.code}: ${hookUpdateCurrentResult.stderr}`);
|
|
2864
|
+
const hookUpdateCurrentJson = JSON.parse(hookUpdateCurrentResult.stdout);
|
|
2865
|
+
const hookUpdateCurrentContext = hookUpdateCurrentJson.hookSpecificOutput?.additionalContext || '';
|
|
2866
|
+
if (String(hookUpdateCurrentContext).includes('Update SKS now') || String(hookUpdateCurrentContext).includes('Skip update for this conversation')) throw new Error('selftest: hook prompted for update even though installed SKS is current');
|
|
2867
|
+
const hookUpdateCurrentState = await readJson(path.join(hookUpdateCurrentTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2868
|
+
if (hookUpdateCurrentState.pending_offer) throw new Error('selftest: current installed SKS left a pending update offer');
|
|
2869
|
+
const hookRuntimeExpected = await selftestRuntimeVersion();
|
|
2870
|
+
if (hookUpdateCurrentState.current !== '9.9.9' || hookUpdateCurrentState.runtime_current !== hookRuntimeExpected || hookUpdateCurrentState.installed_current !== '9.9.9') throw new Error(`selftest: hook did not record effective installed SKS version: ${JSON.stringify({ expected: { current: '9.9.9', runtime_current: hookRuntimeExpected, installed_current: '9.9.9', loaded_runtime_current: PACKAGE_VERSION }, actual: hookUpdateCurrentState })}`);
|
|
2871
|
+
const hookUpdatePendingTmp = tmpdir();
|
|
2872
|
+
await initProject(hookUpdatePendingTmp, {});
|
|
2873
|
+
await writeJsonAtomic(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {
|
|
2874
|
+
current: PACKAGE_VERSION,
|
|
2875
|
+
latest: '9.9.9',
|
|
2876
|
+
pending_offer: { conversation_id: hookUpdatePendingTmp, latest: '9.9.9', offered_at: nowIso() }
|
|
2877
|
+
});
|
|
2878
|
+
const hookUpdatePendingPayload = JSON.stringify({ cwd: hookUpdatePendingTmp, prompt: 'Update SKS now' });
|
|
2879
|
+
const hookUpdatePendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2880
|
+
cwd: hookUpdatePendingTmp,
|
|
2881
|
+
input: hookUpdatePendingPayload,
|
|
2882
|
+
env: hookUpdateCurrentEnv,
|
|
2883
|
+
timeoutMs: 15000,
|
|
2884
|
+
maxOutputBytes: 256 * 1024
|
|
2885
|
+
});
|
|
2886
|
+
if (hookUpdatePendingResult.code !== 0) throw new Error(`selftest: stale pending update hook exited ${hookUpdatePendingResult.code}: ${hookUpdatePendingResult.stderr}`);
|
|
2887
|
+
const hookUpdatePendingJson = JSON.parse(hookUpdatePendingResult.stdout);
|
|
2888
|
+
const hookUpdatePendingContext = hookUpdatePendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2889
|
+
if (String(hookUpdatePendingContext).includes('user accepted update') || String(hookUpdatePendingContext).includes('Before doing other work')) throw new Error('selftest: current installed SKS accepted a stale pending update offer');
|
|
2890
|
+
const hookUpdatePendingState = await readJson(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2891
|
+
if (hookUpdatePendingState.pending_offer) throw new Error('selftest: stale pending update offer was not cleared after installed SKS became current');
|
|
2892
|
+
const hookUpdateSkippedTmp = tmpdir();
|
|
2893
|
+
await initProject(hookUpdateSkippedTmp, {});
|
|
2894
|
+
await writeJsonAtomic(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {
|
|
2895
|
+
current: PACKAGE_VERSION,
|
|
2896
|
+
latest: '9.9.9',
|
|
2897
|
+
skipped: { conversation_id: hookUpdateSkippedTmp, latest: '9.9.9', skipped_at: nowIso() }
|
|
2898
|
+
});
|
|
2899
|
+
const hookUpdateSkippedPayload = JSON.stringify({ cwd: hookUpdateSkippedTmp, prompt: '상태 확인해줘' });
|
|
2900
|
+
const hookUpdateSkippedResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2901
|
+
cwd: hookUpdateSkippedTmp,
|
|
2902
|
+
input: hookUpdateSkippedPayload,
|
|
2903
|
+
env: hookUpdateCurrentEnv,
|
|
2904
|
+
timeoutMs: 15000,
|
|
2905
|
+
maxOutputBytes: 256 * 1024
|
|
2906
|
+
});
|
|
2907
|
+
if (hookUpdateSkippedResult.code !== 0) throw new Error(`selftest: stale skipped update hook exited ${hookUpdateSkippedResult.code}: ${hookUpdateSkippedResult.stderr}`);
|
|
2908
|
+
const hookUpdateSkippedJson = JSON.parse(hookUpdateSkippedResult.stdout);
|
|
2909
|
+
const hookUpdateSkippedContext = hookUpdateSkippedJson.hookSpecificOutput?.additionalContext || '';
|
|
2910
|
+
if (String(hookUpdateSkippedContext).includes('was skipped for this conversation')) throw new Error('selftest: current installed SKS kept stale skipped update context');
|
|
2911
|
+
const hookUpdateSkippedState = await readJson(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2912
|
+
if (hookUpdateSkippedState.skipped) throw new Error('selftest: stale skipped update state was not cleared after installed SKS became current');
|
|
2913
|
+
const hookUpdateOldTmp = tmpdir();
|
|
2914
|
+
await initProject(hookUpdateOldTmp, {});
|
|
2915
|
+
const hookUpdateOldPayload = JSON.stringify({ cwd: hookUpdateOldTmp, prompt: '상태 확인해줘' });
|
|
2916
|
+
const hookUpdateOldResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2917
|
+
cwd: hookUpdateOldTmp,
|
|
2918
|
+
input: hookUpdateOldPayload,
|
|
2919
|
+
env: { ...hookUpdateCurrentEnv, SKS_INSTALLED_SKS_VERSION: '0.0.0' },
|
|
2920
|
+
timeoutMs: 15000,
|
|
2921
|
+
maxOutputBytes: 256 * 1024
|
|
2922
|
+
});
|
|
2923
|
+
if (hookUpdateOldResult.code !== 0) throw new Error(`selftest: stale update hook exited ${hookUpdateOldResult.code}: ${hookUpdateOldResult.stderr}`);
|
|
2924
|
+
const hookUpdateOldJson = JSON.parse(hookUpdateOldResult.stdout);
|
|
2925
|
+
const hookUpdateOldContext = hookUpdateOldJson.hookSpecificOutput?.additionalContext || '';
|
|
2926
|
+
if (!String(hookUpdateOldContext).includes('Update SKS now') || !String(hookUpdateOldContext).includes('Skip update for this conversation')) throw new Error('selftest: hook did not prompt when installed SKS is stale');
|
|
2927
|
+
const hookUpdateOldState = await readJson(path.join(hookUpdateOldTmp, '.sneakoscope', 'state', 'update-check.json'), {});
|
|
2928
|
+
if (hookUpdateOldState.pending_offer?.latest !== '9.9.9') throw new Error('selftest: stale installed SKS did not persist pending update offer');
|
|
2929
|
+
const hookUpdateAcceptPayload = JSON.stringify({ cwd: hookUpdateOldTmp, prompt: 'Update SKS now' });
|
|
2930
|
+
const hookUpdateAcceptResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
|
|
2931
|
+
cwd: hookUpdateOldTmp,
|
|
2932
|
+
input: hookUpdateAcceptPayload,
|
|
2933
|
+
env: { ...hookUpdateCurrentEnv, SKS_INSTALLED_SKS_VERSION: '0.0.0' },
|
|
2934
|
+
timeoutMs: 15000,
|
|
2935
|
+
maxOutputBytes: 256 * 1024
|
|
2936
|
+
});
|
|
2937
|
+
if (hookUpdateAcceptResult.code !== 0) throw new Error(`selftest: accepted update hook exited ${hookUpdateAcceptResult.code}: ${hookUpdateAcceptResult.stderr}`);
|
|
2938
|
+
const hookUpdateAcceptJson = JSON.parse(hookUpdateAcceptResult.stdout);
|
|
2939
|
+
const hookUpdateAcceptContext = hookUpdateAcceptJson.hookSpecificOutput?.additionalContext || '';
|
|
2940
|
+
if (!String(hookUpdateAcceptContext).includes('npm i -g sneakoscope@9.9.9 --registry https://registry.npmjs.org/')) throw new Error('selftest: exact update cmd');
|
|
2941
|
+
if (String(hookUpdateAcceptContext).includes('sks setup') || String(hookUpdateAcceptContext).includes('sks doctor') || String(hookUpdateAcceptContext).includes('npm i -D sneakoscope')) throw new Error('selftest: update cmd scope');
|
|
2942
|
+
const hookKoreanSksTmp = tmpdir();
|
|
2943
|
+
await initProject(hookKoreanSksTmp, {});
|
|
2944
|
+
const hookKoreanSksPayload = JSON.stringify({ cwd: hookKoreanSksTmp, prompt: koreanReadmeInstallPrompt });
|
|
2945
|
+
const hookKoreanSksResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookKoreanSksTmp, input: hookKoreanSksPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2946
|
+
if (hookKoreanSksResult.code !== 0) throw new Error(`selftest: Korean SKS hook exited ${hookKoreanSksResult.code}: ${hookKoreanSksResult.stderr}`);
|
|
2947
|
+
const hookKoreanSksJson = JSON.parse(hookKoreanSksResult.stdout);
|
|
2948
|
+
const hookKoreanSksContext = hookKoreanSksJson.hookSpecificOutput?.additionalContext || '';
|
|
2949
|
+
if (!hookKoreanSksContext.includes('$Team route prepared') || hookKoreanSksContext.includes('GOAL_PRECISE: 이번 작업의 최종 목표') || hookKoreanSksContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest: Korean prompt did not prepare direct Team route');
|
|
2950
|
+
if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest: Korean implementation prompt did not promote to Team route');
|
|
2951
|
+
if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest: Korean implementation prompt still used answer-only pipeline');
|
|
2952
|
+
const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
|
|
2953
|
+
if (hookKoreanSksState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookKoreanSksState.implementation_allowed !== true || !hookKoreanSksState.ambiguity_gate_passed || !hookKoreanSksState.team_plan_ready) throw new Error('selftest: Korean Team auto-seal did not materialize Team');
|
|
2954
|
+
if (!(await exists(path.join(missionDir(hookKoreanSksTmp, hookKoreanSksState.mission_id), 'team-plan.json')))) throw new Error('selftest: Korean Team auto-seal did not write team-plan.json');
|
|
2955
|
+
const hookPaymentTeamTmp = tmpdir();
|
|
2956
|
+
await initProject(hookPaymentTeamTmp, {});
|
|
2957
|
+
const hookPaymentTeamPayload = JSON.stringify({ cwd: hookPaymentTeamTmp, prompt: '$Team 결제 재시도 정책과 로그인 세션 만료 버그 수정 executor:2 user:1' });
|
|
2958
|
+
const hookPaymentTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookPaymentTeamTmp, input: hookPaymentTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2959
|
+
if (hookPaymentTeamResult.code !== 0) throw new Error(`selftest: payment/auth Team hook exited ${hookPaymentTeamResult.code}: ${hookPaymentTeamResult.stderr}`);
|
|
2960
|
+
const hookPaymentTeamJson = JSON.parse(hookPaymentTeamResult.stdout);
|
|
2961
|
+
const hookPaymentTeamContext = hookPaymentTeamJson.hookSpecificOutput?.additionalContext || '';
|
|
2962
|
+
if (!hookPaymentTeamContext.includes('$Team route prepared') || hookPaymentTeamContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest: predictable payment/auth Team prompt did not prepare direct Team route');
|
|
2963
|
+
if (hookPaymentTeamContext.includes('PAYMENT_RETRY_POLICY') || hookPaymentTeamContext.includes('AUTH_PROTOCOL_CHANGE_ALLOWED')) throw new Error('selftest: predictable payment/auth policy defaults were asked instead of inferred');
|
|
2964
|
+
const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
|
|
2965
|
+
if (hookPaymentTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookPaymentTeamState.implementation_allowed !== true || !hookPaymentTeamState.ambiguity_gate_passed || !hookPaymentTeamState.team_plan_ready) throw new Error('selftest: predictable payment/auth Team did not materialize after auto-seal');
|
|
2966
|
+
if (!(await exists(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest: predictable payment/auth Team auto-seal did not write team-plan.json');
|
|
2967
|
+
const hookTeamTmp = tmpdir();
|
|
2968
|
+
await initProject(hookTeamTmp, {});
|
|
2969
|
+
const hookTeamPayload = JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 발표자료 만들어줘 executor:2 user:1' });
|
|
2970
|
+
const hookTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: hookTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2971
|
+
if (hookTeamResult.code !== 0) throw new Error(`selftest: $Team hook exited ${hookTeamResult.code}: ${hookTeamResult.stderr}`);
|
|
2972
|
+
const hookTeamJson = JSON.parse(hookTeamResult.stdout);
|
|
2973
|
+
if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated') || hookTeamJson.hookSpecificOutput?.additionalContext?.includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest: $Team hook still forced ambiguity questions');
|
|
2974
|
+
if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest: $Team hook did not prepare direct Team route');
|
|
2975
|
+
const hookTeamState = await readJson(stateFile(hookTeamTmp), {});
|
|
2976
|
+
if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest: $Team hook did not prepare direct Team mission');
|
|
2977
|
+
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest: $Team hook did not write a pipeline plan');
|
|
2978
|
+
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest: Team plan was not created directly');
|
|
2979
|
+
const hookForbiddenModelResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team should be blocked before route work', model: 'gpt-5.5', metadata: { client: { modelId: 'gpt-5.0-forbidden' } } }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2980
|
+
if (hookForbiddenModelResult.code !== 0) throw new Error(`selftest: forbidden model hook exited ${hookForbiddenModelResult.code}: ${hookForbiddenModelResult.stderr}`);
|
|
2981
|
+
const hookForbiddenModelJson = JSON.parse(hookForbiddenModelResult.stdout);
|
|
2982
|
+
if (hookForbiddenModelJson.decision !== 'block' || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.5') || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.0-forbidden')) throw new Error('selftest: hook did not block forbidden client model metadata');
|
|
2983
|
+
const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2984
|
+
if (hookTeamPendingResult.code !== 0) throw new Error(`selftest: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
|
|
2985
|
+
const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
|
|
2986
|
+
const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
|
|
2987
|
+
const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
|
|
2988
|
+
if (hookTeamPendingState.mission_id === hookTeamState.mission_id || hookTeamPendingContext.includes('Required questions still pending') || hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest: direct Team follow-up was blocked by stale clarification behavior');
|
|
2989
|
+
if (hookTeamPendingState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !hookTeamPendingState.team_plan_ready) throw new Error('selftest: direct Team follow-up did not prepare a fresh Team mission');
|
|
2990
|
+
const pptClarificationTmp = tmpdir();
|
|
2991
|
+
await initProject(pptClarificationTmp, {});
|
|
2992
|
+
const hookPptClarificationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: pptClarificationTmp, input: JSON.stringify({ cwd: pptClarificationTmp, prompt: '$PPT 투자 제안서 만들어줘' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2993
|
+
if (hookPptClarificationResult.code !== 0) throw new Error(`selftest: PPT clarification hook exited ${hookPptClarificationResult.code}: ${hookPptClarificationResult.stderr}`);
|
|
2994
|
+
const hookPptClarificationState = await readJson(stateFile(pptClarificationTmp), {});
|
|
2995
|
+
const hookPptClarificationJson = JSON.parse(hookPptClarificationResult.stdout);
|
|
2996
|
+
const hookPptContext = hookPptClarificationJson.hookSpecificOutput?.additionalContext || '';
|
|
2997
|
+
const hookPptSchema = await readJson(path.join(missionDir(pptClarificationTmp, hookPptClarificationState.mission_id), 'required-answers.schema.json'));
|
|
2998
|
+
if (hookPptClarificationState.phase !== 'PPT_AUDIENCE_STRATEGY_READY' || hookPptClarificationState.implementation_allowed !== true || hookPptSchema.slots.length !== 0) throw new Error('selftest: PPT hook did not auto-seal without visible questions');
|
|
2999
|
+
if (hookPptContext.includes('Required questions') || hookPptContext.includes('VISIBLE RESPONSE CONTRACT') || hookPptContext.includes('MANDATORY ambiguity-removal gate')) throw new Error('selftest: PPT hook still exposed prequestion wording');
|
|
3000
|
+
if (!(await exists(path.join(missionDir(pptClarificationTmp, hookPptClarificationState.mission_id), 'ppt-audience-strategy.json')))) throw new Error('selftest: PPT auto-seal did not materialize audience strategy');
|
|
3001
|
+
const nonGoalsSlot = hookPptSchema.slots.find((s) => s.id === 'NON_GOALS');
|
|
3002
|
+
if (nonGoalsSlot && !nonGoalsSlot.allow_empty) throw new Error('selftest: NON_GOALS does not allow an empty array answer');
|
|
3003
|
+
if (!nonGoalsSlot && !Array.isArray(hookPptSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest: NON_GOALS was neither asked nor inferred');
|
|
3004
|
+
const textParsedAnswers = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'INTENT_TARGET: compact contract sealing');
|
|
3005
|
+
if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest: text answer parser did not parse slot-id answers');
|
|
3006
|
+
const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
|
|
3007
|
+
if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest: text answer parser did not infer the only missing slot');
|
|
3008
|
+
const honestLoopTmp = tmpdir();
|
|
3009
|
+
await initProject(honestLoopTmp, {});
|
|
3010
|
+
const { id: honestLoopId, dir: honestLoopDir } = await createMission(honestLoopTmp, { mode: 'sks', prompt: 'honest loopback selftest' });
|
|
3011
|
+
await writeJsonAtomic(path.join(honestLoopDir, 'decision-contract.json'), { sealed_hash: 'selftest', answers: { GOAL_PRECISE: 'selftest' } });
|
|
3012
|
+
await setCurrent(honestLoopTmp, { mission_id: honestLoopId, route: 'SKS', route_command: '$SKS', mode: 'SKS', phase: 'SKS_CLARIFICATION_CONTRACT_SEALED', implementation_allowed: true, clarification_required: false, ambiguity_gate_passed: true, stop_gate: 'honest_mode' });
|
|
3013
|
+
const honestLoopResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**작업 요약**\nSelftest 경로의 Honest Mode loopback 동작을 검증했습니다.\n**솔직모드**\n검증: selftest ran\n남은 gap: CHANGELOG.md 없음' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3014
|
+
if (honestLoopResult.code !== 0) throw new Error(`selftest: honest loopback hook exited ${honestLoopResult.code}: ${honestLoopResult.stderr}`);
|
|
3015
|
+
const honestLoopJson = JSON.parse(honestLoopResult.stdout);
|
|
3016
|
+
if (honestLoopJson.decision !== 'block' || !String(honestLoopJson.reason || '').includes('post-ambiguity execution phase')) throw new Error('selftest: Honest Mode gap did not trigger loopback');
|
|
3017
|
+
const honestLoopState = await readJson(stateFile(honestLoopTmp), {});
|
|
3018
|
+
if (honestLoopState.phase !== 'SKS_HONEST_LOOPBACK_AFTER_CLARIFICATION' || honestLoopState.implementation_allowed !== true || honestLoopState.clarification_required !== false || honestLoopState.ambiguity_gate_passed !== true) throw new Error('selftest: honest loopback did not preserve post-ambiguity execution state');
|
|
3019
|
+
if (!(await exists(path.join(honestLoopDir, 'honest-loopback.json')))) throw new Error('selftest: honest-loopback artifact missing');
|
|
3020
|
+
const honestCleanResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**작업 요약**\nCHANGELOG 확인과 selftest 통과 상태로 loopback을 닫았습니다.\n**솔직모드**\n검증: CHANGELOG.md check and selftest passed\n남은 gap: 없음' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3021
|
+
if (honestCleanResult.code !== 0) throw new Error(`selftest: clean honest hook exited ${honestCleanResult.code}: ${honestCleanResult.stderr}`);
|
|
3022
|
+
const honestCleanJson = JSON.parse(honestCleanResult.stdout);
|
|
3023
|
+
if (honestCleanJson.decision === 'block') throw new Error('selftest: clean Honest Mode was blocked after loopback was resolved');
|
|
3024
|
+
const honestCleanState = await readJson(stateFile(honestLoopTmp), {});
|
|
3025
|
+
if (honestCleanState.honest_loop_required !== false || honestCleanState.phase !== 'SKS_HONEST_COMPLETE') throw new Error('selftest: honest loopback was not marked resolved');
|
|
3026
|
+
const honestMissingSummaryResult = 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 });
|
|
3027
|
+
if (honestMissingSummaryResult.code !== 0) throw new Error(`selftest: missing-summary honest hook exited ${honestMissingSummaryResult.code}: ${honestMissingSummaryResult.stderr}`);
|
|
3028
|
+
const honestMissingSummaryJson = JSON.parse(honestMissingSummaryResult.stdout);
|
|
3029
|
+
if (honestMissingSummaryJson.decision !== 'block' || !String(honestMissingSummaryJson.reason || '').includes('completion summary')) throw new Error('selftest: Honest Mode without completion summary was accepted');
|
|
3030
|
+
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 });
|
|
3031
|
+
if (honestMissingSummaryRepeatResult.code !== 0) throw new Error(`selftest: repeated missing-summary honest hook exited ${honestMissingSummaryRepeatResult.code}: ${honestMissingSummaryRepeatResult.stderr}`);
|
|
3032
|
+
const honestMissingSummaryRepeatJson = JSON.parse(honestMissingSummaryRepeatResult.stdout);
|
|
3033
|
+
if (honestMissingSummaryRepeatJson.decision === 'block' || !String(honestMissingSummaryRepeatJson.systemMessage || '').includes('repeat guard')) throw new Error('selftest: repeated completion-summary stop prompt was not suppressed');
|
|
3034
|
+
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 });
|
|
3035
|
+
if (honestBlockedAsExpectedResult.code !== 0) throw new Error(`selftest: blocked-as-expected honest hook exited ${honestBlockedAsExpectedResult.code}: ${honestBlockedAsExpectedResult.stderr}`);
|
|
3036
|
+
const honestBlockedAsExpectedJson = JSON.parse(honestBlockedAsExpectedResult.stdout);
|
|
3037
|
+
if (honestBlockedAsExpectedJson.decision === 'block') throw new Error('selftest: blocked-as-expected evidence was treated as an unresolved gap');
|
|
3038
|
+
const honestNoActiveGateResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**Completion Summary**\nWhat changed: verified route-gate closure evidence handling.\n**SKS Honest Mode**\nVerified: pipeline status returned `No active blocking route gate detected`; post-reflection work blocking was verified by selftest.\nRemaining gaps: none' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3039
|
+
if (honestNoActiveGateResult.code !== 0) throw new Error(`selftest: no-active-gate honest hook exited ${honestNoActiveGateResult.code}: ${honestNoActiveGateResult.stderr}`);
|
|
3040
|
+
const honestNoActiveGateJson = JSON.parse(honestNoActiveGateResult.stdout);
|
|
3041
|
+
if (honestNoActiveGateJson.decision === 'block') throw new Error('selftest: no-active-blocking status was treated as an unresolved gap');
|
|
3042
|
+
const honestNotBlockerResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**Completion Summary**\nWhat changed: verified non-blocker wording in final closeout.\n**SKS Honest Mode**\nVerified: selftest passed.\nRemaining gaps: none. Unrelated dirty worktree entries are not a blocker for this scoped task.' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3043
|
+
if (honestNotBlockerResult.code !== 0) throw new Error(`selftest: not-blocker honest hook exited ${honestNotBlockerResult.code}: ${honestNotBlockerResult.stderr}`);
|
|
3044
|
+
const honestNotBlockerJson = JSON.parse(honestNotBlockerResult.stdout);
|
|
3045
|
+
if (honestNotBlockerJson.decision === 'block') throw new Error('selftest: non-blocker boundary wording was treated as unresolved gap');
|
|
3046
|
+
const honestSummaryCaseResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: honestLoopTmp, input: JSON.stringify({ cwd: honestLoopTmp, last_assistant_message: '**작업 요약**\n[src/cli/main.mjs]: selftest에 요약 없으면 차단, 요약 있으면 통과 케이스 추가.\n**솔직모드**\n검증: selftest 통과.\n남은 gap: 없음' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3047
|
+
if (honestSummaryCaseResult.code !== 0) throw new Error(`selftest: summary-case honest hook exited ${honestSummaryCaseResult.code}: ${honestSummaryCaseResult.stderr}`);
|
|
3048
|
+
const honestSummaryCaseJson = JSON.parse(honestSummaryCaseResult.stdout);
|
|
3049
|
+
if (honestSummaryCaseJson.decision === 'block') throw new Error('selftest: summary block/pass wording was treated as unresolved gap');
|
|
3050
|
+
const hookQaTmp = tmpdir();
|
|
3051
|
+
await initProject(hookQaTmp, {});
|
|
3052
|
+
const hookQaPayload = JSON.stringify({ cwd: hookQaTmp, prompt: '$QA-LOOP run API E2E against local dev' });
|
|
3053
|
+
const hookQaResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookQaTmp, input: hookQaPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
3054
|
+
if (hookQaResult.code !== 0) throw new Error(`selftest: $QA-LOOP hook exited ${hookQaResult.code}: ${hookQaResult.stderr}`);
|
|
3055
|
+
const hookQaJson = JSON.parse(hookQaResult.stdout);
|
|
3056
|
+
const hookQaContext = hookQaJson.hookSpecificOutput?.additionalContext || '';
|
|
3057
|
+
if (!hookQaContext.includes('Route contract auto-sealed') || hookQaContext.includes('MANDATORY ambiguity-removal gate activated') || hookQaContext.includes('Required questions:') || hookQaContext.includes('QA_SCOPE:') || hookQaContext.includes('UI_COMPUTER_USE_ACK:')) throw new Error('selftest: $QA-LOOP hook did not auto-seal without visible answer slots');
|
|
3058
|
+
if (!hookQaContext.includes('Codex Computer Use') || !hookQaContext.includes('Playwright') || !hookQaContext.includes('Chrome MCP')) throw new Error('selftest: $QA-LOOP hook did not state Computer Use-only UI policy');
|
|
3059
|
+
if (hookQaContext.includes('Browser Use 또는 Computer Use') || hookQaContext.includes('Browser/Computer Use evidence')) throw new Error('selftest: $QA-LOOP hook still allows Browser Use as UI evidence');
|
|
3060
|
+
const hookQaState = await readJson(stateFile(hookQaTmp), {});
|
|
3061
|
+
if (hookQaState.phase !== 'QALOOP_CLARIFICATION_CONTRACT_SEALED' || hookQaState.implementation_allowed !== true || hookQaState.clarification_required !== false || !hookQaState.ambiguity_gate_passed) throw new Error('selftest: $QA-LOOP hook did not auto-seal the ambiguity gate');
|
|
3062
|
+
const hookQaSchema = await readJson(path.join(missionDir(hookQaTmp, hookQaState.mission_id), 'required-answers.schema.json'));
|
|
3063
|
+
if (hookQaSchema.slots.length !== 0 || hookQaSchema.inferred_answers?.QA_SCOPE !== 'api_e2e_only') throw new Error('selftest: $QA-LOOP schema did not infer QA answers without visible slots');
|
|
3064
|
+
const qaMissionDir = missionDir(hookQaTmp, hookQaState.mission_id);
|
|
3065
|
+
const initialQaGate = await readJson(path.join(qaMissionDir, 'qa-gate.json'));
|
|
3066
|
+
const qaReportFile = initialQaGate.qa_report_file;
|
|
3067
|
+
if (!isQaReportFilename(qaReportFile)) throw new Error(`selftest: QA report filename is not date/version-prefixed: ${qaReportFile}`);
|
|
3068
|
+
if ((await exists(path.join(qaMissionDir, 'qa-report.md')))) throw new Error('selftest: legacy QA report filename was created');
|
|
3069
|
+
if (!(await exists(path.join(qaMissionDir, qaReportFile))) || !(await exists(path.join(qaMissionDir, 'qa-ledger.json'))) || !(await exists(path.join(qaMissionDir, 'qa-gate.json')))) throw new Error('selftest: QA artifacts missing after answer');
|
|
3070
|
+
const legacyQaTmp = tmpdir();
|
|
3071
|
+
await writeJsonAtomic(path.join(legacyQaTmp, 'qa-gate.json'), { ...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' } }, { reportFile: 'qa-report.md' }), passed: true, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, ui_computer_use_evidence: true, post_fix_verification_complete: true, honest_mode_complete: true });
|
|
3072
|
+
await writeJsonAtomic(path.join(legacyQaTmp, 'qa-ledger.json'), { checklist: [] });
|
|
3073
|
+
await writeTextAtomic(path.join(legacyQaTmp, 'qa-report.md'), '# legacy\n');
|
|
3074
|
+
const legacyQaGate = await evaluateQaGate(legacyQaTmp);
|
|
3075
|
+
if (legacyQaGate.passed || !legacyQaGate.reasons.includes('qa_report_filename_prefix_invalid')) throw new Error('selftest: legacy QA report filename was accepted');
|
|
3076
|
+
const unresolvedQaTmp = tmpdir();
|
|
3077
|
+
await writeJsonAtomic(path.join(unresolvedQaTmp, 'qa-gate.json'), { ...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' } }), passed: true, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, ui_computer_use_evidence: true, unresolved_findings: 0, unresolved_fixable_findings: 1, post_fix_verification_complete: true, honest_mode_complete: true });
|
|
3078
|
+
const unresolvedQaGateFile = (await readJson(path.join(unresolvedQaTmp, 'qa-gate.json'))).qa_report_file;
|
|
3079
|
+
await writeJsonAtomic(path.join(unresolvedQaTmp, 'qa-ledger.json'), { checklist: [] });
|
|
3080
|
+
await writeTextAtomic(path.join(unresolvedQaTmp, unresolvedQaGateFile), '# unresolved\n');
|
|
3081
|
+
const unresolvedQaGate = await evaluateQaGate(unresolvedQaTmp);
|
|
3082
|
+
if (unresolvedQaGate.passed || !unresolvedQaGate.reasons.includes('unresolved_fixable_findings_remaining')) throw new Error('selftest: unresolved fixable QA finding was accepted');
|
|
3083
|
+
const forbiddenQaTmp = tmpdir();
|
|
3084
|
+
const forbiddenQaGate = defaultQaGate({ sealed_hash: 'selftest', answers: { QA_SCOPE: 'ui_e2e_only', TARGET_BASE_URL: 'http://localhost:3000', API_BASE_URL: 'same_as_target', TARGET_ENVIRONMENT: 'local_dev_server', DESTRUCTIVE_DEPLOYED_TESTS_ALLOWED: 'never' } });
|
|
3085
|
+
await writeJsonAtomic(path.join(forbiddenQaTmp, 'qa-gate.json'), { ...forbiddenQaGate, passed: true, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, ui_computer_use_evidence: true, ui_evidence_source: 'playwright', post_fix_verification_complete: true, honest_mode_complete: true, evidence: ['Playwright screenshot evidence'] });
|
|
3086
|
+
await writeJsonAtomic(path.join(forbiddenQaTmp, 'qa-ledger.json'), { checklist: [] });
|
|
3087
|
+
await writeTextAtomic(path.join(forbiddenQaTmp, forbiddenQaGate.qa_report_file), '# forbidden\n');
|
|
3088
|
+
const forbiddenQaGateResult = await evaluateQaGate(forbiddenQaTmp);
|
|
3089
|
+
if (forbiddenQaGateResult.passed || !forbiddenQaGateResult.reasons.includes('ui_evidence_source_not_codex_computer_use') || !forbiddenQaGateResult.reasons.includes('forbidden_browser_automation_evidence')) throw new Error('selftest: forbidden browser automation QA evidence was accepted');
|
|
3090
|
+
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 });
|
|
3091
|
+
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: QA-LOOP dogfood prompt');
|
|
3092
|
+
if (!promptQa.includes(CODEX_COMPUTER_USE_ONLY_POLICY) || !promptQa.includes('Chrome MCP') || !promptQa.includes('Playwright') || !promptQa.includes('Browser Use')) throw new Error('selftest: QA-LOOP prompt did not enforce Computer Use-only UI evidence');
|
|
3093
|
+
if (promptQa.includes('Browser/Computer Use evidence')) throw new Error('selftest: QA-LOOP prompt still allows Browser/Computer UI evidence');
|
|
3094
|
+
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' } });
|
|
3095
|
+
if (pkgQa.ui_e2e_required || pkgQa.api_e2e_required || !pkgQa.ui_computer_use_evidence) throw new Error('selftest: package QA target gate');
|
|
3096
|
+
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 });
|
|
3097
|
+
if (qaRunResult.code !== 0) throw new Error(`selftest: qa-loop mock run exited ${qaRunResult.code}: ${qaRunResult.stderr}`);
|
|
3098
|
+
const qaGate = await readJson(path.join(qaMissionDir, 'qa-gate.evaluated.json'));
|
|
3099
|
+
if (!qaGate.passed) throw new Error('selftest: qa-loop mock gate did not pass');
|
|
3100
|
+
const hookDfixTmp = tmpdir();
|
|
3101
|
+
await initProject(hookDfixTmp, {});
|
|
3102
|
+
const hookDfixPayload = JSON.stringify({ cwd: hookDfixTmp, prompt: '$DFix 버튼 라벨 바꿔줘' });
|
|
3103
|
+
const hookDfixResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookDfixTmp, input: hookDfixPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3104
|
+
if (hookDfixResult.code !== 0) throw new Error(`selftest: $DFix hook exited ${hookDfixResult.code}: ${hookDfixResult.stderr}`);
|
|
3105
|
+
const hookDfixJson = JSON.parse(hookDfixResult.stdout);
|
|
3106
|
+
if (hookDfixJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest: $DFix incorrectly triggered ambiguity gate');
|
|
3107
|
+
if (hookDfixJson.hookSpecificOutput?.additionalContext?.includes('SKS skill-first pipeline active')) throw new Error('selftest: $DFix entered the general SKS prompt pipeline');
|
|
3108
|
+
if (hookDfixJson.hookSpecificOutput?.additionalContext?.includes('Mission:')) throw new Error('selftest: $DFix created route mission state');
|
|
3109
|
+
if (!hookDfixJson.hookSpecificOutput?.additionalContext?.includes('DFix ultralight pipeline active')) throw new Error('selftest: $DFix hook missing ultralight pipeline guidance');
|
|
3110
|
+
if (!hookDfixJson.hookSpecificOutput?.additionalContext?.includes('Task list:')) throw new Error('selftest: $DFix hook missing micro task list');
|
|
3111
|
+
if (!hookDfixJson.hookSpecificOutput?.additionalContext?.includes('DFix 완료 요약')) throw new Error('selftest: $DFix hook missing no-record final marker guidance');
|
|
3112
|
+
if (!hookDfixJson.hookSpecificOutput?.additionalContext?.includes('DFix 솔직모드')) throw new Error('selftest: $DFix hook missing lightweight Honest Mode guidance');
|
|
3113
|
+
if (!hookDfixJson.systemMessage?.includes('DFix ultralight')) throw new Error('selftest: $DFix hook missing ultralight system message');
|
|
3114
|
+
if (await exists(path.join(hookDfixTmp, '.sneakoscope', 'state', 'light-route-stop.json'))) throw new Error('selftest: $DFix hook created persistent light-route state');
|
|
3115
|
+
const hookDfixState = await readJson(stateFile(hookDfixTmp), {});
|
|
3116
|
+
if (String(hookDfixState.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) throw new Error('selftest: $DFix state entered clarification gate');
|
|
3117
|
+
const explicitDfixDirectTmp = tmpdir();
|
|
3118
|
+
await initProject(explicitDfixDirectTmp, {});
|
|
3119
|
+
const explicitDfixDirectPayload = JSON.stringify({ cwd: explicitDfixDirectTmp, prompt: '$DFix Update the docs config wording to Direct Fix' });
|
|
3120
|
+
const explicitDfixDirectResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: explicitDfixDirectTmp, input: explicitDfixDirectPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3121
|
+
if (explicitDfixDirectResult.code !== 0) throw new Error(`selftest: explicit Direct Fix docs/config hook exited ${explicitDfixDirectResult.code}: ${explicitDfixDirectResult.stderr}`);
|
|
3122
|
+
const explicitDfixDirectJson = JSON.parse(explicitDfixDirectResult.stdout);
|
|
3123
|
+
const explicitDfixDirectContext = explicitDfixDirectJson.hookSpecificOutput?.additionalContext || '';
|
|
3124
|
+
if (!explicitDfixDirectContext.includes('DFix ultralight pipeline active')) throw new Error('selftest: explicit Direct Fix docs/config request did not use ultralight hook');
|
|
3125
|
+
if (explicitDfixDirectContext.includes('SKS skill-first pipeline active') || explicitDfixDirectContext.includes('Mission:')) throw new Error('selftest: explicit Direct Fix docs/config request leaked general pipeline context');
|
|
3126
|
+
if (await exists(path.join(explicitDfixDirectTmp, '.sneakoscope', 'state', 'light-route-stop.json'))) throw new Error('selftest: explicit Direct Fix docs/config hook created persistent light-route state');
|
|
3127
|
+
const inferredDfixPayload = JSON.stringify({ cwd: hookTeamTmp, prompt: '버튼 라벨 바꿔줘' });
|
|
3128
|
+
const inferredDfixResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: inferredDfixPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3129
|
+
if (inferredDfixResult.code !== 0) throw new Error(`selftest: inferred DFix hook exited ${inferredDfixResult.code}: ${inferredDfixResult.stderr}`);
|
|
3130
|
+
const inferredDfixJson = JSON.parse(inferredDfixResult.stdout);
|
|
3131
|
+
const inferredDfixContext = inferredDfixJson.hookSpecificOutput?.additionalContext || '';
|
|
3132
|
+
if (!inferredDfixContext.includes('DFix ultralight pipeline active')) throw new Error('selftest: inferred DFix did not use ultralight route');
|
|
3133
|
+
if (inferredDfixContext.includes('SKS skill-first pipeline active') || inferredDfixContext.includes('Active Team mission') || inferredDfixContext.includes('Mission:')) throw new Error('selftest: inferred DFix leaked general pipeline or active Team context');
|
|
3134
|
+
const answerPayload = JSON.stringify({ cwd: hookTeamTmp, prompt: '이 파이프라인은 왜 이렇게 동작해?' });
|
|
3135
|
+
const answerResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: answerPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3136
|
+
if (answerResult.code !== 0) throw new Error(`selftest: answer-only hook exited ${answerResult.code}: ${answerResult.stderr}`);
|
|
3137
|
+
const answerJson = JSON.parse(answerResult.stdout);
|
|
3138
|
+
const answerContext = answerJson.hookSpecificOutput?.additionalContext || '';
|
|
3139
|
+
if (!answerContext.includes('SKS answer-only pipeline active')) throw new Error('selftest: question prompt did not use Answer route');
|
|
3140
|
+
if (answerContext.includes('MANDATORY ambiguity-removal gate activated') || answerContext.includes('SKS skill-first pipeline active') || answerContext.includes('Active Team mission') || answerContext.includes('Mission:')) throw new Error('selftest: Answer route leaked execution pipeline or active Team context');
|
|
3141
|
+
if (!answerJson.systemMessage?.includes('answer-only')) throw new Error('selftest: Answer route missing system message');
|
|
3142
|
+
const wikiPayload = JSON.stringify({ cwd: hookTeamTmp, prompt: '$Wiki 갱신' });
|
|
3143
|
+
const wikiResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: wikiPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3144
|
+
if (wikiResult.code !== 0) throw new Error(`selftest: Wiki hook exited ${wikiResult.code}: ${wikiResult.stderr}`);
|
|
3145
|
+
const wikiJson = JSON.parse(wikiResult.stdout);
|
|
3146
|
+
const wikiContext = wikiJson.hookSpecificOutput?.additionalContext || '';
|
|
3147
|
+
if (!wikiContext.includes('SKS wiki pipeline active') || !wikiContext.includes('sks wiki refresh')) throw new Error('selftest: $Wiki hook did not inject wiki route');
|
|
3148
|
+
if (wikiContext.includes('MANDATORY ambiguity-removal gate activated') || wikiContext.includes('Mission:')) throw new Error('selftest: Wiki route created ambiguity mission state');
|
|
3149
|
+
if (!wikiJson.systemMessage?.includes('wiki refresh')) throw new Error('selftest: Wiki route missing system message');
|
|
3150
|
+
const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
|
|
3151
|
+
const missingCodexConfigFlags = missingGeneratedCodexAppFeatureFlags(codexConfigText);
|
|
3152
|
+
if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
|
|
3153
|
+
assertCodexWarn(codexConfigText, 'generated Codex App config');
|
|
3154
|
+
if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest: Context7 MCP not configured');
|
|
3155
|
+
if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-research]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest: GPT-5.5 reasoning profiles not configured');
|
|
3156
|
+
if (!hasResearchProfileConfig(codexConfigText)) throw new Error('selftest: generated Research xhigh profiles not configured');
|
|
3157
|
+
if (!/\[profiles\.sks-mad-high\][\s\S]*?approval_policy = "never"[\s\S]*?sandbox_mode = "danger-full-access"/.test(codexConfigText)) throw new Error('selftest: generated sks-mad-high profile is not full access');
|
|
3158
|
+
if (!codexConfigText.includes('[agents.analysis_scout]')) throw new Error('selftest: analysis_scout agent not configured');
|
|
3159
|
+
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest: team_consensus agent not configured');
|
|
3160
|
+
const preservedConfigTmp = tmpdir();
|
|
3161
|
+
await ensureDir(path.join(preservedConfigTmp, '.codex'));
|
|
3162
|
+
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\ncodex_hooks = true\nfast_mode_ui = false\ncodex_git_commit = false\ncomputer_use = false\napps = false\nplugins = false\ncustom_preview = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
3163
|
+
await initProject(preservedConfigTmp, {});
|
|
3164
|
+
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
3165
|
+
if (!/^model = "gpt-5\.5"/m.test(preservedConfig) || !preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest: Codex config merge dropped or failed to enable Fast mode defaults and GPT-5.5');
|
|
3166
|
+
assertCodexWarn(preservedConfig, 'merged Codex config');
|
|
3167
|
+
if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
|
|
3168
|
+
const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
|
|
3169
|
+
if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]') || !hasResearchProfileConfig(preservedConfig)) throw new Error(`selftest: Codex config merge did not add required app feature flags, Research profiles, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
|
|
3170
|
+
if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
|
|
3171
|
+
const appFeatureTmp = tmpdir();
|
|
3172
|
+
const fakeCodexApp = path.join(appFeatureTmp, 'Codex.app');
|
|
3173
|
+
const fakeCodexBinDir = path.join(appFeatureTmp, 'bin');
|
|
3174
|
+
await ensureDir(fakeCodexApp);
|
|
3175
|
+
await ensureDir(fakeCodexBinDir);
|
|
3176
|
+
await ensureDir(path.join(appFeatureTmp, '.codex'));
|
|
3177
|
+
const codexAppFixtureConfigText = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
3178
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.codex', 'config.toml'), codexAppFixtureConfigText);
|
|
3179
|
+
const fakeDefaultPluginCacheNames = ['browser', 'chrome', 'computer-use', 'latex', 'documents', 'presentations', 'spreadsheets'];
|
|
3180
|
+
for (const name of fakeDefaultPluginCacheNames) await ensureDir(path.join(appFeatureTmp, '.codex', 'plugins', 'cache', name));
|
|
3181
|
+
const fakeCodex = path.join(fakeCodexBinDir, 'codex');
|
|
3182
|
+
await writeTextAtomic(fakeCodex, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3183
|
+
await fsp.chmod(fakeCodex, 0o755);
|
|
3184
|
+
const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
|
|
3185
|
+
const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3186
|
+
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.git_actions?.ok || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, git actions, remote_control, and unlocked Fast UI config');
|
|
3187
|
+
const codexAppProcessRows = parseProcessRows([
|
|
3188
|
+
'101 1 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
|
|
3189
|
+
'200 1 /Applications/Codex.app/Contents/MacOS/Codex',
|
|
3190
|
+
'201 200 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
|
|
3191
|
+
'202 1 /Applications/Codex.app/Contents/Resources/codex app-server --listen stdio://',
|
|
3192
|
+
'203 1 /Users/me/.nvm/versions/node/bin/codex --model gpt-5.5'
|
|
3193
|
+
].join('\n'));
|
|
3194
|
+
const codexAppRepairTargets = findCodexAppUpgradeRepairTargets(codexAppProcessRows);
|
|
3195
|
+
if (codexAppRepairTargets.length !== 1 || codexAppRepairTargets[0].pid !== 101) throw new Error('selftest: Codex App upgrade repair target selection is not limited to orphan desktop app-server processes');
|
|
3196
|
+
const codexAppOldCliStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 0.129.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3197
|
+
if (codexAppOldCliStatus.ok || codexAppOldCliStatus.features?.git_actions?.ok || !codexAppOldCliStatus.guidance.some((line) => line.includes('git commit/push actions are blocked'))) throw new Error('selftest: codex-app check did not block commit/push actions on old Codex CLI remote-control');
|
|
3198
|
+
const missingDefaultPluginTmp = tmpdir();
|
|
3199
|
+
await ensureDir(path.join(missingDefaultPluginTmp, '.codex'));
|
|
3200
|
+
const codexConfigWithoutMarketplaceSources = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '').trim();
|
|
3201
|
+
await writeTextAtomic(path.join(missingDefaultPluginTmp, '.codex', 'config.toml'), `${codexConfigWithoutMarketplaceSources}\n`);
|
|
3202
|
+
const codexAppMissingDefaultPluginStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: missingDefaultPluginTmp, cwd: missingDefaultPluginTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3203
|
+
if (codexAppMissingDefaultPluginStatus.ok || codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.ok || codexAppMissingDefaultPluginStatus.plugins?.picker?.ok || !codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.missing_installed?.includes('browser@openai-bundled') || !codexAppMissingDefaultPluginStatus.guidance.some((line) => line.includes('default plugin source'))) throw new Error('selftest: codex-app check did not block missing default plugin source');
|
|
3204
|
+
await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
|
|
3205
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), stalePluginSkillContent('browser'));
|
|
3206
|
+
const codexAppShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3207
|
+
if (codexAppShadowStatus.ok || codexAppShadowStatus.plugins?.picker?.ok || codexAppShadowStatus.plugins?.skill_shadows?.blocking?.[0]?.name !== 'browser' || codexAppShadowStatus.plugins?.skill_shadows?.generated?.[0]?.name !== 'browser' || !codexAppShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not block generated skill shadow that can hide @ plugin picker entries');
|
|
3208
|
+
await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
|
|
3209
|
+
await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
|
|
3210
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), '---\nname: browser\ndescription: User custom skill, not generated by SKS.\n---\n');
|
|
3211
|
+
const codexAppCustomShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3212
|
+
if (codexAppCustomShadowStatus.ok || codexAppCustomShadowStatus.plugins?.picker?.ok || codexAppCustomShadowStatus.plugins?.skill_shadows?.custom?.[0]?.name !== 'browser' || codexAppCustomShadowStatus.plugins?.skill_shadows?.generated?.length || !codexAppCustomShadowStatus.guidance.some((line) => line.includes('user-owned reserved skill name')) || codexAppCustomShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not distinguish user-owned reserved plugin skill names from generated shadows');
|
|
3213
|
+
await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
|
|
3214
|
+
const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
|
|
3215
|
+
await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3216
|
+
await fsp.chmod(fakeCodexMissing, 0o755);
|
|
3217
|
+
const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3218
|
+
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit || codexAppMissingFeatureStatus.features?.git_actions?.ok) throw new Error('selftest: codex-app check did not block disabled codex_git_commit feature flag');
|
|
3219
|
+
const fakeCodexMissingImageGen = path.join(fakeCodexBinDir, 'codex-missing-imagegen');
|
|
3220
|
+
await writeTextAtomic(fakeCodexMissingImageGen, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable false\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3221
|
+
await fsp.chmod(fakeCodexMissingImageGen, 0o755);
|
|
3222
|
+
const codexAppMissingImageGenStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissingImageGen, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3223
|
+
if (codexAppMissingImageGenStatus.ok || codexAppMissingImageGenStatus.features?.required_flags_ok || codexAppMissingImageGenStatus.features?.image_generation || !codexAppMissingImageGenStatus.guidance.some((line) => line.includes('image_generation'))) throw new Error('selftest: codex-app check did not block disabled image_generation for imagegen pipelines');
|
|
3224
|
+
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
3225
|
+
const autoReviewEnv = { HOME: autoReviewHome };
|
|
3226
|
+
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
|
3227
|
+
if (!autoReviewEnabled.enabled || autoReviewEnabled.profile_name !== 'sks-auto-review-high' || !autoReviewEnabled.high_profile) throw new Error('selftest: auto-review high profile was not enabled');
|
|
3228
|
+
const autoReviewConfig = await safeReadText(path.join(autoReviewHome, '.codex', 'config.toml'));
|
|
3229
|
+
if (!autoReviewConfig.includes('approvals_reviewer = "auto_review"') || autoReviewConfig.includes('approvals_reviewer = "guardian_subagent"') || !autoReviewConfig.includes('[profiles.sks-auto-review-high]')) throw new Error('selftest: auto-review config not written');
|
|
3230
|
+
const autoReviewDisabled = await disableAutoReview({ env: autoReviewEnv });
|
|
3231
|
+
if (autoReviewDisabled.enabled || autoReviewDisabled.approvals_reviewer !== 'user') throw new Error('selftest: auto-review disable did not restore user reviewer');
|
|
3232
|
+
const autoReviewDisabledConfig = await safeReadText(path.join(autoReviewHome, '.codex', 'config.toml'));
|
|
3233
|
+
if (autoReviewDisabledConfig.includes('approvals_reviewer = "guardian_subagent"')) throw new Error('selftest: auto-review disable left legacy reviewer values');
|
|
3234
|
+
const analysisAgentExists = await exists(path.join(tmp, '.codex', 'agents', 'analysis-scout.toml'));
|
|
3235
|
+
if (!analysisAgentExists) throw new Error('selftest: analysis scout agent not installed');
|
|
3236
|
+
const teamAgentExists = await exists(path.join(tmp, '.codex', 'agents', 'team-consensus.toml'));
|
|
3237
|
+
if (!teamAgentExists) throw new Error('selftest: team consensus agent not installed');
|
|
3238
|
+
const teamSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'team', 'SKILL.md'));
|
|
3239
|
+
if (!teamSkillExists) throw new Error('selftest: $Team skill not installed');
|
|
3240
|
+
const honestSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'honest-mode', 'SKILL.md'));
|
|
3241
|
+
if (!honestSkillExists) throw new Error('selftest: honest-mode skill not installed');
|
|
3242
|
+
const autoResearchSkillExists = await exists(path.join(tmp, '.agents', 'skills', 'autoresearch-loop', 'SKILL.md'));
|
|
3243
|
+
if (!autoResearchSkillExists) throw new Error('selftest: autoresearch-loop skill not installed');
|
|
3244
|
+
const requiredSkillsStatus = await checkRequiredSkills(tmp);
|
|
3245
|
+
if (!requiredSkillsStatus.ok) throw new Error(`selftest: required skills missing: ${requiredSkillsStatus.missing.join(', ')}`);
|
|
3246
|
+
const c7Status = await checkContext7(tmp);
|
|
3247
|
+
if (!c7Status.ok || !c7Status.project.ok) throw new Error('selftest: Context7 check failed for project config');
|
|
3248
|
+
if (hasContext7ConfigText('[mcp_servers.other]\ncommand = "npx"\n')) throw new Error('selftest: missing Context7 config passed structural check');
|
|
3249
|
+
const mockContext7Path = path.join(tmp, 'mock-context7.mjs');
|
|
3250
|
+
await writeTextAtomic(mockContext7Path, `process.stdin.setEncoding('utf8');\nlet buf='';\nfunction send(id,result){process.stdout.write(JSON.stringify({jsonrpc:'2.0',id,result})+'\\n');}\nprocess.stdin.on('data',(chunk)=>{buf+=chunk;for(;;){const i=buf.indexOf('\\n');if(i<0)break;const line=buf.slice(0,i).trim();buf=buf.slice(i+1);if(!line)continue;const msg=JSON.parse(line);if(!msg.id)continue;if(msg.method==='initialize')send(msg.id,{protocolVersion:'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'Mock Context7',version:'0.0.0'}});else if(msg.method==='tools/list')send(msg.id,{tools:[{name:'resolve-library-id'},{name:'query-docs'}]});else if(msg.method==='tools/call'&&msg.params.name==='resolve-library-id')send(msg.id,{content:[{type:'text',text:'Context7-compatible library ID: /mock/lib'}]});else if(msg.method==='tools/call'&&msg.params.name==='query-docs')send(msg.id,{content:[{type:'text',text:'mock docs for '+msg.params.arguments.libraryId}]});else send(msg.id,{content:[{type:'text',text:'unknown'}],isError:true});}});\n`);
|
|
3251
|
+
const mockContext7Docs = await context7Docs('Mock Lib', { command: process.execPath, args: [mockContext7Path], query: 'hooks', timeoutMs: 5000 });
|
|
3252
|
+
if (!mockContext7Docs.ok || mockContext7Docs.docs_tool !== 'query-docs' || mockContext7Docs.library_id !== '/mock/lib') throw new Error('selftest: local Context7 MCP client did not resolve/query docs');
|
|
3253
|
+
const passedTeamGate = { passed: true, analysis_artifact: true, triwiki_refreshed: true, triwiki_validated: true, consensus_artifact: true, team_roster_confirmed: true, implementation_team_fresh: true, review_artifact: true, integration_evidence: true, session_cleanup: true };
|
|
3254
|
+
const passedTeamSessionCleanup = { schema_version: 1, passed: true, all_sessions_closed: true, outstanding_sessions: 0, live_transcript_finalized: true, closed_at: nowIso() };
|
|
3255
|
+
const passedFromChatImgCoverageLedger = {
|
|
3256
|
+
schema_version: 1,
|
|
3257
|
+
passed: true,
|
|
3258
|
+
all_chat_requirements_listed: true,
|
|
3259
|
+
all_requirements_mapped_to_work_order: true,
|
|
3260
|
+
all_screenshot_regions_accounted: true,
|
|
3261
|
+
all_attachments_accounted: true,
|
|
3262
|
+
image_analysis_complete: true,
|
|
3263
|
+
verbatim_customer_requests_preserved: true,
|
|
3264
|
+
checklist_updated: true,
|
|
3265
|
+
temp_triwiki_recorded: true,
|
|
3266
|
+
scoped_qa_loop_completed: true,
|
|
3267
|
+
checklist_file: FROM_CHAT_IMG_CHECKLIST_ARTIFACT,
|
|
3268
|
+
temp_triwiki_file: FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT,
|
|
3269
|
+
qa_loop_file: FROM_CHAT_IMG_QA_LOOP_ARTIFACT,
|
|
3270
|
+
unresolved_items: [],
|
|
3271
|
+
chat_requirements: [{ id: 'req-1', source: 'selftest chat text', text: 'Change the hero image.' }],
|
|
3272
|
+
attachment_matches: [{ id: 'match-1', requirement_ids: ['req-1'], attachment: 'original-1.png', confidence: 'high' }],
|
|
3273
|
+
work_order_items: [{ id: 'work-1', requirement_ids: ['req-1'], action: 'Apply the requested hero image change.' }]
|
|
3274
|
+
};
|
|
3275
|
+
const passedFromChatImgChecklist = [
|
|
3276
|
+
'# From-Chat-IMG Checklist',
|
|
3277
|
+
'',
|
|
3278
|
+
'## Customer Requests',
|
|
3279
|
+
'- [x] req-1 source-bound customer request preserved.',
|
|
3280
|
+
'',
|
|
3281
|
+
'## Image Analysis',
|
|
3282
|
+
'- [x] match-1 screenshot image region matched to original-1.png with high confidence.',
|
|
3283
|
+
'',
|
|
3284
|
+
'## Work Items',
|
|
3285
|
+
'- [x] work-1 requested hero image change represented in the work order.',
|
|
3286
|
+
'',
|
|
3287
|
+
'## QA Loop',
|
|
3288
|
+
'- [x] scoped QA-LOOP covered work-1 after implementation with zero unresolved findings.',
|
|
3289
|
+
'',
|
|
3290
|
+
'## Verification',
|
|
3291
|
+
'- [x] coverage ledger, checklist, and temporary TriWiki session context reconciled.',
|
|
3292
|
+
''
|
|
3293
|
+
].join('\n');
|
|
3294
|
+
const passedFromChatImgTempTriWiki = {
|
|
3295
|
+
schema_version: 1,
|
|
3296
|
+
scope: 'temporary',
|
|
3297
|
+
storage: 'triwiki',
|
|
3298
|
+
expires_after_sessions: FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS,
|
|
3299
|
+
claims: [{ id: 'req-1', source: 'selftest chat text', text: 'Change the hero image.', trust: 'source_bound' }]
|
|
3300
|
+
};
|
|
3301
|
+
const passedFromChatImgQaLoop = {
|
|
3302
|
+
schema_version: 1,
|
|
3303
|
+
passed: true,
|
|
3304
|
+
scope: 'from-chat-img-work-order',
|
|
3305
|
+
coverage_ledger: FROM_CHAT_IMG_COVERAGE_ARTIFACT,
|
|
3306
|
+
checklist_file: FROM_CHAT_IMG_CHECKLIST_ARTIFACT,
|
|
3307
|
+
all_work_order_items_qa_checked: true,
|
|
3308
|
+
work_order_item_ids_covered: ['work-1'],
|
|
3309
|
+
unresolved_findings: 0,
|
|
3310
|
+
unresolved_fixable_findings: 0,
|
|
3311
|
+
post_fix_verification_complete: true,
|
|
3312
|
+
computer_use_evidence_source: CODEX_COMPUTER_USE_EVIDENCE_SOURCE,
|
|
3313
|
+
evidence: ['selftest scoped QA-LOOP covered work-1']
|
|
3314
|
+
};
|
|
3315
|
+
const incompleteTeamGateTmp = tmpdir();
|
|
3316
|
+
await initProject(incompleteTeamGateTmp, {});
|
|
3317
|
+
const { id: incompleteGateId, dir: incompleteGateDir } = await createMission(incompleteTeamGateTmp, { mode: 'team', prompt: 'incomplete team gate test' });
|
|
3318
|
+
await writeJsonAtomic(path.join(incompleteGateDir, 'team-gate.json'), { passed: true, analysis_artifact: true, triwiki_refreshed: true });
|
|
3319
|
+
await setCurrent(incompleteTeamGateTmp, { mission_id: incompleteGateId, mode: 'TEAM', route: 'Team', route_command: '$Team', phase: 'TEAM_REVIEW', context7_required: false, subagents_required: false, stop_gate: 'team-gate.json' });
|
|
3320
|
+
const incompleteGateState = await readJson(stateFile(incompleteTeamGateTmp), {});
|
|
3321
|
+
const incompleteGateStop = await evaluateStop(incompleteTeamGateTmp, incompleteGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3322
|
+
if (incompleteGateStop?.decision !== 'block' || !String(incompleteGateStop.reason || '').includes('triwiki_validated')) throw new Error('selftest: incomplete Team gate was not blocked');
|
|
3323
|
+
const routeGateTmp = tmpdir();
|
|
3324
|
+
await initProject(routeGateTmp, {});
|
|
3325
|
+
const { id: gateId, dir: gateDir } = await createMission(routeGateTmp, { mode: 'team', prompt: 'Context7 gate test' });
|
|
3326
|
+
await writeJsonAtomic(path.join(gateDir, 'team-roster.json'), { schema_version: 1, mission_id: gateId, confirmed: true, source: 'selftest' });
|
|
3327
|
+
await writeJsonAtomic(path.join(gateDir, 'team-gate.json'), passedTeamGate);
|
|
3328
|
+
await setCurrent(routeGateTmp, { mission_id: gateId, mode: 'TEAM', route: 'Team', route_command: '$Team', phase: 'TEAM_REVIEW', context7_required: true, stop_gate: 'team-gate.json' });
|
|
3329
|
+
const gateState = await readJson(stateFile(routeGateTmp), {});
|
|
3330
|
+
const missingC7Stop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3331
|
+
if (missingC7Stop?.decision !== 'block' || !String(missingC7Stop.reason || '').includes('Context7')) throw new Error('selftest: Stop hook did not block missing Context7 evidence');
|
|
3332
|
+
const rosterArtifactGateTmp = tmpdir();
|
|
3333
|
+
await initProject(rosterArtifactGateTmp, {});
|
|
3334
|
+
const { id: rosterArtifactGateId, dir: rosterArtifactGateDir } = await createMission(rosterArtifactGateTmp, { mode: 'team', prompt: 'team roster artifact gate test' });
|
|
3335
|
+
await writeJsonAtomic(path.join(rosterArtifactGateDir, 'team-gate.json'), { ...passedTeamGate, session_cleanup: false });
|
|
3336
|
+
await setCurrent(rosterArtifactGateTmp, { mission_id: rosterArtifactGateId, mode: 'TEAM', route: 'Team', route_command: '$Team', phase: 'TEAM_REVIEW', context7_required: false, stop_gate: 'team-gate.json' });
|
|
3337
|
+
const rosterArtifactGateState = await readJson(stateFile(rosterArtifactGateTmp), {});
|
|
3338
|
+
const missingRosterArtifactStop = await evaluateStop(rosterArtifactGateTmp, rosterArtifactGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3339
|
+
if (missingRosterArtifactStop?.decision !== 'block' || !String(missingRosterArtifactStop.reason || '').includes('team-roster.json')) throw new Error('selftest: Team gate did not block missing team roster artifact');
|
|
3340
|
+
const runtimeGateTmp = tmpdir();
|
|
3341
|
+
await initProject(runtimeGateTmp, {});
|
|
3342
|
+
const { id: runtimeGateId, dir: runtimeGateDir } = await createMission(runtimeGateTmp, { mode: 'team', prompt: 'team runtime graph gate test' });
|
|
3343
|
+
await writeJsonAtomic(path.join(runtimeGateDir, 'team-roster.json'), { schema_version: 1, mission_id: runtimeGateId, confirmed: true, source: 'selftest' });
|
|
3344
|
+
await writeJsonAtomic(path.join(runtimeGateDir, TEAM_SESSION_CLEANUP_ARTIFACT), passedTeamSessionCleanup);
|
|
3345
|
+
await writeJsonAtomic(path.join(runtimeGateDir, 'team-gate.json'), {
|
|
3346
|
+
...passedTeamGate,
|
|
3347
|
+
team_graph_required: true,
|
|
3348
|
+
team_graph_compiled: true,
|
|
3349
|
+
runtime_dependencies_concrete: true,
|
|
3350
|
+
worker_inboxes_written: true,
|
|
3351
|
+
write_scope_conflicts_zero: true,
|
|
3352
|
+
task_claim_readiness_checked: true
|
|
3353
|
+
});
|
|
3354
|
+
await setCurrent(runtimeGateTmp, { mission_id: runtimeGateId, mode: 'TEAM', route: 'Team', route_command: '$Team', phase: 'TEAM_REVIEW', context7_required: false, stop_gate: 'team-gate.json' });
|
|
3355
|
+
const runtimeGateState = await readJson(stateFile(runtimeGateTmp), {});
|
|
3356
|
+
const missingRuntimeGraphStop = await evaluateStop(runtimeGateTmp, runtimeGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3357
|
+
if (missingRuntimeGraphStop?.decision !== 'block' || !String(missingRuntimeGraphStop.reason || '').includes(TEAM_GRAPH_ARTIFACT)) throw new Error('selftest: Team gate did not block missing runtime graph artifacts');
|
|
3358
|
+
const fromChatCoverageTmp = tmpdir();
|
|
3359
|
+
await initProject(fromChatCoverageTmp, {});
|
|
3360
|
+
const { id: fromChatCoverageId, dir: fromChatCoverageDir } = await createMission(fromChatCoverageTmp, { mode: 'team', prompt: '$From-Chat-IMG coverage gate test' });
|
|
3361
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, 'team-roster.json'), { schema_version: 1, mission_id: fromChatCoverageId, confirmed: true, source: 'selftest' });
|
|
3362
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, 'team-gate.json'), { ...passedTeamGate, session_cleanup: false, from_chat_img_required: true });
|
|
3363
|
+
await setCurrent(fromChatCoverageTmp, { mission_id: fromChatCoverageId, mode: 'TEAM', route: 'Team', route_command: '$From-Chat-IMG', phase: 'TEAM_REVIEW', context7_required: false, from_chat_img_required: true, stop_gate: 'team-gate.json' });
|
|
3364
|
+
const fromChatCoverageState = await readJson(stateFile(fromChatCoverageTmp), {});
|
|
3365
|
+
const missingFromChatCoverageFieldStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3366
|
+
if (missingFromChatCoverageFieldStop?.decision !== 'block' || !String(missingFromChatCoverageFieldStop.reason || '').includes('from_chat_img_request_coverage')) throw new Error('selftest: From-Chat-IMG coverage field did not block Team gate');
|
|
3367
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, 'team-gate.json'), { ...passedTeamGate, session_cleanup: false, from_chat_img_required: true, from_chat_img_request_coverage: true });
|
|
3368
|
+
const missingFromChatCoverageArtifactStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3369
|
+
if (missingFromChatCoverageArtifactStop?.decision !== 'block' || !String(missingFromChatCoverageArtifactStop.reason || '').includes(FROM_CHAT_IMG_COVERAGE_ARTIFACT)) throw new Error('selftest: From-Chat-IMG coverage artifact did not block Team gate');
|
|
3370
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_COVERAGE_ARTIFACT), { ...passedFromChatImgCoverageLedger, unresolved_items: ['ambiguous request'] });
|
|
3371
|
+
const unresolvedFromChatCoverageStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3372
|
+
if (unresolvedFromChatCoverageStop?.decision !== 'block' || !String(unresolvedFromChatCoverageStop.reason || '').includes(`${FROM_CHAT_IMG_COVERAGE_ARTIFACT}:unresolved_items`)) throw new Error('selftest: From-Chat-IMG unresolved items did not block Team gate');
|
|
3373
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_COVERAGE_ARTIFACT), passedFromChatImgCoverageLedger);
|
|
3374
|
+
const missingFromChatChecklistStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3375
|
+
if (missingFromChatChecklistStop?.decision !== 'block' || !String(missingFromChatChecklistStop.reason || '').includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT)) throw new Error('selftest: From-Chat-IMG checklist artifact did not block Team gate');
|
|
3376
|
+
await writeTextAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_CHECKLIST_ARTIFACT), passedFromChatImgChecklist.replace('- [x] req-1', '- [ ] req-1'));
|
|
3377
|
+
const uncheckedFromChatChecklistStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3378
|
+
if (uncheckedFromChatChecklistStop?.decision !== 'block' || !String(uncheckedFromChatChecklistStop.reason || '').includes(`${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}:unchecked_items`)) throw new Error('selftest: From-Chat-IMG unchecked checklist item did not block Team gate');
|
|
3379
|
+
await writeTextAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_CHECKLIST_ARTIFACT), passedFromChatImgChecklist);
|
|
3380
|
+
const missingFromChatTempTriWikiStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3381
|
+
if (missingFromChatTempTriWikiStop?.decision !== 'block' || !String(missingFromChatTempTriWikiStop.reason || '').includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT)) throw new Error('selftest: From-Chat-IMG temporary TriWiki artifact did not block Team gate');
|
|
3382
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT), { ...passedFromChatImgTempTriWiki, expires_after_sessions: FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS + 1 });
|
|
3383
|
+
const invalidFromChatTempTriWikiStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3384
|
+
if (invalidFromChatTempTriWikiStop?.decision !== 'block' || !String(invalidFromChatTempTriWikiStop.reason || '').includes(`${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}:expires_after_sessions`)) throw new Error('selftest: From-Chat-IMG temporary TriWiki TTL did not block Team gate');
|
|
3385
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT), passedFromChatImgTempTriWiki);
|
|
3386
|
+
const missingFromChatQaLoopStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3387
|
+
if (missingFromChatQaLoopStop?.decision !== 'block' || !String(missingFromChatQaLoopStop.reason || '').includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT)) throw new Error('selftest: From-Chat-IMG scoped QA-LOOP artifact did not block Team gate');
|
|
3388
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_QA_LOOP_ARTIFACT), { ...passedFromChatImgQaLoop, unresolved_findings: 1 });
|
|
3389
|
+
const unresolvedFromChatQaLoopStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3390
|
+
if (unresolvedFromChatQaLoopStop?.decision !== 'block' || !String(unresolvedFromChatQaLoopStop.reason || '').includes(`${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}:unresolved_findings`)) throw new Error('selftest: From-Chat-IMG scoped QA-LOOP findings did not block Team gate');
|
|
3391
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_QA_LOOP_ARTIFACT), { ...passedFromChatImgQaLoop, work_order_item_ids_covered: [] });
|
|
3392
|
+
const uncoveredFromChatQaLoopStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3393
|
+
if (uncoveredFromChatQaLoopStop?.decision !== 'block' || !String(uncoveredFromChatQaLoopStop.reason || '').includes(`${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}:work_order_item_ids_covered`)) throw new Error('selftest: From-Chat-IMG scoped QA-LOOP work item coverage did not block Team gate');
|
|
3394
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_QA_LOOP_ARTIFACT), { ...passedFromChatImgQaLoop, computer_use_evidence_source: 'playwright', evidence: ['Playwright visual verification'] });
|
|
3395
|
+
const forbiddenFromChatQaLoopStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3396
|
+
if (forbiddenFromChatQaLoopStop?.decision !== 'block' || !String(forbiddenFromChatQaLoopStop.reason || '').includes(`${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}:computer_use_evidence_source`) || !String(forbiddenFromChatQaLoopStop.reason || '').includes(`${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}:forbidden_browser_automation_evidence`)) throw new Error('selftest: From-Chat-IMG scoped QA-LOOP accepted forbidden browser automation evidence');
|
|
3397
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, FROM_CHAT_IMG_QA_LOOP_ARTIFACT), passedFromChatImgQaLoop);
|
|
3398
|
+
await writeJsonAtomic(path.join(fromChatCoverageDir, 'team-gate.json'), { ...passedTeamGate, from_chat_img_required: true, from_chat_img_request_coverage: true });
|
|
3399
|
+
const coveredFromChatStop = await evaluateStop(fromChatCoverageTmp, fromChatCoverageState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3400
|
+
if (coveredFromChatStop?.decision !== 'block' || String(coveredFromChatStop.reason || '').includes('from-chat-img') || !String(coveredFromChatStop.reason || '').includes(TEAM_SESSION_CLEANUP_ARTIFACT)) throw new Error('selftest: valid From-Chat-IMG artifacts did not hand off to session cleanup gate');
|
|
3401
|
+
await recordContext7Evidence(routeGateTmp, gateState, { tool_name: 'mcp__context7__resolve_library_id', library: 'react' });
|
|
3402
|
+
const resolveOnlyStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3403
|
+
if (resolveOnlyStop?.decision !== 'block') throw new Error('selftest: resolve-only Context7 evidence unblocked route');
|
|
3404
|
+
await recordContext7Evidence(routeGateTmp, gateState, { tool_name: 'mcp__context7__query_docs', library_id: '/facebook/react' });
|
|
3405
|
+
const missingCleanupStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3406
|
+
if (missingCleanupStop?.decision !== 'block' || !String(missingCleanupStop.reason || '').includes(TEAM_SESSION_CLEANUP_ARTIFACT)) throw new Error('selftest: Team route did not block missing session cleanup gate');
|
|
3407
|
+
await writeJsonAtomic(path.join(gateDir, TEAM_SESSION_CLEANUP_ARTIFACT), passedTeamSessionCleanup);
|
|
3408
|
+
await writeSelftestRouteProof(routeGateTmp, { missionId: gateId, kind: 'team_gate' });
|
|
3409
|
+
const missingReflectionStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3410
|
+
if (missingReflectionStop?.decision !== 'block' || !String(missingReflectionStop.reason || '').includes('reflection')) throw new Error('selftest: full route did not block missing reflection gate');
|
|
3411
|
+
const missingReflectionNoQuestionStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: true });
|
|
3412
|
+
if (missingReflectionNoQuestionStop?.decision !== 'block' || !String(missingReflectionNoQuestionStop.reason || '').includes('reflection')) throw new Error('selftest: no-question route did not block missing reflection gate');
|
|
3413
|
+
await writeTextAtomic(path.join(gateDir, REFLECTION_ARTIFACT), '# Post-Route Reflection\n\nNo issue selftest.\n');
|
|
3414
|
+
await writeJsonAtomic(path.join(gateDir, REFLECTION_GATE), { schema_version: 1, passed: true, mission_id: gateId, route: '$Team', reflection_artifact: true, lessons_recorded: false, no_issue_acknowledged: true, triwiki_recorded: false, wiki_refreshed_or_packed: true, wiki_validated: true, created_at: nowIso() });
|
|
3415
|
+
const c7Unblocked = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3416
|
+
if (c7Unblocked?.decision === 'block') throw new Error('selftest: full Context7 evidence did not unblock route gate');
|
|
3417
|
+
await appendJsonlBounded(path.join(gateDir, 'team-transcript.jsonl'), { ts: new Date(Date.now() + 5000).toISOString(), agent: 'parent_orchestrator', phase: 'IMPLEMENTATION', type: 'status', message: 'work after reflection selftest' });
|
|
3418
|
+
const staleReflectionStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3419
|
+
if (staleReflectionStop?.decision !== 'block' || !String(staleReflectionStop.reason || '').includes('work_after_reflection')) throw new Error('selftest: post-reflection work did not stale the reflection gate');
|
|
3420
|
+
const subagentGateTmp = tmpdir();
|
|
3421
|
+
await initProject(subagentGateTmp, {});
|
|
3422
|
+
const { id: subagentGateId, dir: subagentGateDir } = await createMission(subagentGateTmp, { mode: 'team', prompt: 'subagent evidence gate test' });
|
|
3423
|
+
await writeJsonAtomic(path.join(subagentGateDir, 'team-roster.json'), { schema_version: 1, mission_id: subagentGateId, confirmed: true, source: 'selftest' });
|
|
3424
|
+
await writeJsonAtomic(path.join(subagentGateDir, 'team-gate.json'), passedTeamGate);
|
|
3425
|
+
await setCurrent(subagentGateTmp, { mission_id: subagentGateId, mode: 'TEAM', route: 'Team', route_command: '$Team', phase: 'TEAM_REVIEW', context7_required: false, subagents_required: true, stop_gate: 'team-gate.json' });
|
|
3426
|
+
const subagentGateState = await readJson(stateFile(subagentGateTmp), {});
|
|
3427
|
+
const missingSubagentStop = await evaluateStop(subagentGateTmp, subagentGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3428
|
+
if (missingSubagentStop?.decision !== 'block' || !String(missingSubagentStop.reason || '').includes('subagent')) throw new Error('selftest: Stop hook did not block missing subagent evidence');
|
|
3429
|
+
await recordSubagentEvidence(subagentGateTmp, subagentGateState, { tool_name: 'spawn_agent', agent_type: 'worker' });
|
|
3430
|
+
await writeJsonAtomic(path.join(subagentGateDir, TEAM_SESSION_CLEANUP_ARTIFACT), passedTeamSessionCleanup);
|
|
3431
|
+
await writeTextAtomic(path.join(subagentGateDir, REFLECTION_ARTIFACT), '# Post-Route Reflection\n\nNo issue selftest.\n');
|
|
3432
|
+
await writeJsonAtomic(path.join(subagentGateDir, REFLECTION_GATE), { schema_version: 1, passed: true, mission_id: subagentGateId, route: '$Team', reflection_artifact: true, lessons_recorded: false, no_issue_acknowledged: true, triwiki_recorded: false, wiki_refreshed_or_packed: true, wiki_validated: true, created_at: nowIso() });
|
|
3433
|
+
await writeSelftestRouteProof(subagentGateTmp, { missionId: subagentGateId, kind: 'subagent_gate' });
|
|
3434
|
+
const subagentUnblocked = await evaluateStop(subagentGateTmp, subagentGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3435
|
+
if (subagentUnblocked?.decision === 'block') throw new Error('selftest: subagent evidence did not unblock route gate');
|
|
3436
|
+
const { id: teamId, dir: teamDir } = await createMission(tmp, { mode: 'team', prompt: '병렬 구현 팀 테스트' });
|
|
3437
|
+
const teamPlan = buildTeamPlan(teamId, '병렬 구현 팀 테스트');
|
|
3438
|
+
await writeJsonAtomic(path.join(teamDir, 'team-plan.json'), teamPlan);
|
|
3439
|
+
if (teamPlan.agent_session_count !== 5) throw new Error('selftest: team default sessions not 5');
|
|
3440
|
+
if (teamPlan.role_counts.executor !== 3 || teamPlan.role_counts.user !== 1 || teamPlan.role_counts.reviewer !== 5) throw new Error('selftest: team default role counts invalid');
|
|
3441
|
+
const teamPlanFeatureFlags = teamPlan.codex_config_required?.features || {};
|
|
3442
|
+
const missingTeamPlanFeatureFlags = missingGeneratedCodexAppFeatureFlags(teamPlanFeatureFlags);
|
|
3443
|
+
if (missingTeamPlanFeatureFlags.length || teamPlanFeatureFlags.codex_hooks === true) throw new Error(`selftest: team plan Codex config missing required app flags or still uses deprecated codex_hooks: ${missingTeamPlanFeatureFlags.join(', ')}`);
|
|
3444
|
+
if (!teamPlan.review_gate?.passed || teamPlan.review_gate.required_reviewer_lanes !== 5) throw new Error('selftest: team review policy gate did not pass default plan');
|
|
3445
|
+
if (teamPlan.codex_config_required?.service_tier !== 'fast' || teamPlan.reasoning?.service_tier !== 'fast') throw new Error('selftest: team plan did not require Fast service tier');
|
|
3446
|
+
if (!teamPlan.goal_continuation?.enabled || teamPlan.goal_continuation?.mode !== 'ambient_codex_native_goal_overlay') throw new Error('selftest: Team plan did not include ambient Goal continuation');
|
|
3447
|
+
if (!teamPlan.roster.analysis_team.every((agent) => agent.service_tier === 'fast' && agent.reasoning_effort && agent.reasoning_profile)) throw new Error('selftest: analysis scouts missing dynamic Fast reasoning metadata');
|
|
3448
|
+
const simpleTeamPlan = buildTeamPlan(teamId, '$Team 간단한 코드 수정');
|
|
3449
|
+
if (!simpleTeamPlan.roster.analysis_team.some((agent) => agent.reasoning_effort === 'low')) throw new Error('selftest: simple Team prompt did not allow low-reasoning scouts');
|
|
3450
|
+
const toolingTeamPlan = buildTeamPlan(teamId, '$Team tmux CLI tool-calling runtime fix');
|
|
3451
|
+
if (!toolingTeamPlan.roster.analysis_team.some((agent) => agent.reasoning_effort === 'medium')) throw new Error('selftest: tool-heavy Team prompt did not assign medium-reasoning scouts');
|
|
3452
|
+
const researchTeamPlan = buildTeamPlan(teamId, '$Team external library research and current docs update');
|
|
3453
|
+
if (!researchTeamPlan.roster.analysis_team.some((agent) => agent.reasoning_effort === 'high')) throw new Error('selftest: research/docs Team prompt did not assign high-reasoning scouts');
|
|
3454
|
+
const underProvisionedReviewCount = 2;
|
|
3455
|
+
const blockedReviewGate = evaluateTeamReviewPolicyGate({ roleCounts: { reviewer: underProvisionedReviewCount }, agentSessions: 3, roster: { validation_team: [{ id: 'reviewer_1', role: 'reviewer' }] } });
|
|
3456
|
+
if (blockedReviewGate.passed || !blockedReviewGate.blockers.includes('validation_team_reviewers_below_required')) throw new Error('selftest: team review policy gate did not block under-provisioned review');
|
|
3457
|
+
if (teamPlan.phases[0]?.id !== 'team_roster_confirmation' || teamPlan.phases[1]?.id !== 'parallel_analysis_scouting' || teamPlan.phases[2]?.id !== 'triwiki_refresh') throw new Error('selftest: team plan is not roster-first then scout-first');
|
|
3458
|
+
if (teamPlan.roster.debate_team.length !== 3 || !teamPlan.roster.debate_team.some((agent) => agent.id === 'debate_user_1') || !teamPlan.roster.development_team.some((agent) => agent.id === 'executor_3')) throw new Error('selftest: team roster missing default agents');
|
|
3459
|
+
if (teamPlan.roster.analysis_team.length !== teamPlan.role_counts.executor || !teamPlan.roster.analysis_team.some((agent) => agent.id === 'analysis_scout_3')) throw new Error('selftest: team analysis scout roster missing default agents');
|
|
3460
|
+
if (!teamPlan.required_artifacts.includes('team-roster.json') || !teamPlan.required_artifacts.includes('team-analysis.md') || !teamPlan.required_artifacts.includes(TEAM_SESSION_CLEANUP_ARTIFACT)) throw new Error('selftest: team plan missing required artifacts');
|
|
3461
|
+
if (teamPlan.team_runtime?.graph_artifact !== TEAM_GRAPH_ARTIFACT || !teamPlan.required_artifacts.includes(TEAM_RUNTIME_TASKS_ARTIFACT) || !teamPlan.required_artifacts.includes(TEAM_DECOMPOSITION_ARTIFACT) || !teamPlan.required_artifacts.includes(TEAM_INBOX_DIR)) throw new Error('selftest: team plan missing runtime graph metadata/artifacts');
|
|
3462
|
+
if (!teamPlan.phases.some((phase) => phase.id === 'runtime_task_graph_compile')) throw new Error('selftest: team plan missing runtime task graph compile phase');
|
|
3463
|
+
const teamRuntime = await writeTeamRuntimeArtifacts(teamDir, teamPlan, { contractHash: 'selftest' });
|
|
3464
|
+
const teamRuntimeValidation = await validateTeamRuntimeArtifacts(teamDir);
|
|
3465
|
+
if (!teamRuntimeValidation.ok) throw new Error(`selftest: team runtime graph validation failed: ${teamRuntimeValidation.issues.join(', ')}`);
|
|
3466
|
+
if (!teamRuntime.runtime.tasks.every((task) => (task.depends_on || []).every((dep) => String(dep).startsWith('task-')))) throw new Error('selftest: team runtime graph dependencies are not concrete task ids');
|
|
3467
|
+
if (!Object.keys(teamRuntime.inboxes || {}).length || !teamRuntime.report.inboxes.length) throw new Error('selftest: team runtime graph did not write worker inboxes');
|
|
3468
|
+
if (teamPlan.context_tracking?.ssot !== 'triwiki' || !teamPlan.required_artifacts.includes('.sneakoscope/wiki/context-pack.json')) throw new Error('selftest: team plan missing TriWiki context tracking');
|
|
3469
|
+
if (!teamPlan.context_tracking?.stage_policy?.includes('before_each_route_stage_read_relevant_context_pack')) throw new Error('selftest: team plan missing per-stage TriWiki policy');
|
|
3470
|
+
if (!teamPlan.invariants.some((item) => item.includes('chat-history screenshots'))) throw new Error('selftest: team invariants missing chat capture matching');
|
|
3471
|
+
if (!teamPlan.invariants.some((item) => item.includes('request coverage'))) throw new Error('selftest: team invariants missing From-Chat-IMG request coverage');
|
|
3472
|
+
if (!teamPlan.phases.some((phase) => String(phase.goal || '').includes('refreshes/validates TriWiki before implementation handoff'))) throw new Error('selftest: team plan missing mid-pipeline TriWiki refresh');
|
|
3473
|
+
const fromChatTeamPlan = buildTeamPlan(teamId, '$From-Chat-IMG 채팅 기록 이미지와 첨부 원본 이미지로 고객 요청 작업 지시서 작성');
|
|
3474
|
+
if (fromChatTeamPlan.prompt_command !== '$From-Chat-IMG') throw new Error('selftest: From-Chat-IMG team plan did not preserve prompt command');
|
|
3475
|
+
if (!fromChatTeamPlan.required_artifacts.includes(FROM_CHAT_IMG_COVERAGE_ARTIFACT)) throw new Error('selftest: From-Chat-IMG team plan missing coverage ledger artifact');
|
|
3476
|
+
if (!fromChatTeamPlan.required_artifacts.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT) || !fromChatTeamPlan.required_artifacts.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT) || !fromChatTeamPlan.required_artifacts.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT)) throw new Error('selftest: From-Chat-IMG team plan missing checklist/temp TriWiki/QA artifacts');
|
|
3477
|
+
if (!fromChatTeamPlan.phases.some((phase) => phase.id === 'from_chat_img_coverage_reconciliation')) throw new Error('selftest: From-Chat-IMG team plan missing coverage reconciliation phase');
|
|
3478
|
+
if (!fromChatTeamPlan.invariants.some((item) => item.includes('unresolved_items=[]'))) throw new Error('selftest: From-Chat-IMG team plan missing zero-unresolved invariant');
|
|
3479
|
+
if (!fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT))) throw new Error('selftest: From-Chat-IMG team plan missing checklist/temp TriWiki/QA invariants');
|
|
3480
|
+
const teamWorkflow = teamWorkflowMarkdown(teamPlan);
|
|
3481
|
+
if (!teamWorkflow.includes('SSOT: triwiki') || !teamWorkflow.includes('Analysis Scouts') || !teamWorkflow.includes('sks wiki validate')) throw new Error('selftest: team workflow missing scout-first TriWiki context tracking');
|
|
3482
|
+
if (!teamWorkflow.includes('sks team open-tmux')) throw new Error('selftest: team workflow missing existing-mission tmux open command');
|
|
3483
|
+
if (!teamWorkflow.includes(TEAM_GRAPH_ARTIFACT) || !teamWorkflow.includes(TEAM_INBOX_DIR)) throw new Error('selftest: team workflow missing runtime graph/inbox guidance');
|
|
3484
|
+
if (!teamWorkflow.includes('before every stage') || !teamWorkflow.includes('after findings/artifact changes')) throw new Error('selftest: team workflow missing per-stage TriWiki policy');
|
|
3485
|
+
const customTeamPlan = buildTeamPlan(teamId, '병렬 구현 팀 테스트', { agentSessions: 5 });
|
|
3486
|
+
if (customTeamPlan.agent_session_count !== 5) throw new Error('selftest: custom team sessions not honored');
|
|
3487
|
+
if (parseTeamCreateArgs(['--agents', '4', '작업']).agentSessions !== 5) throw new Error('selftest: team --agents parsing');
|
|
3488
|
+
const maxAgentParsed = parseTeamCreateArgs(['--max-agents', '작업']);
|
|
3489
|
+
if (maxAgentParsed.agentSessions !== 6 || maxAgentParsed.roleCounts.executor !== 6) throw new Error('selftest: team --max-agents parsing');
|
|
3490
|
+
const maxTextParsed = parseTeamSpecText('가용가능한 최대 agents로 분석하고 구현');
|
|
3491
|
+
if (maxTextParsed.agentSessions !== 6 || maxTextParsed.roleCounts.executor !== 6) throw new Error('selftest: team max-agent text parsing');
|
|
3492
|
+
const roleParsed = parseTeamCreateArgs(['executor:5', 'reviewer:6', 'user:1', '작업']);
|
|
3493
|
+
if (roleParsed.roleCounts.executor !== 5 || roleParsed.roleCounts.reviewer !== 6 || roleParsed.agentSessions !== 6 || roleParsed.prompt !== '작업') throw new Error('selftest: team role-count parsing');
|
|
3494
|
+
const openTmuxFlagParsed = parseTeamCreateArgs(['--open-tmux', '작업']);
|
|
3495
|
+
if (openTmuxFlagParsed.prompt !== '작업') throw new Error('selftest: team --open-tmux leaked into prompt');
|
|
3496
|
+
const noOpenTmuxFlagParsed = parseTeamCreateArgs(['--no-open-tmux', '작업']);
|
|
3497
|
+
if (noOpenTmuxFlagParsed.prompt !== '작업') throw new Error('selftest: team --no-open-tmux leaked into prompt');
|
|
3498
|
+
const roleTeamPlan = buildTeamPlan(teamId, '역할 팀 테스트', { roleCounts: roleParsed.roleCounts });
|
|
3499
|
+
if (roleTeamPlan.roster.debate_team.length !== 5) throw new Error('selftest: executor role count not reflected in debate team size');
|
|
3500
|
+
if (roleTeamPlan.roster.analysis_team.length !== 5) throw new Error('selftest: executor role count not reflected in analysis scout team');
|
|
3501
|
+
if (roleTeamPlan.roster.development_team.filter((agent) => agent.role === 'executor').length !== 5) throw new Error('selftest: executor role count not reflected in development team');
|
|
3502
|
+
if (!roleTeamPlan.roster.debate_team.some((agent) => /inconvenience/.test(agent.persona))) throw new Error('selftest: user friction persona missing from debate team');
|
|
3503
|
+
const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
|
|
3504
|
+
if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest: Team tmux view did not expose agent live lanes');
|
|
3505
|
+
if (!roleTeamPlan.roster.analysis_team.every((agent) => tmuxTeam.agents.some((entry) => entry.agent === agent.id))) throw new Error('selftest: Team tmux view collapsed numbered analysis scout lanes');
|
|
3506
|
+
if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest: Team tmux view did not expose orchestration overview plus agent lanes');
|
|
3507
|
+
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'main-vertical' || tmuxTeam.split_ui?.right_side_only !== true || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest: tmux UI');
|
|
3508
|
+
if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest: Team tmux pane banner is too noisy or missing compact follow hint');
|
|
3509
|
+
if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest: Team tmux role palette did not classify lane roles');
|
|
3510
|
+
if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest: Team tmux view did not expose color/title metadata and cleanup policy');
|
|
3511
|
+
if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest: Team tmux session is not named for visibility');
|
|
3512
|
+
const fakeTmuxDir = path.join(tmp, 'fake-tmux');
|
|
3513
|
+
await ensureDir(fakeTmuxDir);
|
|
3514
|
+
const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
|
|
3515
|
+
const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
|
|
3516
|
+
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('fs'),e=process.env,r=process.argv.slice(2),c=r[0];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,r.join(' ')+'\\n');if(c==='new-session')console.log('%1');else if(c==='split-window')console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');else if(c==='list-windows')console.log('@1');else if(c==='display-message')console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');else if(c==='list-sessions')console.log(e.SKS_FAKE_TMUX_SESSIONS||'');else if(c==='list-panes'){let t=r[r.indexOf('-t')+1]||'';console.log(t[0]=='%'&&r.join(' ').includes('pane_dead')?'0\\t'+t:e.SKS_FAKE_TMUX_LIST||'')}\n`);
|
|
3517
|
+
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3518
|
+
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3519
|
+
const previousPath = process.env.PATH;
|
|
3520
|
+
process.env.SKS_FAKE_TMUX_LOG = fakeTmuxLog;
|
|
3521
|
+
process.env.PATH = `${fakeTmuxDir}${path.delimiter}${previousPath || ''}`;
|
|
3522
|
+
const recreatedTmux = await createTmuxSession({ root: tmp, session: 'sks-existing-selftest', tmux: { bin: fakeTmuxBin }, codex: { bin: process.execPath } }, [
|
|
3523
|
+
{ cwd: tmp, command: 'pwd', role: 'overview' },
|
|
3524
|
+
{ cwd: tmp, command: 'pwd', role: 'lane' }
|
|
3525
|
+
], { recreate: true });
|
|
3526
|
+
const fakeTmuxLogText = await safeReadText(fakeTmuxLog);
|
|
3527
|
+
if (!recreatedTmux.ok || !fakeTmuxLogText.includes('kill-session -t sks-existing-selftest') || !fakeTmuxLogText.includes('new-session') || !fakeTmuxLogText.includes('split-window')) throw new Error('selftest: tmux recreate did not replace stale existing session with split panes');
|
|
3528
|
+
if (!recreatedTmux.dynamic_resize?.enabled || !fakeTmuxLogText.includes('list-windows -t sks-existing-selftest -F #{window_id}') || !fakeTmuxLogText.includes('set-window-option -t @1 window-size latest') || !fakeTmuxLogText.includes('set-hook -t sks-existing-selftest client-resized') || !fakeTmuxLogText.includes('resize-window -t @1 -A')) throw new Error('selftest: tmux dynamic resize hooks were not installed for split panes');
|
|
3529
|
+
if (recreatedTmux.layout !== 'tiled' || Number(recreatedTmux.initial_size?.width || 0) < 120 || Number(recreatedTmux.initial_size?.height || 0) < 36) throw new Error('selftest: tmux dynamic resize metadata missing normalized initial size/layout');
|
|
3530
|
+
await ensureDir(path.join(tmp, '.sneakoscope', 'state'));
|
|
3531
|
+
await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-sessions.json'), {
|
|
3532
|
+
schema_version: 1,
|
|
3533
|
+
sessions: {
|
|
3534
|
+
'sks-existing-selftest': {
|
|
3535
|
+
session: 'sks-existing-selftest',
|
|
3536
|
+
root: tmp,
|
|
3537
|
+
panes: [{ pane_id: '%1', role: 'codex', title: 'Codex CLI' }]
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3542
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
|
|
3543
|
+
process.env.SKS_FAKE_TMUX_LIST = '';
|
|
3544
|
+
process.env.SKS_FAKE_TMUX_SPLIT_ID = '%80';
|
|
3545
|
+
const cockpitOpen = await reconcileTmuxTeamCockpit({
|
|
3546
|
+
root: tmp,
|
|
3547
|
+
missionId: teamId,
|
|
3548
|
+
plan: roleTeamPlan,
|
|
3549
|
+
dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
|
|
3550
|
+
control: { status: 'running' },
|
|
3551
|
+
tmux: { bin: fakeTmuxBin },
|
|
3552
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3553
|
+
});
|
|
3554
|
+
const cockpitOpenLog = await safeReadText(fakeTmuxLog);
|
|
3555
|
+
if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || cockpitOpen.main_pane_id !== '%1' || cockpitOpen.relayout?.layout_name !== 'main-vertical' || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -h -t %1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-pane -t %1') || !cockpitOpenLog.includes('select-layout -t @1 main-vertical')) throw new Error('selftest: split');
|
|
3556
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3557
|
+
process.env.SKS_FAKE_TMUX_SPLIT_ID = '%90';
|
|
3558
|
+
process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%84\tscout: analysis_scout_old\tnode\t1\told-team-mission\tanalysis_scout_old\tscout`;
|
|
3559
|
+
const cockpitDedupe = await reconcileTmuxTeamCockpit({
|
|
3560
|
+
root: tmp,
|
|
3561
|
+
missionId: teamId,
|
|
3562
|
+
plan: roleTeamPlan,
|
|
3563
|
+
dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
|
|
3564
|
+
control: { status: 'running' },
|
|
3565
|
+
tmux: { bin: fakeTmuxBin },
|
|
3566
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3567
|
+
});
|
|
3568
|
+
const cockpitDedupeLog = await safeReadText(fakeTmuxLog);
|
|
3569
|
+
if (!cockpitDedupe.ok || cockpitDedupe.closed_lane_count !== 2 || !cockpitDedupeLog.includes('kill-pane -t %82') || !cockpitDedupeLog.includes('kill-pane -t %84') || cockpitDedupeLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not prune duplicate or stale managed panes');
|
|
3570
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3571
|
+
process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`;
|
|
3572
|
+
const cockpitTerminal = await reconcileTmuxTeamCockpit({
|
|
3573
|
+
root: tmp,
|
|
3574
|
+
missionId: teamId,
|
|
3575
|
+
plan: roleTeamPlan,
|
|
3576
|
+
dashboard: { agents: { analysis_scout_1: { status: 'completed' } } },
|
|
3577
|
+
control: { status: 'running' },
|
|
3578
|
+
tmux: { bin: fakeTmuxBin },
|
|
3579
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3580
|
+
});
|
|
3581
|
+
const cockpitTerminalLog = await safeReadText(fakeTmuxLog);
|
|
3582
|
+
if (!cockpitTerminal.ok || cockpitTerminal.closed_lane_count !== 1 || cockpitTerminal.opened_lane_count !== 0 || !cockpitTerminalLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not close terminal agent pane');
|
|
3583
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3584
|
+
const staleTeamId = 'M-20260512-000000-old1';
|
|
3585
|
+
const missionDirOnlyTeamId = 'M-20260512-000000-dir1';
|
|
3586
|
+
await ensureDir(path.join(tmp, '.sneakoscope', 'missions', missionDirOnlyTeamId));
|
|
3587
|
+
await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-team-sessions.json'), {
|
|
3588
|
+
schema_version: 1,
|
|
3589
|
+
missions: {
|
|
3590
|
+
[staleTeamId]: {
|
|
3591
|
+
mission_id: staleTeamId,
|
|
3592
|
+
session: `sks-team-${staleTeamId}`,
|
|
3593
|
+
root: tmp,
|
|
3594
|
+
panes: [{ pane_id: '%201', title: 'scout: analysis_scout_1' }]
|
|
3595
|
+
},
|
|
3596
|
+
[teamId]: {
|
|
3597
|
+
mission_id: teamId,
|
|
3598
|
+
session: 'sks-existing-selftest',
|
|
3599
|
+
root: tmp,
|
|
3600
|
+
panes: [{ pane_id: '%204', title: 'scout: analysis_scout_1' }]
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3604
|
+
process.env.SKS_FAKE_TMUX_LIST = [
|
|
3605
|
+
'sks-existing-selftest\t@1\t%1\tCodex CLI\tnode\t\t\t\t',
|
|
3606
|
+
`sks-team-${staleTeamId}\t@70\t%201\tscout: analysis_scout_1\tnode\t\t\t\t`,
|
|
3607
|
+
`sks-existing-selftest\t@1\t%202\treview: stale_review\tnode\t1\t${staleTeamId}\tstale_review\treview`,
|
|
3608
|
+
`sks-team-${missionDirOnlyTeamId}\t@71\t%205\treview: reviewer_1\tnode\t\t\t\t`,
|
|
3609
|
+
'unrelated-session\t@9\t%203\tscout: analysis_scout_1\tnode\t\t\t\t',
|
|
3610
|
+
`sks-existing-selftest\t@1\t%204\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`
|
|
3611
|
+
].join('\n');
|
|
3612
|
+
const cockpitSweep = await sweepTmuxTeamSurfaces({
|
|
3613
|
+
root: tmp,
|
|
3614
|
+
keepMissionId: teamId,
|
|
3615
|
+
tmux: { bin: fakeTmuxBin },
|
|
3616
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3617
|
+
});
|
|
3618
|
+
const cockpitSweepLog = await safeReadText(fakeTmuxLog);
|
|
3619
|
+
if (!cockpitSweep.ok || cockpitSweep.closed_lane_count !== 3 || !cockpitSweepLog.includes('kill-pane -t %201') || !cockpitSweepLog.includes('kill-pane -t %202') || !cockpitSweepLog.includes('kill-pane -t %205') || cockpitSweepLog.includes('kill-pane -t %203') || cockpitSweepLog.includes('kill-pane -t %204') || cockpitSweepLog.includes('kill-pane -t %1')) throw new Error('selftest: tmux sweep did not close only stale recorded Team panes');
|
|
3620
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3621
|
+
const codexLbSuffix = defaultTmuxSessionName(tmp);
|
|
3622
|
+
const codexLbKeepSession = `sks-codex-lb-keep-${codexLbSuffix}`;
|
|
3623
|
+
const codexLbCurrentSession = `sks-codex-lb-current-${codexLbSuffix}`;
|
|
3624
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = `${codexLbCurrentSession}\t@1\t%1`;
|
|
3625
|
+
process.env.SKS_FAKE_TMUX_SESSIONS = [
|
|
3626
|
+
`sks-codex-lb-old-${codexLbSuffix}\t0\t100\t100`,
|
|
3627
|
+
`sks-codex-lb-attached-${codexLbSuffix}\t1\t101\t101`,
|
|
3628
|
+
`${codexLbKeepSession}\t0\t102\t102`,
|
|
3629
|
+
`${codexLbCurrentSession}\t0\t103\t103`,
|
|
3630
|
+
'sks-codex-lb-other-sks-other-00000000\t0\t104\t104'
|
|
3631
|
+
].join('\n');
|
|
3632
|
+
const codexLbSweep = await sweepCodexLbTmuxSessions({
|
|
3633
|
+
root: tmp,
|
|
3634
|
+
keepSession: codexLbKeepSession,
|
|
3635
|
+
tmux: { bin: fakeTmuxBin },
|
|
3636
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3637
|
+
});
|
|
3638
|
+
const codexLbSweepLog = await safeReadText(fakeTmuxLog);
|
|
3639
|
+
if (!codexLbSweep.ok || codexLbSweep.closed_session_count !== 1 || !codexLbSweepLog.includes(`kill-session -t sks-codex-lb-old-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t sks-codex-lb-attached-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbKeepSession}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbCurrentSession}`) || codexLbSweepLog.includes('kill-session -t sks-codex-lb-other')) throw new Error('selftest: codex-lb tmux sweep did not close only stale detached sessions for this repo');
|
|
3640
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3641
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
|
|
3642
|
+
const fakePanes = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
|
|
3643
|
+
process.env.SKS_FAKE_TMUX_LIST = fakePanes;
|
|
3644
|
+
const cockpitClose = await reconcileTmuxTeamCockpit({
|
|
3645
|
+
root: tmp,
|
|
3646
|
+
missionId: teamId,
|
|
3647
|
+
plan: roleTeamPlan,
|
|
3648
|
+
dashboard: { agents: { analysis_scout_2: { status: 'assigned' } } },
|
|
3649
|
+
control: { status: 'cleanup_requested' },
|
|
3650
|
+
close: true,
|
|
3651
|
+
tmux: { bin: fakeTmuxBin },
|
|
3652
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3653
|
+
});
|
|
3654
|
+
const cockpitCloseLog = await safeReadText(fakeTmuxLog);
|
|
3655
|
+
if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 2 || !cockpitCloseLog.includes('kill-pane -t %81') || !cockpitCloseLog.includes('kill-pane -t %82') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest: cleanup');
|
|
3656
|
+
delete process.env.SKS_FAKE_TMUX_DISPLAY;
|
|
3657
|
+
delete process.env.SKS_FAKE_TMUX_LIST;
|
|
3658
|
+
delete process.env.SKS_FAKE_TMUX_SESSIONS;
|
|
3659
|
+
delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
|
|
3660
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3661
|
+
const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
|
|
3662
|
+
const madTmuxLogText = await safeReadText(fakeTmuxLog);
|
|
3663
|
+
if (!madCockpit.created || madCockpit.mode !== 'mad_session' || madCockpit.opened?.panes?.length !== 1 || !madTmuxLogText.includes('new-session') || madTmuxLogText.includes('split-window')) throw new Error('selftest: MAD tmux launch should create one pane and leave split panes to Team lanes');
|
|
3664
|
+
if (previousFakeTmuxLog === undefined) delete process.env.SKS_FAKE_TMUX_LOG;
|
|
3665
|
+
else process.env.SKS_FAKE_TMUX_LOG = previousFakeTmuxLog;
|
|
3666
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
3667
|
+
else process.env.PATH = previousPath;
|
|
3668
|
+
const codexLaunchArgs = defaultCodexLaunchArgs({ SKS_CODEX_REASONING: 'low' }).join(' ');
|
|
3669
|
+
if (!codexLaunchArgs.includes('service_tier="fast"') || !codexLaunchArgs.includes('model_reasoning_effort="low"')) throw new Error('selftest: Codex tmux launch args do not force Fast service tier plus dynamic reasoning');
|
|
3670
|
+
await initTeamLive(teamId, teamDir, '역할 팀 테스트', { agentSessions: roleTeamPlan.agent_session_count, roleCounts: roleTeamPlan.role_counts, roster: roleTeamPlan.roster });
|
|
3671
|
+
const teamWatch = await renderTeamWatch(teamDir, { missionId: teamId });
|
|
3672
|
+
if (!roleTeamPlan.roster.analysis_team.every((agent) => teamWatch.includes(`- ${agent.id}:`))) throw new Error('selftest: Team watch overview collapsed numbered analysis scout lanes');
|
|
3673
|
+
if (routeReasoning(routePrompt('$Research frontier idea'), '$Research frontier idea').effort !== 'xhigh') throw new Error('selftest: research reasoning not xhigh');
|
|
3674
|
+
if (routeReasoning(routePrompt('$From-Chat-IMG 채팅 이미지 작업'), '$From-Chat-IMG 채팅 이미지 작업').effort !== 'xhigh') throw new Error('selftest: From-Chat-IMG reasoning not xhigh');
|
|
3675
|
+
if (routeReasoning(routePrompt('$Computer-Use localhost UI smoke'), '$Computer-Use localhost UI smoke').effort !== 'low') throw new Error('selftest: Computer Use fast lane reasoning not low');
|
|
3676
|
+
if (routeReasoning(routePrompt('$DB migration'), '$DB migration').effort !== 'high') throw new Error('selftest: logical reasoning not high');
|
|
3677
|
+
if (routeReasoning(routePrompt('$Team 간단한 코드 수정'), '$Team 간단한 코드 수정').effort !== 'low') throw new Error('selftest: simple Team reasoning not low');
|
|
3678
|
+
if (routeReasoning(routePrompt('$Team tmux CLI tool-calling fix'), '$Team tmux CLI tool-calling fix').effort !== 'medium') throw new Error('selftest: tool-heavy Team reasoning not medium');
|
|
3679
|
+
if (routeReasoning(routePrompt('$Team library research current docs'), '$Team library research current docs').effort !== 'high') throw new Error('selftest: research/docs Team reasoning not high');
|
|
3680
|
+
const lowReasoning = routeReasoning({ id: 'LowSmoke', reasoningPolicy: 'low' }, 'small metadata read');
|
|
3681
|
+
if (lowReasoning.effort !== 'low' || lowReasoning.profile !== 'sks-task-low') throw new Error('selftest: low reasoning did not route to sks-task-low');
|
|
3682
|
+
const forensicEffort = selectEffort({ mission_id: 'selftest', task_id: 'TASK-IMG', route: 'from-chat-img', prompt: '$From-Chat-IMG screenshot match' });
|
|
3683
|
+
if (forensicEffort.selected_effort !== 'forensic_vision' || !validateEffortDecision(forensicEffort).ok) throw new Error('selftest: From-Chat-IMG effort did not select forensic_vision');
|
|
3684
|
+
const lowEffort = selectEffort({ mission_id: 'selftest', task_id: 'TASK-LOW', is_deterministic: true, has_verified_skill: true });
|
|
3685
|
+
if (lowEffort.selected_effort !== 'low') throw new Error('selftest: deterministic verified skill did not select low effort');
|
|
3686
|
+
const recoveryEffort = selectEffort({ mission_id: 'selftest', task_id: 'TASK-RECOVERY', failure_count: 2 });
|
|
3687
|
+
if (recoveryEffort.selected_effort !== 'recovery') throw new Error('selftest: repeated failure did not select recovery effort');
|
|
3688
|
+
const invalidLedger = createWorkOrderLedger({ missionId: 'selftest', route: 'team', sourcesComplete: true, requests: [{ verbatim: 'do it', status: 'verified' }] });
|
|
3689
|
+
if (validateWorkOrderLedger(invalidLedger).ok) throw new Error('selftest: work-order ledger accepted verified item without evidence');
|
|
3690
|
+
const validLedger = createWorkOrderLedger({ missionId: 'selftest', route: 'team', sourcesComplete: true, requests: [{ verbatim: 'do it', implementation_tasks: ['TASK-001'], status: 'verified', implementation_evidence: ['file:src/core/routes.mjs'], verification_evidence: ['selftest'] }] });
|
|
3691
|
+
if (!validateWorkOrderLedger(validLedger).ok) throw new Error('selftest: valid work-order ledger rejected');
|
|
3692
|
+
const unresolvedVisualMap = buildFromChatImgVisualMap({ missionId: 'selftest', sources: [{ id: 'chat-img-1', type: 'chat_image', relevant: true, accounted_for: true }], regions: [{ image_id: 'chat-img-1', region_id: 'R01', status: 'uncertain' }] });
|
|
3693
|
+
if (validateFromChatImgVisualMap(unresolvedVisualMap).ok) throw new Error('selftest: unresolved From-Chat-IMG visual region accepted');
|
|
3694
|
+
const validVisualMap = buildFromChatImgVisualMap({ missionId: 'selftest', sources: [{ id: 'chat-img-1', type: 'chat_image', relevant: true, accounted_for: true }], regions: [{ image_id: 'chat-img-1', region_id: 'R01', observed_detail: 'button', matched_customer_request_ids: ['REQ-001'], confidence: 0.9, status: 'mapped' }] });
|
|
3695
|
+
if (!validateFromChatImgVisualMap(validVisualMap).ok) throw new Error('selftest: valid From-Chat-IMG visual map rejected');
|
|
3696
|
+
const dogfoodBlocked = createDogfoodReport({ scenario: 'selftest', computer_use_available: false, browser_available: false, cycles: 1, findings: [classifyDogfoodFinding({ id: 'DF-001', classification: 'fixable', description: 'broken' })], post_fix_verification_complete: false });
|
|
3697
|
+
if (validateDogfoodReport(dogfoodBlocked).ok) throw new Error('selftest: dogfood report accepted unresolved fixable finding');
|
|
3698
|
+
const dogfoodPassed = createDogfoodReport({ scenario: 'selftest', computer_use_available: true, browser_available: true, cycles: 2, findings: [classifyDogfoodFinding({ id: 'DF-001', classification: 'fixable', description: 'fixed', post_fix_verification: 'passed' })], post_fix_verification_complete: true });
|
|
3699
|
+
if (!validateDogfoodReport(dogfoodPassed).ok) throw new Error('selftest: dogfood report rejected post-fix verification');
|
|
3700
|
+
const skillCandidate = createSkillCandidate({ id: 'skill.from-chat-img.visual-work-order.v1', status: 'active', triggers: ['$From-Chat-IMG'], successful_runs: 3, files: ['.agents/skills/from-chat-img/SKILL.md'] });
|
|
3701
|
+
if (!validateSkillCandidate(skillCandidate).ok) throw new Error('selftest: active skill candidate rejected');
|
|
3702
|
+
const injection = decideSkillInjection({ route: 'from-chat-img', task_signature: 'reference images', skills: [skillCandidate, { ...skillCandidate, id: 'deprecated', status: 'deprecated' }] });
|
|
3703
|
+
if (!validateSkillInjectionDecision(injection).ok || injection.injected.length !== 1) throw new Error('selftest: skill injection did not respect active/top-K filtering');
|
|
3704
|
+
const skillDream = await skillDreamFixture(path.join(tmp, 'skill-dream-fixture'));
|
|
3705
|
+
if (!skillDream.passed) throw new Error('selftest: skill dreaming did not keep used skills, recommend unused generated skills, and preserve custom skills');
|
|
3706
|
+
const promptContext = buildPromptContext({ stable: ['stable'], policies: ['policy'], dynamic: ['dynamic'] });
|
|
3707
|
+
if (promptContext.blocks[0]?.cache_region !== 'stable_prefix' || promptContext.blocks.at(-1)?.cache_region !== 'dynamic_suffix') throw new Error('selftest: prompt context did not place dynamic context last');
|
|
3708
|
+
const repeatedMistake = await recordMistake(teamDir, { route: 'from-chat-img', gate: 'visual-map', reason: 'unmatched-reference' });
|
|
3709
|
+
const repeatedMistake2 = await recordMistake(teamDir, { route: 'from-chat-img', gate: 'visual-map', reason: 'unmatched-reference' });
|
|
3710
|
+
if (!repeatedMistake.ledger.entries.length || !repeatedMistake2.ledger.entries[0].prevention) throw new Error('selftest: repeated mistake did not attach prevention');
|
|
3711
|
+
if (routeReasoning(routePrompt('$DFix button label'), '$DFix button label').effort !== 'medium') throw new Error('selftest: simple reasoning not medium');
|
|
3712
|
+
if (routePrompt('이 파이프라인은 왜 이렇게 동작해?')?.id !== 'Answer') throw new Error('selftest: question prompt did not route to Answer');
|
|
3713
|
+
if (routePrompt('React useEffect 최신 문서 기준으로 설명해줘')?.id !== 'Answer') throw new Error('selftest: docs question did not route to Answer');
|
|
3714
|
+
if (routePrompt('질문을 하더라도 진짜 질문인지 아니면 질문형태를 띄는 암묵적인 지시인지를 반드시 파악해야해')?.id !== 'Team') throw new Error('selftest: question-shaped directive did not route to Team');
|
|
3715
|
+
if (routePrompt('근데 왜 팀원 구성을 안하고 작업을 하는 경우가 이렇게 많지?')?.id !== 'Team') throw new Error('selftest: question-shaped Team complaint did not route to Team');
|
|
3716
|
+
if (routePrompt('$DF button label')) throw new Error('selftest: deprecated $DF route still resolved');
|
|
3717
|
+
if (routePrompt('implement feature')?.id !== 'Team') throw new Error('selftest: implementation prompt did not default to Team');
|
|
3718
|
+
const broadMadTeamGoalPrompt = 'sks --mad tmux multi pane scout reasoning commit push $team $goal';
|
|
3719
|
+
if (routePrompt(broadMadTeamGoalPrompt)?.id !== 'Team') throw new Error('selftest: broad MAD/Team/Goal tmux request was misrouted away from Team');
|
|
3720
|
+
if (routePrompt('$SKS implement feature')?.id !== 'Team') throw new Error('selftest: $SKS implementation prompt did not promote to Team');
|
|
3721
|
+
if (routePrompt('$From-Chat-IMG 채팅 기록 이미지와 첨부 이미지로 고객사 요청 수정 작업 수행해줘')?.id !== 'Team') throw new Error('selftest: explicit chat capture client work did not promote to Team');
|
|
3722
|
+
if (routePrompt('$Computer-Use localhost 화면 빠르게 검증해줘')?.id !== 'ComputerUse') throw new Error('selftest: $Computer-Use did not route to ComputerUse fast lane');
|
|
3723
|
+
if (routePrompt('$CU localhost 화면 빠르게 검증해줘')?.id !== 'ComputerUse') throw new Error('selftest: $CU did not route to ComputerUse fast lane');
|
|
3724
|
+
if (routePrompt('computer use 사용하는 파이프라인은 마지막에 triwiki honest mode만 실행되게 조정해줘')?.id !== 'ComputerUse') throw new Error('selftest: Computer Use pipeline request was misrouted away from fast lane');
|
|
3725
|
+
if (routePrompt('triwiki나 honest mode가 마지막에만 실행되게 computer use 파이프라인 조정해줘')?.id !== 'ComputerUse') throw new Error('selftest: Computer Use directive was hijacked by Wiki route');
|
|
3726
|
+
if (routePrompt('$SKS show me available workflows')?.id !== 'SKS') throw new Error('selftest: $SKS workflow discovery should remain SKS');
|
|
3727
|
+
if (routeRequiresSubagents(routePrompt('이 파이프라인은 왜 이렇게 동작해?'), '이 파이프라인은 왜 이렇게 동작해?')) throw new Error('selftest: Answer route requires subagents');
|
|
3728
|
+
if (!routeRequiresSubagents(routePrompt('implement feature'), 'implement feature')) throw new Error('selftest: default Team implementation route does not require subagents');
|
|
3729
|
+
if (!routeRequiresSubagents(routePrompt('$Team implement feature'), '$Team implement feature')) throw new Error('selftest: Team route does not require subagents');
|
|
3730
|
+
if (routeRequiresSubagents(routePrompt('$Computer-Use localhost UI smoke'), '$Computer-Use localhost UI smoke')) throw new Error('selftest: Computer Use fast lane requires subagents');
|
|
3731
|
+
if (!routeRequiresSubagents(routePrompt('$Goal implement feature'), '$Goal implement feature')) throw new Error('selftest: Goal implementation route does not require subagents');
|
|
3732
|
+
if (routeRequiresSubagents(routePrompt('$Help commands'), '$Help commands')) throw new Error('selftest: Help route incorrectly requires subagents');
|
|
3733
|
+
if (!reflectionRequiredForRoute(routePrompt('$Team implement feature'))) throw new Error('selftest: Team route does not require reflection');
|
|
3734
|
+
if (reflectionRequiredForRoute(routePrompt('$Computer-Use localhost UI smoke'))) throw new Error('selftest: Computer Use fast lane requires full-route reflection');
|
|
3735
|
+
if (!reflectionRequiredForRoute(routePrompt('$DB migration'))) throw new Error('selftest: DB route does not require reflection');
|
|
3736
|
+
if (reflectionRequiredForRoute(routePrompt('$DFix button label'))) throw new Error('selftest: DFix route incorrectly requires reflection');
|
|
3737
|
+
if (reflectionRequiredForRoute(routePrompt('이 파이프라인은 왜 이렇게 동작해?'))) throw new Error('selftest: Answer route incorrectly requires reflection');
|
|
3738
|
+
if (!teamPlan.phases.some((phase) => phase.id === 'parallel_implementation')) throw new Error('selftest: team plan missing implementation phase');
|
|
3739
|
+
await initTeamLive(teamId, teamDir, '병렬 구현 팀 테스트', { roleCounts: roleParsed.roleCounts });
|
|
3740
|
+
await appendTeamEvent(teamDir, { agent: 'analysis_scout_1', phase: 'parallel_analysis_scouting', message: 'selftest mapped repo slice' });
|
|
3741
|
+
await appendTeamEvent(teamDir, { agent: 'team_consensus', phase: 'planning_debate', message: 'selftest mapped options' });
|
|
3742
|
+
const teamDashboard = await readTeamDashboard(teamDir);
|
|
3743
|
+
if (teamDashboard?.agent_session_count !== 6 || teamDashboard?.role_counts?.executor !== 5 || teamDashboard?.role_counts?.reviewer !== 6) throw new Error('selftest: team dashboard session/role budget missing');
|
|
3744
|
+
await writeTeamDashboardState(teamDir, { missionId: teamId, mission: { id: teamId, mode: 'team' }, effort: 'high', phase: 'verification' });
|
|
3745
|
+
const teamDashboardState = await readJson(path.join(teamDir, ARTIFACT_FILES.team_dashboard_state), {});
|
|
3746
|
+
if (!validateTeamDashboardState(teamDashboardState).ok || !renderTeamDashboardState(teamDashboardState).includes('Mission / Goal View')) throw new Error('selftest: Team dashboard state missing required cockpit panes');
|
|
3747
|
+
if (teamDashboard?.context_tracking?.ssot !== 'triwiki') throw new Error('selftest: team dashboard missing TriWiki context tracking');
|
|
3748
|
+
if (!teamDashboard?.phases?.includes('parallel_analysis_scouting')) throw new Error('selftest: team dashboard missing analysis scout phase');
|
|
3749
|
+
if (!teamDashboard?.latest_messages?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest: team live dashboard missing analysis scout event');
|
|
3750
|
+
if (!teamDashboard?.latest_messages?.some((entry) => entry.agent === 'team_consensus')) throw new Error('selftest: team live dashboard missing agent event');
|
|
3751
|
+
const teamLive = await readTeamLive(teamDir);
|
|
3752
|
+
if (!teamLive.includes('Analysis scouts') || !teamLive.includes('selftest mapped repo slice')) throw new Error('selftest: team live transcript missing analysis scout section/event');
|
|
3753
|
+
if (!teamLive.includes('sks team open-tmux')) throw new Error('selftest: team live transcript missing existing-mission tmux open command');
|
|
3754
|
+
if (!teamLive.includes('selftest mapped options')) throw new Error('selftest: team live transcript missing event');
|
|
3755
|
+
if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest: team live transcript missing TriWiki context tracking');
|
|
3756
|
+
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest: team transcript tail missing event');
|
|
3757
|
+
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: false });
|
|
3758
|
+
if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest: team agent lane missing event context');
|
|
3759
|
+
const missingChatLaneParts = [
|
|
3760
|
+
['codex chat heading', '## Codex Chat'],
|
|
3761
|
+
['lane speaker', 'me (analysis_scout_1)'],
|
|
3762
|
+
['status role metadata', '[status/scout]'],
|
|
3763
|
+
['agent event body', 'selftest mapped repo slice']
|
|
3764
|
+
].filter(([, needle]) => !teamLane.includes(needle)).map(([label]) => label);
|
|
3765
|
+
if (missingChatLaneParts.length || teamLane.includes('## Global Tail')) {
|
|
3766
|
+
const reason = [
|
|
3767
|
+
missingChatLaneParts.length ? `missing ${missingChatLaneParts.join(', ')}` : null,
|
|
3768
|
+
teamLane.includes('## Global Tail') ? 'unexpected global tail' : null
|
|
3769
|
+
].filter(Boolean).join('; ');
|
|
3770
|
+
throw new Error(`selftest: chat lane (${reason})\n${teamLane.slice(0, 1600)}`);
|
|
3771
|
+
}
|
|
3772
|
+
if (!teamLane.includes('╭─') || !teamLane.includes('│ selftest mapped repo slice') || !teamLane.includes('╰─')) {
|
|
3773
|
+
throw new Error(`selftest: team chat lane did not render framed chat blocks\n${teamLane.slice(0, 1600)}`);
|
|
3774
|
+
}
|
|
3775
|
+
const teamLaneColor = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: true });
|
|
3776
|
+
if (!/\x1b\[[0-9;]+m/.test(teamLaneColor) || !teamLaneColor.includes('Lane color:')) throw new Error('selftest: team chat lane did not render ANSI color metadata/output');
|
|
3777
|
+
const teamLaneCli = await runProcess(process.execPath, [hookBin, 'team', 'lane', teamId, '--agent', 'analysis_scout_1', '--lines', '4'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
3778
|
+
if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest: sks team lane CLI did not render an agent lane');
|
|
3779
|
+
await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
|
|
3780
|
+
const buttonUxSchema = buildQuestionSchema('$Team 버튼 UX 수정');
|
|
3781
|
+
const buttonUxSlotIds = buttonUxSchema.slots.map((s) => s.id);
|
|
3782
|
+
if (buttonUxSlotIds.includes('UI_STATE_BEHAVIOR') || buttonUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest: predictable UI defaults should be inferred, not asked');
|
|
3783
|
+
if (buttonUxSlotIds.length) throw new Error(`selftest: clear small UI work should auto-seal, got ${buttonUxSlotIds.join(',')}`);
|
|
3784
|
+
if (buttonUxSchema.inferred_answers.UI_STATE_BEHAVIOR !== 'infer_from_task_context_and_existing_design_system; preserve existing loading/error/empty/retry behavior unless explicitly requested; add only standard states required by the touched surface') throw new Error('selftest: UI state default inference missing');
|
|
3785
|
+
if (buttonUxSchema.inferred_answers.VISUAL_REGRESSION_REQUIRED !== 'yes_if_available') throw new Error('selftest: visual regression default inference missing');
|
|
3786
|
+
const predictableAuthCliSchema = buildQuestionSchema('회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘');
|
|
3787
|
+
const predictableAuthCliSlotIds = predictableAuthCliSchema.slots.map((s) => s.id);
|
|
3788
|
+
if (predictableAuthCliSlotIds.length) throw new Error(`selftest: clear auth-worded CLI rendering work should auto-seal, got ${predictableAuthCliSlotIds.join(',')}`);
|
|
3789
|
+
if (!predictableAuthCliSchema.inferred_answers.RISK_BOUNDARY?.includes('no destructive commands or live data writes')) throw new Error('selftest: predictable auth-worded CLI work did not infer conservative risk boundary');
|
|
3790
|
+
const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
|
|
3791
|
+
const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
|
|
3792
|
+
if (vagueSlotIds.length !== 0) throw new Error(`selftest: vague work should auto-seal inferred defaults without visible questions, got ${vagueSlotIds.join(',')}`);
|
|
3793
|
+
if (!vagueSchema.inferred_answers?.GOAL_PRECISE || !vagueSchema.inferred_answers?.ACCEPTANCE_CRITERIA) throw new Error('selftest: vague work did not infer core contract defaults');
|
|
3794
|
+
if (vagueSchema.ambiguity_assessment?.method !== 'weighted_clarity_interview' || !vagueSchema.ambiguity_assessment?.adversarial_lenses?.includes('challenge_framing')) throw new Error('selftest: ambiguity schema missing weighted clarity / planning lenses');
|
|
3795
|
+
const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
|
|
3796
|
+
if (pptRoute?.id !== 'PPT') throw new Error('selftest: $PPT did not route to presentation pipeline');
|
|
3797
|
+
if (JSON.stringify(pptRoute.requiredSkills) !== JSON.stringify(PPT_PIPELINE_SKILL_ALLOWLIST)) throw new Error(`selftest: PPT route required skills are not allowlisted: ${pptRoute.requiredSkills.join(',')}`);
|
|
3798
|
+
if (!pptRoute.requiredSkills.includes('imagegen')) throw new Error('selftest: PPT route must load imagegen so required PPT raster assets use Codex App $imagegen');
|
|
3799
|
+
if (pptRoute.requiredSkills.includes('design-artifact-expert') || pptRoute.requiredSkills.includes('design-ui-editor') || pptRoute.requiredSkills.includes('design-system-builder')) throw new Error('selftest: PPT route still requires generic design skills');
|
|
3800
|
+
const pptSchema = buildQuestionSchema('$PPT 투자자용 피치덱 만들어줘');
|
|
3801
|
+
const pptSlotIds = pptSchema.slots.map((s) => s.id);
|
|
3802
|
+
for (const id of ['PRESENTATION_DELIVERY_CONTEXT', 'PRESENTATION_AUDIENCE_PROFILE', 'PRESENTATION_STP_STRATEGY', 'PRESENTATION_PAINPOINT_SOLUTION_MAP', 'PRESENTATION_DECISION_CONTEXT']) {
|
|
3803
|
+
if (pptSlotIds.includes(id) || pptSchema.inferred_answers?.[id] === undefined) throw new Error(`selftest: PPT schema did not infer ${id}`);
|
|
3804
|
+
}
|
|
3805
|
+
const pptSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'ppt', 'SKILL.md'));
|
|
3806
|
+
if (!pptSkillText.includes('STP') || !pptSkillText.includes('target audience profile') || !pptSkillText.includes('decision context') || !pptSkillText.includes('3+ pain-point to solution mappings') || !pptSkillText.includes('Do not surface a prequestion sheet')) throw new Error('selftest: generated PPT skill missing inferred STP/audience/pain-point guidance');
|
|
3807
|
+
if (!pptSkillText.includes('simple, restrained, and information-first') || !pptSkillText.includes('over-designed decoration') || !pptSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !pptSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY) || !pptSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !pptSkillText.includes('only design decision SSOT') || !pptSkillText.includes('instead of treating references as parallel authorities')) throw new Error('selftest: generated PPT skill missing restrained design/imagegen/fused-SSOT guidance');
|
|
3808
|
+
if (!pptSkillText.includes('PPT pipeline allowlist') || !pptSkillText.includes('ignore installed skills and MCPs') || !pptSkillText.includes('prevent AI-like generic presentation design') || !pptSkillText.includes('Do not use generic design skills such as design-artifact-expert')) throw new Error('selftest: generated PPT skill missing pipeline allowlist enforcement');
|
|
3809
|
+
if (!pptSkillText.includes('source-html/') || !pptSkillText.includes('temporary build files') || !pptSkillText.includes('ppt-parallel-report.json')) throw new Error('selftest: generated PPT skill missing source preservation/temp cleanup/parallel guidance');
|
|
3810
|
+
if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('direct API fallback') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output') || !pptSkillText.includes('always loads imagegen') || !pptSkillText.includes('immediately invoke Codex App `$imagegen`')) throw new Error('selftest: generated PPT skill missing fact/image/review loop anti-fake guidance');
|
|
3811
|
+
if (routeRequiresSubagents(pptRoute, '$PPT 투자자용 피치덱 만들어줘')) throw new Error('selftest: PPT route should not require subagents by default');
|
|
3812
|
+
if (!reflectionRequiredForRoute(pptRoute)) throw new Error('selftest: PPT route should require reflection');
|
|
3813
|
+
const pptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 투자자용 피치덱 만들어줘' });
|
|
3814
|
+
await writeQuestions(pptMission.dir, pptSchema);
|
|
3815
|
+
const pptAnswers = {
|
|
3816
|
+
PRESENTATION_DELIVERY_CONTEXT: '대형 화면 16:9 발표, 한국어, 10분',
|
|
3817
|
+
PRESENTATION_AUDIENCE_PROFILE: '투자자, 평균 40대, VC/전략투자 직무, SaaS 이해도 높음, 의사결정권 있음',
|
|
3818
|
+
PRESENTATION_STP_STRATEGY: 'Segmentation: 초기 B2B SaaS 투자자; Targeting: 운영 효율 SaaS에 관심 있는 VC; Positioning: 작은 도입으로 반복 운영비를 줄이는 팀',
|
|
3819
|
+
PRESENTATION_PAINPOINT_SOLUTION_MAP: ['반복 리서치 비용 -> 자동화된 근거 수집 -> 비용 절감 아하', '검토 자료 품질 편차 -> 표준화된 스토리보드 -> 신뢰 아하', '도입 리스크 -> 작은 파일럿 -> 낮은 리스크 아하'],
|
|
3820
|
+
PRESENTATION_DECISION_CONTEXT: '파일럿 투자 승인이 목표이며, 시장 차별성과 실행 리스크가 주요 반대논리'
|
|
3821
|
+
};
|
|
3822
|
+
await writeJsonAtomic(path.join(pptMission.dir, 'answers.json'), pptAnswers);
|
|
3823
|
+
const pptSeal = await sealContract(pptMission.dir, pptMission.mission);
|
|
3824
|
+
if (!pptSeal.ok) throw new Error('selftest: PPT answers rejected');
|
|
3825
|
+
await materializeAfterPipelineAnswer(tmp, pptMission.id, pptMission.dir, pptMission.mission, pptRoute, { route: 'PPT', command: '$PPT', mode: 'PPT', task: pptMission.mission.prompt, context7_required: false }, pptSeal.contract);
|
|
3826
|
+
const pptAudienceStrategy = await readJson(path.join(pptMission.dir, PPT_AUDIENCE_STRATEGY_ARTIFACT));
|
|
3827
|
+
if (!pptAudienceStrategy?.source_answers?.PRESENTATION_STP_STRATEGY || pptAudienceStrategy.painpoint_solution_map.length !== 3) throw new Error('selftest: PPT audience strategy was not materialized from sealed answers');
|
|
3828
|
+
const pptGate = await readJson(path.join(pptMission.dir, PPT_GATE_ARTIFACT));
|
|
3829
|
+
if (pptGate.passed !== false || pptGate.audience_strategy_sealed !== true || pptGate.painpoint_count !== 3) throw new Error('selftest: PPT gate did not initialize with sealed audience strategy');
|
|
3830
|
+
await writeJsonAtomic(path.join(pptMission.dir, PPT_FACT_LEDGER_ARTIFACT), {
|
|
3831
|
+
schema_version: 1,
|
|
3832
|
+
web_research_performed: true,
|
|
3833
|
+
external_research_required: true,
|
|
3834
|
+
sources: [{ id: 'web-source-selftest', type: 'verified_web_source', url: 'https://example.com/ppt-source', support_status: 'verified' }],
|
|
3835
|
+
claims: [{ id: 'claim-selftest-market-risk', text: '시장 차별성과 실행 리스크는 외부 근거가 필요한 주장으로 분리된다.', source_ids: ['web-source-selftest'], support_status: 'supported', criticality: 'high', slide_refs: [2] }],
|
|
3836
|
+
unsupported_critical_claims: [],
|
|
3837
|
+
unsupported_critical_claims_count: 0,
|
|
3838
|
+
passed: true
|
|
3839
|
+
});
|
|
3840
|
+
const pptBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', pptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3841
|
+
if (pptBuildResult.code !== 0) throw new Error(`selftest: sks ppt build failed: ${pptBuildResult.stderr || pptBuildResult.stdout}`);
|
|
3842
|
+
const pptBuild = JSON.parse(pptBuildResult.stdout);
|
|
3843
|
+
if (!pptBuild.ok || !pptBuild.gate?.passed || !pptBuild.gate?.fact_ledger_created || !pptBuild.gate?.unsupported_critical_claims_zero || !pptBuild.gate?.image_asset_ledger_created || !pptBuild.gate?.image_asset_policy_satisfied || !pptBuild.gate?.review_policy_created || !pptBuild.gate?.review_ledger_created || !pptBuild.gate?.bounded_iteration_complete || !pptBuild.gate?.critical_review_issues_zero || !pptBuild.gate?.parallel_build_recorded || !pptBuild.gate?.html_artifact_created || !pptBuild.gate?.source_html_preserved || !pptBuild.gate?.pdf_exported_or_explicitly_deferred || !pptBuild.gate?.render_qa_recorded || !pptBuild.gate?.temp_cleanup_recorded) throw new Error('selftest: PPT build did not pass artifact gate');
|
|
3844
|
+
if (!PPT_HTML_ARTIFACT.startsWith(`${PPT_SOURCE_HTML_DIR}/`)) throw new Error('selftest: PPT HTML source must be stored in source-html folder');
|
|
3845
|
+
const pptHtml = await safeReadText(path.join(pptMission.dir, PPT_HTML_ARTIFACT));
|
|
3846
|
+
if (!pptHtml.includes('<html') || pptHtml.includes('gradient')) throw new Error('selftest: PPT HTML artifact missing or over-designed');
|
|
3847
|
+
const pptStyleTokens = await readJson(path.join(pptMission.dir, 'ppt-style-tokens.json'));
|
|
3848
|
+
if (pptStyleTokens.design_policy?.design_ssot?.authority !== DESIGN_SYSTEM_SSOT.authority_file || !pptStyleTokens.design_policy?.source_inputs?.some((entry) => entry.url === AWESOME_DESIGN_MD_REFERENCE.url && entry.role === 'source_input_for_ssot') || !pptStyleTokens.design_policy?.anti_generic_ai_style) throw new Error('selftest: PPT style tokens missing fused design SSOT/source-input anti-generic policy');
|
|
3849
|
+
if (!pptStyleTokens.design_policy?.design_reference_selection?.primary?.id?.startsWith('awesome-design-md:') || !pptStyleTokens.design_policy?.design_reference_selection?.selected_sources?.length || !pptStyleTokens.layout?.composition || !pptStyleTokens.layout?.treatment) throw new Error('selftest: PPT style tokens did not select and apply a concrete awesome-design-md reference profile');
|
|
3850
|
+
if (JSON.stringify(pptStyleTokens.design_policy?.pipeline_allowlist?.required_skills || []) !== JSON.stringify(PPT_PIPELINE_SKILL_ALLOWLIST) || !pptStyleTokens.design_policy?.pipeline_allowlist?.ignore_installed_out_of_pipeline_skills || !(pptStyleTokens.design_policy?.pipeline_allowlist?.ignored_design_skills_even_if_installed || []).includes('design-artifact-expert') || !/AI-like/.test(pptStyleTokens.design_policy?.pipeline_allowlist?.anti_ai_design_goal || '')) throw new Error('selftest: PPT style tokens missing skill/MCP allowlist enforcement');
|
|
3851
|
+
const audienceScript = pptHtml.match(/id="ppt-audience-strategy">([^<]+)<\/script>/);
|
|
3852
|
+
if (!audienceScript) throw new Error('selftest: PPT HTML missing audience strategy script data');
|
|
3853
|
+
JSON.parse(audienceScript[1]);
|
|
3854
|
+
if (!pptHtml.includes('id="ppt-fact-ledger"') || !pptHtml.includes('id="ppt-image-asset-ledger"') || !pptHtml.includes('id="ppt-review-policy"')) throw new Error('selftest: PPT HTML missing fact/image/review embedded ledgers');
|
|
3855
|
+
const pptPdfBytes = await fsp.readFile(path.join(pptMission.dir, PPT_PDF_ARTIFACT));
|
|
3856
|
+
if (pptPdfBytes.subarray(0, 5).toString('utf8') !== '%PDF-') throw new Error('selftest: PPT PDF artifact does not have a PDF header');
|
|
3857
|
+
const pptFactLedger = await readJson(path.join(pptMission.dir, PPT_FACT_LEDGER_ARTIFACT));
|
|
3858
|
+
if (!pptFactLedger.passed || pptFactLedger.unsupported_critical_claims_count !== 0 || !Array.isArray(pptFactLedger.claims)) throw new Error('selftest: PPT fact ledger did not pass unsupported-claim gate');
|
|
3859
|
+
const pptImageAssetLedger = await readJson(path.join(pptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
|
|
3860
|
+
if (!pptImageAssetLedger.passed || pptImageAssetLedger.required !== false || pptImageAssetLedger.planned_count !== 0 || pptImageAssetLedger.provider?.model !== 'gpt-image-2') throw new Error('selftest: PPT image asset ledger did not pass optional no-cost state');
|
|
3861
|
+
const pptReviewPolicy = await readJson(path.join(pptMission.dir, PPT_REVIEW_POLICY_ARTIFACT));
|
|
3862
|
+
if (pptReviewPolicy.visual_review?.model !== 'gpt-image-2' || pptReviewPolicy.max_full_deck_passes !== 2 || pptReviewPolicy.max_slide_retries !== 2 || pptReviewPolicy.score_threshold < 0.88) throw new Error('selftest: PPT review policy missing bounded gpt-image-2 loop settings');
|
|
3863
|
+
const pptReviewLedger = await readJson(path.join(pptMission.dir, PPT_REVIEW_LEDGER_ARTIFACT));
|
|
3864
|
+
if (!pptReviewLedger.passed || !pptReviewLedger.p0_p1_zero || pptReviewLedger.image_review_status !== 'not_required_or_not_available') throw new Error('selftest: PPT review ledger did not pass deterministic no-blocker state');
|
|
3865
|
+
const pptIterationReport = await readJson(path.join(pptMission.dir, PPT_ITERATION_REPORT_ARTIFACT));
|
|
3866
|
+
if (!pptIterationReport.passed || pptIterationReport.loop_policy?.max_full_deck_passes !== 2 || pptIterationReport.stop_reason !== 'score_threshold_met_and_no_p0_p1_issues') throw new Error('selftest: PPT iteration report did not record bounded pass termination');
|
|
3867
|
+
const pptRenderReport = await readJson(path.join(pptMission.dir, PPT_RENDER_REPORT_ARTIFACT));
|
|
3868
|
+
if (!pptRenderReport.passed || !pptRenderReport.design_policy_checks.every((check) => check.passed)) throw new Error('selftest: PPT render report did not pass design policy checks');
|
|
3869
|
+
const pptParallelReport = await readJson(path.join(pptMission.dir, PPT_PARALLEL_REPORT_ARTIFACT));
|
|
3870
|
+
if (!pptParallelReport.passed || pptParallelReport.parallel_group_count < 2 || !pptParallelReport.parallel_groups.some((group) => group.id === 'render_targets' && group.executed_in_parallel)) throw new Error('selftest: PPT parallel report did not record parallel build groups');
|
|
3871
|
+
const pptCleanupReport = await readJson(path.join(pptMission.dir, PPT_CLEANUP_REPORT_ARTIFACT));
|
|
3872
|
+
if (!pptCleanupReport.source_html_preserved || !pptCleanupReport.temp_cleanup_completed || pptCleanupReport.source_html_path !== PPT_HTML_ARTIFACT) throw new Error('selftest: PPT cleanup report did not preserve source HTML');
|
|
3873
|
+
if (await exists(path.join(pptMission.dir, PPT_TEMP_DIR))) throw new Error('selftest: PPT temp directory was not cleaned');
|
|
3874
|
+
if (await exists(path.join(pptMission.dir, 'artifact.html'))) throw new Error('selftest: legacy root PPT HTML should not remain after source-html preservation');
|
|
3875
|
+
const pptStatusResult = await runProcess(process.execPath, [hookBin, 'ppt', 'status', pptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3876
|
+
if (pptStatusResult.code !== 0 || !JSON.parse(pptStatusResult.stdout).ok) throw new Error('selftest: sks ppt status did not report the built gate');
|
|
3877
|
+
const requiredImagePptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 이미지 리소스 포함 투자자용 피치덱 만들어줘' });
|
|
3878
|
+
await writeQuestions(requiredImagePptMission.dir, pptSchema);
|
|
3879
|
+
await writeJsonAtomic(path.join(requiredImagePptMission.dir, 'answers.json'), {
|
|
3880
|
+
...pptAnswers,
|
|
3881
|
+
PRESENTATION_IMAGE_ASSETS_REQUIRED: 'yes',
|
|
3882
|
+
PRESENTATION_IMAGE_ASSET_REQUESTS: ['한국 B2B SaaS 운영 효율을 상징하는 첫 장용 히어로 이미지']
|
|
3883
|
+
});
|
|
3884
|
+
const requiredImageSeal = await sealContract(requiredImagePptMission.dir, requiredImagePptMission.mission);
|
|
3885
|
+
if (!requiredImageSeal.ok) throw new Error('selftest: PPT required-image answers rejected');
|
|
3886
|
+
await materializeAfterPipelineAnswer(tmp, requiredImagePptMission.id, requiredImagePptMission.dir, requiredImagePptMission.mission, pptRoute, { route: 'PPT', command: '$PPT', mode: 'PPT', task: requiredImagePptMission.mission.prompt, context7_required: false }, requiredImageSeal.contract);
|
|
3887
|
+
await writeJsonAtomic(path.join(requiredImagePptMission.dir, PPT_FACT_LEDGER_ARTIFACT), {
|
|
3888
|
+
schema_version: 1,
|
|
3889
|
+
web_research_performed: true,
|
|
3890
|
+
external_research_required: true,
|
|
3891
|
+
sources: [{ id: 'web-source-required-image-selftest', type: 'verified_web_source', url: 'https://example.com/ppt-source-image', support_status: 'verified' }],
|
|
3892
|
+
claims: [{ id: 'claim-required-image-selftest', text: '이미지 리소스 요구사항은 사실 검증과 별도 게이트로 차단되어야 한다.', source_ids: ['web-source-required-image-selftest'], support_status: 'supported', criticality: 'high', slide_refs: [1] }],
|
|
3893
|
+
unsupported_critical_claims: [],
|
|
3894
|
+
unsupported_critical_claims_count: 0,
|
|
3895
|
+
passed: true
|
|
3896
|
+
});
|
|
3897
|
+
const requiredImageBuildResult = await runProcess(process.execPath, [hookBin, 'ppt', 'build', requiredImagePptMission.id, '--json'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1', SKS_FAKE_IMAGE_GATE_TOKEN: 'ignored-by-sks-route-gate' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
3898
|
+
if (requiredImageBuildResult.code !== 0) throw new Error(`selftest: required-image PPT build command failed: ${requiredImageBuildResult.stderr || requiredImageBuildResult.stdout}`);
|
|
3899
|
+
const requiredImageBuild = JSON.parse(requiredImageBuildResult.stdout);
|
|
3900
|
+
const requiredImageLedger = await readJson(path.join(requiredImagePptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
|
|
3901
|
+
if (requiredImageBuild.ok || requiredImageBuild.gate?.passed || !requiredImageBuild.gate?.image_asset_ledger_created || requiredImageBuild.gate?.image_asset_policy_satisfied !== false || !requiredImageLedger.required || requiredImageLedger.passed || !requiredImageLedger.blockers?.includes('missing_codex_app_imagegen_gpt_image_2_asset_evidence') || requiredImageLedger.generated_count !== 0) throw new Error('selftest: required PPT image assets were not blocked without Codex App imagegen evidence');
|
|
3902
|
+
if (requiredImageLedger.imagegen_execution?.command !== '$imagegen' || requiredImageLedger.imagegen_execution?.required_skill !== 'imagegen' || !requiredImageLedger.assets?.every((asset) => asset.imagegen_invocation?.command === '$imagegen')) throw new Error('selftest: required PPT image assets did not carry Codex App $imagegen invocation instructions');
|
|
3903
|
+
const installUxSchema = buildQuestionSchema('SKS first install/bootstrap UX and Context7 MCP setup improvement');
|
|
3904
|
+
const installUxSlotIds = installUxSchema.slots.map((s) => s.id);
|
|
3905
|
+
if (installUxSchema.domain_hints.includes('uiux') || installUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest: CLI UX install prompt should not ask visual UI questions');
|
|
3906
|
+
if (installUxSlotIds.some((id) => /^(D|SUPA)/.test(id) && id !== 'DEPENDENCY_CHANGE_ALLOWED')) throw new Error('selftest: non-data MCP setup prompt asked guarded slots');
|
|
3907
|
+
if (installUxSlotIds.includes('MID_RUN_UNKNOWN_POLICY')) throw new Error('selftest: no-question fallback ladder should be inferred, not asked');
|
|
3908
|
+
const dbQuestionGateSchema = buildQuestionSchema('DB_SCHEMA_CHANGE_ALLOWED DATABASE_TARGET_ENVIRONMENT DATABASE_WRITE_MODE SUPABASE_MCP_POLICY DB_READ_ONLY_QUERY_LIMIT 이런 질문은 사용자에게 묻지 말고 알아서 판단해줘');
|
|
3909
|
+
const dbQuestionGateSlotIds = dbQuestionGateSchema.slots.map((s) => s.id);
|
|
3910
|
+
if (dbQuestionGateSlotIds.length) throw new Error(`selftest: predictable DB safety prompt should auto-seal, got ${dbQuestionGateSlotIds.join(',')}`);
|
|
3911
|
+
const { id, dir, mission } = await createMission(tmp, { mode: 'goal', prompt: '발표자료 만들어줘' });
|
|
3912
|
+
const schema = buildQuestionSchema(mission.prompt);
|
|
3913
|
+
await writeQuestions(dir, schema);
|
|
3914
|
+
if (!validateAnswers(schema, {}).ok || schema.slots.length !== 0) throw new Error('selftest: inferred empty answer set should be valid after prequestion removal');
|
|
3915
|
+
const answers = { ...(schema.inferred_answers || {}) };
|
|
3916
|
+
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
3917
|
+
const sealed = await sealContract(dir, mission);
|
|
3918
|
+
if (!sealed.ok) throw new Error('selftest: answers rejected');
|
|
3919
|
+
await setCurrent(tmp, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS' });
|
|
3920
|
+
if (!containsUserQuestion('확인해 주세요?')) throw new Error('selftest: question guard');
|
|
3921
|
+
if (classifySql('drop table users;').level !== 'destructive') throw new Error('selftest: destructive sql not detected');
|
|
3922
|
+
const patchPayloadClass = classifyToolPayload({ tool_name: 'apply_patch', command: '*** Update File: src/example.mjs\n+ok\n' });
|
|
3923
|
+
if (patchPayloadClass.level !== 'none') throw new Error('selftest: apply_patch file edits should not be classified as DB writes');
|
|
3924
|
+
const supabaseWritePayloadClass = classifyToolPayload({ tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'x' where id = '1';" });
|
|
3925
|
+
if (supabaseWritePayloadClass.level !== 'write' || !supabaseWritePayloadClass.toolReasons.includes('database_tool')) throw new Error('selftest: Supabase execute_sql write classification was weakened');
|
|
3926
|
+
if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest: supabase db reset not detected');
|
|
3927
|
+
const supabaseMigrationApplyClass = classifyCommand('supabase migration up --linked');
|
|
3928
|
+
if (supabaseMigrationApplyClass.level !== 'write' || !supabaseMigrationApplyClass.reasons.includes('supabase_migration_apply')) throw new Error('selftest: supabase migration apply was not classified as DB write');
|
|
3929
|
+
const supabaseDbPushClass = classifyCommand('supabase db push');
|
|
3930
|
+
if (supabaseDbPushClass.level !== 'write' || !supabaseDbPushClass.reasons.includes('supabase_db_push')) throw new Error('selftest: supabase db push was not classified as migration apply work');
|
|
3931
|
+
const supabaseApplyMigrationToolClass = classifyToolPayload({ tool_name: 'mcp__supabase__apply_migration', name: 'add_selftest_table' });
|
|
3932
|
+
if (supabaseApplyMigrationToolClass.level !== 'write' || !supabaseApplyMigrationToolClass.toolReasons.includes('migration_apply_tool')) throw new Error('selftest: Supabase apply_migration tool was not classified as DB write');
|
|
3933
|
+
const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringNoQuestion: true });
|
|
3934
|
+
if (dbDecision.action !== 'block') throw new Error('selftest: destructive MCP SQL allowed');
|
|
3935
|
+
const computerUseDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__computer_use__open_app', bundle_id: 'com.microsoft.edgemac', action: 'open_app' }, { duringNoQuestion: true });
|
|
3936
|
+
if (computerUseDecision.action !== 'allow') throw new Error('selftest: Computer Use MCP was blocked by DB safety gate');
|
|
3937
|
+
const madMission = await createMission(tmp, { mode: 'mad-sks', prompt: '$MAD-SKS selftest scoped DB override' });
|
|
3938
|
+
await writeJsonAtomic(path.join(madMission.dir, 'team-gate.json'), { schema_version: 1, passed: false, team_roster_confirmed: true });
|
|
3939
|
+
const madState = { mission_id: madMission.id, mode: 'TEAM', route_command: '$Team', stop_gate: 'team-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'team-gate.json' };
|
|
3940
|
+
const columnCleanupSql = 'alter table users ' + 'dr' + 'op column legacy_name;';
|
|
3941
|
+
const madColumnCleanupDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: columnCleanupSql }, { duringNoQuestion: false });
|
|
3942
|
+
if (madColumnCleanupDecision.action !== 'allow' || !madColumnCleanupDecision.mad_sks?.permission_profile?.allowed?.includes('direct_execute_sql_writes')) throw new Error('selftest: MAD-SKS column cleanup was not allowed through the modular permission gate');
|
|
3943
|
+
const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
|
|
3944
|
+
if (madLiveDmlDecision.action !== 'allow' || !madLiveDmlDecision.mad_sks?.live_server_writes_allowed) throw new Error('selftest: MAD-SKS targeted live DML was not allowed');
|
|
3945
|
+
const madMigrationUpDecision = await checkDbOperation(tmp, madState, { command: 'supabase migration up --linked' }, { duringNoQuestion: true });
|
|
3946
|
+
if (madMigrationUpDecision.action !== 'allow' || !madMigrationUpDecision.mad_sks?.permission_profile?.allowed?.includes('migration_apply_when_required')) throw new Error('selftest: MAD-SKS did not allow Supabase migration up during no-question execution');
|
|
3947
|
+
const madDbPushDecision = await checkDbOperation(tmp, madState, { command: 'supabase db push' }, { duringNoQuestion: true });
|
|
3948
|
+
if (madDbPushDecision.action !== 'allow') throw new Error('selftest: MAD-SKS did not allow Supabase db push migration application');
|
|
3949
|
+
const madApplyMigrationDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__apply_migration', name: 'add_selftest_table' }, { duringNoQuestion: true });
|
|
3950
|
+
if (madApplyMigrationDecision.action !== 'allow') throw new Error('selftest: MAD-SKS did not allow Supabase MCP apply_migration');
|
|
3951
|
+
const madTmuxMission = await createMission(tmp, { mode: 'mad-sks', prompt: 'sks --mad migration selftest' });
|
|
3952
|
+
await writeJsonAtomic(path.join(madTmuxMission.dir, 'mad-sks-gate.json'), { schema_version: 1, passed: false, mad_sks_permission_active: true, migration_apply_allowed: true });
|
|
3953
|
+
const madTmuxState = { mission_id: madTmuxMission.id, mode: 'MADSKS', route_command: '$MAD-SKS', stop_gate: 'mad-sks-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'mad-sks-gate.json', migration_apply_allowed: true };
|
|
3954
|
+
const madTmuxMigrationDecision = await checkDbOperation(tmp, madTmuxState, { command: 'supabase migration up --linked' }, { duringNoQuestion: true });
|
|
3955
|
+
if (madTmuxMigrationDecision.action !== 'allow') throw new Error('selftest: sks --mad state did not allow Supabase migration application');
|
|
3956
|
+
const tableRemovalSql = 'dr' + 'op table users;';
|
|
3957
|
+
const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
|
|
3958
|
+
if (madTableRemovalDecision.action !== 'block') throw new Error('selftest: MAD-SKS catastrophic table removal was not blocked');
|
|
3959
|
+
const allRowsSql = 'de' + 'lete from users;';
|
|
3960
|
+
const madAllRowsDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: allRowsSql }, { duringNoQuestion: false });
|
|
3961
|
+
if (madAllRowsDecision.action !== 'block') throw new Error('selftest: MAD-SKS all-row DML was not blocked');
|
|
3962
|
+
await writeJsonAtomic(path.join(madMission.dir, 'team-gate.json'), { schema_version: 1, passed: true, team_roster_confirmed: true, permissions_deactivated: true });
|
|
3963
|
+
const madClosedDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: columnCleanupSql }, { duringNoQuestion: false });
|
|
3964
|
+
if (madClosedDecision.action !== 'block') throw new Error('selftest: MAD-SKS permission persisted after gate close');
|
|
3965
|
+
const nonDbDecision = await checkDbOperation(tmp, {}, { command: 'npm test' }, { duringNoQuestion: true });
|
|
3966
|
+
if (nonDbDecision.action !== 'allow') throw new Error('selftest: non-DB command blocked by DB guard');
|
|
3967
|
+
const evalReport = runEvaluationBenchmark({ iterations: 5 });
|
|
3968
|
+
if (!evalReport.comparison.meaningful_improvement) throw new Error('selftest: evaluation benchmark did not show meaningful improvement');
|
|
3969
|
+
if (!evalReport.candidate.wiki?.valid) throw new Error('selftest: wiki coordinate index invalid in eval');
|
|
3970
|
+
if (evalReport.candidate.wiki?.voxel_schema !== 'sks.wiki-voxel.v1' || evalReport.candidate.wiki?.voxel_rows < 1) throw new Error('selftest: eval did not include voxel overlay metrics');
|
|
3971
|
+
const harnessReport = harnessGrowthReport({});
|
|
3972
|
+
if (!harnessReport.forgetting.fixture.passed || !harnessReport.tmux.views.includes('Harness Experiments View') || !harnessReport.reliability.tool_error_taxonomy.includes('Unknown')) throw new Error('selftest: harness growth fixture incomplete');
|
|
3973
|
+
const proofField = await proofFieldFixture();
|
|
3974
|
+
if (!proofField.validation.ok || !validateProofFieldReport(proofField.report).ok) throw new Error('selftest: proof field report invalid');
|
|
3975
|
+
if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.decision_lattice_present || !proofField.checks.decision_lattice_report_only || !proofField.checks.decision_lattice_selected_path || !proofField.checks.decision_lattice_frontier_present || !proofField.checks.decision_lattice_rejections_present || !proofField.checks.decision_lattice_scoring_formula_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest: proof field fixture checks incomplete');
|
|
3976
|
+
if (!speedLanePolicyText().includes('proof_field_fast_lane') || !proofField.report.execution_lane?.skip_when_fast?.includes('planning_debate')) throw new Error('selftest: Proof Field speed lane policy missing');
|
|
3977
|
+
const fastPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team small CLI help update'), task: 'small CLI help surface update', proofField: proofField.report });
|
|
3978
|
+
if (!validatePipelinePlan(fastPipelinePlan).ok || fastPipelinePlan.runtime_lane?.lane !== 'proof_field_fast_lane' || !fastPipelinePlan.skipped_stages.includes('planning_debate') || !fastPipelinePlan.invariants.includes('no_unrequested_fallback_code')) throw new Error('selftest: pipeline plan did not encode fast lane stage skips and fallback guard');
|
|
3979
|
+
const broadProofField = await buildProofField(tmp, { intent: 'database security route refactor', changedFiles: ['src/core/db-safety.mjs', 'src/core/routes.mjs', 'src/cli/main.mjs', 'README.md'] });
|
|
3980
|
+
const broadPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team database security route refactor'), task: 'database security route refactor', proofField: broadProofField });
|
|
3981
|
+
if (!validatePipelinePlan(broadPipelinePlan).ok || broadPipelinePlan.runtime_lane?.lane === 'proof_field_fast_lane' || broadPipelinePlan.skipped_stages.includes('planning_debate')) throw new Error('selftest: pipeline plan did not fail closed for broad/security work');
|
|
3982
|
+
if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key || !broadPipelinePlan.route_economy.decision_lattice?.report_only) throw new Error('selftest: route economy projection missing from pipeline plan');
|
|
3983
|
+
const workflowPerf = await runWorkflowPerfBench(tmp, {
|
|
3984
|
+
iterations: 2,
|
|
3985
|
+
intent: 'small CLI help surface update',
|
|
3986
|
+
changedFiles: ['src/cli/maintenance-commands.mjs', 'src/core/routes.mjs']
|
|
3987
|
+
});
|
|
3988
|
+
if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || workflowPerf.metrics.pipeline_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || !workflowPerf.metrics.decision_lattice_valid || Number(workflowPerf.metrics.decision_lattice_frontier_count) < 1 || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest: workflow perf proof field did not produce a valid outcome-scored fast lane report');
|
|
3989
|
+
if (classifyToolError({ message: 'operation timed out' }) !== 'Timeout' || classifyToolError({ message: 'unclassified weirdness' }) !== 'Unknown') throw new Error('selftest: tool error taxonomy classification');
|
|
3990
|
+
const coord = rgbaToWikiCoord({ r: 12, g: 34, b: 56, a: 255 });
|
|
3991
|
+
if (coord.schema !== 'sks.wiki-coordinate.v1' || coord.xyzw.length !== 4) throw new Error('selftest: RGBA wiki coordinate conversion');
|
|
3992
|
+
await writeTextAtomic(path.join(tmp, '.sneakoscope', 'memory', 'q2_facts', 'selftest.md'), '- claim: Selftest memory claim must be selected before lower-weight mission notes. | id: selftest-memory-priority | source: src/cli/main.mjs | risk: high | status: supported | evidence_count: 3 | required_weight: 1.0 | trust_score: 0.9\n');
|
|
3993
|
+
await writeTextAtomic(path.join(tmp, '.sneakoscope', 'memory', 'q2_facts', 'tail-repeat.md'), [
|
|
3994
|
+
...Array.from({ length: 60 }, (_, i) => `- claim: Low priority filler memory ${i}. | id: tail-filler-${i} | source: src/cli/main.mjs | risk: low | status: supported | evidence_count: 1 | required_weight: 0.1 | trust_score: 0.5`),
|
|
3995
|
+
'- claim: TriWiki repeated mistake recall must preserve recent high-weight tail lessons. | id: tail-repeat-mistake | source: src/core/mistake-recall.mjs | risk: high | status: supported | freshness: fresh | evidence_count: 4 | required_weight: 1.2 | trust_score: 0.95'
|
|
3996
|
+
].join('\n'));
|
|
3997
|
+
await createMission(tmp, { mode: 'sks', prompt: '모호한 질문은 그만 물어봐야지;; triwiki로 예측해' });
|
|
3998
|
+
await createMission(tmp, { mode: 'sks', prompt: 'triwiki에서 자주 요청하는 것들은 카운팅해서 더 우선 참고해줘' });
|
|
3999
|
+
const projectClaims = await projectWikiClaims(tmp);
|
|
4000
|
+
if (!projectClaims.some((claim) => claim.id === 'tail-repeat-mistake')) throw new Error('selftest: tail high-weight memory claim was dropped from TriWiki ingestion');
|
|
4001
|
+
const recallPrompt = 'triwiki 반복 실수 방지 개선 selftest';
|
|
4002
|
+
const recallMission = await createMission(tmp, { mode: 'team', prompt: recallPrompt });
|
|
4003
|
+
await writeJsonAtomic(path.join(recallMission.dir, 'required-answers.schema.json'), { prompt: recallPrompt, slots: [{ id: 'GOAL_PRECISE', required: true }, { id: 'ACCEPTANCE_CRITERIA', required: true, type: 'array' }] });
|
|
4004
|
+
await writeJsonAtomic(path.join(recallMission.dir, 'answers.json'), { GOAL_PRECISE: recallPrompt, ACCEPTANCE_CRITERIA: ['repeat mistake memory is consumed'] });
|
|
4005
|
+
const recallSeal = await sealContract(recallMission.dir, { id: recallMission.id, prompt: recallPrompt, mode: 'team' });
|
|
4006
|
+
if (!recallSeal.ok) throw new Error('selftest: mistake recall contract did not seal');
|
|
4007
|
+
const recallLedger = await readJson(path.join(recallMission.dir, MISTAKE_RECALL_ARTIFACT), null);
|
|
4008
|
+
if (!recallLedger?.required || !recallLedger.matches?.some((match) => match.id === 'tail-repeat-mistake')) throw new Error('selftest: mistake recall did not match tail TriWiki lesson');
|
|
4009
|
+
if (!contractConsumesMistakeRecall(recallSeal.contract, recallLedger).ok) throw new Error('selftest: mistake recall was not consumed by decision contract');
|
|
4010
|
+
const wikiPack = contextCapsule({
|
|
4011
|
+
mission: { id: 'selftest-wiki', coord: { rgba: { r: 48, g: 132, b: 212, a: 240 } } },
|
|
4012
|
+
role: 'verifier',
|
|
4013
|
+
claims: projectClaims,
|
|
4014
|
+
q4: { mode: 'selftest' },
|
|
4015
|
+
q3: ['sks', 'llm-wiki', 'wiki-coordinate'],
|
|
4016
|
+
budget: { maxWikiAnchors: 48, includeTrustSummary: true }
|
|
4017
|
+
});
|
|
4018
|
+
const wikiValidation = validateWikiCoordinateIndex(wikiPack.wiki);
|
|
4019
|
+
if (!wikiValidation.ok) throw new Error('selftest: wiki coordinate pack invalid');
|
|
4020
|
+
if (wikiPack.wiki.vx?.s !== 'sks.wiki-voxel.v1' || wikiVoxelRowCount(wikiPack.wiki) < 1) throw new Error('selftest: wiki voxel overlay missing');
|
|
4021
|
+
const legacyWiki = { ...wikiPack.wiki };
|
|
4022
|
+
delete legacyWiki.vx;
|
|
4023
|
+
const legacyValidation = validateWikiCoordinateIndex(legacyWiki);
|
|
4024
|
+
if (legacyValidation.ok || !legacyValidation.issues.some((issue) => issue.id === 'vx_missing')) throw new Error('selftest: legacy coordinate-only wiki pack was accepted');
|
|
4025
|
+
if (!wikiPack.trust_summary || !Number.isFinite(Number(wikiPack.trust_summary.needs_evidence))) throw new Error('selftest: wiki trust summary missing');
|
|
4026
|
+
if (wikiPack.attention?.mode !== 'aggressive_triwiki_active_recall' || !wikiPack.attention.use_first?.length || !wikiPack.attention.hydrate_first?.length) throw new Error('selftest: wiki active attention ranking missing');
|
|
4027
|
+
if (!wikiPack.attention.use_first.every((row) => Array.isArray(row) && row[0] && row[1] && row[2])) throw new Error('selftest: wiki attention use_first rows are not hydratable anchors');
|
|
4028
|
+
if (!wikiPack.claims?.some((claim) => claim.id === 'wiki-aggressive-active-recall')) throw new Error('selftest: aggressive TriWiki attention claim missing from pack');
|
|
4029
|
+
if (!(wikiPack.wiki.anchors || wikiPack.wiki.a || []).some((anchor) => Array.isArray(anchor) ? Number.isFinite(Number(anchor[9])) : Number.isFinite(Number(anchor.trust_score)))) throw new Error('selftest: wiki anchor trust score missing');
|
|
4030
|
+
if (!(wikiPack.wiki.anchors || wikiPack.wiki.a || []).some((anchor) => (Array.isArray(anchor) ? anchor[0] : anchor.id) === 'wiki-trig')) throw new Error('selftest: wiki trig anchor missing');
|
|
4031
|
+
if (!(wikiPack.wiki.anchors || wikiPack.wiki.a || []).some((anchor) => String(Array.isArray(anchor) ? anchor[0] : anchor.id).startsWith('team-analysis-'))) throw new Error('selftest: team analysis claim missing from TriWiki pack');
|
|
4032
|
+
if (!wikiPack.claims?.some((claim) => String(claim.id).startsWith('user-request-frequency-'))) throw new Error('selftest: repeated user request frequency claim missing from TriWiki pack');
|
|
4033
|
+
if (!wikiPack.claims?.some((claim) => String(claim.id).startsWith('user-strong-feedback-'))) throw new Error('selftest: strong user feedback claim missing from TriWiki pack');
|
|
4034
|
+
if (!wikiPack.claims?.some((claim) => claim.id === 'selftest-memory-priority')) throw new Error('selftest: memory required_weight claim was not selected in TriWiki pack');
|
|
4035
|
+
if (!wikiPack.claims?.some((claim) => claim.id === 'wiki-stack-current-docs-policy')) throw new Error('selftest: stack current-docs policy claim missing from TriWiki pack');
|
|
4036
|
+
if (!wikiPack.claims?.some((claim) => claim.id === 'wiki-stack-current-docs-vercel-duration')) throw new Error('selftest: Vercel duration current-docs claim missing from TriWiki pack');
|
|
4037
|
+
const cacheHitPack = contextCapsule({
|
|
4038
|
+
mission: { id: 'cache-hit-selftest', coord: { rgba: { r: 24, g: 24, b: 24, a: 255 } } },
|
|
4039
|
+
role: 'worker',
|
|
4040
|
+
claims: [
|
|
4041
|
+
{ id: 'cache-hit-core-1', text: 'Selected high-similarity claim must keep an anchor for attention cache hits.', authority: 'code', risk: 'low', status: 'supported', freshness: 'fresh', trust_score: 0.82, coord: { rgba: { r: 24, g: 24, b: 24, a: 255 } } },
|
|
4042
|
+
{ id: 'cache-hit-core-2', text: 'Second selected high-similarity claim must keep an anchor for attention cache hits.', authority: 'code', risk: 'low', status: 'supported', freshness: 'fresh', trust_score: 0.82, coord: { rgba: { r: 24, g: 24, b: 25, a: 255 } } },
|
|
4043
|
+
...Array.from({ length: 8 }, (_, i) => ({ id: `cache-hit-distractor-${i}`, text: `High-priority distractor ${i}`, authority: 'code', risk: 'critical', status: 'supported', freshness: 'fresh', trust_score: 0.99, coord: { rgba: { r: 180 + i, g: 180, b: 180, a: 255 } } }))
|
|
4044
|
+
],
|
|
4045
|
+
q4: { mode: 'cache-hit-selftest' },
|
|
4046
|
+
q3: ['triwiki', 'cache-hit'],
|
|
4047
|
+
budget: { maxClaims: 2, maxWikiAnchors: 2, maxAttentionUse: 2 }
|
|
4048
|
+
});
|
|
4049
|
+
const cacheHitUseIds = new Set(cacheHitPack.attention?.use_first?.map((row) => row[0]) || []);
|
|
4050
|
+
if (!cacheHitUseIds.has('cache-hit-core-1') || !cacheHitUseIds.has('cache-hit-core-2')) throw new Error('selftest: selected TriWiki claims were not pinned as attention cache-hit anchors');
|
|
4051
|
+
const primingPack = contextCapsule({
|
|
4052
|
+
mission: { id: 'positive-recall-selftest', coord: { rgba: { r: 64, g: 96, b: 128, a: 255 } } },
|
|
4053
|
+
role: 'worker',
|
|
4054
|
+
claims: [
|
|
4055
|
+
{ id: 'positive-recall-guard', text: 'Do not imagine elephant during TriWiki recall.', authority: 'code', risk: 'high', status: 'supported', freshness: 'fresh', required_weight: 1.4, trust_score: 0.95, coord: { rgba: { r: 64, g: 96, b: 128, a: 255 } } }
|
|
4056
|
+
],
|
|
4057
|
+
q4: { mode: 'positive-recall-selftest' },
|
|
4058
|
+
q3: ['triwiki', 'positive-recall'],
|
|
4059
|
+
budget: { maxClaims: 1, maxWikiAnchors: 1, maxAttentionUse: 1, maxAttentionHydrate: 1 }
|
|
4060
|
+
});
|
|
4061
|
+
const primingClaim = primingPack.claims?.find((claim) => claim.id === 'positive-recall-guard');
|
|
4062
|
+
if (!primingClaim || /elephant|do\s+not/i.test(primingClaim.text || '') || primingClaim.text_policy !== 'positive_recall_negation_suppressed') throw new Error('selftest: TriWiki compact recall did not suppress negative priming text');
|
|
4063
|
+
if (!primingPack.attention?.hydrate_first?.some((row) => row[0] === 'positive-recall-guard' && String(row[1]).includes('negative_priming'))) throw new Error('selftest: negative priming claim was not source-hydration gated');
|
|
4064
|
+
const voxelPromotionPack = contextCapsule({
|
|
4065
|
+
mission: { id: 'voxel-promotion-selftest', coord: { rgba: { r: 70, g: 100, b: 130, a: 255 } } },
|
|
4066
|
+
role: 'worker',
|
|
4067
|
+
claims: [
|
|
4068
|
+
{ id: 'voxel-priority-hydrate', text: 'TriWiki memory repeat prevention should hydrate source evidence when priority route layers are high.', authority: 'code', risk: 'low', status: 'supported', freshness: 'fresh', required_weight: 1.25, trust_score: 0.95, coord: { rgba: { r: 70, g: 100, b: 130, a: 255 } } }
|
|
4069
|
+
],
|
|
4070
|
+
q4: { mode: 'voxel-promotion-selftest' },
|
|
4071
|
+
q3: ['triwiki', 'memory'],
|
|
4072
|
+
budget: { maxClaims: 1, maxWikiAnchors: 1, maxAttentionUse: 1, maxAttentionHydrate: 1 }
|
|
4073
|
+
});
|
|
4074
|
+
if (!voxelPromotionPack.attention?.hydrate_first?.some((row) => row[0] === 'voxel-priority-hydrate' && String(row[1]).startsWith('voxel:priority_route'))) throw new Error('selftest: voxel priority route did not promote hydration');
|
|
4075
|
+
const dryRunPack = await writeWikiContextPack(tmp, ['--max-anchors', '4'], { dryRun: true });
|
|
4076
|
+
if (wikiVoxelRowCount(dryRunPack.pack.wiki) !== 4) throw new Error('selftest: dry-run wiki pack did not build voxel rows');
|
|
4077
|
+
if (await exists(dryRunPack.file)) throw new Error('selftest: wiki refresh dry-run wrote context pack');
|
|
4078
|
+
await ensureDir(path.dirname(dryRunPack.file));
|
|
4079
|
+
await writeJsonAtomic(path.join(path.dirname(dryRunPack.file), 'low-trust-artifact.json'), { trust_summary: { avg: 0.1 }, wiki: { anchors: [] } });
|
|
4080
|
+
const wikiPruneDryRun = await pruneWikiArtifacts(tmp, { dryRun: true });
|
|
4081
|
+
if (wikiPruneDryRun.candidates < 1 || !wikiPruneDryRun.actions.some((action) => action.reason === 'low_wiki_trust')) throw new Error('selftest: wiki prune did not flag low-trust artifact');
|
|
4082
|
+
await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'wiki', 'context-pack.json'), wikiPack);
|
|
4083
|
+
const recallPulseRun = await writeRecallPulseArtifacts(tmp, {
|
|
4084
|
+
missionId: recallMission.id,
|
|
4085
|
+
state: { mission_id: recallMission.id, mode: 'team', route: 'team', phase: 'implementation', prompt: recallPrompt },
|
|
4086
|
+
stageId: 'before_implementation'
|
|
4087
|
+
});
|
|
4088
|
+
if (!recallPulseRun.decision?.report_only || !recallPulseRun.decision?.l1?.selected?.length || recallPulseRun.decision?.l2?.tier !== 'L2' || recallPulseRun.decision?.l3?.tier !== 'L3') throw new Error('selftest: RecallPulse did not write L1/L2/L3 report-only decision');
|
|
4089
|
+
if (!recallPulseRun.capsule?.report_only || !recallPulseRun.envelope?.claim_ids_supported?.includes('durable_status_ledger')) throw new Error('selftest: RecallPulse proof capsule/evidence envelope incomplete');
|
|
4090
|
+
const recallPulseStatusLedger = await readMissionStatusLedger(tmp, recallMission.id);
|
|
4091
|
+
if (!recallPulseStatusLedger?.entries?.length || !recallPulseStatusLedger.final_summary_projection?.last_user_visible) throw new Error('selftest: RecallPulse durable status ledger missing');
|
|
4092
|
+
const repeatedRecallPulseRun = await writeRecallPulseArtifacts(tmp, {
|
|
4093
|
+
missionId: recallMission.id,
|
|
4094
|
+
state: { mission_id: recallMission.id, mode: 'team', route: 'team', phase: 'implementation', prompt: recallPrompt },
|
|
4095
|
+
stageId: 'before_implementation'
|
|
4096
|
+
});
|
|
4097
|
+
if (repeatedRecallPulseRun.decision?.recommended_action !== 'suppress' || !repeatedRecallPulseRun.decision?.duplicate_suppression?.repeated) throw new Error('selftest: RecallPulse duplicate reminder suppression missing');
|
|
4098
|
+
const recallPulseEval = await evaluateRecallPulseFixtures(tmp, { missionId: recallMission.id, write: true });
|
|
4099
|
+
if (!recallPulseEval.passed || recallPulseEval.metrics.route_gate_agreement < 1 || recallPulseEval.metrics.unsupported_performance_claims !== 0) throw new Error('selftest: RecallPulse fixture eval failed');
|
|
4100
|
+
const { dir: researchDir, mission: researchMission } = await createMission(tmp, { mode: 'research', prompt: '새로운 코드 리뷰 방법론 연구' });
|
|
4101
|
+
const researchPlan = await writeResearchPlan(researchDir, researchMission.prompt, {});
|
|
4102
|
+
if (researchPlan.methodology !== 'genius-scout-council-frontier-discovery-loop' || researchPlan.web_research_policy?.mode !== 'layered_source_retrieval_and_triangulation') throw new Error('selftest: research plan contract');
|
|
4103
|
+
if (researchPlan.execution_policy?.default_max_cycles !== 12 || researchPlan.mutation_policy?.implementation_allowed !== false || !String(researchPlan.research_council?.debate_policy?.rule || '').includes('every scout records final agreement')) throw new Error('selftest: research consensus/no-code contract');
|
|
4104
|
+
if (!researchPlan.research_council?.scouts?.every((scout) => scout.agent_name && scout.display_name && scout.persona && scout.persona_boundary && scout.reasoning_effort === 'xhigh') || !researchPlan.research_council.scouts.some((scout) => scout.agent_name === 'Einstein Scout')) throw new Error('selftest: research scout persona contract missing from plan');
|
|
4105
|
+
const researchPaperArtifact = researchPlan.artifacts?.research_paper;
|
|
4106
|
+
if (!isDatedResearchPaperArtifact(researchPaperArtifact) || researchPaperArtifact === 'research-paper.md') throw new Error('selftest: research paper artifact filename is not dated and titled');
|
|
4107
|
+
const researchPrompt = buildResearchPrompt({ id: researchMission.id, mission: researchMission, plan: researchPlan, cycle: 1, previous: '' });
|
|
4108
|
+
if (!researchPrompt.includes('NO-CODE-MUTATION POLICY') || !researchPrompt.includes('not a fixed three-cycle run') || !researchPrompt.includes('unanimous_consensus=true') || !researchPrompt.includes('agent_name') || !researchPrompt.includes(researchPaperArtifact)) throw new Error('selftest: research prompt missing no-code unanimous consensus policy');
|
|
4109
|
+
const rArts = researchPlan.required_artifacts || [];
|
|
4110
|
+
for (const a of [rss, 'source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) if (!rArts.includes(a) || !(await exists(path.join(researchDir, a)))) throw new Error('selftest: research artifact');
|
|
4111
|
+
if (!rArts.includes(researchPaperArtifact) || rArts.includes('research-paper.md') || !rArts.includes(gos)) throw new Error('selftest: research paper');
|
|
4112
|
+
const initialResearchGate = await evaluateResearchGate(researchDir);
|
|
4113
|
+
if (initialResearchGate.passed || ['web_search_pass_missing', 'eureka_missing', 'debate_exchanges_missing', 'research_paper_missing', 'consensus_iteration_missing', 'unanimous_consensus_missing'].some((r) => !initialResearchGate.reasons.includes(r))) throw new Error('selftest: research gate');
|
|
4114
|
+
const researchGate = await writeMockResearchResult(researchDir, researchPlan);
|
|
4115
|
+
if (!researchGate.passed) throw new Error('selftest: mock research gate did not pass');
|
|
4116
|
+
if (!(await exists(path.join(researchDir, researchPaperArtifact))) || await exists(path.join(researchDir, 'research-paper.md'))) throw new Error('selftest: mock research paper filename did not use dated title artifact');
|
|
4117
|
+
const rm = researchGate.metrics || {};
|
|
4118
|
+
if (rm.research_paper_artifact !== researchPaperArtifact) throw new Error('selftest: research gate did not report dated paper artifact');
|
|
4119
|
+
if (rm.scout_persona_contract_ok !== true || (rm.scout_persona_issues || []).length) throw new Error('selftest: research scout persona contract did not pass');
|
|
4120
|
+
if (['independent_scouts', 'xhigh_scouts', 'eureka_moments', 'debate_participants', 'genius_opinion_summaries'].some((m) => rm[m] < 5) || ['counterevidence_sources', 'falsification_cases', 'triangulation_checks'].some((m) => rm[m] < 1) || rm.paper_sections < 8 || rm.citation_coverage !== true || rm.source_layers_covered < 7 || rm.consensus_iterations < 1 || rm.unanimous_consensus !== true || rm.consensus_agreed_scouts < 5) throw new Error('selftest: research metrics');
|
|
4121
|
+
await writeJsonAtomic(path.join(dir, 'done-gate.json'), { passed: true, unsupported_critical_claims: 0, database_safety_violation: false, database_safety_reviewed: true, visual_drift: 'low', wiki_drift: 'low', tests_required: false });
|
|
4122
|
+
const gate = await evaluateDoneGate(tmp, id);
|
|
4123
|
+
if (!gate.passed) throw new Error('selftest: done gate');
|
|
4124
|
+
const gxDir = path.join(tmp, '.sneakoscope', 'gx', 'cartridges', 'selftest');
|
|
4125
|
+
await writeJsonAtomic(path.join(gxDir, 'vgraph.json'), defaultVGraph('selftest'));
|
|
4126
|
+
await writeJsonAtomic(path.join(gxDir, 'beta.json'), defaultBeta('selftest'));
|
|
4127
|
+
const render = await renderCartridge(gxDir, { format: 'all' });
|
|
4128
|
+
if (!render.outputs.includes('render.svg')) throw new Error('selftest: gx svg not rendered');
|
|
4129
|
+
const validation = await validateCartridge(gxDir);
|
|
4130
|
+
if (!validation.ok) throw new Error('selftest: gx validation rejected');
|
|
4131
|
+
if (!validateWikiCoordinateIndex(validation.wiki_coordinates).ok) throw new Error('selftest: gx wiki coordinate validation rejected');
|
|
4132
|
+
const drift = await driftCartridge(gxDir);
|
|
4133
|
+
if (drift.status !== 'low') throw new Error('selftest: gx drift is high');
|
|
4134
|
+
const snapshot = await snapshotCartridge(gxDir);
|
|
4135
|
+
if (!snapshot.files.svg || !snapshot.files.html) throw new Error('selftest: gx snapshot incomplete');
|
|
4136
|
+
if (!validateWikiCoordinateIndex(snapshot.wiki_coordinates).ok) throw new Error('selftest: gx snapshot wiki coordinates invalid');
|
|
4137
|
+
const { dir: oldFromChatTempDir } = await createMission(tmp, { mode: 'team', prompt: '$From-Chat-IMG old temp TriWiki retention selftest' });
|
|
4138
|
+
await writeJsonAtomic(path.join(oldFromChatTempDir, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT), { schema_version: 1, scope: 'temporary', storage: 'triwiki', expires_after_sessions: 1, claims: [{ id: 'req-1', text: 'old temporary claim' }] });
|
|
4139
|
+
const oldMtime = new Date(Date.now() - 60 * 1000);
|
|
4140
|
+
await fsp.utimes(oldFromChatTempDir, oldMtime, oldMtime);
|
|
4141
|
+
await createMission(tmp, { mode: 'team', prompt: 'newer mission for temp TriWiki retention selftest' });
|
|
4142
|
+
const gc = await enforceRetention(tmp, { dryRun: true });
|
|
4143
|
+
if (!gc.report.exists) throw new Error('selftest: storage report');
|
|
4144
|
+
if (!gc.actions.some((action) => action.action === 'remove_from_chat_img_temp_triwiki')) throw new Error('selftest: From-Chat-IMG temporary TriWiki retention action missing');
|
|
4145
|
+
console.log(`${sksAsciiLogo()}\nselftest passed.`);
|
|
4146
|
+
console.log(`temp: ${tmp}`);
|
|
4147
|
+
}
|