@ngockhoale/ukit 2.0.1 → 2.0.3
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 +34 -0
- package/manifests/platform.full.yaml +23 -0
- package/package.json +1 -1
- package/src/cli/commands/memory.js +32 -1
- package/templates/.claude/hooks/context-hardcap-gate.sh +102 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +25 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/project-learning/SKILL.md +1 -1
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +28 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +14 -1
- package/templates/docs/AI_HANDOFF/RULES.md +2 -0
- package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +12 -1
- package/templates/ukit/storage/config.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.0.3 - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`ukit memory propose "<text>" [--category <name>] [--project <id>]`** — the missing first step of the Project Pattern Learning loop. Wires the existing `proposePatternCandidate()` to the CLI so a candidate can actually be created; duplicate proposals of the same text return the existing pending entry instead of stacking.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Project Pattern Learning was unreachable in every real install.** v2.0.0 shipped `ukit memory list --pending` / `approve` / `reject`, but nothing could create a candidate: `proposePatternCandidate()` had zero production callers, and the `project-learning` skill told the AI to import it from `src/core/memory/store.js` — a path that only exists inside the UKit repo, never in an installed project. The skill now points at the new CLI subcommand, and `tests/cli/memoryCommand.test.js` drives the loop through the CLI only, so a fix that works solely via a direct function import cannot make those tests pass.
|
|
14
|
+
- **The v2.0.0 handoff Interfaces block never shipped.** `Consumes`/`Produces` were added to the repo's own `docs/AI_HANDOFF/RULES.md` and `tasks/_TEMPLATE.md` but not to the `templates/docs/AI_HANDOFF/` copies that `ukit install` actually delivers, so installed projects got handoff docs without the feature the 2.0.0 changelog claims. Same dev-mirror divergence class as the 2.0.2 hard-cap hooks.
|
|
15
|
+
|
|
16
|
+
## 2.0.2 - 2026-08-11
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Absolute context hard cap** — new `compact.hardCapTokens` (default `220000`) and `compact.hardCapBlock` (default `true`). Crossing the cap makes `context-hardcap-gate.sh` refuse `Edit`/`Write`/`Bash` outright until real compaction happens, instead of only printing a suggestion. `Read`/`Grep`/`Glob` stay open so the agent can still explain the block — only a human can invoke compaction. This is a new, separate ceiling: the existing soft/hard advisory phases (`compact.tokenThreshold` 50k, derived hard 80k) are unchanged.
|
|
21
|
+
- **`reset-compact-pressure.sh`** (SessionStart) — zeroes the compact pressure tracker at session start, so the cap measures the current session rather than accumulating over a project's lifetime.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **The hard cap would have shipped dead.** Both new hooks were wired into `settings.json` but had no entry in `manifests/platform.full.yaml`, so `ukit install` never copied them: real installs got `settings.json` pointing at scripts that do not exist. Found by a scratch-project install, not by the test suite. Added a `tests/manifest/settingsHookCoverage.test.js` regression guard that fails whenever a hook is wired without a manifest entry, or a manifest hook's `sourceTemplate` is missing, or the dev mirror and template `settings.json` disagree.
|
|
26
|
+
- **Permanent-block deadlock.** `compact-pressure.json` accumulated forever, so the cap would eventually block `Edit`/`Write`/`Bash` in every future session even with tiny context — and because the gate covers `Bash`, the documented remedies could not run. Fixed by the SessionStart reset plus a PreCompact reset.
|
|
27
|
+
- **Reset skipped on cache hit.** The PreCompact reset sat after `reinject-context.mjs`'s prompt-cache early return, so a cache hit left the gate tripped permanently. Moved ahead of the return.
|
|
28
|
+
- **Gate bypass via `NotebookEdit`/`MultiEdit`.** The gate compared `tool_name` exactly while the `settings.json` matcher is a regex (`Edit|Write` also matches both), so those tools fired the hook and passed through it. Now matched against an explicit tool set.
|
|
29
|
+
- Prompt cache in `reinject-context.mjs` no longer keys on the live pressure value when the threshold plan is inactive (where it cannot affect output), which had turned every run into a cache miss.
|
|
30
|
+
- `compact.hardCapTokens` reads straight from config instead of deriving from the advisory hard threshold, so raising `tokenThreshold` can never silently raise the ceiling.
|
|
31
|
+
|
|
32
|
+
## 2.0.1 - 2026-08-10
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `ukit memory approve` / `reject` accept an optional `--project <id>` flag, so a pattern candidate belonging to a non-current project (surfaced by `list --pending`'s cross-project scan) can actually be resolved instead of failing "not found".
|
|
37
|
+
- `ukit memory list` reports a conventions count alongside decisions, so an approval's effect is visible.
|
|
38
|
+
|
|
5
39
|
## 2.0.0 - 2026-08-10
|
|
6
40
|
|
|
7
41
|
### Added
|
|
@@ -1098,6 +1098,29 @@ items:
|
|
|
1098
1098
|
packs:
|
|
1099
1099
|
- core
|
|
1100
1100
|
|
|
1101
|
+
- id: hook-context-hardcap-gate
|
|
1102
|
+
type: hook
|
|
1103
|
+
sourceTemplate: .claude/hooks/context-hardcap-gate.sh
|
|
1104
|
+
targetPath: .claude/hooks/context-hardcap-gate.sh
|
|
1105
|
+
requires:
|
|
1106
|
+
- ukit-runtime-compact-threshold-script
|
|
1107
|
+
mergeStrategy: overwrite_with_backup
|
|
1108
|
+
variables: []
|
|
1109
|
+
enabledByDefault: true
|
|
1110
|
+
packs:
|
|
1111
|
+
- core
|
|
1112
|
+
|
|
1113
|
+
- id: hook-reset-compact-pressure
|
|
1114
|
+
type: hook
|
|
1115
|
+
sourceTemplate: .claude/hooks/reset-compact-pressure.sh
|
|
1116
|
+
targetPath: .claude/hooks/reset-compact-pressure.sh
|
|
1117
|
+
requires: []
|
|
1118
|
+
mergeStrategy: overwrite_with_backup
|
|
1119
|
+
variables: []
|
|
1120
|
+
enabledByDefault: true
|
|
1121
|
+
packs:
|
|
1122
|
+
- core
|
|
1123
|
+
|
|
1101
1124
|
- id: hook-compress-output
|
|
1102
1125
|
type: hook
|
|
1103
1126
|
sourceTemplate: .claude/hooks/compress-output.sh
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
forgetMemoryItem,
|
|
4
4
|
listMemoryItems,
|
|
5
5
|
listPendingPatternCandidates,
|
|
6
|
+
proposePatternCandidate,
|
|
6
7
|
resolvePatternCandidate,
|
|
7
8
|
runProjectHygiene,
|
|
8
9
|
} from '../../core/memory/store.js';
|
|
@@ -15,6 +16,15 @@ import fs from 'node:fs/promises';
|
|
|
15
16
|
|
|
16
17
|
const HELP_FLAGS = new Set(['--help', '-h', 'help']);
|
|
17
18
|
|
|
19
|
+
function extractFlag(args, flag) {
|
|
20
|
+
const index = args.indexOf(flag);
|
|
21
|
+
if (index < 0) return { value: null, rest: args };
|
|
22
|
+
return {
|
|
23
|
+
value: args[index + 1] ?? null,
|
|
24
|
+
rest: [...args.slice(0, index), ...args.slice(index + 2)],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
|
|
19
29
|
let entries = [];
|
|
20
30
|
try {
|
|
@@ -75,6 +85,26 @@ export async function runMemory({ projectRoot, argv = [] }) {
|
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
if (subcommand === 'propose') {
|
|
89
|
+
const projectFlag = extractFlag(rest, '--project');
|
|
90
|
+
const categoryFlag = extractFlag(projectFlag.rest, '--category');
|
|
91
|
+
const text = categoryFlag.rest.join(' ').trim();
|
|
92
|
+
if (!text) {
|
|
93
|
+
throw new Error('Missing pattern text. Usage: ukit memory propose "<text>" [--category <name>] [--project <id>]');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectId = projectFlag.value ?? (await detectProjectContext(projectRoot)).project.name;
|
|
97
|
+
const candidate = await proposePatternCandidate(projectRoot, projectId, {
|
|
98
|
+
text,
|
|
99
|
+
category: categoryFlag.value,
|
|
100
|
+
detectedFrom: 'cli',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log(`[UKit] Proposed ${candidate.id} (${candidate.status}) for project ${projectId}.`);
|
|
104
|
+
console.log('[UKit] Review with `ukit memory list --pending`, then `ukit memory approve <id>`.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
if (subcommand === 'approve' || subcommand === 'reject') {
|
|
79
109
|
const projectFlagIndex = rest.indexOf('--project');
|
|
80
110
|
const explicitProjectId = projectFlagIndex >= 0 ? rest[projectFlagIndex + 1] : null;
|
|
@@ -186,7 +216,7 @@ export async function runMemory({ projectRoot, argv = [] }) {
|
|
|
186
216
|
|
|
187
217
|
export function printMemoryHelp() {
|
|
188
218
|
console.log('UKit Memory Commands');
|
|
189
|
-
console.log('Usage: ukit memory <list|search|recall|forget|export|approve|reject|hygiene> [args]');
|
|
219
|
+
console.log('Usage: ukit memory <list|search|recall|forget|export|propose|approve|reject|hygiene> [args]');
|
|
190
220
|
console.log('');
|
|
191
221
|
console.log('Subcommands:');
|
|
192
222
|
console.log(' list List memory items in shared .ukit/storage/memory');
|
|
@@ -195,6 +225,7 @@ export function printMemoryHelp() {
|
|
|
195
225
|
console.log(' recall <task> Print a compact previous-context block for the current task');
|
|
196
226
|
console.log(' forget <id> Remove one memory item by id');
|
|
197
227
|
console.log(' export Print all memory as JSON');
|
|
228
|
+
console.log(' propose "<text>" [--category <name>] [--project <id>] Propose a project convention for human approval');
|
|
198
229
|
console.log(' approve <id> [--project <id>] Approve a pending pattern candidate into project conventions');
|
|
199
230
|
console.log(' reject <id> [--project <id>] Reject a pending pattern candidate');
|
|
200
231
|
console.log(' hygiene [--project <id>] Run decision-conflict resolution + session archiving now');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: hard-enforce an absolute context token cap (compact.hardCapTokens,
|
|
3
|
+
# default 220000), separate from the soft/hard advisory pressure phases in
|
|
4
|
+
# compact-threshold.mjs (default soft=50000/hard=80000, which only print a suggestion).
|
|
5
|
+
#
|
|
6
|
+
# Those advisory phases are just injected text — nothing stops the agent from ignoring
|
|
7
|
+
# them and letting a session run to hundreds of thousands of tokens with no compaction.
|
|
8
|
+
# This gate is the backstop: once estimatedTotalTokens >= hardCapTokens, Edit/Write/Bash
|
|
9
|
+
# are refused (exit 2) until a real compaction happens. Real compaction is detected via
|
|
10
|
+
# the PreCompact hook (reinject-context.mjs), which resets the tracked counter — see
|
|
11
|
+
# resetCompactPressureState in compact-threshold.mjs. No advisory bypass, no exceptions.
|
|
12
|
+
#
|
|
13
|
+
# Matcher is Edit|Write|Bash only. Read/Grep/Glob stay open so the agent can still
|
|
14
|
+
# investigate, report status, and tell the user to run /compact — gating those too would
|
|
15
|
+
# just brick the session with no way out, since only the user (not the agent) can invoke
|
|
16
|
+
# real compaction.
|
|
17
|
+
#
|
|
18
|
+
# Config toggle: compact.hardCapBlock (default true). Set to false only to debug this
|
|
19
|
+
# gate itself; it must not become a normal escape hatch.
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
23
|
+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
|
|
25
|
+
INPUT="$INPUT" PROJECT_ROOT="$PROJECT_ROOT" HOOK_DIR="$HOOK_DIR" node <<'NODE'
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { pathToFileURL } = require('url');
|
|
29
|
+
|
|
30
|
+
const payload = (() => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(process.env.INPUT || '');
|
|
33
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
const projectRoot = process.env.PROJECT_ROOT;
|
|
40
|
+
const hookDir = process.env.HOOK_DIR;
|
|
41
|
+
const toolName = payload?.tool_name || '';
|
|
42
|
+
|
|
43
|
+
// Gate only mutating/costly tools. Everything else (Read, Grep, Glob, TodoWrite, ...)
|
|
44
|
+
// stays free so the agent can still respond and tell the user to compact.
|
|
45
|
+
// Listed explicitly because the settings.json matcher is a regex ("Edit|Write") that also
|
|
46
|
+
// matches NotebookEdit/MultiEdit -- an exact !== comparison would let those slip through
|
|
47
|
+
// the gate while still firing the hook.
|
|
48
|
+
const GATED_TOOLS = new Set(['Edit', 'Write', 'Bash', 'NotebookEdit', 'MultiEdit']);
|
|
49
|
+
if (!GATED_TOOLS.has(toolName)) {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonSafe(filePath, fallback = null) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
(async () => {
|
|
62
|
+
const config = readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'config.json'), {}) || {};
|
|
63
|
+
if (config?.compact?.hardCapBlock === false) {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const thresholdModulePath = path.join(hookDir, '..', 'ukit', 'runtime', 'compact-threshold.mjs');
|
|
69
|
+
if (!fs.existsSync(thresholdModulePath)) {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mod = await import(pathToFileURL(thresholdModulePath).href);
|
|
75
|
+
const pressurePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'compact-pressure.json');
|
|
76
|
+
const rawState = readJsonSafe(pressurePath, null);
|
|
77
|
+
const state = mod.buildCompactPressureState(rawState, config);
|
|
78
|
+
const thresholds = mod.buildCompactThresholds(config);
|
|
79
|
+
|
|
80
|
+
if (state.estimatedTotalTokens < thresholds.hardCapTokens) {
|
|
81
|
+
process.exit(0);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lines = [
|
|
86
|
+
`BLOCKED (context hard cap): estimated context ~${state.estimatedTotalTokens} tokens >= hard cap ${thresholds.hardCapTokens}.`,
|
|
87
|
+
'This is an absolute ceiling (compact.hardCapTokens), separate from the soft/hard advisory phases — those were apparently not followed.',
|
|
88
|
+
`Edit/Write/Bash refused (tool_name=${toolName}) until real compaction happens.`,
|
|
89
|
+
'Remedy (any one): run /compact, or start a new session (SessionStart resets the counter), or delete .ukit/storage/cache/compact-pressure.json.',
|
|
90
|
+
'Do not work around this by summarizing inline and continuing, and do not reach for a non-gated write tool.',
|
|
91
|
+
];
|
|
92
|
+
process.stderr.write(`${lines.join('\n')}\n`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
})().catch((err) => {
|
|
95
|
+
// Unlike vision-gate.sh, a logic error here fails OPEN: this is a backstop on top of
|
|
96
|
+
// advisory nudges, not a correctness gate — a broken gate must not brick every session.
|
|
97
|
+
process.stderr.write(`context-hardcap-gate: internal error, failing open: ${err?.message ?? err}\n`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
NODE
|
|
101
|
+
|
|
102
|
+
exit $?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SessionStart hook: zero the compact pressure tracker at the start of every session.
|
|
3
|
+
#
|
|
4
|
+
# compact-pressure.json is a single per-project file whose sessionTokens counter only ever
|
|
5
|
+
# accumulates (see registerPromptPressure/registerOutputPressure in compact-threshold.mjs).
|
|
6
|
+
# Without this reset it sums every prompt and every command output across the entire
|
|
7
|
+
# lifetime of the project, so it eventually crosses compact.hardCapTokens and permanently
|
|
8
|
+
# blocks Edit/Write/Bash in every future session -- even when actual context is tiny.
|
|
9
|
+
# That is a deadlock: the gate blocks Bash, so the documented remedies (ukit install,
|
|
10
|
+
# ukit doctor) cannot run, and compaction never fires because real context is small.
|
|
11
|
+
#
|
|
12
|
+
# Resetting here makes the counter measure THIS session only, which is what the hard cap
|
|
13
|
+
# is actually trying to approximate. It also gives the gate a guaranteed escape hatch:
|
|
14
|
+
# restarting the session always clears a stuck gate, with no manual file surgery.
|
|
15
|
+
#
|
|
16
|
+
# Always exits 0 -- a failure here must never prevent a session from starting.
|
|
17
|
+
|
|
18
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
19
|
+
PRESSURE_FILE="$PROJECT_ROOT/.ukit/storage/cache/compact-pressure.json"
|
|
20
|
+
|
|
21
|
+
if [ -f "$PRESSURE_FILE" ]; then
|
|
22
|
+
rm -f "$PRESSURE_FILE" 2>/dev/null || true
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
exit 0
|
|
@@ -87,6 +87,11 @@
|
|
|
87
87
|
"type": "command",
|
|
88
88
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/vision-gate.sh\"",
|
|
89
89
|
"timeout": 8
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "command",
|
|
93
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\"",
|
|
94
|
+
"timeout": 8
|
|
90
95
|
}
|
|
91
96
|
]
|
|
92
97
|
},
|
|
@@ -117,6 +122,11 @@
|
|
|
117
122
|
"type": "command",
|
|
118
123
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\"",
|
|
119
124
|
"timeout": 8
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"type": "command",
|
|
128
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\"",
|
|
129
|
+
"timeout": 8
|
|
120
130
|
}
|
|
121
131
|
]
|
|
122
132
|
}
|
|
@@ -177,6 +187,11 @@
|
|
|
177
187
|
"type": "command",
|
|
178
188
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-prune-bash.sh\"",
|
|
179
189
|
"timeout": 8
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"type": "command",
|
|
193
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/reset-compact-pressure.sh\"",
|
|
194
|
+
"timeout": 8
|
|
180
195
|
}
|
|
181
196
|
]
|
|
182
197
|
}
|
|
@@ -18,7 +18,7 @@ One-off patterns are not conventions. Skip this for anything seen only once.
|
|
|
18
18
|
|
|
19
19
|
## What NOT to do
|
|
20
20
|
|
|
21
|
-
Never write directly into `conventions`, and never mention this to the human as if it's already decided. Always propose through
|
|
21
|
+
Never write directly into `conventions`, and never mention this to the human as if it's already decided. Always propose through the CLI — run `ukit memory propose "<text>" [--category <name>]`, then tell the human to check `ukit memory list --pending`. Phrase the candidate as one falsifiable sentence:
|
|
22
22
|
|
|
23
23
|
- Good: "Use `ControlInput` from `src/components/` instead of raw `<input>` for form fields."
|
|
24
24
|
- Bad: "This project seems to like custom inputs."
|
|
@@ -437,14 +437,42 @@ export function buildCompactThresholds(config = {}) {
|
|
|
437
437
|
const softThreshold = Math.max(1, finiteNumber(config?.compact?.tokenThreshold, 50_000));
|
|
438
438
|
const hardThreshold = Math.max(softThreshold + 1, Math.round(softThreshold * 1.6));
|
|
439
439
|
const baselineTokens = Math.max(120, Math.min(18_000, Math.round(softThreshold * 0.18)));
|
|
440
|
+
// Deliberately NOT coupled to hardThreshold: this is an absolute ceiling, so raising
|
|
441
|
+
// the advisory tokenThreshold must never silently raise the cap along with it.
|
|
442
|
+
const hardCapTokens = Math.max(1, finiteNumber(config?.compact?.hardCapTokens, 220_000));
|
|
440
443
|
|
|
441
444
|
return {
|
|
442
445
|
softThreshold,
|
|
443
446
|
hardThreshold,
|
|
444
447
|
baselineTokens,
|
|
448
|
+
hardCapTokens,
|
|
445
449
|
};
|
|
446
450
|
}
|
|
447
451
|
|
|
452
|
+
// Absolute, non-negotiable ceiling — separate from the soft/hard advisory phases above.
|
|
453
|
+
// Crossing this means the advisory nudges were not followed; real compaction is mandatory.
|
|
454
|
+
export function isHardCapExceeded(estimatedTotalTokens, config = {}) {
|
|
455
|
+
const { hardCapTokens } = buildCompactThresholds(config);
|
|
456
|
+
return finiteNumber(estimatedTotalTokens, 0) >= hardCapTokens;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Called from the PreCompact hook path once real compaction has happened, so the
|
|
460
|
+
// hard-cap gate unblocks on the next tool call instead of staying tripped forever.
|
|
461
|
+
export function resetCompactPressureState(rawState = {}, config = {}) {
|
|
462
|
+
const current = buildCompactPressureState(rawState, config);
|
|
463
|
+
return buildCompactPressureState({
|
|
464
|
+
...current,
|
|
465
|
+
updatedAt: Date.now(),
|
|
466
|
+
sessionTokens: 0,
|
|
467
|
+
estimatedContextTokens: 0,
|
|
468
|
+
estimatedTotalTokens: 0,
|
|
469
|
+
recentPrompts: [],
|
|
470
|
+
recentOutputs: [],
|
|
471
|
+
cooldownUntil: 0,
|
|
472
|
+
latestPlan: null,
|
|
473
|
+
}, config);
|
|
474
|
+
}
|
|
475
|
+
|
|
448
476
|
function buildReleaseThresholds(thresholds = buildCompactThresholds()) {
|
|
449
477
|
const softReleaseThreshold = Math.max(
|
|
450
478
|
Math.min(thresholds.softThreshold, Math.round(thresholds.softThreshold * SOFT_RELEASE_RATIO)),
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
buildThresholdCompactPlan,
|
|
16
16
|
readCompactPressureState,
|
|
17
|
+
resetCompactPressureState,
|
|
18
|
+
writeCompactPressureState,
|
|
17
19
|
writeThresholdCompactPlan,
|
|
18
20
|
} from './compact-threshold.mjs';
|
|
19
21
|
|
|
@@ -62,6 +64,14 @@ async function main() {
|
|
|
62
64
|
const rawBodyLines = [...rawStaticLines, ...dynamicLines];
|
|
63
65
|
const bodyLines = [...staticLines, ...dynamicLines];
|
|
64
66
|
const pressureState = await readCompactPressureState(projectRoot, config);
|
|
67
|
+
|
|
68
|
+
// Reset the hard-cap tracker as early as possible: this hook fires on every real
|
|
69
|
+
// compaction, and it is the only signal that context actually shrank. It must happen
|
|
70
|
+
// before the promptCache early-return below, otherwise a cache hit would skip it and
|
|
71
|
+
// leave context-hardcap-gate.sh permanently tripped. Downstream writes
|
|
72
|
+
// (writeThresholdCompactPlan) re-read from disk, so they build on the zeroed state and
|
|
73
|
+
// keep their own stats increments intact.
|
|
74
|
+
await writeCompactPressureState(projectRoot, resetCompactPressureState(pressureState, config), config);
|
|
65
75
|
const anchorLines = dynamicLines.filter((line) => (
|
|
66
76
|
line.startsWith('- Recent routed lane:')
|
|
67
77
|
|| line.startsWith('- Current approach:')
|
|
@@ -93,7 +103,10 @@ async function main() {
|
|
|
93
103
|
activeSkills: Array.isArray(state?.activeSkills) ? state.activeSkills.map((item) => getActiveSkillPath(item)) : [],
|
|
94
104
|
bodyLines: thresholdPlan.active ? thresholdPlan.lines : bodyLines,
|
|
95
105
|
thresholdPhase: thresholdPlan.active ? thresholdPlan.phase : 'monitor',
|
|
96
|
-
|
|
106
|
+
// Only key on the token count when the threshold plan is active — that is the only case
|
|
107
|
+
// where it changes the rendered output. Keying on the live pressure value while inactive
|
|
108
|
+
// would miss the cache on every run, since the reset above zeroes it between runs.
|
|
109
|
+
estimatedTotalTokens: thresholdPlan.active ? thresholdPlan.estimatedTotalTokens : 0,
|
|
97
110
|
});
|
|
98
111
|
|
|
99
112
|
if (config.tokenPipeline?.promptCache) {
|
|
@@ -106,6 +106,8 @@ A task is `ready` only when it has:
|
|
|
106
106
|
- Clear target files
|
|
107
107
|
- Clear action
|
|
108
108
|
- Dependencies stated
|
|
109
|
+
- **Interfaces** — Consumes/Produces với chữ ký thật (function/endpoint/type), không placeholder;
|
|
110
|
+
`(none)` hợp lệ nếu task không có input/output liên task
|
|
109
111
|
- **Test Plan** (PLAN.md §4) — happy path + ≥1 edge case (+ regression test nếu fix bug); hoặc `N/A` kèm lý do
|
|
110
112
|
- Verification command (lệnh executor sẽ chạy)
|
|
111
113
|
- Acceptance criteria
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!--
|
|
4
4
|
Template cho mỗi task. Planner copy file này khi split PLAN.md sang task riêng.
|
|
5
|
-
File này BẮT BUỘC giữ structure: Goal + Test Cases + Test Files + Verification + Acceptance.
|
|
5
|
+
File này BẮT BUỘC giữ structure: Goal + Test Cases + Test Files + Verification + Acceptance + Interfaces.
|
|
6
6
|
Mọi AI (planner / executor / reviewer) đọc và ghi vào file NÀY. Không trao đổi ngoài file.
|
|
7
7
|
-->
|
|
8
8
|
|
|
@@ -48,6 +48,17 @@ yarn test tests/path/to/file.test.js
|
|
|
48
48
|
|
|
49
49
|
- (none) <!-- hoặc TASK-xxx phải done trước -->
|
|
50
50
|
|
|
51
|
+
## Interfaces
|
|
52
|
+
|
|
53
|
+
<!--
|
|
54
|
+
Executor thường CHỈ thấy task file này, không thấy task khác. Block này là cách nó biết đúng
|
|
55
|
+
tên/kiểu mà task khác kỳ vọng — tránh lỗi kiểu "TASK-3 gọi clearLayers() nhưng TASK-7 lại gọi
|
|
56
|
+
clearFullLayers()". Ghi chữ ký thật (function/endpoint/type), không ghi placeholder.
|
|
57
|
+
-->
|
|
58
|
+
|
|
59
|
+
- Consumes: `<what this task uses from earlier tasks — exact function/endpoint signatures, types>` <!-- hoặc (none) -->
|
|
60
|
+
- Produces: `<what later tasks rely on from this task — exact function/endpoint signatures, types>` <!-- hoặc (none) -->
|
|
61
|
+
|
|
51
62
|
---
|
|
52
63
|
|
|
53
64
|
## Discussion
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"compact": {
|
|
10
10
|
"enabled": true,
|
|
11
11
|
"tokenThreshold": 50000,
|
|
12
|
+
"hardCapTokens": 220000,
|
|
13
|
+
"hardCapBlock": true,
|
|
12
14
|
"contextRotDetection": true,
|
|
13
15
|
"askBeforeDrop": true,
|
|
14
16
|
"codexContext": {
|
|
@@ -367,6 +369,8 @@
|
|
|
367
369
|
"compact": {
|
|
368
370
|
"enabled": "Bật/tắt toàn bộ helper compact của UKit.",
|
|
369
371
|
"tokenThreshold": "Ngưỡng token chung cho runtime compact dùng chung.",
|
|
372
|
+
"hardCapTokens": "Ngưỡng cứng tuyệt đối (mặc định 220000 token ước lượng). Chạm/vượt ngưỡng này thì context coi như quá dài — không phải gợi ý nữa, là bắt buộc.",
|
|
373
|
+
"hardCapBlock": "Nếu true, hook context-hardcap-gate chặn cứng Edit/Write/Bash (exit 2) khi vượt hardCapTokens, cho tới khi có compact thật (PreCompact) reset lại bộ đếm. Không có ngoại lệ.",
|
|
370
374
|
"contextRotDetection": "Phát hiện context quá dài/dễ mục để giữ lại state quan trọng trước khi AI nhớ sai.",
|
|
371
375
|
"askBeforeDrop": "Giữ thái độ thận trọng trước khi bỏ context quan trọng. Nếu rủi ro thì hand back cho main model.",
|
|
372
376
|
"agentContext": {
|