sneakoscope 0.7.13 → 0.7.16

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.13",
4
+ "version": "0.7.16",
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
@@ -39,7 +39,7 @@ import {
39
39
  import { contextCapsule } from '../core/triwiki-attention.mjs';
40
40
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
41
41
  import { ALLOWED_REASONING_EFFORTS, CODEX_APP_IMAGE_GENERATION_DOC_URL, 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, GETDESIGN_REFERENCE, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
42
- import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
42
+ import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, projectGateStatus, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
43
43
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
44
44
  import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
45
45
  import { ARTIFACT_FILES, validateDogfoodReport, validateEffortDecision, validateFromChatImgVisualMap, validateSkillCandidate, validateSkillInjectionDecision, validateTeamDashboardState, validateWorkOrderLedger } from '../core/artifact-schemas.mjs';
@@ -469,11 +469,13 @@ async function pipeline(sub = 'status', args = []) {
469
469
  const state = await readJson(stateFile(root), {});
470
470
  const evidence = await context7Evidence(root, state);
471
471
  const plan = state.mission_id ? await readJson(path.join(missionDir(root, state.mission_id), PIPELINE_PLAN_ARTIFACT), null) : null;
472
+ const gateProjection = await projectGateStatus(root, state);
472
473
  const stop = await evaluateStop(root, state, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
473
474
  const result = {
474
475
  root,
475
476
  state,
476
477
  context7: evidence,
478
+ gate_projection: gateProjection,
477
479
  plan: plan ? pipelinePlanSummary(plan, root, state.mission_id) : null,
478
480
  stop_gate: state.stop_gate || null,
479
481
  next_action: stop?.reason || 'No active blocking route gate detected.'
@@ -492,6 +494,7 @@ async function pipeline(sub = 'status', args = []) {
492
494
  }
493
495
  console.log(`Reasoning: ${state.reasoning_effort || 'medium'}${state.reasoning_profile ? ` (${state.reasoning_profile})` : ''}${state.reasoning_temporary ? ' temporary' : ''}`);
494
496
  console.log(`Stop gate: ${state.stop_gate || 'none'}`);
497
+ console.log(`Gate projection: ${gateProjection.ok ? 'ok' : `blocked (${gateProjection.blockers.join(', ')})`}`);
495
498
  console.log(`Context7: ${state.context7_required ? (evidence.ok ? 'ok' : 'required-missing') : 'optional'} (${evidence.count || 0} event(s))`);
496
499
  console.log(`Next: ${result.next_action}`);
497
500
  }
@@ -520,10 +523,11 @@ async function pipelinePlan(root, args = []) {
520
523
  const changedRaw = readOption(args, '--changed', '');
521
524
  const proofField = flag(args, '--proof-field') ? await buildProofField(root, { intent, changedFiles: changedRaw ? changedRaw.split(',') : undefined }) : null;
522
525
  const contract = dir ? await readJson(path.join(dir, 'decision-contract.json'), {}) : {};
526
+ const contractSealed = contract?.status === 'sealed' || Boolean(contract?.sealed_at || contract?.sealed_hash);
523
527
  const ambiguity = {
524
- required: Boolean(routeContext.clarification_gate || state.ambiguity_gate_required),
525
- passed: Boolean(state.ambiguity_gate_passed || state.clarification_passed),
526
- status: state.clarification_required ? 'awaiting_answers' : (state.ambiguity_gate_passed ? 'contract_sealed' : undefined),
528
+ required: Boolean(routeContext.clarification_gate || state.ambiguity_gate_required || contractSealed),
529
+ passed: Boolean(state.ambiguity_gate_passed || state.clarification_passed || contractSealed),
530
+ status: state.clarification_required ? 'awaiting_answers' : ((state.ambiguity_gate_passed || contractSealed) ? 'contract_sealed' : undefined),
527
531
  contract_hash: contract?.sealed_hash || null
528
532
  };
529
533
  const planInput = { missionId: id || null, route, task: intent, required: Boolean(routeContext.context7_required || state.context7_required), ambiguity, proofField };
@@ -1719,6 +1723,27 @@ async function selftest() {
1719
1723
  if (trippedStop) throw new Error('selftest failed: compliance loop guard did not terminally trip');
1720
1724
  const loopBlocker = await readJson(path.join(loopMission.dir, 'hard-blocker.json'), null);
1721
1725
  if (loopBlocker?.reason !== 'compliance_loop_guard_tripped') throw new Error('selftest failed: compliance loop guard did not write hard blocker');
1726
+ const clarificationMission = await createMission(tmp, { mode: 'team', prompt: 'visible question gate selftest' });
1727
+ await writeTextAtomic(path.join(clarificationMission.dir, 'questions.md'), '# Questions\n\n1. GOAL_PRECISE: What should be changed?\n');
1728
+ await writeJsonAtomic(path.join(clarificationMission.dir, 'required-answers.schema.json'), { slots: [{ id: 'GOAL_PRECISE', question: 'What should be changed?' }] });
1729
+ const clarificationState = {
1730
+ mission_id: clarificationMission.id,
1731
+ mode: 'TEAM',
1732
+ route_command: '$Team',
1733
+ phase: 'TEAM_CLARIFICATION_AWAITING_ANSWERS',
1734
+ clarification_required: true,
1735
+ implementation_allowed: false,
1736
+ ambiguity_gate_required: true,
1737
+ ambiguity_gate_passed: false,
1738
+ stop_gate: 'clarification-gate'
1739
+ };
1740
+ for (let i = 0; i < 5; i++) {
1741
+ const stop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'continuing implementation without visible questions' });
1742
+ if (stop?.decision !== 'block' || !String(stop.reason || '').includes('waiting for mandatory ambiguity-removal answers')) throw new Error('selftest failed: clarification gate did not hard-pause without visible questions');
1743
+ }
1744
+ if (await exists(path.join(clarificationMission.dir, 'hard-blocker.json'))) throw new Error('selftest failed: clarification gate used compliance hard-blocker instead of waiting for answers');
1745
+ const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions still pending:\n1. GOAL_PRECISE: What should be changed?\n\nUse sks pipeline answer latest answers.json.' });
1746
+ if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification question block did not allow the question-only turn to stop');
1722
1747
  await setCurrent(tmp, loopState);
1723
1748
  const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
1724
1749
  cwd: tmp,
@@ -2852,13 +2877,14 @@ async function selftest() {
2852
2877
  if (!harnessReport.forgetting.fixture.passed || !harnessReport.tmux.views.includes('Harness Experiments View') || !harnessReport.reliability.tool_error_taxonomy.includes('Unknown')) throw new Error('selftest failed: harness growth fixture incomplete');
2853
2878
  const proofField = await proofFieldFixture();
2854
2879
  if (!proofField.validation.ok || !validateProofFieldReport(proofField.report).ok) throw new Error('selftest failed: proof field report invalid');
2855
- if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest failed: proof field fixture checks incomplete');
2880
+ if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest failed: proof field fixture checks incomplete');
2856
2881
  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');
2857
2882
  const fastPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team small CLI help update'), task: 'small CLI help surface update', proofField: proofField.report });
2858
2883
  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');
2859
2884
  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'] });
2860
2885
  const broadPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team database security route refactor'), task: 'database security route refactor', proofField: broadProofField });
2861
2886
  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');
2887
+ if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key) throw new Error('selftest failed: route economy projection missing from pipeline plan');
2862
2888
  const workflowPerf = await runWorkflowPerfBench(tmp, {
2863
2889
  iterations: 2,
2864
2890
  intent: 'small CLI help surface update',
@@ -529,6 +529,8 @@ export async function perfCommand(sub, args = []) {
529
529
  console.log(`Mode: ${report.metrics.decision_mode}`);
530
530
  console.log(`Fast lane: ${report.metrics.fast_lane_eligible ? 'yes' : 'no'}`);
531
531
  console.log(`Proof Field p95: ${report.metrics.proof_field_build_ms_p95}ms`);
532
+ console.log(`Contract clarity: ${report.metrics.contract_clarity_score}`);
533
+ console.log(`Workflow complexity: ${report.metrics.workflow_complexity_band} (${report.metrics.workflow_complexity_score})`);
532
534
  console.log(`Proof cones: ${report.metrics.proof_cone_count}`);
533
535
  console.log(`Negative work skipped: ${report.metrics.negative_work_skipped_count}`);
534
536
  console.log(`Next: ${report.recommendation.next.join('; ')}`);
@@ -569,6 +571,9 @@ export async function proofFieldCommand(sub, args = []) {
569
571
  console.log(`Mode: ${report.fast_lane_decision.mode}`);
570
572
  console.log(`Eligible: ${report.fast_lane_decision.eligible ? 'yes' : 'no'}`);
571
573
  if (report.fast_lane_decision.blockers.length) console.log(`Blockers: ${report.fast_lane_decision.blockers.join(', ')}`);
574
+ console.log(`Contract clarity: ${report.contract_clarity.score}${report.contract_clarity.ask_recommended ? ' (ask recommended)' : ''}`);
575
+ console.log(`Workflow complexity: ${report.workflow_complexity.band} (${report.workflow_complexity.score})`);
576
+ if (report.team_trigger_matrix.active_triggers.length) console.log(`Team triggers: ${report.team_trigger_matrix.active_triggers.join(', ')}`);
572
577
  console.log(`Proof cones: ${report.proof_cones.map((cone) => cone.id).join(', ')}`);
573
578
  console.log(`Verification: ${report.fast_lane_decision.verification.join('; ')}`);
574
579
  console.log(`Report: ${path.relative(root, report.report_path)}`);
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.7.13';
8
+ export const PACKAGE_VERSION = '0.7.16';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -105,6 +105,11 @@ export async function runWorkflowPerfBench(root, opts = {}) {
105
105
  verification_count: verification.length,
106
106
  negative_work_skipped_count: estimatedSavedWork,
107
107
  simplicity_score: Number(proofField?.simplicity_scorecard?.score || 0),
108
+ contract_clarity_score: Number(proofField?.contract_clarity?.score || 0),
109
+ workflow_complexity_score: Number(proofField?.workflow_complexity?.score || 0),
110
+ workflow_complexity_band: proofField?.workflow_complexity?.band || null,
111
+ team_trigger_count: proofField?.team_trigger_matrix?.active_triggers?.length || 0,
112
+ verification_stage_cache_key: proofField?.verification_stage_cache?.cache_key || null,
108
113
  outcome_criteria_passed: (proofField?.simplicity_scorecard?.criteria || []).filter((item) => item.passed).length,
109
114
  proof_field_valid: proofValidation.ok,
110
115
  pipeline_plan_valid: planValidation.ok
@@ -129,6 +134,10 @@ export function validateWorkflowPerfReport(report = {}) {
129
134
  if (!report.metrics?.execution_lane) issues.push('execution_lane');
130
135
  if (!report.metrics?.pipeline_lane) issues.push('pipeline_lane');
131
136
  if (!Number.isFinite(Number(report.metrics?.simplicity_score))) issues.push('simplicity_score');
137
+ if (!Number.isFinite(Number(report.metrics?.contract_clarity_score))) issues.push('contract_clarity_score');
138
+ if (!Number.isFinite(Number(report.metrics?.workflow_complexity_score))) issues.push('workflow_complexity_score');
139
+ if (!report.metrics?.workflow_complexity_band) issues.push('workflow_complexity_band');
140
+ if (!report.metrics?.verification_stage_cache_key) issues.push('verification_stage_cache_key');
132
141
  if (!report.proof_field || !validateProofFieldReport(report.proof_field).ok) issues.push('proof_field');
133
142
  if (!report.pipeline_plan || !validatePipelinePlan(report.pipeline_plan).ok) issues.push('pipeline_plan');
134
143
  if (!report.recommendation?.mode) issues.push('recommendation');
@@ -65,6 +65,7 @@ export function buildPipelinePlan(input = {}) {
65
65
  const verification = planVerification(route, proof);
66
66
  const skipped = stages.filter((stage) => stage.status === 'skipped').map((stage) => stage.id);
67
67
  const kept = stages.filter((stage) => stage.status !== 'skipped' && stage.status !== 'not_applicable').map((stage) => stage.id);
68
+ const routeEconomy = routeEconomyPlan(proof);
68
69
  return {
69
70
  schema_version: PIPELINE_PLAN_SCHEMA_VERSION,
70
71
  generated_at: nowIso(),
@@ -94,6 +95,7 @@ export function buildPipelinePlan(input = {}) {
94
95
  verification,
95
96
  invariants: ['no_unrequested_fallback_code', 'listed_verification', 'triwiki_validate_before_final', 'honest_mode'],
96
97
  proof_field: proof,
98
+ route_economy: routeEconomy,
97
99
  skill_dream: input.skillDream || { attached: false, reason: 'skill dreaming uses cheap counters and only runs inventory at threshold' },
98
100
  next_actions: planNextActions(route, task, ambiguity, lane),
99
101
  no_unrequested_fallback_code: true
@@ -113,6 +115,7 @@ export function validatePipelinePlan(plan = {}) {
113
115
  if (!plan.runtime_lane?.lane) issues.push('runtime_lane');
114
116
  if (!Array.isArray(plan.stages) || !plan.stages.length) issues.push('stages');
115
117
  if (!Array.isArray(plan.verification) || !plan.verification.length) issues.push('verification');
118
+ if (!plan.route_economy?.mode) issues.push('route_economy');
116
119
  if (plan.no_unrequested_fallback_code !== true || !plan.invariants?.includes('no_unrequested_fallback_code')) issues.push('fallback_guard');
117
120
  if (!plan.next_actions?.length) issues.push('next_actions');
118
121
  return { ok: issues.length === 0, issues };
@@ -147,7 +150,37 @@ function normalizeProofField(report) {
147
150
  keep: report.execution_lane?.keep || SPEED_LANE_POLICY.always_keep,
148
151
  verification: report.execution_lane?.verification || report.fast_lane_decision?.verification || [],
149
152
  proof_cones: (report.proof_cones || []).map((cone) => cone.id),
150
- source_hash: report.source_hash || null
153
+ source_hash: report.source_hash || null,
154
+ contract_clarity: report.contract_clarity || null,
155
+ workflow_complexity: report.workflow_complexity || null,
156
+ team_trigger_matrix: report.team_trigger_matrix || null,
157
+ verification_stage_cache: report.verification_stage_cache || null
158
+ };
159
+ }
160
+
161
+ function routeEconomyPlan(proof = {}) {
162
+ if (!proof.attached) {
163
+ return {
164
+ schema_version: 1,
165
+ mode: 'unavailable',
166
+ report_only: true,
167
+ reason: proof.reason || 'Proof Field not attached yet'
168
+ };
169
+ }
170
+ const triggers = proof.team_trigger_matrix?.active_triggers || [];
171
+ return {
172
+ schema_version: 1,
173
+ mode: 'report_only',
174
+ report_only: true,
175
+ contract_clarity_score: Number(proof.contract_clarity?.score || 0),
176
+ contract_clarity_passed: proof.contract_clarity?.passed === true,
177
+ ask_recommended: proof.contract_clarity?.ask_recommended === true,
178
+ workflow_complexity_score: Number(proof.workflow_complexity?.score || 0),
179
+ workflow_complexity_band: proof.workflow_complexity?.band || null,
180
+ team_trigger_count: triggers.length,
181
+ active_team_triggers: triggers,
182
+ verification_stage_cache_key: proof.verification_stage_cache?.cache_key || null,
183
+ deletion_policy: 'do_not_delete_or_skip_pipeline_stages_until_report_only_metrics_are_calibrated'
151
184
  };
152
185
  }
153
186
 
@@ -920,11 +953,76 @@ function reflectionStopReason(state = {}, status = {}) {
920
953
  return `SKS ${route} must run reflection before final. Write .sneakoscope/missions/${id}/${REFLECTION_ARTIFACT}, record real lessons in ${REFLECTION_MEMORY_PATH} when present, refresh/pack and validate TriWiki, then pass .sneakoscope/missions/${id}/${REFLECTION_GATE}.${missing}`;
921
954
  }
922
955
 
956
+ export async function projectGateStatus(root, state = {}) {
957
+ const gates = [];
958
+ const id = state?.mission_id || null;
959
+ if (clarificationGatePending(state)) {
960
+ gates.push({
961
+ id: 'clarification-gate',
962
+ ok: false,
963
+ missing: ['explicit_user_answers', 'pipeline_answer'],
964
+ source: id ? `.sneakoscope/missions/${id}/questions.md` : null
965
+ });
966
+ }
967
+ if (state?.context7_required) {
968
+ const evidence = await context7Evidence(root, state);
969
+ gates.push({
970
+ id: 'context7-evidence',
971
+ ok: evidence.ok,
972
+ missing: evidence.ok ? [] : ['resolve-library-id', 'query-docs'],
973
+ source: id ? `.sneakoscope/missions/${id}/context7-evidence.jsonl` : '.sneakoscope/state/context7-evidence.jsonl'
974
+ });
975
+ }
976
+ if (state?.subagents_required) {
977
+ const evidence = await subagentEvidence(root, state);
978
+ gates.push({
979
+ id: 'subagent-evidence',
980
+ ok: evidence.ok,
981
+ missing: evidence.ok ? [] : ['spawn_agent_or_exception_evidence'],
982
+ source: id ? `.sneakoscope/missions/${id}/subagent-evidence.jsonl` : '.sneakoscope/state/subagent-evidence.jsonl'
983
+ });
984
+ }
985
+ if (id && state?.stop_gate && !['none', 'honest_mode'].includes(state.stop_gate)) {
986
+ const active = await passedActiveGate(root, state);
987
+ gates.push({
988
+ id: active.file || state.stop_gate,
989
+ ok: active.ok,
990
+ missing: active.missing || (active.ok ? [] : ['passed']),
991
+ source: active.file ? `.sneakoscope/missions/${id}/${active.file}` : null
992
+ });
993
+ }
994
+ const reflection = await reflectionGateStatus(root, state);
995
+ if (reflectionRequiredForState(state)) {
996
+ gates.push({
997
+ id: REFLECTION_GATE,
998
+ ok: reflection.ok,
999
+ missing: reflection.missing || [],
1000
+ source: id ? `.sneakoscope/missions/${id}/${REFLECTION_GATE}` : null
1001
+ });
1002
+ }
1003
+ const blockers = gates.filter((gate) => !gate.ok).flatMap((gate) => gate.missing.map((item) => `${gate.id}:${item}`));
1004
+ return {
1005
+ schema_version: 1,
1006
+ generated_at: nowIso(),
1007
+ mission_id: id,
1008
+ mode: state?.mode || null,
1009
+ report_only: true,
1010
+ ok: blockers.length === 0,
1011
+ blockers,
1012
+ gates
1013
+ };
1014
+ }
1015
+
923
1016
  export async function evaluateStop(root, state, payload, opts = {}) {
924
1017
  const last = extractLastMessage(payload);
925
- if (state?.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) {
1018
+ if (clarificationGatePending(state)) {
926
1019
  if (await hasVisibleClarificationQuestionBlock(root, state, last)) return { continue: true };
927
- return complianceBlock(root, state, await clarificationStopReason(root, state, 'route'), { gate: 'clarification' });
1020
+ return {
1021
+ decision: 'block',
1022
+ reason: await clarificationStopReason(root, state, 'route'),
1023
+ gate: 'clarification',
1024
+ missing: ['explicit_user_answers', 'pipeline_answer']
1025
+ };
928
1026
  }
929
1027
  if (state?.context7_required && !(await hasContext7DocsEvidence(root, state))) {
930
1028
  return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} requires Context7 evidence before completion. Use Context7 resolve-library-id, then query-docs (or legacy get-library-docs), so SKS can record context7-evidence.jsonl.`, { gate: 'context7-evidence' });
@@ -955,6 +1053,16 @@ export async function evaluateStop(root, state, payload, opts = {}) {
955
1053
  return null;
956
1054
  }
957
1055
 
1056
+ function clarificationGatePending(state = {}) {
1057
+ return Boolean(state?.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS'))
1058
+ || Boolean(
1059
+ state?.mission_id
1060
+ && state.implementation_allowed === false
1061
+ && state.ambiguity_gate_required === true
1062
+ && state.ambiguity_gate_passed !== true
1063
+ );
1064
+ }
1065
+
958
1066
  async function complianceBlock(root, state = {}, reason = '', detail = {}) {
959
1067
  if (!state?.mission_id) return { decision: 'block', reason };
960
1068
  const dir = missionDir(root, state.mission_id);
@@ -82,8 +82,12 @@ export async function buildProofField(root, opts = {}) {
82
82
  const negativeWork = buildNegativeWorkCache(selectedCones, risk);
83
83
  const fastLane = fastLaneDecision({ changedFiles, selectedCones, risk, negativeWork });
84
84
  const sourceHash = await sourceDigest(root, changedFiles);
85
- const simplicity = outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane });
86
- const executionLane = executionLaneDecision({ fastLane, simplicity });
85
+ const contractClarity = contractClarityScore({ intent, changedFiles, selectedCones, risk });
86
+ const workflowComplexity = workflowComplexityScore({ changedFiles, selectedCones, risk, verification: fastLane.verification });
87
+ const teamTriggerMatrix = teamTriggerDecision({ intent, changedFiles, risk });
88
+ const verificationStageCache = verificationStageCachePlan({ sourceHash, changedFiles, verification: fastLane.verification });
89
+ const simplicity = outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane, workflowComplexity });
90
+ const executionLane = executionLaneDecision({ fastLane, simplicity, workflowComplexity, teamTriggerMatrix });
87
91
  return {
88
92
  schema_version: PROOF_FIELD_SCHEMA_VERSION,
89
93
  generated_at: nowIso(),
@@ -94,6 +98,10 @@ export async function buildProofField(root, opts = {}) {
94
98
  invariant_ledger: INVARIANT_LEDGER,
95
99
  outcome_rubric: OUTCOME_RUBRIC,
96
100
  speed_lane_policy: SPEED_LANE_POLICY,
101
+ contract_clarity: contractClarity,
102
+ workflow_complexity: workflowComplexity,
103
+ team_trigger_matrix: teamTriggerMatrix,
104
+ verification_stage_cache: verificationStageCache,
97
105
  simplicity_scorecard: simplicity,
98
106
  execution_lane: executionLane,
99
107
  proof_cones: selectedCones,
@@ -120,6 +128,11 @@ export function validateProofFieldReport(report = {}) {
120
128
  if (!Array.isArray(report.simplicity_scorecard?.criteria) || report.simplicity_scorecard.criteria.length !== OUTCOME_RUBRIC.length) issues.push('simplicity_criteria');
121
129
  if (!report.simplicity_scorecard?.criteria?.every((item) => item.adversarial_lens)) issues.push('simplicity_adversarial_lenses');
122
130
  if (!report.speed_lane_policy || Number(report.speed_lane_policy.min_score) !== FAST_LANE_MIN_SCORE) issues.push('speed_lane_policy');
131
+ if (!Number.isFinite(Number(report.contract_clarity?.score))) issues.push('contract_clarity');
132
+ if (!Array.isArray(report.contract_clarity?.components) || report.contract_clarity.components.length < 1) issues.push('contract_clarity_components');
133
+ if (!Number.isFinite(Number(report.workflow_complexity?.score))) issues.push('workflow_complexity');
134
+ if (!Array.isArray(report.team_trigger_matrix?.triggers)) issues.push('team_trigger_matrix');
135
+ if (report.verification_stage_cache?.report_only !== true || !report.verification_stage_cache?.cache_key) issues.push('verification_stage_cache');
123
136
  if (!report.execution_lane?.lane) issues.push('execution_lane');
124
137
  if (report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane && report.execution_lane?.score < FAST_LANE_MIN_SCORE) issues.push('execution_lane_score');
125
138
  if (!Array.isArray(report.proof_cones)) issues.push('proof_cones');
@@ -144,6 +157,7 @@ export async function proofFieldFixture() {
144
157
  negative_release_work_recorded: report.negative_work_cache.some((item) => item.id === 'full_release_gate' && item.disposition === 'skip_with_evidence'),
145
158
  outcome_rubric_present: report.outcome_rubric.length === OUTCOME_RUBRIC.length,
146
159
  adversarial_lenses_present: report.outcome_rubric.every((item) => item.adversarial_lens) && report.simplicity_scorecard.criteria.every((item) => item.adversarial_lens),
160
+ route_economy_present: report.contract_clarity?.report_only === true && report.workflow_complexity?.report_only === true && report.team_trigger_matrix?.report_only === true && report.verification_stage_cache?.report_only === true,
147
161
  simplicity_score_usable: Number(report.simplicity_scorecard?.score) >= FAST_LANE_MIN_SCORE,
148
162
  execution_fast_lane_selected: report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane
149
163
  }
@@ -237,11 +251,120 @@ function fastLaneDecision({ changedFiles, selectedCones, risk, negativeWork }) {
237
251
  };
238
252
  }
239
253
 
240
- function outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane }) {
254
+ function contractClarityScore({ intent, changedFiles, selectedCones, risk }) {
255
+ const components = [
256
+ {
257
+ id: 'goal_fit',
258
+ floor: 0.7,
259
+ score: intent ? 1 : 0.25,
260
+ evidence: intent ? 'intent provided' : 'missing explicit intent'
261
+ },
262
+ {
263
+ id: 'safety_scope_clarity',
264
+ floor: 0.7,
265
+ score: risk.flags.unknown_surface ? 0.4 : 1,
266
+ evidence: risk.flags.unknown_surface ? 'unknown surface present' : `risk flags classified: ${Object.entries(risk.flags).filter(([, value]) => value).map(([key]) => key).join(', ') || 'none'}`
267
+ },
268
+ {
269
+ id: 'acceptance_observability',
270
+ floor: 0.6,
271
+ score: /accept|verify|test|done|complete|검증|완료|구현|개선/i.test(intent || '') ? 1 : 0.65,
272
+ evidence: /accept|verify|test|done|complete|검증|완료|구현|개선/i.test(intent || '') ? 'observable outcome language present' : 'acceptance inferred from proof cones'
273
+ },
274
+ {
275
+ id: 'write_scope_certainty',
276
+ floor: 0.7,
277
+ score: changedFiles.length > 0 && changedFiles.length <= 3 ? 1 : (changedFiles.length > 0 ? 0.55 : 0.2),
278
+ evidence: `${changedFiles.length} changed file(s) in proposed scope`
279
+ },
280
+ {
281
+ id: 'context_confidence',
282
+ floor: 0.7,
283
+ score: selectedCones.length > 0 && !risk.flags.unknown_surface ? 1 : 0.45,
284
+ evidence: `${selectedCones.length} proof cone(s), unknown_surface=${risk.flags.unknown_surface}`
285
+ }
286
+ ];
287
+ const score = Number((components.reduce((sum, item) => sum + item.score, 0) / components.length).toFixed(2));
288
+ const failed = components.filter((item) => item.score < item.floor).map((item) => item.id);
289
+ return {
290
+ schema_version: 1,
291
+ report_only: true,
292
+ score,
293
+ passed: score >= 0.8 && failed.length === 0,
294
+ ask_recommended: failed.length > 0,
295
+ failed_floors: failed,
296
+ components
297
+ };
298
+ }
299
+
300
+ function workflowComplexityScore({ changedFiles, selectedCones, risk, verification }) {
301
+ const surfaces = new Set(selectedCones.flatMap((cone) => cone.surfaces || []));
302
+ const weighted = {
303
+ changed_files: Math.min(changedFiles.length, 8) / 8 * 0.3,
304
+ proof_cones: Math.min(selectedCones.length, 4) / 4 * 0.2,
305
+ risk_flags: Math.min(risk.score, 4) / 4 * 0.25,
306
+ verification: Math.min((verification || []).length, 6) / 6 * 0.15,
307
+ surfaces: Math.min(surfaces.size, 6) / 6 * 0.1
308
+ };
309
+ const score = Number(Object.values(weighted).reduce((sum, value) => sum + value, 0).toFixed(2));
310
+ const band = score >= 0.67 ? 'frontier' : (score >= 0.34 ? 'balanced' : 'focused');
311
+ return {
312
+ schema_version: 1,
313
+ report_only: true,
314
+ score,
315
+ band,
316
+ inputs: {
317
+ changed_file_count: changedFiles.length,
318
+ proof_cone_count: selectedCones.length,
319
+ route_surface_count: surfaces.size,
320
+ risk_flag_count: risk.score,
321
+ verification_count: (verification || []).length
322
+ },
323
+ weighted
324
+ };
325
+ }
326
+
327
+ function teamTriggerDecision({ intent, changedFiles, risk }) {
328
+ const triggers = [
329
+ { id: 'explicit_team', active: /\$?team\b/i.test(intent || ''), reason: 'user explicitly selected Team' },
330
+ { id: 'broad_change_set', active: changedFiles.length > 3, reason: `${changedFiles.length} changed file(s)` },
331
+ { id: 'database_surface', active: risk.flags.database, reason: 'database risk flag present' },
332
+ { id: 'security_surface', active: risk.flags.security, reason: 'security risk flag present' },
333
+ { id: 'visual_forensic_surface', active: risk.flags.visual_forensic, reason: 'visual forensic risk flag present' },
334
+ { id: 'unknown_surface', active: risk.flags.unknown_surface, reason: 'unknown proof cone selected' },
335
+ { id: 'unsupported_claim', active: false, reason: 'only activated by Honest/H-Proof evidence' },
336
+ { id: 'verification_failed', active: false, reason: 'only activated after verification failure' }
337
+ ];
338
+ const active = triggers.filter((trigger) => trigger.active).map((trigger) => trigger.id);
339
+ return {
340
+ schema_version: 1,
341
+ report_only: true,
342
+ full_team_recommended: active.length > 0,
343
+ active_triggers: active,
344
+ triggers
345
+ };
346
+ }
347
+
348
+ function verificationStageCachePlan({ sourceHash, changedFiles, verification }) {
349
+ const stage1 = [...new Set(verification || [])].filter((command) => /packcheck|selftest|commands --json|wiki validate|eval run/i.test(command));
350
+ const cacheInput = { source_hash: sourceHash || null, changed_files: changedFiles, stage1 };
351
+ return {
352
+ schema_version: 1,
353
+ report_only: true,
354
+ status: sourceHash ? 'planned' : 'source_hash_missing',
355
+ cache_key: sha256(JSON.stringify(cacheInput)).slice(0, 16),
356
+ source_hash: sourceHash || null,
357
+ stage1_commands: stage1,
358
+ invalidates_on: ['source_hash_changed', 'command_changed', 'cwd_changed', 'env_changed'],
359
+ reuse_policy: 'fail_closed'
360
+ };
361
+ }
362
+
363
+ function outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane, workflowComplexity }) {
241
364
  const skipped = negativeWork.filter((item) => item.disposition === 'skip_with_evidence').length;
242
365
  const criteria = [
243
366
  { id: 'goal_fit', passed: Boolean(intent || changedFiles.length), evidence: intent ? 'intent provided' : 'changed files define scope' },
244
- { id: 'minimum_surface', passed: changedFiles.length <= 3 && !risk.flags.unknown_surface, evidence: `${changedFiles.length} changed file(s), ${selectedCones.length} proof cone(s)` },
367
+ { id: 'minimum_surface', passed: changedFiles.length <= 3 && !risk.flags.unknown_surface, evidence: `${changedFiles.length} changed file(s), ${selectedCones.length} proof cone(s), complexity=${workflowComplexity?.band || 'unknown'}` },
245
368
  { id: 'bounded_verification', passed: fastLane.verification.length > 0 && fastLane.verification.length <= 4, evidence: `${fastLane.verification.length} focused verification command(s)` },
246
369
  { id: 'escalation_defined', passed: Array.isArray(fastLane.escalate_on) && fastLane.escalate_on.length > 0, evidence: `${fastLane.escalate_on.length} escalation trigger(s)` }
247
370
  ].map((criterion) => ({
@@ -258,7 +381,7 @@ function outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeW
258
381
  };
259
382
  }
260
383
 
261
- function executionLaneDecision({ fastLane, simplicity }) {
384
+ function executionLaneDecision({ fastLane, simplicity, workflowComplexity, teamTriggerMatrix }) {
262
385
  const score = Number(simplicity?.score || 0);
263
386
  const fast = Boolean(fastLane?.eligible) && score >= FAST_LANE_MIN_SCORE;
264
387
  const lane = fast
@@ -276,7 +399,7 @@ function executionLaneDecision({ fastLane, simplicity }) {
276
399
  escalate_on: [...new Set([...(fastLane?.escalate_on || []), ...SPEED_LANE_POLICY.fail_closed_on])],
277
400
  reason: fast
278
401
  ? `Proof Field score ${score} >= ${FAST_LANE_MIN_SCORE} with no fast-lane blockers`
279
- : `Fast lane not allowed: mode=${fastLane?.mode || 'unknown'}, score=${score}, blockers=${(fastLane?.blockers || []).join(', ') || 'none'}`
402
+ : `Fast lane not allowed: mode=${fastLane?.mode || 'unknown'}, score=${score}, complexity=${workflowComplexity?.band || 'unknown'}, team_triggers=${(teamTriggerMatrix?.active_triggers || []).join(', ') || 'none'}, blockers=${(fastLane?.blockers || []).join(', ') || 'none'}`
280
403
  };
281
404
  }
282
405