@ngockhoale/ukit 2.1.5 → 2.2.1
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 +31 -0
- package/README.md +7 -4
- package/manifests/platform.full.yaml +113 -24
- package/package.json +4 -3
- package/src/cli/adapters.js +47 -21
- package/src/cli/index.js +2 -2
- package/src/core/applyPlan.js +5 -2
- package/src/core/ensureGitignore.js +2 -0
- package/src/core/runInstallPipeline.js +19 -0
- package/src/core/runtimeConfig.js +6 -1
- package/src/core/status.js +3 -1
- package/src/core/uninstall.js +16 -0
- package/src/index/routeCatalog.js +1 -1
- package/src/manifest/selectItems.js +11 -5
- package/templates/.claude/commands/ukit/handoff-create.md +6 -4
- package/templates/.claude/commands/ukit/handoff-fullstack.md +12 -10
- package/templates/.claude/commands/ukit/handoff-implement.md +4 -2
- package/templates/.claude/commands/ukit/handoff-review.md +4 -2
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.claude/ukit/index/unic-gateway.mjs +46 -7
- package/templates/.codex/README.md +1 -1
- package/templates/.gitignore +2 -0
- package/templates/.omp/AGENTS.md +9 -0
- package/templates/.omp/README.md +96 -0
- package/templates/.omp/RULES.md +34 -0
- package/templates/.omp/agents/bug-debugger.md +85 -0
- package/templates/.omp/agents/code-reviewer.md +197 -0
- package/templates/.omp/agents/feature-implementer.md +123 -0
- package/templates/.omp/agents/handoff-planner.md +210 -0
- package/templates/.omp/agents/ukit-small-task-maintainer.md +72 -0
- package/templates/.omp/agents/ukit-vision-analyst.md +100 -0
- package/templates/.omp/config.yml +88 -0
- package/templates/.omp/hooks/pre/ukit-bridge.js +336 -0
- package/templates/AGENTS.md +2 -2
- package/templates/docs/PROJECT.md +1 -1
- package/templates/ukit/storage/config.json +10 -0
- package/templates/adapter-presets/antigravity/README.md +0 -22
- package/templates/adapter-presets/antigravity/rules.md +0 -49
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ukit-vision-analyst
|
|
3
|
+
description: "The only lane permitted to interpret images in this repo. Use whenever a prompt references, attaches, or points at an image (screenshot, mockup, diagram, photo of an error) and the caller needs to know what is actually in it. unic-code/unic-smart cannot read images on this gateway and must never guess at their contents — route image analysis here instead. Reports findings only; never writes product code."
|
|
4
|
+
model: "@vision" # real gateway model name, NOT an alias. unic-code/unic-smart cannot read images on this gateway — do NOT "fix" this to sonnet/opus.
|
|
5
|
+
tools: ["read","glob","bash"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are UKit's vision analyst. Your only job is to look at real image files and report what is
|
|
9
|
+
actually in them. You never write, edit, or refactor product code — you analyse and report.
|
|
10
|
+
|
|
11
|
+
## 1. Role
|
|
12
|
+
|
|
13
|
+
- Analyse images (screenshots, mockups, diagrams, photos of errors) and report their content
|
|
14
|
+
faithfully.
|
|
15
|
+
- Never write product code. `Edit`/`Write` are deliberately absent from your `tools`; you report
|
|
16
|
+
via `Bash` (to write your receipt), you never implement.
|
|
17
|
+
- Stay end-user-invisible: this lane is internal UKit orchestration, not something end users invoke
|
|
18
|
+
by name.
|
|
19
|
+
|
|
20
|
+
## 2. Model self-check — first action, before touching any image
|
|
21
|
+
|
|
22
|
+
Before reading any image, determine the model you are actually running on right now.
|
|
23
|
+
|
|
24
|
+
- Vision-capable means: `unic-vision`, or — when `unicMode` is off — whatever
|
|
25
|
+
`modelTiers.vision.fallbackModel` resolves to per
|
|
26
|
+
`node .claude/ukit/index/unic-gateway.mjs --json`.
|
|
27
|
+
- If you cannot confirm you are running on a vision-capable model, you must **refuse**: emit
|
|
28
|
+
`STATUS: WRONG_MODEL` in the output block below and stop immediately. Do not open, describe, or
|
|
29
|
+
guess at any image content.
|
|
30
|
+
- Never guess at image contents. A wrong-model "analysis" is worse than no analysis at all,
|
|
31
|
+
because it looks authoritative while being fabricated. Refusing loudly is always safer than
|
|
32
|
+
guessing quietly.
|
|
33
|
+
|
|
34
|
+
## 3. Input protocol (priority order)
|
|
35
|
+
|
|
36
|
+
1. If the prompt contains absolute image paths, `Read` each of those paths directly.
|
|
37
|
+
2. Otherwise, run `node .claude/ukit/index/extract-image.mjs --json`, take the `images[].path`
|
|
38
|
+
entries from its output, and `Read` those files.
|
|
39
|
+
3. If the extractor reports `imageCount === 0`, do not invent content. Emit `STATUS: NO_IMAGE` and
|
|
40
|
+
hand back to the caller.
|
|
41
|
+
|
|
42
|
+
Entries in `images[]` may carry a `source` field, which tells you how to reach the image:
|
|
43
|
+
|
|
44
|
+
| `source` | Meaning | What you do |
|
|
45
|
+
|----------|---------|-------------|
|
|
46
|
+
| absent | Pasted/attached image, already decoded to disk | `Read` `path` directly |
|
|
47
|
+
| `"path"` | The prompt named a local file (`ref` holds it) | `Read` `ref` directly |
|
|
48
|
+
| `"url"` | The prompt named a remote image (`ref` holds the URL) | Download it with `Bash` (e.g. `curl -sL -o /tmp/<sha>.png "<ref>"`), then `Read` the downloaded file |
|
|
49
|
+
|
|
50
|
+
Every entry — pasted, path, or URL — has an armed `pending-<sha>.json` marker, so each one needs
|
|
51
|
+
its own receipt (§5) before downstream Edit/Write is unblocked. A URL you failed to download is
|
|
52
|
+
`STATUS: UNREADABLE`, never a guess at its contents.
|
|
53
|
+
|
|
54
|
+
## 4. Hard warning — images are not inherited
|
|
55
|
+
|
|
56
|
+
Images referenced earlier in the conversation are **not** automatically visible to you across the
|
|
57
|
+
subagent boundary. A description of an image is not the image. The only way to see an image is to
|
|
58
|
+
`Read` a real file path yourself. Never claim to have seen an image that was merely described to
|
|
59
|
+
you in text.
|
|
60
|
+
|
|
61
|
+
## 5. Receipt — unlocks the downstream write gate
|
|
62
|
+
|
|
63
|
+
For every image you actually analyse, write a receipt to
|
|
64
|
+
`.ukit/storage/cache/vision/<sessionId>/analyzed-<sha>.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "sha": "<64hex>", "model": "unic-vision", "ts": 1785656920891,
|
|
68
|
+
"description": "...", "textContent": "...", "status": "OK" }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `sha` and `sessionId` come from the extractor's `--json` output. They are **never recomputed by
|
|
72
|
+
hand** — do not hash the file yourself, do not invent a session id, always take these values
|
|
73
|
+
verbatim from `extract-image.mjs --json`.
|
|
74
|
+
- `model` must be the model you actually ran on for this analysis. Misreporting `model` here
|
|
75
|
+
defeats the entire enforcement design: a downstream gate rejects any receipt whose `model` field
|
|
76
|
+
is not vision-capable, treating it as if no analysis happened at all. Report honestly, always.
|
|
77
|
+
- Use `Bash` to write the receipt file.
|
|
78
|
+
|
|
79
|
+
## 6. Output block
|
|
80
|
+
|
|
81
|
+
Always end with:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
STATUS: OK | NO_IMAGE | UNREADABLE | WRONG_MODEL
|
|
85
|
+
MODEL: <actual model>
|
|
86
|
+
IMAGES: <n> (<paths>)
|
|
87
|
+
DESCRIPTION: [what is actually visible]
|
|
88
|
+
TEXT_CONTENT: [verbatim text/code/errors legible in the image, or "none"]
|
|
89
|
+
RELEVANT_TO_TASK: [how it answers the caller's question]
|
|
90
|
+
UNCERTAIN: [anything ambiguous or illegible, or "none"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 7. Guardrails
|
|
94
|
+
|
|
95
|
+
- Transcribe error text and code verbatim rather than paraphrasing it.
|
|
96
|
+
- State uncertainty explicitly instead of guessing — use `UNCERTAIN:` for anything ambiguous or
|
|
97
|
+
illegible.
|
|
98
|
+
- Do not read unrelated repo files; stay scoped to the image(s) and the immediate task question.
|
|
99
|
+
- Stay end-user-invisible: end users should never need to know this agent's name or invoke it
|
|
100
|
+
directly.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# UNIC gateway model names are LITERAL model names, not aliases, and they ship as the
|
|
2
|
+
# installed default. Do NOT substitute sonnet/opus/haiku here. See PLAN.md §3 D15.
|
|
3
|
+
#
|
|
4
|
+
# Non-UNIC omp provider? A maintainer edits the three cost tiers to the values in
|
|
5
|
+
# orchestration.modelTiers[*].claudeModel (.ukit/storage/config.json) — currently
|
|
6
|
+
# lite: claude-haiku-4-5, code: claude-sonnet-5, smart: claude-opus-5.
|
|
7
|
+
# `vision` STAYS unic-vision: its claudeModel is already unic-vision, because
|
|
8
|
+
# unic-code/unic-smart cannot read images on this gateway. claude-sonnet-5 is only
|
|
9
|
+
# modelTiers.vision.fallbackModel, used when unicMode is off — never a drop-in here.
|
|
10
|
+
modelRoles:
|
|
11
|
+
lite: unic-lite
|
|
12
|
+
code: unic-code
|
|
13
|
+
smart: unic-smart
|
|
14
|
+
vision: unic-vision
|
|
15
|
+
|
|
16
|
+
tools:
|
|
17
|
+
approvalMode: write
|
|
18
|
+
approval:
|
|
19
|
+
bash: allow
|
|
20
|
+
eval: prompt
|
|
21
|
+
|
|
22
|
+
bash:
|
|
23
|
+
# Translated from templates/.claude/hooks/block-dangerous.sh's DANGEROUS_PATTERNS array.
|
|
24
|
+
# `deny`/`prompt` fire on the WHOLE command or on ANY compound segment (e.g. `foo && rm -rf /`
|
|
25
|
+
# still hits the `rm -rf /*` entry below) — so a `deny` entry below the trailing allow still
|
|
26
|
+
# catches it. `allow` only ever matches an ENTIRE, non-compound command, so the trailing "*"
|
|
27
|
+
# allow is NOT a universal escape hatch: any compound command (`&&`, `;`, `|`) not itself caught
|
|
28
|
+
# by a `deny`/`prompt` entry falls through to `tools.approvalMode` instead of being auto-allowed.
|
|
29
|
+
# `bash.patterns` does not cover the `eval` tool at all — that is why `tools.approval.eval` above
|
|
30
|
+
# is `prompt`, and why TASK-004's bridge separately maps `eval` -> `Bash`.
|
|
31
|
+
patterns:
|
|
32
|
+
- match: "rm -rf /*"
|
|
33
|
+
approval: deny
|
|
34
|
+
- match: "rm -rf ~*"
|
|
35
|
+
approval: deny
|
|
36
|
+
- match: "rm -rf ."
|
|
37
|
+
approval: deny
|
|
38
|
+
- match: "rm -rf .."
|
|
39
|
+
approval: deny
|
|
40
|
+
- match: "git push --force*"
|
|
41
|
+
approval: deny
|
|
42
|
+
- match: "git push -f *"
|
|
43
|
+
approval: deny
|
|
44
|
+
- match: "git reset --hard*"
|
|
45
|
+
approval: deny
|
|
46
|
+
- match: "git clean -fd*"
|
|
47
|
+
approval: deny
|
|
48
|
+
- match: "git checkout .*"
|
|
49
|
+
approval: deny
|
|
50
|
+
- match: "git restore .*"
|
|
51
|
+
approval: deny
|
|
52
|
+
- match: "*> /dev/sda*"
|
|
53
|
+
approval: deny
|
|
54
|
+
- match: "mkfs.*"
|
|
55
|
+
approval: deny
|
|
56
|
+
- match: ":(){ :|:& };:*"
|
|
57
|
+
approval: deny
|
|
58
|
+
- match: "dd if=/dev/*"
|
|
59
|
+
approval: deny
|
|
60
|
+
# block-dangerous.sh also carries a safe-cleanup allowlist for recursive force-deletes
|
|
61
|
+
# (dist/build/coverage/.next/.nuxt/.turbo/tmp/temp/.cache/node_modules) and blocks every other
|
|
62
|
+
# `rm -rf`. bash.patterns has no conditional/allowlist syntax to express that distinction, so
|
|
63
|
+
# this layer is deliberately coarser: it denies ALL remaining `rm -rf` rather than approximating
|
|
64
|
+
# the allowlist. The real script still runs via TASK-004's bridge and is the authority here —
|
|
65
|
+
# bash.patterns is defence in depth, not the only line.
|
|
66
|
+
- match: "rm -rf *"
|
|
67
|
+
approval: deny
|
|
68
|
+
- match: "*"
|
|
69
|
+
approval: allow
|
|
70
|
+
|
|
71
|
+
# UKit owns memory; omp's memory subsystem stays off. See PLAN.md §3 D12 —
|
|
72
|
+
# enabling it would inject a second, independent memory stream into the same
|
|
73
|
+
# context window that auto-compact exists to protect. Do not "fix" this to local.
|
|
74
|
+
# UKit's own memory already lives at .ukit/storage/memory/ and is reinjected by
|
|
75
|
+
# reinject-context.sh; there is no shared eviction policy between the two systems.
|
|
76
|
+
memory:
|
|
77
|
+
backend: off
|
|
78
|
+
|
|
79
|
+
# Auto-compact must trigger BEFORE context-hardcap-gate.sh starts blocking tool
|
|
80
|
+
# calls (compact.hardCapTokens, default 220000). Mirrors the Claude Code value
|
|
81
|
+
# env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = 180000 in templates/.claude/settings.json.
|
|
82
|
+
# TODO(verify): the omp key name below is UNVERIFIED — omp is not installed on the
|
|
83
|
+
# planning machine and no omp schema is vendored in this repo. If it is wrong, omp
|
|
84
|
+
# ignores the unknown key and the session simply compacts later, with
|
|
85
|
+
# context-hardcap-gate.sh + hardCapGraceCalls still acting as the backstop.
|
|
86
|
+
# Degrades to "later, harder compact", never to "no gate". See PLAN.md §3 D13.
|
|
87
|
+
compact:
|
|
88
|
+
autoCompactWindow: 180000 # TODO(verify) key name against a real omp install
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// ukit-bridge.js — omp hook bridge for UKit.
|
|
2
|
+
//
|
|
3
|
+
// Bridge, don't fork (PLAN.md D1): the 18 `.sh` scripts under
|
|
4
|
+
// `.claude/hooks/` are the single source of truth for hook behaviour across
|
|
5
|
+
// both Claude Code and omp. This module never re-implements their logic in
|
|
6
|
+
// JS. It only:
|
|
7
|
+
// 1. Maps an omp event + tool name onto the ordered list of scripts that
|
|
8
|
+
// `.claude/settings.json` would have run for the equivalent Claude Code
|
|
9
|
+
// event (see HOOK_EVENT_MAP, generated by hand from
|
|
10
|
+
// `templates/.claude/settings.json` — keep them in sync).
|
|
11
|
+
// 2. Builds the same stdin JSON payload the scripts already expect
|
|
12
|
+
// (hook_event_name, tool_name, tool_input, session_id, cwd, ...).
|
|
13
|
+
// 3. Executes each script via `pi.exec()` (never spawns/copies the script
|
|
14
|
+
// body) and translates the exit code back into an omp-shaped result.
|
|
15
|
+
//
|
|
16
|
+
// omp's own `pi.exec` / `pi.on` argument shapes are UNVERIFIED (omp is not
|
|
17
|
+
// installed in this environment, no source/schema vendored — PLAN.md D7).
|
|
18
|
+
// The internal contract chosen below (see doc comments on each exported
|
|
19
|
+
// function) is a reasonable, internally consistent design; it is exercised
|
|
20
|
+
// end-to-end against a fake `pi` in tests/hooks/ompHookBridge.test.js.
|
|
21
|
+
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Event -> script mapping (hand-derived from templates/.claude/settings.json;
|
|
26
|
+
// keep this literally in sync with that file — case 1 in
|
|
27
|
+
// tests/hooks/ompHookBridge.test.js re-parses settings.json and asserts
|
|
28
|
+
// exact equality against this table).
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export const HOOK_EVENT_MAP = {
|
|
32
|
+
tool_call: {
|
|
33
|
+
'Read|Grep|Glob': ['skill-router.sh'],
|
|
34
|
+
'Edit|Write': [
|
|
35
|
+
'protect-files.sh',
|
|
36
|
+
'stale-spec-guard.sh',
|
|
37
|
+
'pre-edit-backup.sh',
|
|
38
|
+
'skill-router.sh',
|
|
39
|
+
'handoff-model-guard.sh',
|
|
40
|
+
'vision-gate.sh',
|
|
41
|
+
'context-hardcap-gate.sh',
|
|
42
|
+
],
|
|
43
|
+
Bash: [
|
|
44
|
+
'verification-guard.sh',
|
|
45
|
+
'auto-allow-bash.sh',
|
|
46
|
+
'skill-router.sh',
|
|
47
|
+
'block-dangerous.sh',
|
|
48
|
+
'handoff-model-guard.sh',
|
|
49
|
+
'context-hardcap-gate.sh',
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
tool_result: {
|
|
53
|
+
'Edit|Write': ['post-edit-verify.sh'],
|
|
54
|
+
Bash: ['compress-output.sh'],
|
|
55
|
+
},
|
|
56
|
+
turn_start: ['skill-router.sh', 'vision-router.sh', 'context-window-guard.sh'],
|
|
57
|
+
'session.compacting': ['reinject-context.sh'],
|
|
58
|
+
session_start: ['auto-prune-bash.sh', 'reset-compact-pressure.sh', 'handoff-resume.sh'],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Tool-name mapping (PLAN.md D2).
|
|
63
|
+
//
|
|
64
|
+
// omp's write surface is `edit`, `write`, AND `ast_edit` — `ast_edit` must
|
|
65
|
+
// map to the same `Edit` matcher group as `edit`/`write`, or it silently
|
|
66
|
+
// bypasses protect-files.sh / vision-gate.sh. `eval` is omp's shell-capable
|
|
67
|
+
// tool and must map to `Bash` to hit block-dangerous.sh / verification-guard.sh.
|
|
68
|
+
// Any tool name not in this table maps to `null`, which runs ZERO scripts —
|
|
69
|
+
// it must never silently fall back to Bash or Edit.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const TOOL_NAME_MAP = {
|
|
73
|
+
read: 'Read',
|
|
74
|
+
grep: 'Grep',
|
|
75
|
+
glob: 'Glob',
|
|
76
|
+
edit: 'Edit',
|
|
77
|
+
write: 'Write',
|
|
78
|
+
ast_edit: 'Edit',
|
|
79
|
+
eval: 'Bash',
|
|
80
|
+
bash: 'Bash',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function mapToolName(ompToolName) {
|
|
84
|
+
return TOOL_NAME_MAP[ompToolName] ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function matcherGroupFor(claudeToolName) {
|
|
88
|
+
if (claudeToolName === 'Read' || claudeToolName === 'Grep' || claudeToolName === 'Glob') {
|
|
89
|
+
return 'Read|Grep|Glob';
|
|
90
|
+
}
|
|
91
|
+
if (claudeToolName === 'Edit' || claudeToolName === 'Write') {
|
|
92
|
+
return 'Edit|Write';
|
|
93
|
+
}
|
|
94
|
+
if (claudeToolName === 'Bash') {
|
|
95
|
+
return 'Bash';
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Fail-direction classification (PLAN.md D3). Not a blanket rule -- each
|
|
102
|
+
// script's own header documents its own fail direction; this table is a
|
|
103
|
+
// transcription of those 18 headers, not an invented policy. Gate scripts
|
|
104
|
+
// fail CLOSED (non-zero/throw => block). Advisory scripts fail OPEN
|
|
105
|
+
// (non-zero/throw => log a warning, never block).
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
109
|
+
'protect-files.sh',
|
|
110
|
+
'stale-spec-guard.sh',
|
|
111
|
+
'handoff-model-guard.sh',
|
|
112
|
+
'vision-gate.sh',
|
|
113
|
+
'context-hardcap-gate.sh',
|
|
114
|
+
'block-dangerous.sh',
|
|
115
|
+
'verification-guard.sh',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
export const ADVISORY_SCRIPTS = new Set([
|
|
119
|
+
'skill-router.sh',
|
|
120
|
+
'auto-allow-bash.sh',
|
|
121
|
+
'pre-edit-backup.sh',
|
|
122
|
+
'vision-router.sh',
|
|
123
|
+
'context-window-guard.sh',
|
|
124
|
+
'post-edit-verify.sh',
|
|
125
|
+
'compress-output.sh',
|
|
126
|
+
'reinject-context.sh',
|
|
127
|
+
'auto-prune-bash.sh',
|
|
128
|
+
'reset-compact-pressure.sh',
|
|
129
|
+
'handoff-resume.sh',
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
function classifyFailure(scriptName) {
|
|
133
|
+
if (FAIL_CLOSED_SCRIPTS.has(scriptName)) return 'closed';
|
|
134
|
+
if (ADVISORY_SCRIPTS.has(scriptName)) return 'open';
|
|
135
|
+
// Unclassified script (should not happen for the 18 known scripts): fail
|
|
136
|
+
// open by default rather than blocking on an unknown quantity.
|
|
137
|
+
return 'open';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Payload + result translation.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds the same stdin JSON shape the `.sh` scripts already read via
|
|
146
|
+
* `INPUT=$(cat)`. Fields the caller does not supply are simply omitted --
|
|
147
|
+
* the scripts already degrade gracefully when optional fields (e.g.
|
|
148
|
+
* transcript_path, prompt) are absent (see context-window-guard.sh, which
|
|
149
|
+
* exits 0 immediately when transcript_path is missing).
|
|
150
|
+
*/
|
|
151
|
+
function buildHookPayload(hookEventName, { toolName, toolInput, sessionId, cwd, prompt, transcriptPath } = {}) {
|
|
152
|
+
const payload = { hook_event_name: hookEventName };
|
|
153
|
+
if (toolName !== undefined) payload.tool_name = toolName;
|
|
154
|
+
if (toolInput !== undefined) payload.tool_input = toolInput;
|
|
155
|
+
if (sessionId !== undefined) payload.session_id = sessionId;
|
|
156
|
+
if (cwd !== undefined) payload.cwd = cwd;
|
|
157
|
+
if (prompt !== undefined) payload.prompt = prompt;
|
|
158
|
+
if (transcriptPath !== undefined) payload.transcript_path = transcriptPath;
|
|
159
|
+
return payload;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Translates a single script's `pi.exec` outcome into a bridge-internal
|
|
164
|
+
* verdict. Exit 0 => pass. Exit 2 => this script's own explicit "block"
|
|
165
|
+
* signal, regardless of gate/advisory class (mirrors Claude Code's own
|
|
166
|
+
* "exit 2 = block, stderr = reason" contract). Any other non-zero exit, or
|
|
167
|
+
* a thrown error, is resolved via the script's fail-direction classification.
|
|
168
|
+
*/
|
|
169
|
+
function translateExecResult(scriptName, execResult) {
|
|
170
|
+
const code = execResult?.code ?? 0;
|
|
171
|
+
const stdout = execResult?.stdout ?? '';
|
|
172
|
+
const stderr = execResult?.stderr ?? '';
|
|
173
|
+
|
|
174
|
+
if (code === 0) {
|
|
175
|
+
return { block: false, stdout, stderr };
|
|
176
|
+
}
|
|
177
|
+
if (code === 2) {
|
|
178
|
+
return { block: true, reason: stderr || `${scriptName} exited 2 (blocked)`, stdout, stderr };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const direction = classifyFailure(scriptName);
|
|
182
|
+
if (direction === 'closed') {
|
|
183
|
+
return {
|
|
184
|
+
block: true,
|
|
185
|
+
reason: stderr || `${scriptName} exited ${code} (failing closed)`,
|
|
186
|
+
stdout,
|
|
187
|
+
stderr,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { block: false, warning: `${scriptName} exited ${code} (failing open): ${stderr || 'no stderr'}`, stdout, stderr };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { translateExecResult };
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Script chain runner -- shared by every event handler below. Keeps the
|
|
197
|
+
// fail-direction / short-circuit / context-accumulation logic in exactly
|
|
198
|
+
// one place.
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Runs `scripts` (basenames under `.claude/hooks/`) in order via
|
|
203
|
+
* `pi.exec(absoluteScriptPath, { input: JSON.stringify(payload) })`,
|
|
204
|
+
* short-circuiting on the first block. Returns
|
|
205
|
+
* { block, reason?, context, invoked }
|
|
206
|
+
* where `context` is the concatenation of each script's trimmed, non-empty
|
|
207
|
+
* stdout (used by session.compacting / session_start to surface
|
|
208
|
+
* reinject-context.sh / handoff-resume.sh output back to omp).
|
|
209
|
+
*/
|
|
210
|
+
export async function runScriptChain(pi, scripts, payload, { projectRoot }) {
|
|
211
|
+
const invoked = [];
|
|
212
|
+
const contextParts = [];
|
|
213
|
+
|
|
214
|
+
for (const scriptName of scripts) {
|
|
215
|
+
const scriptPath = path.join(projectRoot, '.claude', 'hooks', scriptName);
|
|
216
|
+
invoked.push(scriptName);
|
|
217
|
+
|
|
218
|
+
let execResult;
|
|
219
|
+
try {
|
|
220
|
+
execResult = await pi.exec(scriptPath, { input: JSON.stringify(payload) });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
execResult = { code: 1, stdout: '', stderr: err?.message ?? String(err) };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const verdict = translateExecResult(scriptName, execResult);
|
|
226
|
+
|
|
227
|
+
if (verdict.stdout && verdict.stdout.trim()) {
|
|
228
|
+
contextParts.push(verdict.stdout.trim());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (verdict.warning) {
|
|
232
|
+
pi.logger?.warn?.(verdict.warning);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (verdict.block) {
|
|
236
|
+
return { block: true, reason: verdict.reason, context: contextParts.join('\n'), invoked };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { block: false, context: contextParts.join('\n'), invoked };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Event handlers (all exported directly for test import; wired onto `pi.on`
|
|
245
|
+
// by the default export below).
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function scriptsForToolCall(claudeToolName) {
|
|
249
|
+
const matcherGroup = matcherGroupFor(claudeToolName);
|
|
250
|
+
if (!matcherGroup) return [];
|
|
251
|
+
return HOOK_EVENT_MAP.tool_call[matcherGroup] ?? [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function scriptsForToolResult(claudeToolName) {
|
|
255
|
+
const matcherGroup = matcherGroupFor(claudeToolName);
|
|
256
|
+
// tool_result only has script chains for Edit|Write and Bash.
|
|
257
|
+
if (matcherGroup !== 'Edit|Write' && matcherGroup !== 'Bash') return [];
|
|
258
|
+
return HOOK_EVENT_MAP.tool_result[matcherGroup] ?? [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {object} event - { tool, input, sessionId, cwd }
|
|
263
|
+
*/
|
|
264
|
+
export async function runToolCall(pi, event, { projectRoot }) {
|
|
265
|
+
const toolName = mapToolName(event.tool);
|
|
266
|
+
const scripts = scriptsForToolCall(toolName);
|
|
267
|
+
const payload = buildHookPayload('PreToolUse', {
|
|
268
|
+
toolName,
|
|
269
|
+
toolInput: event.input,
|
|
270
|
+
sessionId: event.sessionId,
|
|
271
|
+
cwd: event.cwd,
|
|
272
|
+
});
|
|
273
|
+
const result = await runScriptChain(pi, scripts, payload, { projectRoot });
|
|
274
|
+
return { ...result, toolName };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function runToolResult(pi, event, { projectRoot }) {
|
|
278
|
+
const toolName = mapToolName(event.tool);
|
|
279
|
+
const scripts = scriptsForToolResult(toolName);
|
|
280
|
+
const payload = buildHookPayload('PostToolUse', {
|
|
281
|
+
toolName,
|
|
282
|
+
toolInput: event.input,
|
|
283
|
+
sessionId: event.sessionId,
|
|
284
|
+
cwd: event.cwd,
|
|
285
|
+
});
|
|
286
|
+
const result = await runScriptChain(pi, scripts, payload, { projectRoot });
|
|
287
|
+
return { ...result, toolName };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function runTurnStart(pi, event, { projectRoot }) {
|
|
291
|
+
const payload = buildHookPayload('UserPromptSubmit', {
|
|
292
|
+
sessionId: event.sessionId,
|
|
293
|
+
cwd: event.cwd,
|
|
294
|
+
prompt: event.prompt,
|
|
295
|
+
});
|
|
296
|
+
return runScriptChain(pi, HOOK_EVENT_MAP.turn_start, payload, { projectRoot });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// `session_before_compact`'s CANCEL capability is explicitly out of scope
|
|
300
|
+
// this cycle (PLAN.md Known gaps) -- this bridge does not wire that event.
|
|
301
|
+
export async function runSessionCompacting(pi, event, { projectRoot }) {
|
|
302
|
+
const payload = buildHookPayload('PreCompact', {
|
|
303
|
+
sessionId: event.sessionId,
|
|
304
|
+
cwd: event.cwd,
|
|
305
|
+
transcriptPath: event.transcriptPath,
|
|
306
|
+
});
|
|
307
|
+
return runScriptChain(pi, HOOK_EVENT_MAP['session.compacting'], payload, { projectRoot });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Must run on EVERY session start, including a post-compact continuation --
|
|
311
|
+
// no "first start only" guard. handoff-resume.sh is idempotent by design
|
|
312
|
+
// (reads + prints the docs/AI_HANDOFF/RUN.md cursor, never advances state),
|
|
313
|
+
// so re-running it on every start is safe and required for the mid-cycle
|
|
314
|
+
// handoff run to resume correctly after a compaction (PLAN.md D11).
|
|
315
|
+
export async function runSessionStart(pi, event, { projectRoot }) {
|
|
316
|
+
const payload = buildHookPayload('SessionStart', {
|
|
317
|
+
sessionId: event.sessionId,
|
|
318
|
+
cwd: event.cwd,
|
|
319
|
+
});
|
|
320
|
+
return runScriptChain(pi, HOOK_EVENT_MAP.session_start, payload, { projectRoot });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Default export -- omp hook factory contract: `export default function
|
|
325
|
+
// hook(pi) { pi.on(event, handler) }`.
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
export default function hook(pi) {
|
|
329
|
+
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
330
|
+
|
|
331
|
+
pi.on('tool_call', (event) => runToolCall(pi, event, { projectRoot }));
|
|
332
|
+
pi.on('tool_result', (event) => runToolResult(pi, event, { projectRoot }));
|
|
333
|
+
pi.on('turn_start', (event) => runTurnStart(pi, event, { projectRoot }));
|
|
334
|
+
pi.on('session.compacting', (event) => runSessionCompacting(pi, event, { projectRoot }));
|
|
335
|
+
pi.on('session_start', (event) => runSessionStart(pi, event, { projectRoot }));
|
|
336
|
+
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Core UKit Rule
|
|
4
4
|
|
|
5
5
|
- Human-facing workflow should optimize for one remembered command: `ukit install`.
|
|
6
|
-
- After install, normal work should feel natural inside **Claude/Codex/OpenCode
|
|
6
|
+
- After install, normal work should feel natural inside **Claude/Codex/OpenCode/omp**.
|
|
7
7
|
- **Quality first, then speed, then token discipline**: do not waste reads, logs, or repeated helper output.
|
|
8
8
|
- **Never stop after read-only steps.** For implement/apply/fix requests, continue to actual Edit/Write and verification in the same turn.
|
|
9
9
|
|
|
@@ -169,7 +169,7 @@ At the start of every OpenCode session, before working on the first task:
|
|
|
169
169
|
## Skills
|
|
170
170
|
|
|
171
171
|
- Canonical skills live in `.claude/skills/`.
|
|
172
|
-
- Adapter mirrors may also exist, for example `.codex/skills/` → `.claude/skills/` (symlink)
|
|
172
|
+
- Adapter mirrors may also exist, for example `.codex/skills/` → `.claude/skills/` (symlink). **omp** has no mirror: it reads `.claude/skills/` directly through its own `claude` discovery provider.
|
|
173
173
|
- **OpenCode**: reads `AGENTS.md` at session start only — it does NOT auto-load `.claude/skills/`. The model must explicitly read the triggered SKILL.md (see Session Start section above).
|
|
174
174
|
- If `opencode.json` ships `ukit-*` commands, treat them as internal helper entrypoints only and **never ask end users to run them**; humans should still only need `ukit install`.
|
|
175
175
|
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
## Delivery Profile
|
|
38
38
|
|
|
39
39
|
- Primary workflow: natural-language AI tooling installed by UKit
|
|
40
|
-
- Shared adapters: Claude Code, OpenAI Codex,
|
|
40
|
+
- Shared adapters: Claude Code, OpenAI Codex, OpenCode, omp (Oh My Pi)
|
|
41
41
|
- Change policy: smallest correct change with clear verification
|
|
42
42
|
|
|
43
43
|
## Session Start Routine
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
"mode": "native-auto-prune",
|
|
54
54
|
"preserveExistingCompaction": true
|
|
55
55
|
},
|
|
56
|
+
"omp": {
|
|
57
|
+
"autoCompact": true,
|
|
58
|
+
"mode": "precompact-reinject",
|
|
59
|
+
"preserveExistingHooks": true
|
|
60
|
+
},
|
|
56
61
|
"codex": {
|
|
57
62
|
"autoCompact": true,
|
|
58
63
|
"mode": "soft-handoff",
|
|
@@ -410,6 +415,11 @@
|
|
|
410
415
|
"mode": "native-auto-prune nghĩa là OpenCode dùng compaction.auto/prune native.",
|
|
411
416
|
"preserveExistingCompaction": "Nên giữ true để không gỡ compact native của OpenCode."
|
|
412
417
|
},
|
|
418
|
+
"omp": {
|
|
419
|
+
"autoCompact": "Giữ compact lane của omp (Oh My Pi) bật.",
|
|
420
|
+
"mode": "precompact-reinject nghĩa là UKit dùng hook session.compacting/session_before_compact của omp để bơm lại context quan trọng trước khi compact.",
|
|
421
|
+
"preserveExistingHooks": "Nên giữ true để không gỡ các module hooks/pre của bạn khi UKit cài bridge."
|
|
422
|
+
},
|
|
413
423
|
"codex": {
|
|
414
424
|
"autoCompact": "Bật policy soft handoff compact cho Codex Desktop.",
|
|
415
425
|
"mode": "soft-handoff nghĩa là UKit tạo/dùng state tóm tắt, không can thiệp trực tiếp vào internals của app Codex.",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# {{project.name}}
|
|
2
|
-
|
|
3
|
-
Auto-generated by UKit for Google Antigravity.
|
|
4
|
-
|
|
5
|
-
- Packs: {{project.stack}}
|
|
6
|
-
- Package manager: {{runtime.packageManager}}
|
|
7
|
-
|
|
8
|
-
## Start
|
|
9
|
-
Use graduated doc budget:
|
|
10
|
-
- Trivial: no docs
|
|
11
|
-
- Simple: `docs/MEMORY.md`
|
|
12
|
-
- Non-trivial: `docs/MEMORY.md` + `docs/PROJECT.md` + `docs/CODE_MAP.md`
|
|
13
|
-
- `docs/WORKLOG.md`: recent relevant entries only
|
|
14
|
-
|
|
15
|
-
If routing is complex/ambiguous, use:
|
|
16
|
-
- `node .antigravity/ukit/index/route-task.mjs "<prompt>" [--tool-command <cmd>] [--target <file>]`
|
|
17
|
-
|
|
18
|
-
Routing uses natural-language intent (fix/review/brainstorm/build/test/docs).
|
|
19
|
-
If confidence is low, ask one short clarifying question.
|
|
20
|
-
|
|
21
|
-
## Skills
|
|
22
|
-
Skills are shared from `.claude/skills/` via symlink. Each skill folder contains `SKILL.md`.
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Project Rules — {{project.name}}
|
|
2
|
-
|
|
3
|
-
Auto-generated by UKit. Edit `.claude/` sources to update.
|
|
4
|
-
|
|
5
|
-
## Core policy
|
|
6
|
-
|
|
7
|
-
- Natural language only; do not require slash commands or skill names.
|
|
8
|
-
- If helper/index commands are needed, run them internally and never ask users to run them.
|
|
9
|
-
- Quality first, keep latency low, avoid repeated token waste.
|
|
10
|
-
- Reuse shared route state from `.claude/ukit/skill-router-state.json`. If it already carries compact `previous-context` or `recent-output` lines, use those before widening docs, memory, or raw logs.
|
|
11
|
-
- Shared runtime lives in `.ukit/storage/`. Reuse `.ukit/storage/memory/`, `.ukit/storage/cache/compact-pressure.json`, `.ukit/storage/cache/output-history.json`, and `.ukit/storage/cache/tee/` before asking users to restate context.
|
|
12
|
-
|
|
13
|
-
## Route fast
|
|
14
|
-
|
|
15
|
-
- `fix/bug/error/debug/lỗi/sửa` → fix lane
|
|
16
|
-
- `review/audit/check/soát code` → review lane
|
|
17
|
-
- `brainstorm/idea/design/plan` → design lane
|
|
18
|
-
- `build/implement/add/create` → build lane
|
|
19
|
-
- `clone/copy/similar/giống/tương tự` → follow-pattern lane
|
|
20
|
-
- `test/spec/coverage` → test lane
|
|
21
|
-
- `docs/readme/changelog/tài liệu` → docs lane
|
|
22
|
-
- If intent is ambiguous, ask one short clarifying question.
|
|
23
|
-
- If routing is complex or shared route memory needs refresh, run `node .antigravity/ukit/index/route-task.mjs "<prompt>" [--tool-command <cmd>] [--target <file>]`.
|
|
24
|
-
|
|
25
|
-
## Skill + context loop
|
|
26
|
-
|
|
27
|
-
- Auto-activate the smallest useful installed skill set from prompt + tool/file evidence.
|
|
28
|
-
- Prefer `node .antigravity/ukit/index/resolve-context.mjs "<intent>" [--target <file>]` after choosing a skill.
|
|
29
|
-
- Prefer `node .antigravity/ukit/index/verify-context.mjs "<intent>" [--target <file>]` for verification planning.
|
|
30
|
-
|
|
31
|
-
## Indexed reading + verification
|
|
32
|
-
|
|
33
|
-
- Trivial: no docs, 1-2 files, minimal verify.
|
|
34
|
-
- Simple: `docs/MEMORY.md` only, 2-5 files, targeted verify.
|
|
35
|
-
- Non-trivial/risky: `docs/MEMORY.md` + `docs/PROJECT.md` + `docs/CODE_MAP.md`, broader verify.
|
|
36
|
-
- `docs/WORKLOG.md`: recent relevant entries only.
|
|
37
|
-
- Clone/follow-pattern: open at least 1 analog file.
|
|
38
|
-
- Shared logic: open the shared abstraction.
|
|
39
|
-
- Bug with tests: open related test first.
|
|
40
|
-
- If broad verification is blocked because targeted verification was skipped, go back to the routed targeted lane first.
|
|
41
|
-
- For bugfix tasks, run index-first triage: `node .antigravity/ukit/index/build-index.mjs` then `node .antigravity/ukit/index/triage.mjs "<error signature>"`, inspect top suspects, then widen only if needed.
|
|
42
|
-
|
|
43
|
-
## Safety
|
|
44
|
-
|
|
45
|
-
- Package manager: `{{runtime.packageManager}}`.
|
|
46
|
-
- Keep scope tight and prefer existing code paths.
|
|
47
|
-
- Update `docs/WORKLOG.md`, `docs/MEMORY.md`, `docs/CODE_MAP.md`, or `docs/PROJECT.md` after significant work when relevant.
|
|
48
|
-
- Never edit `.env*`, lock files, `.git/`, or `node_modules/`.
|
|
49
|
-
- Never run destructive commands without explicit confirmation.
|