@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.
- package/CHANGELOG.md +13 -5
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/capture-omp-stall.mjs +246 -0
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/scripts/docs/render-instructions.mjs +29 -5
- package/src/core/agentRuntime/supervisor.js +15 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +67 -16
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/diagnostics/ompStallSnapshot.js +199 -0
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +147 -6
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +80 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- 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
|
-
|
|
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) {
|