@ngockhoale/ukit 2.6.10 → 2.7.0
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 +70 -0
- package/manifests/documentation.yaml +24 -2
- package/manifests/instructionRules.yaml +62 -0
- package/manifests/platform.full.yaml +11 -0
- package/package.json +1 -1
- package/scripts/perf/audit-perf.mjs +920 -0
- package/src/cli/commands/doctor.js +23 -4
- package/src/cli/commands/feedback.js +97 -0
- package/src/cli/commands/memory.js +250 -1
- package/src/cli/commands/metrics.js +109 -1
- package/src/cli/index.js +7 -0
- package/src/core/codeintel/retriever.js +65 -0
- package/src/core/diffPlan.js +8 -0
- package/src/core/memory/store.js +7 -2
- package/src/core/ompConfigMerge.js +222 -0
- package/src/core/runInstallPipeline.js +11 -0
- package/src/core/runtimeConfig.js +64 -0
- package/src/core/unattendedDoctor.js +227 -0
- package/src/diagnostics/failurePatterns.js +1 -34
- package/src/diagnostics/feedbackEvents.js +196 -0
- package/src/diagnostics/laneStats.js +111 -0
- package/src/diagnostics/ledgerFiles.js +47 -0
- package/src/diagnostics/skillAccuracy.js +158 -0
- package/src/learning/patternProposals.js +151 -0
- package/src/learning/tuning.js +213 -0
- package/templates/.claude/hooks/block-dangerous.sh +76 -9
- package/templates/.claude/hooks/context-hardcap-gate.sh +26 -8
- package/templates/.claude/hooks/project-important.sh +70 -9
- package/templates/.claude/hooks/protect-files.sh +24 -7
- package/templates/.claude/hooks/sensitive-data-guard.sh +57 -5
- package/templates/.claude/hooks/session-episode.sh +84 -0
- package/templates/.claude/settings.json +29 -113
- package/templates/.claude/ukit/index/route-task.mjs +6 -0
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +217 -10
- package/templates/.claude/ukit/runtime/hook-input.sh +119 -0
- package/templates/.omp/config.yml +32 -4
- package/templates/.omp/hooks/pre/ukit-bridge.js +12 -1
- package/templates/AGENTS.md +22 -10
- package/templates/CLAUDE.md +22 -10
- package/templates/adapter-presets/opencode/opencode.template.json +1 -1
- package/templates/docs/UKIT_INTERNALS.md +17 -0
- package/templates/instructions/core.md +22 -10
- package/templates/instructions/layout.yaml +12 -12
- package/templates/ukit/storage/config.json +20 -0
|
@@ -14,7 +14,9 @@ import { appendTelemetryRow, TELEMETRY_VERSION } from './hook-telemetry.mjs';
|
|
|
14
14
|
// disagree. Kept as a per-run resolution (not module constants) so an operator's
|
|
15
15
|
// env change is honored without a reimport.
|
|
16
16
|
import {
|
|
17
|
+
resolveChainBaseBudgetMs,
|
|
17
18
|
resolveChainBudgetMs,
|
|
19
|
+
resolveChainCeilingMs,
|
|
18
20
|
resolveChainChildBudgetMs,
|
|
19
21
|
} from './hook-chain-budget.mjs';
|
|
20
22
|
|
|
@@ -28,6 +30,41 @@ const FAIL_CLOSED_SCRIPTS = new Set([
|
|
|
28
30
|
|
|
29
31
|
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
30
32
|
|
|
33
|
+
// TASK-234 review fix (critical): stdin staging must be bounded. The old
|
|
34
|
+
// `fs.readFileSync(0)` blocked until the producer closed the pipe — a stalled
|
|
35
|
+
// producer held the runner until the settings timeout SIGKILLed it, so every
|
|
36
|
+
// fail-closed gate in the chain silently failed OPEN (no verdict emitted).
|
|
37
|
+
// Bound the read: at most MAX_STDIN_BYTES and at most STDIN_STAGE_MS, then
|
|
38
|
+
// mark the payload degraded so children see the same truncated-input contract
|
|
39
|
+
// hook-input.sh gives them (UKIT_INPUT_TRUNCATED=1 → fail-closed gates deny).
|
|
40
|
+
const MAX_STDIN_BYTES = 2 * 1024 * 1024;
|
|
41
|
+
const STDIN_STAGE_MS = Number(process.env.UKIT_HOOK_STDIN_STAGE_MS || 2000);
|
|
42
|
+
|
|
43
|
+
async function readStdinBounded() {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
let settled = false;
|
|
48
|
+
const finish = (truncated) => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
process.stdin.removeAllListeners();
|
|
53
|
+
process.stdin.unref?.();
|
|
54
|
+
resolve({ text: Buffer.concat(chunks).toString('utf8'), truncated });
|
|
55
|
+
};
|
|
56
|
+
const timer = setTimeout(() => finish(true), STDIN_STAGE_MS);
|
|
57
|
+
process.stdin.on('data', (chunk) => {
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
bytes += chunk.length;
|
|
60
|
+
if (bytes > MAX_STDIN_BYTES) finish(true);
|
|
61
|
+
});
|
|
62
|
+
process.stdin.on('end', () => finish(false));
|
|
63
|
+
process.stdin.on('error', () => finish(true));
|
|
64
|
+
process.stdin.resume();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
// TASK-018 failure taxonomy (chain level; distinct from hook-process.mjs's
|
|
32
69
|
// process-level kinds). Overflow, timeout, signal, and exit-code failures are
|
|
33
70
|
// distinct values so downstream consumers never have to guess:
|
|
@@ -58,8 +95,24 @@ function recordTiming(projectRoot, payload, timing) {
|
|
|
58
95
|
// appendTelemetryRow carries the same posture (and the per-session cap).
|
|
59
96
|
appendTelemetryRow(projectRoot, payload?.session_id, timing);
|
|
60
97
|
}
|
|
98
|
+
// TASK-234: a script arg may carry a per-script timeout suffix `<path>:<seconds>`
|
|
99
|
+
// so consolidated settings.json chains keep each hook's original settings.json
|
|
100
|
+
// `timeout` instead of sharing one child budget. The suffix is stripped before
|
|
101
|
+
// the path is used; absent → resolveChainChildBudgetMs() fallback, unchanged.
|
|
102
|
+
// A `:0` or negative suffix is rejected (falls back) — zero would mean "no
|
|
103
|
+
// budget", which silently disables the deadline; that is never a valid hook
|
|
104
|
+
// contract.
|
|
105
|
+
function parseScriptArg(arg) {
|
|
106
|
+
const match = /^(.*):(\d+(?:\.\d+)?)$/.exec(arg || '');
|
|
107
|
+
if (!match) return { scriptPath: arg, timeoutMs: null };
|
|
108
|
+
const seconds = Number(match[2]);
|
|
109
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return { scriptPath: arg, timeoutMs: null };
|
|
110
|
+
return { scriptPath: match[1], timeoutMs: Math.round(seconds * 1000) };
|
|
111
|
+
}
|
|
61
112
|
|
|
62
|
-
async function run(payloadText,
|
|
113
|
+
async function run(payloadText, scriptArgs, { chainMarker = true } = {}) {
|
|
114
|
+
const parsedArgs = scriptArgs.map(parseScriptArg);
|
|
115
|
+
const scriptPaths = parsedArgs.map((a) => a.scriptPath);
|
|
63
116
|
const payload = JSON.parse(payloadText || '{}');
|
|
64
117
|
const firstScript = scriptPaths[0] || '';
|
|
65
118
|
const projectRoot = firstScript
|
|
@@ -71,13 +124,41 @@ async function run(payloadText, scriptPaths) {
|
|
|
71
124
|
// (UserPromptSubmit now carries 4 hooks). The floor keeps short chains at 10s,
|
|
72
125
|
// and TASK-018's explicit ceiling stops the per-chain growth from running away.
|
|
73
126
|
// Resolved per run (not at import) so an env change takes effect immediately.
|
|
74
|
-
const
|
|
75
|
-
const
|
|
127
|
+
const defaultChildBudgetMs = resolveChainChildBudgetMs();
|
|
128
|
+
const childBudgetsMs = parsedArgs.map((a) => a.timeoutMs ?? defaultChildBudgetMs);
|
|
129
|
+
// TASK-234: with per-script budgets the total must cover the SUM of declared
|
|
130
|
+
// budgets (not count × shared fallback) or a 6-hook Edit chain would be capped
|
|
131
|
+
// below its own scripts' combined timeouts. When EVERY script declares a
|
|
132
|
+
// budget the declared sum IS the inner budget — the count×fallback floor
|
|
133
|
+
// would otherwise exceed the registered outer timeout (PostToolUse Edit|Write:
|
|
134
|
+
// 3×12s=36s inner vs 34s outer → host kills the runner mid-chain). Mixed
|
|
135
|
+
// chains keep the floor so undeclared scripts still get the fallback.
|
|
136
|
+
const declaredTotalMs = childBudgetsMs.reduce((sum, ms) => sum + ms, 0);
|
|
137
|
+
const allDeclared = parsedArgs.every((a) => a.timeoutMs != null);
|
|
138
|
+
// TASK-234 review fix (important): the ceiling bounds FALLBACK-driven growth,
|
|
139
|
+
// never the declared contract — a 52s ceiling clamping a 63s declared sum
|
|
140
|
+
// starves the last fail-closed gate (context-hardcap-gate) and turns a legal
|
|
141
|
+
// edit into a block. Only EXPLICIT `:N` budgets raise the ceiling floor —
|
|
142
|
+
// fallback-filled budgets must not, or bare bridge chains would bypass the
|
|
143
|
+
// ceiling entirely (ompHookBridge TASK-018 pins that bound).
|
|
144
|
+
const explicitDeclaredMs = parsedArgs.reduce((sum, a) => sum + (a.timeoutMs ?? 0), 0);
|
|
145
|
+
const ceilingMs = Math.max(
|
|
146
|
+
resolveChainCeilingMs(),
|
|
147
|
+
resolveChainBaseBudgetMs(),
|
|
148
|
+
explicitDeclaredMs,
|
|
149
|
+
);
|
|
150
|
+
const totalBudgetMs = Math.min(
|
|
151
|
+
allDeclared
|
|
152
|
+
? declaredTotalMs
|
|
153
|
+
: Math.max(resolveChainBudgetMs(scriptPaths.length), declaredTotalMs),
|
|
154
|
+
ceilingMs,
|
|
155
|
+
);
|
|
76
156
|
const deadline = startedAt + totalBudgetMs;
|
|
77
157
|
const results = [];
|
|
78
158
|
let budgetExhausted = false;
|
|
79
159
|
|
|
80
|
-
for (
|
|
160
|
+
for (let scriptIndex = 0; scriptIndex < scriptPaths.length; scriptIndex++) {
|
|
161
|
+
const scriptPath = scriptPaths[scriptIndex];
|
|
81
162
|
const scriptName = path.basename(scriptPath);
|
|
82
163
|
const remainingMs = deadline - Date.now();
|
|
83
164
|
if (remainingMs <= 0) {
|
|
@@ -97,9 +178,9 @@ async function run(payloadText, scriptPaths) {
|
|
|
97
178
|
const childStartedAt = Date.now();
|
|
98
179
|
const result = await runHookProcess({
|
|
99
180
|
command: scriptPath,
|
|
181
|
+
deadlineMs: Math.min(childBudgetsMs[scriptIndex], remainingMs),
|
|
100
182
|
args: [],
|
|
101
183
|
input: payloadText,
|
|
102
|
-
deadlineMs: Math.min(childBudgetMs, remainingMs),
|
|
103
184
|
maxBuffer: MAX_BUFFER_BYTES,
|
|
104
185
|
cwd: projectRoot,
|
|
105
186
|
// TASK-223 (HK-401): mark chain-spawned children so their structured
|
|
@@ -109,7 +190,10 @@ async function run(payloadText, scriptPaths) {
|
|
|
109
190
|
env: {
|
|
110
191
|
...process.env,
|
|
111
192
|
CLAUDE_PROJECT_DIR: projectRoot,
|
|
112
|
-
|
|
193
|
+
// TASK-234: the marker selects the omp structured-decision contract
|
|
194
|
+
// (ask + exit 2). Under --emit-verdict the runner replays the direct
|
|
195
|
+
// Claude contract (deny + exit 0), so children must NOT see it.
|
|
196
|
+
...(chainMarker ? { UKIT_HOOK_CHAIN_RUNNER: '1' } : {}),
|
|
113
197
|
},
|
|
114
198
|
});
|
|
115
199
|
const failureKind = chainFailureKind(result);
|
|
@@ -139,6 +223,15 @@ async function run(payloadText, scriptPaths) {
|
|
|
139
223
|
}
|
|
140
224
|
}
|
|
141
225
|
|
|
226
|
+
// TASK-234 review fix (critical): a mid-chain break (killed advisory, budget
|
|
227
|
+
// exhausted, or a fail-closed non-zero) leaves later scripts unrun. When any
|
|
228
|
+
// of those unrun scripts is fail-closed, the chain must fail CLOSED — the old
|
|
229
|
+
// per-script path ran every hook independently, so a timed-out advisory never
|
|
230
|
+
// skipped a gate. `skippedFailClosed` carries that signal to the verdict.
|
|
231
|
+
const skippedFailClosed = scriptPaths
|
|
232
|
+
.slice(results.length)
|
|
233
|
+
.some((p) => FAIL_CLOSED_SCRIPTS.has(path.basename(p)));
|
|
234
|
+
|
|
142
235
|
const elapsedMs = Date.now() - startedAt;
|
|
143
236
|
// TASK-019: versioned rows shared with direct hooks. `outcome` reuses this
|
|
144
237
|
// runner's own failure taxonomy — the aggregate of the worst child result —
|
|
@@ -165,23 +258,137 @@ async function run(payloadText, scriptPaths) {
|
|
|
165
258
|
})),
|
|
166
259
|
});
|
|
167
260
|
|
|
168
|
-
return { results, elapsedMs, budgetMs: totalBudgetMs, budgetExhausted };
|
|
261
|
+
return { results, elapsedMs, budgetMs: totalBudgetMs, budgetExhausted, skippedFailClosed };
|
|
169
262
|
}
|
|
170
263
|
|
|
171
264
|
try {
|
|
172
|
-
|
|
265
|
+
let argv = process.argv.slice(2);
|
|
266
|
+
// TASK-234: `--emit-verdict` adapts the runner for Claude Code settings.json
|
|
267
|
+
// hooks, where the command's own stdout/exit-code IS the verdict — not the
|
|
268
|
+
// JSON aggregate the omp bridge parses. In this mode the runner replays the
|
|
269
|
+
// last executed script's stdout verbatim and maps the chain outcome onto the
|
|
270
|
+
// single-command contract: exit 0 normally; exit 2 + the child's stderr when
|
|
271
|
+
// a child blocked (code 2) or a FAIL_CLOSED script could not produce a
|
|
272
|
+
// verdict (killed/error/budget-exhausted). Non-fail-closed transport failures
|
|
273
|
+
// stay fail-open (exit 0), matching today's per-script behavior where a
|
|
274
|
+
// timed-out advisory hook never blocks the call.
|
|
275
|
+
const emitVerdict = argv[0] === '--emit-verdict';
|
|
276
|
+
if (emitVerdict) argv = argv.slice(1);
|
|
277
|
+
const [payloadArg = '{}', ...scriptPaths] = argv;
|
|
173
278
|
// The bridge passes the payload as a temp file (`@path`) when it can: argv is capped
|
|
174
279
|
// (~256KB per arg on macOS) and PostToolUse Bash payloads embed whole tool outputs.
|
|
175
280
|
// A leading '@' cannot occur in raw JSON, so the two forms are unambiguous.
|
|
281
|
+
// '-' reads the payload from stdin — the form Claude Code hook commands use.
|
|
176
282
|
let payloadText = payloadArg;
|
|
177
|
-
|
|
283
|
+
let stdinTruncated = false;
|
|
284
|
+
if (payloadArg === '-') {
|
|
285
|
+
const staged = await readStdinBounded();
|
|
286
|
+
payloadText = staged.text;
|
|
287
|
+
stdinTruncated = staged.truncated;
|
|
288
|
+
} else if (payloadArg.startsWith('@')) {
|
|
178
289
|
try {
|
|
179
290
|
payloadText = fs.readFileSync(payloadArg.slice(1), 'utf8');
|
|
180
291
|
} catch {
|
|
181
292
|
payloadText = '{}';
|
|
182
293
|
}
|
|
183
294
|
}
|
|
184
|
-
|
|
295
|
+
// A truncated stdin means the payload was never fully read — the same
|
|
296
|
+
// degraded contract hook-input.sh enforces (uninspected input must never be
|
|
297
|
+
// treated as a clean scan). Children stage their own stdin and cannot see
|
|
298
|
+
// the runner's truncation, so the runner owns the degraded verdict: when the
|
|
299
|
+
// chain carries a fail-closed gate, emit deny now instead of letting gates
|
|
300
|
+
// pass on a payload they never fully received.
|
|
301
|
+
if (stdinTruncated) {
|
|
302
|
+
const hasFailClosed = scriptPaths.some((p) =>
|
|
303
|
+
FAIL_CLOSED_SCRIPTS.has(path.basename(String(p).replace(/:\d+(?:\.\d+)?$/, ''))));
|
|
304
|
+
if (hasFailClosed) {
|
|
305
|
+
const deny = JSON.stringify({
|
|
306
|
+
hookSpecificOutput: {
|
|
307
|
+
hookEventName: 'PreToolUse',
|
|
308
|
+
permissionDecision: 'deny',
|
|
309
|
+
permissionDecisionReason:
|
|
310
|
+
'UKit could not fully read this tool-call payload (stdin staging was cut at the deadline/size bound), so the fail-closed gate chain cannot prove it safe.',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
if (emitVerdict) {
|
|
314
|
+
process.stdout.write(deny);
|
|
315
|
+
process.exitCode = 0;
|
|
316
|
+
} else {
|
|
317
|
+
process.stdout.write(JSON.stringify({
|
|
318
|
+
results: [],
|
|
319
|
+
wrapperError: 'stdin staging truncated — fail-closed chain refused',
|
|
320
|
+
stdinTruncated: true,
|
|
321
|
+
}));
|
|
322
|
+
process.exitCode = 2;
|
|
323
|
+
}
|
|
324
|
+
process.exit(emitVerdict ? 0 : 2);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const chain = await run(payloadText, scriptPaths, { chainMarker: !emitVerdict });
|
|
328
|
+
if (!emitVerdict) {
|
|
329
|
+
process.stdout.write(JSON.stringify(chain));
|
|
330
|
+
} else {
|
|
331
|
+
// TASK-234: a script that emits a hookSpecificOutput decision JSON owns the
|
|
332
|
+
// verdict even when it exits 0 (the direct Claude contract: deny + exit 0
|
|
333
|
+
// still blocks). The first decision wins, matching per-script semantics
|
|
334
|
+
// where each hook's output is its own verdict and a deny short-circuits.
|
|
335
|
+
const decisionResult = chain.results.find((r) =>
|
|
336
|
+
typeof r.stdout === 'string' && r.stdout.includes('"hookSpecificOutput"'));
|
|
337
|
+
const last = decisionResult ?? chain.results[chain.results.length - 1];
|
|
338
|
+
|
|
339
|
+
// TASK-234 review fix (critical): context stdout must be REPLAYED, not
|
|
340
|
+
// dropped. SessionStart/UserPromptSubmit hooks emit plain-text context
|
|
341
|
+
// (PROJECT_IMPORTANT mandate, skill-router guidance) — replaying only the
|
|
342
|
+
// decision owner's or last script's stdout loses every earlier emission.
|
|
343
|
+
// Concatenate every script's non-decision stdout in order, then append the
|
|
344
|
+
// decision JSON last so the verdict still parses.
|
|
345
|
+
const contextStdout = chain.results
|
|
346
|
+
.filter((r) => r !== decisionResult && r !== last && typeof r.stdout === 'string' && r.stdout.length > 0)
|
|
347
|
+
.map((r) => r.stdout)
|
|
348
|
+
.join('');
|
|
349
|
+
|
|
350
|
+
// TASK-234 review fix (critical): a mid-chain break that skipped a
|
|
351
|
+
// fail-closed gate must fail CLOSED — the old per-script path ran every
|
|
352
|
+
// hook independently, so a killed advisory never skipped a gate.
|
|
353
|
+
if (chain.skippedFailClosed) {
|
|
354
|
+
if (contextStdout) process.stdout.write(contextStdout);
|
|
355
|
+
const skipped = scriptPaths
|
|
356
|
+
.slice(chain.results.length)
|
|
357
|
+
.map((p) => path.basename(String(p).replace(/:\d+(?:\.\d+)?$/, '')))
|
|
358
|
+
.filter((name) => FAIL_CLOSED_SCRIPTS.has(name))
|
|
359
|
+
.join(', ');
|
|
360
|
+
process.stderr.write(`UKit hook chain broke before fail-closed gate(s) ran: ${skipped}\n`);
|
|
361
|
+
process.exitCode = 2;
|
|
362
|
+
} else if (!last) {
|
|
363
|
+
// No script ran at all (empty chain or budget spent before the first
|
|
364
|
+
// child). With fail-closed scripts declared in the chain this must not
|
|
365
|
+
// fail open.
|
|
366
|
+
const hasFailClosed = scriptPaths.some((p) => FAIL_CLOSED_SCRIPTS.has(path.basename(String(p).replace(/:\d+(?:\.\d+)?$/, ''))));
|
|
367
|
+
if (hasFailClosed) {
|
|
368
|
+
process.stderr.write('UKit hook chain produced no verdict — fail-closed gate did not run\n');
|
|
369
|
+
process.exitCode = 2;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
const lastIsFailClosed = FAIL_CLOSED_SCRIPTS.has(last.scriptName);
|
|
373
|
+
const lastFailedToVerdict = last.killed || last.failureKind === 'error' || last.failureKind === 'budget-exhausted';
|
|
374
|
+
if (last.code === 2) {
|
|
375
|
+
if (contextStdout) process.stdout.write(contextStdout);
|
|
376
|
+
if (last.stdout) process.stdout.write(last.stdout);
|
|
377
|
+
if (last.stderr) process.stderr.write(last.stderr);
|
|
378
|
+
process.exitCode = 2;
|
|
379
|
+
} else if (lastIsFailClosed && lastFailedToVerdict) {
|
|
380
|
+
if (contextStdout) process.stdout.write(contextStdout);
|
|
381
|
+
if (last.stderr) process.stderr.write(last.stderr);
|
|
382
|
+
else process.stderr.write(`UKit fail-closed hook ${last.scriptName} could not produce a verdict (${last.failureKind})\n`);
|
|
383
|
+
process.exitCode = 2;
|
|
384
|
+
} else {
|
|
385
|
+
if (contextStdout) process.stdout.write(contextStdout);
|
|
386
|
+
if (last.stdout) process.stdout.write(last.stdout);
|
|
387
|
+
if (last.stderr) process.stderr.write(last.stderr);
|
|
388
|
+
process.exitCode = last.code === 0 ? 0 : (lastIsFailClosed ? 2 : 0);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
185
392
|
} catch (error) {
|
|
186
393
|
process.stdout.write(JSON.stringify({
|
|
187
394
|
results: [],
|
|
@@ -169,6 +169,125 @@ ukit_emit_input_degraded() {
|
|
|
169
169
|
exit 0
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
# TASK-003 (salvage verdicts from truncated payloads): when the staged payload
|
|
173
|
+
# is flagged truncated/stalled, a gated hook may still recover a DECISION-RELEVANT
|
|
174
|
+
# field if that field's value is provably COMPLETE — the closing quote AND a
|
|
175
|
+
# following `,`/`}` boundary must appear before the cut. Anything else (cut
|
|
176
|
+
# inside the string, EOF right after the quote, malformed prefix, missing key)
|
|
177
|
+
# is unrecoverable: callers map that to `deny`, never `ask` (bypassPermissions
|
|
178
|
+
# auto-approves `ask` on the direct host = gates silently skipped).
|
|
179
|
+
#
|
|
180
|
+
# Contract:
|
|
181
|
+
# ukit_salvage_tool_field <json-file> <dotted-field>
|
|
182
|
+
# stdout: the decoded string value (single line may contain \n escapes
|
|
183
|
+
# decoded — the value itself is written raw)
|
|
184
|
+
# exit 0 — field recovered AND proven complete
|
|
185
|
+
# exit 3 — unrecoverable / incomplete / malformed / non-string value
|
|
186
|
+
# Budget: UKIT_SALVAGE_BUDGET_MS (default 3000) bounds the node process; the
|
|
187
|
+
# file is already capped by staging (≤2MiB), so no unbounded reads.
|
|
188
|
+
# Single string fields only — this is a salvage step, not a JSON repairer.
|
|
189
|
+
ukit_salvage_tool_field() {
|
|
190
|
+
UKIT_SALVAGE_FILE="$1" UKIT_SALVAGE_FIELD="$2" \
|
|
191
|
+
UKIT_SALVAGE_BUDGET_MS="${UKIT_SALVAGE_BUDGET_MS:-3000}" node <<'UKIT_SALVAGE_NODE'
|
|
192
|
+
const budget = Number.parseInt(process.env.UKIT_SALVAGE_BUDGET_MS || '3000', 10) || 3000;
|
|
193
|
+
setTimeout(() => process.exit(3), budget).unref();
|
|
194
|
+
const fs = require('fs');
|
|
195
|
+
const fail = () => process.exit(3);
|
|
196
|
+
let data;
|
|
197
|
+
try {
|
|
198
|
+
data = fs.readFileSync(process.env.UKIT_SALVAGE_FILE || '', 'utf8');
|
|
199
|
+
} catch {
|
|
200
|
+
fail();
|
|
201
|
+
}
|
|
202
|
+
const dotted = String(process.env.UKIT_SALVAGE_FIELD || '').split('.').filter(Boolean);
|
|
203
|
+
if (!data || data[0] !== '{' || dotted.length === 0 || dotted.length > 4) fail();
|
|
204
|
+
|
|
205
|
+
const isWs = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
206
|
+
const skipWs = (s, i) => { while (i < s.length && isWs(s[i])) i += 1; return i; };
|
|
207
|
+
// End index of the string literal starting at `start` (which must be `"`), or -1
|
|
208
|
+
// when the string is cut before its closing quote.
|
|
209
|
+
const scanStringEnd = (s, start) => {
|
|
210
|
+
for (let i = start + 1; i < s.length; i += 1) {
|
|
211
|
+
const c = s[i];
|
|
212
|
+
if (c === '\\') { i += 1; continue; }
|
|
213
|
+
if (c === '"') return i;
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
};
|
|
217
|
+
// Closing brace matching the `{` at `open` (string-aware), or -1 if unclosed.
|
|
218
|
+
const matchBrace = (s, open) => {
|
|
219
|
+
let depth = 0;
|
|
220
|
+
for (let i = open; i < s.length; i += 1) {
|
|
221
|
+
const c = s[i];
|
|
222
|
+
if (c === '"') {
|
|
223
|
+
const end = scanStringEnd(s, i);
|
|
224
|
+
if (end === -1) return -1;
|
|
225
|
+
i = end;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (c === '{') depth += 1;
|
|
229
|
+
else if (c === '}') {
|
|
230
|
+
depth -= 1;
|
|
231
|
+
if (depth === 0) return i;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return -1;
|
|
235
|
+
};
|
|
236
|
+
// Find `"key"` used as an object key inside region [lo, hi); returns the index
|
|
237
|
+
// of its `:` or -1. A bare `"key"` inside a string value cannot produce this
|
|
238
|
+
// shape (its quotes are escaped), and non-key uses lack the `:` — both are
|
|
239
|
+
// skipped by scanning forward.
|
|
240
|
+
const findKey = (s, key, lo, hi) => {
|
|
241
|
+
const needle = `"${key}"`;
|
|
242
|
+
let pos = s.indexOf(needle, lo);
|
|
243
|
+
while (pos !== -1 && pos < hi) {
|
|
244
|
+
const colon = skipWs(s, pos + needle.length);
|
|
245
|
+
if (colon < hi && colon < s.length && s[colon] === ':') {
|
|
246
|
+
const prev = pos - 1;
|
|
247
|
+
const pc = prev >= 0 ? s[prev] : '';
|
|
248
|
+
if (prev < 0 || pc === '{' || pc === ',' || isWs(pc)) return colon;
|
|
249
|
+
}
|
|
250
|
+
pos = s.indexOf(needle, pos + 1);
|
|
251
|
+
}
|
|
252
|
+
return -1;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
let regionLo = 0;
|
|
256
|
+
let regionHi = data.length;
|
|
257
|
+
for (let k = 0; k < dotted.length; k += 1) {
|
|
258
|
+
const colon = findKey(data, dotted[k], regionLo, regionHi);
|
|
259
|
+
if (colon === -1) fail();
|
|
260
|
+
const vstart = skipWs(data, colon + 1);
|
|
261
|
+
if (vstart >= data.length) fail();
|
|
262
|
+
const last = k === dotted.length - 1;
|
|
263
|
+
const c = data[vstart];
|
|
264
|
+
if (!last) {
|
|
265
|
+
if (c !== '{') fail();
|
|
266
|
+
const close = matchBrace(data, vstart);
|
|
267
|
+
// An unclosed parent object still bounds the search to what arrived; the
|
|
268
|
+
// leaf's own boundary proof below decides completeness.
|
|
269
|
+
regionLo = vstart + 1;
|
|
270
|
+
regionHi = close === -1 ? data.length : close;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (c !== '"') fail(); // string fields only
|
|
274
|
+
const end = scanStringEnd(data, vstart);
|
|
275
|
+
if (end === -1) fail(); // cut inside the value — never trust a partial field
|
|
276
|
+
const after = skipWs(data, end + 1);
|
|
277
|
+
if (after >= data.length) fail(); // closed quote but no boundary proof — err closed
|
|
278
|
+
const boundary = data[after];
|
|
279
|
+
if (boundary !== ',' && boundary !== '}') fail();
|
|
280
|
+
try {
|
|
281
|
+
process.stdout.write(JSON.parse(data.slice(vstart, end + 1)));
|
|
282
|
+
} catch {
|
|
283
|
+
fail();
|
|
284
|
+
}
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
fail();
|
|
288
|
+
UKIT_SALVAGE_NODE
|
|
289
|
+
}
|
|
290
|
+
|
|
172
291
|
ukit_cleanup_hook_input() {
|
|
173
292
|
# TASK-019: emit the telemetry finish marker while the staged payload file
|
|
174
293
|
# still exists (its mtime is the envelope start). Strictly advisory — the
|
|
@@ -15,10 +15,37 @@ modelRoles:
|
|
|
15
15
|
vision: unic-vision
|
|
16
16
|
|
|
17
17
|
tools:
|
|
18
|
-
|
|
18
|
+
# TASK-009 / SPEC FR-003: the unattended approval surface is pinned HERE —
|
|
19
|
+
# `approvalMode: yolo` plus all ten `tools.approval.*: allow`. The product
|
|
20
|
+
# source of truth is `orchestration.permissionMode: unattended`
|
|
21
|
+
# (.ukit/storage/config.json, written by TASK-008); this map is its omp-side
|
|
22
|
+
# rendering so installed projects run prompt-free out of the box.
|
|
23
|
+
# This SUPERSEDES the TASK-006/C34 omission decision (approvalMode left absent
|
|
24
|
+
# so the end user's global/runtime mode won): an interactive global would
|
|
25
|
+
# silently downgrade a shipped install to prompting, contradicting the
|
|
26
|
+
# unattended contract — the project-level pin is now required, not optional.
|
|
27
|
+
# `approvalMode: yolo` VERIFIED against omp v17.4.2 (2026-08-22) — same
|
|
28
|
+
# docContracts allowlist citation as compaction.thresholdTokens below.
|
|
29
|
+
# Safety under yolo is unchanged: the bash.patterns deny set below, the
|
|
30
|
+
# TASK-004 hook-bridge deny chain, and this approval map still gate dangerous
|
|
31
|
+
# ops — `allow` removes interactive prompts, not the deny checks.
|
|
32
|
+
# Residual risk: yolo + all-allow IS dangerously autonomous and is a
|
|
33
|
+
# deliberate, user-chosen posture (unattended mode). `eval` stays `allow`
|
|
34
|
+
# (was `prompt` pre-TASK-001): bash.patterns never covers the eval tool, but
|
|
35
|
+
# the TASK-004 bridge maps `eval` -> `Bash`, so every eval call is gated by
|
|
36
|
+
# the same block-dangerous chain as a bash command.
|
|
37
|
+
approvalMode: yolo
|
|
19
38
|
approval:
|
|
20
39
|
bash: allow
|
|
21
|
-
eval:
|
|
40
|
+
eval: allow
|
|
41
|
+
task: allow
|
|
42
|
+
read: allow
|
|
43
|
+
grep: allow
|
|
44
|
+
write: allow
|
|
45
|
+
edit: allow
|
|
46
|
+
lsp: allow
|
|
47
|
+
browser: allow
|
|
48
|
+
computer: allow
|
|
22
49
|
|
|
23
50
|
bash:
|
|
24
51
|
# Translated from templates/.claude/hooks/block-dangerous.sh's DANGEROUS_PATTERNS array.
|
|
@@ -27,8 +54,9 @@ bash:
|
|
|
27
54
|
# catches it. `allow` only ever matches an ENTIRE, non-compound command, so the trailing "*"
|
|
28
55
|
# allow is NOT a universal escape hatch: any compound command (`&&`, `;`, `|`) not itself caught
|
|
29
56
|
# by a `deny`/`prompt` entry falls through to `tools.approvalMode` instead of being auto-allowed.
|
|
30
|
-
# `bash.patterns` does not cover the `eval` tool at all — that is why
|
|
31
|
-
#
|
|
57
|
+
# `bash.patterns` does not cover the `eval` tool at all — that is why TASK-004's bridge
|
|
58
|
+
# separately maps `eval` -> `Bash`, and why `tools.approval.eval: allow` above does NOT
|
|
59
|
+
# weaken eval safety: eval stays gated by the bridge's Bash chain (TASK-001).
|
|
32
60
|
patterns:
|
|
33
61
|
- match: "rm -rf /*"
|
|
34
62
|
approval: deny
|
|
@@ -294,10 +294,18 @@ function translateExecResult(scriptName, execResult) {
|
|
|
294
294
|
if (code === 2) {
|
|
295
295
|
const structured = parseStructuredDecision(stdout);
|
|
296
296
|
if (structured?.permissionDecision === 'ask') {
|
|
297
|
+
const askBase = structured.reason || stderr || `${scriptName} requires a human decision`;
|
|
298
|
+
// omp has no native `ask` — hook() converts this to a block. A bare
|
|
299
|
+
// "defers to a human" reason dead-ended under YOLO, so the reason must
|
|
300
|
+
// name the cause class + the recovery action and the chain must surface
|
|
301
|
+
// a display:true note (done in runScriptChain) so a deliberate block is
|
|
302
|
+
// distinguishable from a stall.
|
|
297
303
|
return {
|
|
298
304
|
block: false,
|
|
299
305
|
ask: true,
|
|
300
|
-
reason:
|
|
306
|
+
reason: `${askBase} [UKit ask→block: omp has no native "ask", so this gate decision was surfaced as a block. `
|
|
307
|
+
+ 'Recovery: if the cause was an oversized or truncated payload, shrink the payload or re-send it; '
|
|
308
|
+
+ 'if the gate is asking for a decision, decide explicitly — answer the question and re-send the call.]',
|
|
301
309
|
stdout,
|
|
302
310
|
stderr,
|
|
303
311
|
};
|
|
@@ -628,6 +636,9 @@ export async function runScriptChain(
|
|
|
628
636
|
return { block: true, reason: verdict.reason, context, invoked };
|
|
629
637
|
}
|
|
630
638
|
if (verdict.ask) {
|
|
639
|
+
// The block must be user-visible, not just model-visible — a deliberate
|
|
640
|
+
// ask→block has to be distinguishable from a silent stall.
|
|
641
|
+
sendContext(pi, [`[UKit] Gate asks for a human decision — the call was blocked: ${verdict.reason}`], 'nextTurn', { display: true });
|
|
631
642
|
return { block: false, ask: true, reason: verdict.reason, context, invoked };
|
|
632
643
|
}
|
|
633
644
|
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
<!-- RULE: EXEC-04 -->
|
|
27
27
|
- **Every stop says why — no silent idle.** Turns ending on a user-only action open with `WAITING ON YOU: <command/action>` plus a one-shot wakeup (~20-30 min) when available — an ended turn cannot observe external changes, so without it idle looks identical to a stall. Report any error verbatim the same turn.
|
|
28
28
|
|
|
29
|
+
## Stall & Wait Reporting
|
|
30
|
+
<!-- RULE: STALL-01 -->
|
|
31
|
+
- Announce long waits as they happen: `Waiting for API response / tool result — will keep retrying; check your network/API provider if this persists`. Surface any API/tool error **verbatim** in the same turn, and name network/API as the suspect on long stalls — **never blame UKit**. This guidance makes the model's side of a wait visible; it cannot detect a stall inside the host's own request loop.
|
|
32
|
+
<!-- RULE: STALL-02 -->
|
|
33
|
+
- Every stop still says why (Execution Contract EXEC-04) — the stall rules above extend it, they do not replace it.
|
|
34
|
+
<!-- RULE: STALL-03 -->
|
|
35
|
+
- **Conditional workaround only:** fall back to one tool call per assistant message ONLY when malformed/concatenated tool-input errors are observed or the harness version is known-affected — otherwise keep using legitimate parallel calls; serializing healthy hosts contradicts batching guidance.
|
|
36
|
+
|
|
29
37
|
## Long-Run Continuity
|
|
30
38
|
<!-- RULES: LONG-01 LONG-02 -->
|
|
31
39
|
- Near token-cap: **LAND one thing** end-to-end (edit + verify, ≤3 tool calls), **DEFER** the rest into `docs/STATUS.md` or bounded `docs/AI_HANDOFF/` tasks, **DELEGATE** broad work to subagents. Only then compact.
|
|
@@ -69,8 +77,8 @@ For any task needing code context:
|
|
|
69
77
|
|
|
70
78
|
## UKit v{{ukit.version}} Shared Runtime
|
|
71
79
|
- Runtime state lives in `.ukit/storage/`; `.ukit/storage/config.json` holds runtime toggles (compact, token pipeline, router, memory, validation, Safe Patch).
|
|
72
|
-
- Reuse `.ukit/storage/memory/` + `ukit memory recall "<current task>"` before asking users to restate decisions; inspect via `ukit status` / `ukit memory export`.
|
|
73
|
-
- Route memory: `.claude/ukit/skill-router-state.json` — reuse compact `previous-context`/`recent-output` first. Cache state: `.ukit/storage/cache/output-history.json`, tee
|
|
80
|
+
- Reuse `.ukit/storage/memory/` + `ukit memory recall "<current task>"` before asking users to restate decisions; `ukit memory learn` surfaces pending proposals, `ukit memory promote` writes approved rules to MEMORY.md, `ukit memory episode` records session episodes; inspect via `ukit status` / `ukit memory export`.
|
|
81
|
+
- Route memory: `.claude/ukit/skill-router-state.json` — reuse compact `previous-context`/`recent-output` first. Cache state: `.ukit/storage/cache/output-history.json`, `retriever-lanes.jsonl`, `tee/`. Lifecycle hooks all degrade to exit 0 (SessionEnd `session-episode.sh` auto-writes episodes; full map: `docs/UKIT_INTERNALS.md`). Missing/corrupt runtime or old `ukit/` root → rerun `ukit install`. Detail: `docs/UKIT_INTERNALS.md`.
|
|
74
82
|
|
|
75
83
|
## Prompt Caching
|
|
76
84
|
<!-- RULES: CTX-01 CTX-02 CTX-03 CTX-04 CTX-05 CTX-06 CTX-07 CTX-08 CTX-09 CTX-10 -->
|
|
@@ -84,9 +92,7 @@ For any task needing code context:
|
|
|
84
92
|
|
|
85
93
|
## Handoff Quality Gate — OPT-IN
|
|
86
94
|
<!-- RULE: HAND-01 -->
|
|
87
|
-
CHỈ kích hoạt khi task đi qua `docs/AI_HANDOFF/` (user nói "execute task TASK-xxx" hoặc target là `docs/AI_HANDOFF/tasks/*.md`). Daily prompt → KHÔNG đụng, flow cũ giữ nguyên.
|
|
88
|
-
|
|
89
|
-
Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+Plan → Create Tasks → Implement+Test → Review+Test) + state machine + self-report model. Config: `.ukit/storage/config.json` → `handoff.*`.
|
|
95
|
+
CHỈ kích hoạt khi task đi qua `docs/AI_HANDOFF/` (user nói "execute task TASK-xxx" hoặc target là `docs/AI_HANDOFF/tasks/*.md`). Daily prompt → KHÔNG đụng, flow cũ giữ nguyên. Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+Plan → Create Tasks → Implement+Test → Review+Test) + state machine + self-report model. Config: `.ukit/storage/config.json` → `handoff.*`.
|
|
90
96
|
|
|
91
97
|
## Context + Verification Budget
|
|
92
98
|
<!-- RULE: BUDGET-01 -->
|
|
@@ -99,14 +105,10 @@ Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+P
|
|
|
99
105
|
- `docs/STATUS.md` captures compact current state — not source truth, never replaces source/index-first investigation.
|
|
100
106
|
- "What next?"/"continue" → `next-step` with a freshness cue; after meaningful work → `update-status`. `docs/TASKS.md` is the local AI task queue — prefer `Ready for AI`. Detail: `docs/UKIT_INTERNALS.md`.
|
|
101
107
|
|
|
102
|
-
##
|
|
108
|
+
## Subagent Lanes (internal)
|
|
103
109
|
<!-- RULE: SUBAG-02 -->
|
|
104
110
|
- The `ukit-small-task-maintainer` subagent (`subagents.smallTaskModel`, default `unic-lite`) handles safe/reversible UKit chores as a sidecar lane — never block or slow the user task; risky work hands back to the main model. Detail: `docs/UKIT_INTERNALS.md`.
|
|
105
|
-
|
|
106
|
-
## Post-Edit Sidecar Review (internal)
|
|
107
111
|
- When routed state's `routeSummary.line` carries `review=code-reviewer(diff)`, launch `code-reviewer` in background (`smart` tier) **only after** write + verification evidence; findings advisory — never block the reported completion. Detail: `docs/UKIT_INTERNALS.md`.
|
|
108
|
-
|
|
109
|
-
## Selective Subagent Policy (internal only)
|
|
110
112
|
<!-- RULE: SUBAG-01 -->
|
|
111
113
|
- Direct execution is default for trivial/simple work; delegate only on meaningful context shrink or parallel gains (noisy side lanes, 3+ independent failures, batch plans). Never ask end users to name agents or remember agent commands.
|
|
112
114
|
|
|
@@ -114,6 +116,16 @@ Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+P
|
|
|
114
116
|
<!-- RULE: AUTO-01 -->
|
|
115
117
|
- `autonomy.level` in `.ukit/storage/config.json` controls how much UKit acts without asking: `conservative` (ask more), `balanced` (default), `free-run` (auto-run more), `vibecode` (one prompt to a finished result; the gate stops only on completion evidence, a genuine blocker, or a dangerous-command decision). End users should not need to change it.
|
|
116
118
|
|
|
119
|
+
## Unattended Completion Loop
|
|
120
|
+
<!-- RULE: UNATTENDED-01 -->
|
|
121
|
+
- Under `orchestration.permissionMode: unattended` there is no human in the loop — the loop is UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny decision.
|
|
122
|
+
<!-- RULE: UNATTENDED-02 -->
|
|
123
|
+
- `maxAttemptsPerFailure: 5` per failing verification, `maxRecoveryStrategies: 3` distinct fix strategies before reassessing — retry the same edit, fix the cause differently, then reduce scope/approach; do not loop one strategy.
|
|
124
|
+
<!-- RULE: UNATTENDED-03 -->
|
|
125
|
+
- BLOCKED only on true external blockers: missing credentials, dead service, unreachable network/API. Test failures, lint errors, review findings, and ambiguity are NOT blockers — they feed FIX.
|
|
126
|
+
<!-- RULE: UNATTENDED-04 -->
|
|
127
|
+
- A deny (hook/gate/permission) never becomes a question: pick the next safer alternative that satisfies the intent and record the substitution. Never emit "do you want me to…" — decide and proceed.
|
|
128
|
+
|
|
117
129
|
## 3-Tier Model Routing
|
|
118
130
|
<!-- RULES: TIER-01 TIER-02 -->
|
|
119
131
|
**Internal orchestration only — end users still just use natural language. No new commands.**
|
package/templates/CLAUDE.md
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
<!-- RULE: EXEC-04 -->
|
|
27
27
|
- **Every stop says why — no silent idle.** Turns ending on a user-only action open with `WAITING ON YOU: <command/action>` plus a one-shot wakeup (~20-30 min) when available — an ended turn cannot observe external changes, so without it idle looks identical to a stall. Report any error verbatim the same turn.
|
|
28
28
|
|
|
29
|
+
## Stall & Wait Reporting
|
|
30
|
+
<!-- RULE: STALL-01 -->
|
|
31
|
+
- Announce long waits as they happen: `Waiting for API response / tool result — will keep retrying; check your network/API provider if this persists`. Surface any API/tool error **verbatim** in the same turn, and name network/API as the suspect on long stalls — **never blame UKit**. This guidance makes the model's side of a wait visible; it cannot detect a stall inside the host's own request loop.
|
|
32
|
+
<!-- RULE: STALL-02 -->
|
|
33
|
+
- Every stop still says why (Execution Contract EXEC-04) — the stall rules above extend it, they do not replace it.
|
|
34
|
+
<!-- RULE: STALL-03 -->
|
|
35
|
+
- **Conditional workaround only:** fall back to one tool call per assistant message ONLY when malformed/concatenated tool-input errors are observed or the harness version is known-affected — otherwise keep using legitimate parallel calls; serializing healthy hosts contradicts batching guidance.
|
|
36
|
+
|
|
29
37
|
## Long-Run Continuity
|
|
30
38
|
<!-- RULES: LONG-01 LONG-02 -->
|
|
31
39
|
- Near token-cap: **LAND one thing** end-to-end (edit + verify, ≤3 tool calls), **DEFER** the rest into `docs/STATUS.md` or bounded `docs/AI_HANDOFF/` tasks, **DELEGATE** broad work to subagents. Only then compact.
|
|
@@ -69,8 +77,8 @@ For any task needing code context:
|
|
|
69
77
|
|
|
70
78
|
## UKit v{{ukit.version}} Shared Runtime
|
|
71
79
|
- Runtime state lives in `.ukit/storage/`; `.ukit/storage/config.json` holds runtime toggles (compact, token pipeline, router, memory, validation, Safe Patch).
|
|
72
|
-
- Reuse `.ukit/storage/memory/` + `ukit memory recall "<current task>"` before asking users to restate decisions; inspect via `ukit status` / `ukit memory export`.
|
|
73
|
-
- Route memory: `.claude/ukit/skill-router-state.json` — reuse compact `previous-context`/`recent-output` first. Cache state: `.ukit/storage/cache/output-history.json`, tee
|
|
80
|
+
- Reuse `.ukit/storage/memory/` + `ukit memory recall "<current task>"` before asking users to restate decisions; `ukit memory learn` surfaces pending proposals, `ukit memory promote` writes approved rules to MEMORY.md, `ukit memory episode` records session episodes; inspect via `ukit status` / `ukit memory export`.
|
|
81
|
+
- Route memory: `.claude/ukit/skill-router-state.json` — reuse compact `previous-context`/`recent-output` first. Cache state: `.ukit/storage/cache/output-history.json`, `retriever-lanes.jsonl`, `tee/`. Lifecycle hooks all degrade to exit 0 (SessionEnd `session-episode.sh` auto-writes episodes; full map: `docs/UKIT_INTERNALS.md`). Missing/corrupt runtime or old `ukit/` root → rerun `ukit install`. Detail: `docs/UKIT_INTERNALS.md`.
|
|
74
82
|
|
|
75
83
|
## Prompt Caching
|
|
76
84
|
<!-- RULES: CTX-01 CTX-02 CTX-03 CTX-04 CTX-05 CTX-06 CTX-07 CTX-08 CTX-09 CTX-10 -->
|
|
@@ -84,9 +92,7 @@ For any task needing code context:
|
|
|
84
92
|
|
|
85
93
|
## Handoff Quality Gate — OPT-IN
|
|
86
94
|
<!-- RULE: HAND-01 -->
|
|
87
|
-
CHỈ kích hoạt khi task đi qua `docs/AI_HANDOFF/` (user nói "execute task TASK-xxx" hoặc target là `docs/AI_HANDOFF/tasks/*.md`). Daily prompt → KHÔNG đụng, flow cũ giữ nguyên.
|
|
88
|
-
|
|
89
|
-
Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+Plan → Create Tasks → Implement+Test → Review+Test) + state machine + self-report model. Config: `.ukit/storage/config.json` → `handoff.*`.
|
|
95
|
+
CHỈ kích hoạt khi task đi qua `docs/AI_HANDOFF/` (user nói "execute task TASK-xxx" hoặc target là `docs/AI_HANDOFF/tasks/*.md`). Daily prompt → KHÔNG đụng, flow cũ giữ nguyên. Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+Plan → Create Tasks → Implement+Test → Review+Test) + state machine + self-report model. Config: `.ukit/storage/config.json` → `handoff.*`.
|
|
90
96
|
|
|
91
97
|
## Context + Verification Budget
|
|
92
98
|
<!-- RULE: BUDGET-01 -->
|
|
@@ -99,14 +105,10 @@ Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+P
|
|
|
99
105
|
- `docs/STATUS.md` captures compact current state — not source truth, never replaces source/index-first investigation.
|
|
100
106
|
- "What next?"/"continue" → `next-step` with a freshness cue; after meaningful work → `update-status`. `docs/TASKS.md` is the local AI task queue — prefer `Ready for AI`. Detail: `docs/UKIT_INTERNALS.md`.
|
|
101
107
|
|
|
102
|
-
##
|
|
108
|
+
## Subagent Lanes (internal)
|
|
103
109
|
<!-- RULE: SUBAG-02 -->
|
|
104
110
|
- The `ukit-small-task-maintainer` subagent (`subagents.smallTaskModel`, default `unic-lite`) handles safe/reversible UKit chores as a sidecar lane — never block or slow the user task; risky work hands back to the main model. Detail: `docs/UKIT_INTERNALS.md`.
|
|
105
|
-
|
|
106
|
-
## Post-Edit Sidecar Review (internal)
|
|
107
111
|
- When routed state's `routeSummary.line` carries `review=code-reviewer(diff)`, launch `code-reviewer` in background (`smart` tier) **only after** write + verification evidence; findings advisory — never block the reported completion. Detail: `docs/UKIT_INTERNALS.md`.
|
|
108
|
-
|
|
109
|
-
## Selective Subagent Policy (internal only)
|
|
110
112
|
<!-- RULE: SUBAG-01 -->
|
|
111
113
|
- Direct execution is default for trivial/simple work; delegate only on meaningful context shrink or parallel gains (noisy side lanes, 3+ independent failures, batch plans). Never ask end users to name agents or remember agent commands.
|
|
112
114
|
|
|
@@ -114,6 +116,16 @@ Khi Handoff mode: đọc `docs/AI_HANDOFF/RULES.md` để biết 4 phase (Idea+P
|
|
|
114
116
|
<!-- RULE: AUTO-01 -->
|
|
115
117
|
- `autonomy.level` in `.ukit/storage/config.json` controls how much UKit acts without asking: `conservative` (ask more), `balanced` (default), `free-run` (auto-run more), `vibecode` (one prompt to a finished result; the gate stops only on completion evidence, a genuine blocker, or a dangerous-command decision). End users should not need to change it.
|
|
116
118
|
|
|
119
|
+
## Unattended Completion Loop
|
|
120
|
+
<!-- RULE: UNATTENDED-01 -->
|
|
121
|
+
- Under `orchestration.permissionMode: unattended` there is no human in the loop — the loop is UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny decision.
|
|
122
|
+
<!-- RULE: UNATTENDED-02 -->
|
|
123
|
+
- `maxAttemptsPerFailure: 5` per failing verification, `maxRecoveryStrategies: 3` distinct fix strategies before reassessing — retry the same edit, fix the cause differently, then reduce scope/approach; do not loop one strategy.
|
|
124
|
+
<!-- RULE: UNATTENDED-03 -->
|
|
125
|
+
- BLOCKED only on true external blockers: missing credentials, dead service, unreachable network/API. Test failures, lint errors, review findings, and ambiguity are NOT blockers — they feed FIX.
|
|
126
|
+
<!-- RULE: UNATTENDED-04 -->
|
|
127
|
+
- A deny (hook/gate/permission) never becomes a question: pick the next safer alternative that satisfies the intent and record the substitution. Never emit "do you want me to…" — decide and proceed.
|
|
128
|
+
|
|
117
129
|
## 3-Tier Model Routing
|
|
118
130
|
<!-- RULES: TIER-01 TIER-02 -->
|
|
119
131
|
**Internal orchestration only — end users still just use natural language. No new commands.**
|