@ngockhoale/ukit 3.4.47 → 3.4.49

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 (62) hide show
  1. package/CHANGELOG.md +15 -0
  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/worktree-preflight.mjs +20 -1
  6. package/src/core/diffPlan.js +38 -12
  7. package/src/core/executionContracts.js +79 -18
  8. package/src/core/metadata.js +4 -0
  9. package/src/core/runInstallPipeline.js +19 -2
  10. package/src/core/runtimeConfig.js +1 -1
  11. package/src/index/playbookRegistry.js +22 -3
  12. package/src/index/taskRouting.js +2 -2
  13. package/template_project/.claude/agents/bug-debugger.md +1 -0
  14. package/template_project/.claude/agents/code-reviewer.md +1 -0
  15. package/template_project/.claude/agents/feature-implementer.md +1 -0
  16. package/template_project/.claude/agents/handoff-planner.md +1 -0
  17. package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
  18. package/template_project/.claude/hooks/completion-gate.sh +1 -1
  19. package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
  20. package/template_project/.claude/hooks/skill-router.sh +159 -8
  21. package/template_project/.claude/settings.json +8 -8
  22. package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
  23. package/template_project/.claude/ukit/index/route-resolver.mjs +79 -17
  24. package/template_project/.claude/ukit/index/route-task.mjs +115 -37
  25. package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
  26. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
  27. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +331 -21
  28. package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
  29. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  30. package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
  31. package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
  32. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
  33. package/template_project/.omp/agents/bug-debugger.md +1 -0
  34. package/template_project/.omp/agents/code-reviewer.md +1 -0
  35. package/template_project/.omp/agents/feature-implementer.md +1 -0
  36. package/template_project/.omp/agents/handoff-planner.md +1 -0
  37. package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
  38. package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
  39. package/template_project/ukit/storage/config.json +1 -1
  40. package/scripts/bench/data-foundation.mjs +0 -929
  41. package/scripts/bench/decision-runtime-baseline.mjs +0 -427
  42. package/scripts/bench/decision-runtime-variant.mjs +0 -626
  43. package/scripts/bench/goldTasks.json +0 -38
  44. package/scripts/bench/memory-ablation.mjs +0 -495
  45. package/scripts/bench/memory-baseline.mjs +0 -596
  46. package/scripts/bench/memory-bench.mjs +0 -661
  47. package/scripts/bench/memory-canary.mjs +0 -331
  48. package/scripts/bench/memory-corpus.mjs +0 -354
  49. package/scripts/bench/memory-gate.mjs +0 -389
  50. package/scripts/bench/memory-metrics.mjs +0 -179
  51. package/scripts/bench/outline-savings.mjs +0 -107
  52. package/scripts/bench/parallel-agents.mjs +0 -224
  53. package/scripts/bench/recorder-overhead.mjs +0 -204
  54. package/scripts/bench/runGold.mjs +0 -238
  55. package/scripts/bench/sqlite-spike.mjs +0 -451
  56. package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
  57. package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
  58. package/scripts/bench/v3-ceremony.mjs +0 -220
  59. package/scripts/perf/audit-perf.mjs +0 -1195
  60. package/scripts/perf/diff-perf-findings.mjs +0 -136
  61. package/scripts/perf/perf-findings.json +0 -474
  62. package/scripts/perf/perf-measure.md +0 -271
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.49 - 2026-10-19
6
+
7
+ **Conditional consumer verification intent closure.**
8
+ - Fix a live Completion Gate false write obligation: a verification request ("Mục tiêu: xác nhận …") whose source repair/release steps apply only if a defect is found, plus a disposable test-project setup, was classified CODE_IMPLEMENTATION and demanded write-evidence even after a real `npm test` exit 0. The classifier now recognizes a declared verification goal with a narrowly structured conditional repair branch and `xác nhận` as a verify lead; unconditional trailing orders, quoted goals and non-repair conditionals still block.
9
+ - Safety preserved: when the conditional repair branch is actually taken (an owned in-project substantive source write), the evaluator re-activates the release-publication demand; explicit publish prohibitions dominate. Classifier copies (src, resolver, hook) are pinned by a mirror parity test.
10
+ - Reproduced through the npm 3.4.48 consumer router/record/Stop lifecycle with real native exits; no receipt fabrication, task reassignment or ledger reset. Direct `node --test` recognition is unchanged. Verified on macOS/Node 22 only; OMP live attribution UNVERIFIED; no paid OMP smoke or CI dispatch.
11
+
12
+ ## 3.4.48 - 2026-10-19
13
+
14
+ **Completion evidence closure and reproducible multi-host installation.**
15
+ - 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.
16
+ - 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.
17
+ - Integrate verified OMP child-session/model-ownership safeguards, delegation runtime, installer defaults migration, runtime package dependencies and reproducible scaffolds.
18
+ - 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.
19
+
5
20
  ## 3.4.47 - 2026-10-18
6
21
 
7
22
  **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.47",
3
+ "version": "3.4.49",
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');
@@ -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
- function mergeSettingsObjects(existing, rendered) {
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 merged = mergeSettingsJson(existingContent, entry.renderedContent);
277
- action = settingsJsonIgnoresGatewayEnvDiff(existingContent, entry.renderedContent) ? 'unchanged' : 'update';
278
- return { ...entry, renderedContent: merged, exists, action, existingContent };
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 ladder for the one-band high-risk escalation; effort rides the resolved tier.
99
- const MODEL_TIER_ORDER = ['lite', 'code', 'smart'];
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: 'high',
103
+ smart: 'medium',
104
104
  };
105
105
 
106
106
  /**
107
- * Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor escalates
108
- * the tier one band (lite→code→smart, capped at smart) and forces effort 'high'.
109
- * Unknown mode → {tier:null, effort:null, advisoryOnly:true}. Missing/unknown
110
- * riskFloor → 'none'. Missing hostCapabilities → advisoryOnly:true (the route text
111
- * is all the host gets when it cannot bind model and effort).
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: highRisk ? 'high' : MODEL_TIER_EFFORT[tier],
130
+ effort: complexityEvidence === true ? 'high' : MODEL_TIER_EFFORT[tier],
129
131
  advisoryOnly: !(hostCapabilities?.canBindModel === true && hostCapabilities?.canBindEffort === true),
130
132
  };
131
133
  }
@@ -197,19 +199,72 @@ function nativeOperationGoal(domain, actionable, text) {
197
199
  // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
198
200
  // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
199
201
  const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
202
+ // Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
203
+ // conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
204
+ // branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
205
+ // nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
206
+ const VERIFICATION_GOAL = /(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|kiem tra|validate|double-?check|re-?run)\b/;
207
+ const CONDITIONAL_BRANCH = /(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+(?:phat hien\s+(?:false-block|loi|bug|defect)|co\s+(?:source fix|sua doi))|if\s+(?:(?:a\s+)?(?:bug|defect|failure)\s+is\s+found|(?:you\s+)?find\s+(?:a\s+)?(?:bug|defect)))\b[^\n]*?(?=\.\s+|$|\n)/g;
208
+ const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
209
+ const DISPOSABLE_SETUP = /\b(?:tao|create)\s+(?:mot\s+)?(?:project|du an)\s+(?:thu nghiem|test|disposable|doc lap)\b(?:\s+(?:doc lap|independent))?/g;
200
210
  function neutralizeNonOrders(text) {
201
- return String(text)
211
+ let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
212
+ if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
213
+ return source
202
214
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
203
215
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
204
216
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
205
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
217
+ .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
206
218
  .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, ' ')
219
+ .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
220
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
209
221
  .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
210
222
  .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
211
223
  }
212
224
 
225
+ // Explicit scope declaration (intent ownership): when the request itself declares it is read-only
226
+ // ("READ-ONLY audit", "chi doc", "make no source changes") and carries no explicit order to change
227
+ // anything, verbs and operation names inside it are descriptions (a "git stash" that already ran, "a
228
+ // worklog edit", "confirmed fixes"), not orders - so they must not demand write or operation proof.
229
+ // An explicit order (imperative-initial or modal request) keeps the request MIXED/IMPLEMENTATION:
230
+ // the declaration never overrides a real instruction, and fenced/quoted/blockquoted text is never one.
231
+ // A bare verb with no object is the tail of a negated list ("no commit, release, publish"), not an order.
232
+ 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/;
233
+ 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)';
234
+ const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
235
+ 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`);
236
+ function hasExplicitOrder(clauses) {
237
+ return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
238
+ }
239
+ function declaredReadOnly(promptText, releasePolicy) {
240
+ if (releasePolicy) return false;
241
+ const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
242
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
243
+ if (!READ_ONLY_DECLARATION.test(plain)) return false;
244
+ const text = neutralizeNonOrders(plain);
245
+ const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
246
+ .map((clause) => clause.split(/\bwithout\b/)[0])
247
+ .filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause));
248
+ 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)
249
+ && !hasExplicitOrder(clauses);
250
+ }
251
+
252
+ // Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
253
+ // confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
254
+ // "template/install", "source change") as mutation orders. Order is structural: a request whose
255
+ // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
256
+ // change-leading clause, orders no change - it owes verification evidence, not a write.
257
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
258
+ 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/;
259
+ export function isVerificationOnlyRequest(promptText) {
260
+ const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
261
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
262
+ const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
263
+ .map((clause) => clause.split(/\bwithout\b/)[0]);
264
+ return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
265
+ && !clauses.some((clause) => CHANGE_LEAD.test(clause));
266
+ }
267
+
213
268
  export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
214
269
  // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
215
270
  const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
@@ -221,6 +276,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
221
276
  const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
222
277
  const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
223
278
  && !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
279
+ if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
224
280
  const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
225
281
  && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
226
282
  const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
@@ -238,10 +294,15 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
238
294
  // runtime are separate domains with separate proof; a prompt naming both stays a coding
239
295
  // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
240
296
  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
- const runtimeOperation = actionable.some((clause) => /\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\b/.test(clause)
297
+ // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
298
+ // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
299
+ // "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
300
+ 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
301
  && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
243
302
  const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
244
303
  const operation = Boolean(nativeDomain);
304
+ // A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
305
+ if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
245
306
  // A config MENTION is not a config change: the same clause needs a mutation verb, and a
246
307
  // request that also changes source/code is a coding task (config is part of its tests/fixtures).
247
308
  const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
@@ -252,7 +313,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
252
313
  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
314
  const statusReport = !mutationWord && !operation && !config
254
315
  && /\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)
316
+ 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
317
  && !sourceOrder && !codeChangeVerb && !operation && !config;
257
318
  const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
258
319
  : config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
@@ -271,5 +332,5 @@ export function applyTaskSemantics(contract, semantic) {
271
332
  completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
272
333
  : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
273
334
  : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
274
- : semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
335
+ : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
275
336
  }
@@ -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 diffResults = await diffInstallPlan(plan, { ompOwnership });
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-sonnet-5',
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,