@ngockhoale/ukit 2.3.7 → 2.3.9
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 +71 -0
- package/package.json +1 -1
- package/src/core/compact/threshold.js +109 -6
- package/templates/.claude/agents/bug-debugger.md +2 -2
- package/templates/.claude/agents/feature-implementer.md +5 -3
- package/templates/.claude/hooks/auto-allow-bash.sh +115 -71
- package/templates/.claude/hooks/auto-prune-bash.sh +88 -30
- package/templates/.claude/hooks/context-hardcap-gate.sh +28 -7
- package/templates/.claude/hooks/context-window-guard.sh +1 -1
- package/templates/.claude/hooks/reset-compact-pressure.sh +117 -13
- package/templates/.claude/hooks/verification-guard.sh +87 -19
- package/templates/.claude/ukit/index/cache-utils.mjs +53 -29
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +120 -11
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +52 -20
- package/templates/.claude/ukit/runtime/output-compression.mjs +10 -2
- package/templates/.claude/ukit/runtime/reinject-context.mjs +30 -4
- package/templates/.omp/agents/bug-debugger.md +2 -2
- package/templates/.omp/agents/feature-implementer.md +5 -3
|
@@ -43,6 +43,15 @@ function finiteNumber(value, fallback = 0) {
|
|
|
43
43
|
return Number.isFinite(number) ? number : fallback;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// A malformed hard cap must fail back to a safe ceiling, never collapse to one token and
|
|
47
|
+
// brick every Edit/Write/Bash call. Unlike token estimates, configuration is an operator
|
|
48
|
+
// contract: accept only whole positive token counts.
|
|
49
|
+
function positiveInteger(value, fallback) {
|
|
50
|
+
return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0
|
|
51
|
+
? value
|
|
52
|
+
: fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function uniqueStrings(values) {
|
|
47
56
|
const seen = new Set();
|
|
48
57
|
const unique = [];
|
|
@@ -452,7 +461,7 @@ export function buildCompactThresholds(config = {}) {
|
|
|
452
461
|
// env.CLAUDE_CODE_AUTO_COMPACT_WINDOW and omp's compaction.thresholdTokens are rendered
|
|
453
462
|
// from this number at install time (70% of it), so the client auto-compacts before the
|
|
454
463
|
// gate blocks tools without anyone hand-syncing a second value.
|
|
455
|
-
const hardCapTokens =
|
|
464
|
+
const hardCapTokens = positiveInteger(config?.compact?.hardCapTokens, 500_000);
|
|
456
465
|
|
|
457
466
|
return {
|
|
458
467
|
softThreshold,
|
|
@@ -668,7 +677,71 @@ export function resolveThresholdCompactBudget({
|
|
|
668
677
|
};
|
|
669
678
|
}
|
|
670
679
|
|
|
680
|
+
// ---- session-scoped pressure document ---------------------------------------
|
|
681
|
+
// compact-pressure.json holds ONE record per session id. A session starting must never
|
|
682
|
+
// zero another live session's pressure bookkeeping (its hard-cap gate and compact
|
|
683
|
+
// advisories), and a brand-new session must not inherit a sibling's totals. The newest
|
|
684
|
+
// record's flat fields stay projected at the document's top level so readers that predate
|
|
685
|
+
// this shape (raw JSON readers) still see a coherent state.
|
|
686
|
+
const PRESSURE_SESSIONS_MAX = 8;
|
|
687
|
+
|
|
688
|
+
function normalizeSessionId(value) {
|
|
689
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
690
|
+
return id || null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function readPressureDocument(raw) {
|
|
694
|
+
if (raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object') {
|
|
695
|
+
const sessions = {};
|
|
696
|
+
for (const [id, record] of Object.entries(raw.sessions)) {
|
|
697
|
+
if (id && record && typeof record === 'object') {
|
|
698
|
+
sessions[id] = record;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return sessions;
|
|
702
|
+
}
|
|
703
|
+
// Legacy single-record file: preserve the in-flight session under a shared bucket.
|
|
704
|
+
return raw && typeof raw === 'object' ? { default: raw } : {};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function pickPressureSession(sessions, sessionId) {
|
|
708
|
+
if (sessionId) {
|
|
709
|
+
// A session id that has no record yet starts clean — it must not inherit another
|
|
710
|
+
// session's totals, and the other way round nothing here touches that session.
|
|
711
|
+
return { id: sessionId, record: sessions[sessionId] ?? null };
|
|
712
|
+
}
|
|
713
|
+
const entries = Object.entries(sessions);
|
|
714
|
+
if (!entries.length) {
|
|
715
|
+
return { id: 'default', record: null };
|
|
716
|
+
}
|
|
717
|
+
const newest = entries
|
|
718
|
+
.sort(([, left], [, right]) => finiteNumber(right?.updatedAt, 0) - finiteNumber(left?.updatedAt, 0))[0];
|
|
719
|
+
return { id: newest[0], record: newest[1] };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function resolvePressureRecord(raw, config) {
|
|
723
|
+
if (!raw || typeof raw !== 'object' || !raw.sessions || typeof raw.sessions !== 'object') {
|
|
724
|
+
return raw; // flat legacy state — normalized as-is below
|
|
725
|
+
}
|
|
726
|
+
const { record } = pickPressureSession(readPressureDocument(raw), normalizeSessionId(config?.sessionId));
|
|
727
|
+
return record;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function projectPressureDocument(sessions) {
|
|
731
|
+
const newestFirst = Object.entries(sessions)
|
|
732
|
+
.sort(([, left], [, right]) => finiteNumber(right?.updatedAt, 0) - finiteNumber(left?.updatedAt, 0))
|
|
733
|
+
.slice(0, PRESSURE_SESSIONS_MAX);
|
|
734
|
+
const newestRecord = newestFirst[0]?.[1] ?? null;
|
|
735
|
+
return {
|
|
736
|
+
...(newestRecord && typeof newestRecord === 'object' ? newestRecord : {}),
|
|
737
|
+
updatedAt: finiteNumber(newestRecord?.updatedAt, Date.now()),
|
|
738
|
+
v: 2,
|
|
739
|
+
sessions: Object.fromEntries(newestFirst),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
671
743
|
export function buildCompactPressureState(rawState = null, config = {}) {
|
|
744
|
+
rawState = resolvePressureRecord(rawState, config);
|
|
672
745
|
const thresholds = buildCompactThresholds(config);
|
|
673
746
|
const rawSoftThreshold = finiteNumber(rawState?.softThreshold, 0);
|
|
674
747
|
const rawHardThreshold = finiteNumber(rawState?.hardThreshold, 0);
|
|
@@ -1056,13 +1129,18 @@ export async function readCompactPressureState(projectRoot, config = {}) {
|
|
|
1056
1129
|
// All compact-pressure mutations share one lock on the state file: without it,
|
|
1057
1130
|
// concurrent hook processes (parallel subagents) and same-process flows interleave
|
|
1058
1131
|
// their read-modify-write cycles and silently drop each other's sessionTokens.
|
|
1132
|
+
// Mutations are session-scoped: only the calling session's record is rewritten;
|
|
1133
|
+
// sibling session records pass through untouched.
|
|
1059
1134
|
async function mutateCompactPressureState(projectRoot, mutator, config = {}) {
|
|
1060
1135
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
1061
1136
|
return withFileLock(runtimePaths.compactPressurePath, async () => {
|
|
1062
|
-
const
|
|
1137
|
+
const sessions = readPressureDocument(await readJson(runtimePaths.compactPressurePath, null));
|
|
1138
|
+
const { id, record } = pickPressureSession(sessions, normalizeSessionId(config?.sessionId));
|
|
1139
|
+
const current = buildCompactPressureState(record, config);
|
|
1063
1140
|
const next = mutator(current);
|
|
1064
1141
|
const normalized = buildCompactPressureState(next, config);
|
|
1065
|
-
|
|
1142
|
+
sessions[id] = { ...normalized, updatedAt: Date.now() };
|
|
1143
|
+
await writeJson(runtimePaths.compactPressurePath, projectPressureDocument(sessions));
|
|
1066
1144
|
return normalized;
|
|
1067
1145
|
});
|
|
1068
1146
|
}
|
|
@@ -1070,22 +1148,42 @@ async function mutateCompactPressureState(projectRoot, mutator, config = {}) {
|
|
|
1070
1148
|
export async function writeCompactPressureState(projectRoot, state, config = {}) {
|
|
1071
1149
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
1072
1150
|
return withFileLock(runtimePaths.compactPressurePath, async () => {
|
|
1151
|
+
const sessions = readPressureDocument(await readJson(runtimePaths.compactPressurePath, null));
|
|
1152
|
+
const { id } = pickPressureSession(sessions, normalizeSessionId(config?.sessionId));
|
|
1073
1153
|
const normalized = buildCompactPressureState(state, config);
|
|
1074
|
-
|
|
1154
|
+
sessions[id] = { ...normalized, updatedAt: Date.now() };
|
|
1155
|
+
await writeJson(runtimePaths.compactPressurePath, projectPressureDocument(sessions));
|
|
1075
1156
|
return normalized;
|
|
1076
1157
|
});
|
|
1077
1158
|
}
|
|
1078
1159
|
|
|
1160
|
+
function pressureSessionConfig(payload = {}, config = {}) {
|
|
1161
|
+
const sessionId = normalizeSessionId(payload?.sessionId) ?? normalizeSessionId(config?.sessionId);
|
|
1162
|
+
return sessionId ? { ...config, sessionId } : config;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1079
1165
|
export async function updateCompactPressureFromPrompt(projectRoot, payload, config = {}) {
|
|
1080
|
-
return mutateCompactPressureState(
|
|
1166
|
+
return mutateCompactPressureState(
|
|
1167
|
+
projectRoot,
|
|
1168
|
+
(current) => registerPromptPressure(current, payload, config),
|
|
1169
|
+
pressureSessionConfig(payload, config),
|
|
1170
|
+
);
|
|
1081
1171
|
}
|
|
1082
1172
|
|
|
1083
1173
|
export async function updateCompactPressureFromOutput(projectRoot, payload, config = {}) {
|
|
1084
|
-
return mutateCompactPressureState(
|
|
1174
|
+
return mutateCompactPressureState(
|
|
1175
|
+
projectRoot,
|
|
1176
|
+
(current) => registerOutputPressure(current, payload, config),
|
|
1177
|
+
pressureSessionConfig(payload, config),
|
|
1178
|
+
);
|
|
1085
1179
|
}
|
|
1086
1180
|
|
|
1087
1181
|
export async function writeThresholdCompactPlan(projectRoot, plan, config = {}) {
|
|
1088
|
-
return mutateCompactPressureState(
|
|
1182
|
+
return mutateCompactPressureState(
|
|
1183
|
+
projectRoot,
|
|
1184
|
+
(current) => registerThresholdCompactPlan(current, plan, config),
|
|
1185
|
+
pressureSessionConfig(plan, config),
|
|
1186
|
+
);
|
|
1089
1187
|
}
|
|
1090
1188
|
|
|
1091
1189
|
async function loadRuntimeConfig(projectRoot) {
|
|
@@ -1141,6 +1239,16 @@ async function runCli() {
|
|
|
1141
1239
|
return;
|
|
1142
1240
|
}
|
|
1143
1241
|
|
|
1242
|
+
// Session-scoped bookkeeping: every read/write in this branch targets the calling
|
|
1243
|
+
// session's record, so a sibling session starting in the same project cannot zero
|
|
1244
|
+
// this session's pressure history.
|
|
1245
|
+
const sessionConfig = {
|
|
1246
|
+
...config,
|
|
1247
|
+
sessionId: typeof payload.session_id === 'string' && payload.session_id.trim()
|
|
1248
|
+
? payload.session_id.trim()
|
|
1249
|
+
: undefined,
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1144
1252
|
// Heal a stale counter BEFORE the phase is computed. Auto-compact paths that never
|
|
1145
1253
|
// fire the PreCompact hook leave sessionTokens counting history that was already
|
|
1146
1254
|
// summarised away — that phantom total is how a healthy session got hard-cap blocked.
|
|
@@ -1148,21 +1256,22 @@ async function runCli() {
|
|
|
1148
1256
|
// automatic, so they are the correction signal here.
|
|
1149
1257
|
try {
|
|
1150
1258
|
const synced = await syncCompactPressureStateWithTranscript(
|
|
1151
|
-
await readCompactPressureState(projectRoot,
|
|
1152
|
-
|
|
1259
|
+
await readCompactPressureState(projectRoot, sessionConfig),
|
|
1260
|
+
sessionConfig,
|
|
1153
1261
|
payload.transcript_path,
|
|
1154
1262
|
);
|
|
1155
1263
|
if (synced.reset) {
|
|
1156
|
-
await writeCompactPressureState(projectRoot, synced.state,
|
|
1264
|
+
await writeCompactPressureState(projectRoot, synced.state, sessionConfig);
|
|
1157
1265
|
}
|
|
1158
1266
|
} catch { /* advisory healing only — never block the prompt over it */ }
|
|
1159
1267
|
|
|
1160
1268
|
const nextState = await updateCompactPressureFromPrompt(projectRoot, {
|
|
1161
1269
|
promptText,
|
|
1270
|
+
sessionId: sessionConfig.sessionId,
|
|
1162
1271
|
routingContext: routeState?.routingContext ?? {},
|
|
1163
1272
|
previousContext: routeState?.previousContext ?? null,
|
|
1164
1273
|
routeSummary: routeState?.routeSummary ?? null,
|
|
1165
|
-
},
|
|
1274
|
+
}, sessionConfig);
|
|
1166
1275
|
|
|
1167
1276
|
if (nextState.phase === 'hard') {
|
|
1168
1277
|
process.stdout.write(
|
|
@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
|
|
|
5
5
|
import fsSync from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { withFileLock } from './token-utils.mjs';
|
|
8
9
|
|
|
9
10
|
const LEDGER_VERSION = 1;
|
|
10
11
|
const RESUME_INTENT_VERSION = 1;
|
|
@@ -512,6 +513,28 @@ export function evaluateCompletion({ state = {}, ledger = {} } = {}) {
|
|
|
512
513
|
return { continue: false, notify: false, missingEvidence: [] };
|
|
513
514
|
}
|
|
514
515
|
|
|
516
|
+
// `find-cause` can validly end clean: an investigation may establish that no actionable
|
|
517
|
+
// defect exists. Its contract says a FIX cannot be claimed without write + verification;
|
|
518
|
+
// it does not make a mutation mandatory. Treating missing evidence as an unconditional
|
|
519
|
+
// command to edit made a clean audit self-block forever (Stop -> "make an Edit" -> no
|
|
520
|
+
// honest edit exists -> Stop again). Once a mutation was attempted, retain the normal
|
|
521
|
+
// recovery gate — only a no-mutation, recommend-only investigation that has not already
|
|
522
|
+
// observed a failed verification gets this release valve. A route that named an actionable
|
|
523
|
+
// command, or a failing check, has concrete unfinished work and must keep recovering.
|
|
524
|
+
if (
|
|
525
|
+
mode === 'find-cause'
|
|
526
|
+
&& routeSummary.policyMode === 'recommend-only'
|
|
527
|
+
&& !effectiveLedger.writeAttempted
|
|
528
|
+
&& !effectiveLedger.verificationFailed
|
|
529
|
+
) {
|
|
530
|
+
return {
|
|
531
|
+
continue: false,
|
|
532
|
+
notify: true,
|
|
533
|
+
missingEvidence,
|
|
534
|
+
reason: 'UKit investigation ended without a mutation. A clean audit is valid; report whether no actionable defect was found or a concrete blocker remains. Do not claim a bug was fixed without write and verification evidence.',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
515
538
|
const gated = IMPLEMENT_MODES.has(mode);
|
|
516
539
|
if (!gated) {
|
|
517
540
|
return {
|
|
@@ -566,29 +589,38 @@ export function evaluateCompletion({ state = {}, ledger = {} } = {}) {
|
|
|
566
589
|
}
|
|
567
590
|
|
|
568
591
|
export async function incrementContinuation(projectRoot, payload = {}, ledger = null, requestKey = null) {
|
|
569
|
-
const
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
592
|
+
const target = ledgerPath(projectRoot, payload);
|
|
593
|
+
// Locked re-read: parallel subagents firing Stop hooks share one ledger file, and a
|
|
594
|
+
// stale-snapshot rewrite would reset each other's continuationCount — the budget would
|
|
595
|
+
// never advance and the gate would keep issuing continuations.
|
|
596
|
+
return withFileLock(target, async () => {
|
|
597
|
+
const current = await readExecutionLedger(projectRoot, payload) || ledger || freshLedger(payload, null, 'unknown');
|
|
598
|
+
// Mirror evaluateCompletion's staleness rule: a count minted by an earlier request must
|
|
599
|
+
// not be carried into the new request's budget, or the cap fires early (evaluate says 0,
|
|
600
|
+
// persist says 7) and the next request inherits a nearly exhausted budget.
|
|
601
|
+
const stale = requestKey
|
|
602
|
+
&& current?.continuationRequestKey
|
|
603
|
+
&& current.continuationRequestKey !== requestKey;
|
|
604
|
+
const next = {
|
|
605
|
+
...current,
|
|
606
|
+
continuationCount: (stale ? 0 : Number(current.continuationCount || 0)) + 1,
|
|
607
|
+
...(requestKey ? { continuationRequestKey: requestKey } : {}),
|
|
608
|
+
lastContinuationAt: Date.now(),
|
|
609
|
+
updatedAt: Date.now(),
|
|
610
|
+
};
|
|
611
|
+
await writeJsonAtomic(target, next);
|
|
612
|
+
return next;
|
|
613
|
+
});
|
|
585
614
|
}
|
|
586
615
|
|
|
587
616
|
export async function markNotified(projectRoot, payload = {}, ledger = null) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
617
|
+
const target = ledgerPath(projectRoot, payload);
|
|
618
|
+
return withFileLock(target, async () => {
|
|
619
|
+
const current = await readExecutionLedger(projectRoot, payload) || ledger || freshLedger(payload, null, 'unknown');
|
|
620
|
+
const next = { ...current, notified: true, updatedAt: Date.now() };
|
|
621
|
+
await writeJsonAtomic(target, next);
|
|
622
|
+
return next;
|
|
623
|
+
});
|
|
592
624
|
}
|
|
593
625
|
|
|
594
626
|
async function readStdin() {
|
|
@@ -1189,6 +1189,14 @@ async function main() {
|
|
|
1189
1189
|
|
|
1190
1190
|
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1191
1191
|
const config = await loadRuntimeConfig(projectRoot);
|
|
1192
|
+
// Session-scoped pressure bookkeeping: output tokens must land in the calling
|
|
1193
|
+
// session's record, never a sibling session's.
|
|
1194
|
+
const sessionConfig = {
|
|
1195
|
+
...config,
|
|
1196
|
+
sessionId: typeof payload.session_id === 'string' && payload.session_id.trim()
|
|
1197
|
+
? payload.session_id.trim()
|
|
1198
|
+
: undefined,
|
|
1199
|
+
};
|
|
1192
1200
|
if (!shouldCompress(config)) {
|
|
1193
1201
|
process.exit(0);
|
|
1194
1202
|
return;
|
|
@@ -1247,7 +1255,7 @@ async function main() {
|
|
|
1247
1255
|
tokensAfter: cached.tokensAfter ?? 0,
|
|
1248
1256
|
savedTokens: cached.savedTokens ?? 0,
|
|
1249
1257
|
exitCode,
|
|
1250
|
-
},
|
|
1258
|
+
}, sessionConfig);
|
|
1251
1259
|
process.stdout.write(String(cached.content));
|
|
1252
1260
|
return;
|
|
1253
1261
|
}
|
|
@@ -1345,7 +1353,7 @@ async function main() {
|
|
|
1345
1353
|
tokensAfter: result.tokensAfter,
|
|
1346
1354
|
savedTokens: result.savedTokens,
|
|
1347
1355
|
exitCode,
|
|
1348
|
-
},
|
|
1356
|
+
}, sessionConfig);
|
|
1349
1357
|
|
|
1350
1358
|
if (config?.tokenPipeline?.promptCache) {
|
|
1351
1359
|
await writePromptCacheEntry(projectRoot, {
|
|
@@ -43,6 +43,30 @@ async function main() {
|
|
|
43
43
|
? process.env.CLAUDE_PROJECT_DIR
|
|
44
44
|
: process.cwd();
|
|
45
45
|
const config = await loadRuntimeConfig(projectRoot);
|
|
46
|
+
// Session-scoped pressure bookkeeping: this PreCompact run must zero ONLY the calling
|
|
47
|
+
// session's record. Reading stdin here is safe — the harness payload is modest and this
|
|
48
|
+
// process owns it exclusively.
|
|
49
|
+
const rawHookInput = await (async () => {
|
|
50
|
+
if (process.stdin.isTTY) return '';
|
|
51
|
+
const chunks = [];
|
|
52
|
+
for await (const chunk of process.stdin) {
|
|
53
|
+
chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
54
|
+
}
|
|
55
|
+
return chunks.join('');
|
|
56
|
+
})();
|
|
57
|
+
const hookPayload = (() => {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(rawHookInput || '{}');
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
const sessionConfig = {
|
|
65
|
+
...config,
|
|
66
|
+
sessionId: typeof hookPayload.session_id === 'string' && hookPayload.session_id.trim()
|
|
67
|
+
? hookPayload.session_id.trim()
|
|
68
|
+
: undefined,
|
|
69
|
+
};
|
|
46
70
|
const state = await loadFreshRouteState(projectRoot);
|
|
47
71
|
const compressEnabled = shouldCompress(config);
|
|
48
72
|
const header = '=== PROJECT CONTEXT (post-compaction) ===';
|
|
@@ -67,15 +91,16 @@ async function main() {
|
|
|
67
91
|
|
|
68
92
|
const rawBodyLines = [...rawStaticLines, ...dynamicLines];
|
|
69
93
|
const bodyLines = [...staticLines, ...dynamicLines];
|
|
70
|
-
const pressureState = await readCompactPressureState(projectRoot,
|
|
94
|
+
const pressureState = await readCompactPressureState(projectRoot, sessionConfig);
|
|
71
95
|
|
|
72
96
|
// Reset the hard-cap tracker as early as possible: this hook fires on every real
|
|
73
97
|
// compaction, and it is the only signal that context actually shrank. It must happen
|
|
74
98
|
// before the promptCache early-return below, otherwise a cache hit would skip it and
|
|
75
99
|
// leave context-hardcap-gate.sh permanently tripped. Downstream writes
|
|
76
100
|
// (writeThresholdCompactPlan) re-read from disk, so they build on the zeroed state and
|
|
77
|
-
// keep their own stats increments intact.
|
|
78
|
-
|
|
101
|
+
// keep their own stats increments intact. Session-scoped: sibling sessions keep their
|
|
102
|
+
// own records.
|
|
103
|
+
await writeCompactPressureState(projectRoot, resetCompactPressureState(pressureState, sessionConfig), sessionConfig);
|
|
79
104
|
const anchorLines = dynamicLines.filter((line) => (
|
|
80
105
|
line.startsWith('- Recent routed lane:')
|
|
81
106
|
|| line.startsWith('- Current approach:')
|
|
@@ -153,7 +178,8 @@ async function main() {
|
|
|
153
178
|
tokensBefore: compactedBody.tokensBefore,
|
|
154
179
|
tokensAfter: compactedBody.tokensAfter,
|
|
155
180
|
savedTokens: compactedBody.savedTokens,
|
|
156
|
-
|
|
181
|
+
sessionId: sessionConfig.sessionId,
|
|
182
|
+
}, sessionConfig);
|
|
157
183
|
}
|
|
158
184
|
|
|
159
185
|
const compactSource = thresholdPlan.active
|
|
@@ -17,7 +17,7 @@ Systematic debugging — understand before fixing.
|
|
|
17
17
|
|
|
18
18
|
- Run the failing command/action.
|
|
19
19
|
- Capture exact error message and stack trace.
|
|
20
|
-
- If not reproducible → document conditions
|
|
20
|
+
- If not reproducible → document conditions, run the next most-discriminating bounded repro, then report any exact missing user-only artifact/permission to the parent agent. Never wait for or ask a user directly — you are a worker.
|
|
21
21
|
|
|
22
22
|
### 2. Trace Root Cause
|
|
23
23
|
|
|
@@ -82,4 +82,4 @@ Daily mode: skip. Handoff mode: set task status `pending_review` in `INDEX.md`;
|
|
|
82
82
|
- non-trivial bug: `docs/MEMORY.md` + `docs/PROJECT.md` + `docs/CODE_MAP.md`
|
|
83
83
|
- read `docs/WORKLOG.md` only recent relevant entries
|
|
84
84
|
- Keep fix scope minimal — no drive-by refactors.
|
|
85
|
-
- If root cause is unclear after 5 minutes of tracing → ask user
|
|
85
|
+
- If root cause is unclear after 5 minutes of tracing → try one bounded alternative hypothesis/repro, then report precise evidence plus the smallest needed missing context to the parent agent. Never wait for or ask a user directly — the parent owns user communication.
|
|
@@ -11,7 +11,9 @@ Implement requested behavior with minimal scope drift.
|
|
|
11
11
|
- **Daily/ad-hoc mode** (DEFAULT): task didn't come from `docs/AI_HANDOFF/` → use the original lightweight workflow. Tests only when touched code already has coverage. No reviewer trigger.
|
|
12
12
|
- **Handoff mode**: task file is `docs/AI_HANDOFF/tasks/TASK-xxx.md` OR user explicitly invokes handoff (e.g. "execute task TASK-001") → activate full Quality Gate: test-first → green → reviewer.
|
|
13
13
|
|
|
14
|
-
If unsure which mode applies,
|
|
14
|
+
If unsure which mode applies, default to Daily/ad-hoc mode unless the task path or prompt
|
|
15
|
+
explicitly selects Handoff. Do not ask a user — you are a worker; report the ambiguity and
|
|
16
|
+
reasoning to the parent agent so it can decide whether to re-route.
|
|
15
17
|
|
|
16
18
|
**In Handoff mode you are running unattended — ask nothing.** You were spawned by an
|
|
17
19
|
orchestrator driving a pipeline; there is no human in your conversation to answer, and a
|
|
@@ -32,7 +34,7 @@ and even then, report it, don't ask about it.
|
|
|
32
34
|
- non-trivial: `docs/MEMORY.md` + `docs/PROJECT.md` + `docs/CODE_MAP.md`
|
|
33
35
|
- Identify target files and existing patterns.
|
|
34
36
|
- If task came from handoff, read `tasks/TASK-xxx.md` and locate its **Test Plan** + **Verification Commands**.
|
|
35
|
-
- Daily mode: if confidence is low or risk is high,
|
|
37
|
+
- Daily mode: if confidence is low or risk is high, inspect the next bounded source/context signal and hand back a concise `STATUS: BLOCKED` report with the exact missing decision or artifact if confidence remains low. Do not ask a user directly. Handoff mode: do not ask — decide and record the decision (see above).
|
|
36
38
|
|
|
37
39
|
### 2. Plan Approach (< 1 minute)
|
|
38
40
|
|
|
@@ -43,7 +45,7 @@ and even then, report it, don't ask about it.
|
|
|
43
45
|
|
|
44
46
|
- Write the test(s) from §2 / from task Test Plan.
|
|
45
47
|
- Run them: must FAIL for the expected reason. Capture output.
|
|
46
|
-
- If test passes immediately → test is wrong or behavior already exists. Fix the test
|
|
48
|
+
- If test passes immediately → test is wrong or behavior already exists. Fix the test; if the intended behavior cannot be inferred, hand back `STATUS: BLOCKED` with the observed behavior and the smallest decision the parent must resolve. Never silently stop.
|
|
47
49
|
- **Daily mode**: skip this step unless touched code already has tests (then follow original rule).
|
|
48
50
|
|
|
49
51
|
|