@ngockhoale/ukit 3.4.23 → 3.4.25
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/package.json +1 -1
- package/scripts/audit/subagent-cost/analyze.mjs +107 -0
- package/scripts/audit/subagent-cost/check-doc.mjs +91 -0
- package/scripts/audit/subagent-cost/duplicates.mjs +195 -0
- package/scripts/audit/subagent-cost/metrics.mjs +181 -0
- package/scripts/audit/subagent-cost/reader.mjs +320 -0
- package/scripts/audit/subagent-cost/render.mjs +34 -0
- package/src/cli/commands/telemetry.js +67 -4
- package/src/cli/index.js +1 -1
- package/src/core/observability/adapters/routeAdapter.js +48 -9
- package/src/core/observability/analytics/usageReadiness.js +342 -0
- package/src/core/observability/privacy/allowlist.js +10 -1
- package/src/core/observability/schema/constants.js +13 -0
- package/src/core/observability/schema/registry.js +20 -0
- package/src/core/observability/schema/validate.js +45 -2
- package/src/core/observability/segments/readSegments.js +23 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +53 -2
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +132 -1
|
@@ -956,7 +956,7 @@ async function bumpHandoffStallCount({ projectRoot, signature, lockBudgetMs }) {
|
|
|
956
956
|
*
|
|
957
957
|
* Advisory lane: EVERY failure path returns { kind: 'none' }, never throws.
|
|
958
958
|
*/
|
|
959
|
-
export async function evaluateHandoffCursor({ projectRoot, now = Date.now(), lockBudgetMs = LOCK_BUDGET_MS, transcriptPath = null } = {}) {
|
|
959
|
+
export async function evaluateHandoffCursor({ projectRoot, now = Date.now(), lockBudgetMs = LOCK_BUDGET_MS, transcriptPath = null, stopHookActive = false } = {}) {
|
|
960
960
|
const gate = await loadHandoffGateConfig(projectRoot);
|
|
961
961
|
if (!gate.enabled) return { kind: 'none' };
|
|
962
962
|
|
|
@@ -1024,6 +1024,20 @@ export async function evaluateHandoffCursor({ projectRoot, now = Date.now(), loc
|
|
|
1024
1024
|
|
|
1025
1025
|
const signature = `${phase}|${cursor}|${next}`;
|
|
1026
1026
|
const streak = await bumpHandoffStallCount({ projectRoot, signature, lockBudgetMs });
|
|
1027
|
+
// Reentrancy valve (stop_hook_active): Claude Code sets it when this Stop fires
|
|
1028
|
+
// inside the continuation turn WE forced. The same cursor signature seen again
|
|
1029
|
+
// (streak > 1) means that continuation made no cursor progress — blocking again
|
|
1030
|
+
// is a no-progress loop, so release once instead of waiting for the stalled cap.
|
|
1031
|
+
// A moved cursor resets the streak to 1 and keeps blocking (real progress).
|
|
1032
|
+
if (stopHookActive === true && streak !== null && streak > 1) {
|
|
1033
|
+
return {
|
|
1034
|
+
kind: 'advisory',
|
|
1035
|
+
systemMessage:
|
|
1036
|
+
`[ukit-stop-coordinator] handoff-cursor released this reentrant stop (stop_hook_active): ` +
|
|
1037
|
+
`the run cursor ("${signature}") did not advance during the forced continuation. ` +
|
|
1038
|
+
`RUN.md is untouched; resume with /ukit:handoff-fullstack or clear with /ukit:handoff-clear.`,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1027
1041
|
if (streak !== null && streak > gate.maxStalledBlocks) {
|
|
1028
1042
|
let predicateState = 'no ExitPredicate recorded';
|
|
1029
1043
|
if (exitPredicate) {
|
|
@@ -1064,6 +1078,101 @@ export async function evaluateHandoffCursor({ projectRoot, now = Date.now(), loc
|
|
|
1064
1078
|
return { kind: 'block', reason: lines.join('\n') };
|
|
1065
1079
|
}
|
|
1066
1080
|
|
|
1081
|
+
// ─── Explicit user interruption (pause / cancel) ──────────────────────────
|
|
1082
|
+
// A Stop must never be forced while the USER has paused or cancelled the run:
|
|
1083
|
+
// every lane (completion, watchdog, handoff-cursor) would otherwise bounce the
|
|
1084
|
+
// agent's "PAUSED" reply back as "CONTINUE IMMEDIATELY". The signal is read ONLY
|
|
1085
|
+
// from genuine user turns in the host-authored transcript — never from the agent's
|
|
1086
|
+
// own words, tool output, sub-agent reports or hook feedback (all skipped). The
|
|
1087
|
+
// latest pause/cancel-vs-resume turn wins; neutral turns (questions, health
|
|
1088
|
+
// checks) keep the earlier state; an unreadable/over-budget scan is "no
|
|
1089
|
+
// interruption" so enforcement is never relaxed by a lost read.
|
|
1090
|
+
export const INTERRUPT_SCAN_BUDGET_MS = 800;
|
|
1091
|
+
export const INTERRUPT_SCAN_MAX_BYTES = 8 * 1024 * 1024;
|
|
1092
|
+
const INTERRUPT_MAX_TEXT = 240;
|
|
1093
|
+
const WORD_END = '(?![\\p{L}\\p{N}])';
|
|
1094
|
+
const CANCEL_RE = new RegExp(`^(?:please\\s+)?(?:cancel|abort)${WORD_END}|^(?:hủy|huỷ|huy)${WORD_END}`, 'iu');
|
|
1095
|
+
const PAUSE_RE = new RegExp(
|
|
1096
|
+
`^(?:please\\s+)?(?:pause|halt|stop|hold\\s+(?:on|off))${WORD_END}`
|
|
1097
|
+
+ `|^(?:tạm\\s+dừng|tạm\\s+ngưng|tạm\\s+ngừng|tam\\s+dung|dừng(?:\\s+lại)?|dung\\s+lai|ngừng)${WORD_END}`,
|
|
1098
|
+
'iu',
|
|
1099
|
+
);
|
|
1100
|
+
const RESUME_RE = new RegExp(
|
|
1101
|
+
`^(?:please\\s+)?(?:continue|resume|proceed|unpause|go\\s+on|carry\\s+on)${WORD_END}`
|
|
1102
|
+
+ `|^(?:tiếp\\s+tục|tiep\\s+tuc)${WORD_END}`,
|
|
1103
|
+
'iu',
|
|
1104
|
+
);
|
|
1105
|
+
const SYNTHETIC_USER_PREFIX = /^(?:<system-reminder|<task-notification|<local-command|<user-prompt-submit-hook|Stop hook feedback|\[ukit|Caveat:|\[Request interrupted)/i;
|
|
1106
|
+
const RESUME_COMMAND_TAG = /^<command-name>\/?(?:ukit:)?handoff-(?:fullstack|implement)(?![\w-])/;
|
|
1107
|
+
|
|
1108
|
+
function genuineUserText(entry) {
|
|
1109
|
+
if (!entry || entry.type !== 'user' || entry.isSidechain === true || entry.isMeta === true) return null;
|
|
1110
|
+
const msg = entry.message;
|
|
1111
|
+
if (!msg || msg.role !== 'user') return null;
|
|
1112
|
+
let text = '';
|
|
1113
|
+
if (typeof msg.content === 'string') {
|
|
1114
|
+
text = msg.content;
|
|
1115
|
+
} else if (Array.isArray(msg.content)) {
|
|
1116
|
+
for (const b of msg.content) {
|
|
1117
|
+
if (!b || typeof b !== 'object') continue;
|
|
1118
|
+
if (b.type === 'tool_result') return null;
|
|
1119
|
+
if (b.type === 'text' && typeof b.text === 'string') text += (text ? '\n' : '') + b.text;
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
text = text.trim();
|
|
1125
|
+
if (!text || SYNTHETIC_USER_PREFIX.test(text)) return null;
|
|
1126
|
+
return text;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* @returns {Promise<{ kind: 'pause'|'cancel'|null }>} Newest decisive genuine user
|
|
1131
|
+
* turn: pause/cancel → interrupted; resume → not interrupted; none → not interrupted.
|
|
1132
|
+
*/
|
|
1133
|
+
export async function detectUserInterruption(transcriptPath, {
|
|
1134
|
+
budgetMs = INTERRUPT_SCAN_BUDGET_MS,
|
|
1135
|
+
maxBytes = INTERRUPT_SCAN_MAX_BYTES,
|
|
1136
|
+
} = {}) {
|
|
1137
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return { kind: null };
|
|
1138
|
+
let handle;
|
|
1139
|
+
try {
|
|
1140
|
+
handle = await fs.open(transcriptPath, 'r');
|
|
1141
|
+
const size = Number((await handle.stat()).size) || 0;
|
|
1142
|
+
const start = Math.max(0, size - maxBytes);
|
|
1143
|
+
const deadline = Date.now() + budgetMs;
|
|
1144
|
+
const buf = Buffer.allocUnsafe(size - start);
|
|
1145
|
+
let got = 0;
|
|
1146
|
+
while (got < buf.length) {
|
|
1147
|
+
if (Date.now() > deadline) return { kind: null };
|
|
1148
|
+
const { bytesRead } = await handle.read(buf, got, buf.length - got, start + got);
|
|
1149
|
+
if (!bytesRead) break;
|
|
1150
|
+
got += bytesRead;
|
|
1151
|
+
}
|
|
1152
|
+
let lines = buf.toString('utf8', 0, got).split('\n');
|
|
1153
|
+
if (start > 0) lines = lines.slice(1);
|
|
1154
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1155
|
+
if (Date.now() > deadline) return { kind: null };
|
|
1156
|
+
const line = lines[i];
|
|
1157
|
+
if (!line || !line.includes('"user"') || line.includes('"tool_result"')) continue;
|
|
1158
|
+
let entry;
|
|
1159
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
1160
|
+
const text = genuineUserText(entry);
|
|
1161
|
+
if (text === null) continue;
|
|
1162
|
+
if (RESUME_COMMAND_TAG.test(text)) return { kind: null };
|
|
1163
|
+
if (text.length > INTERRUPT_MAX_TEXT) continue;
|
|
1164
|
+
if (CANCEL_RE.test(text)) return { kind: 'cancel' };
|
|
1165
|
+
if (PAUSE_RE.test(text)) return { kind: 'pause' };
|
|
1166
|
+
if (RESUME_RE.test(text)) return { kind: null };
|
|
1167
|
+
}
|
|
1168
|
+
return { kind: null };
|
|
1169
|
+
} catch {
|
|
1170
|
+
return { kind: null };
|
|
1171
|
+
} finally {
|
|
1172
|
+
try { await handle?.close(); } catch {}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1067
1176
|
// ─── Once-per-stop guard ──────────────────────────────────────────────────
|
|
1068
1177
|
/**
|
|
1069
1178
|
* Lock-scoped dedupe: persist `{ lastStop: { key, ts } }` and report whether this
|
|
@@ -1124,6 +1233,27 @@ export async function runStopCoordinator({
|
|
|
1124
1233
|
return { skip: 'duplicate Stop invocation for this session inside the dedupe window', merged: null };
|
|
1125
1234
|
}
|
|
1126
1235
|
|
|
1236
|
+
// User pause/cancel outranks every lane: release, change nothing (RUN.md, tasks,
|
|
1237
|
+
// branches, worktrees stay as they are so the run can be resumed).
|
|
1238
|
+
const interruption = await detectUserInterruption(
|
|
1239
|
+
typeof payload?.transcript_path === 'string' ? payload.transcript_path : null,
|
|
1240
|
+
);
|
|
1241
|
+
if (interruption.kind) {
|
|
1242
|
+
return {
|
|
1243
|
+
skip: null,
|
|
1244
|
+
merged: {
|
|
1245
|
+
decision: null,
|
|
1246
|
+
reason: undefined,
|
|
1247
|
+
systemMessage:
|
|
1248
|
+
`[ukit-stop-coordinator] stop allowed: the user requested ${interruption.kind}. ` +
|
|
1249
|
+
`No verification or continuation is forced; run state is preserved. ` +
|
|
1250
|
+
`Resume by asking to continue (or /ukit:handoff-fullstack).`,
|
|
1251
|
+
policyOwner: 'user-interruption',
|
|
1252
|
+
policies: ['user-interruption'],
|
|
1253
|
+
},
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1127
1257
|
let completion = null;
|
|
1128
1258
|
const failures = {};
|
|
1129
1259
|
try {
|
|
@@ -1149,6 +1279,7 @@ export async function runStopCoordinator({
|
|
|
1149
1279
|
now,
|
|
1150
1280
|
lockBudgetMs,
|
|
1151
1281
|
transcriptPath: typeof payload?.transcript_path === 'string' ? payload.transcript_path : null,
|
|
1282
|
+
stopHookActive: payload?.stop_hook_active === true,
|
|
1152
1283
|
});
|
|
1153
1284
|
} catch (error) {
|
|
1154
1285
|
failures.handoff = error?.message || String(error);
|