@ngockhoale/ukit 2.2.12 → 2.2.16
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 +75 -0
- package/manifests/platform.full.yaml +0 -13
- package/package.json +1 -1
- package/src/index/buildIndex.js +15 -2
- package/src/index/paths.js +1 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/taskRouting.js +5 -5
- package/templates/.claude/hooks/context-hardcap-gate.sh +20 -4
- package/templates/.claude/hooks/context-window-guard.sh +1 -1
- package/templates/.claude/hooks/handoff-model-guard.sh +99 -29
- package/templates/.claude/hooks/skill-router.sh +54 -6
- package/templates/.claude/hooks/vision-router.sh +8 -14
- package/templates/.claude/settings.json +0 -5
- package/templates/.claude/ukit/index/lib/index-core.mjs +35 -23
- package/templates/.claude/ukit/index/provision-worktree.mjs +1 -1
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.claude/ukit/index/route-task.mjs +52 -6
- package/templates/.claude/ukit/index/unic-gateway.mjs +1 -1
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +97 -2
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +0 -1
- package/templates/.omp/README.md +3 -3
- package/templates/.omp/hooks/pre/ukit-bridge.js +0 -2
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/storage/config.json +2 -2
- package/templates/.claude/hooks/vision-gate.sh +0 -230
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.2.16 - 2026-09-06
|
|
6
|
+
|
|
7
|
+
The vision gate is gone — image work is advisory-routed, never blocked. Since TASK-008, a
|
|
8
|
+
pasted image on the UNIC gateway armed `vision-gate.sh`, which refused every Edit/Write until
|
|
9
|
+
`ukit-vision-analyst` had produced receipts for ALL pending images. In practice this
|
|
10
|
+
repeatedly stranded sessions (receipts that never landed, one stale marker blocking
|
|
11
|
+
unrelated writes), so the maintainer retired the hard block: `vision-gate.sh` and its
|
|
12
|
+
manifest entry `hook-vision-gate` are removed from both the live tree and templates, the omp
|
|
13
|
+
bridge and `hook-chain-runner.mjs` no longer list it, and `vision-router.sh` now says "never
|
|
14
|
+
guess at image contents" instead of claiming edits are GATED. Routing to
|
|
15
|
+
`ukit-vision-analyst` still happens — on this gateway the main tier genuinely cannot read
|
|
16
|
+
images — but nothing blocks while it runs. Supersedes the 2.2.15 escape hatch, which patched
|
|
17
|
+
a message on a gate that no longer exists. Verified: vision-hook, route-vision,
|
|
18
|
+
vision-agent, install-wiring, provision-worktree, and ompHookBridge (52/52) all green.
|
|
19
|
+
(An interim 2026-09-05 patch that added an escape hatch to the block message is included
|
|
20
|
+
here and immediately superseded — the gate itself no longer exists.)
|
|
21
|
+
|
|
22
|
+
## 2.2.14 - 2026-09-05
|
|
23
|
+
|
|
24
|
+
The context hard-cap gate no longer bricks healthy sessions with a phantom token count. The
|
|
25
|
+
tracker behind `compact.hardCapTokens` accumulates every prompt and tool output across a
|
|
26
|
+
session, and it only reset on the PreCompact hook — which fires for a manual `/compact` but not
|
|
27
|
+
for every harness auto-compact path (SDK/VSCode being the known gap). A session whose live
|
|
28
|
+
context had been compacted down to almost nothing could therefore reach a tracked 528k against
|
|
29
|
+
the 500k cap: Read/Grep stayed open but every Edit/Write/Bash was refused, and the user saw the
|
|
30
|
+
assistant freeze with no error. Every real compaction — manual or automatic — now also leaves a
|
|
31
|
+
`compact_boundary` entry in the session transcript, so `compact-threshold.mjs` rebase the
|
|
32
|
+
counter from that record (`syncCompactPressureStateWithTranscript`, stamped
|
|
33
|
+
`lastCompactBoundaryAt` so each boundary resets exactly once). Both the prompt-time hook and
|
|
34
|
+
`context-hardcap-gate.sh` sync before acting, and the reset is persisted so the gate re-arms
|
|
35
|
+
instead of disabling itself.
|
|
36
|
+
|
|
37
|
+
When the cap is genuinely reached, the model is now ordered to tell the user instead of going
|
|
38
|
+
quiet. The hard-phase prompt warning and the gate's block message both lead with an explicit
|
|
39
|
+
directive — tell the user "gõ /compact ngay" as the first sentence of the reply, since typing
|
|
40
|
+
"continue" alone hits the same block — and the advisory soft phase tells the model to surface
|
|
41
|
+
`/compact` at the next natural pause. `context-window-guard.sh`'s overflow directive likewise
|
|
42
|
+
demands the compact instruction as the first sentence rather than a buried suggestion.
|
|
43
|
+
|
|
44
|
+
Vietnamese concrete bug reports route to the debug lane again. Signals such as `lỗi`, `sửa`,
|
|
45
|
+
`đăng nhập`, `đang ở đâu` were written inside `\b(...)\b`, but JavaScript `\b` is ASCII-only and
|
|
46
|
+
those words have diacritic edges, so the patterns could never match — "app bị lỗi khi đăng nhập,
|
|
47
|
+
sửa giúp tôi" fell through to informational lanes while the identical English prompt routed
|
|
48
|
+
correctly. All Vietnamese intent patterns in the router and catalog now use explicit
|
|
49
|
+
`(?<![A-Za-z0-9_])` / `(?![A-Za-z0-9_])` edges; regression tests cover the debug-lane prompt,
|
|
50
|
+
the open-ended status question, and hook-level state.
|
|
51
|
+
|
|
52
|
+
## 2.2.13 - 2026-09-05
|
|
53
|
+
|
|
54
|
+
Question-shaped prompts no longer get blocked by the completion gate. "Đây là lỗi gì?",
|
|
55
|
+
"model bạn đang dùng là model gì", or "what is this error?" route to the informational lane and
|
|
56
|
+
answer freely; only prompts that actually ask for a change (fix/sửa/build/…) still owe write and
|
|
57
|
+
verification evidence before claiming done. Two layered causes: the Vietnamese question pattern
|
|
58
|
+
only matched adjacent `là gì` (missing "là model gì" / "là lỗi gì"), and the `\blỗi` veto never
|
|
59
|
+
fired at all — JavaScript `\b` word boundaries don't exist around diacritic characters. A
|
|
60
|
+
`strongQuestion` signal (an explicit `?`, a leading interrogative, or the Vietnamese pattern) now
|
|
61
|
+
overrides error-wording vetoes, while "error when I run tests" and "why is build failing?" still
|
|
62
|
+
route to investigation.
|
|
63
|
+
|
|
64
|
+
The source-code index understands SQL from the canonical source, not just the installed mirror.
|
|
65
|
+
`CREATE TABLE/VIEW/FUNCTION/PROCEDURE/INDEX/TYPE` and `ALTER TABLE` statements are extracted as
|
|
66
|
+
symbols with schema-qualified names captured by their bare object name (`qas.fn_rpt_billing` →
|
|
67
|
+
`fn_rpt_billing`), and `.sql` files are tracked and code-indexed. The schema bump had previously
|
|
68
|
+
landed only in the runtime mirror; `src/index/` now carries the same v8 schema so the mirror-parity
|
|
69
|
+
test holds and the `scripts/index` lane benefits too.
|
|
70
|
+
|
|
71
|
+
The handoff model-tier guard closes an under-block found while scenario-testing it. Once a real
|
|
72
|
+
`EXECUTOR_MODEL` or `REVIEWER_MODEL` was on disk, an in-place rewrite could silently demote the
|
|
73
|
+
executor to a lite tier or set executor == reviewer; validation now also fires when an
|
|
74
|
+
already-real model line changes value (same-model rewrites and upward escalation stay free). The
|
|
75
|
+
push-time backstop was unaffected — this moves the rejection to write time, where it belongs.
|
|
76
|
+
|
|
77
|
+
`handoff.maxParallelAgents` template default is now 2, matching the operator setting; the config
|
|
78
|
+
help prose no longer claims a default of 12 one line above a configured 2.
|
|
79
|
+
|
|
5
80
|
## 2.2.12 - 2026-09-04
|
|
6
81
|
|
|
7
82
|
`ukit install` and `ukit update` now clean up broken Claude Code hook registrations automatically.
|
|
@@ -1131,19 +1131,6 @@ items:
|
|
|
1131
1131
|
packs:
|
|
1132
1132
|
- core
|
|
1133
1133
|
|
|
1134
|
-
- id: hook-vision-gate
|
|
1135
|
-
type: hook
|
|
1136
|
-
sourceTemplate: .claude/hooks/vision-gate.sh
|
|
1137
|
-
targetPath: .claude/hooks/vision-gate.sh
|
|
1138
|
-
requires:
|
|
1139
|
-
- ukit-index-extract-image-script
|
|
1140
|
-
- ukit-index-unic-gateway-script
|
|
1141
|
-
mergeStrategy: overwrite_with_backup
|
|
1142
|
-
variables: []
|
|
1143
|
-
enabledByDefault: true
|
|
1144
|
-
packs:
|
|
1145
|
-
- core
|
|
1146
|
-
|
|
1147
1134
|
- id: hook-context-hardcap-gate
|
|
1148
1135
|
type: hook
|
|
1149
1136
|
sourceTemplate: .claude/hooks/context-hardcap-gate.sh
|
package/package.json
CHANGED
package/src/index/buildIndex.js
CHANGED
|
@@ -29,11 +29,11 @@ const EXCLUDED_DIR_NAMES = new Set([
|
|
|
29
29
|
'.venv',
|
|
30
30
|
'venv',
|
|
31
31
|
]);
|
|
32
|
-
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue']);
|
|
32
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.sql']);
|
|
33
33
|
const STYLE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
34
34
|
// `.sh` is tracked but never parsed, like `.md`/`.json`/`.yaml` — it is matched
|
|
35
35
|
// by path only. Without it every shipped hook script was invisible to the index.
|
|
36
|
-
const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.yaml', '.yml', '.md', '.sh']);
|
|
36
|
+
const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.sql', '.json', '.yaml', '.yml', '.md', '.sh']);
|
|
37
37
|
const DISCOVERED_EXTENSIONS = new Set([...TRACKED_EXTENSIONS, ...STYLE_EXTENSIONS]);
|
|
38
38
|
export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
39
39
|
const INDEX_PARSE_BATCH_SIZE = 8;
|
|
@@ -729,6 +729,19 @@ export function extractSymbols(filePath, content) {
|
|
|
729
729
|
}
|
|
730
730
|
};
|
|
731
731
|
|
|
732
|
+
if (filePath.endsWith('.sql')) {
|
|
733
|
+
// Capture the object name, skipping an optional schema qualifier (qas.fn_rpt_billing → fn_rpt_billing).
|
|
734
|
+
const sqlIdent = '(?:(?:["`]?[A-Za-z_][A-Za-z0-9_$]*["`]?)\\s*\\.\\s*)?["`]?([A-Za-z_][A-Za-z0-9_$]*)["`]?';
|
|
735
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'table');
|
|
736
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?VIEW\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'view');
|
|
737
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'function');
|
|
738
|
+
addMatches(new RegExp(`CREATE\\s+(?:OR\\s+REPLACE\\s+)?PROCEDURE\\s+${sqlIdent}`, 'gi'), 'procedure');
|
|
739
|
+
addMatches(new RegExp(`CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'index');
|
|
740
|
+
addMatches(new RegExp(`CREATE\\s+(?:TYPE|DOMAIN|SEQUENCE|MATERIALIZED\\s+VIEW)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'type');
|
|
741
|
+
addMatches(new RegExp(`ALTER\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${sqlIdent}`, 'gi'), 'table');
|
|
742
|
+
return dedupeBy(symbols, (item) => `${item.filePath}:${item.type}:${item.name}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
732
745
|
addMatches(/export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'function');
|
|
733
746
|
addMatches(/export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'class');
|
|
734
747
|
addMatches(/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, 'callable');
|
package/src/index/paths.js
CHANGED
|
@@ -234,7 +234,7 @@ export const ROUTE_CATALOG = [
|
|
|
234
234
|
contextMode: 'standalone',
|
|
235
235
|
signals: [
|
|
236
236
|
{ type: 'prompt', regex: /\b(what(?:'s| is)? next|next steps?|project status|current status|where are we|continue(?: from)?(?: last session)?|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/i, score: 7 },
|
|
237
|
-
{ type: 'prompt', regex:
|
|
237
|
+
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/i, score: 7 },
|
|
238
238
|
],
|
|
239
239
|
},
|
|
240
240
|
{
|
package/src/index/taskRouting.js
CHANGED
|
@@ -960,7 +960,7 @@ function hasHandoffSignal(lower, raw) {
|
|
|
960
960
|
|| /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
|
|
961
961
|
|| /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
|
|
962
962
|
|| /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
|
|
963
|
-
||
|
|
963
|
+
|| /(?<![A-Za-z0-9_])(gom|chia|tach|tách|lên|len|xóa|clear|dọn).{0,80}(?<![A-Za-z0-9_])(idea|ý tưởng|y tuong|task|công việc|cong viec|handoff)(?![A-Za-z0-9_])/.test(raw)
|
|
964
964
|
|| /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
|
|
965
965
|
}
|
|
966
966
|
|
|
@@ -980,7 +980,7 @@ function hasStatusUpdateSignal(lower, raw) {
|
|
|
980
980
|
function hasOpenEndedStatusSignal(lower, raw) {
|
|
981
981
|
return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/.test(lower)
|
|
982
982
|
|| /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project|task tiep theo|viec tiep theo trong tasks)\b/.test(lower)
|
|
983
|
-
||
|
|
983
|
+
|| /(?<![A-Za-z0-9_])(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/.test(raw);
|
|
984
984
|
}
|
|
985
985
|
|
|
986
986
|
function hasTaskQueueNextSignal(lower, raw) {
|
|
@@ -994,7 +994,7 @@ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false }
|
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
|
|
997
|
-
||
|
|
997
|
+
|| /(?<![A-Za-z0-9_])(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)(?![A-Za-z0-9_])/.test(raw);
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
1000
|
function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
|
|
@@ -1017,11 +1017,11 @@ function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = fal
|
|
|
1017
1017
|
|
|
1018
1018
|
return mentionsTargetDoc(lower, targetName)
|
|
1019
1019
|
|| /\b(edit|write|improve|document|docs?|readme|changelog|worklog|memory|code map|template|wording|copy|grammar|format|structure|heading|section|handoff notes?)\b/.test(lower)
|
|
1020
|
-
||
|
|
1020
|
+
|| /(?<![A-Za-z0-9_])(câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu|ghi chú bàn giao)(?![A-Za-z0-9_])/.test(raw);
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
1023
|
return /\b(template|wording|edit|copy|grammar|format|structure|heading|section|docs?)\b/.test(lower)
|
|
1024
|
-
||
|
|
1024
|
+
|| /(?<![A-Za-z0-9_])(mẫu|câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu)(?![A-Za-z0-9_])/.test(raw);
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
1027
|
function hasExplicitStatusUpdateSignal(lower, raw) {
|
|
@@ -100,7 +100,21 @@ function readRunCursor() {
|
|
|
100
100
|
const mod = await import(pathToFileURL(thresholdModulePath).href);
|
|
101
101
|
const pressurePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'compact-pressure.json');
|
|
102
102
|
const rawState = readJsonSafe(pressurePath, null);
|
|
103
|
-
|
|
103
|
+
|
|
104
|
+
// A compaction the PreCompact hook missed (SDK/VSCode auto-compact) leaves the tracked
|
|
105
|
+
// counter counting history that was already summarised away — the exact failure that
|
|
106
|
+
// bricks a healthy session at this cap. The transcript's own compact_boundary entries
|
|
107
|
+
// are the reliable record of real compactions, so sync before deciding, and persist the
|
|
108
|
+
// reset so it re-arms exactly once per boundary instead of disabling the gate.
|
|
109
|
+
let state = mod.buildCompactPressureState(rawState, config);
|
|
110
|
+
try {
|
|
111
|
+
const synced = await mod.syncCompactPressureStateWithTranscript(rawState, config, payload.transcript_path);
|
|
112
|
+
if (synced.reset) {
|
|
113
|
+
state = await mod.writeCompactPressureState(projectRoot, synced.state, config);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
state = mod.buildCompactPressureState(rawState, config);
|
|
117
|
+
}
|
|
104
118
|
const thresholds = mod.buildCompactThresholds(config);
|
|
105
119
|
|
|
106
120
|
const gracePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'hardcap-grace.json');
|
|
@@ -140,6 +154,7 @@ function readRunCursor() {
|
|
|
140
154
|
`An unfinished run is in flight (Phase: ${run.phase}${run.cursor ? `, Cursor: ${run.cursor}` : ''}), so this call is allowed instead of stranding it mid-edit.`,
|
|
141
155
|
'Spend the remaining grace on LANDING, not on new work: finish the current edit, commit, update docs/AI_HANDOFF/RUN.md, push.',
|
|
142
156
|
'Do NOT start a new task, open new files, or spawn agents. When grace runs out the gate blocks hard.',
|
|
157
|
+
'Tell the user in your reply that context is over the cap and they should run /compact as soon as this run lands.',
|
|
143
158
|
'After the user runs /compact, the SessionStart resume hook replays the cursor and the run continues automatically.',
|
|
144
159
|
].join('\n') + '\n',
|
|
145
160
|
);
|
|
@@ -150,19 +165,20 @@ function readRunCursor() {
|
|
|
150
165
|
|
|
151
166
|
const lines = [
|
|
152
167
|
`BLOCKED (context hard cap): estimated context ~${state.estimatedTotalTokens} tokens >= hard cap ${thresholds.hardCapTokens}.`,
|
|
168
|
+
'FIRST ACTION IN YOUR REPLY — tell the user plainly, do not just stop silently: "Context đã đầy — hãy gõ /compact ngay (run /compact now)".',
|
|
169
|
+
'Typing "continue" alone will hit this same block; only /compact (or a new session) resets the counter. After compacting, resume the interrupted task.',
|
|
153
170
|
'This is an absolute ceiling (compact.hardCapTokens), separate from the soft/hard advisory phases — those were apparently not followed.',
|
|
154
171
|
`Edit/Write/Bash refused (tool_name=${toolName}) until real compaction happens.`,
|
|
155
172
|
run
|
|
156
173
|
? 'The unfinished-run grace window (compact.hardCapGraceCalls) is already exhausted — progress should be committed and the cursor written by now.'
|
|
157
174
|
: 'No unfinished run in docs/AI_HANDOFF/RUN.md, so there is no grace window to spend.',
|
|
158
|
-
'Remedy (any one): run /compact, or start a new session (SessionStart resets the counter), or delete .ukit/storage/cache/compact-pressure.json.',
|
|
159
175
|
'Do not work around this by summarizing inline and continuing, and do not reach for a non-gated write tool.',
|
|
160
176
|
];
|
|
161
177
|
process.stderr.write(`${lines.join('\n')}\n`);
|
|
162
178
|
process.exit(2);
|
|
163
179
|
})().catch((err) => {
|
|
164
|
-
//
|
|
165
|
-
//
|
|
180
|
+
// A logic error here fails OPEN: this is a backstop on top of advisory nudges,
|
|
181
|
+
// not a correctness gate — a broken gate must not brick every session.
|
|
166
182
|
process.stderr.write(`context-hardcap-gate: internal error, failing open: ${err?.message ?? err}\n`);
|
|
167
183
|
process.exit(0);
|
|
168
184
|
});
|
|
@@ -211,7 +211,7 @@ if (withinCooldown) {
|
|
|
211
211
|
lines_out.push('ACTION THIS TURN, before starting new investigation/subagents/pipeline phases:');
|
|
212
212
|
lines_out.push('1) Persist current progress now (update docs/STATUS.md, e.g. via the update-status skill) so nothing is lost.');
|
|
213
213
|
lines_out.push('2) If the remaining work still has multiple steps or pipeline phases left, split what remains into bounded docs/AI_HANDOFF tasks instead of continuing in this one thread — smaller phases keep each turn under the cap.');
|
|
214
|
-
lines_out.push('3) Otherwise,
|
|
214
|
+
lines_out.push('3) Otherwise, make the user\'s next action the FIRST sentence of your reply so it cannot be missed: "Context sắp đầy — gõ /compact ngay (run /compact now)", or wrap up and start a fresh session — work already committed/persisted to disk is not lost. Do not keep working silently until tools start being refused.');
|
|
215
215
|
if (sidechainEntries > 0) {
|
|
216
216
|
lines_out.push(`Note: ${sidechainEntries} subagent entries in this stretch — each teammate carries its own context window, and every finished report is injected back here, so running many at once is the fastest way to overflow this session. Avoid spawning more until context drops back under the cap.`);
|
|
217
217
|
}
|
|
@@ -60,6 +60,20 @@ function extractField(text, name) {
|
|
|
60
60
|
return matches[matches.length - 1][1].trim();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Phase-section detection and model extraction must run on visible content only:
|
|
64
|
+
// task/plan templates carry `## Executor Report` / `## Reviewer Verdict` inside HTML
|
|
65
|
+
// comments, and matching those made plain task creation fail with reviewer/executor
|
|
66
|
+
// model errors (the Phase 2 misfire).
|
|
67
|
+
function stripHtmlComments(text) {
|
|
68
|
+
return String(text || '').replace(/<!--[\s\S]*?-->/g, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlaceholderValue(value) {
|
|
72
|
+
const raw = String(value || '').trim();
|
|
73
|
+
if (!raw || /^[<(]/.test(raw)) return true;
|
|
74
|
+
return ['-','--','tbd','todo','n/a','na','none','unknown','pending'].includes(raw.toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
if (toolName === 'Write' || toolName === 'Edit') {
|
|
64
78
|
const filePath = String(input.file_path || '');
|
|
65
79
|
if (!filePath) process.exit(0);
|
|
@@ -84,9 +98,11 @@ if (toolName === 'Write' || toolName === 'Edit') {
|
|
|
84
98
|
: currentContent.replace(oldStr, newStr);
|
|
85
99
|
}
|
|
86
100
|
const newContent = resultingContent();
|
|
101
|
+
const newVisible = stripHtmlComments(newContent);
|
|
102
|
+
const currentVisible = stripHtmlComments(currentContent);
|
|
87
103
|
|
|
88
|
-
if (isPlan && /## Planner Report/.test(
|
|
89
|
-
const plannerModel = extractField(
|
|
104
|
+
if (isPlan && /## Planner Report/.test(newVisible)) {
|
|
105
|
+
const plannerModel = extractField(newVisible, 'PLANNER_MODEL');
|
|
90
106
|
if (!plannerModel || /^unknown$/i.test(plannerModel)) {
|
|
91
107
|
block('PLANNER_MODEL missing/unknown in PLAN.md. Planning must run via Agent tool subagent_type: "handoff-planner" (opus/unic-smart) and self-report its model.');
|
|
92
108
|
}
|
|
@@ -102,42 +118,96 @@ if (toolName === 'Write' || toolName === 'Edit') {
|
|
|
102
118
|
const taskId = taskMatch[1];
|
|
103
119
|
const planPath = path.join(projectRoot, 'docs/AI_HANDOFF/PLAN.md');
|
|
104
120
|
|
|
105
|
-
const
|
|
121
|
+
const hasExecutorReport = /## Executor Report/.test(newVisible);
|
|
122
|
+
const hasReviewerVerdict = /## Reviewer Verdict/.test(newVisible);
|
|
123
|
+
const isFreshTaskFile = !fileExists;
|
|
106
124
|
if (isFreshTaskFile) {
|
|
107
125
|
const planContent = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf8') : '';
|
|
108
|
-
const plannerModel = extractField(planContent, 'PLANNER_MODEL');
|
|
126
|
+
const plannerModel = extractField(stripHtmlComments(planContent), 'PLANNER_MODEL');
|
|
109
127
|
if (!plannerModel || tierOf(plannerModel) !== 'smart') {
|
|
110
128
|
block(`Cannot create ${taskId}.md — PLAN.md has no valid smart-tier PLANNER_MODEL yet. Run planning via Agent tool subagent_type: "handoff-planner" (opus/unic-smart) first.`);
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
// A section already on disk only counts as validated when its model is real —
|
|
133
|
+
// otherwise a placeholder skeleton created at Phase 2 could be silently promoted
|
|
134
|
+
// to a "real" report without ever passing tier validation.
|
|
135
|
+
const currentExecutorReal = /## Executor Report/.test(currentVisible)
|
|
136
|
+
&& !isPlaceholderValue(extractField(currentVisible, 'EXECUTOR_MODEL'));
|
|
137
|
+
if (hasExecutorReport && !currentExecutorReal) {
|
|
138
|
+
const executorModel = extractField(newVisible, 'EXECUTOR_MODEL');
|
|
139
|
+
if (isPlaceholderValue(executorModel) || /^unknown$/i.test(executorModel)) {
|
|
140
|
+
if (!isFreshTaskFile) {
|
|
141
|
+
block(`${taskId}: EXECUTOR_MODEL missing/placeholder. Implementation must run via Agent tool subagent_type: "feature-implementer" (sonnet/unic-code) and self-report its model.`);
|
|
142
|
+
}
|
|
143
|
+
// Else: skeleton sections copied from a task template at creation time — the
|
|
144
|
+
// real Executor Report is appended in Phase 3 and validated there.
|
|
145
|
+
} else {
|
|
146
|
+
if (tierOf(executorModel) === 'vision') {
|
|
147
|
+
block(`${taskId}: EXECUTOR_MODEL "${executorModel}" ${VISION_LANE_MESSAGE}`);
|
|
148
|
+
}
|
|
149
|
+
if (tierOf(executorModel) === 'lite') {
|
|
150
|
+
block(`${taskId}: EXECUTOR_MODEL "${executorModel}" is lite tier. Implementation must run on at least sonnet/unic-code, never haiku/unic-lite.`);
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
153
|
+
} else if (hasExecutorReport && currentExecutorReal) {
|
|
154
|
+
// An in-place rewrite of an already-real model line must not silently demote the
|
|
155
|
+
// tier or collide with the reviewer; the push-time check would catch it, but the
|
|
156
|
+
// violating report should never be accepted onto disk in the first place.
|
|
157
|
+
const previousExecutorModel = extractField(currentVisible, 'EXECUTOR_MODEL');
|
|
158
|
+
const executorModel = extractField(newVisible, 'EXECUTOR_MODEL');
|
|
159
|
+
if (executorModel && previousExecutorModel && executorModel !== previousExecutorModel) {
|
|
160
|
+
if (tierOf(executorModel) === 'vision') {
|
|
161
|
+
block(`${taskId}: EXECUTOR_MODEL "${executorModel}" ${VISION_LANE_MESSAGE}`);
|
|
162
|
+
}
|
|
163
|
+
if (tierOf(executorModel) === 'lite') {
|
|
164
|
+
block(`${taskId}: EXECUTOR_MODEL "${executorModel}" is lite tier. Implementation must run on at least sonnet/unic-code, never haiku/unic-lite.`);
|
|
165
|
+
}
|
|
166
|
+
const verdictModel = extractField(newVisible, 'REVIEWER_MODEL');
|
|
167
|
+
if (!isPlaceholderValue(verdictModel) && verdictModel.toLowerCase() === executorModel.toLowerCase()) {
|
|
168
|
+
block(`${taskId}: EXECUTOR_MODEL ("${executorModel}") matches REVIEWER_MODEL ("${verdictModel}") — reviewer must differ from executor.`);
|
|
169
|
+
}
|
|
124
170
|
}
|
|
125
171
|
}
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
173
|
+
const currentReviewerReal = /## Reviewer Verdict/.test(currentVisible)
|
|
174
|
+
&& !isPlaceholderValue(extractField(currentVisible, 'REVIEWER_MODEL'));
|
|
175
|
+
if (hasReviewerVerdict && !currentReviewerReal) {
|
|
176
|
+
const executorModel = extractField(currentVisible, 'EXECUTOR_MODEL') || extractField(newVisible, 'EXECUTOR_MODEL');
|
|
177
|
+
const reviewerModel = extractField(newVisible, 'REVIEWER_MODEL');
|
|
178
|
+
if (isPlaceholderValue(reviewerModel) || /^unknown$/i.test(reviewerModel)) {
|
|
179
|
+
if (!isFreshTaskFile) {
|
|
180
|
+
block(`${taskId}: REVIEWER_MODEL missing/placeholder. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart) and self-report its model.`);
|
|
181
|
+
}
|
|
182
|
+
// Else: skeleton Reviewer Verdict on a freshly created task file — Phase 4
|
|
183
|
+
// appends the real verdict and it is validated there.
|
|
184
|
+
} else {
|
|
185
|
+
if (tierOf(reviewerModel) === 'vision') {
|
|
186
|
+
block(`${taskId}: REVIEWER_MODEL "${reviewerModel}" ${VISION_LANE_MESSAGE}`);
|
|
187
|
+
}
|
|
188
|
+
if (tierOf(reviewerModel) !== 'smart') {
|
|
189
|
+
block(`${taskId}: REVIEWER_MODEL "${reviewerModel}" is not strong/opus tier. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart).`);
|
|
190
|
+
}
|
|
191
|
+
if (executorModel && reviewerModel && executorModel.toLowerCase() === reviewerModel.toLowerCase()) {
|
|
192
|
+
block(`${taskId}: REVIEWER_MODEL ("${reviewerModel}") matches EXECUTOR_MODEL — reviewer must differ from executor. Re-run review through a different subagent/model.`);
|
|
193
|
+
}
|
|
138
194
|
}
|
|
139
|
-
|
|
140
|
-
|
|
195
|
+
} else if (hasReviewerVerdict && currentReviewerReal) {
|
|
196
|
+
// Mirror of the executor in-place rule: a real reviewer verdict must not be
|
|
197
|
+
// rewritten onto a weaker tier or onto the executor's own model.
|
|
198
|
+
const previousReviewerModel = extractField(currentVisible, 'REVIEWER_MODEL');
|
|
199
|
+
const reviewerModel = extractField(newVisible, 'REVIEWER_MODEL');
|
|
200
|
+
if (reviewerModel && previousReviewerModel && reviewerModel !== previousReviewerModel) {
|
|
201
|
+
if (tierOf(reviewerModel) === 'vision') {
|
|
202
|
+
block(`${taskId}: REVIEWER_MODEL "${reviewerModel}" ${VISION_LANE_MESSAGE}`);
|
|
203
|
+
}
|
|
204
|
+
if (tierOf(reviewerModel) !== 'smart') {
|
|
205
|
+
block(`${taskId}: REVIEWER_MODEL "${reviewerModel}" is not strong/opus tier. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart).`);
|
|
206
|
+
}
|
|
207
|
+
const executorModel = extractField(newVisible, 'EXECUTOR_MODEL');
|
|
208
|
+
if (!isPlaceholderValue(executorModel) && executorModel.toLowerCase() === reviewerModel.toLowerCase()) {
|
|
209
|
+
block(`${taskId}: REVIEWER_MODEL ("${reviewerModel}") matches EXECUTOR_MODEL — reviewer must differ from executor. Re-run review through a different subagent/model.`);
|
|
210
|
+
}
|
|
141
211
|
}
|
|
142
212
|
}
|
|
143
213
|
}
|
|
@@ -160,7 +230,7 @@ if (toolName === 'Bash') {
|
|
|
160
230
|
for (const taskId of taskIds) {
|
|
161
231
|
const taskPath = path.join(projectRoot, `docs/AI_HANDOFF/tasks/${taskId}.md`);
|
|
162
232
|
if (!fs.existsSync(taskPath)) continue;
|
|
163
|
-
const text = fs.readFileSync(taskPath, 'utf8');
|
|
233
|
+
const text = stripHtmlComments(fs.readFileSync(taskPath, 'utf8'));
|
|
164
234
|
if (!text.includes('## Reviewer Verdict')) continue; // not yet reviewed, not this push's concern
|
|
165
235
|
|
|
166
236
|
// A documented, human-approved exception (e.g. an opus-tier executor escalation left no
|
|
@@ -172,10 +242,10 @@ if (toolName === 'Bash') {
|
|
|
172
242
|
const executorModel = extractField(text, 'EXECUTOR_MODEL');
|
|
173
243
|
const reviewerModel = extractField(text, 'REVIEWER_MODEL');
|
|
174
244
|
|
|
175
|
-
if (
|
|
245
|
+
if (isPlaceholderValue(executorModel) || /^unknown$/i.test(executorModel)) problems.push(`${taskId}: EXECUTOR_MODEL missing/placeholder`);
|
|
176
246
|
else if (tierOf(executorModel) === 'lite') problems.push(`${taskId}: EXECUTOR_MODEL "${executorModel}" is lite tier`);
|
|
177
247
|
|
|
178
|
-
if (
|
|
248
|
+
if (isPlaceholderValue(reviewerModel) || /^unknown$/i.test(reviewerModel)) problems.push(`${taskId}: REVIEWER_MODEL missing/placeholder`);
|
|
179
249
|
else if (tierOf(reviewerModel) !== 'smart') problems.push(`${taskId}: REVIEWER_MODEL "${reviewerModel}" is not opus/unic-smart tier`);
|
|
180
250
|
|
|
181
251
|
if (executorModel && reviewerModel && executorModel.toLowerCase() === reviewerModel.toLowerCase()) {
|
|
@@ -331,7 +331,7 @@ const { pathToFileURL } = require('url');
|
|
|
331
331
|
function hasOpenEndedStatusSignal(lower, raw) {
|
|
332
332
|
return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/.test(lower)
|
|
333
333
|
|| /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project|task tiep theo|viec tiep theo trong tasks)\b/.test(lower)
|
|
334
|
-
||
|
|
334
|
+
|| /(?<![A-Za-z0-9_])(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/.test(raw);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
function hasTaskQueueNextSignal(lower, raw) {
|
|
@@ -345,7 +345,7 @@ const { pathToFileURL } = require('url');
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
|
|
348
|
-
||
|
|
348
|
+
|| /(?<![A-Za-z0-9_])(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)(?![A-Za-z0-9_])/.test(raw);
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
|
|
@@ -368,11 +368,11 @@ const { pathToFileURL } = require('url');
|
|
|
368
368
|
|
|
369
369
|
return mentionsTargetDoc(lower, targetName)
|
|
370
370
|
|| /\b(edit|write|improve|document|docs?|readme|changelog|worklog|memory|code map|template|wording|copy|grammar|format|structure|heading|section|handoff notes?)\b/.test(lower)
|
|
371
|
-
||
|
|
371
|
+
|| /(?<![A-Za-z0-9_])(câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu|ghi chú bàn giao)(?![A-Za-z0-9_])/.test(raw);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
return /\b(template|wording|edit|copy|grammar|format|structure|heading|section|docs?)\b/.test(lower)
|
|
375
|
-
||
|
|
375
|
+
|| /(?<![A-Za-z0-9_])(mẫu|câu chữ|chỉnh|sửa chữ|ngữ pháp|định dạng|cấu trúc|tài liệu)(?![A-Za-z0-9_])/.test(raw);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
function hasExplicitStatusUpdateSignal(lower, raw) {
|
|
@@ -1473,6 +1473,48 @@ const { pathToFileURL } = require('url');
|
|
|
1473
1473
|
};
|
|
1474
1474
|
}
|
|
1475
1475
|
|
|
1476
|
+
function isInformationalPrompt({
|
|
1477
|
+
promptText = '',
|
|
1478
|
+
commandText = '',
|
|
1479
|
+
scores = null,
|
|
1480
|
+
} = {}) {
|
|
1481
|
+
if (String(commandText || '').trim()) return false;
|
|
1482
|
+
const raw = String(promptText || '').toLowerCase().trim();
|
|
1483
|
+
if (!raw) return false;
|
|
1484
|
+
const questionSignal = raw.includes('?')
|
|
1485
|
+
|| /\b(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
1486
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu|nghĩa là|được không|không\b)/.test(raw);
|
|
1487
|
+
// '?' or a leading/Vietnamese interrogative — required to overrule error wording.
|
|
1488
|
+
const strongQuestion = raw.includes('?')
|
|
1489
|
+
|| /^(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
1490
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu)/.test(raw);
|
|
1491
|
+
if (scores) {
|
|
1492
|
+
if (
|
|
1493
|
+
scores.implementSignal
|
|
1494
|
+
|| scores.reviewSignal
|
|
1495
|
+
|| scores.debugSignal
|
|
1496
|
+
|| scores.impactSignal
|
|
1497
|
+
|| scores.smallFixSignal
|
|
1498
|
+
|| scores.directTransformSignal
|
|
1499
|
+
) {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
if (scores.failureSignal && !strongQuestion) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const implementWords = /(?<![A-Za-z0-9_])(implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|sửa|thêm|tạo|xóa|đổi|thay thế|cập nhật|viết|build|chạy|cài)(?![A-Za-z0-9_])/.test(raw);
|
|
1507
|
+
if (implementWords) return false;
|
|
1508
|
+
const investigationWords = /\b(why|debug|triage|root cause|investigate|tại sao)\b/.test(raw);
|
|
1509
|
+
if (investigationWords) return false;
|
|
1510
|
+
// \b never matches around 'lỗi' (diacritics are not \w), so test it as a plain
|
|
1511
|
+
// substring: an error report is informational only when phrased as a question.
|
|
1512
|
+
if (raw.includes('lỗi') && !strongQuestion) return false;
|
|
1513
|
+
const reviewWords = /\b(review|audit|verify)\b/.test(raw);
|
|
1514
|
+
if (reviewWords) return false;
|
|
1515
|
+
return questionSignal;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1476
1518
|
function deriveExecutionMode({
|
|
1477
1519
|
promptText = '',
|
|
1478
1520
|
commandText = '',
|
|
@@ -1495,6 +1537,10 @@ const { pathToFileURL } = require('url');
|
|
|
1495
1537
|
&& (scores.impactSignal || /\b(check all affected|map all affected|across all affected)\b/.test(raw));
|
|
1496
1538
|
const boundedLocalBuildCandidate = scores.buildSignal && scores.boundedEditSignal && scores.explicitTarget && !scores.sharedRisk;
|
|
1497
1539
|
|
|
1540
|
+
if (isInformationalPrompt({ promptText, commandText, scores })) {
|
|
1541
|
+
return 'informational';
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1498
1544
|
if ((intentMode === 'review-specific' || explicitReviewLead) && !scores.implementSignal) {
|
|
1499
1545
|
return 'review-release';
|
|
1500
1546
|
}
|
|
@@ -1778,7 +1824,7 @@ const { pathToFileURL } = require('url');
|
|
|
1778
1824
|
}
|
|
1779
1825
|
|
|
1780
1826
|
function buildCompletionState({ executionMode = null, verificationRecommendation = null } = {}) {
|
|
1781
|
-
if (!executionMode) {
|
|
1827
|
+
if (!executionMode || executionMode === 'informational') {
|
|
1782
1828
|
return null;
|
|
1783
1829
|
}
|
|
1784
1830
|
|
|
@@ -2331,8 +2377,9 @@ const { pathToFileURL } = require('url');
|
|
|
2331
2377
|
const now = Date.now();
|
|
2332
2378
|
const debounceMs = 10 * 60 * 1000;
|
|
2333
2379
|
const previousTs = Number(previous?.ts || 0);
|
|
2380
|
+
const informationalNoTargetPrompt = isInformationalPrompt({ promptText, commandText }) && !filePath;
|
|
2334
2381
|
|
|
2335
|
-
if (active.length === 0) {
|
|
2382
|
+
if (active.length === 0 || informationalNoTargetPrompt) {
|
|
2336
2383
|
const lastExplicitUserPromptText = promptText.trim()
|
|
2337
2384
|
|| previous?.routingContext?.lastExplicitUserPromptText
|
|
2338
2385
|
|| '';
|
|
@@ -2343,6 +2390,7 @@ const { pathToFileURL } = require('url');
|
|
|
2343
2390
|
targetFile: filePath,
|
|
2344
2391
|
contextIntent: '',
|
|
2345
2392
|
taskType: '',
|
|
2393
|
+
...(informationalNoTargetPrompt ? { executionMode: 'informational' } : {}),
|
|
2346
2394
|
};
|
|
2347
2395
|
const previousContext = buildPreviousContextSnapshot({
|
|
2348
2396
|
projectRoot,
|