@ngockhoale/ukit 2.2.14 → 2.3.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/manifests/platform.full.yaml +11 -13
  3. package/package.json +1 -1
  4. package/src/core/executionContracts.js +130 -0
  5. package/src/core/runtimeConfig.js +13 -50
  6. package/src/index/taskRouting.js +10 -103
  7. package/templates/.claude/agents/ukit-vision-analyst.md +32 -21
  8. package/templates/.claude/hooks/context-hardcap-gate.sh +2 -2
  9. package/templates/.claude/hooks/protect-files.sh +1 -0
  10. package/templates/.claude/hooks/sensitive-data-guard.sh +269 -0
  11. package/templates/.claude/hooks/skill-router.sh +6 -0
  12. package/templates/.claude/hooks/vision-router.sh +67 -47
  13. package/templates/.claude/settings.json +17 -6
  14. package/templates/.claude/ukit/index/extract-image.mjs +18 -8
  15. package/templates/.claude/ukit/index/provision-worktree.mjs +1 -1
  16. package/templates/.claude/ukit/index/route-task.mjs +53 -2
  17. package/templates/.claude/ukit/index/unic-gateway.mjs +1 -1
  18. package/templates/.claude/ukit/runtime/execution-ledger.mjs +120 -21
  19. package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +8 -5
  20. package/templates/.omp/README.md +3 -3
  21. package/templates/.omp/hooks/pre/ukit-bridge.js +15 -7
  22. package/templates/AGENTS.md +2 -2
  23. package/templates/CLAUDE.md +2 -2
  24. package/templates/ukit/storage/config.json +8 -7
  25. package/src/core/router/advisor.js +0 -42
  26. package/src/core/router/router.js +0 -180
  27. package/src/core/validation/confidence.js +0 -89
  28. package/src/core/validation/validator.js +0 -165
  29. package/templates/.claude/hooks/vision-gate.sh +0 -230
  30. package/templates/docs/INSTALL.md +0 -115
  31. package/templates/docs/STATUS.md +0 -81
  32. package/templates/docs/TASKS.md +0 -79
  33. package/templates/docs/UKIT_USAGE_GUIDE.md +0 -163
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.3.0 - 2026-09-08
6
+
7
+ Model-agnostic hardening release. Every configured model name is treated as a gateway
8
+ mapping whose backend can change at any time, so correctness now comes from stable role
9
+ contracts, explicit evidence, and regression fixtures — never provider identity.
10
+
11
+ **Sensitive-data gate (fail-closed, default ON).** NEW `sensitive-data-guard.sh` scans
12
+ PreToolUse Read/Grep paths, Bash commands (secret-dump shapes, inline credentials,
13
+ high-confidence token patterns like `sk-…`), and UserPromptSubmit text; on suspicion it
14
+ blocks with exit 2 and shows only redacted previews (3-char prefix + length + sha256
15
+ prefix) — never the value. The only approval path is the USER editing the protected
16
+ allowlist `.ukit/storage/security/allowlist.json` (sha256 of the value, or an allowed file
17
+ path); `security.sensitiveDataGate: true` ships as the default and the gate can be disabled
18
+ in `.ukit/storage/config.json` for debugging. Wired into Claude settings hook chains, the
19
+ omp bridge, and the install manifest. Live-fire verified across all four channels.
20
+
21
+ **Contract tables unified to one source.** NEW `src/core/executionContracts.js` is the
22
+ canonical table set (execution modes, contracts, contract→tier mapping, risk profiles).
23
+ `taskRouting.js` imports it, runtime config defaults derive from it, and the shipped
24
+ `route-task.mjs` keeps a literal copy locked by a new sync test — fixing a real drift where
25
+ route-task's `local-build` contract was missing verification evidence and the post-edit
26
+ review policy. `config.json` no longer duplicates `modelTier` per contract.
27
+
28
+ **Tier-role wiring.** Route state now carries a `tierLane` hand-off: silent when the
29
+ contract tier is `code` (the default implementation lane), an explicit "hand this to an
30
+ agent bound to the lite/smart tier" instruction otherwise, and it follows `escalatedTier`
31
+ on repeat failures. This surfaced and fixed a real gap: `compactRouteSummary` is a
32
+ whitelist and was silently dropping `expectedSourceFiles`, which made target-aware
33
+ completion evidence inert on the real CLI path.
34
+
35
+ **Evidence tightening.** Completion-gate `impact-evidence` now requires reading one of the
36
+ routed target files (any random read no longer counts), and verification receipts record
37
+ `targeted` vs `broad` scope against the routed verification plan; recovery instructions
38
+ name the exact files expected.
39
+
40
+ **Vision flow smoothing.** Already-analysed images no longer re-trigger route hints
41
+ (`alreadyAnalyzed[]` from `extract-image.mjs --json`); unresolvable image paths get a short
42
+ note instead of a wrong remedy; the vision analyst gets a capability self-check (first
43
+ image Read is the probe), a task envelope carrying the ORIGINAL user prompt verbatim, and
44
+ an observations-vs-inference output block; hook-chain budgets now scale with chain size so
45
+ growing Edit/Write chains never orphan the runner.
46
+
47
+ **Mapping fingerprint fixtures.** New consistency tests lock agent↔tier bindings across
48
+ Claude Code and omp, the omp modelRoles → `unic-*` labels, and the tier structure
49
+ (vision stays a capability tier outside the escalation ladder) — provider identities are
50
+ deliberately left unpinned so gateway mappings can change freely without breaking
51
+ behavior contracts.
52
+
53
+ Verified: `yarn test` 1277/1277 (74 files), `test:release-core` 1270, `test:artifact` 7,
54
+ `release:verify` all checks passed, 13/13 handoff node suites, sensitive-data-guard
55
+ live-fire 5/5 as designed.
56
+
57
+ ## 2.2.16 - 2026-09-06
58
+
59
+ The vision gate is gone — image work is advisory-routed, never blocked. Since TASK-008, a
60
+ pasted image on the UNIC gateway armed `vision-gate.sh`, which refused every Edit/Write until
61
+ `ukit-vision-analyst` had produced receipts for ALL pending images. In practice this
62
+ repeatedly stranded sessions (receipts that never landed, one stale marker blocking
63
+ unrelated writes), so the maintainer retired the hard block: `vision-gate.sh` and its
64
+ manifest entry `hook-vision-gate` are removed from both the live tree and templates, the omp
65
+ bridge and `hook-chain-runner.mjs` no longer list it, and `vision-router.sh` now says "never
66
+ guess at image contents" instead of claiming edits are GATED. Routing to
67
+ `ukit-vision-analyst` still happens — on this gateway the main tier genuinely cannot read
68
+ images — but nothing blocks while it runs. Supersedes the 2.2.15 escape hatch, which patched
69
+ a message on a gate that no longer exists. Verified: vision-hook, route-vision,
70
+ vision-agent, install-wiring, provision-worktree, and ompHookBridge (52/52) all green.
71
+ (An interim 2026-09-05 patch that added an escape hatch to the block message is included
72
+ here and immediately superseded — the gate itself no longer exists.)
73
+
5
74
  ## 2.2.14 - 2026-09-05
6
75
 
7
76
  The context hard-cap gate no longer bricks healthy sessions with a phantom token count. The
@@ -1094,6 +1094,17 @@ items:
1094
1094
  packs:
1095
1095
  - core
1096
1096
 
1097
+ - id: hook-sensitive-data-guard
1098
+ type: hook
1099
+ sourceTemplate: .claude/hooks/sensitive-data-guard.sh
1100
+ targetPath: .claude/hooks/sensitive-data-guard.sh
1101
+ requires: []
1102
+ mergeStrategy: overwrite_with_backup
1103
+ variables: []
1104
+ enabledByDefault: true
1105
+ packs:
1106
+ - core
1107
+
1097
1108
  - id: hook-handoff-model-guard
1098
1109
  type: hook
1099
1110
  sourceTemplate: .claude/hooks/handoff-model-guard.sh
@@ -1131,19 +1142,6 @@ items:
1131
1142
  packs:
1132
1143
  - core
1133
1144
 
1134
- - id: hook-vision-gate
1135
- type: hook
1136
- sourceTemplate: .claude/hooks/vision-gate.sh
1137
- targetPath: .claude/hooks/vision-gate.sh
1138
- requires:
1139
- - ukit-index-extract-image-script
1140
- - ukit-index-unic-gateway-script
1141
- mergeStrategy: overwrite_with_backup
1142
- variables: []
1143
- enabledByDefault: true
1144
- packs:
1145
- - core
1146
-
1147
1145
  - id: hook-context-hardcap-gate
1148
1146
  type: hook
1149
1147
  sourceTemplate: .claude/hooks/context-hardcap-gate.sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.2.14",
3
+ "version": "2.3.0",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,130 @@
1
+ // Single source of truth for UKit execution contracts (WS-D contract table unification).
2
+ //
3
+ // Every routing decision that is keyed on an execution contract derives from the tables in
4
+ // this file:
5
+ // - src/index/taskRouting.js (in-process routing: contracts + risk profiles + mode order)
6
+ // - src/core/runtimeConfig.js (config defaults: contracts minus runtime-only keys)
7
+ // - templates/ukit/storage/config.json (literal mirror of the config defaults)
8
+ // - templates/.claude/ukit/index/route-task.mjs (shipped standalone CLI: contracts + tiers)
9
+ //
10
+ // route-task.mjs runs inside target projects and cannot import from src/, so it keeps a
11
+ // literal copy — tests/consistency/executionContractSync.test.js locks that copy to these
12
+ // tables so the four can never drift again (the local-build completionEvidence drift that
13
+ // motivated this module is the failure mode it exists to prevent).
14
+
15
+ export const EXECUTION_MODE_ORDER = [
16
+ 'tiny-fix',
17
+ 'local-fix',
18
+ 'local-build',
19
+ 'find-cause',
20
+ 'shared-edit',
21
+ 'map-impact',
22
+ 'review-release',
23
+ ];
24
+
25
+ export const EXECUTION_CONTRACTS = {
26
+ 'tiny-fix': {
27
+ maxReadPasses: 0,
28
+ maxContextPulls: 0,
29
+ verificationPolicy: 'minimal-or-targeted',
30
+ completionRule: 'never-claim-done-without-write',
31
+ delegationPolicy: 'disallow',
32
+ completionEvidence: ['write-evidence'],
33
+ },
34
+ 'local-fix': {
35
+ maxReadPasses: 1,
36
+ maxContextPulls: 1,
37
+ verificationPolicy: 'targeted-if-covered',
38
+ completionRule: 'require-write',
39
+ delegationPolicy: 'disallow',
40
+ completionEvidence: ['write-evidence'],
41
+ },
42
+ 'local-build': {
43
+ maxReadPasses: 2,
44
+ maxContextPulls: 1,
45
+ verificationPolicy: 'targeted-if-covered',
46
+ completionRule: 'require-write-and-verification',
47
+ delegationPolicy: 'disallow-by-default',
48
+ completionEvidence: ['write-evidence', 'verification-evidence'],
49
+ postEditReviewPolicy: 'sidecar-non-blocking',
50
+ },
51
+ 'find-cause': {
52
+ maxReadPassesBeforeReassess: 3,
53
+ verificationPolicy: 'root-cause-then-targeted',
54
+ completionRule: 'never-claim-fixed-without-write-and-verification',
55
+ delegationPolicy: 'allow-specialized-debug-lane',
56
+ completionEvidence: ['write-evidence', 'verification-evidence'],
57
+ },
58
+ 'shared-edit': {
59
+ maxReadPasses: 2,
60
+ maxContextPulls: 2,
61
+ verificationPolicy: 'targeted-then-widen-on-risk',
62
+ completionRule: 'require-write-and-verification',
63
+ delegationPolicy: 'allow-qualified-sidecar',
64
+ completionEvidence: ['write-evidence', 'verification-evidence'],
65
+ mirrorConsistencyRequired: true,
66
+ postEditReviewPolicy: 'sidecar-non-blocking',
67
+ },
68
+ 'map-impact': {
69
+ maxReadPasses: 3,
70
+ maxContextPulls: 3,
71
+ verificationPolicy: 'impact-first-then-targeted-then-widen-on-risk',
72
+ completionRule: 'require-impact-evidence-before-edit-claim',
73
+ delegationPolicy: 'allow-impact-sidecar',
74
+ completionEvidence: ['impact-evidence', 'write-evidence', 'verification-evidence'],
75
+ mirrorConsistencyRequired: true,
76
+ },
77
+ 'review-release': {
78
+ verificationPolicy: 'evidence-first',
79
+ completionRule: 'report-findings-not-implementation',
80
+ delegationPolicy: 'allow-review-sidecar',
81
+ completionEvidence: ['verification-evidence'],
82
+ },
83
+ };
84
+
85
+ export const MODEL_TIER_BY_CONTRACT = {
86
+ 'tiny-fix': 'lite',
87
+ 'local-fix': 'code',
88
+ 'local-build': 'code',
89
+ 'find-cause': 'code',
90
+ 'shared-edit': 'code',
91
+ 'map-impact': 'code',
92
+ 'review-release': 'smart',
93
+ };
94
+
95
+ export const CONTRACT_RISK_PROFILES = {
96
+ 'tiny-fix': { riskLevel: 'minimal', contextPolicy: 'confirm-target-only' },
97
+ 'local-fix': { riskLevel: 'local', contextPolicy: 'bounded-local' },
98
+ 'local-build': { riskLevel: 'local', contextPolicy: 'bounded-with-related-files' },
99
+ 'find-cause': { riskLevel: 'investigation', contextPolicy: 'trace-then-bounded-context' },
100
+ 'shared-edit': { riskLevel: 'shared', contextPolicy: 'bounded-with-related-tests' },
101
+ 'map-impact': { riskLevel: 'wide', contextPolicy: 'impact-map-first' },
102
+ 'review-release': { riskLevel: 'release', contextPolicy: 'evidence-review-only' },
103
+ };
104
+
105
+ const CONFIG_EXCLUDED_KEYS = new Set(['modelTier', 'completionEvidence', 'mirrorConsistencyRequired']);
106
+
107
+ /** The user-facing config subset of a contract: runtime-only bookkeeping keys stripped. */
108
+ export function stripContractForConfig(contract = {}) {
109
+ return Object.fromEntries(
110
+ Object.entries(contract).filter(([key]) => !CONFIG_EXCLUDED_KEYS.has(key))
111
+ );
112
+ }
113
+
114
+ export function buildExecutionContract(executionMode = null) {
115
+ if (!executionMode) {
116
+ return null;
117
+ }
118
+ const contract = EXECUTION_CONTRACTS[executionMode];
119
+ if (!contract) {
120
+ return null;
121
+ }
122
+ return { ...contract, modelTier: MODEL_TIER_BY_CONTRACT[executionMode] };
123
+ }
124
+
125
+ /** Config-default shape: contracts without modelTier/completionEvidence/mirror keys. */
126
+ export function buildConfigContracts() {
127
+ return Object.fromEntries(
128
+ Object.entries(EXECUTION_CONTRACTS).map(([mode, contract]) => [mode, stripContractForConfig(contract)])
129
+ );
130
+ }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { buildRuntimePaths } from './runtimePaths.js';
5
5
  import { loadShippedCompactBudget } from './compact/contextBudget.js';
6
+ import { buildConfigContracts } from './executionContracts.js';
6
7
 
7
8
  const require = createRequire(import.meta.url);
8
9
  const { version: PACKAGE_VERSION } = require('../../package.json');
@@ -69,6 +70,10 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
69
70
  affectVerification: true,
70
71
  affectDelegation: true,
71
72
  },
73
+ security: {
74
+ sensitiveDataGate: true,
75
+ allowlistPath: '.ukit/storage/security/allowlist.json',
76
+ },
72
77
  compact: {
73
78
  enabled: true,
74
79
  tokenThreshold: loadShippedCompactBudget().tokenThreshold,
@@ -132,56 +137,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
132
137
  enabled: true,
133
138
  orchestratorModel: 'claude-sonnet-5',
134
139
  advisorEnabled: true,
135
- contracts: {
136
- 'tiny-fix': {
137
- maxReadPasses: 0,
138
- maxContextPulls: 0,
139
- verificationPolicy: 'minimal-or-targeted',
140
- completionRule: 'never-claim-done-without-write',
141
- delegationPolicy: 'disallow',
142
- },
143
- 'local-fix': {
144
- maxReadPasses: 1,
145
- maxContextPulls: 1,
146
- verificationPolicy: 'targeted-if-covered',
147
- completionRule: 'require-write',
148
- delegationPolicy: 'disallow',
149
- },
150
- 'local-build': {
151
- maxReadPasses: 2,
152
- maxContextPulls: 1,
153
- verificationPolicy: 'targeted-if-covered',
154
- completionRule: 'require-write-and-verification',
155
- delegationPolicy: 'disallow-by-default',
156
- postEditReviewPolicy: 'sidecar-non-blocking',
157
- },
158
- 'find-cause': {
159
- maxReadPassesBeforeReassess: 3,
160
- verificationPolicy: 'root-cause-then-targeted',
161
- completionRule: 'never-claim-fixed-without-write-and-verification',
162
- delegationPolicy: 'allow-specialized-debug-lane',
163
- },
164
- 'shared-edit': {
165
- maxReadPasses: 2,
166
- maxContextPulls: 2,
167
- verificationPolicy: 'targeted-then-widen-on-risk',
168
- completionRule: 'require-write-and-verification',
169
- delegationPolicy: 'allow-qualified-sidecar',
170
- postEditReviewPolicy: 'sidecar-non-blocking',
171
- },
172
- 'map-impact': {
173
- maxReadPasses: 3,
174
- maxContextPulls: 3,
175
- verificationPolicy: 'impact-first-then-targeted-then-widen-on-risk',
176
- completionRule: 'require-impact-evidence-before-edit-claim',
177
- delegationPolicy: 'allow-impact-sidecar',
178
- },
179
- 'review-release': {
180
- verificationPolicy: 'evidence-first',
181
- completionRule: 'report-findings-not-implementation',
182
- delegationPolicy: 'allow-review-sidecar',
183
- },
184
- },
140
+ contracts: buildConfigContracts(),
185
141
  modelTiers: {
186
142
  lite: { claudeModel: 'claude-haiku-4-5', genericModel: 'unic-lite' },
187
143
  code: { claudeModel: 'claude-sonnet-5', genericModel: 'unic-code' },
@@ -312,6 +268,13 @@ export function validateRuntimeConfig(config) {
312
268
  pushBooleanError(errors, config.autonomy.affectDelegation, 'autonomy.affectDelegation');
313
269
  }
314
270
 
271
+ if (!isPlainObject(config.security)) {
272
+ errors.push('security must be an object.');
273
+ } else {
274
+ pushBooleanError(errors, config.security.sensitiveDataGate, 'security.sensitiveDataGate');
275
+ pushNonEmptyStringError(errors, config.security.allowlistPath, 'security.allowlistPath');
276
+ }
277
+
315
278
  if (!VALID_AGENTS.has(config.agent)) {
316
279
  errors.push(`agent must be one of: ${[...VALID_AGENTS].join(', ')}.`);
317
280
  }
@@ -6,6 +6,11 @@ import { buildRouteSignalText } from './languageTools.js';
6
6
  import { resolveContext } from './resolveContext.js';
7
7
  import { deriveVerificationPlan } from './verificationPlan.js';
8
8
  import { isSharedImpactFile } from './impactCatalog.js';
9
+ import {
10
+ CONTRACT_RISK_PROFILES,
11
+ EXECUTION_CONTRACTS,
12
+ EXECUTION_MODE_ORDER,
13
+ } from '../core/executionContracts.js';
9
14
 
10
15
  const MAX_ACTIVE_ROUTE_SKILLS = 2;
11
16
 
@@ -545,18 +550,8 @@ function applySafeUpwardBias(candidates = [], fallbackMode = 'local-build') {
545
550
  }
546
551
 
547
552
  function executionModeRank(mode = '') {
548
- const orderedModes = [
549
- 'tiny-fix',
550
- 'local-fix',
551
- 'local-build',
552
- 'find-cause',
553
- 'shared-edit',
554
- 'map-impact',
555
- 'review-release',
556
- ];
557
-
558
- const index = orderedModes.indexOf(mode);
559
- return index >= 0 ? index : orderedModes.length;
553
+ const index = EXECUTION_MODE_ORDER.indexOf(mode);
554
+ return index >= 0 ? index : EXECUTION_MODE_ORDER.length;
560
555
  }
561
556
 
562
557
  function buildExecutionContract(executionMode = null) {
@@ -564,67 +559,8 @@ function buildExecutionContract(executionMode = null) {
564
559
  return null;
565
560
  }
566
561
 
567
- const contracts = {
568
- 'tiny-fix': {
569
- maxReadPasses: 0,
570
- maxContextPulls: 0,
571
- verificationPolicy: 'minimal-or-targeted',
572
- completionRule: 'never-claim-done-without-write',
573
- delegationPolicy: 'disallow',
574
- completionEvidence: ['write-evidence'],
575
- },
576
- 'local-fix': {
577
- maxReadPasses: 1,
578
- maxContextPulls: 1,
579
- verificationPolicy: 'targeted-if-covered',
580
- completionRule: 'require-write',
581
- delegationPolicy: 'disallow',
582
- completionEvidence: ['write-evidence'],
583
- },
584
- 'local-build': {
585
- maxReadPasses: 2,
586
- maxContextPulls: 1,
587
- verificationPolicy: 'targeted-if-covered',
588
- completionRule: 'require-write-and-verification',
589
- delegationPolicy: 'disallow-by-default',
590
- completionEvidence: ['write-evidence', 'verification-evidence'],
591
- postEditReviewPolicy: 'sidecar-non-blocking',
592
- },
593
- 'find-cause': {
594
- maxReadPassesBeforeReassess: 3,
595
- verificationPolicy: 'root-cause-then-targeted',
596
- completionRule: 'never-claim-fixed-without-write-and-verification',
597
- delegationPolicy: 'allow-specialized-debug-lane',
598
- completionEvidence: ['write-evidence', 'verification-evidence'],
599
- },
600
- 'shared-edit': {
601
- maxReadPasses: 2,
602
- maxContextPulls: 2,
603
- verificationPolicy: 'targeted-then-widen-on-risk',
604
- completionRule: 'require-write-and-verification',
605
- delegationPolicy: 'allow-qualified-sidecar',
606
- completionEvidence: ['write-evidence', 'verification-evidence'],
607
- mirrorConsistencyRequired: true,
608
- postEditReviewPolicy: 'sidecar-non-blocking',
609
- },
610
- 'map-impact': {
611
- maxReadPasses: 3,
612
- maxContextPulls: 3,
613
- verificationPolicy: 'impact-first-then-targeted-then-widen-on-risk',
614
- completionRule: 'require-impact-evidence-before-edit-claim',
615
- delegationPolicy: 'allow-impact-sidecar',
616
- completionEvidence: ['impact-evidence', 'write-evidence', 'verification-evidence'],
617
- mirrorConsistencyRequired: true,
618
- },
619
- 'review-release': {
620
- verificationPolicy: 'evidence-first',
621
- completionRule: 'report-findings-not-implementation',
622
- delegationPolicy: 'allow-review-sidecar',
623
- completionEvidence: ['verification-evidence'],
624
- },
625
- };
626
-
627
- return contracts[executionMode] ? { ...contracts[executionMode] } : null;
562
+ const contract = EXECUTION_CONTRACTS[executionMode];
563
+ return contract ? { ...contract } : null;
628
564
  }
629
565
 
630
566
  function buildApproachSelectorResult({
@@ -637,36 +573,7 @@ function buildApproachSelectorResult({
637
573
  }
638
574
 
639
575
  const executionContract = buildExecutionContract(executionMode);
640
- const policyByMode = {
641
- 'tiny-fix': {
642
- riskLevel: 'minimal',
643
- contextPolicy: 'confirm-target-only',
644
- },
645
- 'local-fix': {
646
- riskLevel: 'local',
647
- contextPolicy: 'bounded-local',
648
- },
649
- 'local-build': {
650
- riskLevel: 'local',
651
- contextPolicy: 'bounded-with-related-files',
652
- },
653
- 'find-cause': {
654
- riskLevel: 'investigation',
655
- contextPolicy: 'trace-then-bounded-context',
656
- },
657
- 'shared-edit': {
658
- riskLevel: 'shared',
659
- contextPolicy: 'bounded-with-related-tests',
660
- },
661
- 'map-impact': {
662
- riskLevel: 'wide',
663
- contextPolicy: 'impact-map-first',
664
- },
665
- 'review-release': {
666
- riskLevel: 'release',
667
- contextPolicy: 'evidence-review-only',
668
- },
669
- };
576
+ const policyByMode = CONTRACT_RISK_PROFILES;
670
577
  const selectedPolicy = policyByMode[executionMode] ?? {
671
578
  riskLevel: 'local',
672
579
  contextPolicy: 'bounded-local',
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ukit-vision-analyst
3
- description: "The only lane permitted to interpret images in this repo. Use whenever a prompt references, attaches, or points at an image (screenshot, mockup, diagram, photo of an error) and the caller needs to know what is actually in it. unic-code/unic-smart cannot read images on this gateway and must never guess at their contents — route image analysis here instead. Reports findings only; never writes product code."
4
- model: unic-vision # real gateway model name, NOT an alias. unic-code/unic-smart cannot read images on this gateway — do NOT "fix" this to sonnet/opus.
3
+ description: "The vision specialist lane for this repo. Use whenever a prompt references, attaches, or points at an image (screenshot, mockup, diagram, photo of an error) and the caller needs to know what is actually in it. Non-vision mappings (unic-code/unic-smart) must never guess at image contents — route image analysis here whenever the active model has not verified native vision. Reports findings only; never writes product code."
4
+ model: unic-vision # capability-lane gateway mapping (image-capable backend). Not a cost tier; never "fix" this to a lite/code/smart label.
5
5
  color: magenta
6
6
  tools: ["Read", "Glob", "Bash"]
7
7
  ---
@@ -18,19 +18,20 @@ actually in them. You never write, edit, or refactor product code — you analys
18
18
  - Stay end-user-invisible: this lane is internal UKit orchestration, not something end users invoke
19
19
  by name.
20
20
 
21
- ## 2. Model self-check — first action, before touching any image
21
+ ## 2. Capability self-check — the first image Read is the probe
22
22
 
23
- Before reading any image, determine the model you are actually running on right now.
23
+ Model names are gateway mappings whose backend can change at any time, so capability is
24
+ demonstrated, never assumed from a name:
24
25
 
25
- - Vision-capable means: `unic-vision`, or — when `unicMode` is off — whatever
26
- `modelTiers.vision.fallbackModel` resolves to per
27
- `node .claude/ukit/index/unic-gateway.mjs --json`.
28
- - If you cannot confirm you are running on a vision-capable model, you must **refuse**: emit
29
- `STATUS: WRONG_MODEL` in the output block below and stop immediately. Do not open, describe, or
30
- guess at any image content.
31
- - Never guess at image contents. A wrong-model "analysis" is worse than no analysis at all,
32
- because it looks authoritative while being fabricated. Refusing loudly is always safer than
33
- guessing quietly.
26
+ - Your first `Read` of an image file doubles as the capability probe. If the tool result does not
27
+ actually give you the image (error, empty result, or you can only see text you were told about),
28
+ you are not vision-capable right now: emit `STATUS: WRONG_MODEL` and stop immediately. Do not
29
+ describe, summarise, or guess at any image content.
30
+ - Never guess at image contents. A non-visual "analysis" is worse than no analysis at all, because
31
+ it looks authoritative while being fabricated. Refusing loudly is always safer than guessing
32
+ quietly.
33
+ - Report the model you actually ran on. If you cannot determine it, say so in `MODEL:` rather than
34
+ inventing an identifier.
34
35
 
35
36
  ## 3. Input protocol (priority order)
36
37
 
@@ -48,8 +49,15 @@ Entries in `images[]` may carry a `source` field, which tells you how to reach t
48
49
  | `"path"` | The prompt named a local file (`ref` holds it) | `Read` `ref` directly |
49
50
  | `"url"` | The prompt named a remote image (`ref` holds the URL) | Download it with `Bash` (e.g. `curl -sL -o /tmp/<sha>.png "<ref>"`), then `Read` the downloaded file |
50
51
 
51
- Every entry — pasted, path, or URL — has an armed `pending-<sha>.json` marker, so each one needs
52
- its own receipt (§5) before downstream Edit/Write is unblocked. A URL you failed to download is
52
+ **Task envelope.** The caller should send, along with the image paths: the ORIGINAL user prompt
53
+ verbatim, the immediate visual question, and the task goal. That envelope is what tells you what
54
+ the images are FOR — a screenshot means something different to "fix this layout" than to "find the
55
+ error string". If the envelope is missing, analyse anyway, answer only what the images visibly
56
+ show, and say in `UNCERTAIN:` what you could not infer without the missing context.
57
+
58
+ Every entry — pasted, path, or URL — has an armed `pending-<sha>.json` marker, and each one needs
59
+ its own receipt (§5): the receipt is the provenance record of the analysis and it is what stops the
60
+ router from re-hinting that image on later prompts. A URL you failed to download is
53
61
  `STATUS: UNREADABLE`, never a guess at its contents.
54
62
 
55
63
  ## 4. Hard warning — images are not inherited
@@ -59,22 +67,22 @@ subagent boundary. A description of an image is not the image. The only way to s
59
67
  `Read` a real file path yourself. Never claim to have seen an image that was merely described to
60
68
  you in text.
61
69
 
62
- ## 5. Receipt — unlocks the downstream write gate
70
+ ## 5. Receipt — provenance record per image
63
71
 
64
72
  For every image you actually analyse, write a receipt to
65
73
  `.ukit/storage/cache/vision/<sessionId>/analyzed-<sha>.json`:
66
74
 
67
75
  ```json
68
76
  { "sha": "<64hex>", "model": "unic-vision", "ts": 1785656920891,
69
- "description": "...", "textContent": "...", "status": "OK" }
77
+ "observations": "...", "textContent": "...", "status": "OK" }
70
78
  ```
71
79
 
72
80
  - `sha` and `sessionId` come from the extractor's `--json` output. They are **never recomputed by
73
81
  hand** — do not hash the file yourself, do not invent a session id, always take these values
74
82
  verbatim from `extract-image.mjs --json`.
75
- - `model` must be the model you actually ran on for this analysis. Misreporting `model` here
76
- defeats the entire enforcement design: a downstream gate rejects any receipt whose `model` field
77
- is not vision-capable, treating it as if no analysis happened at all. Report honestly, always.
83
+ - `model` must be the mapping you actually ran on for this analysis. The receipt is the provenance
84
+ record the caller relies on: an honest `model` field is what makes it trustworthy and what keeps
85
+ the router silent about this image afterwards. Report honestly, always.
78
86
  - Use `Bash` to write the receipt file.
79
87
 
80
88
  ## 6. Output block
@@ -85,8 +93,9 @@ Always end with:
85
93
  STATUS: OK | NO_IMAGE | UNREADABLE | WRONG_MODEL
86
94
  MODEL: <actual model>
87
95
  IMAGES: <n> (<paths>)
88
- DESCRIPTION: [what is actually visible]
96
+ OBSERVATIONS: [what is directly visible — layout, elements, colors, structure; facts only]
89
97
  TEXT_CONTENT: [verbatim text/code/errors legible in the image, or "none"]
98
+ INFERENCES: [what the observations likely mean — interpretation, clearly separate from facts]
90
99
  RELEVANT_TO_TASK: [how it answers the caller's question]
91
100
  UNCERTAIN: [anything ambiguous or illegible, or "none"]
92
101
  ```
@@ -94,6 +103,8 @@ UNCERTAIN: [anything ambiguous or illegible, or "none"]
94
103
  ## 7. Guardrails
95
104
 
96
105
  - Transcribe error text and code verbatim rather than paraphrasing it.
106
+ - Keep `OBSERVATIONS:` strictly factual; every interpretation goes in `INFERENCES:`. The caller
107
+ must be able to tell what you saw from what you concluded.
97
108
  - State uncertainty explicitly instead of guessing — use `UNCERTAIN:` for anything ambiguous or
98
109
  illegible.
99
110
  - Do not read unrelated repo files; stay scoped to the image(s) and the immediate task question.
@@ -177,8 +177,8 @@ function readRunCursor() {
177
177
  process.stderr.write(`${lines.join('\n')}\n`);
178
178
  process.exit(2);
179
179
  })().catch((err) => {
180
- // Unlike vision-gate.sh, a logic error here fails OPEN: this is a backstop on top of
181
- // advisory nudges, not a correctness gate — a broken gate must not brick every session.
180
+ // A logic error here fails OPEN: this is a backstop on top of advisory nudges,
181
+ // not a correctness gate — a broken gate must not brick every session.
182
182
  process.stderr.write(`context-hardcap-gate: internal error, failing open: ${err?.message ?? err}\n`);
183
183
  process.exit(0);
184
184
  });
@@ -25,6 +25,7 @@ PROTECTED_PATTERNS=(
25
25
  ".git/"
26
26
  "node_modules/"
27
27
  ".claude/settings.local.json"
28
+ ".ukit/storage/security/"
28
29
  )
29
30
 
30
31
  for pattern in "${PROTECTED_PATTERNS[@]}"; do