Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4f4c5cb
chore: ignore .worktrees/ directory
nhuelstng Jun 29, 2026
b2818fc
docs: add design spec for codex + copilot backends
nhuelstng Jun 29, 2026
0512cce
feat: add codex + copilot harness descriptors + session listing dispatch
nhuelstng Jun 29, 2026
909bcd3
feat: extract shared manifest generator to internal/manifest
nhuelstng Jun 29, 2026
7b3c92c
feat: add codex bridge script + hooks.json registration
nhuelstng Jun 29, 2026
5de9ad0
feat: add copilot bridge script + hooks registration
nhuelstng Jun 29, 2026
98c8df9
docs: update spec to reflect verified review findings
nhuelstng Jun 29, 2026
7a3e777
docs: add implementation plan for remaining work
nhuelstng Jun 29, 2026
e6291c0
docs: add openspec change proposal for codex + copilot harnesses
nhuelstng Jun 29, 2026
1dc6697
docs: fix openspec proposal spec compliance issues
nhuelstng Jun 29, 2026
5a6d376
docs: fix openspec quality issues (section labeling, tests, wording)
nhuelstng Jun 29, 2026
ec7564d
docs: fix remaining 'deactivate' -> 'deactivation' word form
nhuelstng Jun 29, 2026
b7ff188
docs: add codex + copilot to README harness list
nhuelstng Jun 29, 2026
b70d13e
docs: fix README bridge mechanism, indentation, sandbox profile for c…
nhuelstng Jun 29, 2026
f38eb60
docs: add codex + copilot skills dirs to CREATING_A_SKILL.md
nhuelstng Jun 29, 2026
0fbd91b
docs: add codex+copilot to CREATING_A_SKILL discovery + sidecar sections
nhuelstng Jun 29, 2026
9577813
docs: sync bundled creating-a-skill.md with CREATING_A_SKILL.md
nhuelstng Jun 29, 2026
e7ef842
docs: add design spec for deferred items (manifest subcommand, binary…
nhuelstng Jun 29, 2026
64060fc
docs: update deferred-items spec with review findings (serve path, ex…
nhuelstng Jun 29, 2026
3f37d33
docs: add implementation plan for deferred items
nhuelstng Jun 29, 2026
50c1bf8
feat: implement deferred items (manifest subcommand, binary check, en…
nhuelstng Jun 29, 2026
66a6dde
feat(briefing): inject sandbox briefing into codex via -c instructions
nhuelstng Jun 30, 2026
241ec26
feat(briefing): inject sandbox briefing into copilot via COPILOT_CUST…
nhuelstng Jun 30, 2026
84e8623
fix: grant intermediate symlink dirs for inner harness binary
nhuelstng Jun 30, 2026
ba06693
fix(security): replace symlink-chain walking with explicit HarnessRea…
nhuelstng Jun 30, 2026
6c473c1
fix(security): resolve symlink argv[0] + grant only harness config dir
nhuelstng Jun 30, 2026
2f311a7
refactor: move harness dirs from default profile to SandboxDirs
nhuelstng Jun 30, 2026
3a1875d
feat: shebang detection for script binaries + trim default profile
nhuelstng Jun 30, 2026
a0fabce
fix(security): remove ~/.cache from default profile, scope per-harness
nhuelstng Jun 30, 2026
e5a604e
feat: per-workdir persistent cache dir for tool caches
nhuelstng Jun 30, 2026
aeb12b4
fix: remove XDG_CACHE_HOME redirect, keep only specific tool env vars
nhuelstng Jun 30, 2026
e0765df
revert: remove per-workdir cache dir and DefaultProfile stripping
nhuelstng Jun 30, 2026
fd593ab
fix: address Copilot PR review comments
nhuelstng Jun 30, 2026
2ebdb0c
feat(session+config): codex/copilot session listing, resume hint, des…
nhuelstng Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "command",
"command": "bash \"$(git rev-parse --show-toplevel)/.codex/hooks/omac-bridge.sh\"",
"statusMessage": "Loading omac skills"
}
Comment on lines +7 to +11
]
}
]
}
}
121 changes: 121 additions & 0 deletions .codex/hooks/omac-bridge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# omac Codex CLI bridge hook
# ==========================
#
# This is the Codex CLI-side counterpart to the Claude Code bridge
# (.claude/hooks/omac-bridge.sh). It wires Codex, running inside
# `omac start codex` / `omac serve codex`, to the omac control plane so the
# session's skills come online and are surfaced to the agent.
#
# It implements the common omac bridge interface:
# 1. Activate on session start — POST /__omac__/activate {dir}
# 2. Surface skills to the agent — emit the skills manifest as the
# SessionStart hook's additionalContext
#
# NOTE: No deactivate on Stop. Codex's Stop event is turn-scoped (fires every
# turn end), NOT session-scoped. Codex has no SessionEnd event. Deactivate
# relies on omac's TTL-based reaper (same as a crash). See design.md.
#
# Degradation: if OMAC_CONTROL_BASE is unset (Codex not running under omac),
# every branch is a no-op. The hook is inert and safe to ship anywhere.
#
# Requirements: bash, curl, and jq (degrades gracefully without jq).

set -euo pipefail

control_base="${OMAC_CONTROL_BASE:-}"
control_base="${control_base%/}"

# Codex delivers the hook payload as JSON on stdin. We read it once.
payload="$(cat || true)"

have_jq=0
if command -v jq >/dev/null 2>&1; then
have_jq=1
fi

json_get() {
local json="$1" filter="$2"
if [ "$have_jq" -eq 1 ]; then
printf '%s' "$json" | jq -r "$filter // empty" 2>/dev/null || true
fi
}

# The hook event name ("SessionStart", "Stop", …) and the session's
# working directory both come from the payload.
event="$(json_get "$payload" '.hook_event_name')"
dir="$(json_get "$payload" '.cwd')"
if [ -z "$dir" ]; then
dir="${CODEX_PROJECT_DIR:-$PWD}"
fi

# Inert when not running under omac.
if [ -z "$control_base" ]; then
exit 0
fi

control_post() {
local path="$1" body="$2"
curl -fsS -X POST "${control_base}${path}" \
-H 'content-type: application/json' \
-d "$body" 2>/dev/null || true
}

# Render the manifest from the activate response JSON.
render_manifest() {
local manifest="$1"
[ "$have_jq" -eq 1 ] || return 0
local skills_dir="${OMAC_HARNESS_SKILLS_DIR:-.codex/skills}"
printf '%s' "$manifest" | jq -r --arg skillsdir "$skills_dir" '
def skillsarr: (.skills // []);
"## omac skills available in this workspace\n" +
"\n" +
"You can call the following skill HTTP endpoints. Each `base` is the root URL for that skill'"'"'s sidecar; append the skill'"'"'s documented path.\n" +
"\n" +
"This workspace'"'"'s project directory is: `" + (.dir // "") + "`\n" +
(if (skillsarr | map(select(.scope == "global" and .state == "ready")) | length) > 0 then
"\n" +
"IMPORTANT: **global** skills are shared by every workspace. When a global skill writes into the project (e.g. the marketplace installing a skill), you MUST pass this workspace'"'"'s project directory explicitly — for the marketplace use `\"target_path\": \"" + (.dir // "") + "/" + $skillsdir + "\"` (the active harness'"'"'s skills directory) in the /install request body. Otherwise it installs into the wrong directory.\n"
else "" end) +
"\n" +
( skillsarr | sort_by(.name) | map(
. as $sk |
if .state == "ready" and (.base // "") != "" then
"- **" + .name + "** (" + (.scope // "") + ") — ready — base: `" + .base + "`"
elif .state == "pending-credentials" then
"- **" + .name + "** (" + (.scope // "") + ") — UNAVAILABLE (missing credentials: " + ((.missing // []) | join(", ")) + "). Run in your own terminal: " + ((.missing // []) | map("omac secrets set " + ($sk.name) + " " + .) | join(" ; "))
elif .state == "broken" then
"- **" + .name + "** (" + (.scope // "") + ") — BROKEN: " + (.detail // "see omac logs")
else empty end
) | join("\n") )
' 2>/dev/null || true
}

emit_context() {
local context="$1"
[ -n "$context" ] || return 0
if [ "$have_jq" -eq 1 ]; then
jq -n --arg ctx "$context" '{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $ctx
}
}'
fi
}

case "$event" in
SessionStart)
manifest="$(control_post "/__omac__/activate" "{\"dir\":\"${dir}\"}")"
if [ -n "$manifest" ]; then
context="$(render_manifest "$manifest")"
emit_context "$context"
fi
exit 0
;;
*)
# Stop and other events: no-op. Codex Stop is turn-scoped; deactivate
# relies on omac's TTL reaper. See design.md.
exit 0
;;
esac
117 changes: 117 additions & 0 deletions .copilot/hooks/omac-bridge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# omac Copilot CLI bridge hook
# =============================
#
# This is the Copilot CLI-side counterpart to the Claude Code bridge
# (.claude/hooks/omac-bridge.sh). It wires Copilot, running inside
# `omac start copilot` / `omac serve copilot`, to the omac control plane so
# the session's skills come online and are surfaced to the agent.
#
# It implements the common omac bridge interface:
# 1. Activate on session start — POST /__omac__/activate {dir}
# 2. Surface skills to the agent — emit the skills manifest as the
# SessionStart hook's additionalContext
# 3. Deactivate on session end — POST /__omac__/deactivate {dir}
#
# Registered with PascalCase event names (SessionStart, SessionEnd) so the
# payload uses snake_case fields (hook_event_name, session_id, cwd) —
# compatible with the claude bridge's dispatch logic. The output shape uses
# hookSpecificOutput.additionalContext nesting (VS Code-compatible format).
#
# Degradation: if OMAC_CONTROL_BASE is unset (Copilot not running under omac),
# every branch is a no-op. The hook is inert and safe to ship anywhere.
#
# Requirements: bash, curl, and jq (degrades gracefully without jq).

set -euo pipefail

control_base="${OMAC_CONTROL_BASE:-}"
control_base="${control_base%/}"

payload="$(cat || true)"

have_jq=0
if command -v jq >/dev/null 2>&1; then
have_jq=1
fi

json_get() {
local json="$1" filter="$2"
if [ "$have_jq" -eq 1 ]; then
printf '%s' "$json" | jq -r "$filter // empty" 2>/dev/null || true
fi
}

event="$(json_get "$payload" '.hook_event_name')"
dir="$(json_get "$payload" '.cwd')"
if [ -z "$dir" ]; then
dir="${COPILOT_PROJECT_DIR:-$PWD}"
fi

if [ -z "$control_base" ]; then
exit 0
fi

control_post() {
local path="$1" body="$2"
curl -fsS -X POST "${control_base}${path}" \
-H 'content-type: application/json' \
-d "$body" 2>/dev/null || true
}

render_manifest() {
local manifest="$1"
[ "$have_jq" -eq 1 ] || return 0
local skills_dir="${OMAC_HARNESS_SKILLS_DIR:-.copilot/skills}"
printf '%s' "$manifest" | jq -r --arg skillsdir "$skills_dir" '
def skillsarr: (.skills // []);
"## omac skills available in this workspace\n" +
"\n" +
"You can call the following skill HTTP endpoints. Each `base` is the root URL for that skill'"'"'s sidecar; append the skill'"'"'s documented path.\n" +
"\n" +
"This workspace'"'"'s project directory is: `" + (.dir // "") + "`\n" +
(if (skillsarr | map(select(.scope == "global" and .state == "ready")) | length) > 0 then
"\n" +
"IMPORTANT: **global** skills are shared by every workspace. When a global skill writes into the project (e.g. the marketplace installing a skill), you MUST pass this workspace'"'"'s project directory explicitly — for the marketplace use `\"target_path\": \"" + (.dir // "") + "/" + $skillsdir + "\"` (the active harness'"'"'s skills directory) in the /install request body. Otherwise it installs into the wrong directory.\n"
else "" end) +
"\n" +
( skillsarr | sort_by(.name) | map(
. as $sk |
if .state == "ready" and (.base // "") != "" then
"- **" + .name + "** (" + (.scope // "") + ") — ready — base: `" + .base + "`"
elif .state == "pending-credentials" then
"- **" + .name + "** (" + (.scope // "") + ") — UNAVAILABLE (missing credentials: " + ((.missing // []) | join(", ")) + "). Run in your own terminal: " + ((.missing // []) | map("omac secrets set " + ($sk.name) + " " + .) | join(" ; "))
elif .state == "broken" then
"- **" + .name + "** (" + (.scope // "") + ") — BROKEN: " + (.detail // "see omac logs")
else empty end
) | join("\n") )
' 2>/dev/null || true
}

emit_context() {
local context="$1"
[ -n "$context" ] || return 0
if [ "$have_jq" -eq 1 ]; then
jq -n --arg ctx "$context" '{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $ctx
}
}'
fi
}

case "$event" in
SessionEnd)
control_post "/__omac__/deactivate" "{\"dir\":\"${dir}\"}" >/dev/null
exit 0
;;
*)
manifest="$(control_post "/__omac__/activate" "{\"dir\":\"${dir}\"}")"
if [ -n "$manifest" ]; then
context="$(render_manifest "$manifest")"
emit_context "$context"
fi
exit 0
;;
esac
21 changes: 21 additions & 0 deletions .copilot/hooks/omac.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"hooks": {
"SessionStart": [
{
"type": "command",
"bash": "bash \"$(git rev-parse --show-toplevel)/.copilot/hooks/omac-bridge.sh\"",
"cwd": ".",
"timeoutSec": 30
}
Comment on lines +5 to +10
],
"SessionEnd": [
{
"type": "command",
"bash": "bash \"$(git rev-parse --show-toplevel)/.copilot/hooks/omac-bridge.sh\"",
"cwd": ".",
"timeoutSec": 10
}
Comment on lines +13 to +18
]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ __pycache__/
.AppleDouble
.LSOverride

# Local worktrees (isolated workspaces for investigation/feature work)
/.worktrees/

# Editors
*.swp
*.swo
Expand Down
12 changes: 8 additions & 4 deletions CREATING_A_SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@ which harness is chosen:
- **Reaching your sidecar.** Inside the sandbox the agent reads
`OMAC_<MOUNT>_BASE` (a `http://127.0.0.1:<port>/<mount>` URL) — or
`OMAC_<MOUNT>_SOCKET_BASE` for the `http+unix://` form — and appends your
documented path. These names are the same under OpenCode and Claude Code.
Global skills also get `OMAC_G_<MOUNT>_BASE`.
documented path. These names are the same under every harness (OpenCode,
Claude Code, Codex, Copilot). Global skills also get `OMAC_G_<MOUNT>_BASE`.
- **Discovery.** Each harness ships a *bridge* that surfaces a skills manifest
to the agent listing every ready skill's `base` URL. Under **OpenCode** this
is the plugin in `.opencode/plugins/`; under **Claude Code** it is the
`SessionStart` hook in `.claude/`. Both produce the same manifest content
from omac's control plane — you do not interact with either directly.
`SessionStart` hook in `.claude/`; under **Codex** it is the hook in
`.codex/hooks.json`; under **Copilot** it is the hook in
`.copilot/hooks/omac.json`. All produce the same manifest content from
omac's control plane — you do not interact with any of them directly.
- **Where skills live (harness-scoped).** Each harness reads `SKILL.md` from
its own skills dir, and omac scopes discovery to match:
- OpenCode → `.opencode/skills` (+ `~/.config/opencode/skills`)
- Claude Code → `.claude/skills` (+ `~/.claude/skills`)
- Codex → `.codex/skills` (+ `~/.codex/skills`)
- Copilot → `.copilot/skills` (+ `~/.copilot/skills`)
- **Shared** → `.agents/skills` (+ `~/.config/agents/skills`), in scope for
every harness.

Expand Down
Loading
Loading