@ngockhoale/ukit 3.4.47 → 3.4.48
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 +8 -0
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +67 -16
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +147 -6
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- package/scripts/perf/perf-measure.md +0 -271
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: bug-debugger
|
|
3
3
|
description: "Debugging specialist for reproducible errors, failing tests, and unexpected behavior. Use proactively when investigation will involve noisy logs, stack traces, or a self-contained reproduce-trace-fix-verify loop. Do not use for trivial obvious fixes or broad architecture ideation."
|
|
4
4
|
model: sonnet # unic-code
|
|
5
|
+
effort: medium
|
|
5
6
|
color: red
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: code-reviewer
|
|
3
3
|
description: "Independent reviewer for handoff Phase 3 and for spec/plan documents. For code (default): use after executor reports STATUS: DONE on a handoff task, MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart), produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL. For spec/plan documents (set REVIEW_TARGET_TYPE=spec or plan): reviews a docs/plans/*.md file for completeness/consistency/clarity/scope/YAGNI, produces Status: Approved | Issues Found."
|
|
4
4
|
model: opus # unic-smart
|
|
5
|
+
effort: medium
|
|
5
6
|
color: yellow
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: feature-implementer
|
|
3
3
|
description: "Implementation specialist for clear, self-contained coding tasks. Use proactively when the approach is clear and the work can be completed in a bounded pass with a concise summary. Do not use for trivial one-file tweaks or tightly coupled exploratory work."
|
|
4
4
|
model: sonnet # unic-code
|
|
5
|
+
effort: medium
|
|
5
6
|
color: green
|
|
6
7
|
tools: ["Read", "Edit", "Write", "Grep", "Glob", "Bash", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: handoff-planner
|
|
3
3
|
description: "Handoff planning specialist for Phase 1+2. Use when creating a new handoff cycle: writes PLAN.md with full test plan, splits into TASK-xxx.md files with TDD-embedded test cases, and updates INDEX.md. Always use the strongest available model (Opus/unic-smart)."
|
|
4
4
|
model: opus # unic-smart
|
|
5
|
+
effort: medium
|
|
5
6
|
color: purple
|
|
6
7
|
tools: ["Read", "Edit", "Write", "Glob", "Bash", "TodoWrite"]
|
|
7
8
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: ukit-small-task-maintainer
|
|
3
3
|
description: "Internal UKit maintenance subagent for low-risk, reversible UKit decisions. Use proactively when UKit needs to decide or perform safe cleanup such as pruning docs/TASKS.md, classifying queued work, choosing whether compact/summarization is appropriate, summarizing local docs/status, or maintaining small UKit runtime queues. Do not use for product implementation, security, release/publish, data-loss, architecture, or risky/shared code changes."
|
|
4
4
|
model: haiku # unic-lite
|
|
5
|
+
effort: low
|
|
5
6
|
color: cyan
|
|
6
7
|
tools: ["Read", "Grep", "Glob", "Edit", "Write"]
|
|
7
8
|
---
|
|
@@ -85,7 +85,7 @@ fi
|
|
|
85
85
|
# (coordinator deadline + margin); a forced kill lands on the existing crash-streak
|
|
86
86
|
# path (non-zero STATUS) so the outcome is announced, never silently released.
|
|
87
87
|
__ukit_coord_out="$(mktemp "${TMPDIR:-/tmp}/ukit-coord-out.XXXXXX")"
|
|
88
|
-
printf '%s' "$INPUT" | UKIT_HARNESS=claude-code UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT" --evaluate-stop > "$__ukit_coord_out" 2>/dev/null &
|
|
88
|
+
printf '%s' "$INPUT" | UKIT_HARNESS="${UKIT_HARNESS:-claude-code}" UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT" --evaluate-stop > "$__ukit_coord_out" 2>/dev/null &
|
|
89
89
|
__ukit_coord_pid=$!
|
|
90
90
|
__ukit_coord_ms="${UKIT_HOOK_DEADLINE_MS:-3000}"
|
|
91
91
|
case "$__ukit_coord_ms" in ''|*[!0-9]*) __ukit_coord_ms=3000 ;; esac
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// delegation-gate.mjs — PreToolUse chain step (in-proc `runHook(ctx)`, Claude Code).
|
|
2
|
+
//
|
|
3
|
+
// Turns the feature-implementation Playbook's "delegate the coding to Tier 2" requirement from
|
|
4
|
+
// advice into a host-native refusal. It fires ONLY when ALL of these hold:
|
|
5
|
+
// * the host is Claude Code (UKIT_HARNESS unset/'claude-code') — omp/codex worker identity is
|
|
6
|
+
// UNVERIFIED, so nothing is enforced there (the acceptance verdict stays UNVERIFIED);
|
|
7
|
+
// * the live route state is this task's `feature-implementation` run with
|
|
8
|
+
// `tierLane.delegationRequired === true` (Tier 3 main, Tier 2 executor);
|
|
9
|
+
// * the call is a source/test mutation (Edit/Write/MultiEdit/NotebookEdit, or a Bash command
|
|
10
|
+
// whose file write is reliably identifiable) from the MAIN thread — the host stamps
|
|
11
|
+
// `agent_id` on every subagent tool payload, so main vs worker is read from the host, never
|
|
12
|
+
// from model text.
|
|
13
|
+
// A main-thread mutation is refused until this task's ledger holds an evidenced native worker
|
|
14
|
+
// handback (the "valid delegation path"). Reads, test runs, docs/bookkeeping writes and every
|
|
15
|
+
// unrelated operation pass untouched.
|
|
16
|
+
//
|
|
17
|
+
// Bounded, never a bypass: each refused attempt is a task-owned `delegation-violation` receipt;
|
|
18
|
+
// the DELEGATION_VIOLATION_LIMIT-th one mints a visible sticky `delegation-required` blocker, so
|
|
19
|
+
// the Stop gate ends the run as BLOCKED. Further attempts stay refused. There is no
|
|
20
|
+
// "second attempt succeeds" path.
|
|
21
|
+
//
|
|
22
|
+
// Worker side: a worker's Bash heredoc/redirect/tee/sed -i/script write has no trustworthy file
|
|
23
|
+
// attribution (the ledger would only see an anonymous workspace delta), so it is refused with a
|
|
24
|
+
// pointer to the supported Edit/Write path.
|
|
25
|
+
//
|
|
26
|
+
// Boundary (documented, not hidden): Bash mutations are detected by a quote-aware scan of the
|
|
27
|
+
// command text. Writes made by a script FILE (`node build.js`), by variables/command
|
|
28
|
+
// substitution, or after a `cd` inside the command are not identifiable here. The Stop-time
|
|
29
|
+
// acceptance backstop still counts any anonymous workspace delta as an inline write
|
|
30
|
+
// (`delegation-required-not-performed`), so such work is never reported as PASS.
|
|
31
|
+
//
|
|
32
|
+
// Fail-open on any internal error (a broken advisory gate must not wedge the session). User
|
|
33
|
+
// overrides: `UKIT_DELEGATION_GATE=off` or `.ukit/storage/config.json` orchestration.delegationGate=false.
|
|
34
|
+
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
export const hookFailClosed = false;
|
|
39
|
+
|
|
40
|
+
const FEATURE_PLAYBOOK_ID = 'feature-implementation';
|
|
41
|
+
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
42
|
+
const PASS = { code: 0, stdout: '', stderr: '' };
|
|
43
|
+
|
|
44
|
+
// First path segment under which a write is never "source/test" (tooling, build output, docs, scratch).
|
|
45
|
+
const EXEMPT_ROOTS = new Set([
|
|
46
|
+
'.git', '.claude', '.ukit', '.omp', '.codex', '.cache', 'node_modules', 'dist', 'build', 'coverage',
|
|
47
|
+
'.next', '.nuxt', '.turbo', 'tmp', 'docs',
|
|
48
|
+
]);
|
|
49
|
+
const EXEMPT_EXT = /\.(md|mdx|txt|log|tmp|lock)$/i;
|
|
50
|
+
|
|
51
|
+
const INTERPRETERS = /^(node|nodejs|deno|bun|python|python2|python3|ruby|perl|php)$/;
|
|
52
|
+
const WRAPPERS = new Set(['sudo', 'env', 'command', 'time', 'nice', 'nohup', 'exec', 'builtin']);
|
|
53
|
+
const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash']);
|
|
54
|
+
// File-writing APIs of the interpreters above. `process.stdout.write` / `sys.stdout.write` are not file writes.
|
|
55
|
+
const WRITE_API = /\b(writeFile(?:Sync)?|appendFile(?:Sync)?|createWriteStream|copyFile(?:Sync)?|rename(?:Sync)?|writeFileSync|write_text|write_bytes|File\.write|file_put_contents)\b|\bopen\s*\([^)]*,\s*['"][wax][+b]*['"]/;
|
|
56
|
+
|
|
57
|
+
// ─── Path classification ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** True when `raw` names a project source/test file (i.e. a path this gate protects). */
|
|
60
|
+
export function isSourceTarget(raw, { projectRoot, cwd } = {}) {
|
|
61
|
+
let text = String(raw ?? '').trim();
|
|
62
|
+
if (!text) return false;
|
|
63
|
+
if (/[$`]/.test(text) || text.startsWith('~')) return false; // not statically resolvable
|
|
64
|
+
if (text.startsWith('/dev/')) return false;
|
|
65
|
+
const root = path.resolve(projectRoot || '.');
|
|
66
|
+
const abs = path.isAbsolute(text) ? path.resolve(text) : path.resolve(cwd || root, text);
|
|
67
|
+
const rel = path.relative(root, abs);
|
|
68
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false; // outside the project
|
|
69
|
+
const parts = rel.split(path.sep);
|
|
70
|
+
if (EXEMPT_ROOTS.has(parts[0])) return false;
|
|
71
|
+
if (EXEMPT_EXT.test(parts[parts.length - 1])) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Bash scan ──────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
// Splits `command` into heredoc-free text plus the heredoc bodies (bodies may contain `>` etc.).
|
|
78
|
+
function extractHeredocs(command) {
|
|
79
|
+
const lines = String(command).split('\n');
|
|
80
|
+
const kept = [];
|
|
81
|
+
const bodies = [];
|
|
82
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
kept.push(line);
|
|
85
|
+
const re = /<<(-?)\s*(['"]?)([A-Za-z_][\w-]*)\2/g;
|
|
86
|
+
const delimiters = [];
|
|
87
|
+
for (let m = re.exec(line); m; m = re.exec(line)) {
|
|
88
|
+
if (line[m.index - 1] === '<') continue; // here-string `<<<`
|
|
89
|
+
delimiters.push({ strip: m[1] === '-', tag: m[3] });
|
|
90
|
+
}
|
|
91
|
+
for (const { strip, tag } of delimiters) {
|
|
92
|
+
const body = [];
|
|
93
|
+
let closed = false;
|
|
94
|
+
while (i + 1 < lines.length) {
|
|
95
|
+
i += 1;
|
|
96
|
+
const candidate = strip ? lines[i].replace(/^\t+/, '') : lines[i];
|
|
97
|
+
if (candidate.trim() === tag) { closed = true; break; }
|
|
98
|
+
body.push(lines[i]);
|
|
99
|
+
}
|
|
100
|
+
bodies.push(body.join('\n'));
|
|
101
|
+
if (!closed) break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { text: kept.join('\n'), bodies };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Quote-aware pass: returns segments (arrays of words) and file-redirect targets.
|
|
108
|
+
function tokenize(text) {
|
|
109
|
+
const segments = [[]];
|
|
110
|
+
const redirects = [];
|
|
111
|
+
let word = null;
|
|
112
|
+
const pushWord = () => { if (word !== null) { segments[segments.length - 1].push(word); word = null; } };
|
|
113
|
+
const newSegment = () => { pushWord(); if (segments[segments.length - 1].length) segments.push([]); };
|
|
114
|
+
const n = text.length;
|
|
115
|
+
let i = 0;
|
|
116
|
+
const readQuoted = (quote) => {
|
|
117
|
+
let out = '';
|
|
118
|
+
i += 1;
|
|
119
|
+
while (i < n && text[i] !== quote) {
|
|
120
|
+
if (quote === '"' && text[i] === '\\' && i + 1 < n) { out += text[i + 1]; i += 2; continue; }
|
|
121
|
+
out += text[i]; i += 1;
|
|
122
|
+
}
|
|
123
|
+
i += 1;
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
while (i < n) {
|
|
127
|
+
const ch = text[i];
|
|
128
|
+
if (ch === "'" || ch === '"') { word = (word ?? '') + readQuoted(ch); continue; }
|
|
129
|
+
if (ch === '\\' && i + 1 < n) { word = (word ?? '') + text[i + 1]; i += 2; continue; }
|
|
130
|
+
if (ch === '#' && word === null) { while (i < n && text[i] !== '\n') i += 1; continue; }
|
|
131
|
+
if (/\s/.test(ch)) { if (ch === '\n') newSegment(); else pushWord(); i += 1; continue; }
|
|
132
|
+
if (ch === ';' || ch === '|' || ch === '(' || ch === ')' || ch === '`') { newSegment(); i += 1; if (ch === '|' && text[i] === '|') i += 1; continue; }
|
|
133
|
+
if (ch === '&' && text[i + 1] === '&') { newSegment(); i += 2; continue; }
|
|
134
|
+
if (ch === '&' && text[i + 1] !== '>') { newSegment(); i += 1; continue; }
|
|
135
|
+
if (ch === '&' && text[i + 1] === '>') {
|
|
136
|
+
pushWord(); i += 2; if (text[i] === '>') i += 1;
|
|
137
|
+
const target = readRedirectTarget();
|
|
138
|
+
if (target !== null) redirects.push(target);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '>' ) {
|
|
142
|
+
// `2>file` : the fd digit already landed in `word`; drop it when it is a bare fd number.
|
|
143
|
+
if (word !== null && /^\d$/.test(word)) word = null; else pushWord();
|
|
144
|
+
i += 1;
|
|
145
|
+
if (text[i] === '(') continue; // process substitution
|
|
146
|
+
if (text[i] === '&') { i += 1; while (i < n && /[\d-]/.test(text[i])) i += 1; continue; } // fd dup
|
|
147
|
+
if (text[i] === '>' || text[i] === '|') i += 1;
|
|
148
|
+
const target = readRedirectTarget();
|
|
149
|
+
if (target !== null) redirects.push(target);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
word = (word ?? '') + ch; i += 1;
|
|
153
|
+
}
|
|
154
|
+
pushWord();
|
|
155
|
+
function readRedirectTarget() {
|
|
156
|
+
while (i < n && (text[i] === ' ' || text[i] === '\t')) i += 1;
|
|
157
|
+
if (i >= n) return null;
|
|
158
|
+
if (text[i] === "'" || text[i] === '"') return readQuoted(text[i]);
|
|
159
|
+
let out = '';
|
|
160
|
+
while (i < n && !/[\s;|&<>()]/.test(text[i])) { out += text[i]; i += 1; }
|
|
161
|
+
return out || null;
|
|
162
|
+
}
|
|
163
|
+
return { segments: segments.filter((s) => s.length), redirects };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isFlag(word) { return /^-/.test(word); }
|
|
167
|
+
|
|
168
|
+
function stripLeading(words) {
|
|
169
|
+
const out = [...words];
|
|
170
|
+
while (out.length) {
|
|
171
|
+
if (/^[A-Za-z_]\w*=/.test(out[0]) || WRAPPERS.has(out[0])) out.shift(); else break;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scriptWrites(script, ctx) {
|
|
177
|
+
if (!WRITE_API.test(script)) return null;
|
|
178
|
+
const literals = [...script.matchAll(/['"`]([^'"`\n]{1,200})['"`]/g)].map((m) => m[1])
|
|
179
|
+
.filter((value) => /[\\/]/.test(value) || /\.[A-Za-z0-9]{1,6}$/.test(value));
|
|
180
|
+
if (literals.length === 0) return { kind: 'script-write', target: null };
|
|
181
|
+
const hit = literals.find((value) => isSourceTarget(value, ctx));
|
|
182
|
+
return hit ? { kind: 'script-write', target: hit } : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the reliably identifiable source/test file mutations in a Bash command:
|
|
187
|
+
* [{ kind: 'redirect'|'heredoc'|'tee'|'sed-inplace'|'copy'|'patch'|'script-write', target }].
|
|
188
|
+
* Reads, test runs and writes to exempt paths yield []. Script FILES are not identifiable (see header).
|
|
189
|
+
*/
|
|
190
|
+
export function scanBashMutations(command, ctx = {}, depth = 0) {
|
|
191
|
+
const raw = String(command ?? '');
|
|
192
|
+
if (!raw.trim()) return [];
|
|
193
|
+
const hits = [];
|
|
194
|
+
const { text, bodies } = extractHeredocs(raw);
|
|
195
|
+
const hasHeredoc = bodies.length > 0;
|
|
196
|
+
const { segments, redirects } = tokenize(text);
|
|
197
|
+
for (const target of redirects) {
|
|
198
|
+
if (isSourceTarget(target, ctx)) hits.push({ kind: hasHeredoc ? 'heredoc' : 'redirect', target });
|
|
199
|
+
}
|
|
200
|
+
for (const rawWords of segments) {
|
|
201
|
+
const words = stripLeading(rawWords);
|
|
202
|
+
if (!words.length) continue;
|
|
203
|
+
const cmd = path.basename(words[0]);
|
|
204
|
+
const args = words.slice(1);
|
|
205
|
+
const plain = args.filter((a) => !isFlag(a));
|
|
206
|
+
if (cmd === 'tee') {
|
|
207
|
+
for (const target of plain) if (isSourceTarget(target, ctx)) hits.push({ kind: 'tee', target });
|
|
208
|
+
} else if (cmd === 'sed' || cmd === 'perl' || cmd === 'ruby') {
|
|
209
|
+
const inPlace = args.some((a) => (cmd === 'sed' ? /^-[A-Za-z]*i|^--in-place/ : /^-[A-Za-z]*i/).test(a));
|
|
210
|
+
if (inPlace) {
|
|
211
|
+
const eIndex = args.findIndex((a) => a === '-e' || a === '-f');
|
|
212
|
+
const files = args.filter((a, idx) => !isFlag(a) && !(eIndex >= 0 && idx === eIndex + 1));
|
|
213
|
+
const candidates = cmd === 'sed' && eIndex < 0 ? files.slice(1) : files;
|
|
214
|
+
for (const target of candidates) if (isSourceTarget(target, ctx)) hits.push({ kind: 'sed-inplace', target });
|
|
215
|
+
}
|
|
216
|
+
} else if (cmd === 'cp' || cmd === 'mv' || cmd === 'install') {
|
|
217
|
+
if (plain.length >= 2 && isSourceTarget(plain[plain.length - 1], ctx)) hits.push({ kind: 'copy', target: plain[plain.length - 1] });
|
|
218
|
+
} else if (cmd === 'patch' || (cmd === 'git' && /^(apply|am)$/.test(plain[0] || '') && !args.includes('--check') && !args.includes('--stat'))) {
|
|
219
|
+
hits.push({ kind: 'patch', target: null });
|
|
220
|
+
} else if (cmd === 'dd') {
|
|
221
|
+
const of = args.find((a) => a.startsWith('of='));
|
|
222
|
+
if (of && isSourceTarget(of.slice(3), ctx)) hits.push({ kind: 'copy', target: of.slice(3) });
|
|
223
|
+
} else if (INTERPRETERS.test(cmd)) {
|
|
224
|
+
const inline = [];
|
|
225
|
+
args.forEach((a, idx) => { if (/^-(e|c|p)$/.test(a) && args[idx + 1] !== undefined) inline.push(args[idx + 1]); });
|
|
226
|
+
const scripts = [...inline, ...(hasHeredoc && (plain.length === 0 || args.includes('-') || args.some((a) => /^-[A-Za-z]*$/.test(a))) ? bodies : [])];
|
|
227
|
+
for (const script of scripts) {
|
|
228
|
+
const found = scriptWrites(script, ctx);
|
|
229
|
+
if (found) hits.push(found);
|
|
230
|
+
}
|
|
231
|
+
} else if (SHELLS.has(cmd) && depth < 2) {
|
|
232
|
+
const cIndex = args.indexOf('-c');
|
|
233
|
+
if (cIndex >= 0 && args[cIndex + 1] !== undefined) hits.push(...scanBashMutations(args[cIndex + 1], ctx, depth + 1));
|
|
234
|
+
} else if (cmd === 'eval' && depth < 2) {
|
|
235
|
+
hits.push(...scanBashMutations(args.join(' '), ctx, depth + 1));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return hits;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Gate ───────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function readJson(file) {
|
|
244
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function userDisabled(projectRoot, env) {
|
|
248
|
+
if (/^(off|0|false)$/i.test(String(env.UKIT_DELEGATION_GATE || ''))) return true;
|
|
249
|
+
const config = readJson(path.join(projectRoot, '.ukit', 'storage', 'config.json'));
|
|
250
|
+
return config?.orchestration?.delegationGate === false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function deny(reason, env) {
|
|
254
|
+
const inChain = Boolean(env.UKIT_HOOK_CHAIN_RUNNER);
|
|
255
|
+
return {
|
|
256
|
+
code: inChain ? 2 : 0,
|
|
257
|
+
stdout: `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } })}\n`,
|
|
258
|
+
stderr: `BLOCKED: ${reason}\n`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function describeTarget(mutation, projectRoot) {
|
|
263
|
+
if (!mutation.target) return 'a source file';
|
|
264
|
+
const text = String(mutation.target);
|
|
265
|
+
const rel = path.isAbsolute(text) && projectRoot ? path.relative(projectRoot, text) : text;
|
|
266
|
+
return `"${(rel && !rel.startsWith('..') ? rel : text).slice(0, 120)}"`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function mainMessage(attempt, limit, taskId, mutation, projectRoot) {
|
|
270
|
+
if (attempt >= limit) {
|
|
271
|
+
return `UKit delegation gate — BLOCKED: this task's feature-implementation Playbook requires the coding to be delegated to the Tier 2 worker, but the orchestrator tried ${attempt} times to write ${describeTarget(mutation, projectRoot)} itself. This run is now recorded as BLOCKED (delegation-required-not-performed). Do not retry the write; report the blocker to the user.`;
|
|
272
|
+
}
|
|
273
|
+
return `UKit delegation gate (attempt ${attempt} of ${limit}): this task's feature-implementation Playbook requires Tier 2 coding delegation, and you are the Tier 3 orchestrator — writing ${describeTarget(mutation, projectRoot)} yourself is refused. Dispatch a native worker now with the Agent tool (subagent_type "feature-implementer", the Tier 2 coding model) and start its prompt with "[ukit-task ${taskId}]", naming the files and behaviour to implement; the worker must edit with Edit/Write. When it returns, verify the integrated result yourself with the bare test runner (for example \`npm test\`) — no pipe, tail, grep or compound command — so the runner's own exit status is the evidence. Repeating this write will end the run as BLOCKED.`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function workerMessage(mutation, projectRoot) {
|
|
277
|
+
return `UKit delegation gate: a Bash ${mutation.kind} write to ${describeTarget(mutation, projectRoot)} has no trustworthy file attribution, so it is not accepted as worker evidence. Redo this change with the Edit or Write tool (the supported worker edit path), then run the tests with the bare runner command.`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function targetsOf(toolName, input) {
|
|
281
|
+
if (toolName === 'Bash') return null;
|
|
282
|
+
const file = input.file_path ?? input.notebook_path ?? input.path;
|
|
283
|
+
return typeof file === 'string' && file ? file : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function evaluate(ctx) {
|
|
287
|
+
const env = ctx.env || {};
|
|
288
|
+
const payload = ctx.payload || {};
|
|
289
|
+
const toolName = payload.tool_name;
|
|
290
|
+
if (toolName !== 'Bash' && !MUTATING_TOOLS.has(toolName)) return PASS;
|
|
291
|
+
if (env.UKIT_HARNESS && env.UKIT_HARNESS !== 'claude-code') return PASS;
|
|
292
|
+
const projectRoot = env.CLAUDE_PROJECT_DIR || ctx.projectRoot || process.cwd();
|
|
293
|
+
if (userDisabled(projectRoot, env)) return PASS;
|
|
294
|
+
|
|
295
|
+
const ledgerMod = await import(new URL('../ukit/runtime/execution-ledger.mjs', import.meta.url).href);
|
|
296
|
+
const playbook = await import(new URL('../ukit/runtime/playbook-execution.mjs', import.meta.url).href);
|
|
297
|
+
const state = await ledgerMod.readRouteState(projectRoot, payload);
|
|
298
|
+
const summary = state?.routeSummary;
|
|
299
|
+
if (!state?.taskInstanceId || summary?.playbookId !== FEATURE_PLAYBOOK_ID || summary.tierLane?.delegationRequired !== true) return PASS;
|
|
300
|
+
|
|
301
|
+
const input = payload.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
|
|
302
|
+
const scanCtx = { projectRoot, cwd: payload.cwd || projectRoot };
|
|
303
|
+
let mutation = null;
|
|
304
|
+
if (toolName === 'Bash') {
|
|
305
|
+
mutation = scanBashMutations(input.command, scanCtx)[0] || null;
|
|
306
|
+
} else {
|
|
307
|
+
const file = targetsOf(toolName, input);
|
|
308
|
+
if (file && isSourceTarget(file, scanCtx)) mutation = { kind: 'edit', target: file };
|
|
309
|
+
}
|
|
310
|
+
if (!mutation) return PASS;
|
|
311
|
+
|
|
312
|
+
if (payload.agent_id) {
|
|
313
|
+
// A worker: Edit/Write is the supported path; only anonymous Bash writes are refused.
|
|
314
|
+
return toolName === 'Bash' ? deny(workerMessage(mutation, projectRoot), env) : PASS;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ledger = await ledgerMod.readExecutionLedger(projectRoot, payload);
|
|
318
|
+
const owned = ledger && ledger.taskInstanceId === state.taskInstanceId ? ledger : null;
|
|
319
|
+
const workers = playbook.assessWorkers(owned?.playbookRun || null);
|
|
320
|
+
if (workers.workers.some((worker) => worker.state === 'evidenced')) return PASS; // valid delegation path exists
|
|
321
|
+
|
|
322
|
+
const limit = playbook.DELEGATION_VIOLATION_LIMIT;
|
|
323
|
+
const prior = Number(owned?.delegationViolations?.count) || 0;
|
|
324
|
+
const attempt = Math.min(prior + 1, limit);
|
|
325
|
+
if (prior < limit) {
|
|
326
|
+
await ledgerMod.recordLedgerEvent({
|
|
327
|
+
type: 'receipt',
|
|
328
|
+
receipt: {
|
|
329
|
+
ts: Date.now(), kind: 'delegation-violation', toolName, toolUseId: payload.tool_use_id || null,
|
|
330
|
+
success: false, mutation: mutation.kind, target: mutation.target ? String(mutation.target).slice(0, 160) : null,
|
|
331
|
+
},
|
|
332
|
+
}, { projectRoot, payload, harness: 'claude-code', routeState: state, deadlineMs: ctx.deadlineMs });
|
|
333
|
+
}
|
|
334
|
+
return deny(mainMessage(attempt, limit, state.taskInstanceId, mutation, projectRoot), env);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function runHook(ctx) {
|
|
338
|
+
try {
|
|
339
|
+
return await evaluate(ctx || {});
|
|
340
|
+
} catch {
|
|
341
|
+
return PASS; // advisory gate: an internal fault never wedges the session
|
|
342
|
+
}
|
|
343
|
+
}
|