sneakoscope 0.6.100 → 0.7.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![](https://github.com/mandarange/Sneakoscope-Codex/raw/dev/docs/assets/sneakoscope-codex-logo.png)
4
4
 
5
- Sneakoscope Codex (`sks`, displayed as `ㅅㅋㅅ`) is a Codex CLI/App harness for repeatable agent workflows. It adds terminal commands, Codex App `$` prompt commands, warp-native CLI workspaces, Team/QA/Research routes, a maximum-speed Computer Use lane, a fast Goal bridge for native `/goal` persistence, Context7 evidence checks, DB safety, TriWiki context tracking, lightweight skill dreaming, Honest Mode, and release-readiness gates.
5
+ Sneakoscope Codex (`sks`, displayed as `ㅅㅋㅅ`) is a Codex CLI/App harness for repeatable agent workflows. It adds terminal commands, Codex App `$` prompt commands, warp-native CLI workspaces, Team/QA/Research routes, inspectable pipeline plans, a maximum-speed Computer Use lane, a fast Goal bridge for native `/goal` persistence, Context7 evidence checks, DB safety, TriWiki context tracking, lightweight skill dreaming, Honest Mode, and release-readiness gates.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -45,6 +45,7 @@ sks selftest --mock
45
45
  | --- | --- |
46
46
  | CLI runtime | `sks warp open` and `sks --mad` explicitly launch Codex CLI with Warp; bare `sks` only prints help/readiness surfaces. |
47
47
  | Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. |
48
+ | Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
48
49
  | Team orchestration | Runs substantial work through ambiguity handling, scouts, TriWiki refresh, debate, runtime task graphs, worker inboxes, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence to skip unrelated pipeline work instead of expanding Team. |
49
50
  | Skill dreaming | Records cheap generated-skill usage counters in JSON and only periodically scans `.agents/skills` for keep, merge, prune, and improvement candidates. Reports are recommendation-only and never delete skills automatically. |
50
51
  | From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
@@ -218,6 +219,7 @@ sks harness fixture --json
218
219
  sks gx init homepage
219
220
  sks gx render homepage --format html
220
221
  sks validate-artifacts latest --json
222
+ sks pipeline plan latest --proof-field --json
221
223
  sks perf run --json
222
224
  sks perf workflow --json --intent "small CLI change" --changed src/cli/main.mjs,src/core/routes.mjs
223
225
  sks proof-field scan --json --intent "small CLI change"
@@ -226,7 +228,9 @@ sks skill-dream run --json
226
228
  sks code-structure scan --json
227
229
  ```
228
230
 
229
- `sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path.
231
+ `sks pipeline plan` is the 0.7 runtime map. It reads or refreshes `.sneakoscope/missions/<id>/pipeline-plan.json`, then shows which lane is active, which stages are kept or skipped, which verification commands are required, and whether the no-unrequested-fallback invariant is present.
232
+
233
+ `sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path. Use `sks pipeline plan --proof-field` after changed files are known to bind that Proof Field decision to the mission plan.
230
234
 
231
235
  `sks skill-dream` keeps generated skill complexity bounded without doing a heavy evaluation on every prompt. Route use writes compact counters to `.sneakoscope/skills/dream-state.json`; after the configured count/cooldown threshold, or when you run `sks skill-dream run`, SKS scans `.agents/skills` and writes `.sneakoscope/reports/skill-dream-latest.json` with keep, merge, prune, and improvement candidates. The report is intentionally advisory: deleting or merging skills requires explicit approval.
232
236
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.100",
4
+ "version": "0.7.1",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -14,7 +14,7 @@ import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-q
14
14
  import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
15
15
  import { emitHook } from '../core/hooks-runtime.mjs';
16
16
  import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
17
- import { classifySql, classifyCommand, checkDbOperation, handleMadSksUserConfirmation } from '../core/db-safety.mjs';
17
+ import { classifySql, classifyCommand, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
18
18
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
19
19
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
20
20
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
@@ -26,7 +26,7 @@ import { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, wri
26
26
  import { contextCapsule } from '../core/triwiki-attention.mjs';
27
27
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
28
28
  import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_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, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
29
- import { context7Evidence, evaluateStop, recordContext7Evidence, recordSubagentEvidence } from '../core/pipeline.mjs';
29
+ import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
30
30
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
31
31
  import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
32
32
  import { ARTIFACT_FILES, validateDogfoodReport, validateEffortDecision, validateFromChatImgVisualMap, validateSkillCandidate, validateSkillInjectionDecision, validateTeamDashboardState, validateWorkOrderLedger } from '../core/artifact-schemas.mjs';
@@ -37,7 +37,7 @@ import { classifyDogfoodFinding, createDogfoodReport, writeDogfoodReport } from
37
37
  import { createSkillCandidate, decideSkillInjection, skillDreamFixture, writeSkillCandidate, writeSkillForgeReport, writeSkillInjectionDecision } from '../core/skill-forge.mjs';
38
38
  import { classifyToolError, harnessGrowthReport } from '../core/evaluation.mjs';
39
39
  import { runWorkflowPerfBench, validateWorkflowPerfReport } from '../core/perf-bench.mjs';
40
- import { proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
40
+ import { buildProofField, proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
41
41
  import { recordMistake, writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
42
42
  import { buildPromptContext } from '../core/prompt-context-builder.mjs';
43
43
  import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
@@ -149,7 +149,7 @@ Usage:
149
149
  sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]
150
150
  sks qa-loop status <mission-id|latest>
151
151
  sks context7 check|setup|tools|resolve|docs|evidence ...
152
- sks pipeline status|resume [--json]
152
+ sks pipeline status|resume|plan [--json] [--proof-field]
153
153
  sks pipeline answer <mission-id|latest> <answers.json>
154
154
  sks guard check [--json]
155
155
  sks conflicts check|prompt [--json]
@@ -730,13 +730,16 @@ async function pipeline(sub = 'status', args = []) {
730
730
  const root = await sksRoot();
731
731
  const action = sub || 'status';
732
732
  if (action === 'answer') return pipelineAnswer(root, args);
733
+ if (action === 'plan') return pipelinePlan(root, args);
733
734
  const state = await readJson(stateFile(root), {});
734
735
  const evidence = await context7Evidence(root, state);
736
+ const plan = state.mission_id ? await readJson(path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT), null) : null;
735
737
  const stop = await evaluateStop(root, state, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
736
738
  const result = {
737
739
  root,
738
740
  state,
739
741
  context7: evidence,
742
+ plan: plan ? pipelinePlanSummary(plan, root, state.mission_id) : null,
740
743
  stop_gate: state.stop_gate || null,
741
744
  next_action: stop?.reason || 'No active blocking route gate detected.'
742
745
  };
@@ -747,12 +750,92 @@ async function pipeline(sub = 'status', args = []) {
747
750
  console.log(`Route: ${state.route_command || state.route || 'none'}`);
748
751
  console.log(`Phase: ${state.phase || 'IDLE'}`);
749
752
  console.log(`Mission: ${state.mission_id || 'none'}`);
753
+ if (plan) {
754
+ console.log(`Plan: ${path.relative(root, path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT))}`);
755
+ console.log(`Lane: ${plan.runtime_lane?.lane || 'unknown'} (${plan.runtime_lane?.source || 'unknown'})`);
756
+ console.log(`Stages: keep ${plan.stage_summary?.kept ?? '?'} / skip ${plan.stage_summary?.skipped ?? '?'}`);
757
+ }
750
758
  console.log(`Reasoning: ${state.reasoning_effort || 'medium'}${state.reasoning_profile ? ` (${state.reasoning_profile})` : ''}${state.reasoning_temporary ? ' temporary' : ''}`);
751
759
  console.log(`Stop gate: ${state.stop_gate || 'none'}`);
752
760
  console.log(`Context7: ${state.context7_required ? (evidence.ok ? 'ok' : 'required-missing') : 'optional'} (${evidence.count || 0} event(s))`);
753
761
  console.log(`Next: ${result.next_action}`);
754
762
  }
755
763
 
764
+ async function pipelinePlan(root, args = []) {
765
+ const state = await readJson(stateFile(root), {});
766
+ const missionArg = pipelineMissionArg(args);
767
+ const id = await resolveMissionId(root, missionArg);
768
+ let dir = null;
769
+ let mission = {};
770
+ let routeContext = {};
771
+ if (id) {
772
+ const loaded = await loadMission(root, id);
773
+ dir = loaded.dir;
774
+ mission = loaded.mission || {};
775
+ routeContext = await readJson(path.join(dir, 'route-context.json'), {});
776
+ const existing = await readJson(path.join(dir, PIPELINE_PLAN_ARTIFACT), null);
777
+ if (existing && !flag(args, '--refresh') && !flag(args, '--proof-field')) {
778
+ 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));
779
+ return printPipelinePlan(root, id, existing);
780
+ }
781
+ }
782
+ const intent = readOption(args, '--intent', routeContext.task || mission.prompt || state.prompt || '');
783
+ const route = ROUTES.find((candidate) => candidate.id === routeContext.route || candidate.command === routeContext.command || candidate.id === state.route || candidate.command === state.route_command)
784
+ || routePrompt(routeContext.command || state.route_command || intent || '$SKS');
785
+ const changedRaw = readOption(args, '--changed', '');
786
+ const proofField = flag(args, '--proof-field') ? await buildProofField(root, { intent, changedFiles: changedRaw ? changedRaw.split(',') : undefined }) : null;
787
+ const contract = dir ? await readJson(path.join(dir, 'decision-contract.json'), {}) : {};
788
+ const ambiguity = {
789
+ required: Boolean(routeContext.clarification_gate || state.ambiguity_gate_required),
790
+ passed: Boolean(state.ambiguity_gate_passed || state.clarification_passed),
791
+ status: state.clarification_required ? 'awaiting_answers' : (state.ambiguity_gate_passed ? 'contract_sealed' : undefined),
792
+ contract_hash: contract?.sealed_hash || null
793
+ };
794
+ const planInput = { missionId: id || null, route, task: intent, required: Boolean(routeContext.context7_required || state.context7_required), ambiguity, proofField };
795
+ const plan = dir ? await writePipelinePlan(dir, planInput) : buildPipelinePlan(planInput);
796
+ const validation = validatePipelinePlan(plan);
797
+ 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));
798
+ printPipelinePlan(root, id || 'none', plan);
799
+ }
800
+
801
+ function pipelineMissionArg(args = []) {
802
+ const valueFlags = new Set(['--intent', '--changed']);
803
+ for (let i = 0; i < args.length; i++) {
804
+ const arg = String(args[i]);
805
+ if (valueFlags.has(arg)) {
806
+ i++;
807
+ continue;
808
+ }
809
+ if (!arg.startsWith('--')) return arg;
810
+ }
811
+ return 'latest';
812
+ }
813
+
814
+ function pipelinePlanSummary(plan, root, id) {
815
+ return {
816
+ path: id ? path.join(missionDir(root, id), PIPELINE_PLAN_ARTIFACT) : null,
817
+ validation: validatePipelinePlan(plan),
818
+ lane: plan.runtime_lane?.lane || null,
819
+ source: plan.runtime_lane?.source || null,
820
+ kept: plan.stage_summary?.kept ?? null,
821
+ skipped: plan.stage_summary?.skipped ?? null,
822
+ next_actions: plan.next_actions || []
823
+ };
824
+ }
825
+
826
+ function printPipelinePlan(root, id, plan) {
827
+ const validation = validatePipelinePlan(plan);
828
+ console.log('SKS Pipeline Plan\n');
829
+ console.log(`Mission: ${id}`);
830
+ console.log(`Route: ${plan.route?.command || plan.route?.id || 'unknown'}`);
831
+ console.log(`Lane: ${plan.runtime_lane?.lane || 'unknown'} (${plan.runtime_lane?.source || 'unknown'})`);
832
+ console.log(`Valid: ${validation.ok ? 'yes' : `no (${validation.issues.join(', ')})`}`);
833
+ if (id && id !== 'none') console.log(`Artifact: ${path.relative(root, path.join(missionDir(root, id), PIPELINE_PLAN_ARTIFACT))}`);
834
+ console.log(`Stages: keep ${plan.stage_summary?.kept ?? 0}, skip ${plan.stage_summary?.skipped ?? 0}, n/a ${plan.stage_summary?.not_applicable ?? 0}`);
835
+ console.log(`Verify: ${(plan.verification || []).join('; ')}`);
836
+ console.log(`Next: ${(plan.next_actions || []).join(' -> ')}`);
837
+ }
838
+
756
839
  async function pipelineAnswer(root, args = []) {
757
840
  const [missionArg, answerFile] = args;
758
841
  const id = await resolveMissionId(root, missionArg);
@@ -772,6 +855,7 @@ async function pipelineAnswer(root, args = []) {
772
855
  || routePrompt(routeContext.command || routeContext.route || '$SKS');
773
856
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'pipeline.clarification.contract_sealed', route: route?.id || routeContext.route, hash: result.contract.sealed_hash });
774
857
  const materialized = await materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext, result.contract);
858
+ 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 } });
775
859
  if (route?.id === 'QALoop') await writeQaLoopArtifacts(dir, mission, result.contract);
776
860
  await setCurrent(root, {
777
861
  mission_id: id,
@@ -793,16 +877,19 @@ async function pipelineAnswer(root, args = []) {
793
877
  ambiguity_gate_required: true,
794
878
  ambiguity_gate_passed: true,
795
879
  implementation_allowed: true,
880
+ pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok,
881
+ pipeline_plan_path: PIPELINE_PLAN_ARTIFACT,
796
882
  reasoning_effort: route ? routeReasoning(route, routeContext.task || mission.prompt || '').effort : 'medium',
797
883
  reasoning_profile: route ? routeReasoning(route, routeContext.task || mission.prompt || '').profile : 'sks-task-medium',
798
884
  reasoning_temporary: true,
799
885
  prompt: materialized.prompt || routeContext.task || mission.prompt || '',
800
886
  ...materialized.state
801
887
  });
802
- 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 }, null, 2));
888
+ 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));
803
889
  console.log(`SKS ambiguity gate passed for ${id}`);
804
890
  console.log(`Route: ${route?.command || routeContext.command || '$SKS'}`);
805
891
  console.log(`Hash: ${result.contract.sealed_hash}`);
892
+ console.log(`Plan: ${path.relative(root, path.join(dir, PIPELINE_PLAN_ARTIFACT))}`);
806
893
  console.log('Next: continue the original route lifecycle using decision-contract.json.');
807
894
  }
808
895
 
@@ -1624,10 +1711,11 @@ async function doctor(args) {
1624
1711
  let conflictScan = await scanHarnessConflicts(root);
1625
1712
  let repairApplied = false;
1626
1713
  let globalSkillsRepair = null;
1714
+ const globalCommand = await globalSksCommand();
1627
1715
  if (flag(args, '--fix') && !conflictScan.hard_block) {
1628
- const fixScope = requestedScope || 'global';
1629
1716
  const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
1630
- await initProject(root, { installScope: fixScope, globalCommand: await globalSksCommand(), localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1717
+ const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
1718
+ await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1631
1719
  if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
1632
1720
  repairApplied = true;
1633
1721
  conflictScan = await scanHarnessConflicts(root);
@@ -1639,7 +1727,7 @@ async function doctor(args) {
1639
1727
  const pkgBytes = await dirSize(packageRoot()).catch(() => 0);
1640
1728
  const manifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
1641
1729
  const installScope = requestedScope || normalizeInstallScope(manifest?.installation?.scope || 'global');
1642
- const install = await installStatus(root, installScope);
1730
+ const install = await installStatus(root, installScope, { globalCommand });
1643
1731
  const dbPolicyExists = await exists(path.join(root, '.sneakoscope', 'db-safety.json'));
1644
1732
  const dbScan = await scanDbSafety(root).catch((err) => ({ ok: false, findings: [{ id: 'db_safety_scan_failed', severity: 'high', reason: err.message }] }));
1645
1733
  const context7Status = await checkContext7(root);
@@ -1781,7 +1869,10 @@ async function installStatus(root, scope, opts = {}) {
1781
1869
  const discoveredGlobalBin = await discoverGlobalSksCommand();
1782
1870
  const configuredGlobalBin = await configuredSksBin(opts.globalCommand);
1783
1871
  const globalBin = configuredGlobalBin || discoveredGlobalBin;
1784
- const commandPrefix = sksCommandPrefix(scope, { globalCommand: globalBin || undefined });
1872
+ const sourceProject = await isHarnessSourceProject(root).catch(() => false);
1873
+ const sourceBin = path.join(root, 'bin', 'sks.mjs');
1874
+ const sourceBinExists = sourceProject && await exists(sourceBin);
1875
+ const commandPrefix = sourceBinExists ? 'node ./bin/sks.mjs' : sksCommandPrefix(scope, { globalCommand: globalBin || undefined });
1785
1876
  const projectBin = path.join(root, 'node_modules', 'sneakoscope', 'bin', 'sks.mjs');
1786
1877
  const projectBinExists = await exists(projectBin);
1787
1878
  return {
@@ -1790,7 +1881,9 @@ async function installStatus(root, scope, opts = {}) {
1790
1881
  command_prefix: commandPrefix,
1791
1882
  global_bin: globalBin,
1792
1883
  project_bin: projectBin,
1793
- ok: scope === 'project' ? projectBinExists : Boolean(globalBin)
1884
+ source_project: sourceProject,
1885
+ source_bin: sourceBinExists ? sourceBin : null,
1886
+ ok: sourceBinExists || (scope === 'project' ? projectBinExists : Boolean(globalBin))
1794
1887
  };
1795
1888
  }
1796
1889
 
@@ -1951,18 +2044,47 @@ async function selftest() {
1951
2044
  const guardStatus = await harnessGuardStatus(tmp);
1952
2045
  if (!guardStatus.ok || !guardStatus.locked || guardStatus.source_exception) throw new Error('selftest failed: harness guard not locked in installed project');
1953
2046
  const repairTmp = tmpdir();
1954
- await initProject(repairTmp, {});
2047
+ await writeJsonAtomic(path.join(repairTmp, 'package.json'), { name: 'sneakoscope', version: '0.0.0', type: 'module' });
2048
+ await ensureDir(path.join(repairTmp, 'bin'));
2049
+ await writeTextAtomic(path.join(repairTmp, 'bin', 'sks.mjs'), '#!/usr/bin/env node\n');
2050
+ await ensureDir(path.join(repairTmp, 'src', 'core'));
2051
+ await writeTextAtomic(path.join(repairTmp, 'src', 'core', 'init.mjs'), '// source-project marker\n');
2052
+ await writeTextAtomic(path.join(repairTmp, 'src', 'core', 'hooks-runtime.mjs'), '// source-project marker\n');
2053
+ await initProject(repairTmp, { installScope: 'project', localOnly: true });
1955
2054
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'), 'tampered\n');
1956
2055
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
1957
2056
  await ensureDir(path.join(repairTmp, '.agents', 'skills', 'custom-keep'));
1958
2057
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md'), '---\nname: custom-keep\ndescription: User custom skill, not generated by SKS.\n---\n');
1959
2058
  await writeTextAtomic(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'), 'legacy mirror\n');
1960
- await initProject(repairTmp, { force: true, repair: true });
2059
+ await writeTextAtomic(path.join(repairTmp, '.codex', 'hooks.json'), '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "tampered hook" }] }] } }\n');
2060
+ await writeTextAtomic(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'), 'tampered quick reference\n');
2061
+ await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'policy.json'), { broken: true });
2062
+ const existingAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
2063
+ 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'));
2064
+ const doctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--local-only', '--json'], {
2065
+ cwd: repairTmp,
2066
+ env: { HOME: path.join(repairTmp, 'home'), SKS_DISABLE_UPDATE_CHECK: '1' },
2067
+ timeoutMs: 30000,
2068
+ maxOutputBytes: 1024 * 1024
2069
+ });
2070
+ if (doctorRepair.code !== 0) throw new Error(`selftest failed: doctor --fix exited ${doctorRepair.code}: ${doctorRepair.stderr}`);
2071
+ const doctorRepairJson = JSON.parse(doctorRepair.stdout || '{}');
2072
+ if (!doctorRepairJson.repair?.applied || doctorRepairJson.install?.scope !== 'project' || !doctorRepairJson.install?.ok || !doctorRepairJson.install?.source_project) throw new Error('selftest failed: doctor --fix did not preserve project source install scope');
2073
+ const repairedManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
2074
+ if (repairedManifest.installation?.scope !== 'project' || repairedManifest.installation?.hook_command_prefix !== 'node ./bin/sks.mjs') throw new Error('selftest failed: doctor --fix rewrote project source install scope to the wrong command prefix');
1961
2075
  const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
1962
2076
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest failed: doctor repair did not regenerate team skill');
1963
2077
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove deprecated agent-team alias skill');
1964
2078
  if (!(await exists(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md')))) throw new Error('selftest failed: doctor repair removed a user-owned custom skill');
1965
2079
  if (await exists(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove legacy .codex/skills');
2080
+ const repairedQuickReference = await safeReadText(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'));
2081
+ if (!repairedQuickReference.includes('Install scope: `project`') || repairedQuickReference.includes('tampered')) throw new Error('selftest failed: doctor --fix did not regenerate quick reference');
2082
+ const repairedHooks = await safeReadText(path.join(repairTmp, '.codex', 'hooks.json'));
2083
+ if (!repairedHooks.includes('node ./bin/sks.mjs hook stop') || repairedHooks.includes('tampered hook')) throw new Error('selftest failed: doctor --fix did not regenerate Codex hooks');
2084
+ const repairedPolicy = await readJson(path.join(repairTmp, '.sneakoscope', 'policy.json'));
2085
+ if (repairedPolicy.broken || repairedPolicy.installation?.scope !== 'project' || !repairedPolicy.prompt_pipeline?.dollar_commands?.includes('$Team')) throw new Error('selftest failed: doctor --fix did not regenerate policy');
2086
+ const repairedAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
2087
+ if (!repairedAgentsMd.includes('Do not create unrequested fallback implementation code') || repairedAgentsMd.includes('tampered managed block')) throw new Error('selftest failed: doctor --fix did not repair AGENTS managed block');
1966
2088
  const conflictTmp = tmpdir();
1967
2089
  await ensureDir(path.join(conflictTmp, '.omx'));
1968
2090
  const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
@@ -2388,6 +2510,7 @@ async function selftest() {
2388
2510
  if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('Codex plan-tool interaction')) throw new Error('selftest failed: $Team ambiguity gate did not inject plan-tool guidance');
2389
2511
  const hookTeamState = await readJson(stateFile(hookTeamTmp), {});
2390
2512
  if (hookTeamState.phase !== 'TEAM_CLARIFICATION_AWAITING_ANSWERS' || hookTeamState.implementation_allowed !== false) throw new Error('selftest failed: $Team hook did not lock execution behind ambiguity gate');
2513
+ if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pending pipeline plan');
2391
2514
  if (await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) throw new Error('selftest failed: Team plan was created before ambiguity gate passed');
2392
2515
  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 });
2393
2516
  if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
@@ -2425,8 +2548,9 @@ async function selftest() {
2425
2548
  const pipelineAnswerResult = await runProcess(process.execPath, [hookBin, 'pipeline', 'answer', 'latest', hookTeamAnswersPath], { cwd: hookTeamTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2426
2549
  if (pipelineAnswerResult.code !== 0) throw new Error(`selftest failed: pipeline answer exited ${pipelineAnswerResult.code}: ${pipelineAnswerResult.stderr}`);
2427
2550
  const answeredTeamState = await readJson(stateFile(hookTeamTmp), {});
2428
- if (answeredTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !answeredTeamState.ambiguity_gate_passed || answeredTeamState.implementation_allowed !== true || !answeredTeamState.team_plan_ready) throw new Error('selftest failed: pipeline answer did not materialize Team after ambiguity gate');
2551
+ if (answeredTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !answeredTeamState.ambiguity_gate_passed || answeredTeamState.implementation_allowed !== true || !answeredTeamState.team_plan_ready || !answeredTeamState.pipeline_plan_ready) throw new Error('selftest failed: pipeline answer did not materialize Team after ambiguity gate');
2429
2552
  if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'decision-contract.json')))) throw new Error('selftest failed: pipeline answer did not seal decision contract');
2553
+ if (validatePipelinePlan(await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT))).ok !== true) throw new Error('selftest failed: pipeline answer did not refresh a valid pipeline plan');
2430
2554
  if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-live.md')))) throw new Error('selftest failed: Team artifacts missing after ambiguity gate passed');
2431
2555
  const honestLoopTmp = tmpdir();
2432
2556
  await initProject(honestLoopTmp, {});
@@ -2968,12 +3092,17 @@ async function selftest() {
2968
3092
  if (!proofField.validation.ok || !validateProofFieldReport(proofField.report).ok) throw new Error('selftest failed: proof field report invalid');
2969
3093
  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.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest failed: proof field fixture checks incomplete');
2970
3094
  if (!speedLanePolicyText().includes('proof_field_fast_lane') || !proofField.report.execution_lane?.skip_when_fast?.includes('planning_debate')) throw new Error('selftest failed: Proof Field speed lane policy missing');
3095
+ const fastPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team small CLI help update'), task: 'small CLI help surface update', proofField: proofField.report });
3096
+ 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 failed: pipeline plan did not encode fast lane stage skips and fallback guard');
3097
+ 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'] });
3098
+ const broadPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team database security route refactor'), task: 'database security route refactor', proofField: broadProofField });
3099
+ if (!validatePipelinePlan(broadPipelinePlan).ok || broadPipelinePlan.runtime_lane?.lane === 'proof_field_fast_lane' || broadPipelinePlan.skipped_stages.includes('planning_debate')) throw new Error('selftest failed: pipeline plan did not fail closed for broad/security work');
2971
3100
  const workflowPerf = await runWorkflowPerfBench(tmp, {
2972
3101
  iterations: 2,
2973
3102
  intent: 'small CLI help surface update',
2974
3103
  changedFiles: ['src/cli/maintenance-commands.mjs', 'src/core/routes.mjs']
2975
3104
  });
2976
- if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest failed: workflow perf proof field did not produce a valid outcome-scored fast lane report');
3105
+ 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 || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest failed: workflow perf proof field did not produce a valid outcome-scored fast lane report');
2977
3106
  if (classifyToolError({ message: 'operation timed out' }) !== 'Timeout' || classifyToolError({ message: 'unclassified weirdness' }) !== 'Unknown') throw new Error('selftest failed: tool error taxonomy classification');
2978
3107
  const coord = rgbaToWikiCoord({ r: 12, g: 34, b: 56, a: 255 });
2979
3108
  if (coord.schema !== 'sks.wiki-coordinate.v1' || coord.xyzw.length !== 4) throw new Error('selftest failed: RGBA wiki coordinate conversion');
@@ -25,6 +25,7 @@ import { writeFromChatImgArtifacts } from '../core/from-chat-img-forensics.mjs';
25
25
  import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
26
26
  import { runPerfBench, runWorkflowPerfBench } from '../core/perf-bench.mjs';
27
27
  import { writeProofFieldReport } from '../core/proof-field.mjs';
28
+ import { PIPELINE_PLAN_ARTIFACT, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
28
29
  import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../core/goal-workflow.mjs';
29
30
  import { scanCodeStructure, writeCodeStructureReport } from '../core/code-structure.mjs';
30
31
  import { writeMemorySweepReport } from '../core/memory-governor.mjs';
@@ -1462,6 +1463,9 @@ export async function team(args) {
1462
1463
  let dashboardState = await writeTeamDashboardState(dir, { missionId: id, mission: { id, mode: 'team' }, effort: effortDecision.selected_effort, phase: 'intake', next_action: fromChatImgRequired ? 'complete visual source inventory and work-order mapping' : 'run Team analysis scouts' });
1463
1464
  await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
1464
1465
  dashboardState = await writeTeamDashboardState(dir, { missionId: id, mission: { id, mode: 'team' }, effort: effortDecision.selected_effort, phase: 'intake', next_action: fromChatImgRequired ? 'complete visual source inventory and work-order mapping' : 'run Team analysis scouts' });
1466
+ const route = routePrompt(`$Team ${prompt}`) || ROUTES.find((candidate) => candidate.id === 'Team');
1467
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: prompt, required: false, ambiguity: { required: false, status: 'team_cli_direct' } });
1468
+ await setCurrent(root, { mission_id: id, route: 'Team', route_command: '$Team', mode: 'TEAM', phase: 'TEAM_PARALLEL_ANALYSIS_SCOUTING', questions_allowed: false, implementation_allowed: true, context7_required: false, context7_verified: false, subagents_required: true, subagents_verified: false, reflection_required: true, visible_progress_required: true, context_tracking: 'triwiki', required_skills: route?.requiredSkills || ['team'], stop_gate: 'team-gate.json', reasoning_effort: 'high', reasoning_profile: 'sks-logic-high', reasoning_temporary: true, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, team_live_ready: true, from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, prompt });
1465
1469
  const result = {
1466
1470
  mission_id: id,
1467
1471
  mission_dir: dir,
@@ -1477,6 +1481,7 @@ export async function team(args) {
1477
1481
  dashboard_state: path.join(dir, ARTIFACT_FILES.team_dashboard_state),
1478
1482
  effort_decision: path.join(dir, ARTIFACT_FILES.effort_decision),
1479
1483
  work_order_ledger: path.join(dir, ARTIFACT_FILES.work_order_ledger),
1484
+ pipeline_plan: path.join(dir, PIPELINE_PLAN_ARTIFACT),
1480
1485
  dashboard_state_valid: dashboardState.ok,
1481
1486
  context_pack: path.join(root, '.sneakoscope', 'wiki', 'context-pack.json'),
1482
1487
  agent_sessions: agentSessions,
@@ -1489,6 +1494,7 @@ export async function team(args) {
1489
1494
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
1490
1495
  console.log(`Team mission created: ${id}`);
1491
1496
  console.log(`Plan: ${path.relative(root, result.plan)}`);
1497
+ console.log(`Pipeline plan: ${path.relative(root, result.pipeline_plan)}`);
1492
1498
  console.log(`Agent sessions: ${agentSessions}`);
1493
1499
  console.log(`Role counts: ${formatRoleCounts(roleCounts)}`);
1494
1500
  console.log(`Workflow: ${path.relative(root, result.workflow)}`);
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.6.100';
8
+ export const PACKAGE_VERSION = '0.7.1';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -519,7 +519,7 @@ function codexAppQuickReference(scope, commandPrefix) {
519
519
  `Install scope: \`${scope}\``,
520
520
  `Command: \`${commandPrefix} <command>\``,
521
521
  'Files: AGENTS.md, .codex/hooks.json, .codex/config.toml, .codex/SNEAKOSCOPE.md, .agents/skills, .codex/agents, .sneakoscope/missions.',
522
- `Discover: ${commandPrefix} bootstrap; ${commandPrefix} deps check; ${commandPrefix} commands; ${commandPrefix} codex-app check; ${commandPrefix} warp check; ${commandPrefix} dollar-commands; ${commandPrefix} pipeline status.`,
522
+ `Discover: ${commandPrefix} bootstrap; ${commandPrefix} deps check; ${commandPrefix} commands; ${commandPrefix} codex-app check; ${commandPrefix} warp check; ${commandPrefix} dollar-commands; ${commandPrefix} pipeline status; ${commandPrefix} pipeline plan.`,
523
523
  'dollar-commands:',
524
524
  ...DOLLAR_COMMANDS.map((c) => `- \`${c.command}\`: ${c.route}`),
525
525
  `Picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}.`,
@@ -538,9 +538,9 @@ export async function installSkills(root) {
538
538
  const skills = {
539
539
  'dfix': `---\nname: dfix\ndescription: Ultralight fast design/content fix mode for $DFix or $dfix requests and inferred simple edits such as text color, copy, labels, spacing, or translation.\n---\n\nUse for tiny copy/color/label/spacing/translation edits. List exact micro-edits, inspect only needed files, apply only those edits, and run cheap verification. Bypass broad SKS routing, mission state, TriWiki/TriFix/reflection/state recording, Goal, Research, eval, redesign, and repeated full-route Honest Mode loops. Start the final answer with \`DFix 완료 요약:\` and include one \`DFix 솔직모드:\` line covering verified, not verified, and remaining issues. Read \`design.md\` for UI work when present; use imagegen for image/logo/raster assets.\n`,
540
540
  'answer': `---\nname: answer\ndescription: Answer-only research route for ordinary questions that should not start implementation.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer summary plus Honest Mode; do not create missions, subagents, or file edits.\n`,
541
- 'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline, skill-dream. Promote code-changing work to Team unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and require human-approved conflict cleanup. ${skillDreamPolicyText()}\n`,
541
+ 'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline status, pipeline plan, skill-dream. Promote code-changing work to Team unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and require human-approved conflict cleanup. ${skillDreamPolicyText()}\n`,
542
542
  'wiki': `---\nname: wiki\ndescription: Dollar-command route for $Wiki TriWiki refresh, pack, validate, and prune commands.\n---\n\nUse for $Wiki or Korean wiki-refresh requests. Refresh/update/갱신: run sks wiki refresh, then validate .sneakoscope/wiki/context-pack.json. Pack: run sks wiki pack, then validate. Prune/clean/정리: use sks wiki refresh --prune, or sks wiki prune --dry-run for inspection. Report claims, anchors, trust, attention.use_first/hydrate_first, validation, and blockers. Do not start ambiguity-gated implementation, subagents, or unrelated work.\n`,
543
- 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded Warp lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-warp or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
543
+ 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded Warp lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-warp or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
544
544
  'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Team alias for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the normal Team pipeline. Treat uploads as chat screenshot plus originals. Use Codex Computer Use visual inspection when available, list requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Team gates, review, reflection, and Honest Mode. ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. team-gate.json cannot pass From-Chat-IMG completion until unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
545
545
  'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Ask scope, target, mutation, login. Credentials are runtime-only; never save secrets. UI-level E2E needs Codex Computer Use evidence or must be marked unverified; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI/browser verification. Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
546
546
  'computer-use': `---\nname: computer-use\ndescription: Maximum-speed $Computer-Use/$CU lane for Codex Computer Use UI/browser/visual tasks.\n---\n\nUse only when the user invokes $Computer-Use/$CU or asks for a Computer Use-specific fast lane. Skip Team debate, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Infer the smallest target, use Codex Computer Use directly, and never substitute Playwright, Chrome MCP, Browser Use, Selenium, Puppeteer, or other browser automation for UI/browser evidence. If Computer Use is unavailable, mark UI/browser evidence unverified and stop with the blocker. At the end only, refresh or pack TriWiki, validate it, then provide a concise completion summary plus Honest Mode.\n`,
@@ -553,9 +553,9 @@ export async function installSkills(root) {
553
553
  'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened DB permission applies only while the active mission gate is open, must be deactivated when the task ends, and opens Supabase MCP column/schema cleanup, direct execute SQL, and normal DB write permissions. Keep only catastrophic database-wipe safeguards: whole database/table removal, all-row delete/update, reset, and dangerous project/branch management remain blocked. Do not carry MAD-SKS permission into later prompts or routes.\n`,
554
554
  'gx': `---\nname: gx\ndescription: Dollar-command route for $GX or $gx deterministic GX visual context cartridges.\n---\n\nUse when the user invokes $GX/$gx or asks for architecture/context visualization through SKS. Prefer sks gx init, render, validate, drift, and snapshot. vgraph.json remains the source of truth.\n`,
555
555
  'help': `---\nname: help\ndescription: Dollar-command route for $Help or $help explaining installed SKS commands and workflows.\n---\n\nUse when the user invokes $Help/$help or asks what commands exist. Prefer concise output from sks commands, sks usage <topic>, sks quickstart, sks aliases, and sks codex-app.\n`,
556
- 'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Ask only scope/safety/behavior/acceptance-changing questions; otherwise seal inferred answers. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: read design.md; if missing use design-system-builder; use imagegen for image/logo/raster. TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
556
+ 'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Ask only scope/safety/behavior/acceptance-changing questions; otherwise seal inferred answers. Materialize pipeline-plan.json for the runtime lane, kept/skipped stages, no-fallback invariant, and verification; inspect with sks pipeline plan, adding --proof-field when changed files are known. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: read design.md; if missing use design-system-builder; use imagegen for image/logo/raster. TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
557
557
  'reasoning-router': `---\nname: reasoning-router\ndescription: Temporary SKS reasoning-effort routing for every command and pipeline route.\n---\n\nmedium: simple copy/color/discovery/setup/mechanical edits. high: logic, safety, architecture, DB, orchestration, refactor, multi-file work. xhigh: research, AutoResearch, falsification, benchmarks, SEO/GEO, open-ended discovery, and From-Chat-IMG image work-order analysis. Routing is temporary; return to default after the gate. Inspect with sks reasoning and sks pipeline status.\n`,
558
- 'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS dollar-command routes as stateful pipelines with mission artifacts, route gates, Context7 evidence, temporary reasoning routing, reflection, and Honest Mode.\n---\n\nEvery $ command is a route. Use current.json and mission artifacts, temporary reasoning, TriWiki before stages, source hydration, Context7 when required, Team cleanup before reflection, reflection for full routes, and completion summary plus Honest Mode before final. Surface guard/scopes, record evidence, refresh/pack/validate TriWiki, and check sks pipeline status/resume. ${speedLanePolicyText()} ${skillDreamPolicyText()}\n`,
558
+ 'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS dollar-command routes as stateful pipelines with mission artifacts, route gates, Context7 evidence, temporary reasoning routing, reflection, and Honest Mode.\n---\n\nEvery $ command is a route. Use current.json, mission artifacts, and pipeline-plan.json as the execution plan: it records the lane, skipped stages, kept stages, verification, and no-unrequested-fallback invariant. Use temporary reasoning, TriWiki before stages, source hydration, Context7 when required, Team cleanup before reflection, reflection for full routes, and completion summary plus Honest Mode before final. Surface guard/scopes, record evidence, refresh/pack/validate TriWiki, and check sks pipeline status/resume/plan. ${speedLanePolicyText()} ${skillDreamPolicyText()}\n`,
559
559
  'context7-docs': `---\nname: context7-docs\ndescription: Enforce Context7 MCP documentation evidence for SKS routes that depend on external libraries, frameworks, APIs, MCPs, package managers, DB SDKs, or generated docs.\n---\n\nWhen required, resolve-library-id, then query-docs for the resolved id. Legacy get-library-docs evidence is accepted. Prefer sks context7 tools/resolve/docs/evidence and finish only after both evidence stages exist. Check setup with sks context7 check.\n`,
560
560
  'seo-geo-optimizer': `---\nname: seo-geo-optimizer\ndescription: SEO/GEO support for README, npm, GitHub, keywords, snippets, schema, and AI-search visibility.\n---\n\nUse for SEO/GEO, package metadata, README ranking, snippets, schema, and AI search. Optimize README, package.json, docs, badges, topics, quickstart, examples, command discovery, exact names, keywords, and AI Answer Snapshot. Do not invent metrics; use $AutoResearch unless it is a tiny copy edit.\n`,
561
561
  'reflection': `---\nname: reflection\ndescription: Post-route self-review for full SKS routes that records real misses, gaps, and corrective lessons into TriWiki memory.\n---\n\nUse after full route work/tests and before final. DFix, Answer, Help, Wiki, SKS discovery are exempt. Do not invent faults. Write reflection.md; append real lessons to ${REFLECTION_MEMORY_PATH}; refresh/pack, validate context-pack.json, pass reflection-gate.json.\n\n${reflectionInstructionText()}\n`,
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { performance } from 'node:perf_hooks';
3
3
  import { dirSize, fileSize, nowIso, packageRoot, runProcess, writeJsonAtomic } from './fsx.mjs';
4
4
  import { buildProofField, validateProofFieldReport } from './proof-field.mjs';
5
+ import { buildPipelinePlan, validatePipelinePlan } from './pipeline.mjs';
5
6
 
6
7
  export const DEFAULT_PERF_BUDGETS = {
7
8
  cli_startup_ms_p95: 250,
@@ -10,6 +11,7 @@ export const DEFAULT_PERF_BUDGETS = {
10
11
  artifact_validation_ms_p95: 150,
11
12
  dashboard_render_ms_p95: 100,
12
13
  proof_field_build_ms_p95: 150,
14
+ pipeline_plan_build_ms_p95: 50,
13
15
  workflow_scan_ms_p95: 1000,
14
16
  fast_selftest_ms_p95: 5000,
15
17
  package_size_kb_max: 1024,
@@ -60,7 +62,9 @@ export async function runWorkflowPerfBench(root, opts = {}) {
60
62
  const intent = String(opts.intent || '').trim();
61
63
  const changedFiles = normalizeChangedFiles(opts.changedFiles);
62
64
  const proofFieldBuild = [];
65
+ const pipelinePlanBuild = [];
63
66
  let proofField = null;
67
+ let pipelinePlan = null;
64
68
  for (let i = 0; i < iterations; i++) {
65
69
  const t0 = performance.now();
66
70
  proofField = await buildProofField(root, {
@@ -68,8 +72,12 @@ export async function runWorkflowPerfBench(root, opts = {}) {
68
72
  changedFiles: changedFiles.length ? changedFiles : undefined
69
73
  });
70
74
  proofFieldBuild.push(performance.now() - t0);
75
+ const t1 = performance.now();
76
+ pipelinePlan = buildPipelinePlan({ task: intent, proofField });
77
+ pipelinePlanBuild.push(performance.now() - t1);
71
78
  }
72
79
  const proofValidation = validateProofFieldReport(proofField);
80
+ const planValidation = validatePipelinePlan(pipelinePlan);
73
81
  const verification = proofField?.fast_lane_decision?.verification || [];
74
82
  const negativeWork = proofField?.negative_work_cache || [];
75
83
  const estimatedSavedWork = negativeWork.filter((item) => item.disposition === 'skip_with_evidence').length;
@@ -84,9 +92,13 @@ export async function runWorkflowPerfBench(root, opts = {}) {
84
92
  budgets: DEFAULT_PERF_BUDGETS,
85
93
  metrics: {
86
94
  proof_field_build_ms_p95: proofFieldMsP95,
87
- workflow_scan_ms_p95: workflowScanMsP95,
95
+ pipeline_plan_build_ms_p95: Math.round(percentile(pipelinePlanBuild, 95)),
96
+ workflow_scan_ms_p95: workflowScanMsP95 + Math.round(percentile(pipelinePlanBuild, 95)),
88
97
  decision_mode: proofField?.fast_lane_decision?.mode || null,
89
98
  execution_lane: proofField?.execution_lane?.lane || null,
99
+ pipeline_lane: pipelinePlan?.runtime_lane?.lane || null,
100
+ pipeline_stage_count: pipelinePlan?.stages?.length || 0,
101
+ pipeline_skipped_stage_count: pipelinePlan?.skipped_stages?.length || 0,
90
102
  fast_lane_eligible: Boolean(proofField?.fast_lane_decision?.eligible),
91
103
  fast_lane_allowed: Boolean(proofField?.execution_lane?.fast_lane_allowed),
92
104
  proof_cone_count: proofField?.proof_cones?.length || 0,
@@ -94,12 +106,15 @@ export async function runWorkflowPerfBench(root, opts = {}) {
94
106
  negative_work_skipped_count: estimatedSavedWork,
95
107
  simplicity_score: Number(proofField?.simplicity_scorecard?.score || 0),
96
108
  outcome_criteria_passed: (proofField?.simplicity_scorecard?.criteria || []).filter((item) => item.passed).length,
97
- proof_field_valid: proofValidation.ok
109
+ proof_field_valid: proofValidation.ok,
110
+ pipeline_plan_valid: planValidation.ok
98
111
  },
99
112
  proof_field: proofField,
113
+ pipeline_plan: pipelinePlan,
100
114
  recommendation: workflowRecommendation(proofField, proofValidation),
101
115
  raw: {
102
- proof_field_build_ms: proofFieldBuild.map((value) => Math.round(value))
116
+ proof_field_build_ms: proofFieldBuild.map((value) => Math.round(value)),
117
+ pipeline_plan_build_ms: pipelinePlanBuild.map((value) => Math.round(value))
103
118
  }
104
119
  };
105
120
  }
@@ -109,10 +124,13 @@ export function validateWorkflowPerfReport(report = {}) {
109
124
  if (report.schema_version !== 1) issues.push('schema_version');
110
125
  if (report.theory !== 'Potential Proof Field') issues.push('theory');
111
126
  if (!report.metrics || !Number.isFinite(Number(report.metrics.proof_field_build_ms_p95))) issues.push('proof_field_build_ms_p95');
127
+ if (!Number.isFinite(Number(report.metrics?.pipeline_plan_build_ms_p95))) issues.push('pipeline_plan_build_ms_p95');
112
128
  if (!report.metrics?.decision_mode) issues.push('decision_mode');
113
129
  if (!report.metrics?.execution_lane) issues.push('execution_lane');
130
+ if (!report.metrics?.pipeline_lane) issues.push('pipeline_lane');
114
131
  if (!Number.isFinite(Number(report.metrics?.simplicity_score))) issues.push('simplicity_score');
115
132
  if (!report.proof_field || !validateProofFieldReport(report.proof_field).ok) issues.push('proof_field');
133
+ if (!report.pipeline_plan || !validatePipelinePlan(report.pipeline_plan).ok) issues.push('pipeline_plan');
116
134
  if (!report.recommendation?.mode) issues.push('recommendation');
117
135
  return { ok: issues.length === 0, issues };
118
136
  }
@@ -12,12 +12,15 @@ import { writeMemorySweepReport } from './memory-governor.mjs';
12
12
  import { writeMistakeMemoryReport } from './mistake-memory.mjs';
13
13
  import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } from './skill-forge.mjs';
14
14
  import { writeResearchPlan } from './research.mjs';
15
+ import { SPEED_LANE_POLICY } from './proof-field.mjs';
15
16
  import { CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, hasFromChatImgSignal, hasMadSksSignal, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
16
17
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
17
18
  import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
18
19
 
19
20
  export { routePrompt };
20
21
 
22
+ export const PIPELINE_PLAN_ARTIFACT = 'pipeline-plan.json';
23
+ export const PIPELINE_PLAN_SCHEMA_VERSION = 1;
21
24
  const REFLECTION_ARTIFACT = 'reflection.md';
22
25
  const REFLECTION_GATE = 'reflection-gate.json';
23
26
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
@@ -25,11 +28,185 @@ const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
25
28
  const COMPLIANCE_LOOP_GUARD_ARTIFACT = 'compliance-loop-guard.json';
26
29
  const HARD_BLOCKER_ARTIFACT = 'hard-blocker.json';
27
30
  const DEFAULT_COMPLIANCE_LOOP_LIMIT = 3;
31
+ const CLARIFICATION_BYPASS_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki', 'ComputerUse', 'Goal']);
32
+ const LIGHTWEIGHT_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki']);
33
+ const FULL_ROUTE_STAGES = Object.freeze([
34
+ 'route_classification',
35
+ 'skill_dream_counter',
36
+ 'ambiguity_gate',
37
+ 'pipeline_plan',
38
+ 'proof_field_scan',
39
+ 'triwiki_use_first',
40
+ 'context7_evidence',
41
+ 'parallel_analysis_scouting',
42
+ 'planning_debate',
43
+ 'route_materialization',
44
+ 'fresh_executor_team',
45
+ 'broad_route_rework',
46
+ 'focused_implementation',
47
+ 'listed_verification',
48
+ 'triwiki_validate_before_final',
49
+ 'reflection',
50
+ 'honest_mode'
51
+ ]);
28
52
 
29
53
  function reflectionInstructionText(commandPrefix = 'sks') {
30
54
  return `Post-route reflection: full routes load \`reflection\` after work/tests and before final; DFix/Answer/Help/Wiki/SKS discovery are exempt. Write ${REFLECTION_ARTIFACT}; record only real misses/gaps, or no_issue_acknowledged. For lessons, append TriWiki claim rows to ${REFLECTION_MEMORY_PATH}. Run "${commandPrefix} wiki refresh" or pack, validate, then pass ${REFLECTION_GATE}.`;
31
55
  }
32
56
 
57
+ export function buildPipelinePlan(input = {}) {
58
+ const route = input.route || routePrompt(input.task || '$SKS');
59
+ const task = String(input.task || '').trim();
60
+ const ambiguity = normalizeAmbiguity(input.ambiguity, route);
61
+ const proof = normalizeProofField(input.proofField);
62
+ const lane = selectPipelineLane(route, task, proof);
63
+ const stages = buildPipelineStages(route, task, ambiguity, lane, Boolean(input.required));
64
+ const verification = planVerification(route, proof);
65
+ const skipped = stages.filter((stage) => stage.status === 'skipped').map((stage) => stage.id);
66
+ const kept = stages.filter((stage) => stage.status !== 'skipped' && stage.status !== 'not_applicable').map((stage) => stage.id);
67
+ return {
68
+ schema_version: PIPELINE_PLAN_SCHEMA_VERSION,
69
+ generated_at: nowIso(),
70
+ mission_id: input.missionId || null,
71
+ route: {
72
+ id: route?.id || 'SKS',
73
+ command: route?.command || '$SKS',
74
+ mode: route?.mode || 'SKS',
75
+ stop_gate: route?.stopGate || 'honest_mode',
76
+ required_skills: route?.requiredSkills || [],
77
+ context7_required: Boolean(input.required),
78
+ subagents_required: routeRequiresSubagents(route, task),
79
+ reflection_required: reflectionRequiredForRoute(route)
80
+ },
81
+ task,
82
+ ambiguity_gate: ambiguity,
83
+ runtime_lane: lane,
84
+ stages,
85
+ stage_summary: {
86
+ total: stages.length,
87
+ kept: kept.length,
88
+ skipped: skipped.length,
89
+ not_applicable: stages.filter((stage) => stage.status === 'not_applicable').length
90
+ },
91
+ skipped_stages: skipped,
92
+ kept_stages: kept,
93
+ verification,
94
+ invariants: ['no_unrequested_fallback_code', 'listed_verification', 'triwiki_validate_before_final', 'honest_mode'],
95
+ proof_field: proof,
96
+ skill_dream: input.skillDream || { attached: false, reason: 'skill dreaming uses cheap counters and only runs inventory at threshold' },
97
+ next_actions: planNextActions(route, task, ambiguity, lane),
98
+ no_unrequested_fallback_code: true
99
+ };
100
+ }
101
+
102
+ export async function writePipelinePlan(dir, input = {}) {
103
+ const plan = buildPipelinePlan(input);
104
+ await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
105
+ return plan;
106
+ }
107
+
108
+ export function validatePipelinePlan(plan = {}) {
109
+ const issues = [];
110
+ if (plan.schema_version !== PIPELINE_PLAN_SCHEMA_VERSION) issues.push('schema_version');
111
+ if (!plan.route?.id || !plan.route?.command) issues.push('route');
112
+ if (!plan.runtime_lane?.lane) issues.push('runtime_lane');
113
+ if (!Array.isArray(plan.stages) || !plan.stages.length) issues.push('stages');
114
+ if (!Array.isArray(plan.verification) || !plan.verification.length) issues.push('verification');
115
+ if (plan.no_unrequested_fallback_code !== true || !plan.invariants?.includes('no_unrequested_fallback_code')) issues.push('fallback_guard');
116
+ if (!plan.next_actions?.length) issues.push('next_actions');
117
+ return { ok: issues.length === 0, issues };
118
+ }
119
+
120
+ function normalizeAmbiguity(value = {}, route) {
121
+ const required = value.required ?? !CLARIFICATION_BYPASS_ROUTES.has(route?.id);
122
+ const slots = Number(value.slots || 0);
123
+ let status = value.status || (required ? 'required' : 'not_required');
124
+ if (required && value.auto_sealed) status = 'auto_sealed';
125
+ else if (required && slots > 0) status = 'awaiting_answers';
126
+ else if (required && value.passed) status = 'contract_sealed';
127
+ return {
128
+ required: Boolean(required),
129
+ status,
130
+ slots,
131
+ auto_sealed: Boolean(value.auto_sealed),
132
+ passed: Boolean(value.passed || value.auto_sealed || status === 'contract_sealed'),
133
+ contract_hash: value.contract_hash || null
134
+ };
135
+ }
136
+
137
+ function normalizeProofField(report) {
138
+ if (!report) return { attached: false, reason: 'not built during route intake; attach with sks pipeline plan --proof-field after concrete scope/changed files are known' };
139
+ return {
140
+ attached: true,
141
+ lane: report.execution_lane?.lane || null,
142
+ fast_lane_allowed: Boolean(report.execution_lane?.fast_lane_allowed),
143
+ score: Number(report.execution_lane?.score || report.simplicity_scorecard?.score || 0),
144
+ blockers: report.execution_lane?.blockers || report.fast_lane_decision?.blockers || [],
145
+ skip_when_fast: report.execution_lane?.skip_when_fast || [],
146
+ keep: report.execution_lane?.keep || SPEED_LANE_POLICY.always_keep,
147
+ verification: report.execution_lane?.verification || report.fast_lane_decision?.verification || [],
148
+ proof_cones: (report.proof_cones || []).map((cone) => cone.id),
149
+ source_hash: report.source_hash || null
150
+ };
151
+ }
152
+
153
+ function selectPipelineLane(route, task, proof) {
154
+ if (proof.attached && proof.lane) {
155
+ return {
156
+ lane: proof.lane,
157
+ source: 'proof_field',
158
+ fast_lane_allowed: Boolean(proof.fast_lane_allowed),
159
+ reason: proof.fast_lane_allowed ? 'Proof Field allowed the fast lane.' : `Proof Field selected ${proof.lane}.`,
160
+ blockers: proof.blockers || [],
161
+ skip_when_fast: proof.fast_lane_allowed ? SPEED_LANE_POLICY.skip_when_fast : [],
162
+ keep: proof.keep || SPEED_LANE_POLICY.always_keep
163
+ };
164
+ }
165
+ if (route?.id === 'ComputerUse') return { lane: 'computer_use_fast_lane', source: 'route_policy', fast_lane_allowed: true, reason: 'Computer Use route is intentionally direct and defers wiki/honest checks to closeout.', blockers: [], skip_when_fast: ['parallel_analysis_scouting', 'planning_debate', 'fresh_executor_team'], keep: ['focused_implementation', 'triwiki_validate_before_final', 'honest_mode'] };
166
+ if (LIGHTWEIGHT_ROUTES.has(route?.id)) return { lane: `${String(route.id).toLowerCase()}_lightweight_lane`, source: 'route_policy', fast_lane_allowed: true, reason: 'Lightweight route bypasses full mission orchestration by design.', blockers: [], skip_when_fast: SPEED_LANE_POLICY.skip_when_fast, keep: ['focused_implementation', 'listed_verification', 'honest_mode'] };
167
+ if (routeRequiresSubagents(route, task)) return { lane: SPEED_LANE_POLICY.full_lane, source: 'route_policy', fast_lane_allowed: false, reason: 'No Proof Field attached and this route normally requires full Team evidence.', blockers: ['proof_field_not_attached'], skip_when_fast: [], keep: SPEED_LANE_POLICY.always_keep };
168
+ return { lane: SPEED_LANE_POLICY.balanced_lane, source: 'route_policy', fast_lane_allowed: false, reason: 'Balanced parent-owned route until Proof Field proves a narrower lane.', blockers: ['proof_field_not_attached'], skip_when_fast: [], keep: SPEED_LANE_POLICY.always_keep };
169
+ }
170
+
171
+ function buildPipelineStages(route, task, ambiguity, lane, context7Required) {
172
+ return FULL_ROUTE_STAGES.map((id) => {
173
+ const optional = optionalStage(route, task, ambiguity, context7Required, id);
174
+ const skippedByFast = lane.fast_lane_allowed && SPEED_LANE_POLICY.skip_when_fast.includes(id);
175
+ const skipped = skippedByFast || optional.skip;
176
+ return {
177
+ id,
178
+ status: skipped ? (optional.notApplicable ? 'not_applicable' : 'skipped') : (id === 'ambiguity_gate' && ambiguity.passed ? 'passed' : 'keep'),
179
+ reason: skippedByFast ? 'proof_field_fast_lane' : optional.reason
180
+ };
181
+ });
182
+ }
183
+
184
+ function optionalStage(route, task, ambiguity, context7Required, id) {
185
+ if (id === 'ambiguity_gate' && ambiguity?.required === false) return { skip: true, notApplicable: true, reason: 'ambiguity_gate_not_required_for_entrypoint' };
186
+ if (id === 'ambiguity_gate' && CLARIFICATION_BYPASS_ROUTES.has(route?.id)) return { skip: true, notApplicable: true, reason: 'route_bypasses_clarification' };
187
+ if (id === 'context7_evidence' && !context7Required) return { skip: true, notApplicable: true, reason: 'context7_not_required_by_route' };
188
+ if (id === 'reflection' && !reflectionRequiredForRoute(route)) return { skip: true, notApplicable: true, reason: 'reflection_not_required_for_route' };
189
+ if (['parallel_analysis_scouting', 'planning_debate', 'fresh_executor_team'].includes(id) && !routeRequiresSubagents(route, task)) return { skip: true, notApplicable: true, reason: 'subagent_team_not_required_by_route' };
190
+ return { skip: false, reason: 'required_by_lane' };
191
+ }
192
+
193
+ function planVerification(route, proof) {
194
+ const out = new Set(proof.verification || []);
195
+ if (route?.id === 'Team') out.add('sks validate-artifacts latest --json');
196
+ if (reflectionRequiredForRoute(route)) out.add('sks wiki validate .sneakoscope/wiki/context-pack.json');
197
+ out.add('npm run packcheck');
198
+ out.add('npm run selftest -- --mock');
199
+ return [...out];
200
+ }
201
+
202
+ function planNextActions(route, task, ambiguity, lane) {
203
+ if (ambiguity.required && !ambiguity.passed) return ['ask ambiguity-removal questions', 'write answers.json', 'run sks pipeline answer <mission-id> answers.json'];
204
+ const actions = ['read pipeline-plan.json before work', 'execute kept stages only', 'run listed verification'];
205
+ if (!lane.fast_lane_allowed && routeRequiresSubagents(route, task)) actions.splice(1, 0, 'materialize full Team artifacts before implementation');
206
+ actions.push('refresh/validate TriWiki when required', 'finish with completion summary and Honest Mode');
207
+ return actions;
208
+ }
209
+
33
210
  export function promptPipelineContext(prompt, route = routePrompt(prompt)) {
34
211
  const required = routeNeedsContext7(route, prompt);
35
212
  const reasoning = routeReasoning(route, prompt);
@@ -215,34 +392,47 @@ export async function activeRouteContext(root, state) {
215
392
  if (!state?.route && !state?.mode) return '';
216
393
  const id = state.route || state.mode;
217
394
  const reasoningNote = state.reasoning_effort ? ` Temporary reasoning remains ${state.reasoning_effort} (${state.reasoning_profile}); return to the default profile after this route completes.` : '';
395
+ const planNote = await activePipelinePlanNote(root, state);
218
396
  if (state.honest_loop_required || /HONEST_LOOPBACK_AFTER_CLARIFICATION/.test(String(state.phase || ''))) {
219
- return `SKS Honest Mode found unresolved gaps for ${state.route_command || state.route || state.mode}. Do not ask ambiguity questions again. Continue from the sealed decision-contract.json, inspect .sneakoscope/missions/${state.mission_id}/honest-loopback.json, fix gaps, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.${reasoningNote}`;
397
+ return `SKS Honest Mode found unresolved gaps for ${state.route_command || state.route || state.mode}. Do not ask ambiguity questions again. Continue from the sealed decision-contract.json, inspect .sneakoscope/missions/${state.mission_id}/honest-loopback.json, fix gaps, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.${reasoningNote}${planNote}`;
220
398
  }
221
399
  if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) return clarificationAwaitingAnswersContext(root, state);
222
400
  if (state.clarification_passed && String(state.phase || '').includes('CLARIFICATION_CONTRACT_SEALED')) {
223
- return `Mandatory ambiguity-removal gate passed for ${state.route_command || state.route || state.mode}. Use the sealed decision-contract.json before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the clarified goal, constraints, non-goals, risk boundary, and test scope.`;
401
+ return `Mandatory ambiguity-removal gate passed for ${state.route_command || state.route || state.mode}. Use the sealed decision-contract.json and ${PIPELINE_PLAN_ARTIFACT} before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the clarified goal, constraints, non-goals, risk boundary, and test scope.${planNote}`;
224
402
  }
225
403
  if (state.mode === 'TEAM') {
226
404
  const context7 = state.context7_required && !(await hasContext7DocsEvidence(root, state))
227
405
  ? ' Context7 evidence is still required before completion: use resolve-library-id, then query-docs (or legacy get-library-docs).'
228
406
  : '';
229
407
  const roles = state.role_counts ? ` Role counts: ${formatRoleCounts(state.role_counts)}.` : '';
230
- return `Active Team mission ${state.mission_id || 'latest'} must keep the user-visible live transcript updated. Agent session budget: ${state.agent_sessions || 3}.${roles} Run scouts, TriWiki refresh, debate, consensus, fresh development, review/integration, then close or account for every Team subagent session and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final. After each subagent status/result/handoff, run: sks team event ${state.mission_id || 'latest'} --agent <name> --phase <phase> --message "...". Inspect with sks team log/watch ${state.mission_id || 'latest'}.${reasoningNote}${context7}`;
408
+ return `Active Team mission ${state.mission_id || 'latest'} must keep the user-visible live transcript updated. Agent session budget: ${state.agent_sessions || 3}.${roles} Run scouts, TriWiki refresh, debate, consensus, fresh development, review/integration, then close or account for every Team subagent session and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final. After each subagent status/result/handoff, run: sks team event ${state.mission_id || 'latest'} --agent <name> --phase <phase> --message "...". Inspect with sks team log/watch ${state.mission_id || 'latest'}.${reasoningNote}${context7}${planNote}`;
231
409
  }
232
410
  if (state.subagents_required && !(await hasSubagentEvidence(root, state))) {
233
- return `Active SKS route ${id} requires subagent execution evidence before code-changing work can be considered complete. Spawn worker/reviewer subagents for disjoint write scopes, or record an explicit unavailable/unsplittable subagent evidence event before editing.${reasoningNote}`;
411
+ return `Active SKS route ${id} requires subagent execution evidence before code-changing work can be considered complete. Spawn worker/reviewer subagents for disjoint write scopes, or record an explicit unavailable/unsplittable subagent evidence event before editing.${reasoningNote}${planNote}`;
234
412
  }
235
- if (state.mode === 'GOAL') return `Active Goal mission ${state.mission_id || 'latest'} uses Codex native /goal continuation. Inspect .sneakoscope/missions/${state.mission_id || 'latest'}/${GOAL_WORKFLOW_ARTIFACT}, then use /goal create, pause, resume, or clear in the Codex runtime as appropriate.`;
413
+ if (state.mode === 'GOAL') return `Active Goal mission ${state.mission_id || 'latest'} uses Codex native /goal continuation. Inspect .sneakoscope/missions/${state.mission_id || 'latest'}/${GOAL_WORKFLOW_ARTIFACT}, then use /goal create, pause, resume, or clear in the Codex runtime as appropriate.${planNote}`;
236
414
  if (state.context7_required && !(await hasContext7DocsEvidence(root, state))) {
237
- return `Active SKS route ${id} still requires Context7 evidence. Use resolve-library-id, then query-docs (or legacy get-library-docs) for relevant docs/APIs before completing.${reasoningNote}`;
415
+ return `Active SKS route ${id} still requires Context7 evidence. Use resolve-library-id, then query-docs (or legacy get-library-docs) for relevant docs/APIs before completing.${reasoningNote}${planNote}`;
238
416
  }
239
- return '';
417
+ return planNote.trim();
418
+ }
419
+
420
+ async function activePipelinePlanNote(root, state = {}) {
421
+ if (!state?.mission_id) return '';
422
+ const plan = await readJson(path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT), null);
423
+ if (!plan) return '';
424
+ const lane = plan.runtime_lane?.lane || 'unknown';
425
+ const kept = plan.stage_summary?.kept ?? plan.kept_stages?.length ?? 0;
426
+ const skipped = plan.stage_summary?.skipped ?? plan.skipped_stages?.length ?? 0;
427
+ const next = Array.isArray(plan.next_actions) && plan.next_actions.length ? ` Next planned action: ${plan.next_actions[0]}.` : '';
428
+ return ` Pipeline plan: .sneakoscope/missions/${state.mission_id}/${PIPELINE_PLAN_ARTIFACT} (${lane}; kept=${kept}, skipped=${skipped}).${next}`;
240
429
  }
241
430
 
242
431
  async function prepareGoal(root, route, task, required) {
243
432
  const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt: task });
244
433
  const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt: task });
245
434
  await writeJsonAtomic(path.join(dir, 'route-context.json'), { route: route.id, command: route.command, mode: route.mode, task, required_skills: route.requiredSkills, context7_required: required, native_goal: workflow.native_goal, stop_gate: route.stopGate });
435
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'not_required' } });
246
436
  const executionRoute = routePrompt(task);
247
437
  const shouldDelegateExecution = routeRequiresSubagents(route, task)
248
438
  && executionRoute
@@ -262,7 +452,7 @@ Delegated execution route: ${executionRoute.command}. The delegated route missio
262
452
  ].filter(Boolean).join('\n\n')
263
453
  };
264
454
  }
265
- await setCurrent(root, routeState(id, route, 'GOAL_READY', required, { prompt: task, native_goal: workflow.native_goal, stop_gate: route.stopGate, implementation_allowed: true, questions_allowed: true }));
455
+ await setCurrent(root, routeState(id, route, 'GOAL_READY', required, { prompt: task, native_goal: workflow.native_goal, stop_gate: route.stopGate, implementation_allowed: true, questions_allowed: true, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
266
456
  return routeContext(route, id, task, required, `Use Codex native ${workflow.native_goal.slash_command} control for persisted continuation, then continue the relevant SKS route gates for any implementation work.`);
267
457
  }
268
458
 
@@ -276,6 +466,7 @@ async function prepareClarificationGate(root, route, task, required, opts = {})
276
466
  if (schema.slots.length === 0) {
277
467
  await writeJsonAtomic(path.join(dir, 'answers.json'), schema.inferred_answers || {});
278
468
  const result = await sealContract(dir, mission);
469
+ const plan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: true, slots: 0, auto_sealed: result.ok, passed: result.ok, contract_hash: result.contract?.sealed_hash || null } });
279
470
  await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.auto_sealed', route: route.id, slots: 0, ok: result.ok });
280
471
  await setCurrent(root, routeState(id, route, result.ok ? `${route.mode}_CLARIFICATION_CONTRACT_SEALED` : `${route.mode}_CLARIFICATION_AWAITING_ANSWERS`, required, {
281
472
  prompt: task,
@@ -285,6 +476,8 @@ async function prepareClarificationGate(root, route, task, required, opts = {})
285
476
  clarification_passed: result.ok,
286
477
  ambiguity_gate_required: true,
287
478
  ambiguity_gate_passed: result.ok,
479
+ pipeline_plan_ready: validatePipelinePlan(plan).ok,
480
+ pipeline_plan_path: PIPELINE_PLAN_ARTIFACT,
288
481
  original_stop_gate: route.stopGate,
289
482
  stop_gate: route.stopGate
290
483
  }));
@@ -296,12 +489,14 @@ Ambiguity gate auto-sealed for ${route.command}: all contract answers were infer
296
489
  Mission: ${id}
297
490
  Decision contract: .sneakoscope/missions/${id}/decision-contract.json
298
491
  Resolved answers: .sneakoscope/missions/${id}/resolved-answers.json
492
+ Pipeline plan: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}
299
493
  Next atomic action: continue the original route lifecycle with the sealed decision-contract.json.`
300
494
  };
301
495
  }
496
+ const plan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: true, slots: schema.slots.length, status: 'awaiting_answers' } });
302
497
  await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.questions_created', route: route.id, slots: schema.slots.length });
303
498
  const phase = `${route.mode}_CLARIFICATION_AWAITING_ANSWERS`;
304
- await setCurrent(root, routeState(id, route, phase, required, { prompt: task, questions_allowed: true, implementation_allowed: false, clarification_required: true, ambiguity_gate_required: true, original_stop_gate: route.stopGate, stop_gate: 'clarification-gate' }));
499
+ await setCurrent(root, routeState(id, route, phase, required, { prompt: task, questions_allowed: true, implementation_allowed: false, clarification_required: true, ambiguity_gate_required: true, pipeline_plan_ready: validatePipelinePlan(plan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, original_stop_gate: route.stopGate, stop_gate: 'clarification-gate' }));
305
500
  const answerCommand = 'sks pipeline answer latest answers.json, then continue the original route lifecycle';
306
501
  const title = 'MANDATORY ambiguity-removal gate activated.';
307
502
  return {
@@ -313,6 +508,7 @@ Task: ${task}
313
508
  State: ${phase}
314
509
  Question file: .sneakoscope/missions/${id}/questions.md
315
510
  Answer schema: .sneakoscope/missions/${id}/required-answers.schema.json
511
+ Pipeline plan: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}
316
512
 
317
513
  Do not execute the route yet. Ask the user the required ambiguity-removal questions now. After the user answers, convert the answers to answers.json, run "${answerCommand}".
318
514
  ${clarificationVisibleResponseContract(id)}
@@ -402,14 +598,16 @@ async function prepareTeam(root, route, task, required) {
402
598
  await writeMistakeMemoryReport(dir, { mission_id: id, route: 'team', task: cleanTask }).catch(() => null);
403
599
  await writeCodeStructureReport(root, dir, { missionId: id, exception: 'Team prepare records split-review risk; extraction happens only when the mission scope includes the touched file.' }).catch(() => null);
404
600
  await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
405
- await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired }));
601
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: cleanTask, required, ambiguity: { required: false, status: 'direct_team_cli' } });
602
+ await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
406
603
  return routeContext(route, id, cleanTask, required, `Run scouts, refresh/validate TriWiki, debate, close debate agents, form a fresh ${roster.bundle_size}-person executor team, then close/clean Team sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection.`);
407
604
  }
408
605
 
409
606
  async function prepareResearch(root, route, task, required) {
410
607
  const { id, dir } = await createMission(root, { mode: 'research', prompt: task });
411
608
  await writeResearchPlan(dir, task, {});
412
- await setCurrent(root, routeState(id, route, 'RESEARCH_PREPARED', required, { prompt: task }));
609
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
610
+ await setCurrent(root, routeState(id, route, 'RESEARCH_PREPARED', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
413
611
  return routeContext(route, id, task, required, 'Run sks research run latest, produce research-report.md, novelty-ledger.json, falsification evidence, and pass research-gate.json.');
414
612
  }
415
613
 
@@ -418,7 +616,8 @@ async function prepareAutoResearch(root, route, task, required) {
418
616
  await writeJsonAtomic(path.join(dir, 'autoresearch-plan.json'), { schema_version: 1, task, loop: ['program', 'hypothesis', 'experiment', 'measure', 'keep_or_discard', 'falsify', 'honest_conclusion'] });
419
617
  await writeJsonAtomic(path.join(dir, 'experiment-ledger.json'), { schema_version: 1, entries: [] });
420
618
  await writeJsonAtomic(path.join(dir, 'autoresearch-gate.json'), { passed: false, experiment_ledger_present: true, metric_present: false, keep_or_discard_decision: false, falsification_present: false, honest_conclusion: false, context7_evidence: false });
421
- await setCurrent(root, routeState(id, route, 'AUTORESEARCH_EXPERIMENT_LOOP', required, { prompt: task }));
619
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
620
+ await setCurrent(root, routeState(id, route, 'AUTORESEARCH_EXPERIMENT_LOOP', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
422
621
  return routeContext(route, id, task, required, 'Run the smallest useful experiment loop, update experiment-ledger.json, falsify the result, and pass autoresearch-gate.json.');
423
622
  }
424
623
 
@@ -427,21 +626,24 @@ async function prepareDb(root, route, task, required) {
427
626
  const scan = await scanDbSafety(root).catch((err) => ({ ok: false, findings: [{ id: 'db_scan_failed', severity: 'high', reason: err.message }] }));
428
627
  await writeJsonAtomic(path.join(dir, 'db-safety-scan.json'), scan);
429
628
  await writeJsonAtomic(path.join(dir, 'db-review.json'), { passed: false, scan_ok: scan.ok, destructive_operation_zero: true, safe_mcp_policy: false, context7_evidence: false, notes: [] });
430
- await setCurrent(root, routeState(id, route, 'DB_REVIEW_REQUIRED', required, { prompt: task }));
629
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
630
+ await setCurrent(root, routeState(id, route, 'DB_REVIEW_REQUIRED', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
431
631
  return routeContext(route, id, task, required, 'Run sks db policy/scan/check as needed, keep DB operations read-only, record safe MCP policy, and pass db-review.json.');
432
632
  }
433
633
 
434
634
  async function prepareGx(root, route, task, required) {
435
635
  const { id, dir } = await createMission(root, { mode: 'gx', prompt: task });
436
636
  await writeJsonAtomic(path.join(dir, 'gx-gate.json'), { passed: false, vgraph_beta_render: false, validation: false, drift_snapshot: false, context7_evidence: false });
437
- await setCurrent(root, routeState(id, route, 'GX_VALIDATE_REQUIRED', required, { prompt: task }));
637
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
638
+ await setCurrent(root, routeState(id, route, 'GX_VALIDATE_REQUIRED', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
438
639
  return routeContext(route, id, task, required, 'Run sks gx init/render/validate/drift/snapshot, then pass gx-gate.json.');
439
640
  }
440
641
 
441
642
  async function prepareLightRoute(root, route, task, required) {
442
643
  const { id, dir } = await createMission(root, { mode: route.id.toLowerCase(), prompt: task });
443
644
  await writeJsonAtomic(path.join(dir, 'route-context.json'), { route: route.id, command: route.command, task, required_skills: route.requiredSkills, context7_required: required, context_tracking: triwikiContextTracking(), stop_gate: 'honest_mode' });
444
- await setCurrent(root, routeState(id, route, 'ROUTE_CONTEXT_READY', required, { prompt: task, stop_gate: 'none' }));
645
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'light_route' } });
646
+ await setCurrent(root, routeState(id, route, 'ROUTE_CONTEXT_READY', required, { prompt: task, stop_gate: 'none', pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
445
647
  return routeContext(route, id, task, required, 'Load the route skill context, execute the smallest matching action, and finish with Honest Mode.');
446
648
  }
447
649
 
@@ -459,6 +661,7 @@ function routeContext(route, id, task, required, next) {
459
661
  ${route.command} route prepared.
460
662
  Mission: ${id}
461
663
  Task: ${task}
664
+ Pipeline plan: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}
462
665
  Required skills: ${route.requiredSkills.join(', ')}
463
666
  Stop gate: ${route.stopGate}
464
667
  Subagents: ${routeRequiresSubagents(route, task) ? 'required before code-changing execution; spawn parallel workers/reviewers with disjoint ownership or record explicit unavailable/unsplittable evidence.' : 'optional'}
@@ -475,7 +678,8 @@ async function clarificationAwaitingAnswersContext(root, state) {
475
678
  if (!id) return '';
476
679
  const schema = await readJson(path.join(missionDir(root, id), 'required-answers.schema.json'), null);
477
680
  const questionBlock = schema ? `\n\nRequired questions still pending:\n${formatRequiredQuestions(schema)}` : '';
478
- return `Active SKS route ${state.route_command || state.route || state.mode} is waiting for mandatory ambiguity-removal answers. If the user answered, write answers.json, run "sks pipeline answer ${id} answers.json", then continue the original route lifecycle. If required answers are missing, use the Codex plan tool first, then ask only those questions. Do not execute the route before this gate passes.${clarificationVisibleResponseContract(id, false)}${clarificationPlanHint({ command: state.route_command || state.route || '$SKS', route: state.route || state.mode || 'SKS route' }, id, false)}${questionBlock}`;
681
+ const planNote = await activePipelinePlanNote(root, state);
682
+ return `Active SKS route ${state.route_command || state.route || state.mode} is waiting for mandatory ambiguity-removal answers. If the user answered, write answers.json, run "sks pipeline answer ${id} answers.json", then continue the original route lifecycle. If required answers are missing, use the Codex plan tool first, then ask only those questions. Do not execute the route before this gate passes.${planNote}${clarificationVisibleResponseContract(id, false)}${clarificationPlanHint({ command: state.route_command || state.route || '$SKS', route: state.route || state.mode || 'SKS route' }, id, false)}${questionBlock}`;
479
683
  }
480
684
 
481
685
  function clarificationVisibleResponseContract(id) {
@@ -387,7 +387,7 @@ export const COMMAND_CATALOG = [
387
387
  { name: 'dfix', usage: 'sks dfix', description: 'Explain $DFix ultralight design/content fix mode.' },
388
388
  { name: 'qa-loop', usage: 'sks qa-loop prepare|answer|run|status ...', description: 'Dogfood UI/API as human proxy with safety gates, safe fixes, rechecks, Codex Computer Use-only UI evidence, report.' },
389
389
  { name: 'context7', usage: 'sks context7 check|setup|tools|resolve|docs|evidence ...', description: 'Check, configure, and call the local Context7 MCP requirement.' },
390
- { name: 'pipeline', usage: 'sks pipeline status|resume|answer ...', description: 'Inspect the active skill-first route, pass mandatory ambiguity gates, and inspect completion gates.' },
390
+ { name: 'pipeline', usage: 'sks pipeline status|resume|plan|answer ...', description: 'Inspect the active skill-first route, materialized execution plan, ambiguity gates, and completion gates.' },
391
391
  { name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
392
392
  { name: 'conflicts', usage: 'sks conflicts check|prompt [--json]', description: 'Detect other Codex harnesses such as OMX/DCodex and print the GPT-5.5 high cleanup prompt.' },
393
393
  { name: 'versioning', usage: 'sks versioning status|bump|pre-commit [--json]', description: 'Manage automatic project version bumps on every commit with a shared Git lock.' },