@damian87/omp 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,14 +37,39 @@ the `omp schedule …` commands on the user's behalf.
37
37
  ```bash
38
38
  omp schedule add --id <id> --cron "<expr>" --prompt "<text>" \
39
39
  [--allow-all-tools] [--cwd <dir>] [--model <m>] [--timeout <ms>] \
40
- [--max-runs <n>] [--ttl-hours <h>] --json
40
+ [--max-runs <n>] [--ttl-hours <h>] \
41
+ [--notify-target slack:<ID>] [--notify-desktop] [--notify-open-omp] --json
41
42
  ```
42
43
  Jobs auto-expire after 72h by default (`--ttl-hours`) — set a longer TTL or a
43
44
  `--max-runs` cap as needed. Use `--dry-run` to preview the OS entry first.
45
+
46
+ **End-of-run notifications (all opt-in, default off; failures never affect the job):**
47
+ - `--notify-target slack:<C|G|D|U…>` — post the run summary to Slack (needs
48
+ `SLACK_BOT_TOKEN`; falls back to `SLACK_HOME_CHANNEL` when no target).
49
+ - `--notify-desktop` — fire a native desktop notification (job id + status +
50
+ one-line summary). Transport per OS: **macOS → `osascript`** (the only path
51
+ that reliably displays on Sequoia; shown under "Script Editor", **not
52
+ clickable**); **Linux/Windows → node-notifier** (notify-send / SnoreToast).
53
+ - `--notify-open-omp` — make the notification's click open an interactive `omp`
54
+ session in the schedule state root (the SessionStart banner then surfaces the
55
+ latest result). **Requires a click-capable transport**, which on macOS means a
56
+ system `terminal-notifier` enabled via `OMP_NOTIFY_USE_TERMINAL_NOTIFIER=1`
57
+ (`brew install terminal-notifier`). Note: terminal-notifier does **not**
58
+ display on some macOS Sequoia builds — if notifications stop appearing,
59
+ unset that env to fall back to osascript (display-only). Disable desktop
60
+ notifications entirely with `OMP_DISABLE_DESKTOP_NOTIFY=1`.
61
+
62
+ Slack and desktop are independent and can be combined on one job.
44
63
  4. **Confirm** by listing: `omp schedule list --json`.
45
64
  5. **Trigger now** to test it once: `omp schedule run-now --id <id>`.
46
65
  6. **Inspect** results: `omp schedule status --id <id> --json` (recent results
47
- are also surfaced automatically at the start of future sessions).
66
+ are also surfaced automatically at the start of future sessions). To pull up
67
+ the latest run with full context by id — e.g. after seeing a desktop
68
+ notification titled `schedule: <id>` — run `omp schedule open <id>`, which
69
+ prints the latest status, summary, and the full captured output. Add `--tmux`
70
+ to instead drop into an interactive `omp` session (auto-wrapped in tmux) rooted
71
+ at the project; the SessionStart banner surfaces *recent* scheduled runs there
72
+ (not pinned to `<id>`) — for this id's exact output, use plain `omp schedule open <id>`.
48
73
  7. **Remove** when done: `omp schedule remove --id <id>` (fully uninstalls the OS
49
74
  entry; do NOT delete `.omp/state/schedule/` by hand).
50
75
 
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: verify-byok
3
+ description: Verify an omp change end-to-end — static gate (build/tsc/tests/lint/catalog) plus a live BYOK run that drives Copilot/teams on a real model and captures evidence. Use before merging any PR that touches hooks, comms, team, launch, or skills.
4
+ argument-hint: "<branch-or-PR to verify>"
5
+ ---
6
+
7
+ # verify-byok — evidence-based verification for omp changes
8
+
9
+ **Invocation:** `/verify-byok <branch-or-PR>`
10
+
11
+ Prove a change actually works, not just that it compiles. Two gates: a **static** gate (cheap, always) and a **live BYOK** gate (drives real Copilot CLI sessions on a Bring-Your-Own-Key model so no GitHub quota is needed). Report **real command output**, never claims. Be honest about what you did NOT test.
12
+
13
+ ## When to use
14
+ Before merging any PR — especially ones touching `hooks/`, `scripts/*.mjs`, `src/comms`, `src/team`, `src/copilot/launch`, `src/copilot/trust`, or `.github/skills`. Anything whose behavior only appears at runtime (tmux submit keys, hook firing, trust dialog, model tool-calls) needs the live gate.
15
+
16
+ ## Prerequisites (one-time, persistent)
17
+ - `~/.omp/.env` holds BYOK: `COPILOT_PROVIDER_BASE_URL=https://openrouter.ai/api/v1`, `COPILOT_PROVIDER_TYPE=openai`, `COPILOT_PROVIDER_API_KEY=…`, `COPILOT_MODEL=…`, `COPILOT_PROVIDER_MODEL_ID=…`. omp auto-loads it; `~/.zshrc` sourcing it makes direct `copilot` BYOK too.
18
+ - Free model `openai/gpt-oss-120b:free` exercises plumbing but botches structured edits/`apply_patch` and narrates instead of acting — use a capable model (`anthropic/claude-sonnet-4.5` + `COPILOT_PROVIDER_MODEL_ID=claude-sonnet-4`) when verifying that real tasks complete.
19
+ - Folder trust: a session only skips the "Do you trust this folder?" dialog if the cwd is in `~/.copilot/config.json#trustedFolders` (`--yolo` does NOT skip it). `omp` auto-adds it on launch; `/private/tmp` is pre-trusted.
20
+
21
+ ## Static gate (run from the branch worktree)
22
+ ```bash
23
+ npm run build # tsc
24
+ npx tsc -p tsconfig.json --noEmit # type-clean
25
+ npx vitest run # ALL must pass — note the count
26
+ node dist/src/cli.js lint:skills --root . # 0 issues
27
+ node dist/src/cli.js catalog validate # PASS
28
+ ```
29
+ A merged-overlap PR? Confirm the test count rose (new tests survived the rebase) and old behavior's tests still pass.
30
+
31
+ ## Live BYOK gate
32
+ Build the branch CLI first (`npm run build`); installed `omp` may be older. Drive Copilot inside tmux (each pane is a pty). **Submit with the `Enter` key name, never `C-m`** — Copilot ≥1.0.61 ignores `C-m`.
33
+
34
+ 1. **Launch + reach a model:** `tmux new-session -d -s vbyok -c <dir>` → send `omp` (or `omp --madmax` for bypass) → wait for `/ commands`. Confirm the model line shows your BYOK model and `Session: 0 AIC used` (0 AI Credits = not on GitHub quota). Send a prompt, send `Enter`, confirm a real reply.
35
+ 2. **Exercise the changed surface** — pick the user-visible behavior the change claims:
36
+ - hooks → tail `<cwd>/.omp/state/hooks.log` for the events firing;
37
+ - team → run a 2-worker team, confirm files/artifacts on disk and the script's `🎉 All N agents completed!` (read the raw stdout, not the leader's summary);
38
+ - cost/minify → pipe a postToolUse payload to `scripts/post-tool-use.mjs` and check `modifiedResult` + raw preserved + ledger `savedTokens` + `omp cost`.
39
+ 3. **Capture evidence**: `tmux capture-pane -p -t <session>` plus the on-disk artifact. Keep transcripts.
40
+
41
+ ## Reporting (mandatory)
42
+ - PASS/FAIL table with the actual captured output (model line, reply, artifact contents, ledger numbers).
43
+ - State the model used and that no GitHub quota was consumed.
44
+ - **Honest limitations**: heuristics (e.g. pane-scrape completion detection), model-quality effects (free model botching edits), anything gated/conditional, and anything you did NOT exercise.
45
+ - Never report "works" without exercising the user-visible surface — verify before asserting.
46
+
47
+ ## Anti-patterns
48
+ - Trusting a subagent's "Complete." — re-run the gate yourself.
49
+ - `C-m` to submit to Copilot (use `Enter`).
50
+ - Concluding from a green build alone — runtime bugs (env propagation, trust dialog, submit keys) pass the build and fail live.
package/README.md CHANGED
@@ -57,6 +57,83 @@ That's it.
57
57
  - **Chat bridge** — `omp gateway` runs long-lived chat connectors (Slack today, more next) so you can DM Copilot from anywhere
58
58
  - **Lifecycle hooks** — `sessionStart`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `sessionEnd`, `errorOccurred`
59
59
  - **Doctor included** — `omp doctor` verifies plugin manifest, skills discovery, hooks, and the underlying `copilot` CLI in one shot
60
+ - **Self-update** — when a newer release is published, `omp` (and `omp --version`) offers to update in a TTY; `omp update` self-updates the CLI and refreshes the Copilot plugin so both stay in lockstep. Never prompts in CI / `--json` / non-TTY; opt out with `OMP_NO_UPDATE_CHECK=1`
61
+
62
+ ---
63
+
64
+ ## Architecture
65
+
66
+ oh-my-copilot is two things working together: a **shell CLI** (`omp`) that wraps and scripts the GitHub Copilot CLI, and a **Copilot plugin** that ships in-session slash skills, custom agents, and native lifecycle hooks. Both feed the same orchestration modes, the same file-based memory, and the same `.omp` state — so whether you drive from the shell, an in-session `/skill`, Slack, or cron, you hit one coherent system.
67
+
68
+ ```mermaid
69
+ flowchart TB
70
+ subgraph Surfaces["① Where you drive it"]
71
+ SH["Shell CLI<br/><code>omp</code> / <code>omp --madmax</code>"]
72
+ IDE["In-session <code>/skills</code><br/>(Copilot plugin)"]
73
+ GW["Gateway<br/>Slack / messaging"]
74
+ CRON["Cron<br/><code>omp schedule</code>"]
75
+ end
76
+
77
+ subgraph Copilot["② GitHub Copilot CLI (wrapped)"]
78
+ CP["copilot session"]
79
+ HK["Lifecycle hooks<br/>sessionStart · sessionEnd · agentStop<br/>pre/postToolUse · userPromptSubmitted"]
80
+ PS["Plugin skills + agents<br/>ralph · ralplan · team · council<br/>code-review · tdd · research · …"]
81
+ end
82
+
83
+ subgraph Orchestration["③ Orchestration modes"]
84
+ direction LR
85
+ RALPH["ralph<br/>PRD verify/fix loop"]
86
+ UW["ultrawork<br/>parallel fan-out"]
87
+ UQA["ultraqa<br/>QA cycles"]
88
+ AP["autopilot"]
89
+ TEAM["team<br/>tmux multi-agent"]
90
+ COUNCIL["council<br/>weighted consensus"]
91
+ end
92
+
93
+ subgraph Learning["④ Memory & learning loop"]
94
+ direction LR
95
+ MR["memory-review<br/>cheap model, end-of-session"]
96
+ PM["project-memory<br/>directives + notes"]
97
+ SE["self-evolve<br/>skill drafts"]
98
+ DL["daily-log"]
99
+ IM["instructions-memory<br/>→ copilot-instructions.md"]
100
+ end
101
+
102
+ subgraph Store["⑤ Config & state"]
103
+ direction LR
104
+ G["~/.omp<br/>global config + .env"]
105
+ P[".omp/<br/>project config · memory<br/>cost · trace · mode-state"]
106
+ SST["~/.copilot/session-state<br/>events.jsonl transcripts"]
107
+ end
108
+
109
+ SH --> CP
110
+ GW --> CP
111
+ CRON --> CP
112
+ IDE --> PS
113
+ CP --> HK
114
+ PS --> Orchestration
115
+ SH --> Orchestration
116
+ HK -- "agentStop drives the loop" --> Orchestration
117
+ Orchestration --> P
118
+
119
+ HK -- "sessionEnd (detached)" --> MR
120
+ SH -- "wrapper fallback (headless -p)" --> MR
121
+ MR -- reads transcript --> SST
122
+ MR -- "facts" --> PM
123
+ MR -- "procedures" --> SE
124
+ MR -- "rules (gated)" --> PM
125
+ PM --> IM
126
+ DL --> IM
127
+ IM -- "injected next session" --> CP
128
+
129
+ G -. config .-> MR
130
+ P -. config .-> MR
131
+ Orchestration -. cost/trace .-> P
132
+ ```
133
+
134
+ **The flow:** you launch a Copilot session from any surface ①. It runs through the wrapped Copilot CLI ②, where plugin hooks and skills can spin up orchestration modes ③ (ralph/ultrawork/ultraqa/team/council). When the session ends, the **learning loop** ④ fires — a cheap model reviews the transcript and writes durable **notes**, gated **directives**, and **skill drafts**, which `instructions-memory` injects into the *next* session so it starts smarter. Everything persists in layered config/state ⑤: global `~/.omp`, per-project `.omp/`, and Copilot's own session transcripts.
135
+
136
+ > The learning loop is **opt-in** (`omp config set memory-mode on`) and runs on a cheap model (`gpt-5-mini` by default) — the expensive reasoning already happened in your main session. See [docs/memory-mode.md](docs/memory-mode.md).
60
137
 
61
138
  ---
62
139
 
@@ -182,7 +259,8 @@ Notes:
182
259
 
183
260
  ```bash
184
261
  omp --help
185
- omp version
262
+ omp version # prints version; in a TTY, offers to self-update if one is available
263
+ omp update # self-update the CLI (npm) + refresh the Copilot plugin
186
264
  omp doctor # verify install + copilot binary
187
265
  omp list # show discovered skills and agents
188
266
  omp setup [--dry-run] [--scope project|user]
@@ -205,9 +283,10 @@ omp gateway notify --text "<msg>" [--target slack:C…|G…|D…|U… [:thread_t
205
283
  omp slack serve # deprecated alias of `gateway serve --only slack`
206
284
  omp slack doctor [--json] # deprecated alias of `gateway status --only slack`
207
285
  omp env init [--force] # write ~/.omp/.env (interactive Slack token setup)
208
- omp schedule add --id <id> --cron "*/15 * * * *" --prompt "<text>" [--allow-all-tools] [--cwd <dir>] [--model <m>] [--timeout <ms>] [--max-runs N] [--ttl-hours H] [--notify-target slack:U0123ABCD] [--dry-run]
286
+ omp schedule add --id <id> --cron "*/15 * * * *" --prompt "<text>" [--allow-all-tools] [--cwd <dir>] [--model <m>] [--timeout <ms>] [--max-runs N] [--ttl-hours H] [--notify-target slack:U0123ABCD] [--notify-desktop] [--notify-open-omp] [--dry-run]
209
287
  omp schedule list # registered jobs + OS-install status
210
288
  omp schedule status <id> # last run + result summary
289
+ omp schedule open <id> [--tmux] # print this id's latest status + full output (--tmux: open an omp session)
211
290
  omp schedule run-now <id> # trigger one run immediately
212
291
  omp schedule remove <id> # uninstall the OS entry + delete the job
213
292
  omp goal set "<objective>" | read [--json]
@@ -227,6 +306,8 @@ Environment overrides:
227
306
  - `OMP_COPILOT_BIN` — alternate `copilot` binary
228
307
  - `OMP_BIN` — absolute path to the `omp` wrapper written into OS-scheduler entries (overrides `which omp`)
229
308
  - `OMP_SKIP_USER_ENV` — when `1`, skip auto-loading `~/.omp/.env` (useful for hermetic CI runs)
309
+ - `OMP_DISABLE_DESKTOP_NOTIFY` — when set, suppress all `--notify-desktop` notifications
310
+ - `OMP_NOTIFY_USE_TERMINAL_NOTIFIER` — when set (+ a system `terminal-notifier` on PATH), use it for desktop notifications so the click can open omp; otherwise macOS uses `osascript` (display-only)
230
311
 
231
312
  **Scheduled jobs** register a durable per-job entry with the OS scheduler (macOS launchd,
232
313
  Linux systemd-user timers, or a managed `crontab` block as a cross-platform fallback) that
@@ -234,8 +315,13 @@ invokes `omp schedule run --id <id>` on the cron schedule. Each tick spawns a fr
234
315
  session; overlapping runs are locked out and every run is killed at its `--timeout`
235
316
  (default 5 min). Jobs default to **read-only** (`--allow-all-tools` is opt-in and prints a
236
317
  warning) and auto-expire after 72h unless `--ttl-hours`/`--max-runs` say otherwise. Recent
237
- run results are surfaced automatically at the start of new Copilot sessions. Always use
238
- `omp schedule remove`, never delete `.omp/state/schedule/` by hand, so the OS entry is
318
+ run results are surfaced automatically at the start of new Copilot sessions, and
319
+ `omp schedule open <id>` prints any job's latest status + full captured output on demand.
320
+ Opt into end-of-run **notifications** (default off; failures never affect the job): `--notify-target slack:…`
321
+ posts to Slack, and `--notify-desktop` fires a native notification (job id + status + one-line
322
+ summary) — on macOS via `osascript` (display-only; for a clickable notification that opens omp,
323
+ set `OMP_NOTIFY_USE_TERMINAL_NOTIFIER=1` with a system `terminal-notifier` and add `--notify-open-omp`).
324
+ Always use `omp schedule remove`, never delete `.omp/state/schedule/` by hand, so the OS entry is
239
325
  uninstalled cleanly.
240
326
 
241
327
  ### Chat bridge: drive Copilot from Slack
@@ -269,7 +355,7 @@ omp grows in vertical slices. Items aren't pinned to specific semver versions
269
355
 
270
356
  ### Already shipped
271
357
 
272
- - **Scheduled tasks** (v0.6.0) — durable local cron: `omp schedule add --id pr-watch --cron "*/15 * * * *" --prompt "…"` plus `/schedule` in-session. Each job registers an OS-scheduler entry (launchd / systemd-user / crontab fallback) that fires a fresh agent session, survives reboot, locks out overlap, and surfaces results at the next session start.
358
+ - **Scheduled tasks** (v0.6.0) — durable local cron: `omp schedule add --id pr-watch --cron "*/15 * * * *" --prompt "…"` plus `/schedule` in-session. Each job registers an OS-scheduler entry (launchd / systemd-user / crontab fallback) that fires a fresh agent session, survives reboot, locks out overlap, and surfaces results at the next session start. Opt-in end-of-run notifications (`--notify-target` Slack, `--notify-desktop` native) and `omp schedule open <id>` to pull up a run's full output by id.
273
359
  - **Chat bridge — Slack inbound** (v0.8.0) — `omp gateway` runs long-lived chat connectors that forward messages into a running Copilot tmux session and post replies back. Slack is the first connector (Socket Mode, no public URL). `omp env init` walks you through one-time token setup; tokens live in `~/.omp/.env` (auto-loaded on every invocation). See [`docs/slack-setup.md`](docs/slack-setup.md).
274
360
  - **Slack outbound — `omp gateway notify`** — stateless REST `chat.postMessage` from any process (cron `--notify-target`, in-session `/slack <message>`, ad-hoc `omp gateway notify --text "..."`). Default destination from `SLACK_HOME_CHANNEL`; explicit `--target slack:C…/G…/D…/U…` overrides; `U…` auto-resolves to a DM via `conversations.open`.
275
361
  - **Weighted-consensus council** — multi-model council with role weights + minority report. Via `omp council` or `/weighted-consensus`.
@@ -908,6 +908,29 @@
908
908
  "notes": "Use /slack from .github/skills/slack/SKILL.md."
909
909
  }
910
910
  }
911
+ },
912
+ {
913
+ "id": "verify-byok",
914
+ "name": "verify-byok",
915
+ "title": "BYOK verification",
916
+ "category": "verification",
917
+ "summary": "Static + live BYOK verification of omp changes with evidence capture.",
918
+ "notes": "Maintainer verification skill: static gate plus a live Copilot/team run on a BYOK model.",
919
+ "defaultCommand": "verify-byok",
920
+ "phase1": true,
921
+ "sourceSkill": "verify-byok",
922
+ "providers": {
923
+ "copilot": "supported"
924
+ },
925
+ "support": {
926
+ "copilot": "native"
927
+ },
928
+ "providerSupport": {
929
+ "copilot": {
930
+ "state": "native",
931
+ "notes": "Use /verify-byok from .github/skills/verify-byok/SKILL.md."
932
+ }
933
+ }
911
934
  }
912
935
  ]
913
936
  }
@@ -528,6 +528,31 @@
528
528
  },
529
529
  "projection": "project-skill",
530
530
  "phase1": true
531
+ },
532
+ {
533
+ "name": "verify-byok",
534
+ "capabilityId": "verify-byok",
535
+ "capabilityIds": [
536
+ "verify-byok"
537
+ ],
538
+ "source": ".github/skills/verify-byok/SKILL.md",
539
+ "sourcePath": ".github/skills/verify-byok/SKILL.md",
540
+ "canonicalPath": ".github/skills/verify-byok/SKILL.md",
541
+ "description": "Verify an omp change with a static gate plus a live BYOK run.",
542
+ "summary": "Verify an omp change with a static gate plus a live BYOK run.",
543
+ "support": "project-skill",
544
+ "aliases": [],
545
+ "slashCommands": [
546
+ "verify-byok"
547
+ ],
548
+ "projections": {
549
+ "copilot": {
550
+ "command": "/verify-byok",
551
+ "state": "supported"
552
+ }
553
+ },
554
+ "projection": "project-skill",
555
+ "phase1": true
531
556
  }
532
557
  ]
533
558
  }
package/dist/src/cli.js CHANGED
@@ -16,6 +16,72 @@ function flagValue(args, flag) {
16
16
  return undefined;
17
17
  return args[index + 1];
18
18
  }
19
+ /**
20
+ * Interactive only when both streams are TTYs, we're not emitting JSON, and
21
+ * we're not in CI (which may allocate a pseudo-TTY but must never block on a
22
+ * prompt). Honors the same no-block contract as update-prompt.ts.
23
+ */
24
+ function isInteractive(json) {
25
+ if (process.env.CI?.trim())
26
+ return false;
27
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY) && !json;
28
+ }
29
+ /**
30
+ * Provide an UpdatePromptIO backed by readline. The interface is created lazily
31
+ * on the first `ask` (so no-update launches never touch the TTY) and always
32
+ * closed before we return, freeing stdin for a subsequent copilot launch.
33
+ */
34
+ async function withUpdateIO(fn) {
35
+ let rl;
36
+ const io = {
37
+ print: (line) => console.log(line),
38
+ ask: async (prompt) => {
39
+ if (!rl) {
40
+ const readline = await import("node:readline/promises");
41
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
42
+ }
43
+ const answer = await rl.question(prompt);
44
+ // Release stdin immediately so the update/plugin child processes that
45
+ // follow (stdio: "inherit") never contend with an open readline.
46
+ rl.close();
47
+ rl = undefined;
48
+ return answer;
49
+ },
50
+ };
51
+ try {
52
+ return await fn(io);
53
+ }
54
+ finally {
55
+ rl?.close();
56
+ }
57
+ }
58
+ /**
59
+ * Re-exec the freshly-installed omp after a self-update so the launch runs the
60
+ * new version. `npm i -g` overwrote the entrypoint at `process.argv[1]` in
61
+ * place, so re-running it with the same node executes the new code. The child
62
+ * inherits stdio (so copilot's TUI works) and gets `OMP_NO_UPDATE_CHECK=1` to
63
+ * guarantee it can't loop back into the update prompt. Returns null when the
64
+ * entrypoint can't be resolved, so the caller falls back to a normal launch.
65
+ */
66
+ async function reexecAfterUpdate(argv) {
67
+ const entry = process.argv[1];
68
+ if (!entry)
69
+ return undefined;
70
+ const { spawn } = await import("node:child_process");
71
+ return await new Promise((resolve) => {
72
+ const child = spawn(process.execPath, [entry, ...argv], {
73
+ stdio: "inherit",
74
+ env: { ...process.env, OMP_NO_UPDATE_CHECK: "1" },
75
+ });
76
+ child.on("error", () => resolve({ ok: false, exitCode: 127, message: "re-exec failed" }));
77
+ child.on("close", (code) => {
78
+ // Match launchCopilot's convention: a signal-terminated child yields a
79
+ // null code — treat that as a non-zero exit, not success.
80
+ const exitCode = typeof code === "number" ? code : 1;
81
+ resolve({ ok: exitCode === 0, exitCode, message: `relaunched omp exit=${exitCode}` });
82
+ });
83
+ });
84
+ }
19
85
  function printResult(result, json) {
20
86
  if (json || typeof result.output === "object") {
21
87
  console.log(JSON.stringify(result.output ?? { ok: result.ok, message: result.message }, null, 2));
@@ -26,7 +92,8 @@ function printResult(result, json) {
26
92
  }
27
93
  }
28
94
  function help() {
29
- return `oh-my-copilot\n\nRun \`omp\` with no arguments to launch copilot (permissions bypass OFF).\nUse \`omp help\` to show this list.\n\nCommands:\n (no args) launch copilot (bypass OFF by default)\n version [--json]\n list [--json]\n setup [--dry-run] [--scope project|user] [--plugin-root <dir>] [--json]\n doctor [--json] [--copilot-bin <path>] [--skip-copilot] [--hooks]\n cost [--json] [--session <id>] [--days <n>]\n launch -- <args...>\n --madmax [args...] (bare-flag launch with permissions bypass; alias of --yolo)\n team <N:role> "<task>" [--name <name>] [--json]\n team status <name> [--json]\n team shutdown <name> [--json]\n team api claim-task --input '<json>' [--json]\n team api transition-task-status --input '<json>' [--json]\n team api send-message --input '<json>' [--json]\n team api broadcast --input '<json>' [--json]\n team api mailbox-list --input '<json>' [--json]\n team api mailbox-mark-delivered --input '<json>' [--json]\n council "<question>" [--models a,b,c|m:role:weight] [--context <text|@file>] [--rubric <text|@file>] [--synth <model>] [--probe] [--timeout <ms>] [--synth-timeout <ms>] [--min-survivors <n>] [--max-concurrency <n>] [--tmp-dir <dir>] [--json]\n comms status [--session <name>] [--json] (is copilot on + online? auto-discovers session)\n comms send --text "<prompt>" [--force] [--session <name>] [--json]\n comms recv [--wait] [--lines <n>] [--timeout <ms>] [--session <name>] [--json]\n comms ask --text "<prompt>" [--force] [--lines <n>] [--timeout <ms>] [--session <name>] [--json]\n gateway serve [--only <name>[,<name>]] (run all configured connectors; today: slack)\n gateway status [--json] [--only <name>[,...]] (per-connector readiness; no sockets opened)\n gateway doctor [--json] [--only <name>[,...]] (alias for 'gateway status')\n gateway notify --text "<msg>" [--target slack:C\\|D\\|G\\|U... [:thread_ts]] [--thread-ts <ts>] [--json]\n (one-shot outbound Slack post; falls back to SLACK_HOME_CHANNEL)\n slack serve (deprecated alias for 'gateway serve --only slack')\n slack doctor [--json] (deprecated alias for 'gateway status --only slack')\n env init [--force] (interactive: write ~/.omp/.env with Slack tokens + optional SLACK_HOME_CHANNEL)\n non-interactive: set OMP_INIT_BOT_TOKEN/OMP_INIT_APP_TOKEN/OMP_INIT_HOME_CHANNEL\n (env vars preferred over --bot-token/--app-token/--home-channel flags)\n (--session is optional when exactly one omp-<digits> tmux session is running)\n${registeredCommandHelpLines().join("\n")}\n ralph start "<task>" [--max-iterations <n>] [--session-id <id>] [--json]\n ralph status [--json]\n ralph tick [--json]\n ralph cancel [--json]\n ultrawork start "<objective>" [--task-count <n>] [--summary <s>] [--json]\n ultrawork status [--json]\n ultrawork cancel [--json]\n ultraqa start "<goal>" [--max-cycles <n>] [--json]\n ultraqa cycle pass|fail|pending [--json]\n ultraqa status [--json]\n ultraqa cancel [--json]\n schedule add --id <id> --cron "<expr>" --prompt "<text>" [--bin copilot] [--model <m>] [--cwd <dir>] [--timeout <ms>] [--max-runs <n>] [--ttl-hours <h>] [--allow-all-tools] [--notify-target slack:<ID>] [--dry-run] [--json]\n schedule list [--json]\n schedule status <id> [--json]\n schedule run-now <id> [--json]\n schedule remove <id> [--json]\n goal set "<objective>" [--json]\n goal read [--json]\n memory sync [--json] (render goal+directives into copilot-instructions.md)\n config get [--json] | config set memory-mode on|off | config set memory-review-model <slug> | config set memory-review-min-messages <n> [--global]\n (--global writes ~/.omp/config.json; applies to every project. project .omp/config.json overrides it)\n memory-review --session <uuid|latest> [--model <slug>] [--json] (cheap-model end-of-session review; opt-in via memory-mode)\n daily-log set-goal "<text>" [--json]\n daily-log add "<text>" [--json]\n daily-log read [--days <n>] [--json]\n daily-log prune [--keep-days <n>] [--json]\n state write <key> <val> [--ttl <s>] | read|delete|status <key> | list | cleanup [--json]\n project-memory read [<id>] | index | add-note "<title>" [--body "<text>"] | add-directive "<rule>" | prune-notes --keep <n>|--older-than <days> [--json]\n trace timeline [<sessionId>] [--limit <n>] | summary [<sessionId>] | add <sessionId> <event> [<json>] [--json]\n catalog list [--json]\n catalog validate [--json]\n catalog capability <id> [--json]\n project inspect [--json]\n skill install <skill-dir> [--root <repo>] [--scope project|user] [--dry-run] [--json]\n lint:skills [--root <repo>]\n sync:dry-run [--root <repo>]\n jira:dry-run [--root <repo>]\n jira render <plan-file> [--root <repo>] [--json]\n jira apply <ticket-key-or-plan-file> --comment|--update|--transition|--link [--dry-run] [--json]\n`;
95
+ return `oh-my-copilot\n\nRun \`omp\` with no arguments to launch copilot (permissions bypass OFF).\nUse \`omp help\` to show this list.\n\nCommands:\n (no args) launch copilot (bypass OFF by default)\n version [--json]\n update (self-update: npm i -g @damian87/omp@latest)\n list [--json]\n setup [--dry-run] [--scope project|user] [--plugin-root <dir>] [--json]\n doctor [--json] [--copilot-bin <path>] [--skip-copilot] [--hooks]\n cost [--json] [--session <id>] [--days <n>]\n launch -- <args...>\n --madmax [args...] (bare-flag launch with permissions bypass; alias of --yolo)\n team <N:role> "<task>" [--name <name>] [--json]\n team status <name> [--json]\n team shutdown <name> [--json]\n team api claim-task --input '<json>' [--json]\n team api transition-task-status --input '<json>' [--json]\n team api send-message --input '<json>' [--json]\n team api broadcast --input '<json>' [--json]\n team api mailbox-list --input '<json>' [--json]\n team api mailbox-mark-delivered --input '<json>' [--json]\n council "<question>" [--models a,b,c|m:role:weight] [--context <text|@file>] [--rubric <text|@file>] [--synth <model>] [--probe] [--timeout <ms>] [--synth-timeout <ms>] [--min-survivors <n>] [--max-concurrency <n>] [--tmp-dir <dir>] [--json]\n comms status [--session <name>] [--json] (is copilot on + online? auto-discovers session)\n comms send --text "<prompt>" [--force] [--session <name>] [--json]\n comms recv [--wait] [--lines <n>] [--timeout <ms>] [--session <name>] [--json]\n comms ask --text "<prompt>" [--force] [--lines <n>] [--timeout <ms>] [--session <name>] [--json]\n gateway serve [--only <name>[,<name>]] (run all configured connectors; today: slack)\n gateway status [--json] [--only <name>[,...]] (per-connector readiness; no sockets opened)\n gateway doctor [--json] [--only <name>[,...]] (alias for 'gateway status')\n gateway notify --text "<msg>" [--target slack:C\\|D\\|G\\|U... [:thread_ts]] [--thread-ts <ts>] [--json]\n (one-shot outbound Slack post; falls back to SLACK_HOME_CHANNEL)\n slack serve (deprecated alias for 'gateway serve --only slack')\n slack doctor [--json] (deprecated alias for 'gateway status --only slack')\n env init [--force] (interactive: write ~/.omp/.env with Slack tokens + optional SLACK_HOME_CHANNEL)\n non-interactive: set OMP_INIT_BOT_TOKEN/OMP_INIT_APP_TOKEN/OMP_INIT_HOME_CHANNEL\n (env vars preferred over --bot-token/--app-token/--home-channel flags)\n (--session is optional when exactly one omp-<digits> tmux session is running)\n${registeredCommandHelpLines().join("\n")}\n ralph start "<task>" [--max-iterations <n>] [--session-id <id>] [--json]\n ralph status [--json]\n ralph tick [--json]\n ralph cancel [--json]\n ultrawork start "<objective>" [--task-count <n>] [--summary <s>] [--json]\n ultrawork status [--json]\n ultrawork cancel [--json]\n ultraqa start "<goal>" [--max-cycles <n>] [--json]\n ultraqa cycle pass|fail|pending [--json]\n ultraqa status [--json]\n ultraqa cancel [--json]\n schedule add --id <id> --cron "<expr>" --prompt "<text>" [--bin copilot] [--model <m>] [--cwd <dir>] [--timeout <ms>] [--max-runs <n>] [--ttl-hours <h>] [--allow-all-tools] [--notify-target slack:<ID>] [--notify-desktop] [--notify-open-omp] [--dry-run] [--json]
96
+ (--notify-desktop: native OS notification on completion [macOS uses osascript]; --notify-open-omp: click opens an omp session in the state root — needs OMP_NOTIFY_USE_TERMINAL_NOTIFIER=1 + terminal-notifier on macOS)\n schedule list [--json]\n schedule status <id> [--json]\n schedule open <id> [--tmux] [--json] (show this id's latest status + full output; --tmux instead opens an interactive omp session in the project — recent runs show via the startup banner)\n schedule run-now <id> [--json]\n schedule remove <id> [--json]\n goal set "<objective>" [--json]\n goal read [--json]\n memory sync [--json] (render goal+directives into copilot-instructions.md)\n config get [--json] | config set memory-mode on|off | config set memory-review-model <slug> | config set memory-review-min-messages <n> [--global]\n (--global writes ~/.omp/config.json; applies to every project. project .omp/config.json overrides it)\n memory-review --session <uuid|latest> [--model <slug>] [--json] (cheap-model end-of-session review; opt-in via memory-mode)\n daily-log set-goal "<text>" [--json]\n daily-log add "<text>" [--json]\n daily-log read [--days <n>] [--json]\n daily-log prune [--keep-days <n>] [--json]\n state write <key> <val> [--ttl <s>] | read|delete|status <key> | list | cleanup [--json]\n project-memory read [<id>] | index | add-note "<title>" [--body "<text>"] | add-directive "<rule>" | prune-notes --keep <n>|--older-than <days> [--json]\n trace timeline [<sessionId>] [--limit <n>] | summary [<sessionId>] | add <sessionId> <event> [<json>] [--json]\n catalog list [--json]\n catalog validate [--json]\n catalog capability <id> [--json]\n project inspect [--json]\n skill install <skill-dir> [--root <repo>] [--scope project|user] [--dry-run] [--json]\n lint:skills [--root <repo>]\n sync:dry-run [--root <repo>]\n jira:dry-run [--root <repo>]\n jira render <plan-file> [--root <repo>] [--json]\n jira apply <ticket-key-or-plan-file> --comment|--update|--transition|--link [--dry-run] [--json]\n`;
30
97
  }
31
98
  async function resolveExistingInputPath(value) {
32
99
  const { existsSync } = await import("node:fs");
@@ -79,15 +146,40 @@ export async function runCli(argv = process.argv.slice(2)) {
79
146
  const versionCheckUrl = pathToFileURL(join(packageRootFromImportMeta(import.meta.url), "scripts", "lib", "version-check.mjs")).href;
80
147
  const { checkForUpdate, formatUpdateNotice } = (await import(versionCheckUrl));
81
148
  const cwd = flagValue(argv, "--root") ?? process.cwd();
82
- const update = await checkForUpdate({ stateDir: join(ompRoot(cwd), ".omp", "state") });
149
+ const stateDir = join(ompRoot(cwd), ".omp", "state");
150
+ const skipCheck = Boolean(process.env.OMP_NO_UPDATE_CHECK?.trim());
83
151
  if (json) {
152
+ const update = skipCheck ? null : await checkForUpdate({ stateDir });
84
153
  return { ok: true, output: { ...info, update } };
85
154
  }
155
+ // Interactive TTY: print the version, then offer to self-update.
156
+ if (isInteractive(json)) {
157
+ console.log(formatVersionInfo(info));
158
+ const { maybePromptUpdate } = await import("./copilot/update-prompt.js");
159
+ await withUpdateIO((io) => maybePromptUpdate({ cwd, io, interactive: true, importMetaUrl: import.meta.url }));
160
+ return { ok: true };
161
+ }
162
+ // Non-interactive: keep the passive notice behavior (honoring the opt-out).
163
+ const update = skipCheck ? null : await checkForUpdate({ stateDir });
86
164
  const message = update
87
165
  ? `${formatVersionInfo(info)}\n\n${formatUpdateNotice(update.current, update.latest)}`
88
166
  : formatVersionInfo(info);
89
167
  return { ok: true, message };
90
168
  }
169
+ if (group === "update") {
170
+ const { runSelfUpdate, updateCopilotPlugin } = await import("./copilot/update-prompt.js");
171
+ const ok = await runSelfUpdate();
172
+ if (!ok) {
173
+ return { ok: false, message: "Update failed; run manually: npm i -g @damian87/omp@latest" };
174
+ }
175
+ const plugin = await updateCopilotPlugin();
176
+ const pluginMsg = plugin === "updated"
177
+ ? "Copilot plugin updated."
178
+ : plugin === "failed"
179
+ ? "Copilot plugin update failed; run: copilot plugin update oh-my-copilot"
180
+ : "Copilot plugin: skipped (copilot CLI not found).";
181
+ return { ok: true, message: `omp CLI updated.\n${pluginMsg}\nre-run \`omp\`.` };
182
+ }
91
183
  // Auto-load ~/.omp/.env so subcommands that read process.env (slack tokens,
92
184
  // OMP_*, COPILOT_TMUX_SESSION, etc.) work from any cwd without `source .env`.
93
185
  // Shell exports take precedence — see src/env/dotenv.ts.
@@ -98,6 +190,27 @@ export async function runCli(argv = process.argv.slice(2)) {
98
190
  if (!group || BARE_LAUNCH_FLAGS.has(group)) {
99
191
  const { launchCopilot } = await import("./copilot/launch.js");
100
192
  const launchCwd = flagValue(argv, "--root") ?? process.cwd();
193
+ // Truly-bare `omp` in a TTY: offer to self-update, then show the
194
+ // first-run hint, before handing the terminal to copilot. `--madmax`/
195
+ // `--yolo` and any non-TTY launch are never blocked.
196
+ if (!group && isInteractive(json)) {
197
+ const { maybePromptUpdate, maybeWelcome } = await import("./copilot/update-prompt.js");
198
+ const outcome = await withUpdateIO((io) => maybePromptUpdate({ cwd: launchCwd, io, interactive: true, importMetaUrl: import.meta.url }));
199
+ // The running process still holds the OLD code in memory. After a
200
+ // successful self-update, re-exec the freshly-installed omp so this
201
+ // launch uses the new version instead of stale wrapper logic.
202
+ if (outcome.updated) {
203
+ const reexec = await reexecAfterUpdate(argv);
204
+ if (reexec)
205
+ return reexec;
206
+ // Fall through to a normal launch if re-exec wasn't possible.
207
+ }
208
+ maybeWelcome({
209
+ cwd: launchCwd,
210
+ io: { print: (line) => console.log(line), ask: async () => undefined },
211
+ interactive: true,
212
+ });
213
+ }
101
214
  const beforeSessions = await snapshotSessionsForReview();
102
215
  const result = await launchCopilot({
103
216
  args: argv,
@@ -1363,6 +1476,8 @@ async function handleScheduleCommand(argv, json) {
1363
1476
  allowAllTools: hasFlag(argv, "--allow-all-tools"),
1364
1477
  dryRun: hasFlag(argv, "--dry-run"),
1365
1478
  notifyTarget,
1479
+ notifyDesktop: hasFlag(argv, "--notify-desktop"),
1480
+ notifyOpenOmp: hasFlag(argv, "--notify-open-omp"),
1366
1481
  });
1367
1482
  return json
1368
1483
  ? { ok: result.ok, exitCode: result.ok ? 0 : 1, output: result }
@@ -1399,10 +1514,39 @@ async function handleScheduleCommand(argv, json) {
1399
1514
  ? { ok: result.ok, exitCode: result.ok ? 0 : 1, output: result }
1400
1515
  : { ok: result.ok, exitCode: result.ok ? 0 : 1, message: result.message };
1401
1516
  }
1517
+ if (command === "open" && targetId) {
1518
+ const r = mod.openScheduleResult(cwd, targetId);
1519
+ if (!r.ok)
1520
+ return { ok: false, exitCode: 1, output: json ? r : undefined, message: r.error };
1521
+ // --tmux: drop into an interactive omp session (auto-wrapped in tmux by
1522
+ // launchCopilot) rooted at the project. The SessionStart [SCHEDULE RESULTS]
1523
+ // banner surfaces RECENT unseen results across all jobs (not pinned to <id>,
1524
+ // and cursor-gated) — for this id's exact output use `schedule open <id>`
1525
+ // without --tmux. We resolve <id> first only to fail fast on a bad id.
1526
+ if (hasFlag(argv, "--tmux")) {
1527
+ const { launchCopilot } = await import("./copilot/launch.js");
1528
+ const result = await launchCopilot({ args: [], cwd, env: { ...process.env, OMP_FORCE_TMUX_WRAP: "1" } });
1529
+ return {
1530
+ ok: result.ok,
1531
+ exitCode: result.exitCode,
1532
+ message: result.ok ? undefined : `failed to launch omp in tmux (exit ${result.exitCode})`,
1533
+ };
1534
+ }
1535
+ if (json)
1536
+ return { ok: true, output: r };
1537
+ const j = r.job;
1538
+ const header = `${j.id} — ${j.lastStatus ?? "(never run)"} @ ${j.lastRunAt ?? "-"}\n${j.lastSummary ?? ""}`.trim();
1539
+ const body = r.logContent
1540
+ ? `\n\n--- full output (${j.lastLogPath}) ---\n${r.logContent}`
1541
+ : j.lastRunAt
1542
+ ? `\n(log not found: ${j.lastLogPath ?? "n/a"})`
1543
+ : "\n(no run recorded yet)";
1544
+ return { ok: true, message: header + body };
1545
+ }
1402
1546
  return {
1403
1547
  ok: false,
1404
1548
  exitCode: 1,
1405
- message: 'Unknown schedule subcommand. Try: schedule add --id <id> --cron "<expr>" --prompt "<text>" | list | status <id> | remove <id> | run-now <id>',
1549
+ message: 'Unknown schedule subcommand. Try: schedule add --id <id> --cron "<expr>" --prompt "<text>" | list | status <id> | open <id> | remove <id> | run-now <id>',
1406
1550
  };
1407
1551
  }
1408
1552
  function isEntrypoint() {