@ngockhoale/ukit 3.4.43 → 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 +19 -0
- package/manifests/documentation.yaml +11 -0
- package/manifests/instructionRules.yaml +4 -1
- 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 +23 -5
- package/template_project/.claude/ukit/index/route-task.mjs +23 -5
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +99 -12
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +201 -48
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +15 -1
- package/template_project/AGENTS.md +1 -0
- package/template_project/CLAUDE.md +1 -0
- package/template_project/instructions/core.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,25 @@ 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
|
+
|
|
21
|
+
## 3.4.44 - 2026-10-14
|
|
22
|
+
|
|
23
|
+
**Completion Gate scope and permission-choice preservation.**
|
|
24
|
+
- Publish refusals in Vietnamese, including accented/uppercase `chưa`, `không` and `ĐỪNG`, no longer create publication obligations. Real publication requests still require registry evidence and release-tag consistency.
|
|
25
|
+
- Conditional cron cancellation verifies the requested absence using a fresh native observation without inventing successful deletion. Existing in-flight contracts reconcile only the same conditional absence goal; mixed code tasks retain write/verification requirements.
|
|
26
|
+
- Blockers are task-scoped in Stop and unfinished-work detection. Current blockers end explicitly BLOCKED, never COMPLETE; previous-task blockers cannot discharge new-task verification.
|
|
27
|
+
- Preserve the established bypass startup template and explicit user permission choices. Instructions distinguish UKit workflow PLAN from host permission mode; these are preventive boundaries, not a runtime mode lock.
|
|
28
|
+
- **The reported Claude Code extension spontaneous permission-mode switch remains OPEN and is not claimed fixed.** The two VS Code User startup settings configured on the owner's machine are local settings, not distributed by this npm package.
|
|
29
|
+
- Includes the previously unreleased manual-only `/ukit:ci-check` tooling above; this release does not dispatch GitHub Actions.
|
|
30
|
+
|
|
12
31
|
## 3.4.43 - 2026-10-13
|
|
13
32
|
|
|
14
33
|
**Completion Gate: an ordered `git tag` is discharged by the tag itself, so the live release Stop is accepted.**
|
|
@@ -407,6 +407,17 @@ entries:
|
|
|
407
407
|
load_policy: never
|
|
408
408
|
validation: [none]
|
|
409
409
|
archive_policy: immutable
|
|
410
|
+
- id: docs-analysis-claude-permission-mode
|
|
411
|
+
path: docs/analysis/CLAUDE_PERMISSION_MODE.md
|
|
412
|
+
class: canonical
|
|
413
|
+
audience: [maintainer]
|
|
414
|
+
owner: product
|
|
415
|
+
source_of_truth: docs/analysis/CLAUDE_PERMISSION_MODE.md
|
|
416
|
+
merge_strategy: none
|
|
417
|
+
load_policy: on-demand
|
|
418
|
+
validation: [manual]
|
|
419
|
+
archive_policy: owner-decides
|
|
420
|
+
notes: bounded investigation evidence; extension transition remains OPEN
|
|
410
421
|
- id: docs-analysis-subagent-cost-audit
|
|
411
422
|
path: docs/analysis/SUBAGENT-COST-AUDIT.md
|
|
412
423
|
class: archive
|
|
@@ -120,7 +120,10 @@ rules:
|
|
|
120
120
|
Under orchestration.permissionMode: unattended there is no human in the
|
|
121
121
|
loop — the loop is UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX →
|
|
122
122
|
REVERIFY → COMPLETE, and a turn ends only on COMPLETE evidence, a real
|
|
123
|
-
external blocker, or a deny decision.
|
|
123
|
+
external blocker, or a deny decision. Workflow phases do not authorize
|
|
124
|
+
changing the user-owned host permission mode: internal PLAN is not host
|
|
125
|
+
Plan Mode. Honor managed policy, newer user choices, and parent/subagent
|
|
126
|
+
and session boundaries.
|
|
124
127
|
marker_in: [template_project/CLAUDE.md, template_project/AGENTS.md, CLAUDE.md, AGENTS.md]
|
|
125
128
|
anchor: 'no human in the loop'
|
|
126
129
|
- id: UNATTENDED-02
|
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.
|
|
@@ -1121,13 +1136,16 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1121
1136
|
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua)\b/.test(text);
|
|
1122
1137
|
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text) || statusReport)
|
|
1123
1138
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1124
|
-
const taskKind = operation ? (sourceOrder ||
|
|
1139
|
+
const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
|
|
1125
1140
|
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1126
1141
|
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
1127
1142
|
return { taskKind, ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1128
1143
|
...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
|
|
1129
1144
|
...(operation && nativeDomain === 'scheduled-job' ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
|
|
1130
|
-
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
|
|
1145
|
+
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
|
|
1146
|
+
|| actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy)\b/.test(clause)
|
|
1147
|
+
&& /\b(?:if\s+(?:it\s+)?(?:still\s+)?exists|if\s+present|neu\s+(?:van\s+|con\s+)?ton tai)\b/.test(clause))
|
|
1148
|
+
? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
1131
1149
|
}
|
|
1132
1150
|
|
|
1133
1151
|
export function applyTaskSemantics(contract, semantic) {
|
|
@@ -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, ' ')
|
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { withAsyncLock, LOCK_MAX_SLICE_MS } from './async-lock.mjs';
|
|
10
10
|
import { cronOperationReceipt, bashOperationReceipt, releaseOperationReceipt } from './cron-operation-adapter.mjs';
|
|
11
|
-
import { operationCompletionPolicy, foldOperationEvidence, compactOperation } from './operation-evidence.mjs';
|
|
11
|
+
import { operationCompletionPolicy, foldOperationEvidence, compactOperation, conditionalAbsenceGoal } from './operation-evidence.mjs';
|
|
12
12
|
|
|
13
13
|
// Hook-context self-deadline (2.4.1 orphan-leak class): armed ONLY on direct
|
|
14
14
|
// CLI invocation (see isDirectRun below) — never at import time. An import-time
|
|
@@ -348,6 +348,7 @@ function explicitPromptKey(routeState = {}) {
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
export function hasUnfinishedCompletion(state = {}, ledger = {}, { cwd } = {}) {
|
|
351
|
+
ledger = effectiveRequestLedger(state, ledger);
|
|
351
352
|
if (state?.routeSummary?.executionContract?.taskKind) {
|
|
352
353
|
const result = evaluateCompletion({ state, ledger, cwd });
|
|
353
354
|
return result.complete !== true && (result.missingEvidence || []).length > 0 && !ledger.blocker;
|
|
@@ -3268,8 +3269,18 @@ function scopedContract(state, ledger) {
|
|
|
3268
3269
|
const contract = state?.routeSummary?.executionContract;
|
|
3269
3270
|
if (!contract?.operationGoal) return contract;
|
|
3270
3271
|
const session = state?.sessionId || ledger?.sessionId;
|
|
3271
|
-
|
|
3272
|
-
|
|
3272
|
+
let goal = contract.operationGoal;
|
|
3273
|
+
// In-flight contracts retain their original semantics across tool reroutes. Reconcile
|
|
3274
|
+
// the conditional-absence correction only for the same native goal; never change
|
|
3275
|
+
// targets or discard the contract's existing code/verification obligations.
|
|
3276
|
+
if (goal.domain === 'scheduled-job' && goal.desiredState === 'absent'
|
|
3277
|
+
&& goal.completionSemantics === 'execute-and-verify') {
|
|
3278
|
+
if (conditionalAbsenceGoal(state?.routingContext?.lastExplicitUserPromptText, goal.target)) {
|
|
3279
|
+
goal = { ...goal, completionSemantics: 'ensure-state' };
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return { ...contract, operationGoal: { ...goal,
|
|
3283
|
+
scope: goal.scope || (session ? `session:${session}` : null) } };
|
|
3273
3284
|
}
|
|
3274
3285
|
|
|
3275
3286
|
// Gate-time local git truth for the remote-parity proof (cwd-bound, so it lives here, not in the pure evidence module).
|
|
@@ -3304,14 +3315,16 @@ function effectiveRequestLedger(state = {}, ledger = {}) {
|
|
|
3304
3315
|
// Release postcondition. A request that ORDERS `npm publish` is complete only when trusted receipts
|
|
3305
3316
|
// (real Bash results recorded by the adapter - never agent prose) show the registry serving exactly the
|
|
3306
3317
|
// package.json version, and the local Git state agrees: a local release tag, if present, must DEREFERENCE
|
|
3307
|
-
// (`^{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
|
|
3308
3320
|
// stays missing. A registry that already serves the version counts as published, so a finished release is
|
|
3309
3321
|
// never re-published just to satisfy the gate.
|
|
3310
3322
|
function promptOrdersNpmPublish(state = {}) {
|
|
3311
|
-
const text = String(state?.routingContext?.lastExplicitUserPromptText || '')
|
|
3323
|
+
const text = String(state?.routingContext?.lastExplicitUserPromptText || '')
|
|
3324
|
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
3312
3325
|
if (!/\bnpm\s+publish\b/.test(text)) return false;
|
|
3313
3326
|
return text.split(/[\n;.,]|\bthen\b|\band\b/).some((clause) => /\bnpm\s+publish\b/.test(clause)
|
|
3314
|
-
&& !/\b(?:do not|don't|never|without|not yet|dry-run|khong|dung)\b|--dry-run/.test(clause));
|
|
3327
|
+
&& !/\b(?:do not|don't|never|without|not yet|dry-run|khong|dung|chua)\b|--dry-run/.test(clause));
|
|
3315
3328
|
}
|
|
3316
3329
|
|
|
3317
3330
|
function releasePublicationMissing(state, ledger, cwd) {
|
|
@@ -3327,10 +3340,73 @@ function releasePublicationMissing(state, ledger, cwd) {
|
|
|
3327
3340
|
if (view?.operation?.state !== wanted) return 'release-publication';
|
|
3328
3341
|
const tagCommit = gitOutput(['rev-parse', '--verify', '--quiet', `v${pkg.version}^{commit}`], cwd);
|
|
3329
3342
|
const head = gitOutput(['rev-parse', 'HEAD'], cwd);
|
|
3330
|
-
|
|
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';
|
|
3331
3348
|
return null;
|
|
3332
3349
|
}
|
|
3333
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
|
+
|
|
3334
3410
|
function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null } = {}) {
|
|
3335
3411
|
const routeSummary = state?.routeSummary || {};
|
|
3336
3412
|
// Explicit vibecode autonomy: the user asked UKit to run one prompt to a finished result,
|
|
@@ -3338,17 +3414,26 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3338
3414
|
const vibecode = routeSummary.autonomyLevel === 'vibecode';
|
|
3339
3415
|
const mode = routeSummary.executionMode || routeSummary.approachSelector?.executionMode || null;
|
|
3340
3416
|
const evidence = requiredEvidence(state);
|
|
3341
|
-
|
|
3417
|
+
// Request-scoped blocker: the stored session ledger persists until the new request's first
|
|
3418
|
+
// receipt re-keys it, so the RAW `ledger.blocker` can still be the PREVIOUS task's escape.
|
|
3419
|
+
// Scope the check to the effective request ledger — a stale blocker must never release the
|
|
3420
|
+
// new task's gate (its verification debt stays enforced below) — while the current task's
|
|
3421
|
+
// own blocker still ends the run truthfully: complete:false + outcome:'blocked', never a
|
|
3422
|
+
// fabricated complete and never another continuation loop.
|
|
3423
|
+
const requestLedger = effectiveRequestLedger(state, ledger);
|
|
3424
|
+
if (requestLedger?.blocker) {
|
|
3342
3425
|
// A blocker minted by the gate itself (verification loop) is the run's exit path — it
|
|
3343
3426
|
// must be user-visible or the release is indistinguishable from a stall. A blocker
|
|
3344
3427
|
// minted by a permission decision is already named in the model's own reply and stays
|
|
3345
3428
|
// silent here.
|
|
3346
|
-
if (
|
|
3429
|
+
if (requestLedger.blocker.visible === true) {
|
|
3347
3430
|
return {
|
|
3348
3431
|
continue: false,
|
|
3349
3432
|
notify: true,
|
|
3433
|
+
complete: false,
|
|
3434
|
+
outcome: 'blocked',
|
|
3350
3435
|
missingEvidence: [],
|
|
3351
|
-
reason: `UKit stopped automatic recovery: ${
|
|
3436
|
+
reason: `UKit stopped automatic recovery: ${requestLedger.blocker.detail || 'a recorded blocker ended this run.'}`,
|
|
3352
3437
|
};
|
|
3353
3438
|
}
|
|
3354
3439
|
// An invisible blocker (e.g. a permission decision the model already named in its own
|
|
@@ -3357,8 +3442,10 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3357
3442
|
return {
|
|
3358
3443
|
continue: false,
|
|
3359
3444
|
notify: true,
|
|
3445
|
+
complete: false,
|
|
3446
|
+
outcome: 'blocked',
|
|
3360
3447
|
missingEvidence: [],
|
|
3361
|
-
reason: `UKit stopped automatic recovery: ${
|
|
3448
|
+
reason: `UKit stopped automatic recovery: ${requestLedger.blocker.detail || 'a recorded blocker ended this run.'}`,
|
|
3362
3449
|
};
|
|
3363
3450
|
}
|
|
3364
3451
|
if (!state || !state.routeSummary) {
|
|
@@ -3644,7 +3731,7 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3644
3731
|
|
|
3645
3732
|
const finalAttempt = !vibecode && continuationCount === MAX_CONTINUATIONS - 1;
|
|
3646
3733
|
const instruction = missingEvidence.includes('release-publication')
|
|
3647
|
-
? '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.'
|
|
3648
3735
|
: operationalTask
|
|
3649
3736
|
? 'Inspect native mutation receipts and verify the requested operational state; do not fabricate source edits or source-test evidence.'
|
|
3650
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 {
|
|
@@ -66,6 +66,17 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
|
|
|
66
66
|
[key]: { ...prior, [op.role]: proof } };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function conditionalAbsenceGoal(promptText, target) {
|
|
70
|
+
if (typeof target !== 'string' || !/^[a-f0-9]{8}(?:[a-f0-9-]{0,28})$/.test(target)) return false;
|
|
71
|
+
const text = String(promptText || '').normalize('NFD')
|
|
72
|
+
.replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
73
|
+
return text.split(/[\n;.,]/).some((clause) => clause.split(/\s+/).some((word) => word === target)
|
|
74
|
+
&& /\b(?:cron|scheduled\s+job)\b/.test(clause)
|
|
75
|
+
&& /\b(?:delete|remove|cancel|xoa|huy)\b/.test(clause)
|
|
76
|
+
&& !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
77
|
+
&& /\b(?:if\s+(?:it\s+)?(?:still\s+)?exists|if\s+present|neu\s+(?:van\s+|con\s+)?ton tai)\b/.test(clause));
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
export function operationEvidenceStatus(goal, ledger = {}) {
|
|
70
81
|
if (!goal || typeof goal.domain !== 'string' || !goal.domain || typeof goal.desiredState !== 'string'
|
|
71
82
|
|| !goal.desiredState || (goal.target !== null && (typeof goal.target !== 'string' || !goal.target))
|
|
@@ -104,7 +115,10 @@ export function operationEvidenceStatus(goal, ledger = {}) {
|
|
|
104
115
|
if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth
|
|
105
116
|
&& (op.remoteSync.sha !== auth.head || op.remoteSync.ref !== `refs/heads/${auth.branch}`
|
|
106
117
|
|| (auth.remote && op.remoteSync.remote !== auth.remote))) return incomplete;
|
|
107
|
-
|
|
118
|
+
// Ensuring a state does not promise execution. A fresh native observation after
|
|
119
|
+
// every attempt proves the postcondition even when an already-absent delete failed.
|
|
120
|
+
if (goal.completionSemantics === 'ensure-state'
|
|
121
|
+
&& attempts.every((proof) => Number.isSafeInteger(proof.sequence) && observation.sequence > proof.sequence)) {
|
|
108
122
|
return { complete: true, operationProof: 'desired-state-only' };
|
|
109
123
|
}
|
|
110
124
|
if (!proofs.every((proof) => proof?.success && proof.toolUseId && observation.toolUseId
|
|
@@ -106,6 +106,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
106
106
|
## Unattended Completion Loop
|
|
107
107
|
<!-- RULE: UNATTENDED-01 -->
|
|
108
108
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
109
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
109
110
|
<!-- RULE: UNATTENDED-02 -->
|
|
110
111
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
111
112
|
<!-- RULE: UNATTENDED-03 -->
|
|
@@ -106,6 +106,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
106
106
|
## Unattended Completion Loop
|
|
107
107
|
<!-- RULE: UNATTENDED-01 -->
|
|
108
108
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
109
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
109
110
|
<!-- RULE: UNATTENDED-02 -->
|
|
110
111
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
111
112
|
<!-- RULE: UNATTENDED-03 -->
|
|
@@ -103,6 +103,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
103
103
|
## Unattended Completion Loop
|
|
104
104
|
<!-- RULE: UNATTENDED-01 -->
|
|
105
105
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
106
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
106
107
|
<!-- RULE: UNATTENDED-02 -->
|
|
107
108
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
108
109
|
<!-- RULE: UNATTENDED-03 -->
|