@ngockhoale/ukit 3.4.44 → 3.4.45
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 +9 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +18 -3
- package/src/index/taskRouting.js +22 -4
- package/template_project/.claude/hooks/handoff-model-guard.sh +1 -1
- package/template_project/.claude/hooks/skill-router.sh +41 -8
- package/template_project/.claude/ukit/index/route-resolver.mjs +18 -3
- package/template_project/.claude/ukit/index/route-task.mjs +23 -5
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +67 -3
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +201 -48
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,15 @@ All notable changes to UKit are documented here.
|
|
|
9
9
|
- New `/ukit:ci-check` command + `ci-check.mjs` helper (core pack): prechecks, fail-closed budget guard, local preflight, at most one dispatch per invocation, write-ahead run-id state, `status` (never dispatches), `--ref`, `--dry-run`, per-OS report. Never pushes, merges or publishes.
|
|
10
10
|
- Default-branch bootstrap stub documented in `docs/design/CI-CROSS-PLATFORM-MATRIX.md` (requires separate approval).
|
|
11
11
|
|
|
12
|
+
## 3.4.45 - 2026-10-17
|
|
13
|
+
|
|
14
|
+
**P1 correctness repair: implementation prompts, release identity, large-transcript intent, continuation ownership.**
|
|
15
|
+
- Implementation prompts ("fix/apply ...", Vietnamese "van sua" defer-strip) are no longer misread as READ_ONLY; negation scope is bounded and covered by a classification matrix built from a real prompt fixture.
|
|
16
|
+
- Completion Gate release identity is decided by tag + shipped-file state, not HEAD==tag alone: a docs-only HEAD ahead of the tag passes; uncommitted/untracked shipped files, or a missing tag in a tagging repo, still require `release-publication`.
|
|
17
|
+
- Transcripts above 32 MiB use one shared incremental intent scan (project-local cache under `.ukit/storage/cache/handoff-intent`, never written into `template_project`), wired into the Claude guard, OMP bridge and Stop coordinator. The first inconclusive call is a visible advisory, never a silent allow; append, truncation, rotation and corrupt/forged cache never create false authorization. Bounded-scan limits are unchanged.
|
|
18
|
+
- Continuation phrases ("tiếp tục", "continue", "go on", ...) inherit only the active task instance's contract; completed, blocked, read-only and cross-session tasks do not leak authority or evidence.
|
|
19
|
+
- Known limits: live OMP and Windows/Linux not exercised; verified on macOS/Node 22.
|
|
20
|
+
|
|
12
21
|
## 3.4.44 - 2026-10-14
|
|
13
22
|
|
|
14
23
|
**Completion Gate scope and permission-choice preservation.**
|
package/package.json
CHANGED
|
@@ -190,14 +190,29 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// Task semantics are independent of source execution strategy and observed progress.
|
|
193
|
+
// P1-A (forensic false block): the clause splitter below cuts at commas, so a negation only reached the
|
|
194
|
+
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
195
|
+
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
196
|
+
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
197
|
+
// Vietnamese negators stay clause-local. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
198
|
+
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
199
|
+
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';
|
|
200
|
+
function neutralizeNonOrders(text) {
|
|
201
|
+
return String(text)
|
|
202
|
+
.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
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
204
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
205
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
194
|
-
const text = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
209
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase());
|
|
195
210
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
196
211
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
197
212
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
198
|
-
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
213
|
+
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
199
214
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
200
|
-
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
215
|
+
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
201
216
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
202
217
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
203
218
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
package/src/index/taskRouting.js
CHANGED
|
@@ -1103,8 +1103,17 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
1103
1103
|
const residue = trimmed
|
|
1104
1104
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
1105
1105
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
1106
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
1106
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
1107
1107
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
1108
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
1109
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
1110
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
1111
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
1112
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1113
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1114
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
1115
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
1116
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
1108
1117
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
1109
1118
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
1110
1119
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -1125,7 +1134,7 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
1125
1134
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
1126
1135
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
1127
1136
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
1128
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
1137
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
1129
1138
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
1130
1139
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
1131
1140
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -1216,11 +1225,20 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
1216
1225
|
const residue = trimmed
|
|
1217
1226
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
1218
1227
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
1219
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
1228
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
1220
1229
|
// Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
|
|
1221
1230
|
// something already done. Only the verb is stripped, so a later imperative still survives.
|
|
1222
1231
|
.replace(/(?<![a-z0-9_])(?:da|vua)\s+(?:duoc\s+)?(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![a-z0-9_])/g, ' ')
|
|
1223
1232
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
1233
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
1234
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
1235
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
1236
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
1237
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1238
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1239
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
1240
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
1241
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
1224
1242
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
1225
1243
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
1226
1244
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -1252,7 +1270,7 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
1252
1270
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
1253
1271
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
1254
1272
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
1255
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
1273
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
1256
1274
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
1257
1275
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
1258
1276
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -668,7 +668,7 @@ async function scanTranscript() {
|
|
|
668
668
|
if (readable) {
|
|
669
669
|
try {
|
|
670
670
|
const mod = await import(require('url').pathToFileURL(path.resolve(process.env.HOOK_DIR || '.', '../ukit/runtime/handoff-intent.mjs')).href);
|
|
671
|
-
scan = await (mod.transcriptHandoffIntentInherited || mod.transcriptHandoffIntent)(transcriptPath);
|
|
671
|
+
scan = await (mod.transcriptHandoffIntentInherited || mod.transcriptHandoffIntent)(transcriptPath, { cacheDir: path.join(projectRoot, '.ukit/storage/cache/handoff-intent') });
|
|
672
672
|
} catch {
|
|
673
673
|
scan = { intent: null, known: false };
|
|
674
674
|
}
|
|
@@ -1814,14 +1814,29 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1814
1814
|
}
|
|
1815
1815
|
|
|
1816
1816
|
// Task semantics are independent of source execution strategy and observed progress.
|
|
1817
|
+
// P1-A (forensic false block): the clause splitter below cuts at commas, so a negation only reached the
|
|
1818
|
+
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
1819
|
+
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
1820
|
+
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
1821
|
+
// Vietnamese negators stay clause-local. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1822
|
+
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1823
|
+
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';
|
|
1824
|
+
function neutralizeNonOrders(text) {
|
|
1825
|
+
return String(text)
|
|
1826
|
+
.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'), ' ')
|
|
1827
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1828
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1829
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1817
1832
|
function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1818
|
-
const text = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
1833
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase());
|
|
1819
1834
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1820
1835
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1821
1836
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1822
|
-
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1837
|
+
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1823
1838
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1824
|
-
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1839
|
+
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1825
1840
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
1826
1841
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
1827
1842
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
|
@@ -2207,8 +2222,17 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2207
2222
|
const residue = trimmed
|
|
2208
2223
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
2209
2224
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
2210
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
2225
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
2211
2226
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
2227
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
2228
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
2229
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
2230
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
2231
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
2232
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
2233
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
2234
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
2235
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
2212
2236
|
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
2213
2237
|
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
2214
2238
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
@@ -2231,7 +2255,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2231
2255
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
2232
2256
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
2233
2257
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
2234
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
2258
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
2235
2259
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
2236
2260
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
2237
2261
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -2269,11 +2293,20 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2269
2293
|
const residue = trimmed
|
|
2270
2294
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
2271
2295
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
2272
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
2296
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
2273
2297
|
// Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
|
|
2274
2298
|
// something already done. Only the verb is stripped, so a later imperative still survives.
|
|
2275
2299
|
.replace(/(?<![a-z0-9_])(?:da|vua)\s+(?:duoc\s+)?(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![a-z0-9_])/g, ' ')
|
|
2276
2300
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
2301
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
2302
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
2303
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
2304
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
2305
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
2306
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
2307
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
2308
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
2309
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
2277
2310
|
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
2278
2311
|
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
2279
2312
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
@@ -2307,7 +2340,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2307
2340
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
2308
2341
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
2309
2342
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
2310
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
2343
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
2311
2344
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
2312
2345
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
2313
2346
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -3783,7 +3816,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3783
3816
|
let previous = sessionId
|
|
3784
3817
|
? [storedPrevious, sidecarPrevious].find((state) => String(state?.sessionId || '').trim() === sessionId) || {}
|
|
3785
3818
|
: storedPrevious;
|
|
3786
|
-
const continuationPrompt = /^(?:continue|resume|keep going|proceed|tiep tuc)(?:\s+(?:the
|
|
3819
|
+
const continuationPrompt = /^(?:continue|resume|keep going|go on|proceed|tiep tuc|lam tiep)(?:\s+(?:with\s+)?(?:the\s+|this\s+)?(?:previous\s+|last\s+|current\s+|same\s+)?(?:implementation\s+)?(?:task|work))?(?:\s+(?:thoi|di|đi|nhe|nao|nua|a|ha|please|pls|now|then))*\s*[.!]?$/i.test(
|
|
3787
3820
|
extractPrompt(payload).normalize('NFD').replace(/[̀-ͯ]/g, '').trim());
|
|
3788
3821
|
// Restore a contract parked by an informational turn: only for the SAME session, and only
|
|
3789
3822
|
// on an explicit continuation or a mutating tool event (never a fresh prompt, which routes on its own,
|
|
@@ -1086,14 +1086,29 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
1088
1088
|
// Task semantics are independent of source execution strategy and observed progress.
|
|
1089
|
+
// P1-A (forensic false block): the clause splitter below cuts at commas, so a negation only reached the
|
|
1090
|
+
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
1091
|
+
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
1092
|
+
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
1093
|
+
// Vietnamese negators stay clause-local. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1094
|
+
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1095
|
+
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';
|
|
1096
|
+
function neutralizeNonOrders(text) {
|
|
1097
|
+
return String(text)
|
|
1098
|
+
.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'), ' ')
|
|
1099
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1100
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1101
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1089
1104
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1090
|
-
const text = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
1105
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase());
|
|
1091
1106
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1092
1107
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1093
1108
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1094
|
-
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1109
|
+
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1095
1110
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1096
|
-
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1111
|
+
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1097
1112
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
1098
1113
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
1099
1114
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
|
@@ -1167,7 +1167,7 @@ async function main() {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
|
|
1169
1169
|
const continuingTask = Boolean(previousState.taskInstanceId && lastPrompt
|
|
1170
|
-
&& /^(?:continue|resume|keep going|proceed|tiep tuc)(?:\s+(?:the
|
|
1170
|
+
&& /^(?:continue|resume|keep going|go on|proceed|tiep tuc|lam tiep)(?:\s+(?:with\s+)?(?:the\s+|this\s+)?(?:previous\s+|last\s+|current\s+|same\s+)?(?:implementation\s+)?(?:task|work))?(?:\s+(?:thoi|di|đi|nhe|nao|nua|a|ha|please|pls|now|then))*\s*[.!]?$/i.test(
|
|
1171
1171
|
promptText.normalize('NFD').replace(/[̀-ͯ]/g, '').trim()));
|
|
1172
1172
|
const taskInstanceId = readFlagValue(args, '--task-instance-id')
|
|
1173
1173
|
|| (((['skill-router', 'skill-router-provisional'].includes(previousState.source)
|
|
@@ -4431,8 +4431,17 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
4431
4431
|
const residue = trimmed
|
|
4432
4432
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
4433
4433
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
4434
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
4434
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
4435
4435
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
4436
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
4437
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
4438
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
4439
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
4440
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
4441
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
4442
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
4443
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
4444
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
4436
4445
|
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
4437
4446
|
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
4438
4447
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
@@ -4455,7 +4464,7 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
4455
4464
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
4456
4465
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
4457
4466
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
4458
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4467
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4459
4468
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
4460
4469
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
4461
4470
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -4547,11 +4556,20 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
4547
4556
|
const residue = trimmed
|
|
4548
4557
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
4549
4558
|
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
4550
|
-
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
4559
|
+
.replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
|
|
4551
4560
|
// Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
|
|
4552
4561
|
// something already done. Only the verb is stripped, so a later imperative still survives.
|
|
4553
4562
|
.replace(/(?<![a-z0-9_])(?:da|vua)\s+(?:duoc\s+)?(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![a-z0-9_])/g, ' ')
|
|
4554
4563
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
4564
|
+
// P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
|
|
4565
|
+
// advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
|
|
4566
|
+
// answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
|
|
4567
|
+
// commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
|
|
4568
|
+
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
4569
|
+
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
4570
|
+
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
|
|
4571
|
+
.replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
|
|
4572
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
4555
4573
|
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
4556
4574
|
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
4557
4575
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
@@ -4585,7 +4603,7 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
4585
4603
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
4586
4604
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
4587
4605
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
4588
|
-
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4606
|
+
.replace(new RegExp(`\\b(?<!\\bvan\\s)(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4589
4607
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
4590
4608
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
4591
4609
|
.replace(/\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi|can|la)\b[^.,;\n]*/g, ' ')
|
|
@@ -3315,7 +3315,8 @@ function effectiveRequestLedger(state = {}, ledger = {}) {
|
|
|
3315
3315
|
// Release postcondition. A request that ORDERS `npm publish` is complete only when trusted receipts
|
|
3316
3316
|
// (real Bash results recorded by the adapter - never agent prose) show the registry serving exactly the
|
|
3317
3317
|
// package.json version, and the local Git state agrees: a local release tag, if present, must DEREFERENCE
|
|
3318
|
-
// (`^{commit}`, never the annotated tag-object SHA) to HEAD
|
|
3318
|
+
// (`^{commit}`, never the annotated tag-object SHA) to HEAD, or to an ancestor of HEAD with no shipped change
|
|
3319
|
+
// since (publishedReleaseDrifted). A failed/missing publish with no registry proof
|
|
3319
3320
|
// stays missing. A registry that already serves the version counts as published, so a finished release is
|
|
3320
3321
|
// never re-published just to satisfy the gate.
|
|
3321
3322
|
function promptOrdersNpmPublish(state = {}) {
|
|
@@ -3339,10 +3340,73 @@ function releasePublicationMissing(state, ledger, cwd) {
|
|
|
3339
3340
|
if (view?.operation?.state !== wanted) return 'release-publication';
|
|
3340
3341
|
const tagCommit = gitOutput(['rev-parse', '--verify', '--quiet', `v${pkg.version}^{commit}`], cwd);
|
|
3341
3342
|
const head = gitOutput(['rev-parse', 'HEAD'], cwd);
|
|
3342
|
-
|
|
3343
|
+
// A repository that tags its releases (any v<semver> tag exists) must have tagged THIS version: without the
|
|
3344
|
+
// tag there is no immutable identity to tie the published artifact to. A repository that never tags keeps
|
|
3345
|
+
// the registry receipt as its only proof (the pre-P1 behaviour) instead of being blocked forever.
|
|
3346
|
+
if (!tagCommit && head && repoTagsReleases(cwd)) return 'release-publication';
|
|
3347
|
+
if (tagCommit && head && (tagCommit !== head || workingTreeShippedChange(pkg, tagCommit, cwd)) && publishedReleaseDrifted(pkg, tagCommit, head, cwd)) return 'release-publication';
|
|
3343
3348
|
return null;
|
|
3344
3349
|
}
|
|
3345
3350
|
|
|
3351
|
+
// P1-B: the published release is identified by the IMMUTABLE tag commit, not by the moving HEAD. A later
|
|
3352
|
+
// commit must not invalidate a verified publication, but HEAD may only be ahead of the tag when (a) the tag
|
|
3353
|
+
// commit is an ancestor of HEAD, (b) package.json AT THE TAG carries the published version, and (c) nothing
|
|
3354
|
+
// the package ships changed between tag and HEAD (docs/tests/ledger-only follow-ups are fine; unshipped
|
|
3355
|
+
// source is not). Every git failure is fail-closed: unknown means drifted.
|
|
3356
|
+
function gitSucceeds(args, cwd) {
|
|
3357
|
+
try {
|
|
3358
|
+
execFileSync('git', args, { cwd, stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 });
|
|
3359
|
+
return true;
|
|
3360
|
+
} catch { return false; }
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function packageFilePattern(entry) {
|
|
3364
|
+
const negated = entry.startsWith('!');
|
|
3365
|
+
let body = (negated ? entry.slice(1) : entry).replace(/^\.?\//, '');
|
|
3366
|
+
const dir = body.endsWith('/');
|
|
3367
|
+
if (dir) body = body.slice(0, -1);
|
|
3368
|
+
const source = body.split('**').map((part) => part.split('*')
|
|
3369
|
+
.map((seg) => seg.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')).join('.*');
|
|
3370
|
+
return { negated, regex: new RegExp(`^${source}(?:/.*)?$`) };
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
function shippedByPackage(pkg, file) {
|
|
3374
|
+
if (/^(?:package\.json|readme(?:\.[a-z]+)?|license(?:\.[a-z]+)?|licence(?:\.[a-z]+)?|changelog(?:\.[a-z]+)?)$/i.test(file)) return true;
|
|
3375
|
+
if (!Array.isArray(pkg?.files)) return true; // no allow-list: npm ships everything, so any change is shipped code
|
|
3376
|
+
const patterns = pkg.files.filter((entry) => typeof entry === 'string' && entry.trim()).map(packageFilePattern);
|
|
3377
|
+
const included = patterns.some((p) => !p.negated && p.regex.test(file));
|
|
3378
|
+
return included && !patterns.some((p) => p.negated && p.regex.test(file));
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function publishedReleaseDrifted(pkg, tagCommit, head, cwd) {
|
|
3382
|
+
if (!gitSucceeds(['merge-base', '--is-ancestor', tagCommit, head], cwd)) return true;
|
|
3383
|
+
let taggedVersion = null;
|
|
3384
|
+
try {
|
|
3385
|
+
taggedVersion = JSON.parse(gitOutput(['show', `${tagCommit}:package.json`], cwd) || 'null')?.version ?? null;
|
|
3386
|
+
} catch { return true; }
|
|
3387
|
+
if (taggedVersion !== pkg.version) return true;
|
|
3388
|
+
let changed = null;
|
|
3389
|
+
try {
|
|
3390
|
+
changed = execFileSync('git', ['diff', '--name-only', '-z', tagCommit, head], { cwd, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
3391
|
+
} catch { return true; }
|
|
3392
|
+
if (String(changed).split('\0').filter(Boolean).some((file) => shippedByPackage(pkg, file))) return true;
|
|
3393
|
+
return workingTreeShippedChange(pkg, tagCommit, cwd);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function repoTagsReleases(cwd) {
|
|
3397
|
+
const tags = gitOutput(['tag', '--list', 'v[0-9]*'], cwd);
|
|
3398
|
+
return Boolean(tags && tags.trim());
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// Uncommitted edits (tracked or untracked) to shipped files are not the published artifact either.
|
|
3402
|
+
function workingTreeShippedChange(pkg, tagCommit, cwd) {
|
|
3403
|
+
try {
|
|
3404
|
+
const tracked = execFileSync('git', ['diff', '--name-only', '-z', tagCommit], { cwd, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
3405
|
+
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
3406
|
+
return `${tracked}${untracked}`.split('\0').filter(Boolean).some((file) => shippedByPackage(pkg, file));
|
|
3407
|
+
} catch { return true; }
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3346
3410
|
function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null } = {}) {
|
|
3347
3411
|
const routeSummary = state?.routeSummary || {};
|
|
3348
3412
|
// Explicit vibecode autonomy: the user asked UKit to run one prompt to a finished result,
|
|
@@ -3667,7 +3731,7 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3667
3731
|
|
|
3668
3732
|
const finalAttempt = !vibecode && continuationCount === MAX_CONTINUATIONS - 1;
|
|
3669
3733
|
const instruction = missingEvidence.includes('release-publication')
|
|
3670
|
-
? 'The release is not proven published: run `npm publish` once if the registry does not yet serve this version, then confirm with `npm view <package> version` (must equal package.json) and that the release tag dereferences to HEAD. Do not republish a version the registry already serves.'
|
|
3734
|
+
? 'The release is not proven published: run `npm publish` once if the registry does not yet serve this version, then confirm with `npm view <package> version` (must equal package.json) and that the release tag dereferences to HEAD (or to an ancestor of HEAD with no shipped change since). Never move the tag. Do not republish a version the registry already serves.'
|
|
3671
3735
|
: operationalTask
|
|
3672
3736
|
? 'Inspect native mutation receipts and verify the requested operational state; do not fabricate source edits or source-test evidence.'
|
|
3673
3737
|
: recoveryInstruction(missingEvidence, effectiveLedger, routeSummary, { cwd });
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
import fs from 'node:fs/promises';
|
|
39
39
|
import fsSync from 'node:fs';
|
|
40
40
|
import path from 'node:path';
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
41
43
|
import { spawnSync } from 'node:child_process';
|
|
42
44
|
|
|
43
45
|
// The `(?![\w-])` closer is the exact token boundary: a command name followed by
|
|
@@ -273,15 +275,164 @@ export function classifyHandoffLine(line) {
|
|
|
273
275
|
}
|
|
274
276
|
}
|
|
275
277
|
|
|
278
|
+
const SCAN_CHUNK = 4 * 1024 * 1024;
|
|
279
|
+
const INTENT_CACHE_VERSION = 1;
|
|
280
|
+
const RUNTIME_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
281
|
+
// The shipped template tree must never grow runtime state: only an installed project gets a default cache.
|
|
282
|
+
const DEFAULT_INTENT_CACHE_DIR = path.basename(RUNTIME_ROOT) === 'template_project' ? null : path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '.ukit', 'storage', 'cache', 'handoff-intent');
|
|
283
|
+
const EVIDENCE_MARKERS = ['handoff-create', 'handoff-fullstack', 'handoff-implement', 'handoff-review', 'handoff-status', 'handoff-clear', 'HANDOFF RESUME'];
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Walk the byte range [lo, hi) NEWEST line first, in bounded chunks, and stop at the
|
|
287
|
+
* first line that carries a handoff intent. `hi` must be EOF or a line boundary. `lo`
|
|
288
|
+
* is a line boundary only when `loIsBoundary`; otherwise the fragment cut by `lo` is
|
|
289
|
+
* not a whole entry and is dropped. `reached` is the lowest absolute byte offset up to
|
|
290
|
+
* which every line was fully examined; `lastNL` the highest newline seen (so callers
|
|
291
|
+
* can persist a line-aligned offset and never cache a half-written trailing record).
|
|
292
|
+
* @returns {{status:'found'|'done'|'torn'|'timeout', intent?:string, evidence?:{pos:number,len:number}, reached:number, lastNL:number}}
|
|
293
|
+
*/
|
|
294
|
+
async function scanBack(handle, hi, lo, loIsBoundary, deadline) {
|
|
295
|
+
let pos = hi;
|
|
296
|
+
let carry = Buffer.alloc(0);
|
|
297
|
+
let lastNL = -1;
|
|
298
|
+
let reached = hi;
|
|
299
|
+
while (pos > lo) {
|
|
300
|
+
if (Date.now() > deadline) return { status: 'timeout', reached, lastNL };
|
|
301
|
+
const start = Math.max(lo, pos - SCAN_CHUNK);
|
|
302
|
+
const chunk = Buffer.allocUnsafe(pos - start);
|
|
303
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, start);
|
|
304
|
+
if (bytesRead !== chunk.length) return { status: 'timeout', reached, lastNL };
|
|
305
|
+
const buf = carry.length ? Buffer.concat([chunk, carry]) : chunk;
|
|
306
|
+
if (lastNL < 0) {
|
|
307
|
+
const i = buf.lastIndexOf(10);
|
|
308
|
+
if (i >= 0) lastNL = start + i;
|
|
309
|
+
}
|
|
310
|
+
let base = start;
|
|
311
|
+
let region = buf;
|
|
312
|
+
if (start > 0 && !(loIsBoundary && start === lo)) {
|
|
313
|
+
// This chunk begins mid-line: the head fragment belongs to the next lower chunk.
|
|
314
|
+
const nl = buf.indexOf(10);
|
|
315
|
+
if (nl < 0) { carry = buf; pos = start; continue; }
|
|
316
|
+
carry = buf.subarray(0, nl);
|
|
317
|
+
region = buf.subarray(nl + 1);
|
|
318
|
+
base = start + nl + 1;
|
|
319
|
+
reached = base;
|
|
320
|
+
} else {
|
|
321
|
+
carry = Buffer.alloc(0);
|
|
322
|
+
reached = start;
|
|
323
|
+
}
|
|
324
|
+
let end = region.length;
|
|
325
|
+
for (;;) {
|
|
326
|
+
const i = end > 0 ? region.lastIndexOf(10, end - 1) : -1;
|
|
327
|
+
const lineBuf = region.subarray(i + 1, end);
|
|
328
|
+
if (lineBuf.length) {
|
|
329
|
+
const line = lineBuf.toString('utf8');
|
|
330
|
+
// Cheap prefilter — skip JSON.parse for lines that cannot be evidence.
|
|
331
|
+
if (EVIDENCE_MARKERS.some((m) => line.includes(m))) {
|
|
332
|
+
// This is the NEWEST line carrying intent evidence. A host append can be
|
|
333
|
+
// cut mid-record (killed turn, torn write), leaving it unparseable. Skipping
|
|
334
|
+
// it would fall through to an OLDER — possibly OPPOSITE — intent (a torn
|
|
335
|
+
// `handoff-implement` after a create released a real fullstack run). An
|
|
336
|
+
// incomplete newest evidence-bearing record is UNDETERMINED, not "no
|
|
337
|
+
// intent": the caller stays fail-closed (MODULE02-SPEC FR-M2-002).
|
|
338
|
+
let entry;
|
|
339
|
+
try { entry = JSON.parse(line); } catch { return { status: 'torn', reached, lastNL }; }
|
|
340
|
+
const intent = classifyHandoffEntry(entry);
|
|
341
|
+
if (intent) return { status: 'found', intent, evidence: { pos: base + i + 1, len: lineBuf.length }, reached, lastNL };
|
|
342
|
+
// Complete record with no recognised intent (tool output, foreign envelope):
|
|
343
|
+
// not evidence — keep walking older lines.
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (i < 0) break;
|
|
347
|
+
end = i;
|
|
348
|
+
}
|
|
349
|
+
pos = start;
|
|
350
|
+
}
|
|
351
|
+
return { status: 'done', reached, lastNL };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fp = (buf) => createHash('sha1').update(buf).digest('hex');
|
|
355
|
+
|
|
356
|
+
async function readBytes(handle, pos, len) {
|
|
357
|
+
const buf = Buffer.allocUnsafe(len);
|
|
358
|
+
const { bytesRead } = await handle.read(buf, 0, len, pos);
|
|
359
|
+
return bytesRead === len ? buf : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Load the per-transcript incremental state and prove it still describes THIS file:
|
|
364
|
+
* same head bytes (rotation / session reuse), same bytes just below the cached offset
|
|
365
|
+
* (in-place rewrite), size not below the offset (truncation), and any cached intent
|
|
366
|
+
* line re-read and re-classified. Anything malformed, stale or unverifiable returns
|
|
367
|
+
* null so the caller re-scans from scratch — a bad cache can only cost time, and can
|
|
368
|
+
* never turn an inconclusive scan into a conclusive one.
|
|
369
|
+
*/
|
|
370
|
+
async function loadIntentCache(file, handle, size, maxBytes) {
|
|
371
|
+
try {
|
|
372
|
+
const c = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
373
|
+
if (!c || c.v !== INTENT_CACHE_VERSION) return null;
|
|
374
|
+
const ints = [c.offset, c.coveredFrom, c.headLen, c.tailLen];
|
|
375
|
+
if (!ints.every((n) => Number.isSafeInteger(n) && n >= 0)) return null;
|
|
376
|
+
if (c.offset > size || size - c.offset > maxBytes || c.coveredFrom > c.offset) return null;
|
|
377
|
+
if (c.headLen > Math.min(size, 4096) || c.tailLen > c.offset) return null;
|
|
378
|
+
const head = await readBytes(handle, 0, c.headLen);
|
|
379
|
+
if (!head || fp(head) !== c.headFp) return null;
|
|
380
|
+
const tail = await readBytes(handle, c.offset - c.tailLen, c.tailLen);
|
|
381
|
+
if (!tail || fp(tail) !== c.tailFp) return null;
|
|
382
|
+
let intent = null;
|
|
383
|
+
let evidence = null;
|
|
384
|
+
if (c.intent) {
|
|
385
|
+
const ev = c.evidence;
|
|
386
|
+
if (!ev || !Number.isSafeInteger(ev.pos) || !Number.isSafeInteger(ev.len) || ev.pos < c.coveredFrom || ev.pos + ev.len >= c.offset) return null;
|
|
387
|
+
const raw = await readBytes(handle, ev.pos, ev.len);
|
|
388
|
+
if (!raw || classifyHandoffLine(raw.toString('utf8')) !== c.intent) return null;
|
|
389
|
+
intent = c.intent;
|
|
390
|
+
evidence = { pos: ev.pos, len: ev.len };
|
|
391
|
+
}
|
|
392
|
+
return { offset: c.offset, coveredFrom: c.coveredFrom, intent, evidence };
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function saveIntentCache(file, handle, st) {
|
|
399
|
+
try {
|
|
400
|
+
if (!(st.coveredFrom <= st.offset)) return;
|
|
401
|
+
const headLen = Math.min(4096, st.offset);
|
|
402
|
+
const tailLen = Math.min(256, st.offset);
|
|
403
|
+
const head = await readBytes(handle, 0, headLen);
|
|
404
|
+
const tail = await readBytes(handle, st.offset - tailLen, tailLen);
|
|
405
|
+
if (!head || !tail) return;
|
|
406
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
407
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
408
|
+
await fs.writeFile(tmp, JSON.stringify({
|
|
409
|
+
v: INTENT_CACHE_VERSION, offset: st.offset, coveredFrom: st.coveredFrom,
|
|
410
|
+
headLen, headFp: fp(head), tailLen, tailFp: fp(tail),
|
|
411
|
+
intent: st.intent || null, evidence: st.intent ? st.evidence : null,
|
|
412
|
+
}), 'utf8');
|
|
413
|
+
await fs.rename(tmp, file);
|
|
414
|
+
} catch { /* best effort: the next call just re-scans */ }
|
|
415
|
+
}
|
|
416
|
+
|
|
276
417
|
/**
|
|
277
418
|
* Read a session transcript and return its MOST RECENT handoff intent.
|
|
419
|
+
*
|
|
420
|
+
* Bounded per call: at most `maxBytes` examined within `budgetMs`. When `cacheDir` is
|
|
421
|
+
* given, progress is kept per transcript as a line-aligned offset + the newest intent
|
|
422
|
+
* line found + the lowest byte covered, so later calls scan only the appended bytes and
|
|
423
|
+
* (for a transcript larger than `maxBytes`) backfill one more window below the covered
|
|
424
|
+
* region until the whole file is proven to hold no intent. Hooks never rescan the whole
|
|
425
|
+
* transcript, yet an oversized session still becomes conclusive. Without `cacheDir` the
|
|
426
|
+
* scan is stateless (tail window only).
|
|
278
427
|
* @returns {Promise<{ intent: 'fullstack'|'create'|null, known: boolean }>}
|
|
279
428
|
* `known=false` means the scan could not be completed (no path, unreadable,
|
|
280
|
-
* budget overrun
|
|
429
|
+
* budget overrun, torn newest evidence, region not yet covered); callers keep
|
|
430
|
+
* their existing blocking behaviour there.
|
|
281
431
|
*/
|
|
282
432
|
export async function transcriptHandoffIntent(transcriptPath, {
|
|
283
433
|
budgetMs = INTENT_SCAN_BUDGET_MS,
|
|
284
434
|
maxBytes = INTENT_SCAN_MAX_BYTES,
|
|
435
|
+
cacheDir = undefined,
|
|
285
436
|
} = {}) {
|
|
286
437
|
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) {
|
|
287
438
|
return { intent: null, known: false };
|
|
@@ -289,58 +440,60 @@ export async function transcriptHandoffIntent(transcriptPath, {
|
|
|
289
440
|
let handle;
|
|
290
441
|
try {
|
|
291
442
|
handle = await fs.open(transcriptPath, 'r');
|
|
292
|
-
const
|
|
293
|
-
const size = Number(stat.size) || 0;
|
|
294
|
-
const start = Math.max(0, size - maxBytes);
|
|
443
|
+
const size = Number((await handle.stat()).size) || 0;
|
|
295
444
|
const deadline = Date.now() + budgetMs;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
445
|
+
// One shared mechanism for every caller (Claude hook, omp bridge, Stop coordinator):
|
|
446
|
+
// an oversized transcript gets the incremental cache by default, rooted at this
|
|
447
|
+
// runtime's own project (<root>/.claude/ukit/runtime -> <root>/.ukit/storage/cache).
|
|
448
|
+
// `cacheDir:false|null` forces a stateless scan; small transcripts never need one.
|
|
449
|
+
if (cacheDir === undefined) cacheDir = size > maxBytes ? (process.env.UKIT_INTENT_CACHE_DIR || DEFAULT_INTENT_CACHE_DIR) : null;
|
|
450
|
+
const cacheFile = cacheDir ? path.join(cacheDir, `${fp(Buffer.from(path.resolve(transcriptPath))).slice(0, 24)}.json`) : null;
|
|
451
|
+
const cache = cacheFile ? await loadIntentCache(cacheFile, handle, size, maxBytes) : null;
|
|
452
|
+
|
|
453
|
+
let offset;
|
|
454
|
+
let covered;
|
|
455
|
+
let intent = null;
|
|
456
|
+
let evidence = null;
|
|
457
|
+
let persist = false;
|
|
458
|
+
// Step A: bytes the cache has not seen (the whole tail window on a cold start).
|
|
459
|
+
const a = await scanBack(handle, size, cache ? cache.offset : Math.max(0, size - maxBytes), Boolean(cache), deadline);
|
|
460
|
+
if (a.status === 'torn') return { intent: null, known: false };
|
|
461
|
+
if (cache) {
|
|
462
|
+
offset = a.lastNL >= 0 ? a.lastNL + 1 : cache.offset;
|
|
463
|
+
covered = cache.coveredFrom;
|
|
464
|
+
if (a.status === 'found') {
|
|
465
|
+
if (a.evidence.pos + a.evidence.len < offset) { intent = a.intent; evidence = a.evidence; persist = true; }
|
|
466
|
+
else intent = a.intent; // still being appended: answer now, cache once the line is complete
|
|
467
|
+
} else if (a.status === 'done') {
|
|
468
|
+
intent = cache.intent; evidence = cache.evidence; persist = offset !== cache.offset;
|
|
469
|
+
} else {
|
|
470
|
+
return { intent: null, known: false };
|
|
318
471
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// their blocking/conservative posture (MODULE02-SPEC FR-M2-002).
|
|
327
|
-
let entry;
|
|
328
|
-
try {
|
|
329
|
-
entry = JSON.parse(line);
|
|
330
|
-
} catch {
|
|
472
|
+
} else {
|
|
473
|
+
offset = a.lastNL >= 0 ? a.lastNL + 1 : size;
|
|
474
|
+
covered = a.status === 'found' ? a.evidence.pos : a.reached;
|
|
475
|
+
if (a.status === 'found') { intent = a.intent; evidence = a.evidence; persist = a.evidence.pos + a.evidence.len < offset; }
|
|
476
|
+
else persist = a.lastNL >= 0 && covered <= offset;
|
|
477
|
+
if (a.status === 'timeout') {
|
|
478
|
+
if (persist && cacheFile) await saveIntentCache(cacheFile, handle, { offset, coveredFrom: covered, intent: null });
|
|
331
479
|
return { intent: null, known: false };
|
|
332
480
|
}
|
|
333
|
-
const intent = classifyHandoffEntry(entry);
|
|
334
|
-
if (intent) return { intent, known: true };
|
|
335
|
-
// Complete record that carries no recognised intent (tool output, a foreign
|
|
336
|
-
// envelope, a malformed-but-whole unrelated line): not evidence — keep
|
|
337
|
-
// walking older lines.
|
|
338
481
|
}
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
482
|
+
// Step B: nothing newer — extend coverage one bounded window further down.
|
|
483
|
+
if (!intent && covered > 0) {
|
|
484
|
+
const room = Math.max(0, maxBytes - (size - (cache ? cache.offset : Math.max(0, size - maxBytes))));
|
|
485
|
+
if (room > 0) {
|
|
486
|
+
const b = await scanBack(handle, covered, Math.max(0, covered - room), false, deadline);
|
|
487
|
+
if (b.status === 'torn') return { intent: null, known: false };
|
|
488
|
+
if (b.status === 'found') { intent = b.intent; evidence = b.evidence; covered = b.evidence.pos; persist = true; }
|
|
489
|
+
else { covered = b.reached; persist = true; }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (persist && cacheFile) await saveIntentCache(cacheFile, handle, { offset, coveredFrom: covered, intent, evidence });
|
|
493
|
+
// Only a scan of the WHOLE transcript proves "no handoff intent"; an uncovered
|
|
494
|
+
// region (the invoking turn usually appears once, near the start of an omp
|
|
495
|
+
// session) stays unknown and callers keep blocking.
|
|
496
|
+
return { intent, known: Boolean(intent) || covered === 0 };
|
|
344
497
|
} catch {
|
|
345
498
|
return { intent: null, known: false };
|
|
346
499
|
} finally {
|