@ngockhoale/ukit 2.2.1 → 2.2.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.
- package/CHANGELOG.md +47 -0
- package/manifests/platform.full.yaml +54 -0
- package/package.json +1 -1
- package/templates/.claude/commands/ukit/handoff-clear.md +1 -1
- package/templates/.claude/commands/ukit/handoff-create.md +4 -4
- package/templates/.claude/commands/ukit/handoff-fullstack.md +11 -9
- package/templates/.claude/commands/ukit/handoff-implement.md +2 -2
- package/templates/.claude/commands/ukit/handoff-review.md +1 -1
- package/templates/.claude/commands/ukit/handoff-status.md +1 -1
- package/templates/.claude/hooks/completion-gate.sh +12 -0
- package/templates/.claude/hooks/record-execution.sh +12 -0
- package/templates/.claude/settings.json +31 -0
- package/templates/.claude/ukit/index/unic-gateway.mjs +23 -26
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +354 -0
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +115 -0
- package/templates/.omp/RULES.md +40 -12
- package/templates/.omp/config.yml +12 -9
- package/templates/.omp/hooks/pre/ukit-bridge.js +310 -181
- package/templates/AGENTS.md +131 -63
- package/templates/CLAUDE.md +59 -21
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import fsSync from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const LEDGER_VERSION = 1;
|
|
10
|
+
const MAX_RECEIPTS = 24;
|
|
11
|
+
const MAX_CONTINUATIONS = 6;
|
|
12
|
+
const IMPLEMENT_MODES = new Set([
|
|
13
|
+
'tiny-fix',
|
|
14
|
+
'local-fix',
|
|
15
|
+
'local-build',
|
|
16
|
+
'shared-edit',
|
|
17
|
+
'find-cause',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function safeSegment(value) {
|
|
21
|
+
return String(value || 'default')
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
24
|
+
.slice(0, 96) || 'default';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function firstDefined(values) {
|
|
28
|
+
return values.find((value) => value !== undefined && value !== null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sessionIdentity(payload = {}) {
|
|
32
|
+
const sessionId = firstDefined([
|
|
33
|
+
payload.session_id,
|
|
34
|
+
payload.sessionId,
|
|
35
|
+
]);
|
|
36
|
+
if (sessionId) return safeSegment(sessionId);
|
|
37
|
+
|
|
38
|
+
const transcriptPath = firstDefined([
|
|
39
|
+
payload.transcript_path,
|
|
40
|
+
payload.transcriptPath,
|
|
41
|
+
]);
|
|
42
|
+
if (transcriptPath) {
|
|
43
|
+
return `transcript-${crypto.createHash('sha256').update(String(transcriptPath)).digest('hex').slice(0, 20)}`;
|
|
44
|
+
}
|
|
45
|
+
return 'default';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ledgerPath(projectRoot, payload = {}) {
|
|
49
|
+
return path.join(
|
|
50
|
+
projectRoot,
|
|
51
|
+
'.ukit',
|
|
52
|
+
'storage',
|
|
53
|
+
'cache',
|
|
54
|
+
'exec-ledger',
|
|
55
|
+
`${sessionIdentity(payload)}.json`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readJson(filePath, fallback = null) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writeJsonAtomic(filePath, value) {
|
|
68
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
69
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
70
|
+
await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
71
|
+
await fs.rename(tempPath, filePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function readRouteState(projectRoot) {
|
|
75
|
+
return readJson(path.join(projectRoot, '.claude', 'ukit', 'skill-router-state.json'), null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function readExecutionLedger(projectRoot, payload = {}) {
|
|
79
|
+
return readJson(ledgerPath(projectRoot, payload), null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function explicitError(payload = {}) {
|
|
83
|
+
return [
|
|
84
|
+
payload.isError,
|
|
85
|
+
payload.is_error,
|
|
86
|
+
payload.tool_output?.isError,
|
|
87
|
+
payload.tool_output?.is_error,
|
|
88
|
+
payload.tool_result?.isError,
|
|
89
|
+
payload.tool_result?.is_error,
|
|
90
|
+
payload.tool_response?.isError,
|
|
91
|
+
payload.tool_response?.is_error,
|
|
92
|
+
].some((value) => value === true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractExitCode(payload = {}) {
|
|
96
|
+
const candidates = [
|
|
97
|
+
payload.tool_output?.exitCode,
|
|
98
|
+
payload.tool_output?.exit_code,
|
|
99
|
+
payload.tool_result?.details?.exitCode,
|
|
100
|
+
payload.tool_result?.details?.exit_code,
|
|
101
|
+
payload.tool_result?.exitCode,
|
|
102
|
+
payload.tool_result?.exit_code,
|
|
103
|
+
payload.tool_response?.exitCode,
|
|
104
|
+
payload.tool_response?.exit_code,
|
|
105
|
+
payload.exitCode,
|
|
106
|
+
payload.exit_code,
|
|
107
|
+
];
|
|
108
|
+
for (const value of candidates) {
|
|
109
|
+
const number = Number(value);
|
|
110
|
+
if (Number.isFinite(number)) return number;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isVerificationCommand(command) {
|
|
116
|
+
const text = String(command || '').trim();
|
|
117
|
+
if (!text) return false;
|
|
118
|
+
return [
|
|
119
|
+
/(?:^|\s)(?:vitest|jest|mocha|ava|pytest|py\.test)(?:\s|$)/i,
|
|
120
|
+
/(?:^|\s)(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+(?:test|lint|typecheck|check|build)(?:\s|$)/i,
|
|
121
|
+
/(?:^|\s)(?:tsc|eslint|biome|ruff|mypy)(?:\s|$)/i,
|
|
122
|
+
/(?:^|\s)node\s+--check(?:\s|$)/i,
|
|
123
|
+
].some((pattern) => pattern.test(text));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compactReceipt(receipt) {
|
|
127
|
+
const compact = {
|
|
128
|
+
ts: receipt.ts,
|
|
129
|
+
kind: receipt.kind,
|
|
130
|
+
success: receipt.success,
|
|
131
|
+
};
|
|
132
|
+
for (const key of ['toolName', 'toolUseId', 'file', 'command', 'exitCode', 'error']) {
|
|
133
|
+
if (receipt[key] !== undefined && receipt[key] !== null && receipt[key] !== '') {
|
|
134
|
+
compact[key] = receipt[key];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return compact;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function appendReceipt(receipts, receipt) {
|
|
141
|
+
return [...(receipts || []), compactReceipt(receipt)].slice(-MAX_RECEIPTS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function freshLedger(payload, routeState, harness) {
|
|
145
|
+
return {
|
|
146
|
+
version: LEDGER_VERSION,
|
|
147
|
+
sessionKey: sessionIdentity(payload),
|
|
148
|
+
sessionId: payload.session_id || payload.sessionId || null,
|
|
149
|
+
transcriptPath: payload.transcript_path || payload.transcriptPath || null,
|
|
150
|
+
harness,
|
|
151
|
+
requestKey: routeState?.requestKey || null,
|
|
152
|
+
routeFingerprint: routeState?.fingerprint || null,
|
|
153
|
+
sourceSucceeded: false,
|
|
154
|
+
writeAttempted: false,
|
|
155
|
+
writeSucceeded: false,
|
|
156
|
+
verificationAttempted: false,
|
|
157
|
+
verificationSucceeded: false,
|
|
158
|
+
verificationFailed: false,
|
|
159
|
+
receipts: [],
|
|
160
|
+
blocker: null,
|
|
161
|
+
continuationCount: 0,
|
|
162
|
+
updatedAt: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function recordExecutionReceipt({
|
|
167
|
+
projectRoot,
|
|
168
|
+
payload = {},
|
|
169
|
+
toolName = payload.tool_name,
|
|
170
|
+
harness = 'unknown',
|
|
171
|
+
} = {}) {
|
|
172
|
+
if (!projectRoot || !toolName) return null;
|
|
173
|
+
const routeState = await readRouteState(projectRoot);
|
|
174
|
+
const current = await readExecutionLedger(projectRoot, payload);
|
|
175
|
+
const ledger = !current || current.requestKey !== (routeState?.requestKey || null)
|
|
176
|
+
? freshLedger(payload, routeState, harness)
|
|
177
|
+
: { ...current, harness: current.harness || harness };
|
|
178
|
+
|
|
179
|
+
const failed = explicitError(payload);
|
|
180
|
+
const exitCode = extractExitCode(payload);
|
|
181
|
+
const success = !failed && (exitCode === null || exitCode === 0);
|
|
182
|
+
const toolInput = payload.tool_input || {};
|
|
183
|
+
const receipt = {
|
|
184
|
+
ts: Date.now(),
|
|
185
|
+
toolName,
|
|
186
|
+
toolUseId: payload.tool_use_id || null,
|
|
187
|
+
success,
|
|
188
|
+
exitCode,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (toolName === 'Read' || toolName === 'Grep' || toolName === 'Glob') {
|
|
192
|
+
receipt.kind = 'source';
|
|
193
|
+
receipt.file = toolInput.file_path || toolInput.path || null;
|
|
194
|
+
ledger.sourceSucceeded ||= success;
|
|
195
|
+
} else if (toolName === 'Edit' || toolName === 'Write') {
|
|
196
|
+
receipt.kind = 'write';
|
|
197
|
+
receipt.file = toolInput.file_path || toolInput.path || toolInput.paths?.[0] || null;
|
|
198
|
+
ledger.writeAttempted = true;
|
|
199
|
+
ledger.writeSucceeded ||= success;
|
|
200
|
+
} else if (toolName === 'Bash' && isVerificationCommand(toolInput.command)) {
|
|
201
|
+
receipt.kind = 'verification';
|
|
202
|
+
receipt.command = String(toolInput.command || '').trim();
|
|
203
|
+
ledger.verificationAttempted = true;
|
|
204
|
+
ledger.verificationSucceeded ||= success;
|
|
205
|
+
ledger.verificationFailed ||= !success;
|
|
206
|
+
} else {
|
|
207
|
+
return ledger;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ledger.receipts = appendReceipt(ledger.receipts, receipt);
|
|
211
|
+
ledger.updatedAt = Date.now();
|
|
212
|
+
await writeJsonAtomic(ledgerPath(projectRoot, payload), ledger);
|
|
213
|
+
return ledger;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function requiredEvidence(state = {}) {
|
|
217
|
+
const routeSummary = state?.routeSummary || {};
|
|
218
|
+
const contractEvidence = routeSummary.executionContract?.completionEvidence;
|
|
219
|
+
if (Array.isArray(contractEvidence) && contractEvidence.length > 0) {
|
|
220
|
+
return [...new Set(contractEvidence)];
|
|
221
|
+
}
|
|
222
|
+
return [...new Set(routeSummary.completionState?.missingEvidence || [])];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function evidenceSatisfied(evidence, ledger = {}) {
|
|
226
|
+
if (evidence === 'write-evidence') return ledger.writeSucceeded === true;
|
|
227
|
+
if (evidence === 'verification-evidence') return ledger.verificationSucceeded === true;
|
|
228
|
+
if (evidence === 'impact-evidence') return ledger.sourceSucceeded === true;
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function recoveryInstruction(missingEvidence, ledger = {}) {
|
|
233
|
+
if (missingEvidence.includes('write-evidence')) {
|
|
234
|
+
if (!ledger.sourceSucceeded) {
|
|
235
|
+
return 'Pull one bounded indexed source slice, then make the requested Edit/Write in this continuation.';
|
|
236
|
+
}
|
|
237
|
+
if (ledger.writeAttempted && !ledger.writeSucceeded) {
|
|
238
|
+
return 'Recover from the failed mutation using the current error, then retry the smallest correct Edit/Write.';
|
|
239
|
+
}
|
|
240
|
+
return 'Make the smallest correct Edit/Write now; do not end after more read-only analysis.';
|
|
241
|
+
}
|
|
242
|
+
if (missingEvidence.includes('verification-evidence')) {
|
|
243
|
+
if (ledger.verificationFailed) {
|
|
244
|
+
return 'Use the latest failed verification output, fix the failure, and rerun targeted verification.';
|
|
245
|
+
}
|
|
246
|
+
return 'Run the routed targeted verification now and inspect its result before stopping.';
|
|
247
|
+
}
|
|
248
|
+
return 'Complete the current routed milestone before stopping.';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function evaluateCompletion({ state = {}, ledger = {} } = {}) {
|
|
252
|
+
const routeSummary = state?.routeSummary || {};
|
|
253
|
+
const mode = routeSummary.executionMode || routeSummary.approachSelector?.executionMode || null;
|
|
254
|
+
const evidence = requiredEvidence(state);
|
|
255
|
+
if (!IMPLEMENT_MODES.has(mode) || evidence.length === 0 || ledger?.blocker) {
|
|
256
|
+
return { continue: false, missingEvidence: [] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sameRequest = !ledger?.requestKey || !state?.requestKey || ledger.requestKey === state.requestKey;
|
|
260
|
+
const effectiveLedger = sameRequest ? ledger : {};
|
|
261
|
+
const missingEvidence = evidence.filter((item) => !evidenceSatisfied(item, effectiveLedger));
|
|
262
|
+
if (missingEvidence.length === 0) {
|
|
263
|
+
return { continue: false, missingEvidence: [] };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const continuationCount = Number(effectiveLedger?.continuationCount || 0);
|
|
267
|
+
if (continuationCount >= MAX_CONTINUATIONS) {
|
|
268
|
+
return {
|
|
269
|
+
continue: false,
|
|
270
|
+
capped: true,
|
|
271
|
+
missingEvidence,
|
|
272
|
+
reason: `UKit continuation cap reached with missing evidence: ${missingEvidence.join(', ')}.`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const finalAttempt = continuationCount === MAX_CONTINUATIONS - 1;
|
|
277
|
+
const instruction = recoveryInstruction(missingEvidence, effectiveLedger);
|
|
278
|
+
return {
|
|
279
|
+
continue: true,
|
|
280
|
+
missingEvidence,
|
|
281
|
+
reason: [
|
|
282
|
+
`UKit completion gate: missing ${missingEvidence.join(', ')}.`,
|
|
283
|
+
instruction,
|
|
284
|
+
finalAttempt ? 'Final automatic recovery attempt: finish now or report a concrete blocker with its evidence.' : null,
|
|
285
|
+
].filter(Boolean).join(' '),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function incrementContinuation(projectRoot, payload = {}, ledger = null) {
|
|
290
|
+
const current = ledger || await readExecutionLedger(projectRoot, payload) || freshLedger(payload, null, 'unknown');
|
|
291
|
+
const next = {
|
|
292
|
+
...current,
|
|
293
|
+
continuationCount: Number(current.continuationCount || 0) + 1,
|
|
294
|
+
lastContinuationAt: Date.now(),
|
|
295
|
+
updatedAt: Date.now(),
|
|
296
|
+
};
|
|
297
|
+
await writeJsonAtomic(ledgerPath(projectRoot, payload), next);
|
|
298
|
+
return next;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function readStdin() {
|
|
302
|
+
if (process.stdin.isTTY) return '';
|
|
303
|
+
const chunks = [];
|
|
304
|
+
for await (const chunk of process.stdin) chunks.push(String(chunk));
|
|
305
|
+
return chunks.join('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
const payload = JSON.parse((await readStdin()) || '{}');
|
|
310
|
+
const projectRoot = process.env.CLAUDE_PROJECT_DIR || payload.cwd || process.cwd();
|
|
311
|
+
if (process.argv.includes('--record')) {
|
|
312
|
+
await recordExecutionReceipt({
|
|
313
|
+
projectRoot,
|
|
314
|
+
payload,
|
|
315
|
+
toolName: payload.tool_name,
|
|
316
|
+
harness: process.env.UKIT_HARNESS || 'claude-code',
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (process.argv.includes('--evaluate-stop')) {
|
|
321
|
+
const state = await readRouteState(projectRoot);
|
|
322
|
+
const ledger = await readExecutionLedger(projectRoot, payload) || {};
|
|
323
|
+
const result = evaluateCompletion({ state, ledger });
|
|
324
|
+
if (result.continue) {
|
|
325
|
+
await incrementContinuation(projectRoot, payload, ledger);
|
|
326
|
+
process.stdout.write(`${JSON.stringify({ decision: 'block', reason: result.reason })}\n`);
|
|
327
|
+
} else if (result.capped) {
|
|
328
|
+
process.stderr.write(`[ukit-completion] ${result.reason}\n`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Compare real paths: a project under a symlinked root (macOS /tmp -> /private/tmp, or a
|
|
334
|
+
// symlinked work directory) makes import.meta.url resolve to the real path while argv[1]
|
|
335
|
+
// keeps the symlinked spelling. Without realpath the guard silently never runs the CLI.
|
|
336
|
+
function isDirectRun() {
|
|
337
|
+
const argvPath = process.argv[1];
|
|
338
|
+
if (!argvPath) return false;
|
|
339
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
340
|
+
const resolved = path.resolve(argvPath);
|
|
341
|
+
if (selfPath === resolved) return true;
|
|
342
|
+
try {
|
|
343
|
+
return fsSync.realpathSync(selfPath) === fsSync.realpathSync(resolved);
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (isDirectRun()) {
|
|
350
|
+
main().catch((error) => {
|
|
351
|
+
process.stderr.write(`[ukit-execution-ledger] ${error?.message || error}\n`);
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const FAIL_CLOSED_SCRIPTS = new Set([
|
|
8
|
+
'protect-files.sh',
|
|
9
|
+
'stale-spec-guard.sh',
|
|
10
|
+
'handoff-model-guard.sh',
|
|
11
|
+
'vision-gate.sh',
|
|
12
|
+
'context-hardcap-gate.sh',
|
|
13
|
+
'block-dangerous.sh',
|
|
14
|
+
'verification-guard.sh',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const TOTAL_BUDGET_MS = 10000;
|
|
18
|
+
const CHILD_BUDGET_MS = 4000;
|
|
19
|
+
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
function safeName(value) {
|
|
22
|
+
return String(value || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'unknown';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function recordTiming(projectRoot, payload, timing) {
|
|
26
|
+
try {
|
|
27
|
+
const dir = path.join(projectRoot, '.ukit', 'storage', 'cache', 'hook-latency');
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const filePath = path.join(dir, `${safeName(payload?.session_id)}.jsonl`);
|
|
30
|
+
fs.appendFileSync(filePath, `${JSON.stringify(timing)}\n`, 'utf8');
|
|
31
|
+
} catch {
|
|
32
|
+
// Timing telemetry is advisory and must never delay or block a tool call.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run(payloadText, scriptPaths) {
|
|
37
|
+
const payload = JSON.parse(payloadText || '{}');
|
|
38
|
+
const firstScript = scriptPaths[0] || '';
|
|
39
|
+
const projectRoot = firstScript
|
|
40
|
+
? path.resolve(path.dirname(firstScript), '../..')
|
|
41
|
+
: (payload.cwd || process.cwd());
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
const deadline = startedAt + TOTAL_BUDGET_MS;
|
|
44
|
+
const results = [];
|
|
45
|
+
|
|
46
|
+
for (const scriptPath of scriptPaths) {
|
|
47
|
+
const scriptName = path.basename(scriptPath);
|
|
48
|
+
const remainingMs = deadline - Date.now();
|
|
49
|
+
if (remainingMs <= 0) {
|
|
50
|
+
results.push({
|
|
51
|
+
scriptName,
|
|
52
|
+
code: 1,
|
|
53
|
+
stdout: '',
|
|
54
|
+
stderr: `hook chain exceeded its ${TOTAL_BUDGET_MS}ms total budget`,
|
|
55
|
+
killed: true,
|
|
56
|
+
elapsedMs: 0,
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const childStartedAt = Date.now();
|
|
62
|
+
const result = spawnSync(scriptPath, [], {
|
|
63
|
+
cwd: projectRoot,
|
|
64
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectRoot },
|
|
65
|
+
input: payloadText,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
timeout: Math.min(CHILD_BUDGET_MS, remainingMs),
|
|
68
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
69
|
+
});
|
|
70
|
+
const code = Number.isFinite(result.status) ? result.status : 1;
|
|
71
|
+
const killed = Boolean(result.signal || result.error?.code === 'ETIMEDOUT');
|
|
72
|
+
const stderr = [result.stderr, result.error?.message].filter(Boolean).join('\n');
|
|
73
|
+
results.push({
|
|
74
|
+
scriptName,
|
|
75
|
+
code,
|
|
76
|
+
stdout: result.stdout || '',
|
|
77
|
+
stderr,
|
|
78
|
+
killed,
|
|
79
|
+
elapsedMs: Date.now() - childStartedAt,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (code === 2 || killed || (code !== 0 && FAIL_CLOSED_SCRIPTS.has(scriptName))) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const elapsedMs = Date.now() - startedAt;
|
|
88
|
+
recordTiming(projectRoot, payload, {
|
|
89
|
+
ts: Date.now(),
|
|
90
|
+
hookEvent: payload?.hook_event_name || null,
|
|
91
|
+
toolName: payload?.tool_name || null,
|
|
92
|
+
toolUseId: payload?.tool_use_id || null,
|
|
93
|
+
elapsedMs,
|
|
94
|
+
budgetMs: TOTAL_BUDGET_MS,
|
|
95
|
+
scripts: results.map(({ scriptName, code, killed, elapsedMs: scriptElapsedMs }) => ({
|
|
96
|
+
scriptName,
|
|
97
|
+
code,
|
|
98
|
+
killed,
|
|
99
|
+
elapsedMs: scriptElapsedMs,
|
|
100
|
+
})),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return { results, elapsedMs, budgetMs: TOTAL_BUDGET_MS };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const [, , payloadText = '{}', ...scriptPaths] = process.argv;
|
|
108
|
+
process.stdout.write(JSON.stringify(run(payloadText, scriptPaths)));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
process.stdout.write(JSON.stringify({
|
|
111
|
+
results: [],
|
|
112
|
+
wrapperError: error?.message || String(error),
|
|
113
|
+
}));
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
package/templates/.omp/RULES.md
CHANGED
|
@@ -1,34 +1,62 @@
|
|
|
1
1
|
# .omp/RULES.md — sticky always-apply rules
|
|
2
2
|
|
|
3
3
|
omp re-attaches this file near every turn from its native location (`.omp/RULES.md` only, never a
|
|
4
|
-
copy elsewhere). It carries the always-apply subset of root `
|
|
5
|
-
|
|
6
|
-
`CLAUDE.md`. Keep this short: every line here is a per-turn tax.
|
|
4
|
+
copy elsewhere). It carries the always-apply subset of root `AGENTS.md` that must survive even when
|
|
5
|
+
nothing else is loaded. Keep this short: every line here is a per-turn tax.
|
|
7
6
|
|
|
8
|
-
## 1.
|
|
7
|
+
## 1. Classify first, then act
|
|
8
|
+
|
|
9
|
+
- **Trivial** (typo, rename, flag, obvious config) — act directly. No doc reads, no index, no agents.
|
|
10
|
+
- **Simple** (1-2 files, existing pattern) — act directly, pull the smallest useful context.
|
|
11
|
+
- **Non-trivial / risky** (auth, migration, shared runtime, data-loss, flaky) — index-first, then
|
|
12
|
+
activate the matching skill, then verify.
|
|
13
|
+
|
|
14
|
+
Pick the lane before the first tool call. Skipping this is what turns a direct chat into a
|
|
15
|
+
scattershot one.
|
|
16
|
+
|
|
17
|
+
## 2. Execution Contract
|
|
9
18
|
|
|
10
19
|
For implement/apply/fix requests, continue until the actual edit is made or a real blocker is found.
|
|
11
20
|
Do not stop after a read-only inspection step (read/grep/glob/search).
|
|
12
21
|
|
|
13
|
-
##
|
|
22
|
+
## 3. No "done" after read-only
|
|
14
23
|
|
|
15
24
|
Never say "done", "applied", or "fixed" after a read-only step. Completion wording requires concrete
|
|
16
25
|
edit/write evidence in the current turn, plus verification when the change is risky.
|
|
17
26
|
|
|
18
|
-
##
|
|
27
|
+
## 4. Index-first loop
|
|
19
28
|
|
|
20
29
|
Check `.cache/index/` freshness first. Then run
|
|
21
30
|
`node .claude/ukit/index/query-index.mjs "<error|symbol|path>"` and open only the top 1-3 suspect
|
|
22
31
|
files before widening further. These node scripts work unchanged under omp.
|
|
23
32
|
|
|
24
|
-
##
|
|
33
|
+
## 5. Auto-activate skills
|
|
34
|
+
|
|
35
|
+
Skills live in `.claude/skills/` and omp reads them through its `claude` discovery provider — no
|
|
36
|
+
mirror under `.omp/`. On any non-trivial task, and again after the first tool calls reveal what the
|
|
37
|
+
work really is, read the matching `SKILL.md` without being asked. Users never name skills.
|
|
38
|
+
|
|
39
|
+
## 6. Delegate to bind a model tier
|
|
40
|
+
|
|
41
|
+
Your own model does not change mid-turn. A tier only applies when work is handed to a task-agent in
|
|
42
|
+
`.omp/agents/`, whose `model:` field (`@lite` / `@code` / `@smart` / `@vision`) resolves through
|
|
43
|
+
`modelRoles` in `.omp/config.yml`.
|
|
44
|
+
|
|
45
|
+
- `tiny-fix` → `@lite` · `local-fix`, `local-build`, `shared-edit`, `find-cause`, `map-impact` →
|
|
46
|
+
`@code` · `review-release` → `@smart`
|
|
47
|
+
- Images: `@vision` only. `@code`/`@smart` cannot see images on the UNIC gateway and must never
|
|
48
|
+
guess at their contents — hand every image to `ukit-vision-analyst` first.
|
|
49
|
+
|
|
50
|
+
Doing everything inline is exactly what makes UKit look like it only has one model. Keep direct
|
|
51
|
+
execution for trivial/simple work; delegate when the tier differs or when a noisy lane would other-
|
|
52
|
+
wise flood this context.
|
|
53
|
+
|
|
54
|
+
## 7. Safe Patch
|
|
25
55
|
|
|
26
56
|
Prefer unique current-file anchors over line numbers or stale pasted blocks. Never silently merge a
|
|
27
57
|
stale spec — re-read current source and confirm before applying. Preserve existing BOM and line
|
|
28
58
|
endings.
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
only the three cost-tier values — `lite`, `code`, `smart` — never `vision`, which stays `unic-vision`
|
|
34
|
-
because it is a capability lane, not a cost tier.
|
|
60
|
+
> Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
|
|
61
|
+
> On a non-UNIC provider, edit only the three cost tiers — `lite`, `code`, `smart` — never `vision`,
|
|
62
|
+
> which stays `unic-vision` because it is a capability lane, not a cost tier.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# UNIC gateway model names are
|
|
2
|
-
#
|
|
1
|
+
# UNIC gateway model names are stable provider-neutral aliases. Their hidden backend
|
|
2
|
+
# models may change during development; orchestration must not branch on vendor names.
|
|
3
|
+
# Do NOT substitute sonnet/opus/haiku here. See PLAN.md §3 D15.
|
|
3
4
|
#
|
|
4
5
|
# Non-UNIC omp provider? A maintainer edits the three cost tiers to the values in
|
|
5
6
|
# orchestration.modelTiers[*].claudeModel (.ukit/storage/config.json) — currently
|
|
@@ -79,10 +80,12 @@ memory:
|
|
|
79
80
|
# Auto-compact must trigger BEFORE context-hardcap-gate.sh starts blocking tool
|
|
80
81
|
# calls (compact.hardCapTokens, default 220000). Mirrors the Claude Code value
|
|
81
82
|
# env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = 180000 in templates/.claude/settings.json.
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
compact
|
|
88
|
-
|
|
83
|
+
#
|
|
84
|
+
# Key VERIFIED against omp v17.4.2 (2026-08-22): `compaction.thresholdTokens` is a
|
|
85
|
+
# fixed token limit for context maintenance and overrides the percentage threshold
|
|
86
|
+
# when set; omp's own default is -1 ("use compaction.thresholdPercent"). The key this
|
|
87
|
+
# file shipped with in 2.2.1 — `compact.autoCompactWindow` — was a plan-time guess and
|
|
88
|
+
# omp rejects it outright (`omp config get compact.autoCompactWindow` → "Unknown
|
|
89
|
+
# setting"), so auto-compact never moved off omp's default. See PLAN.md §3 D13.
|
|
90
|
+
compaction:
|
|
91
|
+
thresholdTokens: 180000
|