@ngockhoale/ukit 2.6.0 → 2.6.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 (36) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/manifests/platform.full.yaml +16 -0
  3. package/package.json +1 -1
  4. package/src/cli/commands/install.js +49 -2
  5. package/src/cli/commands/update.js +5 -0
  6. package/src/core/output/index.js +77 -0
  7. package/src/core/update.js +36 -5
  8. package/templates/.claude/commands/ukit/handoff-clear.md +2 -2
  9. package/templates/.claude/commands/ukit/handoff-fullstack.md +10 -7
  10. package/templates/.claude/hooks/auto-allow-bash.sh +17 -5
  11. package/templates/.claude/hooks/block-dangerous.sh +18 -5
  12. package/templates/.claude/hooks/completion-gate.sh +17 -5
  13. package/templates/.claude/hooks/compress-output.sh +15 -6
  14. package/templates/.claude/hooks/context-hardcap-gate.sh +77 -46
  15. package/templates/.claude/hooks/context-window-guard.sh +41 -27
  16. package/templates/.claude/hooks/handoff-model-guard.sh +41 -17
  17. package/templates/.claude/hooks/handoff-resume.sh +20 -7
  18. package/templates/.claude/hooks/post-edit-verify.sh +17 -5
  19. package/templates/.claude/hooks/pre-edit-backup.sh +17 -5
  20. package/templates/.claude/hooks/project-important.sh +18 -1
  21. package/templates/.claude/hooks/protect-files.sh +18 -5
  22. package/templates/.claude/hooks/record-execution.sh +17 -5
  23. package/templates/.claude/hooks/sensitive-data-guard.sh +48 -9
  24. package/templates/.claude/hooks/skill-router.sh +124 -86
  25. package/templates/.claude/hooks/stale-spec-guard.sh +22 -6
  26. package/templates/.claude/hooks/task-watchdog.sh +22 -7
  27. package/templates/.claude/hooks/verification-guard.sh +17 -5
  28. package/templates/.claude/hooks/vision-router.sh +17 -5
  29. package/templates/.claude/settings.json +15 -10
  30. package/templates/.claude/ukit/index/provision-worktree.mjs +30 -2
  31. package/templates/.claude/ukit/runtime/execution-ledger.mjs +115 -1
  32. package/templates/.claude/ukit/runtime/hook-input.sh +25 -0
  33. package/templates/.claude/ukit/runtime/hook-telemetry.mjs +86 -2
  34. package/templates/.claude/ukit/runtime/output-compression.mjs +87 -0
  35. package/templates/.omp/hooks/pre/ukit-bridge.js +110 -3
  36. package/templates/ukit/storage/config.json +1 -0
@@ -11,7 +11,15 @@ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
11
11
  source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
12
12
  trap ukit_cleanup_hook_input EXIT
13
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
14
- ukit_stage_hook_input 2097152 truncate || exit 0
14
+ ukit_stage_hook_input 2097152 truncate
15
+ __ukit_main_stage_rc=$?
16
+ if [ "$__ukit_main_stage_rc" -ne 0 ]; then
17
+ # Infra failure during staging (mktemp/truncate): payload was never
18
+ # inspected — announce the degrade (SPEC §8), never a silent pass.
19
+ ukit_emit_input_degraded advisory "pre-edit-backup"
20
+ fi
21
+ # BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
22
+ ukit_input_degraded && ukit_emit_input_degraded advisory "pre-edit-backup"
15
23
  else
16
24
  # Runtime helper missing (pre-install tree): the SAME bounded staging,
17
25
  # inline - `cat >/dev/null` used to block forever on a producer that never
@@ -38,10 +46,14 @@ else
38
46
  wait "$__ukit_waiter" 2>/dev/null
39
47
  exec 8<&-
40
48
  __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
41
- # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
42
- # already degrades on - the shared helper's truncate posture (R4.5).
43
- if [ "$__ukit_size" -gt 2097152 ]; then
44
- truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
49
+ if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
50
+ # BUG-C21-01 fallback parity (SPEC §8): a truncated/stalled staged payload
51
+ # must be announced, never silently acted on — same shape as the helper
52
+ # path's ukit_emit_input_degraded.
53
+ rm -f "$UKIT_INPUT_FILE"
54
+ UKIT_INPUT_FILE=""
55
+ printf '%s\n' '{"systemMessage":"UKit pre-edit-backup: tool-call payload was truncated or stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
56
+ exit 0
45
57
  fi
46
58
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
47
59
  fi
@@ -15,7 +15,15 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
15
15
  # Bounded stdin (H01): the SessionStart payload is unused, but it still flows
16
16
  # through the staging bound so a producer that never closes the pipe cannot
17
17
  # hold this hook open (R4.5).
18
- ukit_stage_hook_input 1048576 truncate || exit 0
18
+ ukit_stage_hook_input 1048576 truncate
19
+ __ukit_main_stage_rc=$?
20
+ if [ "$__ukit_main_stage_rc" -ne 0 ]; then
21
+ # Infra failure during staging (mktemp/truncate): payload was never
22
+ # inspected — announce the degrade (SPEC §8), never a silent pass.
23
+ ukit_emit_input_degraded advisory "project-important"
24
+ fi
25
+ # BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
26
+ ukit_input_degraded && ukit_emit_input_degraded advisory "project-important"
19
27
  else
20
28
  # Runtime helper missing (pre-install tree): the SAME bounded staging,
21
29
  # inline — read cap+1 in the background, bound the wait, never drain with an
@@ -37,9 +45,18 @@ else
37
45
  ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
38
46
  __ukit_waiter=$!
39
47
  wait "$__ukit_reader" 2>/dev/null
48
+ __ukit_stage_rc=$?
40
49
  kill "$__ukit_waiter" 2>/dev/null
41
50
  wait "$__ukit_waiter" 2>/dev/null
42
51
  exec 8<&-
52
+ if [ "$__ukit_stage_rc" -ne 0 ]; then
53
+ # BUG-C21-01 fallback parity (SPEC §8): a stalled SessionStart payload is
54
+ # announced, never silently swallowed.
55
+ rm -f "$UKIT_INPUT_FILE"
56
+ UKIT_INPUT_FILE=""
57
+ printf '%s\n' '{"systemMessage":"UKit project-important: session payload stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
58
+ exit 0
59
+ fi
43
60
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
44
61
  fi
45
62
 
@@ -11,7 +11,15 @@ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
11
11
  source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
12
12
  trap ukit_cleanup_hook_input EXIT
13
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
14
- ukit_stage_hook_input 2097152 truncate || exit 0
14
+ ukit_stage_hook_input 2097152 truncate
15
+ __ukit_main_stage_rc=$?
16
+ if [ "$__ukit_main_stage_rc" -ne 0 ]; then
17
+ # Infra failure during staging (mktemp/truncate): payload was never
18
+ # inspected — announce the degrade (SPEC §8), never a silent pass.
19
+ ukit_emit_input_degraded failclosed "protected-file gate"
20
+ fi
21
+ # BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
22
+ ukit_input_degraded && ukit_emit_input_degraded failclosed "protected-file gate"
15
23
  else
16
24
  # Runtime helper missing (pre-install tree): the SAME bounded staging,
17
25
  # inline - `cat >/dev/null` used to block forever on a producer that never
@@ -38,10 +46,15 @@ else
38
46
  wait "$__ukit_waiter" 2>/dev/null
39
47
  exec 8<&-
40
48
  __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
41
- # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
42
- # already degrades on - the shared helper's truncate posture (R4.5).
43
- if [ "$__ukit_size" -gt 2097152 ]; then
44
- truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
49
+ if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
50
+ # BUG-C21-01 fallback parity (SPEC §8): a truncated/stalled staged payload
51
+ # must be announced, never silently acted on — same shape as the helper
52
+ # path's ukit_emit_input_degraded.
53
+ rm -f "$UKIT_INPUT_FILE"
54
+ UKIT_INPUT_FILE=""
55
+ printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so protected-file gate cannot prove it safe. UKit defers this to a human decision."}}'
56
+ echo "BLOCKED: protected-file gate could not inspect a truncated/stalled payload; deferred to human." >&2
57
+ exit 2
45
58
  fi
46
59
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
47
60
  fi
@@ -12,7 +12,15 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
12
12
  trap ukit_cleanup_hook_input EXIT
13
13
  # Bounded stdin (H01): receipts can legitimately be large, so the staged file
14
14
  # is the transport and the shell var only mirrors the capped payload.
15
- ukit_stage_hook_input 33554432 temp-file || exit 0
15
+ ukit_stage_hook_input 33554432 temp-file
16
+ __ukit_main_stage_rc=$?
17
+ if [ "$__ukit_main_stage_rc" -ne 0 ]; then
18
+ # Infra failure during staging (mktemp/truncate): payload was never
19
+ # inspected — announce the degrade (SPEC §8), never a silent pass.
20
+ ukit_emit_input_degraded advisory "record-execution"
21
+ fi
22
+ # BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
23
+ ukit_input_degraded && ukit_emit_input_degraded advisory "record-execution"
16
24
  else
17
25
  # Runtime helper missing (pre-install tree): the SAME bounded staging,
18
26
  # inline - `cat >/dev/null` used to block forever on a producer that never
@@ -39,10 +47,14 @@ else
39
47
  wait "$__ukit_waiter" 2>/dev/null
40
48
  exec 8<&-
41
49
  __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
42
- # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
43
- # already degrades on - the shared helper's truncate posture (R4.5).
44
- if [ "$__ukit_size" -gt 33554432 ]; then
45
- truncate -s 33554432 "$UKIT_INPUT_FILE" 2>/dev/null
50
+ if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 33554432 ]; then
51
+ # BUG-C21-01 fallback parity (SPEC §8): a truncated/stalled staged payload
52
+ # must be announced, never silently acted on — same shape as the helper
53
+ # path's ukit_emit_input_degraded.
54
+ rm -f "$UKIT_INPUT_FILE"
55
+ UKIT_INPUT_FILE=""
56
+ printf '%s\n' '{"systemMessage":"UKit record-execution: tool-call payload was truncated or stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
57
+ exit 0
46
58
  fi
47
59
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
48
60
  fi
@@ -43,7 +43,13 @@ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
43
43
  ukit_stage_hook_input 2097152 refuse
44
44
  STAGE_RC=$?
45
45
  if [ "$STAGE_RC" -ne 0 ]; then
46
- exit "$STAGE_RC"
46
+ # BUG-C21-02: a fail-closed refusal must never be a SILENT block — emit the
47
+ # §8 reason (labels only, no payload bytes) so the user sees why it stopped.
48
+ # Any non-zero rc is fail-closed: rc=1 is a staging infra failure and a bare
49
+ # exit 1 is a *non-blocking* hook error that would let unscanned input pass.
50
+ printf '%s\n' '{"systemMessage":"UKit sensitive-data gate blocked this call: the payload could not be staged within bounds (oversized or stalled stdin), so it was never scanned. Fail-closed — reduce the payload or check for a stalled producer."}'
51
+ echo 'BLOCKED (sensitive data gate): payload could not be staged within bounds; never scanned (fail-closed).' >&2
52
+ exit 2
47
53
  fi
48
54
  else
49
55
  # Runtime helper missing (pre-install tree): the SAME bounded staging,
@@ -73,8 +79,12 @@ else
73
79
  __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
74
80
  if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
75
81
  # Fail-closed: an unstaged/oversized payload was never inspected
76
- # (exit 2 blocks; 1 would be a non-blocking hook error).
82
+ # (exit 2 blocks; 1 would be a non-blocking hook error). BUG-C21-02: the
83
+ # refusal announces its §8 reason — a silent block is indistinguishable
84
+ # from a stall in the UI.
77
85
  rm -f "$UKIT_INPUT_FILE"
86
+ printf '%s\n' '{"systemMessage":"UKit sensitive-data gate blocked this call: the payload could not be staged within bounds (oversized or stalled stdin), so it was never scanned. Fail-closed — reduce the payload or check for a stalled producer."}'
87
+ echo 'BLOCKED (sensitive data gate): payload could not be staged within bounds; never scanned (fail-closed).' >&2
78
88
  exit 2
79
89
  fi
80
90
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -82,13 +92,32 @@ fi
82
92
 
83
93
  INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" SCRIPT_PATH="$SCRIPT_PATH" UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node <<'NODE'
84
94
  const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
85
- setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
95
+ // BUG-C21-03 (chosen posture: FAIL-CLOSED, parity with the staging refusal):
96
+ // a wedged scan must never pass an uninspected payload. The deadline emits a
97
+ // redacted §8 degrade reason and exits 2 — never a silent exit-0 pass.
98
+ setTimeout(() => {
99
+ try {
100
+ process.stdout.write(`${JSON.stringify({
101
+ systemMessage: 'UKit sensitive-data gate timed out before finishing its scan; blocked fail-closed rather than pass an uninspected payload.',
102
+ })}\n`);
103
+ } catch {}
104
+ try {
105
+ // stderr mirror: piped stdout is async and process.exit() can truncate the
106
+ // pending write — without this the block can arrive with no visible reason.
107
+ process.stderr.write('BLOCKED (sensitive data gate): scan timed out; blocked fail-closed rather than pass an uninspected payload.\n');
108
+ } catch {}
109
+ process.exit(2);
110
+ }, HOOK_DEADLINE_MS).unref();
86
111
  const fs = require('fs');
112
+ const fsp = fs.promises;
87
113
  const path = require('path');
88
114
  const { createHash } = require('crypto');
89
115
 
116
+ // BUG-C21-04: every fs call below is async — a sync read on a stalled mount
117
+ // parks the event loop and the deadline above could never fire.
118
+ void (async () => {
90
119
  let rawInput = '';
91
- try { rawInput = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
120
+ try { rawInput = await fsp.readFile(process.env.INPUT_FILE || '', 'utf8'); } catch {}
92
121
  const payload = (() => {
93
122
  try {
94
123
  const parsed = JSON.parse(rawInput);
@@ -100,20 +129,20 @@ const payload = (() => {
100
129
 
101
130
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
102
131
 
103
- function readJsonSafe(filePath, fallback) {
132
+ async function readJsonSafe(filePath, fallback) {
104
133
  try {
105
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
134
+ return JSON.parse(await fsp.readFile(filePath, 'utf8'));
106
135
  } catch {
107
136
  return fallback;
108
137
  }
109
138
  }
110
139
 
111
- const config = readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'config.json'), null);
140
+ const config = await readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'config.json'), null);
112
141
  if (config && config.security && config.security.sensitiveDataGate === false) {
113
142
  process.exit(0);
114
143
  }
115
144
 
116
- const allowlist = readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'security', 'allowlist.json'), null) || {};
145
+ const allowlist = (await readJsonSafe(path.join(projectRoot, '.ukit', 'storage', 'security', 'allowlist.json'), null)) || {};
117
146
  const allowedValueHashes = new Set(
118
147
  Array.isArray(allowlist.values) ? allowlist.values.filter((v) => typeof v === 'string') : [],
119
148
  );
@@ -395,6 +424,16 @@ process.stdout.write(`${JSON.stringify({
395
424
  })}\n`);
396
425
  process.exit(2);
397
426
  })();
427
+ })();
398
428
  NODE
399
429
 
400
- exit $?
430
+ NODE_RC=$?
431
+ # BUG-C21-03: a node crash (any non-blocking non-zero exit) means the payload
432
+ # was never fully scanned — fail-closed parity: announce + block, never a
433
+ # silent non-blocking hook error.
434
+ if [ "$NODE_RC" -ne 0 ] && [ "$NODE_RC" -ne 2 ]; then
435
+ printf '%s\n' '{"systemMessage":"UKit sensitive-data gate crashed before finishing its scan; blocked fail-closed rather than pass an uninspected payload. Run `ukit install` if this repeats."}'
436
+ echo "BLOCKED (sensitive data gate): scanner process exited $NODE_RC; payload never inspected (fail-closed)." >&2
437
+ exit 2
438
+ fi
439
+ exit "$NODE_RC"