@ngockhoale/ukit 3.4.47 → 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 +8 -0
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/worktree-preflight.mjs +20 -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/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 +79 -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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.48 - 2026-10-19
|
|
6
|
+
|
|
7
|
+
**Completion evidence closure and reproducible multi-host installation.**
|
|
8
|
+
- Recognize standalone `git diff --check` as native Git verification rather than a dry-run mutation. Failed/masked mutations, piped verification, foreign-task evidence and release remote/tag divergence still block.
|
|
9
|
+
- Preserve task ownership through host-consumed coordination notices and continuations; bound Stop diagnostics without accepting incomplete work. Explicit no-publication lists and quoted/report-only references do not authorize a release.
|
|
10
|
+
- Integrate verified OMP child-session/model-ownership safeguards, delegation runtime, installer defaults migration, runtime package dependencies and reproducible scaffolds.
|
|
11
|
+
- Verified locally on macOS/Node 22. Claude/Codex/OMP asset installation is covered; OMP Full Playbook live correctness and trusted worker/dispatch attribution remain UNVERIFIED. No paid OMP smoke or cross-platform live PASS is claimed.
|
|
12
|
+
|
|
5
13
|
## 3.4.47 - 2026-10-18
|
|
6
14
|
|
|
7
15
|
**Supervisor subagent usage attribution and rendered-instruction parity checks.**
|
|
@@ -517,6 +517,28 @@ entries:
|
|
|
517
517
|
validation: [none]
|
|
518
518
|
archive_policy: immutable
|
|
519
519
|
notes: 2026-10-10 orchestration regression test inventory
|
|
520
|
+
- id: docs-analysis-completion-task-ownership
|
|
521
|
+
path: docs/analysis/COMPLETION-TASK-OWNERSHIP.md
|
|
522
|
+
class: archive
|
|
523
|
+
audience: [maintainer]
|
|
524
|
+
owner: product
|
|
525
|
+
source_of_truth: docs/analysis/COMPLETION-TASK-OWNERSHIP.md
|
|
526
|
+
merge_strategy: none
|
|
527
|
+
load_policy: never
|
|
528
|
+
validation: [none]
|
|
529
|
+
archive_policy: immutable
|
|
530
|
+
notes: 2026-10-19 task boundary reproduction and bounded Stop verification
|
|
531
|
+
- id: docs-analysis-omp-runtime-closure
|
|
532
|
+
path: docs/analysis/OMP-RUNTIME-CLOSURE.md
|
|
533
|
+
class: archive
|
|
534
|
+
audience: [maintainer]
|
|
535
|
+
owner: product
|
|
536
|
+
source_of_truth: docs/analysis/OMP-RUNTIME-CLOSURE.md
|
|
537
|
+
merge_strategy: none
|
|
538
|
+
load_policy: never
|
|
539
|
+
validation: [none]
|
|
540
|
+
archive_policy: immutable
|
|
541
|
+
notes: 2026-10-19 captured OMP lifecycle closure and offline verification boundaries
|
|
520
542
|
- id: docs-analysis-orchestration-final-verification
|
|
521
543
|
path: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
|
|
522
544
|
class: archive
|
|
@@ -1222,6 +1222,7 @@ items:
|
|
|
1222
1222
|
- hook-auto-allow-bash
|
|
1223
1223
|
- hook-block-dangerous
|
|
1224
1224
|
- hook-handoff-model-guard
|
|
1225
|
+
- hook-delegation-gate-module
|
|
1225
1226
|
- hook-auto-prune-bash
|
|
1226
1227
|
- hook-project-important
|
|
1227
1228
|
# Env block is post-merged by applyGatewayResilienceEnv (TASK-011) to add managed
|
|
@@ -1422,6 +1423,20 @@ items:
|
|
|
1422
1423
|
packs:
|
|
1423
1424
|
- core
|
|
1424
1425
|
|
|
1426
|
+
# P0 delegation enforcement: in-proc PreToolUse chain step (Edit|Write, MultiEdit, Bash). Claude Code
|
|
1427
|
+
# only (agent_id main/worker identity); reads route state + ledger from ukit-runtime-scripts.
|
|
1428
|
+
- id: hook-delegation-gate-module
|
|
1429
|
+
type: hook
|
|
1430
|
+
sourceTemplate: .claude/hooks/delegation-gate.mjs
|
|
1431
|
+
targetPath: .claude/hooks/delegation-gate.mjs
|
|
1432
|
+
requires:
|
|
1433
|
+
- ukit-runtime-scripts
|
|
1434
|
+
mergeStrategy: overwrite_with_backup
|
|
1435
|
+
variables: []
|
|
1436
|
+
enabledByDefault: true
|
|
1437
|
+
packs:
|
|
1438
|
+
- core
|
|
1439
|
+
|
|
1425
1440
|
- id: hook-handoff-model-guard
|
|
1426
1441
|
type: hook
|
|
1427
1442
|
sourceTemplate: .claude/hooks/handoff-model-guard.sh
|
|
@@ -1442,6 +1457,7 @@ items:
|
|
|
1442
1457
|
- ukit-runtime-scripts
|
|
1443
1458
|
- ukit-index-route-resolver-script
|
|
1444
1459
|
- ukit-index-tier-selection-script
|
|
1460
|
+
- ukit-index-tier-lane-script
|
|
1445
1461
|
mergeStrategy: overwrite_with_backup
|
|
1446
1462
|
variables: []
|
|
1447
1463
|
enabledByDefault: true
|
|
@@ -1865,6 +1881,16 @@ items:
|
|
|
1865
1881
|
packs:
|
|
1866
1882
|
- core
|
|
1867
1883
|
|
|
1884
|
+
- id: ukit-index-tier-lane-script
|
|
1885
|
+
type: config
|
|
1886
|
+
sourceTemplate: .claude/ukit/index/tier-lane.mjs
|
|
1887
|
+
targetPath: .claude/ukit/index/tier-lane.mjs
|
|
1888
|
+
mergeStrategy: overwrite_with_backup
|
|
1889
|
+
variables: []
|
|
1890
|
+
enabledByDefault: true
|
|
1891
|
+
packs:
|
|
1892
|
+
- core
|
|
1893
|
+
|
|
1868
1894
|
- id: ukit-index-session-history-extractor-script
|
|
1869
1895
|
type: config
|
|
1870
1896
|
sourceTemplate: .claude/ukit/index/session-history-extractor.mjs
|
|
@@ -2012,6 +2038,7 @@ items:
|
|
|
2012
2038
|
- ukit-index-session-history-extractor-script
|
|
2013
2039
|
- ukit-index-fix-loop-escalation-script
|
|
2014
2040
|
- ukit-index-tier-selection-script
|
|
2041
|
+
- ukit-index-tier-lane-script
|
|
2015
2042
|
mergeStrategy: overwrite_with_backup
|
|
2016
2043
|
variables: []
|
|
2017
2044
|
enabledByDefault: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngockhoale/ukit",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.48",
|
|
4
4
|
"description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"!scripts/audit/c100/usage-provenance.mjs",
|
|
37
37
|
"!scripts/audit/decision-coverage.mjs",
|
|
38
38
|
"!scripts/audit/subagent-cost/**",
|
|
39
|
+
"!scripts/bench/**",
|
|
40
|
+
"scripts/bench/decision-runtime-metrics.mjs",
|
|
41
|
+
"!scripts/perf/**",
|
|
39
42
|
"template_project/",
|
|
40
43
|
"template_user/",
|
|
41
44
|
"!template_project/**/.smbdelete*",
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// - already bootstrapped → no-op
|
|
8
8
|
// - missing prerequisites → run `ukit install` once, re-check
|
|
9
9
|
// - not a linked worktree → refuse (the primary checkout's .claude/ is the maintainer runtime)
|
|
10
|
+
// Install can also create template-owned scaffolds missing from the checkout (docs/AI_HANDOFF/PLAN.md, SPEC.md);
|
|
11
|
+
// those are not gitignored, so they are reported as `visibleWrites` + a warning, never hidden or deleted.
|
|
10
12
|
// `ukit install` writes the install flavour of root CLAUDE.md/AGENTS.md, but those files are
|
|
11
13
|
// gitignored *render outputs* of template_project/instructions (docs:render). The preflight
|
|
12
14
|
// re-renders them when they drift — never a tracked render target, which is only reported.
|
|
@@ -57,6 +59,15 @@ function isTracked(root, rel) {
|
|
|
57
59
|
|
|
58
60
|
const RENDER_CLI = 'scripts/docs/render-instructions.mjs';
|
|
59
61
|
|
|
62
|
+
// Visible (non-ignored) paths in `git status`. `ukit install` creates template-owned scaffolds that
|
|
63
|
+
// are missing from the checkout (docs/AI_HANDOFF/PLAN.md, SPEC.md — mergeStrategy skip, handoff-clear
|
|
64
|
+
// restores them to template, never deletes them). They are not gitignored, so a HEAD that lost them
|
|
65
|
+
// turns the bootstrap into a visible write. Report it truthfully instead of hiding or deleting it.
|
|
66
|
+
function visibleStatus(root) {
|
|
67
|
+
const res = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: root, encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
|
|
68
|
+
return res.status === 0 ? new Set(res.stdout.split('\n').filter(Boolean)) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
// → rel paths whose rendered bytes differ from disk (`DRIFT <target>` lines of --check).
|
|
61
72
|
export function renderDrift(root) {
|
|
62
73
|
const res = spawnSync(process.execPath, [path.join(root, RENDER_CLI), '--check'], { cwd: root, encoding: 'utf8', timeout: RENDER_TIMEOUT_MS });
|
|
@@ -71,7 +82,7 @@ export function missingPrerequisites(root) {
|
|
|
71
82
|
|
|
72
83
|
export function preflight({ root, check = false } = {}) {
|
|
73
84
|
const projectRoot = fs.realpathSync(root);
|
|
74
|
-
const result = { root: projectRoot, ok: false, bootstrapped: false, missing: [], rendered: [], warnings: [], home: null, error: null };
|
|
85
|
+
const result = { root: projectRoot, ok: false, bootstrapped: false, missing: [], rendered: [], visibleWrites: [], warnings: [], home: null, error: null };
|
|
75
86
|
|
|
76
87
|
if (!fs.existsSync(path.join(projectRoot, 'bin/ukit'))) {
|
|
77
88
|
return { ...result, exitCode: 2, error: `not a UKit checkout (no bin/ukit): ${projectRoot}` };
|
|
@@ -91,6 +102,7 @@ export function preflight({ root, check = false } = {}) {
|
|
|
91
102
|
// worktree's ignored .cache/ so a preflight never mutates the real home or leaks from it.
|
|
92
103
|
result.home = path.join(projectRoot, '.cache/worktree-preflight-home');
|
|
93
104
|
fs.mkdirSync(result.home, { recursive: true });
|
|
105
|
+
const visibleBefore = visibleStatus(projectRoot);
|
|
94
106
|
const install = spawnSync(process.execPath, [path.join(projectRoot, 'bin/ukit'), 'install'], {
|
|
95
107
|
cwd: projectRoot,
|
|
96
108
|
encoding: 'utf8',
|
|
@@ -98,6 +110,13 @@ export function preflight({ root, check = false } = {}) {
|
|
|
98
110
|
timeout: 120_000,
|
|
99
111
|
});
|
|
100
112
|
result.bootstrapped = true;
|
|
113
|
+
const visibleAfter = visibleStatus(projectRoot);
|
|
114
|
+
if (visibleBefore && visibleAfter) {
|
|
115
|
+
result.visibleWrites = [...visibleAfter].filter((l) => !visibleBefore.has(l)).map((l) => l.slice(3));
|
|
116
|
+
if (result.visibleWrites.length > 0) {
|
|
117
|
+
result.warnings.push(`ukit install created non-ignored file(s) missing from this checkout: ${result.visibleWrites.join(', ')} — template-owned scaffolds; restore/commit them (not deleted)`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
101
120
|
result.missing = missingPrerequisites(projectRoot);
|
|
102
121
|
if (install.status !== 0 || result.missing.length > 0) {
|
|
103
122
|
const tail = `${install.stdout || ''}${install.stderr || ''}`.trim().split('\n').slice(-5).join('\n');
|
package/src/core/diffPlan.js
CHANGED
|
@@ -62,6 +62,15 @@ async function checkLinkStatus(targetPath, linkTarget) {
|
|
|
62
62
|
// Template-filled: keys the user does not have take the template value.
|
|
63
63
|
// Gateway-resilience env keys are post-apply managed (applyGatewayResilienceEnv) and are
|
|
64
64
|
// ignored by the drift comparison.
|
|
65
|
+
//
|
|
66
|
+
// One-time defaults migration (orchestrator tier + effort): UKit used to ship `model: "sonnet"`
|
|
67
|
+
// and `effortLevel: "high"`; it now ships `opus` / `medium` (main orchestrator = Tier 3, effort
|
|
68
|
+
// Medium unless the task is high-risk). A key is migrated ONLY when the file is UKit-tracked, the
|
|
69
|
+
// user's value still equals the old shipped default, and the migration has not run before
|
|
70
|
+
// (install.json `settingsDefaultsMigration`) — so a later deliberate choice is never touched
|
|
71
|
+
// again, and settings.local.json / user settings (never edited by UKit) always win at runtime.
|
|
72
|
+
export const SETTINGS_DEFAULTS_MIGRATION_ID = 'orchestrator-opus-medium-v1';
|
|
73
|
+
const LEGACY_SHIPPED_SETTINGS = { model: 'sonnet', effortLevel: 'high' };
|
|
65
74
|
const UKIT_OWNED_TOP_LEVEL_KEYS = new Set(['hooks']);
|
|
66
75
|
const PERMISSION_LIST_KEYS = new Set(['allow', 'deny', 'ask', 'additionalDirectories']);
|
|
67
76
|
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
@@ -94,7 +103,22 @@ function unionList(templateList, userList) {
|
|
|
94
103
|
return out;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
// Keys whose old shipped default is still in place and should take the new template value.
|
|
107
|
+
function legacyDefaultKeys(existing, rendered, migrate) {
|
|
108
|
+
if (!migrate) return [];
|
|
109
|
+
return Object.keys(LEGACY_SHIPPED_SETTINGS).filter((key) => (
|
|
110
|
+
existing[key] === LEGACY_SHIPPED_SETTINGS[key] && key in rendered && rendered[key] !== existing[key]
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function settingsDefaultsMigrationKeys(existingContent, renderedContent) {
|
|
115
|
+
const existing = parseSettingsJson(existingContent);
|
|
116
|
+
const rendered = parseSettingsJson(renderedContent);
|
|
117
|
+
return existing && rendered ? legacyDefaultKeys(existing, rendered, true) : [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mergeSettingsObjects(existing, rendered, { migrateDefaults = false } = {}) {
|
|
121
|
+
const migrated = new Set(legacyDefaultKeys(existing, rendered, migrateDefaults));
|
|
98
122
|
const merged = {};
|
|
99
123
|
for (const key of Object.keys(rendered)) {
|
|
100
124
|
if (BLOCKED_KEYS.has(key)) continue;
|
|
@@ -124,7 +148,7 @@ function mergeSettingsObjects(existing, rendered) {
|
|
|
124
148
|
// user value has the wrong shape (e.g. permissions: "x") — template shape wins
|
|
125
149
|
merged[key] = templateValue;
|
|
126
150
|
} else {
|
|
127
|
-
merged[key] = userValue; // user-owned scalar/section (model, effortLevel, …)
|
|
151
|
+
merged[key] = migrated.has(key) ? templateValue : userValue; // user-owned scalar/section (model, effortLevel, …)
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
154
|
return merged;
|
|
@@ -133,11 +157,11 @@ function mergeSettingsObjects(existing, rendered) {
|
|
|
133
157
|
// Merge rendered settings.json with the user's existing file per the ownership model above.
|
|
134
158
|
// Returns the merged JSON as a string, or the rendered content unchanged when either side is
|
|
135
159
|
// unparseable (the caller still takes a timestamped backup before writing).
|
|
136
|
-
export function mergeSettingsJson(existingContent, renderedContent) {
|
|
160
|
+
export function mergeSettingsJson(existingContent, renderedContent, options = {}) {
|
|
137
161
|
const existing = parseSettingsJson(existingContent);
|
|
138
162
|
const rendered = parseSettingsJson(renderedContent);
|
|
139
163
|
if (!existing || !rendered) return renderedContent;
|
|
140
|
-
return `${JSON.stringify(mergeSettingsObjects(existing, rendered), null, 2)}\n`;
|
|
164
|
+
return `${JSON.stringify(mergeSettingsObjects(existing, rendered, options), null, 2)}\n`;
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
function withoutGatewayEnv(settings) {
|
|
@@ -147,11 +171,11 @@ function withoutGatewayEnv(settings) {
|
|
|
147
171
|
return { ...settings, env };
|
|
148
172
|
}
|
|
149
173
|
|
|
150
|
-
function settingsJsonIgnoresGatewayEnvDiff(existingContent, renderedContent) {
|
|
174
|
+
function settingsJsonIgnoresGatewayEnvDiff(existingContent, renderedContent, options = {}) {
|
|
151
175
|
const existing = parseSettingsJson(existingContent);
|
|
152
176
|
const rendered = parseSettingsJson(renderedContent);
|
|
153
177
|
if (!existing || !rendered) return false;
|
|
154
|
-
const merged = mergeSettingsObjects(existing, rendered);
|
|
178
|
+
const merged = mergeSettingsObjects(existing, rendered, options);
|
|
155
179
|
return JSON.stringify(canonicalize(withoutGatewayEnv(merged)))
|
|
156
180
|
=== JSON.stringify(canonicalize(withoutGatewayEnv(existing)));
|
|
157
181
|
}
|
|
@@ -208,7 +232,7 @@ function ukitInstalledText(mergedText, renderedText) {
|
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
|
|
211
|
-
function resolveFileAction(entry, existingContent, ompOwnership = null) {
|
|
235
|
+
function resolveFileAction(entry, existingContent, ompOwnership = null, settingsMigration = null) {
|
|
212
236
|
const exists = existingContent !== null;
|
|
213
237
|
const binaryEntry = Buffer.isBuffer(entry.renderedContent);
|
|
214
238
|
|
|
@@ -273,9 +297,11 @@ function resolveFileAction(entry, existingContent, ompOwnership = null) {
|
|
|
273
297
|
// User-owned permission keys (permissions.defaultMode) are excluded from the drift
|
|
274
298
|
// check AND merged back into the content that gets written, so install/update can
|
|
275
299
|
// never flip the user's chosen permission mode.
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
300
|
+
const mergeOptions = { migrateDefaults: settingsMigration?.pending === true };
|
|
301
|
+
const merged = mergeSettingsJson(existingContent, entry.renderedContent, mergeOptions);
|
|
302
|
+
action = settingsJsonIgnoresGatewayEnvDiff(existingContent, entry.renderedContent, mergeOptions) ? 'unchanged' : 'update';
|
|
303
|
+
const migratedKeys = mergeOptions.migrateDefaults ? settingsDefaultsMigrationKeys(existingContent, entry.renderedContent) : [];
|
|
304
|
+
return { ...entry, renderedContent: merged, exists, action, existingContent, ...(migratedKeys.length ? { settingsDefaultsMigrated: migratedKeys } : {}) };
|
|
279
305
|
} else if (entry.id === 'runtime-config' && typeof existingContent === 'string' && typeof entry.renderedContent === 'string') {
|
|
280
306
|
// U2: .ukit/storage/config.json is user-owned state seeded from the template — merge
|
|
281
307
|
// by ownership instead of overwriting (see runtimeConfigMerge.js).
|
|
@@ -332,7 +358,7 @@ async function resolveSkipEntryAction(entry, ompOwnership = null) {
|
|
|
332
358
|
return resolveFileAction(entry, read.content, ompOwnership);
|
|
333
359
|
}
|
|
334
360
|
|
|
335
|
-
export async function diffInstallPlan(plan, { ompOwnership = null } = {}) {
|
|
361
|
+
export async function diffInstallPlan(plan, { ompOwnership = null, settingsMigration = null } = {}) {
|
|
336
362
|
// Check all entries in parallel
|
|
337
363
|
const results = await Promise.all(
|
|
338
364
|
plan.entries.map(async (entry) => {
|
|
@@ -352,7 +378,7 @@ export async function diffInstallPlan(plan, { ompOwnership = null } = {}) {
|
|
|
352
378
|
if (read.status === 'unreadable') {
|
|
353
379
|
return conflictResult(entry, read.error);
|
|
354
380
|
}
|
|
355
|
-
return resolveFileAction(entry, read.status === 'ok' ? read.content : null, ompOwnership);
|
|
381
|
+
return resolveFileAction(entry, read.status === 'ok' ? read.content : null, ompOwnership, settingsMigration);
|
|
356
382
|
}),
|
|
357
383
|
);
|
|
358
384
|
|
|
@@ -95,37 +95,39 @@ export const MODEL_TIER_BY_CONTRACT = {
|
|
|
95
95
|
// no registry, no leases, no outbound capability calls.
|
|
96
96
|
export const ROUTE_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
97
97
|
|
|
98
|
-
// Tier
|
|
99
|
-
|
|
98
|
+
// Tier, effort and risk are independent decisions. Effort is Medium for every tier (Low for chores);
|
|
99
|
+
// High needs complexity evidence (`complexityEvidence` below), never risk or tier alone.
|
|
100
100
|
const MODEL_TIER_EFFORT = {
|
|
101
101
|
lite: 'low',
|
|
102
102
|
code: 'medium',
|
|
103
|
-
smart: '
|
|
103
|
+
smart: 'medium',
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
107
|
+
* Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor lifts a Tier 1 chore to the
|
|
108
|
+
* Tier 2 executor (lite→code) and goes no further: smart is the orchestrator / acceptance tier and is
|
|
109
|
+
* reached only through its own contract (review-release) or failure evidence, never through risk, so
|
|
110
|
+
* risk cannot turn the orchestrator into the implementation worker. Risk raises verification rigor
|
|
111
|
+
* (the route's riskFloor / rigor), not the tier ceiling or the effort. Effort is 'high' only with
|
|
112
|
+
* `complexityEvidence`. Unknown mode → {tier:null, effort:null, advisoryOnly:true}. Missing/unknown
|
|
113
|
+
* riskFloor → 'none'. Missing hostCapabilities → advisoryOnly:true (the route text is all the host
|
|
114
|
+
* gets when it cannot bind model and effort).
|
|
112
115
|
*/
|
|
113
116
|
export function resolveModelTier({
|
|
114
117
|
executionMode = null,
|
|
115
118
|
riskFloor = null,
|
|
116
119
|
hostCapabilities = null,
|
|
120
|
+
complexityEvidence = false,
|
|
117
121
|
} = {}) {
|
|
118
122
|
const baseTier = MODEL_TIER_BY_CONTRACT[executionMode] ?? null;
|
|
119
123
|
if (!baseTier) {
|
|
120
124
|
return { tier: null, effort: null, advisoryOnly: true };
|
|
121
125
|
}
|
|
122
126
|
const highRisk = riskFloor?.floor === 'high-risk';
|
|
123
|
-
const tier = highRisk
|
|
124
|
-
? MODEL_TIER_ORDER[Math.min(MODEL_TIER_ORDER.indexOf(baseTier) + 1, MODEL_TIER_ORDER.length - 1)]
|
|
125
|
-
: baseTier;
|
|
127
|
+
const tier = highRisk && baseTier === 'lite' ? 'code' : baseTier;
|
|
126
128
|
return {
|
|
127
129
|
tier,
|
|
128
|
-
effort:
|
|
130
|
+
effort: complexityEvidence === true ? 'high' : MODEL_TIER_EFFORT[tier],
|
|
129
131
|
advisoryOnly: !(hostCapabilities?.canBindModel === true && hostCapabilities?.canBindEffort === true),
|
|
130
132
|
};
|
|
131
133
|
}
|
|
@@ -204,12 +206,55 @@ function neutralizeNonOrders(text) {
|
|
|
204
206
|
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
205
207
|
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
206
208
|
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
207
|
-
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
209
|
+
.replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
|
|
208
210
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
209
211
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
210
212
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
// Explicit scope declaration (intent ownership): when the request itself declares it is read-only
|
|
216
|
+
// ("READ-ONLY audit", "chi doc", "make no source changes") and carries no explicit order to change
|
|
217
|
+
// anything, verbs and operation names inside it are descriptions (a "git stash" that already ran, "a
|
|
218
|
+
// worklog edit", "confirmed fixes"), not orders - so they must not demand write or operation proof.
|
|
219
|
+
// An explicit order (imperative-initial or modal request) keeps the request MIXED/IMPLEMENTATION:
|
|
220
|
+
// the declaration never overrides a real instruction, and fenced/quoted/blockquoted text is never one.
|
|
221
|
+
// A bare verb with no object is the tail of a negated list ("no commit, release, publish"), not an order.
|
|
222
|
+
const READ_ONLY_DECLARATION = /\bread[\s-]*only\s+(?:audit|investigation|review|analysis|inspection|task|request|session|pass|check|diagnos\w+|verification|forensics?)\b|(?:^|\n)[\s*#>_-]*read[\s-]*only\s*[.:!—-]|\b(?:audit|investigation|review|analysis)[\s-]*only\b|[(,]\s*read[\s-]*only\s*[).]|\bchi\s+(?:doc|audit|kiem tra|phan tich|dieu tra)\b|\b(?:make|with)\s+no\s+(?:source\s+|code\s+|file\s+)?(?:changes|edits|modifications)\b/;
|
|
223
|
+
const ORDER_VERBS = 'implement|fix|apply|refactor|modify|edit|write|update|add|create|build|install|deploy|redeploy|restart|push|commit|rename|remove|delete|replace|change|sua|viet|chinh|them|tao|xoa|enable|disable|set|toggle|configure|migrate|upgrade|publish|release|trien khai|khoi dong lai|(?:run|execute)\\s+(?:git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)|(?:npm|yarn)\\s+publish)|git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)';
|
|
224
|
+
const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
|
|
225
|
+
const MODAL_ORDER = new RegExp(`\\b(?:please|hay|vui long|need to|needs to|must|should|want(?:\\s+you)? to|toi muon|can phai|yeu cau)\\s+(?:[\\w-]+\\s+){0,2}(?:${ORDER_VERBS})\\b`);
|
|
226
|
+
function hasExplicitOrder(clauses) {
|
|
227
|
+
return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
|
|
228
|
+
}
|
|
229
|
+
function declaredReadOnly(promptText, releasePolicy) {
|
|
230
|
+
if (releasePolicy) return false;
|
|
231
|
+
const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
|
|
232
|
+
.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
233
|
+
if (!READ_ONLY_DECLARATION.test(plain)) return false;
|
|
234
|
+
const text = neutralizeNonOrders(plain);
|
|
235
|
+
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
|
|
236
|
+
.map((clause) => clause.split(/\bwithout\b/)[0])
|
|
237
|
+
.filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause));
|
|
238
|
+
return /\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|investigation|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text)
|
|
239
|
+
&& !hasExplicitOrder(clauses);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
|
|
243
|
+
// confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
|
|
244
|
+
// "template/install", "source change") as mutation orders. Order is structural: a request whose
|
|
245
|
+
// command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
|
|
246
|
+
// change-leading clause, orders no change - it owes verification evidence, not a write.
|
|
247
|
+
const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
|
|
248
|
+
const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
|
|
249
|
+
export function isVerificationOnlyRequest(promptText) {
|
|
250
|
+
const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
|
|
251
|
+
.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
252
|
+
const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
|
|
253
|
+
.map((clause) => clause.split(/\bwithout\b/)[0]);
|
|
254
|
+
return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
|
|
255
|
+
&& !clauses.some((clause) => CHANGE_LEAD.test(clause));
|
|
256
|
+
}
|
|
257
|
+
|
|
213
258
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
214
259
|
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
215
260
|
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
@@ -221,6 +266,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
221
266
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
222
267
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
223
268
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
269
|
+
if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
|
|
224
270
|
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
225
271
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
226
272
|
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
@@ -238,10 +284,15 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
238
284
|
// runtime are separate domains with separate proof; a prompt naming both stays a coding
|
|
239
285
|
// task rather than being half-verified. A code-change verb anywhere keeps write evidence.
|
|
240
286
|
const gitOperation = actionable.some((clause) => /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
|
|
241
|
-
|
|
287
|
+
// 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
|
|
288
|
+
// when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
|
|
289
|
+
// "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
|
|
290
|
+
const runtimeOperation = actionable.some((clause) => /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
242
291
|
&& !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
|
|
243
292
|
const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
|
|
244
293
|
const operation = Boolean(nativeDomain);
|
|
294
|
+
// A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
|
|
295
|
+
if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
|
|
245
296
|
// A config MENTION is not a config change: the same clause needs a mutation verb, and a
|
|
246
297
|
// request that also changes source/code is a coding task (config is part of its tests/fixtures).
|
|
247
298
|
const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
|
|
@@ -252,7 +303,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
252
303
|
const mutationWord = /\b(?:implement|fix|refactor|modify|edit|write|update|add|create|build|make|install|deploy|remove|delete|rename|replace|change|sua|viet|chinh|them|tao|xoa|doi|cai|cap nhat)\b/.test(text);
|
|
253
304
|
const statusReport = !mutationWord && !operation && !config
|
|
254
305
|
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text);
|
|
255
|
-
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text) || statusReport)
|
|
306
|
+
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text.replace(/\bread[\s-]*only\b/g, ' ')) || statusReport)
|
|
256
307
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
257
308
|
const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
|
|
258
309
|
: config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
@@ -271,5 +322,5 @@ export function applyTaskSemantics(contract, semantic) {
|
|
|
271
322
|
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
272
323
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
273
324
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
274
|
-
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
325
|
+
: semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
|
|
275
326
|
}
|
package/src/core/metadata.js
CHANGED
|
@@ -69,6 +69,7 @@ export async function writeInstallMetadata({
|
|
|
69
69
|
managedRelativePaths = null,
|
|
70
70
|
retainedManagedRelativePaths = [],
|
|
71
71
|
ompConfigOwnership = null,
|
|
72
|
+
settingsDefaultsMigration = null,
|
|
72
73
|
lockStaleMs,
|
|
73
74
|
lockMaxWaitMs,
|
|
74
75
|
}) {
|
|
@@ -138,6 +139,9 @@ export async function writeInstallMetadata({
|
|
|
138
139
|
// before) — lets uninstall revert only UKit's changes. Kept across runs.
|
|
139
140
|
const ownership = ompConfigOwnership ?? (previousData?.tool === 'ukit' ? previousData.ompConfigOwnership : null);
|
|
140
141
|
if (ownership) metadata.ompConfigOwnership = ownership;
|
|
142
|
+
// One-time .claude/settings.json defaults migration marker (see diffPlan.js). Kept across runs.
|
|
143
|
+
const migrationMarker = settingsDefaultsMigration ?? (previousData?.tool === 'ukit' ? previousData.settingsDefaultsMigration : null);
|
|
144
|
+
if (migrationMarker) metadata.settingsDefaultsMigration = migrationMarker;
|
|
141
145
|
|
|
142
146
|
await ensureDir(path.dirname(installMetaPath));
|
|
143
147
|
await writeJson(installMetaPath, metadata);
|
|
@@ -7,7 +7,7 @@ import { detectProjectContext } from '../context/detectProjectContext.js';
|
|
|
7
7
|
import { detectProviders } from '../context/detectProviders.js';
|
|
8
8
|
import { buildTemplateVariables } from '../render/buildVariables.js';
|
|
9
9
|
import { buildInstallPlan } from './buildPlan.js';
|
|
10
|
-
import { diffInstallPlan } from './diffPlan.js';
|
|
10
|
+
import { diffInstallPlan, SETTINGS_DEFAULTS_MIGRATION_ID } from './diffPlan.js';
|
|
11
11
|
import { applyDiffResults, pruneOldBackups } from './applyPlan.js';
|
|
12
12
|
import { summarizeDiff, toDiffRows } from './report.js';
|
|
13
13
|
import { writeInstallMetadata, writeCliLocator, readInstallMetadata } from './metadata.js';
|
|
@@ -439,7 +439,10 @@ export async function runInstallPipeline({
|
|
|
439
439
|
.some((e) => (typeof e === 'string' ? e : e?.p) === '.omp/config.yml');
|
|
440
440
|
const ompOwnership = installMetaData?.ompConfigOwnership
|
|
441
441
|
?? (trackedOmpConfig ? { originalText: null, installedText: 'compaction:\n thresholdTokens: 350000\n' } : null);
|
|
442
|
-
const
|
|
442
|
+
const trackedSettings = (Array.isArray(installMetaData?.files) ? installMetaData.files : [])
|
|
443
|
+
.some((e) => (typeof e === 'string' ? e : e?.p) === '.claude/settings.json');
|
|
444
|
+
const settingsMigration = { pending: trackedSettings && !installMetaData?.settingsDefaultsMigration };
|
|
445
|
+
const diffResults = await diffInstallPlan(plan, { ompOwnership, settingsMigration });
|
|
443
446
|
const plannedSummary = summarizeDiff(diffResults);
|
|
444
447
|
|
|
445
448
|
if (!dryRun) {
|
|
@@ -464,6 +467,19 @@ export async function runInstallPipeline({
|
|
|
464
467
|
}
|
|
465
468
|
}
|
|
466
469
|
|
|
470
|
+
// One-time settings.json defaults migration: report it explicitly (never silent) and mark it
|
|
471
|
+
// done once the settings entry was processed, so later user edits are never migrated again.
|
|
472
|
+
let settingsDefaultsMigration = null;
|
|
473
|
+
for (const entry of diffResults) {
|
|
474
|
+
if (!/(^|[\\/])\.claude[\\/]settings\.json$/.test(String(entry.targetPath))) continue;
|
|
475
|
+
if (!['create', 'update', 'unchanged'].includes(entry.action)) continue;
|
|
476
|
+
if (entry.action !== 'unchanged' && !writes.some((w) => w.targetPath === entry.targetPath)) continue;
|
|
477
|
+
settingsDefaultsMigration = SETTINGS_DEFAULTS_MIGRATION_ID;
|
|
478
|
+
if (entry.settingsDefaultsMigrated?.length) {
|
|
479
|
+
console.log(`[UKit] .claude/settings.json: migrated old default ${entry.settingsDefaultsMigrated.join(' + ')} to model "opus" (main orchestrator) / effortLevel "medium"; previous file backed up. To pin your own, set it in .claude/settings.local.json (UKit never edits it).`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
467
483
|
// SPEC §7 step 9: surface the .omp/config.yml unattended migration result.
|
|
468
484
|
for (const entry of diffResults) {
|
|
469
485
|
const report = entry?.migrationReport;
|
|
@@ -546,6 +562,7 @@ export async function runInstallPipeline({
|
|
|
546
562
|
managedRelativePaths: [...trackedManagedRelativePaths],
|
|
547
563
|
retainedManagedRelativePaths,
|
|
548
564
|
ompConfigOwnership,
|
|
565
|
+
settingsDefaultsMigration,
|
|
549
566
|
});
|
|
550
567
|
|
|
551
568
|
// 3.3.0: hooks spawn the CLI (episodes, self-improve) from a GUI-launched
|
|
@@ -442,7 +442,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
442
442
|
},
|
|
443
443
|
orchestration: {
|
|
444
444
|
enabled: true,
|
|
445
|
-
orchestratorModel: 'claude-
|
|
445
|
+
orchestratorModel: 'claude-opus-5',
|
|
446
446
|
permissionMode: 'unattended',
|
|
447
447
|
// FR-003: eval-subprocess gate for rendered omp approval (tools.approval.eval).
|
|
448
448
|
// Fail-closed: only an explicit user `true` renders `eval: allow`; absent,
|
|
@@ -114,6 +114,20 @@ function hasSqlIntent(text) {
|
|
|
114
114
|
// The SQL ask touches shape/data rather than a read-only query — routes the
|
|
115
115
|
// migration row instead of a feature build.
|
|
116
116
|
const SQL_SCHEMA_RE = /(?<![A-Za-z0-9_])(?:ddl|alter|drop|(?:create|alter|drop|truncate)\s+table|(?:add|drop|rename|change|modify|update|thêm|xóa|xoá|sửa|sua|đổi|doi)\s+(?:a\s+|the\s+|new\s+|một\s+)*(?:column|cột|cot)(?!\s+of\b)|kiểu\s+dữ\s+liệu|kieu\s+du\s+lieu|data\s?type|not\s+null|constraint|migration|migrate)(?![A-Za-z0-9_])/i;
|
|
117
|
+
// Explicit delegation order: the user themself asks for a worker/subagent to implement
|
|
118
|
+
// ("Delegate the implementation to a worker agent", "have a subagent implement it"). That is an
|
|
119
|
+
// opt-in to the orchestrated path (Tier 3 orchestrates, Tier 2 implements, integration verified by
|
|
120
|
+
// the orchestrator), so it lifts the small-feature fast path into feature-implementation. Live omp
|
|
121
|
+
// run: this exact order stayed on small-feature (explicit-local-target fast path, subagents
|
|
122
|
+
// suppressed) and the Tier 3 main implemented nothing of the delegation. Bare mentions of the
|
|
123
|
+
// words ("worker thread", "the delegate pattern") do not match; negated orders ("don't delegate")
|
|
124
|
+
// veto.
|
|
125
|
+
const DELEGATION_ORDER_RE = /\bdelegat(?:e|ing)\b[^.\n]{0,80}?\bto\s+(?:an?\s+|the\s+|your\s+)?(?:[\w-]+\s+){0,2}?(?:worker|sub-?agents?|agents?|implementer)\b|\b(?:use|spawn|dispatch|launch|assign|have|let)\s+(?:an?\s+|the\s+|your\s+)?(?:[\w-]+\s+)?(?:worker|sub-?agents?|implementer)(?:\s+agent)?\s+(?:to|for)\b|(?<![A-Za-z0-9_])giao\s+(?:việc\s+)?cho\s+(?:worker|sub-?agent|agent)/i;
|
|
126
|
+
const DELEGATION_NEGATED_RE = /\b(?:don['’]?t|do\s+not|never|without|avoid|no\s+need\s+to)\s+(?:[\w-]+\s+){0,2}?(?:delegat\w*|(?:use|spawn|dispatch)\s+(?:an?\s+|any\s+)?(?:worker|sub-?agents?))|\bno\s+(?:worker|sub-?agents?|delegation)\b|\b(?:inline|yourself)\s+(?:only|not)\b/i;
|
|
127
|
+
export function hasDelegationOrder(text) {
|
|
128
|
+
const t = String(text ?? '');
|
|
129
|
+
return DELEGATION_ORDER_RE.test(t) && !DELEGATION_NEGATED_RE.test(t);
|
|
130
|
+
}
|
|
117
131
|
// Reuse-first ask: "use the existing/core function", "dùng hàm có sẵn",
|
|
118
132
|
// "tái sử dụng" — search for the function before writing a new one.
|
|
119
133
|
const REUSE_FIRST_RE = /(?:\buse\s+(?:the\s+)?(?:existing|core|shared|same|current)\b)|(?:\buse\s+[\w.]+\s+from\s+(?:the\s+)?(?:core|shared|utils?|common|lib)\b)|\b(?:existing|core|shared)\s+(?:function|helper|util(?:ity|ities)?|service|component)\b|\breuse\b|(?<![A-Za-z0-9_])(?:tái\s+sử\s+dụng|tai\s+su\s+dung)|(?<![A-Za-z0-9_])(?:dùng|dung|dựa|dua)\s+(?:vào\s+|vao\s+)?(?:hàm|ham)(?:\s+(?:có\s+sẵn|co\s+san|sẵn\s+có|san\s+co|đã\s+có|da\s+co|cũ|cu))?(?![A-Za-z0-9_])/i;
|
|
@@ -174,7 +188,7 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
174
188
|
// contract signals) escalates to feature-implementation. Reference-driven,
|
|
175
189
|
// clone, reuse-first and SQL-schema asks take the same escape: they name a
|
|
176
190
|
// source of truth or a data contract the small path must not gloss over.
|
|
177
|
-
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && s.changeVerbOrCreate,
|
|
191
|
+
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && !s.delegationOrdered && s.changeVerbOrCreate,
|
|
178
192
|
fastPath: 'small-feature',
|
|
179
193
|
},
|
|
180
194
|
{
|
|
@@ -195,7 +209,7 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
195
209
|
// migration row instead of capturing the build lane.
|
|
196
210
|
discriminator: (s) => (
|
|
197
211
|
(s.contractSignal || s.multiSurface || (s.escalated && !s.sqlSchemaChange) || s.wiredShipAsk)
|
|
198
|
-
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst) && s.defectIdx === -1)
|
|
212
|
+
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst || s.delegationOrdered) && s.defectIdx === -1)
|
|
199
213
|
) && !s.disposable && !s.releaseAsk,
|
|
200
214
|
},
|
|
201
215
|
{
|
|
@@ -291,7 +305,9 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
291
305
|
lanes: ['local-fix', 'review-release'],
|
|
292
306
|
triggers: [/\bverify\b|\bcheck\b|\bconfirm\b|\bprove\b|\bvalidate\b|\bassert\b/i],
|
|
293
307
|
// the ask IS a check — a check verb leads, or no defect verb exists at all.
|
|
294
|
-
|
|
308
|
+
// A build/create verb that leads the ask makes the check its closing step ("implement X … then run the
|
|
309
|
+
// tests and confirm"), not the ask itself — the build row owns it instead of tying into ambiguity.
|
|
310
|
+
discriminator: (s) => (s.checkFirst || s.defectIdx === -1) && !s.buildLeadsCheck,
|
|
295
311
|
},
|
|
296
312
|
{
|
|
297
313
|
workGroup: 'skill-evaluation',
|
|
@@ -409,6 +425,7 @@ async function buildRouteSignals({
|
|
|
409
425
|
const explanationAsk = explainIdx !== -1 && (firstVerbIdx === -1 || explainIdx < firstVerbIdx);
|
|
410
426
|
const changeRequested = !consultLead && firstVerbIdx !== -1 && (explainIdx === -1 || firstVerbIdx < explainIdx);
|
|
411
427
|
const checkFirst = checkIdx !== -1 && (defectIdx === -1 || checkIdx < defectIdx);
|
|
428
|
+
const buildLeadsCheck = changeRequested && checkIdx !== -1 && firstVerbIdx !== -1 && firstVerbIdx < checkIdx;
|
|
412
429
|
// C85-019: a QUESTION about a defect is not a defect order. The bug-fix floor
|
|
413
430
|
// demands repro+root-cause receipts — demanding them on a "did it get fixed
|
|
414
431
|
// yet?" turn forces edits nobody asked for (user-reported lock). Question
|
|
@@ -440,12 +457,14 @@ async function buildRouteSignals({
|
|
|
440
457
|
changeRequested,
|
|
441
458
|
questionAsk,
|
|
442
459
|
checkFirst,
|
|
460
|
+
buildLeadsCheck,
|
|
443
461
|
changeVerbOrCreate: changeIdx !== -1 || createIdx !== -1,
|
|
444
462
|
referenceTarget,
|
|
445
463
|
cloneIntent,
|
|
446
464
|
sqlIntent,
|
|
447
465
|
sqlSchemaChange: sqlIntent && SQL_SCHEMA_RE.test(signalText),
|
|
448
466
|
reuseFirst,
|
|
467
|
+
delegationOrdered: hasDelegationOrder(signalText) && changeRequested,
|
|
449
468
|
contractSignal: CONTRACT_SIGNAL_RE.test(signalText) || /\boauth\b/i.test(signalText),
|
|
450
469
|
multiSurface: MULTI_SURFACE_RE.test(signalText),
|
|
451
470
|
escalated: Array.isArray(escalationTriggers) && escalationTriggers.length > 0,
|
package/src/index/taskRouting.js
CHANGED
|
@@ -895,7 +895,7 @@ function deriveExecutionScores({
|
|
|
895
895
|
const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
|
|
896
896
|
const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
|
|
897
897
|
const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
|
|
898
|
-
const failureSignal = /\b(failing|failed|broken|error|crash|timeout|undefined|exception|eacces|trace)\b/.test(raw);
|
|
898
|
+
const failureSignal = /\b(failing|failed|broken|error|crash|timeout|undefined|exception|eacces|trace)\b/.test(raw.replace(/\b(?:throw|raise|emit|reject|return|log|show|surface)(?:s|es|ing)?\s+(?:an?\s+|the\s+)?(?:[a-z-]+\s+){0,2}[a-z]*(?:error|exception)s?\b/g, ' '));
|
|
899
899
|
const impactSignal = /\b(map impact|impact|blast radius|all affected|across (?:runtime|templates|helpers|adapters|mirrors)|affected adapters|affected mirrors|inspect impact)\b/.test(raw);
|
|
900
900
|
const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
|
|
901
901
|
const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
|
|
@@ -1813,7 +1813,7 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
|
|
|
1813
1813
|
}
|
|
1814
1814
|
|
|
1815
1815
|
if (concreteTask) {
|
|
1816
|
-
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|timeout)\b/.test(lower)) {
|
|
1816
|
+
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|timeout)\b/.test(lower.replace(/\b(?:throw|raise|emit|reject|return|log|show|surface)(?:s|es|ing)?\s+(?:an?\s+|the\s+)?(?:[a-z-]+\s+){0,2}[a-z]*(?:error|exception)s?\b/g, ' '))) {
|
|
1817
1817
|
return 'debug-specific';
|
|
1818
1818
|
}
|
|
1819
1819
|
if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(raw)) {
|