@ngockhoale/ukit 3.4.1 → 3.4.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/code.js +29 -5
  4. package/src/cli/commands/decision.js +18 -4
  5. package/src/cli/commands/doctor.js +7 -3
  6. package/src/cli/commands/install.js +29 -4
  7. package/src/cli/commands/memory.js +25 -5
  8. package/src/cli/commands/telemetry.js +18 -1
  9. package/src/cli/commands/vm.js +7 -1
  10. package/src/context/detectProjectContext.js +7 -2
  11. package/src/core/agentRuntime/contract.js +5 -1
  12. package/src/core/agentRuntime/eventStore.js +54 -7
  13. package/src/core/agentRuntime/recovery.js +22 -15
  14. package/src/core/agentRuntime/supervisor.js +71 -13
  15. package/src/core/applyPlan.js +11 -1
  16. package/src/core/codeintel/compiler.js +51 -8
  17. package/src/core/codeintel/diagnostics.js +124 -33
  18. package/src/core/codeintel/freshness.js +25 -12
  19. package/src/core/codeintel/invalidation.js +11 -3
  20. package/src/core/codeintel/retriever.js +53 -20
  21. package/src/core/codeintel/router.js +19 -9
  22. package/src/core/codeintel/summaries.js +4 -3
  23. package/src/core/codeintel/vectorProvider.js +30 -4
  24. package/src/core/compact/index.js +24 -7
  25. package/src/core/compact/threshold.js +49 -14
  26. package/src/core/diffPlan.js +51 -23
  27. package/src/core/ensureGitignore.js +19 -2
  28. package/src/core/fileOps.js +61 -0
  29. package/src/core/memory/hygiene.js +51 -1
  30. package/src/core/memory/migrate.js +41 -21
  31. package/src/core/memory/store.js +96 -61
  32. package/src/core/metadata.js +37 -2
  33. package/src/core/observability/adapters/ingest.js +30 -2
  34. package/src/core/observability/emit/config.js +19 -4
  35. package/src/core/observability/emit/crash.js +3 -1
  36. package/src/core/observability/emit/recorder.js +15 -7
  37. package/src/core/observability/privacy/sanitizeObserved.js +3 -1
  38. package/src/core/observability/segments/internal.js +36 -8
  39. package/src/core/observability/segments/retention.js +11 -0
  40. package/src/core/observability/support/import.js +27 -1
  41. package/src/core/output/index.js +16 -1
  42. package/src/core/permissionDoctor.js +72 -9
  43. package/src/core/repairBrokenHooks.js +15 -2
  44. package/src/core/reviewPanelAggregate.js +26 -10
  45. package/src/core/runInstallPipeline.js +71 -22
  46. package/src/core/runtimeConfig.js +2 -0
  47. package/src/core/status.js +2 -0
  48. package/src/core/taskBudgetValidator.js +7 -1
  49. package/src/core/taskProgressGuard.js +11 -1
  50. package/src/core/unattendedDoctor.js +36 -5
  51. package/src/core/uninstall.js +52 -12
  52. package/src/core/update.js +5 -1
  53. package/src/decision/client.js +158 -27
  54. package/src/decision/reviewVerdict.js +23 -7
  55. package/src/diagnostics/failurePatterns.js +1 -1
  56. package/src/diagnostics/feedbackEvents.js +1 -1
  57. package/src/diagnostics/routeOutcomes.js +42 -4
  58. package/src/diagnostics/skillAccuracy.js +35 -4
  59. package/src/index/buildIndex.js +123 -26
  60. package/src/index/fixLoopEscalation.js +3 -0
  61. package/src/index/gitHooks.js +99 -29
  62. package/src/index/importResolution.js +7 -1
  63. package/src/index/playbookRegistry.js +15 -11
  64. package/src/index/queryIndex.js +28 -10
  65. package/src/index/routeResolver.js +8 -3
  66. package/src/index/taskRouting.js +37 -2
  67. package/src/learning/codeProposals.js +24 -5
  68. package/src/learning/selfImprove.js +29 -5
  69. package/src/learning/tunedOverlay.js +18 -7
  70. package/src/learning/tuning.js +10 -4
  71. package/src/skill/auditSkill.js +46 -7
  72. package/template_project/.claude/commands/ukit/handoff-review.md +4 -1
  73. package/template_project/.claude/hooks/auto-allow-bash.sh +10 -1
  74. package/template_project/.claude/hooks/block-dangerous.mjs +10 -2
  75. package/template_project/.claude/hooks/handoff-model-guard.sh +46 -16
  76. package/template_project/.claude/hooks/reset-compact-pressure.sh +128 -72
  77. package/template_project/.claude/hooks/sensitive-data-guard.mjs +394 -11
  78. package/template_project/.claude/hooks/session-episode.sh +60 -28
  79. package/template_project/.claude/hooks/verification-guard.sh +26 -15
  80. package/template_project/.claude/skills/pptx/scripts/thumbnail.py +6 -1
  81. package/template_project/.claude/ukit/index/lib/index-core.mjs +156 -39
  82. package/template_project/.claude/ukit/index/playbook-registry.mjs +15 -11
  83. package/template_project/.claude/ukit/index/post-edit-verify.mjs +25 -4
  84. package/template_project/.claude/ukit/index/pre-edit-backup.mjs +4 -0
  85. package/template_project/.claude/ukit/index/provision-worktree.mjs +15 -10
  86. package/template_project/.claude/ukit/index/query-index.mjs +13 -6
  87. package/template_project/.claude/ukit/index/reset-auto-permissions.mjs +127 -25
  88. package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +36 -14
  89. package/template_project/.claude/ukit/index/review-verdict.mjs +93 -19
  90. package/template_project/.claude/ukit/index/route-resolver.mjs +8 -3
  91. package/template_project/.claude/ukit/index/route-task.mjs +15 -0
  92. package/template_project/.claude/ukit/index/safe-patch.mjs +4 -1
  93. package/template_project/.claude/ukit/index/sidecar-decision.mjs +43 -10
  94. package/template_project/.claude/ukit/index/stale-spec-check.mjs +13 -3
  95. package/template_project/.claude/ukit/index/task-budget-validator.mjs +7 -1
  96. package/template_project/.claude/ukit/index/unic-decision.mjs +179 -28
  97. package/template_project/.claude/ukit/index/unic-gateway.mjs +33 -8
  98. package/template_project/.claude/ukit/index/verify-context.mjs +9 -2
  99. package/template_project/.claude/ukit/index/worktree-sweep.mjs +89 -31
  100. package/template_project/.claude/ukit/runtime/compact-threshold.mjs +47 -15
  101. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +63 -30
  102. package/template_project/.claude/ukit/runtime/hook-field-salvage.mjs +49 -13
  103. package/template_project/.claude/ukit/runtime/hook-input.sh +48 -13
  104. package/template_project/.claude/ukit/runtime/hook-telemetry.mjs +92 -7
  105. package/template_project/.claude/ukit/runtime/memory-freshness.mjs +17 -0
  106. package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -10
  107. package/template_project/.claude/ukit/runtime/output-compression.mjs +11 -0
  108. package/template_project/.claude/ukit/runtime/reinject-context.mjs +24 -3
  109. package/template_project/.claude/ukit/runtime/resumable-run.mjs +62 -32
  110. package/template_project/.claude/ukit/runtime/token-utils.mjs +57 -14
@@ -24,23 +24,55 @@ if [ ! -f "$PRESSURE_FILE" ]; then
24
24
  exit 0
25
25
  fi
26
26
 
27
- # O1 (TASK-009): armed AFTER the no-pressure-file short-circuit above, so the common
28
- # "nothing to reset" start stays unmeasured while real resets get timed. This hook
29
- # stages no stdin (SPEC §14), so it owns its EXIT trap; the trap restores the captured
30
- # status and the always-exit-0 posture below is unchanged.
27
+ # O1 (TASK-009): armed AFTER the no-pressure-file short-circuit above, so the
28
+ # common "nothing to reset" start stays unmeasured while real resets get timed.
29
+ # W2-D05 (TASK-C91-005): stdin is staged through the shared bounded path — the
30
+ # hook used to wait on the producer's EOF inside node, so a pipe held open
31
+ # parked every SessionStart reset. Payload completeness is proven by staging;
32
+ # a stalled/truncated payload degrades (announced, skipped) rather than acting
33
+ # on input it never fully read — the whole-file escape hatch still works via a
34
+ # CLOSED empty stdin, which stages cleanly.
31
35
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
- # shellcheck source=/dev/null
33
- source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
34
- if [ "${UKIT_TEL_ARMED:-}" = "1" ]; then
35
- ukit_hook_telemetry_arm
36
- __ukit_tel_finish() {
37
- local __ukit_tel_rc=$?
38
- if [ "${UKIT_TEL_ARMED:-}" = "1" ] && command -v ukit_hook_telemetry_finish >/dev/null 2>&1; then
39
- UKIT_TEL_RC="$__ukit_tel_rc" UKIT_TEL_EVENT="SessionStart" ukit_hook_telemetry_finish
40
- fi
41
- return "$__ukit_tel_rc"
42
- }
43
- trap __ukit_tel_finish EXIT
36
+ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
37
+ # shellcheck source=/dev/null
38
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
39
+ trap ukit_cleanup_hook_input EXIT
40
+ ukit_stage_hook_input 65536 truncate
41
+ __ukit_reset_stage_rc=$?
42
+ if [ "$__ukit_reset_stage_rc" -ne 0 ] || ukit_input_degraded; then
43
+ ukit_emit_input_degraded advisory "reset-compact-pressure"
44
+ fi
45
+ else
46
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
47
+ # inline — cap+1 in the background, bounded wait, never a bare EOF wait.
48
+ UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
49
+ if [ -e /dev/fd/0 ]; then
50
+ exec 8<&0
51
+ head -c 65537 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
52
+ else
53
+ head -c 65537 > "$UKIT_INPUT_FILE" 2>/dev/null &
54
+ fi
55
+ __ukit_reader=$!
56
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
57
+ case "$__ukit_stage_ms" in
58
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
59
+ esac
60
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
61
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
62
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
63
+ __ukit_waiter=$!
64
+ wait "$__ukit_reader" 2>/dev/null
65
+ __ukit_stage_rc=$?
66
+ kill "$__ukit_waiter" 2>/dev/null
67
+ wait "$__ukit_waiter" 2>/dev/null
68
+ exec 8<&-
69
+ if [ "$__ukit_stage_rc" -ne 0 ]; then
70
+ rm -f "$UKIT_INPUT_FILE"
71
+ UKIT_INPUT_FILE=""
72
+ printf '%s\n' '{"systemMessage":"UKit reset-compact-pressure: session payload stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
73
+ exit 0
74
+ fi
75
+ trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
44
76
  fi
45
77
 
46
78
  # The lock directory lives beside the state file; its parent exists because the file does.
@@ -91,6 +123,11 @@ async function withLock(lockPath, fn) {
91
123
  const maxWaitMs = 5000;
92
124
  const startedAt = Date.now();
93
125
  const ownerToken = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
126
+ // W2-D04: a read-modify-write that could not take the lock must NOT run
127
+ // unlocked — a half-read pressure file torn under a concurrent writer is how
128
+ // sibling records get lost. The escape hatch survives ONLY as an explicit,
129
+ // operator-chosen override: UKIT_PRESSURE_RESET_FORCE=1.
130
+ const force = process.env.UKIT_PRESSURE_RESET_FORCE === "1";
94
131
  let held = false;
95
132
  while (!held) {
96
133
  try {
@@ -109,7 +146,15 @@ async function withLock(lockPath, fn) {
109
146
  }
110
147
  held = true;
111
148
  } catch (error) {
112
- if (!error || error.code !== "EEXIST") return fn();
149
+ if (!error || error.code !== "EEXIST") {
150
+ // Non-contention failure (EACCES, ENOSPC, EPERM on mkdir/owner write):
151
+ // retrying will not help — fail closed, or run unlocked only when the
152
+ // operator explicitly forced it.
153
+ if (!force) {
154
+ throw new Error(`could not acquire pressure lock (${error?.code ?? error})`);
155
+ }
156
+ return fn();
157
+ }
113
158
  try {
114
159
  const stat = await fsp.stat(lockPath);
115
160
  if (Date.now() - stat.mtimeMs > staleMs) {
@@ -122,7 +167,12 @@ async function withLock(lockPath, fn) {
122
167
  }
123
168
  }
124
169
  } catch {}
125
- if (Date.now() - startedAt > maxWaitMs) break;
170
+ if (Date.now() - startedAt > maxWaitMs) {
171
+ if (!force) {
172
+ throw new Error("pressure lock held past the 5s wait bound");
173
+ }
174
+ return fn();
175
+ }
126
176
  await new Promise((resolve) => setTimeout(resolve, 3 + Math.floor(Math.random() * 9)));
127
177
  }
128
178
  }
@@ -140,12 +190,15 @@ async function withLock(lockPath, fn) {
140
190
  }
141
191
  }
142
192
 
143
- const chunks = [];
144
- process.stdin.on("data", (chunk) => chunks.push(chunk));
145
- process.stdin.on("end", () => {
193
+ // W2-D05: the payload arrives as a STAGED file (bounded by the shell wrapper),
194
+ // never a live stdin stream — a producer that holds the pipe open cannot park
195
+ // this hook waiting on EOF.
196
+ const inputFile = process.argv[2];
197
+ (async () => {
146
198
  const payload = (() => {
147
199
  try {
148
- return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
200
+ const raw = require("fs").readFileSync(inputFile, "utf8");
201
+ return JSON.parse(raw || "{}");
149
202
  } catch {
150
203
  return {};
151
204
  }
@@ -160,60 +213,63 @@ process.stdin.on("end", () => {
160
213
  const transcriptKey = typeof payload.transcript_path === "string" ? payload.transcript_path.trim() : "";
161
214
  const pressurePath = process.argv[1];
162
215
 
163
- withLock(`${pressurePath}.lock`, async () => {
164
- const raw = await (async () => {
165
- try {
166
- return JSON.parse(await fsp.readFile(pressurePath, "utf8"));
167
- } catch {
168
- return null;
216
+ try {
217
+ await withLock(`${pressurePath}.lock`, async () => {
218
+ const raw = await (async () => {
219
+ try {
220
+ return JSON.parse(await fsp.readFile(pressurePath, "utf8"));
221
+ } catch {
222
+ return null;
223
+ }
224
+ })();
225
+ if (!raw || typeof raw !== "object") {
226
+ try {
227
+ await fsp.rm(pressurePath, { force: true });
228
+ } catch {}
229
+ return;
169
230
  }
170
- })();
171
- if (!raw || typeof raw !== "object") {
172
- try {
173
- await fsp.rm(pressurePath, { force: true });
174
- } catch {}
175
- return;
176
- }
177
231
 
178
- // Without a session id (unknown caller) keep the old whole-file reset: this hook is
179
- // the documented escape hatch for a bricked gate, and it must stay one.
180
- if (!sessionId || !raw.sessions || typeof raw.sessions !== "object") {
181
- try {
182
- await fsp.rm(pressurePath, { force: true });
183
- } catch {}
184
- return;
185
- }
232
+ // Without a session id (unknown caller) keep the old whole-file reset: this hook is
233
+ // the documented escape hatch for a bricked gate, and it must stay one.
234
+ if (!sessionId || !raw.sessions || typeof raw.sessions !== "object") {
235
+ try {
236
+ await fsp.rm(pressurePath, { force: true });
237
+ } catch {}
238
+ return;
239
+ }
186
240
 
187
- const sessions = { ...raw.sessions };
188
- delete sessions[sessionId];
189
- if (transcriptKey && transcriptKey !== sessionId) {
190
- delete sessions[transcriptKey];
191
- }
192
- const newest = Object.entries(sessions)
193
- .sort(([, a], [, b]) => (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0))[0]?.[1] ?? null;
194
- const tempPath = `${pressurePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
195
- try {
196
- await fsp.writeFile(
197
- tempPath,
198
- JSON.stringify({
199
- ...(newest && typeof newest === "object" ? newest : {}),
200
- updatedAt: newest?.updatedAt ?? Date.now(),
201
- v: 2,
202
- sessions,
203
- }, null, 2) + "\n",
204
- );
205
- await fsp.rename(tempPath, pressurePath);
206
- } catch (error) {
241
+ const sessions = { ...raw.sessions };
242
+ delete sessions[sessionId];
243
+ if (transcriptKey && transcriptKey !== sessionId) {
244
+ delete sessions[transcriptKey];
245
+ }
246
+ const newest = Object.entries(sessions)
247
+ .sort(([, a], [, b]) => (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0))[0]?.[1] ?? null;
248
+ const tempPath = `${pressurePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
207
249
  try {
208
- await fsp.rm(tempPath, { force: true });
209
- } catch {}
210
- throw error;
211
- }
212
- }).catch((error) => {
213
- emitDegrade(`reset failed (${error?.message ?? error}); the compact-pressure record may still be stale — restarting the session retries the reset.`);
250
+ await fsp.writeFile(
251
+ tempPath,
252
+ JSON.stringify({
253
+ ...(newest && typeof newest === "object" ? newest : {}),
254
+ updatedAt: newest?.updatedAt ?? Date.now(),
255
+ v: 2,
256
+ sessions,
257
+ }, null, 2) + "\n",
258
+ );
259
+ await fsp.rename(tempPath, pressurePath);
260
+ } catch (error) {
261
+ try {
262
+ await fsp.rm(tempPath, { force: true });
263
+ } catch {}
264
+ throw error;
265
+ }
266
+ });
267
+ process.exit(0);
268
+ } catch (error) {
269
+ emitDegrade(`reset skipped or failed (${error?.message ?? error}); the compact-pressure record may still be stale — restarting the session retries the reset.`);
214
270
  process.exit(0);
215
- });
216
- });
217
- ' "$PRESSURE_FILE" 2>/dev/null || true
271
+ }
272
+ })();
273
+ ' "$PRESSURE_FILE" "$UKIT_INPUT_FILE" 2>/dev/null || true
218
274
 
219
275
  exit 0