@ngockhoale/ukit 3.4.46 → 3.4.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -5
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/capture-omp-stall.mjs +246 -0
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/scripts/docs/render-instructions.mjs +29 -5
- package/src/core/agentRuntime/supervisor.js +15 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +67 -16
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/diagnostics/ompStallSnapshot.js +199 -0
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +147 -6
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +80 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- package/scripts/perf/perf-measure.md +0 -271
|
@@ -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,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Development-only incident capture: bounded metadata, no session control or payload persistence.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const ID_PATTERN = /^[\w.:/-]{1,120}$/;
|
|
7
|
+
const id = (value) => typeof value === 'string' && ID_PATTERN.test(value) ? value : null;
|
|
8
|
+
const time = (value) => Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export async function runBoundedProcess(command, args = [], { timeoutMs = 650, maxBuffer = 65536 } = {}) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
let timedOut = false;
|
|
15
|
+
let oversized = false;
|
|
16
|
+
let stdoutBytes = 0;
|
|
17
|
+
let stderrBytes = 0;
|
|
18
|
+
const stdout = [];
|
|
19
|
+
const stderr = [];
|
|
20
|
+
let timer;
|
|
21
|
+
const finish = (result) => { if (!settled) { settled = true; clearTimeout(timer); resolve(result); } };
|
|
22
|
+
let child;
|
|
23
|
+
try { child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); }
|
|
24
|
+
catch (error) { resolve({ ok: false, errorCode: error.code || 'UNKNOWN', stdout: '', stderr: '' }); return; }
|
|
25
|
+
timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, Math.max(1, timeoutMs));
|
|
26
|
+
const collect = (chunks, field) => (chunk) => {
|
|
27
|
+
const next = field === 'stdout' ? stdoutBytes + chunk.length : stderrBytes + chunk.length;
|
|
28
|
+
if (next + (field === 'stdout' ? stderrBytes : stdoutBytes) > maxBuffer) { oversized = true; child.kill('SIGKILL'); return; }
|
|
29
|
+
chunks.push(chunk);
|
|
30
|
+
if (field === 'stdout') stdoutBytes = next; else stderrBytes = next;
|
|
31
|
+
};
|
|
32
|
+
child.stdout.on('data', collect(stdout, 'stdout'));
|
|
33
|
+
child.stderr.on('data', collect(stderr, 'stderr'));
|
|
34
|
+
child.on('error', (error) => finish({ ok: false, errorCode: error.code || 'UNKNOWN', stdout: '', stderr: '' }));
|
|
35
|
+
child.on('close', (code) => finish({ ok: !timedOut && !oversized && code === 0,
|
|
36
|
+
errorCode: timedOut ? 'ETIMEDOUT' : oversized ? 'MAXBUFFER' : code === 0 ? null : 'EXIT_NONZERO',
|
|
37
|
+
exitCode: code, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') }));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readBoundedTail(file, { maxBytes = 262144 } = {}) {
|
|
42
|
+
const cap = Math.max(1, Math.min(1048576, maxBytes));
|
|
43
|
+
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NONBLOCK || 0) | (constants.O_NOFOLLOW || 0));
|
|
44
|
+
try {
|
|
45
|
+
const stat = await handle.stat();
|
|
46
|
+
if (!stat.isFile()) throw new Error('capture requires a regular file');
|
|
47
|
+
const size = Math.min(cap, stat.size);
|
|
48
|
+
const buffer = Buffer.alloc(size);
|
|
49
|
+
const { bytesRead } = await handle.read(buffer, 0, size, Math.max(0, stat.size - size));
|
|
50
|
+
let text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
51
|
+
const truncated = stat.size > size;
|
|
52
|
+
if (truncated) text = text.includes('\n') ? text.slice(text.indexOf('\n') + 1) : '';
|
|
53
|
+
return { text, bytesRead, truncated, size: stat.size, modifiedAt: stat.mtimeMs };
|
|
54
|
+
} finally { await handle.close(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const operationKinds = ['model', 'tool', 'hook', 'subagent'];
|
|
58
|
+
const activeStates = new Set(['started', 'running', 'pending', 'waiting']);
|
|
59
|
+
const terminalStates = new Set(['completed', 'finished', 'failed', 'stopped', 'cancelled']);
|
|
60
|
+
export function processInventoryCommand(platform) {
|
|
61
|
+
return platform === 'win32' ? ['tasklist', ['/FO', 'CSV', '/NH']]
|
|
62
|
+
: ['ps', ['-A', '-o', 'pid=,ppid=,stat=,etime=,comm=']];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseProcessInventory(platform, text) {
|
|
66
|
+
if (platform === 'win32') return text.split(/\r?\n/).flatMap((line) => {
|
|
67
|
+
const fields = line.match(/"(?:[^"]|"")*"|[^,]+/g) || [];
|
|
68
|
+
const name = fields[0]?.replace(/^"|"$/g, '').replace(/""/g, '"');
|
|
69
|
+
const pid = Number(fields[1]?.replaceAll('"', ''));
|
|
70
|
+
return Number.isSafeInteger(pid) && pid > 0 ? [{ pid, name: (name || '').split(/[\\/]/).at(-1) }] : [];
|
|
71
|
+
});
|
|
72
|
+
return text.split('\n').flatMap((line) => {
|
|
73
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
|
|
74
|
+
return match ? [{ pid: Number(match[1]), name: match[5].trim().split(/[\\/]/).at(-1) }] : [];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseOwnerPids(command, text) {
|
|
79
|
+
const values = command === 'fuser' ? (text.match(/:\s*([^\r\n]+)/g) || []).flatMap((line) => line.slice(line.indexOf(':') + 1).trim().split(/\s+/).map((token) => token.match(/^(\d+)[A-Za-z]*$/)?.[1]).filter(Boolean))
|
|
80
|
+
: (text.match(/\b\d+\b/g) || []);
|
|
81
|
+
return [...new Set(values.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0))];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function classifySessionActivity({ association = 'NOT_OBSERVABLE', summary = null, now = Date.now(), idleMs = 120000 } = {}) {
|
|
85
|
+
if (summary?.operatorBlock === 'OBSERVED_BLOCK') return 'OPERATOR_BLOCK';
|
|
86
|
+
if (summary?.sessionState === 'STOPPED') return 'STOPPED';
|
|
87
|
+
if (association !== 'ACTIVE_OWNER' || !summary) return 'NOT_OBSERVABLE';
|
|
88
|
+
const progressAge = Number.isFinite(summary.lastProgressAt) ? Math.max(0, now - summary.lastProgressAt) : null;
|
|
89
|
+
const outputAge = Number.isFinite(summary.lastOutputAt) ? Math.max(0, now - summary.lastOutputAt) : null;
|
|
90
|
+
const pending = summary.pendingOperations || {};
|
|
91
|
+
if (progressAge === null) return 'NOT_OBSERVABLE';
|
|
92
|
+
if (progressAge >= idleMs) return 'SUSPECTED_STALL';
|
|
93
|
+
if (Object.values(pending).some((items) => Array.isArray(items) && items.length > 0)
|
|
94
|
+
&& Object.entries(pending).some(([kind, items]) => items.length > 0 && summary.operationObservability?.[kind] === 'OBSERVED')) return 'WAITING';
|
|
95
|
+
if (outputAge !== null && outputAge >= idleMs) return 'NO_OUTPUT';
|
|
96
|
+
return 'ACTIVE_PROGRESS';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export function summarizeSessionTail(text) {
|
|
101
|
+
const result = { sessionId: null, modelAlias: 'NOT_OBSERVABLE', resolvedRoute: 'NOT_OBSERVABLE', currentTurn: 'NOT_OBSERVABLE', lastCompletedTurn: 'NOT_OBSERVABLE',
|
|
102
|
+
pendingModelRequest: 'NOT_OBSERVABLE', lastChunk: 'NOT_OBSERVABLE', terminalEvent: 'NOT_OBSERVABLE', pendingTools: [], unmatchedToolCallCandidates: [], pendingSubagent: 'NOT_OBSERVABLE',
|
|
103
|
+
pendingOperations: { model: [], tool: [], hook: [], subagent: [] }, operationObservability: { model: 'NOT_OBSERVABLE', tool: 'NOT_OBSERVABLE', hook: 'NOT_OBSERVABLE', subagent: 'NOT_OBSERVABLE' },
|
|
104
|
+
operatorBlock: 'NOT_OBSERVABLE', turnState: 'NOT_OBSERVABLE', sessionState: 'NOT_OBSERVABLE', compaction: 'NOT_OBSERVABLE', stopContinuation: 'NOT_OBSERVABLE',
|
|
105
|
+
lastProgressAt: 'NOT_OBSERVABLE', lastOutputAt: 'NOT_OBSERVABLE', outputState: 'NOT_OBSERVABLE' };
|
|
106
|
+
const operations = Object.fromEntries(operationKinds.map((kind) => [kind, new Map()]));
|
|
107
|
+
const tools = new Map();
|
|
108
|
+
const observedOperations = new Set();
|
|
109
|
+
const operatorBlocks = new Map();
|
|
110
|
+
for (const line of text.split('\n')) {
|
|
111
|
+
let row;
|
|
112
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
113
|
+
const ts = time(row.timestamp);
|
|
114
|
+
if (ts) result.lastProgressAt = Date.parse(ts);
|
|
115
|
+
if (row.type === 'session' && id(row.id)) { result.sessionId = id(row.id); result.sessionIdentityObserved = true; }
|
|
116
|
+
if (row.type === 'compaction') result.compaction = { timestamp: ts, id: id(row.id), tokensBefore: Number.isFinite(row.tokensBefore) ? row.tokensBefore : null };
|
|
117
|
+
if (row.type === 'session_end' || row.type === 'session_stopped') result.sessionState = 'STOPPED';
|
|
118
|
+
|
|
119
|
+
if (row.type === 'operation' && operationKinds.includes(row.operation) && id(row.id)) {
|
|
120
|
+
observedOperations.add(row.operation);
|
|
121
|
+
const state = typeof row.state === 'string' ? row.state.toLowerCase() : '';
|
|
122
|
+
if (activeStates.has(state)) operations[row.operation].set(row.id, { id: row.id, ...(id(row.name) ? { name: id(row.name) } : {}) });
|
|
123
|
+
if (terminalStates.has(state)) operations[row.operation].delete(row.id);
|
|
124
|
+
}
|
|
125
|
+
const eventState = String(row.state || '').toLowerCase();
|
|
126
|
+
if (row.type === 'operator_block' || row.operation === 'operator') {
|
|
127
|
+
const key = id(row.id) || `block-${operatorBlocks.size}`;
|
|
128
|
+
if (['pending', 'blocked', 'waiting'].includes(eventState)) operatorBlocks.set(key, { id: id(row.id), state: 'BLOCKED' });
|
|
129
|
+
if (terminalStates.has(eventState) || ['unblocked', 'cleared'].includes(eventState)) operatorBlocks.delete(key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const message = row.message;
|
|
133
|
+
if (!message) continue;
|
|
134
|
+
if (message.role === 'toolResult' && id(message.toolCallId)) tools.delete(message.toolCallId);
|
|
135
|
+
if (message.role !== 'assistant') continue;
|
|
136
|
+
result.modelAlias = { provider: id(message.provider), model: id(message.model) };
|
|
137
|
+
if (message.stopReason) {
|
|
138
|
+
const stopReason = ['stop', 'toolUse', 'error', 'aborted', 'length'].includes(message.stopReason) ? message.stopReason : 'other';
|
|
139
|
+
result.lastCompletedTurn = { id: id(row.id), timestamp: ts, stopReason };
|
|
140
|
+
result.terminalEvent = stopReason;
|
|
141
|
+
result.turnState = message.stopReason === 'aborted' ? 'TURN_ABORTED' : message.stopReason === 'error' ? 'TURN_ERROR' : 'TURN_COMPLETED';
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(message.content)) {
|
|
144
|
+
for (const part of message.content) {
|
|
145
|
+
if (part.type === 'toolCall' && id(part.id)) tools.set(part.id, { id: id(part.id), name: id(part.name) });
|
|
146
|
+
if (part.type === 'text' && ts) {
|
|
147
|
+
result.lastOutputAt = Date.parse(ts);
|
|
148
|
+
result.outputState = 'OBSERVED_OUTPUT';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
result.unmatchedToolCallCandidates = [...tools.values()].slice(-32);
|
|
154
|
+
result.pendingOperations = Object.fromEntries(operationKinds.map((kind) => [kind, [...operations[kind].values()].slice(-32)]));
|
|
155
|
+
result.pendingTools = result.pendingOperations.tool;
|
|
156
|
+
result.operationObservability = Object.fromEntries(operationKinds.map((kind) => [kind, observedOperations.has(kind) || (kind === 'tool' && tools.size > 0) ? 'OBSERVED' : 'NOT_OBSERVABLE']));
|
|
157
|
+
result.pendingModelRequest = result.operationObservability.model === 'OBSERVED' ? (result.pendingOperations.model.length ? 'OBSERVED_PENDING_EVENT' : 'NO_PENDING_EVENT_OBSERVED') : 'NOT_OBSERVABLE';
|
|
158
|
+
result.pendingSubagent = result.operationObservability.subagent === 'OBSERVED' ? (result.pendingOperations.subagent.length ? 'OBSERVED_PENDING_EVENT' : 'NO_PENDING_EVENT_OBSERVED') : 'NOT_OBSERVABLE';
|
|
159
|
+
result.operatorBlock = operatorBlocks.size ? 'OBSERVED_BLOCK' : 'NOT_OBSERVABLE';
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function associateSessionEvidence({ sessions = [], ownerLocks = {}, processInventory = { state: 'NOT_OBSERVABLE', processes: [] }, logs = [] } = {}) {
|
|
164
|
+
const sessionCounts = new Map();
|
|
165
|
+
for (const session of sessions) if (id(session.sessionId)) sessionCounts.set(session.sessionId, (sessionCounts.get(session.sessionId) || 0) + 1);
|
|
166
|
+
return sessions.map((session) => {
|
|
167
|
+
const sessionId = id(session.sessionId);
|
|
168
|
+
const lock = sessionId ? ownerLocks[sessionId] : null;
|
|
169
|
+
const lockState = lock?.state || 'NOT_OBSERVABLE';
|
|
170
|
+
const pids = lockState === 'OBSERVED' && Array.isArray(lock.pids) ? [...new Set(lock.pids.filter((pid) => Number.isSafeInteger(pid) && pid > 0))] : [];
|
|
171
|
+
const uniqueOwnerPid = pids.length === 1 ? pids[0] : null;
|
|
172
|
+
const duplicateSession = sessionId !== null && sessionCounts.get(sessionId) > 1;
|
|
173
|
+
const process = uniqueOwnerPid !== null && processInventory.state === 'OBSERVED'
|
|
174
|
+
? processInventory.processes.find((entry) => entry.pid === uniqueOwnerPid && /^omp(?:\.exe)?$/i.test(entry.name)) || null : null;
|
|
175
|
+
const processAssociation = duplicateSession ? 'AMBIGUOUS_TRANSCRIPT' : pids.length > 1 ? 'AMBIGUOUS_OWNER_LOCK'
|
|
176
|
+
: uniqueOwnerPid === null ? (lockState === 'NOT_OBSERVABLE' ? 'NOT_OBSERVABLE' : 'UNHELD')
|
|
177
|
+
: processInventory.state !== 'OBSERVED' ? 'NOT_OBSERVABLE' : process ? 'VERIFIED' : 'PID_NOT_FOUND';
|
|
178
|
+
const ownerPid = duplicateSession ? null : uniqueOwnerPid;
|
|
179
|
+
const identityVerified = session?.sessionIdentityObserved === true || session?.sourceCoverage?.sessionIdentityObserved === true;
|
|
180
|
+
const ompLog = ownerPid === null ? null : logs.find((entry) => entry.pid === ownerPid) || null;
|
|
181
|
+
const logAssociation = ownerPid === null ? 'NOT_OBSERVABLE' : ompLog ? 'VERIFIED' : 'NOT_OBSERVABLE';
|
|
182
|
+
return { sessionId, transcript: typeof session.transcriptPath === 'string' ? session.transcriptPath.split(/[\\/]/).at(-1) : null,
|
|
183
|
+
sessionAssociation: identityVerified && processAssociation === 'VERIFIED' && logAssociation === 'VERIFIED' ? 'ACTIVE_OWNER' : 'NOT_OBSERVABLE',
|
|
184
|
+
ownerLockState: duplicateSession ? 'AMBIGUOUS_TRANSCRIPT' : lockState, ownerPid, processAssociation, logAssociation, identityVerified,
|
|
185
|
+
process: process ? { pid: process.pid, name: id(process.name) } : null, ompLog };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildStallSnapshot({ now = Date.now(), lastProgressAt = null, lastOutputAt = null, idleMs = 120000, executionState = 'NOT_OBSERVABLE', ...evidence } = {}) {
|
|
190
|
+
const age = (ts) => Number.isFinite(ts) ? Math.max(0, now - ts) : null;
|
|
191
|
+
const progressAgeMs = age(lastProgressAt);
|
|
192
|
+
const outputAgeMs = age(lastOutputAt);
|
|
193
|
+
const activity = evidence.activity || (executionState === 'NO_EXECUTION' ? 'NO_EXECUTION'
|
|
194
|
+
: executionState !== 'EXECUTING' ? 'NOT_OBSERVABLE'
|
|
195
|
+
: progressAgeMs === null ? 'NOT_OBSERVABLE' : progressAgeMs >= idleMs ? 'NO_OBSERVED_PROGRESS'
|
|
196
|
+
: outputAgeMs !== null && outputAgeMs >= idleMs ? 'NO_OUTPUT' : 'RECENT_PROGRESS');
|
|
197
|
+
return { schemaVersion: 1, capturedAt: new Date(now).toISOString(), verdict: 'INSUFFICIENT_EVIDENCE', activity,
|
|
198
|
+
executionState, progressAgeMs, outputAgeMs, ...evidence };
|
|
199
|
+
}
|
|
@@ -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)) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: bug-debugger
|
|
3
3
|
description: "Debugging specialist for reproducible errors, failing tests, and unexpected behavior. Use proactively when investigation will involve noisy logs, stack traces, or a self-contained reproduce-trace-fix-verify loop. Do not use for trivial obvious fixes or broad architecture ideation."
|
|
4
4
|
model: sonnet # unic-code
|
|
5
|
+
effort: medium
|
|
5
6
|
color: red
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: code-reviewer
|
|
3
3
|
description: "Independent reviewer for handoff Phase 3 and for spec/plan documents. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found."
|
|
4
4
|
model: opus # unic-smart
|
|
5
|
+
effort: medium
|
|
5
6
|
color: yellow
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: feature-implementer
|
|
3
3
|
description: "Implementation specialist for clear, self-contained coding tasks. Use proactively when the approach is clear and the work can be completed in a bounded pass with a concise summary. Do not use for trivial one-file tweaks or tightly coupled exploratory work."
|
|
4
4
|
model: sonnet # unic-code
|
|
5
|
+
effort: medium
|
|
5
6
|
color: green
|
|
6
7
|
tools: ["Read", "Edit", "Write", "Grep", "Glob", "Bash", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: handoff-planner
|
|
3
3
|
description: "Handoff planning specialist for Phase 1+2. Use when creating a new handoff cycle: writes PLAN.md with full test plan, splits into TASK-xxx.md files with TDD-embedded test cases, and updates INDEX.md. Always use the strongest available model (Opus/unic-smart)."
|
|
4
4
|
model: opus # unic-smart
|
|
5
|
+
effort: medium
|
|
5
6
|
color: purple
|
|
6
7
|
tools: ["Read", "Edit", "Write", "Glob", "Bash", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: ukit-small-task-maintainer
|
|
3
3
|
description: "Internal UKit maintenance subagent for low-risk, reversible UKit decisions. Use proactively when UKit needs to decide or perform safe cleanup such as pruning docs/TASKS.md, classifying queued work, choosing whether compact/summarization is appropriate, summarizing local docs/status, or maintaining small UKit runtime queues. Do not use for product implementation, security, release/publish, data-loss, architecture, or risky/shared code changes."
|
|
4
4
|
model: haiku # unic-lite
|
|
5
|
+
effort: low
|
|
5
6
|
color: cyan
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Edit", "Write"]
|
|
7
8
|
---
|
|
@@ -85,7 +85,7 @@ fi
|
|
|
85
85
|
# (coordinator deadline + margin); a forced kill lands on the existing crash-streak
|
|
86
86
|
# path (non-zero STATUS) so the outcome is announced, never silently released.
|
|
87
87
|
__ukit_coord_out="$(mktemp "${TMPDIR:-/tmp}/ukit-coord-out.XXXXXX")"
|
|
88
|
-
printf '%s' "$INPUT" | UKIT_HARNESS=claude-code UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT" --evaluate-stop > "$__ukit_coord_out" 2>/dev/null &
|
|
88
|
+
printf '%s' "$INPUT" | UKIT_HARNESS="${UKIT_HARNESS:-claude-code}" UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT" --evaluate-stop > "$__ukit_coord_out" 2>/dev/null &
|
|
89
89
|
__ukit_coord_pid=$!
|
|
90
90
|
__ukit_coord_ms="${UKIT_HOOK_DEADLINE_MS:-3000}"
|
|
91
91
|
case "$__ukit_coord_ms" in ''|*[!0-9]*) __ukit_coord_ms=3000 ;; esac
|