@ngockhoale/ukit 3.4.46 → 3.4.48

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 (66) hide show
  1. package/CHANGELOG.md +13 -5
  2. package/manifests/documentation.yaml +22 -0
  3. package/manifests/platform.full.yaml +27 -0
  4. package/package.json +4 -1
  5. package/scripts/dev/capture-omp-stall.mjs +246 -0
  6. package/scripts/dev/worktree-preflight.mjs +20 -1
  7. package/scripts/docs/render-instructions.mjs +29 -5
  8. package/src/core/agentRuntime/supervisor.js +15 -1
  9. package/src/core/diffPlan.js +38 -12
  10. package/src/core/executionContracts.js +67 -16
  11. package/src/core/metadata.js +4 -0
  12. package/src/core/runInstallPipeline.js +19 -2
  13. package/src/core/runtimeConfig.js +1 -1
  14. package/src/diagnostics/ompStallSnapshot.js +199 -0
  15. package/src/index/playbookRegistry.js +22 -3
  16. package/src/index/taskRouting.js +2 -2
  17. package/template_project/.claude/agents/bug-debugger.md +1 -0
  18. package/template_project/.claude/agents/code-reviewer.md +1 -0
  19. package/template_project/.claude/agents/feature-implementer.md +1 -0
  20. package/template_project/.claude/agents/handoff-planner.md +1 -0
  21. package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
  22. package/template_project/.claude/hooks/completion-gate.sh +1 -1
  23. package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
  24. package/template_project/.claude/hooks/skill-router.sh +147 -6
  25. package/template_project/.claude/settings.json +8 -8
  26. package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
  27. package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
  28. package/template_project/.claude/ukit/index/route-task.mjs +115 -37
  29. package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
  30. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
  31. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
  32. package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
  33. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  34. package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
  35. package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
  36. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +80 -3
  37. package/template_project/.omp/agents/bug-debugger.md +1 -0
  38. package/template_project/.omp/agents/code-reviewer.md +1 -0
  39. package/template_project/.omp/agents/feature-implementer.md +1 -0
  40. package/template_project/.omp/agents/handoff-planner.md +1 -0
  41. package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
  42. package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
  43. package/template_project/ukit/storage/config.json +1 -1
  44. package/scripts/bench/data-foundation.mjs +0 -929
  45. package/scripts/bench/decision-runtime-baseline.mjs +0 -427
  46. package/scripts/bench/decision-runtime-variant.mjs +0 -626
  47. package/scripts/bench/goldTasks.json +0 -38
  48. package/scripts/bench/memory-ablation.mjs +0 -495
  49. package/scripts/bench/memory-baseline.mjs +0 -596
  50. package/scripts/bench/memory-bench.mjs +0 -661
  51. package/scripts/bench/memory-canary.mjs +0 -331
  52. package/scripts/bench/memory-corpus.mjs +0 -354
  53. package/scripts/bench/memory-gate.mjs +0 -389
  54. package/scripts/bench/memory-metrics.mjs +0 -179
  55. package/scripts/bench/outline-savings.mjs +0 -107
  56. package/scripts/bench/parallel-agents.mjs +0 -224
  57. package/scripts/bench/recorder-overhead.mjs +0 -204
  58. package/scripts/bench/runGold.mjs +0 -238
  59. package/scripts/bench/sqlite-spike.mjs +0 -451
  60. package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
  61. package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
  62. package/scripts/bench/v3-ceremony.mjs +0 -220
  63. package/scripts/perf/audit-perf.mjs +0 -1195
  64. package/scripts/perf/diff-perf-findings.mjs +0 -136
  65. package/scripts/perf/perf-findings.json +0 -474
  66. package/scripts/perf/perf-measure.md +0 -271
@@ -0,0 +1,202 @@
1
+ // Tier-lane dispatch: who performs the edit (worker hand-off vs inline) for a routed task, and the
2
+ // tier of the MAIN session. A leaf module (node builtins only) so route-task.mjs and the
3
+ // UserPromptSubmit hook share ONE implementation — the hook cannot import the large route-task CLI.
4
+ import * as fsSync from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ // WS-E tier-role wiring: a contract tier only takes effect when the executing session hands
8
+ // the work to an agent whose definition binds that tier — otherwise every session behaves as
9
+ // if it only had one model. Labels are stable tier roles (lite/code/smart), never provider
10
+ // or model identity. Vision never appears here — it is a capability lane, not a cost tier.
11
+ //
12
+ // `mainTier` is the tier the MAIN session actually runs on (resolveMainTier; null = unknown).
13
+ // - main is not smart (or unknown): legacy behavior — `code` is the silent default lane,
14
+ // lite/smart get a hand-off.
15
+ // - main is smart (UKit's default orchestrator): the smart lane is silent. A code lane is
16
+ // dispatched to its Tier 2 worker only where delegationPolicy admits it; `disallow` lanes
17
+ // (tiny-fix/local-fix) stay inline because the hand-off costs more than the work, and
18
+ // `disallow-by-default` (local-build) delegates only for multi-file / long edit loops.
19
+ export const TIER_WORKER_AGENT = { lite: 'ukit-small-task-maintainer', code: 'feature-implementer' };
20
+
21
+ // @decision-point orchestration.tier-lane-dispatch — deterministic choice of who performs the edit
22
+ // (worker hand-off vs inline) from tier, main tier, delegation policy and risk; regression:
23
+ // tests/orchestration/tierRiskIndependence.test.js, mainTierLane.test.js.
24
+ export function buildTierLane({ modelTier = null, escalatedTier = null, mainTier = null, mainTierSource = null, delegationPolicy = null, executionMode = null, complexityHigh = false, riskHigh = false, playbookId = null, subagentSession = false } = {}) {
25
+ const tier = escalatedTier ?? modelTier;
26
+ if (!tier || tier === 'vision') {
27
+ return null;
28
+ }
29
+ // A host-marked subagent session IS the worker: it executes the brief it was handed. Handing the
30
+ // same brief onward (or requiring a delegation it cannot make) would nest orchestration — omp runs
31
+ // the router in child sessions too (live: child session routed with mainTier smart).
32
+ if (subagentSession) {
33
+ return { tier, mainTier: null, mainTierSource: 'subagent-session', defaultLane: true, executorTier: tier, instruction: null, subagentSession: true };
34
+ }
35
+ // Effort is independent of tier and of risk: Medium unless complexity evidence (verified failure /
36
+ // stuck) says High. The per-task effort can only be bound on a dispatch (Agent tool `effort` / omp
37
+ // task effort); the running session's own effort is never changed mid-task, so an inline lane
38
+ // records the High need as unbound.
39
+ const effort = complexityHigh ? 'high' : null;
40
+ const withEffort = (instruction) => (instruction && effort
41
+ ? `${instruction} Complexity evidence is high: pass effort="high" on that Agent/task call.`
42
+ : instruction);
43
+ const effortFields = effort ? { effort, effortBinding: 'dispatch-param-only' } : {};
44
+ if (mainTier !== 'smart') {
45
+ return {
46
+ tier,
47
+ defaultLane: tier === 'code',
48
+ instruction: tier === 'code'
49
+ ? null
50
+ : withEffort(`Hand this task to an agent bound to the ${tier} tier instead of doing it inline, unless this session already runs on the ${tier} tier.`),
51
+ ...effortFields,
52
+ };
53
+ }
54
+ const policy = delegationPolicy ?? 'allow-qualified-sidecar';
55
+ const brief = 'Give the worker scope, dependencies, acceptance criteria and the verification to run; verify its diff yourself after it returns, then run the project\'s bare test runner yourself (e.g. `npm test`, no pipe/tail/grep/compound command) and read its exit status — that status is the verification evidence.';
56
+ // A high-risk target on a Tier 2 coding lane is still Tier 2 work: the inline-by-policy exemption
57
+ // (hand-off costs more than the work) does not apply, otherwise the Tier 3 orchestrator would be the
58
+ // one editing the dangerous file. Risk raises verification rigor, not the executor.
59
+ if (tier === 'code' && riskHigh && (policy === 'disallow' || policy === 'disallow-by-default')) {
60
+ const worker = executionMode === 'find-cause' ? 'bug-debugger' : TIER_WORKER_AGENT.code;
61
+ return {
62
+ tier, mainTier, mainTierSource, defaultLane: false, executorTier: 'code', delegationRequired: true, ...effortFields,
63
+ instruction: withEffort(`High-risk target: hand the implementation to a code-tier worker (${worker}) instead of editing inline; this session runs on the smart tier. ${brief}`),
64
+ };
65
+ }
66
+ // A routed feature-implementation playbook is the contract-bearing / multi-surface build path, not a
67
+ // small local edit: the inline-by-policy exemption (hand-off costs more than the work) belongs to
68
+ // small-feature, so the Tier 3 orchestrator hands this implementation to the Tier 2 worker.
69
+ if (tier === 'code' && playbookId === 'feature-implementation' && policy === 'disallow-by-default') {
70
+ return {
71
+ tier, mainTier, mainTierSource, defaultLane: false, executorTier: 'code', delegationRequired: true, ...effortFields,
72
+ instruction: withEffort(`Feature-implementation playbook: hand the implementation to a code-tier worker (${TIER_WORKER_AGENT.code}) instead of editing inline; this session runs on the smart tier. ${brief}`),
73
+ };
74
+ }
75
+ // executorTier = the tier that actually performs the edit: the worker's when a hand-off is
76
+ // instructed, the main session's when the lane stays inline (an inline lane under a smart main is
77
+ // Tier 3 work), null when the hand-off is conditional on edit size.
78
+ const base = { tier, mainTier, mainTierSource, defaultLane: tier === 'smart', ...effortFields };
79
+ // Failure evidence escalated a coding lane to smart: the orchestrator takes the analysis, not the edit.
80
+ // Where delegation is admitted it still hands the implementation to the Tier 2 worker with its diagnosis.
81
+ if (tier === 'smart' && modelTier === 'code' && policy !== 'disallow' && policy !== 'disallow-by-default') {
82
+ const worker = executionMode === 'find-cause' ? 'bug-debugger' : TIER_WORKER_AGENT.code;
83
+ return {
84
+ ...base,
85
+ defaultLane: false,
86
+ executorTier: 'code',
87
+ delegationRequired: true,
88
+ instruction: withEffort(`Repeated failure evidence escalated this task: do the root-cause analysis yourself, then hand the implementation to a code-tier worker (${worker}) with your diagnosis instead of editing inline. ${brief}`),
89
+ };
90
+ }
91
+ if (tier === 'smart' || policy === 'disallow') {
92
+ return { ...base, executorTier: mainTier, instruction: null };
93
+ }
94
+ const worker = executionMode === 'find-cause' && tier === 'code' ? 'bug-debugger' : TIER_WORKER_AGENT[tier];
95
+ return {
96
+ ...base,
97
+ executorTier: policy === 'disallow-by-default' ? null : tier,
98
+ ...(policy === 'disallow-by-default' ? {} : { delegationRequired: true }),
99
+ instruction: withEffort(policy === 'disallow-by-default'
100
+ ? `Work inline unless the change spans several files or needs a long edit loop; then hand it to a ${tier}-tier worker (${worker}). ${brief}`
101
+ : `Hand the implementation to a ${tier}-tier worker (${worker}) instead of doing it inline; this session runs on the smart tier. ${brief}`),
102
+ };
103
+ }
104
+
105
+ const MODEL_TIER_ALIASES = [
106
+ [/opus|unic-smart|^slow$|^smart$/, 'smart'],
107
+ [/sonnet|unic-code|^default$|^code$/, 'code'],
108
+ [/haiku|unic-lite|^smol$|^lite$/, 'lite'],
109
+ ];
110
+
111
+ /** Maps a model alias/id/role to lite|code|smart (config modelTiers first, then name families). */
112
+ export function classifyModelTier(model, config = null) {
113
+ const value = typeof model === 'string' ? model.trim().toLowerCase().replace(/\[[^\]]*\]$/, '').replace(/:(off|minimal|low|medium|high|xhigh|max|auto)$/, '') : '';
114
+ if (!value) {
115
+ return null;
116
+ }
117
+ const tiers = config?.orchestration?.modelTiers;
118
+ for (const name of ['lite', 'code', 'smart']) {
119
+ const entry = tiers?.[name];
120
+ if (entry && [entry.claudeModel, entry.genericModel, entry.ompRole].some((v) => typeof v === 'string' && v.toLowerCase() === value)) {
121
+ return name;
122
+ }
123
+ }
124
+ const bare = value.includes('/') ? value.slice(value.lastIndexOf('/') + 1) : value;
125
+ return MODEL_TIER_ALIASES.find(([re]) => re.test(bare))?.[1] ?? null;
126
+ }
127
+
128
+ function readJsonFileSync(file) {
129
+ try {
130
+ return JSON.parse(fsSync.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Tier of the MAIN session, with provenance: {tier, source}. Host-selected model first
138
+ * (Claude Code: ANTHROPIC_MODEL > settings.local.json > settings.json `model`; OMP: the model the
139
+ * bridge recorded at session_start), then orchestration.orchestratorModel. Unknown → {tier:null}
140
+ * so buildTierLane keeps the legacy behavior. Never infers a model from anything else.
141
+ */
142
+ export function resolveMainTier({ rootDir = null, config = null, env = process.env, sessionId = null } = {}) {
143
+ const candidates = [];
144
+ if (env?.UKIT_HARNESS === 'codex') {
145
+ return { tier: null, source: 'unknown' }; // codex cannot bind a model; never guess its main tier
146
+ }
147
+ if (env?.UKIT_HARNESS === 'omp') {
148
+ const recorded = rootDir ? readJsonFileSync(path.join(rootDir, '.ukit', 'storage', 'cache', 'session-main-model.json')) : null;
149
+ // STRICT ownership: the record is a single per-project slot describing ONE main session
150
+ // (the omp bridge stamps the writer's host session id). Without the caller's host session
151
+ // id — or with a record that is unstamped (a pre-provenance/legacy write) or owned by a
152
+ // DIFFERENT session — the OMP main tier is UNKNOWN: never read a foreign slot, and never
153
+ // fall through to the Claude orchestrator default on another session's data. The omp bridge
154
+ // refreshes the owner's record on session_start/session_switch/before_agent_start
155
+ // (observed-active-model-changed), so a matched owner sees current state, not a stale role.
156
+ const owned = recorded?.harness === 'omp'
157
+ && typeof sessionId === 'string' && sessionId !== ''
158
+ && typeof recorded.session === 'string' && recorded.session !== ''
159
+ && recorded.session === sessionId;
160
+ // OMP result owns the answer end-to-end: never fall through to the Claude
161
+ // orchestrator default on top of (or instead of) this harness's own record.
162
+ return { tier: classifyModelTier(owned ? (recorded.role ?? recorded.model) : null, config), source: owned ? 'omp-session' : 'unknown' };
163
+ } else {
164
+ candidates.push(['env:ANTHROPIC_MODEL', env?.ANTHROPIC_MODEL]);
165
+ for (const file of ['settings.local.json', 'settings.json']) {
166
+ candidates.push([`.claude/${file}`, rootDir ? readJsonFileSync(path.join(rootDir, '.claude', file))?.model : null]);
167
+ }
168
+ }
169
+ candidates.push(['orchestration.orchestratorModel', config?.orchestration?.orchestratorModel]);
170
+ for (const [source, model] of candidates) {
171
+ const tier = classifyModelTier(model, config);
172
+ if (tier) {
173
+ return { tier, source };
174
+ }
175
+ }
176
+ return { tier: null, source: 'unknown' };
177
+ }
178
+
179
+ /**
180
+ * Attaches `tierLane` to a route summary from the final route fields. Shared by the route CLI and the
181
+ * UserPromptSubmit hook so both persist the same delegation requirement; the Stop-side playbook
182
+ * acceptance reads `tierLane.delegationRequired` from that persisted state.
183
+ */
184
+ export function applyTierLaneToRouteSummary(routeSummary, { rootDir = null, config = null, env = process.env, complexityHigh = false, subagentSession = false, sessionId = null } = {}) {
185
+ if (!routeSummary || typeof routeSummary !== 'object') {
186
+ return routeSummary;
187
+ }
188
+ const mainTier = resolveMainTier({ rootDir, config, env, sessionId });
189
+ routeSummary.tierLane = buildTierLane({
190
+ modelTier: routeSummary.executionContract?.modelTier ?? routeSummary.execution?.modelTier ?? null,
191
+ escalatedTier: routeSummary.escalatedTier ?? null,
192
+ mainTier: mainTier.tier,
193
+ mainTierSource: mainTier.source,
194
+ delegationPolicy: routeSummary.executionContract?.delegationPolicy ?? null,
195
+ executionMode: routeSummary.executionMode ?? null,
196
+ complexityHigh,
197
+ riskHigh: routeSummary.riskFloor?.floor === 'high-risk',
198
+ playbookId: routeSummary.playbookId ?? null,
199
+ subagentSession: subagentSession === true,
200
+ });
201
+ return routeSummary;
202
+ }
@@ -82,7 +82,9 @@ export function bashOperationReceipt(rawCommand, response, transportSuccess, goa
82
82
  const hasOutputField = typeof response === 'string' || typeof response?.stdout === 'string' || typeof response?.output === 'string';
83
83
  let role = null; let proven = false; let target = null; let remoteSync = null;
84
84
  for (const seg of real) {
85
- if (!seg || /^cd\s+\S+$/.test(seg) || DRY_RUN.test(seg.replace(/'[^']*'|"[^"]*"/g, '""'))) continue;
85
+ // `git diff --check` is a real verification, not a dry-run mutation.
86
+ if (!seg || /^cd\s+\S+$/.test(seg) || (DRY_RUN.test(seg.replace(/'[^']*'|"[^"]*"/g, '""'))
87
+ && !(domain === 'git-integrity' && /^git\s+diff\s+--check(?:\s|$)/.test(seg)))) continue;
86
88
  if (domain === 'git-integrity') {
87
89
  const lsRemote = seg.match(GIT_LS_REMOTE);
88
90
  if (lsRemote) {