@ngockhoale/ukit 2.6.0 → 2.6.2
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 +47 -0
- package/manifests/platform.full.yaml +16 -0
- package/package.json +1 -1
- package/src/cli/commands/install.js +49 -2
- package/src/cli/commands/update.js +5 -0
- package/src/core/output/index.js +77 -0
- package/src/core/update.js +36 -5
- package/templates/.claude/commands/ukit/handoff-clear.md +2 -2
- package/templates/.claude/commands/ukit/handoff-fullstack.md +10 -7
- package/templates/.claude/hooks/auto-allow-bash.sh +17 -5
- package/templates/.claude/hooks/block-dangerous.sh +18 -5
- package/templates/.claude/hooks/completion-gate.sh +17 -5
- package/templates/.claude/hooks/compress-output.sh +15 -6
- package/templates/.claude/hooks/context-hardcap-gate.sh +77 -46
- package/templates/.claude/hooks/context-window-guard.sh +41 -27
- package/templates/.claude/hooks/handoff-model-guard.sh +41 -17
- package/templates/.claude/hooks/handoff-resume.sh +20 -7
- package/templates/.claude/hooks/post-edit-verify.sh +17 -5
- package/templates/.claude/hooks/pre-edit-backup.sh +17 -5
- package/templates/.claude/hooks/project-important.sh +18 -1
- package/templates/.claude/hooks/protect-files.sh +18 -5
- package/templates/.claude/hooks/record-execution.sh +17 -5
- package/templates/.claude/hooks/sensitive-data-guard.sh +48 -9
- package/templates/.claude/hooks/skill-router.sh +124 -86
- package/templates/.claude/hooks/stale-spec-guard.sh +22 -6
- package/templates/.claude/hooks/task-watchdog.sh +22 -7
- package/templates/.claude/hooks/verification-guard.sh +17 -5
- package/templates/.claude/hooks/vision-router.sh +17 -5
- package/templates/.claude/settings.json +15 -10
- package/templates/.claude/ukit/index/provision-worktree.mjs +30 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +99 -1
- package/templates/.claude/ukit/runtime/hook-input.sh +25 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +86 -2
- package/templates/.claude/ukit/runtime/output-compression.mjs +87 -0
- package/templates/.omp/hooks/pre/ukit-bridge.js +110 -3
- package/templates/ukit/storage/config.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.6.2 - 2026-09-18
|
|
6
|
+
|
|
7
|
+
Stability release — 17 confirmed hang / silent-idle / silent-hook defects fixed with
|
|
8
|
+
root causes (full register: `docs/AI_HANDOFF/BUGS-2.6.2.md`), focused on the "system
|
|
9
|
+
goes quiet and does nothing" failure class.
|
|
10
|
+
|
|
11
|
+
- **No more silent degraded input (§8 contract, BUG-C21-01/02/03)**: every hook that
|
|
12
|
+
stages stdin now *announces* when staging fails, stalls, or refuses — fail-closed
|
|
13
|
+
hooks (`block-dangerous`, `protect-files`, `context-hardcap-gate`,
|
|
14
|
+
`sensitive-data-guard`) emit an `ask`/`systemMessage` JSON + stderr `BLOCKED` +
|
|
15
|
+
`exit 2`; advisory hooks emit a `systemMessage` + `exit 0`. The old bounded-cut
|
|
16
|
+
silently truncated and proceeded. `sensitive-data-guard` now also fails closed
|
|
17
|
+
(exit 2) on any non-refusal staging failure and mirrors its deadline block to
|
|
18
|
+
stderr so the reason survives `process.exit` truncation.
|
|
19
|
+
- **Sync-fs defeats inside self-deadlines removed (BUG-C21-04)**: the
|
|
20
|
+
deadline-guarded node blocks in `context-window-guard.sh` and
|
|
21
|
+
`context-hardcap-gate.sh` no longer call synchronous `fs.*Sync` inside the kill
|
|
22
|
+
window — a stalled `.ukit` mount can no longer freeze the gate so the unref'd
|
|
23
|
+
deadline can never fire. (Residual call-frame past the grant boundary recorded
|
|
24
|
+
as BUG-C21-R1/R2 for the next cycle.)
|
|
25
|
+
- **Host timeouts now cover internal budgets (BUG-C21-05)**: every hook registered
|
|
26
|
+
in `settings.json` gets `timeout ≥ stage bound + node deadline + margin`; the
|
|
27
|
+
regression test iterates *all* registered hooks instead of a hand-picked list,
|
|
28
|
+
and four under-budgeted hooks (`auto-allow-bash`, `vision-router`,
|
|
29
|
+
`auto-prune-bash`, `reset-compact-pressure`) were raised to 12s.
|
|
30
|
+
- **Missing `await` in skill-router (review major)**: an un-awaited async context
|
|
31
|
+
snapshot could leave the router writing a stale/partial decision silently.
|
|
32
|
+
- **CLI hangs eliminated (BUG-C21-12/13)**: `ukit update`/`install` `spawnSync`
|
|
33
|
+
calls now carry timeouts with `killSignal: 'SIGKILL'` (SIGTERM alone keeps
|
|
34
|
+
waiting on signal-resistant children); a wedged `npm root -g` now reports
|
|
35
|
+
`global-root-query-timeout` instead of hanging forever. `promptYesNo` resolves
|
|
36
|
+
on stdin EOF / `ERR_USE_AFTER_CLOSE` instead of hanging or crashing.
|
|
37
|
+
- **Unbounded growth bounded (BUG-C21-07/09/10/11)**: `tee/` cache, hook-errors
|
|
38
|
+
jsonl, hook-latency and exec-ledger session files now have count caps; the
|
|
39
|
+
ledger protects receipt files (`.journal`, `.quarantine`, gate crash counter)
|
|
40
|
+
from count-cap eviction, and the overflow basis uses `entries.length`
|
|
41
|
+
(post-filter) everywhere.
|
|
42
|
+
- **Handoff machinery (BUG-C21-15)**: all four `RUN.md` `Phase:` consumers treat
|
|
43
|
+
`done`/`blocked` as terminal — dead runs no longer nag forever.
|
|
44
|
+
- **Wiring/parity (BUG-C21-06/08/14/16/17)**: stale-spec-guard deadline exits
|
|
45
|
+
distinguish "stall" from "match"; `provision-worktree` spawnSync calls carry
|
|
46
|
+
timeouts; `task-budget-validator.mjs` is manifest-covered so the documented
|
|
47
|
+
command works on fresh installs; `verification-guard.sh` is actually registered
|
|
48
|
+
on PreToolUse Bash; shipped `config.json` regained `codex-context-budget` and a
|
|
49
|
+
parity test now guards every array field under `subagents`.
|
|
50
|
+
|
|
51
|
+
|
|
5
52
|
## 2.6.0 - 2026-09-18
|
|
6
53
|
|
|
7
54
|
handoff-fullstack v2 — autonomous create→implement→review loop that runs to 100%
|
|
@@ -1548,6 +1548,22 @@ items:
|
|
|
1548
1548
|
packs:
|
|
1549
1549
|
- core
|
|
1550
1550
|
|
|
1551
|
+
# BUG-C21-14: this file shipped in templates/ but was never manifest-listed, so
|
|
1552
|
+
# autoDiscoverTemplates:false silently skipped it and the agent-documented
|
|
1553
|
+
# `node .claude/ukit/index/task-budget-validator.mjs` command 404'd on installs.
|
|
1554
|
+
# tests/manifest/settingsHookCoverage.test.js now asserts every file under
|
|
1555
|
+
# templates/.claude/ukit/index/ is covered, which immunizes the next added module.
|
|
1556
|
+
- id: ukit-index-task-budget-validator-script
|
|
1557
|
+
type: config
|
|
1558
|
+
sourceTemplate: .claude/ukit/index/task-budget-validator.mjs
|
|
1559
|
+
targetPath: .claude/ukit/index/task-budget-validator.mjs
|
|
1560
|
+
requires: []
|
|
1561
|
+
mergeStrategy: overwrite_with_backup
|
|
1562
|
+
variables: []
|
|
1563
|
+
enabledByDefault: true
|
|
1564
|
+
packs:
|
|
1565
|
+
- core
|
|
1566
|
+
|
|
1551
1567
|
- id: gitignore-root
|
|
1552
1568
|
type: config
|
|
1553
1569
|
sourceTemplate: .gitignore
|
package/package.json
CHANGED
|
@@ -81,9 +81,46 @@ async function loadTrackedManagedPathSet(projectRoot) {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// BUG-C21-13: `readline/promises` `question()` does not settle when the input
|
|
85
|
+
// stream hits EOF without a pending newline — the promise stays pending after
|
|
86
|
+
// the interface `close` event, so an unattended Ctrl+D / piped stdin froze
|
|
87
|
+
// `runInstall` forever. Race every question against the interface's `close`
|
|
88
|
+
// event; on EOF treat it as the default answer (decline → retain) and stop.
|
|
89
|
+
// A post-EOF `question()` also throws ERR_USE_AFTER_CLOSE — same treatment.
|
|
90
|
+
function onceInterfaceClosed(line) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
if (typeof line.once === 'function') {
|
|
93
|
+
line.once('close', resolve);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function askWithEofGuard(line, question) {
|
|
99
|
+
if (line.closed === true) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const answer = await Promise.race([line.question(question), onceInterfaceClosed(line)]);
|
|
104
|
+
return typeof answer === 'string' ? answer : null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code === 'ERR_USE_AFTER_CLOSE') {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
async function promptYesNo(line, question) {
|
|
85
114
|
while (true) {
|
|
86
|
-
const
|
|
115
|
+
const raw = await askWithEofGuard(line, question);
|
|
116
|
+
|
|
117
|
+
if (raw === null) {
|
|
118
|
+
// EOF / interface closed: default answer (No) — never re-prompt a dead stream.
|
|
119
|
+
console.log('[UKit] Input ended (EOF) — keeping existing files.');
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const answer = raw.trim().toLowerCase();
|
|
87
124
|
|
|
88
125
|
if (answer === '') {
|
|
89
126
|
return false;
|
|
@@ -97,6 +134,12 @@ async function promptYesNo(line, question) {
|
|
|
97
134
|
return false;
|
|
98
135
|
}
|
|
99
136
|
|
|
137
|
+
// A real terminal stays open after a bad answer, so re-prompting is safe —
|
|
138
|
+
// but if the stream just closed mid-loop, bail out as EOF.
|
|
139
|
+
if (line.closed === true) {
|
|
140
|
+
console.log('[UKit] Input ended (EOF) — keeping existing files.');
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
100
143
|
console.log('[UKit] Please answer y/yes or n/no.');
|
|
101
144
|
}
|
|
102
145
|
}
|
|
@@ -191,7 +234,11 @@ export async function pruneDeselectedAdapters({
|
|
|
191
234
|
console.log(`[UKit] Removed ${removedCount} ${adapter.label} path(s).`);
|
|
192
235
|
}
|
|
193
236
|
} finally {
|
|
194
|
-
|
|
237
|
+
try {
|
|
238
|
+
line.close();
|
|
239
|
+
} catch {
|
|
240
|
+
// close() is best-effort cleanup — never let it mask the real result.
|
|
241
|
+
}
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
return { retainedManagedPaths: [...new Set(retainedManagedPaths)] };
|
|
@@ -44,6 +44,11 @@ export async function runUpdate({ packageVersion, argv = [], cwd = process.cwd()
|
|
|
44
44
|
console.log('[UKit] Run `ukit install` inside a project to set it up.');
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
+
if (reason === 'global-root-query-timeout') {
|
|
48
|
+
console.log('[UKit] Update complete, but locating the upgraded CLI timed out.');
|
|
49
|
+
console.log('[UKit] `npm root -g` did not answer within 15s — check for a wedged npm/network, then run `ukit install` to refresh this project.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
if (reason === 'global-bin-not-found') {
|
|
48
53
|
console.log('[UKit] Update complete, but the upgraded CLI could not be located.');
|
|
49
54
|
console.log('[UKit] Run `ukit install` to refresh this project.');
|
package/src/core/output/index.js
CHANGED
|
@@ -353,6 +353,81 @@ function buildRecoveryHintSummary(summary, rawPath, { tokensBefore = 0 } = {}) {
|
|
|
353
353
|
return null;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// --- tee/ cache bound (BUG-C21-07, FR-006) -----------------------------------
|
|
357
|
+
// Mirror of the bound in templates/.claude/ukit/runtime/output-compression.mjs —
|
|
358
|
+
// keep the two implementations identical. persistRawOutput wrote one preserved
|
|
359
|
+
// file per compression and nothing ever deleted from tee/. Sweeps are SAMPLED
|
|
360
|
+
// (~1/16 writes, UKIT_TEE_SWEEP_PROBABILITY to force/disable) and BOUNDED
|
|
361
|
+
// (<= maxEntries stats, <= maxRemovals unlinks per sweep).
|
|
362
|
+
const TEE_SWEEP_PROBABILITY_DEFAULT = 1 / 16;
|
|
363
|
+
const TEE_SWEEP_MAX_ENTRIES = 128;
|
|
364
|
+
const TEE_SWEEP_MAX_REMOVALS = 64;
|
|
365
|
+
const TEE_MAX_FILES = 500;
|
|
366
|
+
const TEE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
367
|
+
|
|
368
|
+
function teeSweepProbabilityFromEnv() {
|
|
369
|
+
const raw = Number(process.env.UKIT_TEE_SWEEP_PROBABILITY);
|
|
370
|
+
if (!Number.isFinite(raw)) return TEE_SWEEP_PROBABILITY_DEFAULT;
|
|
371
|
+
return Math.min(1, Math.max(0, raw));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// See the runtime mirror for the contract: removes entries older than maxAgeMs,
|
|
375
|
+
// then the oldest scanned entries while the dir count exceeds maxFiles — all
|
|
376
|
+
// bounded by maxEntries/maxRemovals so an oversized dir is amortized down.
|
|
377
|
+
export async function sweepTeeCache(dir, {
|
|
378
|
+
now = Date.now,
|
|
379
|
+
maxAgeMs = TEE_MAX_AGE_MS,
|
|
380
|
+
maxFiles = TEE_MAX_FILES,
|
|
381
|
+
maxEntries = TEE_SWEEP_MAX_ENTRIES,
|
|
382
|
+
maxRemovals = TEE_SWEEP_MAX_REMOVALS,
|
|
383
|
+
} = {}) {
|
|
384
|
+
let names;
|
|
385
|
+
try {
|
|
386
|
+
names = await fs.readdir(dir);
|
|
387
|
+
} catch {
|
|
388
|
+
return { sampled: true, scanned: 0, removed: 0 };
|
|
389
|
+
}
|
|
390
|
+
const cutoff = now() - maxAgeMs;
|
|
391
|
+
const entries = [];
|
|
392
|
+
let scanned = 0;
|
|
393
|
+
for (const name of names) {
|
|
394
|
+
if (scanned >= maxEntries) break;
|
|
395
|
+
scanned += 1;
|
|
396
|
+
try {
|
|
397
|
+
const stats = await fs.stat(path.join(dir, name));
|
|
398
|
+
if (stats.isFile()) entries.push({ name, mtimeMs: stats.mtimeMs });
|
|
399
|
+
} catch { /* raced away — fine */ }
|
|
400
|
+
}
|
|
401
|
+
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
402
|
+
// Overflow counts eligible FILES only — foreign entries (non-matching names,
|
|
403
|
+
// dirs) inflate `names.length` and would evict real entries while the dir is
|
|
404
|
+
// actually under the cap.
|
|
405
|
+
const overflow = Math.max(0, entries.length - maxFiles);
|
|
406
|
+
let removed = 0;
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
if (removed >= maxRemovals) break;
|
|
409
|
+
if (removed < overflow || entry.mtimeMs < cutoff) {
|
|
410
|
+
try {
|
|
411
|
+
await fs.rm(path.join(dir, entry.name), { force: true });
|
|
412
|
+
removed += 1;
|
|
413
|
+
} catch { /* raced away — fine */ }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { sampled: true, scanned, removed };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function maybeSweepTeeCache(dir, {
|
|
420
|
+
probability,
|
|
421
|
+
random = Math.random,
|
|
422
|
+
...options
|
|
423
|
+
} = {}) {
|
|
424
|
+
const p = Number.isFinite(probability)
|
|
425
|
+
? Math.min(1, Math.max(0, probability))
|
|
426
|
+
: teeSweepProbabilityFromEnv();
|
|
427
|
+
if (random() >= p) return { sampled: false, scanned: 0, removed: 0 };
|
|
428
|
+
return sweepTeeCache(dir, options);
|
|
429
|
+
}
|
|
430
|
+
|
|
356
431
|
async function persistRawOutput(projectRoot, {
|
|
357
432
|
command = '',
|
|
358
433
|
summary = '',
|
|
@@ -379,6 +454,8 @@ async function persistRawOutput(projectRoot, {
|
|
|
379
454
|
|
|
380
455
|
await fs.mkdir(teeCacheDir, { recursive: true });
|
|
381
456
|
await fs.writeFile(absolutePath, rawOutputText, 'utf8');
|
|
457
|
+
// Bounded, sampled tee/ prune (BUG-C21-07): advisory only.
|
|
458
|
+
await maybeSweepTeeCache(teeCacheDir).catch(() => {});
|
|
382
459
|
|
|
383
460
|
return {
|
|
384
461
|
rawSaved: true,
|
package/src/core/update.js
CHANGED
|
@@ -10,6 +10,22 @@ export const UKIT_PACKAGE_NAME = '@ngockhoale/ukit';
|
|
|
10
10
|
// CLAUDE.md and edits .gitignore — an unwanted surprise in, say, a home directory).
|
|
11
11
|
const INSTALLED_MARKERS = ['.ukit', '.claude', 'CLAUDE.md', 'AGENTS.md', '.codex'];
|
|
12
12
|
|
|
13
|
+
// BUG-C21-12 (SPEC FR-005): spawnSync defaults to NO timeout, so a wedged npm
|
|
14
|
+
// (network partition, hung credential helper) would block the CLI forever.
|
|
15
|
+
// Per-operation CLI budgets: a pure `npm root -g` resolution is cheap (~15s);
|
|
16
|
+
// `npm install -g` and the post-update reinstall child get a generous 10-minute
|
|
17
|
+
// bound so slow-but-alive installs still finish.
|
|
18
|
+
// killSignal SIGKILL, not the default SIGTERM: a wedged child that ignores
|
|
19
|
+
// SIGTERM (npm's signal-exit cleanup handler can itself wedge) would keep
|
|
20
|
+
// spawnSync waiting past the timeout — verified on Node v22: SIGTERM on a
|
|
21
|
+
// signal-ignoring child blocks indefinitely, SIGKILL returns ~immediately with
|
|
22
|
+
// ETIMEDOUT. Direct-child kill only (no process-group tree kill): these are
|
|
23
|
+
// short-lived CLI one-shots; the detached-group TERM→KILL posture stays in
|
|
24
|
+
// templates/.claude/ukit/runtime/hook-process.mjs where hooks need it.
|
|
25
|
+
export const NPM_QUERY_TIMEOUT_MS = 15_000;
|
|
26
|
+
export const NPM_INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
27
|
+
const KILL_ON_TIMEOUT = 'SIGKILL';
|
|
28
|
+
|
|
13
29
|
export function hasUkitInstalled(projectRoot) {
|
|
14
30
|
return INSTALLED_MARKERS.some((marker) => fs.existsSync(path.join(projectRoot, marker)));
|
|
15
31
|
}
|
|
@@ -23,12 +39,23 @@ export function hasUkitInstalled(projectRoot) {
|
|
|
23
39
|
* Only a fresh child process picks up the newly written package.
|
|
24
40
|
*/
|
|
25
41
|
export function resolveGlobalUkitBin({ spawnSync = defaultSpawnSync } = {}) {
|
|
26
|
-
|
|
42
|
+
return resolveGlobalUkitBinDetailed({ spawnSync }).binPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Detailed variant: also reports WHY resolution failed, so callers can surface
|
|
46
|
+
// a timeout as actionable ("npm query timed out — is the network/proxy wedged?")
|
|
47
|
+
// instead of misattributing it to a missing file.
|
|
48
|
+
function resolveGlobalUkitBinDetailed({ spawnSync = defaultSpawnSync } = {}) {
|
|
49
|
+
const result = spawnSync('npm', ['root', '-g'], {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
timeout: NPM_QUERY_TIMEOUT_MS,
|
|
52
|
+
killSignal: KILL_ON_TIMEOUT,
|
|
53
|
+
});
|
|
27
54
|
if (result.error || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
28
|
-
return null;
|
|
55
|
+
return { binPath: null, timedOut: result.error?.code === 'ETIMEDOUT' };
|
|
29
56
|
}
|
|
30
57
|
const binPath = path.join(result.stdout.trim(), ...UKIT_PACKAGE_NAME.split('/'), 'bin', 'ukit');
|
|
31
|
-
return fs.existsSync(binPath) ? binPath : null;
|
|
58
|
+
return { binPath: fs.existsSync(binPath) ? binPath : null, timedOut: false };
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
/**
|
|
@@ -45,14 +72,16 @@ export function runInstallAfterUpdate({
|
|
|
45
72
|
return { ran: false, reason: 'not-a-ukit-project' };
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
const binPath =
|
|
75
|
+
const { binPath, timedOut } = resolveGlobalUkitBinDetailed({ spawnSync });
|
|
49
76
|
if (!binPath) {
|
|
50
|
-
return { ran: false, reason: 'global-bin-not-found' };
|
|
77
|
+
return { ran: false, reason: timedOut ? 'global-root-query-timeout' : 'global-bin-not-found' };
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
const result = spawnSync(execPath, [binPath, 'install'], {
|
|
54
81
|
cwd: projectRoot,
|
|
55
82
|
stdio: 'inherit',
|
|
83
|
+
timeout: NPM_INSTALL_TIMEOUT_MS,
|
|
84
|
+
killSignal: KILL_ON_TIMEOUT,
|
|
56
85
|
});
|
|
57
86
|
if (result.error) {
|
|
58
87
|
return { ran: false, reason: `install-failed: ${result.error.message}` };
|
|
@@ -66,6 +95,8 @@ export function runInstallAfterUpdate({
|
|
|
66
95
|
export function updateUkit({ spawnSync = defaultSpawnSync } = {}) {
|
|
67
96
|
const result = spawnSync('npm', ['install', '-g', UKIT_PACKAGE_NAME], {
|
|
68
97
|
stdio: 'inherit',
|
|
98
|
+
timeout: NPM_INSTALL_TIMEOUT_MS,
|
|
99
|
+
killSignal: KILL_ON_TIMEOUT,
|
|
69
100
|
});
|
|
70
101
|
|
|
71
102
|
if (result.error) {
|
|
@@ -49,8 +49,8 @@ PLAN.md → "# PLAN\n_(empty)_"
|
|
|
49
49
|
SPEC.md → restore the untouched template (keep section headings, clear content)
|
|
50
50
|
INDEX.md → empty table header only
|
|
51
51
|
ACTIVE.md → "# ACTIVE\n_(no active cycle)_"
|
|
52
|
-
RUN.md → set `Phase: done` (or delete) — a live
|
|
53
|
-
refuse the next session's stops; clearing a cycle MUST close the cursor.
|
|
52
|
+
RUN.md → set `Phase: done` (or delete) — a live cursor that is neither `done` nor `blocked`
|
|
53
|
+
makes the Stop gate refuse the next session's stops; clearing a cycle MUST close the cursor.
|
|
54
54
|
tasks/TASK-*.md → delete all (keep _TEMPLATE.md)
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -77,13 +77,16 @@ interrupted run into a continuable one.
|
|
|
77
77
|
|
|
78
78
|
### Resume — when the run is re-invoked mid-flight
|
|
79
79
|
|
|
80
|
-
If `docs/AI_HANDOFF/RUN.md` exists with `Phase:` not `done`, this is a
|
|
81
|
-
new cycle**. Do NOT re-plan, do NOT overwrite `PLAN.md`, do NOT ask
|
|
82
|
-
continue. Read the cursor, then jump straight to `Next:` and carry on.
|
|
83
|
-
or `pending_review` are skipped; only `ready`, `in_progress`,
|
|
84
|
-
`blocked` tasks are picked up.
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
If `docs/AI_HANDOFF/RUN.md` exists with `Phase:` not `done` or `blocked`, this is a
|
|
81
|
+
**continuation, not a new cycle**. Do NOT re-plan, do NOT overwrite `PLAN.md`, do NOT ask
|
|
82
|
+
the human whether to continue. Read the cursor, then jump straight to `Next:` and carry on.
|
|
83
|
+
Tasks already `done` or `pending_review` are skipped; only `ready`, `in_progress`,
|
|
84
|
+
`changes_requested` and `blocked` tasks are picked up.
|
|
85
|
+
|
|
86
|
+
`Phase: blocked` is terminal for the run — the machinery (Stop gate, hooks, stop-coordinator)
|
|
87
|
+
all treats it as closed. It is *paused*, not abandoned: an explicit `/ukit:handoff-fullstack`
|
|
88
|
+
re-invocation resumes it as a continuation; `ukit handoff-clear` abandons it. Only when
|
|
89
|
+
`RUN.md` is absent or `Phase: done` does a new request start a fresh cycle.
|
|
87
90
|
|
|
88
91
|
---
|
|
89
92
|
|
|
@@ -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
|
|
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 "auto-allow-bash"
|
|
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 "auto-allow-bash"
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 auto-allow-bash: 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
|
|
@@ -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
|
|
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 "dangerous-command 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 "dangerous-command 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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 dangerous-command gate cannot prove it safe. UKit defers this to a human decision."}}'
|
|
56
|
+
echo "BLOCKED: dangerous-command 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
|
|
@@ -22,7 +22,15 @@ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
|
22
22
|
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
23
23
|
trap ukit_cleanup_hook_input EXIT
|
|
24
24
|
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
25
|
-
ukit_stage_hook_input 2097152 truncate
|
|
25
|
+
ukit_stage_hook_input 2097152 truncate
|
|
26
|
+
__ukit_main_stage_rc=$?
|
|
27
|
+
if [ "$__ukit_main_stage_rc" -ne 0 ]; then
|
|
28
|
+
# Infra failure during staging (mktemp/truncate): payload was never
|
|
29
|
+
# inspected — announce the degrade (SPEC §8), never a silent pass.
|
|
30
|
+
ukit_emit_input_degraded advisory "stop gate"
|
|
31
|
+
fi
|
|
32
|
+
# BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
|
|
33
|
+
ukit_input_degraded && ukit_emit_input_degraded advisory "stop gate"
|
|
26
34
|
else
|
|
27
35
|
# wrapper's own fail-loud behavior below alive.
|
|
28
36
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
@@ -50,10 +58,14 @@ else
|
|
|
50
58
|
wait "$__ukit_waiter" 2>/dev/null
|
|
51
59
|
exec 8<&-
|
|
52
60
|
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
|
|
62
|
+
# BUG-C21-01 fallback parity (SPEC §8): a truncated/stalled staged payload
|
|
63
|
+
# must be announced, never silently acted on — same shape as the helper
|
|
64
|
+
# path's ukit_emit_input_degraded.
|
|
65
|
+
rm -f "$UKIT_INPUT_FILE"
|
|
66
|
+
UKIT_INPUT_FILE=""
|
|
67
|
+
printf '%s\n' '{"systemMessage":"UKit stop gate: tool-call payload was truncated or stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
|
|
68
|
+
exit 0
|
|
57
69
|
fi
|
|
58
70
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
59
71
|
fi
|
|
@@ -18,7 +18,15 @@ if [ -f "$SCRIPT_PATH" ]; then
|
|
|
18
18
|
trap ukit_cleanup_hook_input EXIT
|
|
19
19
|
# Bounded stdin (H01): compressed tool output can be large, so the staged
|
|
20
20
|
# file is the transport and the shell var only mirrors the capped payload.
|
|
21
|
-
ukit_stage_hook_input 33554432 temp-file
|
|
21
|
+
ukit_stage_hook_input 33554432 temp-file
|
|
22
|
+
__ukit_main_stage_rc=$?
|
|
23
|
+
if [ "$__ukit_main_stage_rc" -ne 0 ]; then
|
|
24
|
+
# Infra failure during staging (mktemp/truncate): payload was never
|
|
25
|
+
# inspected — announce the degrade (SPEC §8), never a silent pass.
|
|
26
|
+
ukit_emit_input_degraded advisory "output compression"
|
|
27
|
+
fi
|
|
28
|
+
# BUG-C21-01: a truncated/stalled staged payload must be announced (SPEC §8), never silently passed.
|
|
29
|
+
ukit_input_degraded && ukit_emit_input_degraded advisory "output compression"
|
|
22
30
|
else
|
|
23
31
|
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
24
32
|
# inline — `cat >/dev/null` used to block forever on a producer that never
|
|
@@ -45,14 +53,15 @@ if [ -f "$SCRIPT_PATH" ]; then
|
|
|
45
53
|
wait "$__ukit_waiter" 2>/dev/null
|
|
46
54
|
exec 8<&-
|
|
47
55
|
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
48
|
-
if [ "$__ukit_stage_rc" -ne 0 ]; then
|
|
49
|
-
# Unstaged: the hook cannot inspect this payload
|
|
56
|
+
if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 33554432 ]; then
|
|
57
|
+
# Unstaged or oversized: the hook cannot inspect this payload — announce
|
|
58
|
+
# the degrade (SPEC §8) instead of silently compressing an empty or
|
|
59
|
+
# truncated input (SUS-02 / BUG-C21-01 fallback parity).
|
|
50
60
|
rm -f "$UKIT_INPUT_FILE"
|
|
51
61
|
UKIT_INPUT_FILE=""
|
|
62
|
+
printf '%s\n' '{"systemMessage":"UKit output compression: tool-call payload was truncated or stalled during stdin staging; skipped this pass rather than act on incomplete input."}'
|
|
63
|
+
exit 0
|
|
52
64
|
else
|
|
53
|
-
if [ "$__ukit_size" -gt 33554432 ]; then
|
|
54
|
-
truncate -s 33554432 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
55
|
-
fi
|
|
56
65
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
57
66
|
fi
|
|
58
67
|
fi
|