@damian87/omp 0.12.0 → 0.14.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.
Files changed (94) hide show
  1. package/.github/skills/schedule/SKILL.md +27 -2
  2. package/.github/skills/verify-byok/SKILL.md +50 -0
  3. package/README.md +88 -4
  4. package/catalog/capabilities.json +23 -0
  5. package/catalog/skills-general.json +25 -0
  6. package/dist/src/cli.js +158 -5
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/commands/comms.d.ts +2 -0
  9. package/dist/src/commands/comms.js +110 -0
  10. package/dist/src/commands/comms.js.map +1 -0
  11. package/dist/src/commands/council.d.ts +2 -0
  12. package/dist/src/commands/council.js +77 -0
  13. package/dist/src/commands/council.js.map +1 -0
  14. package/dist/src/commands/env.d.ts +2 -0
  15. package/dist/src/commands/env.js +95 -0
  16. package/dist/src/commands/env.js.map +1 -0
  17. package/dist/src/commands/gateway.d.ts +3 -0
  18. package/dist/src/commands/gateway.js +129 -0
  19. package/dist/src/commands/gateway.js.map +1 -0
  20. package/dist/src/commands/memory.d.ts +7 -0
  21. package/dist/src/commands/memory.js +202 -0
  22. package/dist/src/commands/memory.js.map +1 -0
  23. package/dist/src/commands/mode.d.ts +4 -0
  24. package/dist/src/commands/mode.js +119 -0
  25. package/dist/src/commands/mode.js.map +1 -0
  26. package/dist/src/commands/schedule.d.ts +2 -0
  27. package/dist/src/commands/schedule.js +91 -0
  28. package/dist/src/commands/schedule.js.map +1 -0
  29. package/dist/src/commands/team.d.ts +2 -0
  30. package/dist/src/commands/team.js +146 -0
  31. package/dist/src/commands/team.js.map +1 -0
  32. package/dist/src/commands/utils.d.ts +13 -0
  33. package/dist/src/commands/utils.js +68 -0
  34. package/dist/src/commands/utils.js.map +1 -0
  35. package/dist/src/gateway/desktop-notify.d.ts +56 -0
  36. package/dist/src/gateway/desktop-notify.js +183 -0
  37. package/dist/src/gateway/desktop-notify.js.map +1 -0
  38. package/dist/src/goal.js +6 -8
  39. package/dist/src/goal.js.map +1 -1
  40. package/dist/src/instructions-memory.js +26 -3
  41. package/dist/src/instructions-memory.js.map +1 -1
  42. package/dist/src/memory-review/apply.d.ts +7 -0
  43. package/dist/src/memory-review/apply.js +75 -0
  44. package/dist/src/memory-review/apply.js.map +1 -0
  45. package/dist/src/memory-review/config.d.ts +22 -0
  46. package/dist/src/memory-review/config.js +54 -0
  47. package/dist/src/memory-review/config.js.map +1 -0
  48. package/dist/src/memory-review/guard.d.ts +5 -0
  49. package/dist/src/memory-review/guard.js +37 -0
  50. package/dist/src/memory-review/guard.js.map +1 -0
  51. package/dist/src/memory-review/index.d.ts +17 -0
  52. package/dist/src/memory-review/index.js +87 -0
  53. package/dist/src/memory-review/index.js.map +1 -0
  54. package/dist/src/memory-review/prompt.d.ts +18 -0
  55. package/dist/src/memory-review/prompt.js +89 -0
  56. package/dist/src/memory-review/prompt.js.map +1 -0
  57. package/dist/src/memory-review/spawn.d.ts +2 -0
  58. package/dist/src/memory-review/spawn.js +51 -0
  59. package/dist/src/memory-review/spawn.js.map +1 -0
  60. package/dist/src/memory-review/transcript.d.ts +24 -0
  61. package/dist/src/memory-review/transcript.js +212 -0
  62. package/dist/src/memory-review/transcript.js.map +1 -0
  63. package/dist/src/memory-review/trigger.d.ts +21 -0
  64. package/dist/src/memory-review/trigger.js +27 -0
  65. package/dist/src/memory-review/trigger.js.map +1 -0
  66. package/dist/src/project-memory.d.ts +9 -0
  67. package/dist/src/project-memory.js +72 -1
  68. package/dist/src/project-memory.js.map +1 -1
  69. package/dist/src/schedule/commands.d.ts +13 -0
  70. package/dist/src/schedule/commands.js +24 -1
  71. package/dist/src/schedule/commands.js.map +1 -1
  72. package/dist/src/schedule/deep-link.d.ts +18 -0
  73. package/dist/src/schedule/deep-link.js +41 -0
  74. package/dist/src/schedule/deep-link.js.map +1 -0
  75. package/dist/src/schedule/runner.d.ts +10 -0
  76. package/dist/src/schedule/runner.js +36 -0
  77. package/dist/src/schedule/runner.js.map +1 -1
  78. package/dist/src/schedule/types.d.ts +16 -0
  79. package/dist/src/state.js +25 -37
  80. package/dist/src/state.js.map +1 -1
  81. package/dist/src/utils/fs.d.ts +14 -0
  82. package/dist/src/utils/fs.js +32 -0
  83. package/dist/src/utils/fs.js.map +1 -0
  84. package/dist/src/utils/paths.d.ts +14 -0
  85. package/dist/src/utils/paths.js +21 -0
  86. package/dist/src/utils/paths.js.map +1 -0
  87. package/docs/memory-mode.md +94 -0
  88. package/docs/research/2026-06-22-schedule-desktop-notifications.md +193 -0
  89. package/package.json +4 -2
  90. package/plugin.json +1 -1
  91. package/scripts/lib/memory-review-trigger.mjs +59 -0
  92. package/scripts/lib/pending-directives.mjs +36 -0
  93. package/scripts/session-end.mjs +8 -0
  94. package/scripts/session-start.mjs +4 -0
@@ -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
@@ -60,6 +60,82 @@ That's it.
60
60
 
61
61
  ---
62
62
 
63
+ ## Architecture
64
+
65
+ 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.
66
+
67
+ ```mermaid
68
+ flowchart TB
69
+ subgraph Surfaces["① Where you drive it"]
70
+ SH["Shell CLI<br/><code>omp</code> / <code>omp --madmax</code>"]
71
+ IDE["In-session <code>/skills</code><br/>(Copilot plugin)"]
72
+ GW["Gateway<br/>Slack / messaging"]
73
+ CRON["Cron<br/><code>omp schedule</code>"]
74
+ end
75
+
76
+ subgraph Copilot["② GitHub Copilot CLI (wrapped)"]
77
+ CP["copilot session"]
78
+ HK["Lifecycle hooks<br/>sessionStart · sessionEnd · agentStop<br/>pre/postToolUse · userPromptSubmitted"]
79
+ PS["Plugin skills + agents<br/>ralph · ralplan · team · council<br/>code-review · tdd · research · …"]
80
+ end
81
+
82
+ subgraph Orchestration["③ Orchestration modes"]
83
+ direction LR
84
+ RALPH["ralph<br/>PRD verify/fix loop"]
85
+ UW["ultrawork<br/>parallel fan-out"]
86
+ UQA["ultraqa<br/>QA cycles"]
87
+ AP["autopilot"]
88
+ TEAM["team<br/>tmux multi-agent"]
89
+ COUNCIL["council<br/>weighted consensus"]
90
+ end
91
+
92
+ subgraph Learning["④ Memory & learning loop"]
93
+ direction LR
94
+ MR["memory-review<br/>cheap model, end-of-session"]
95
+ PM["project-memory<br/>directives + notes"]
96
+ SE["self-evolve<br/>skill drafts"]
97
+ DL["daily-log"]
98
+ IM["instructions-memory<br/>→ copilot-instructions.md"]
99
+ end
100
+
101
+ subgraph Store["⑤ Config & state"]
102
+ direction LR
103
+ G["~/.omp<br/>global config + .env"]
104
+ P[".omp/<br/>project config · memory<br/>cost · trace · mode-state"]
105
+ SST["~/.copilot/session-state<br/>events.jsonl transcripts"]
106
+ end
107
+
108
+ SH --> CP
109
+ GW --> CP
110
+ CRON --> CP
111
+ IDE --> PS
112
+ CP --> HK
113
+ PS --> Orchestration
114
+ SH --> Orchestration
115
+ HK -- "agentStop drives the loop" --> Orchestration
116
+ Orchestration --> P
117
+
118
+ HK -- "sessionEnd (detached)" --> MR
119
+ SH -- "wrapper fallback (headless -p)" --> MR
120
+ MR -- reads transcript --> SST
121
+ MR -- "facts" --> PM
122
+ MR -- "procedures" --> SE
123
+ MR -- "rules (gated)" --> PM
124
+ PM --> IM
125
+ DL --> IM
126
+ IM -- "injected next session" --> CP
127
+
128
+ G -. config .-> MR
129
+ P -. config .-> MR
130
+ Orchestration -. cost/trace .-> P
131
+ ```
132
+
133
+ **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.
134
+
135
+ > 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).
136
+
137
+ ---
138
+
63
139
  ## Features
64
140
 
65
141
  ### Orchestration Modes
@@ -205,9 +281,10 @@ omp gateway notify --text "<msg>" [--target slack:C…|G…|D…|U… [:thread_t
205
281
  omp slack serve # deprecated alias of `gateway serve --only slack`
206
282
  omp slack doctor [--json] # deprecated alias of `gateway status --only slack`
207
283
  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]
284
+ 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
285
  omp schedule list # registered jobs + OS-install status
210
286
  omp schedule status <id> # last run + result summary
287
+ omp schedule open <id> [--tmux] # print this id's latest status + full output (--tmux: open an omp session)
211
288
  omp schedule run-now <id> # trigger one run immediately
212
289
  omp schedule remove <id> # uninstall the OS entry + delete the job
213
290
  omp goal set "<objective>" | read [--json]
@@ -227,6 +304,8 @@ Environment overrides:
227
304
  - `OMP_COPILOT_BIN` — alternate `copilot` binary
228
305
  - `OMP_BIN` — absolute path to the `omp` wrapper written into OS-scheduler entries (overrides `which omp`)
229
306
  - `OMP_SKIP_USER_ENV` — when `1`, skip auto-loading `~/.omp/.env` (useful for hermetic CI runs)
307
+ - `OMP_DISABLE_DESKTOP_NOTIFY` — when set, suppress all `--notify-desktop` notifications
308
+ - `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
309
 
231
310
  **Scheduled jobs** register a durable per-job entry with the OS scheduler (macOS launchd,
232
311
  Linux systemd-user timers, or a managed `crontab` block as a cross-platform fallback) that
@@ -234,8 +313,13 @@ invokes `omp schedule run --id <id>` on the cron schedule. Each tick spawns a fr
234
313
  session; overlapping runs are locked out and every run is killed at its `--timeout`
235
314
  (default 5 min). Jobs default to **read-only** (`--allow-all-tools` is opt-in and prints a
236
315
  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
316
+ run results are surfaced automatically at the start of new Copilot sessions, and
317
+ `omp schedule open <id>` prints any job's latest status + full captured output on demand.
318
+ Opt into end-of-run **notifications** (default off; failures never affect the job): `--notify-target slack:…`
319
+ posts to Slack, and `--notify-desktop` fires a native notification (job id + status + one-line
320
+ summary) — on macOS via `osascript` (display-only; for a clickable notification that opens omp,
321
+ set `OMP_NOTIFY_USE_TERMINAL_NOTIFIER=1` with a system `terminal-notifier` and add `--notify-open-omp`).
322
+ Always use `omp schedule remove`, never delete `.omp/state/schedule/` by hand, so the OS entry is
239
323
  uninstalled cleanly.
240
324
 
241
325
  ### Chat bridge: drive Copilot from Slack
@@ -269,7 +353,7 @@ omp grows in vertical slices. Items aren't pinned to specific semver versions
269
353
 
270
354
  ### Already shipped
271
355
 
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.
356
+ - **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
357
  - **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
358
  - **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
359
  - **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
@@ -26,7 +26,8 @@ function printResult(result, json) {
26
26
  }
27
27
  }
28
28
  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 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>" [--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`;
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>] [--notify-desktop] [--notify-open-omp] [--dry-run] [--json]
30
+ (--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
31
  }
31
32
  async function resolveExistingInputPath(value) {
32
33
  const { existsSync } = await import("node:fs");
@@ -40,6 +41,33 @@ async function resolveExistingInputPath(value) {
40
41
  return direct;
41
42
  }
42
43
  const BARE_LAUNCH_FLAGS = new Set(["--madmax", "--yolo"]);
44
+ // Snapshot existing Copilot session dirs BEFORE launch, so after a headless run
45
+ // we can identify the exact session it created (instead of guessing "latest").
46
+ async function snapshotSessionsForReview() {
47
+ try {
48
+ const { listSessionIds } = await import("./memory-review/transcript.js");
49
+ return listSessionIds();
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
55
+ // Headless `copilot -p` skips hooks, so when omp launched copilot in headless
56
+ // mode we detach the end-of-session memory review here. We review the session
57
+ // that appeared since `before` — never a guessed "latest" — and skip if none
58
+ // is identifiable. Best-effort: a failure here must never affect the launch.
59
+ async function maybeTriggerHeadlessReview(argv, cwd, before) {
60
+ try {
61
+ const { triggerHeadlessReview } = await import("./memory-review/trigger.js");
62
+ const { newestSessionSince } = await import("./memory-review/transcript.js");
63
+ const cliPath = join(packageRootFromImportMeta(import.meta.url), "dist", "src", "cli.js");
64
+ const sessionId = newestSessionSince(before) ?? "";
65
+ triggerHeadlessReview({ cwd, argv, cliPath, sessionId });
66
+ }
67
+ catch {
68
+ // never fail a launch on the review trigger
69
+ }
70
+ }
43
71
  export async function runCli(argv = process.argv.slice(2)) {
44
72
  const [group, command, value] = argv;
45
73
  const json = hasFlag(argv, "--json");
@@ -70,11 +98,14 @@ export async function runCli(argv = process.argv.slice(2)) {
70
98
  // case argv is empty, so normalizeCopilotLaunchArgs emits no --yolo.
71
99
  if (!group || BARE_LAUNCH_FLAGS.has(group)) {
72
100
  const { launchCopilot } = await import("./copilot/launch.js");
101
+ const launchCwd = flagValue(argv, "--root") ?? process.cwd();
102
+ const beforeSessions = await snapshotSessionsForReview();
73
103
  const result = await launchCopilot({
74
104
  args: argv,
75
105
  bin: flagValue(argv, "--bin"),
76
- cwd: flagValue(argv, "--root") ?? process.cwd(),
106
+ cwd: launchCwd,
77
107
  });
108
+ await maybeTriggerHeadlessReview(argv, launchCwd, beforeSessions);
78
109
  return json
79
110
  ? { ok: result.ok, exitCode: result.exitCode, output: result }
80
111
  : {
@@ -125,11 +156,14 @@ export async function runCli(argv = process.argv.slice(2)) {
125
156
  const dashIndex = argv.indexOf("--");
126
157
  const passthrough = dashIndex >= 0 ? argv.slice(dashIndex + 1) : argv.slice(1);
127
158
  const { launchCopilot } = await import("./copilot/launch.js");
159
+ const launchCwd = flagValue(argv, "--root") ?? process.cwd();
160
+ const beforeSessions = await snapshotSessionsForReview();
128
161
  const result = await launchCopilot({
129
162
  args: passthrough,
130
163
  bin: flagValue(argv, "--bin"),
131
- cwd: flagValue(argv, "--root") ?? process.cwd(),
164
+ cwd: launchCwd,
132
165
  });
166
+ await maybeTriggerHeadlessReview(passthrough, launchCwd, beforeSessions);
133
167
  return json
134
168
  ? { ok: result.ok, exitCode: result.exitCode, output: result }
135
169
  : {
@@ -201,6 +235,80 @@ export async function runCli(argv = process.argv.slice(2)) {
201
235
  }
202
236
  return { ok: false, exitCode: 1, message: "Unknown memory subcommand. Try: memory sync" };
203
237
  }
238
+ if (group === "config") {
239
+ const { readMemoryConfig, setMemoryConfigValue } = await import("./memory-review/config.js");
240
+ const cwd = flagValue(argv, "--root") ?? process.cwd();
241
+ // OMP_HOME_OVERRIDE relocates the global ~/.omp config dir (test seam; also
242
+ // lets users point at a custom home). Undefined => os.homedir() default.
243
+ const homeDir = process.env.OMP_HOME_OVERRIDE || undefined;
244
+ // `--global` writes to ~/.omp/config.json so the setting applies everywhere.
245
+ const scope = hasFlag(argv, "--global") ? "global" : "project";
246
+ if (command === "get" || command === undefined) {
247
+ const cfg = readMemoryConfig(cwd, { homeDir });
248
+ return json
249
+ ? { ok: true, output: cfg }
250
+ : {
251
+ ok: true,
252
+ message: `memory-mode=${cfg.memoryMode}\nmemory-review-model=${cfg.memoryReviewModel}\nmemory-review-min-messages=${cfg.memoryReviewMinMessages}`,
253
+ };
254
+ }
255
+ if (command === "set") {
256
+ const setVal = argv[3];
257
+ const where = scope === "global" ? " (global ~/.omp)" : "";
258
+ if (value === "memory-mode") {
259
+ if (setVal !== "on" && setVal !== "off") {
260
+ return { ok: false, exitCode: 1, message: "usage: omp config set memory-mode on|off [--global]" };
261
+ }
262
+ setMemoryConfigValue(cwd, "memoryMode", setVal, { scope, homeDir });
263
+ return json ? { ok: true, output: { memoryMode: setVal, scope } } : { ok: true, message: `memory-mode=${setVal}${where}` };
264
+ }
265
+ if (value === "memory-review-model") {
266
+ if (!setVal || setVal.startsWith("-")) {
267
+ return { ok: false, exitCode: 1, message: "usage: omp config set memory-review-model <slug> [--global]" };
268
+ }
269
+ setMemoryConfigValue(cwd, "memoryReviewModel", setVal, { scope, homeDir });
270
+ return json ? { ok: true, output: { memoryReviewModel: setVal, scope } } : { ok: true, message: `memory-review-model=${setVal}${where}` };
271
+ }
272
+ if (value === "memory-review-min-messages") {
273
+ const n = Number(setVal);
274
+ if (!Number.isFinite(n) || n < 0) {
275
+ return { ok: false, exitCode: 1, message: "usage: omp config set memory-review-min-messages <non-negative integer> [--global]" };
276
+ }
277
+ setMemoryConfigValue(cwd, "memoryReviewMinMessages", String(Math.floor(n)), { scope, homeDir });
278
+ return json ? { ok: true, output: { memoryReviewMinMessages: Math.floor(n), scope } } : { ok: true, message: `memory-review-min-messages=${Math.floor(n)}${where}` };
279
+ }
280
+ return { ok: false, exitCode: 1, message: "Unknown config key. Try: memory-mode | memory-review-model | memory-review-min-messages" };
281
+ }
282
+ return { ok: false, exitCode: 1, message: "Unknown config subcommand. Try: config get | config set <key> <value>" };
283
+ }
284
+ if (group === "memory-review") {
285
+ const { runMemoryReview } = await import("./memory-review/index.js");
286
+ const { createReviewSpawn } = await import("./memory-review/spawn.js");
287
+ const { isValidSessionId, latestSessionId } = await import("./memory-review/transcript.js");
288
+ const cwd = flagValue(argv, "--root") ?? process.cwd();
289
+ let sessionId = flagValue(argv, "--session") ?? "";
290
+ if (!sessionId || sessionId === "latest") {
291
+ sessionId = latestSessionId() ?? "";
292
+ }
293
+ if (!sessionId) {
294
+ return { ok: false, exitCode: 1, message: "usage: omp memory-review --session <uuid|latest>" };
295
+ }
296
+ if (!isValidSessionId(sessionId)) {
297
+ return { ok: false, exitCode: 1, message: "invalid --session id" };
298
+ }
299
+ const res = await runMemoryReview({
300
+ cwd,
301
+ sessionId,
302
+ spawn: createReviewSpawn(flagValue(argv, "--bin")),
303
+ model: flagValue(argv, "--model"),
304
+ });
305
+ return json
306
+ ? { ok: true, output: res }
307
+ : {
308
+ ok: true,
309
+ message: res.ran ? `memory-review ran: ${JSON.stringify(res.summary)}` : `memory-review skipped: ${res.reason}`,
310
+ };
311
+ }
204
312
  if (group === "daily-log") {
205
313
  const { setDailyGoal, addLogEntry, readDailyLog, pruneDailyLog } = await import("./daily-log.js");
206
314
  const cwd = flagValue(argv, "--root") ?? process.cwd();
@@ -313,6 +421,20 @@ export async function runCli(argv = process.argv.slice(2)) {
313
421
  if (command === "index") {
314
422
  return { ok: true, output: { notes: pm.noteIndex(cwd) } };
315
423
  }
424
+ if (command === "prune-notes") {
425
+ const keepRaw = flagValue(argv, "--keep");
426
+ const olderRaw = flagValue(argv, "--older-than");
427
+ const keep = keepRaw !== undefined && Number.isFinite(Number(keepRaw)) ? Number(keepRaw) : undefined;
428
+ const olderThanDays = olderRaw !== undefined && Number.isFinite(Number(olderRaw)) ? Number(olderRaw) : undefined;
429
+ if (keep === undefined && olderThanDays === undefined) {
430
+ return { ok: false, exitCode: 1, message: "usage: omp project-memory prune-notes --keep <n> | --older-than <days>" };
431
+ }
432
+ const removed = pm.pruneNotes(cwd, { keep, olderThanDays });
433
+ syncInstructionsMemory(cwd); // refresh the managed block after pruning
434
+ return json
435
+ ? { ok: true, output: { removed } }
436
+ : { ok: true, message: `pruned ${removed.length} note${removed.length === 1 ? "" : "s"}` };
437
+ }
316
438
  if (command === "read" || command === undefined) {
317
439
  // `read <id>` loads one note's body on demand; bare `read` returns the
318
440
  // bounded summary (directives + note index — never note bodies).
@@ -327,7 +449,7 @@ export async function runCli(argv = process.argv.slice(2)) {
327
449
  return {
328
450
  ok: false,
329
451
  exitCode: 1,
330
- message: 'Unknown project-memory subcommand. Try: project-memory read [<id>] | index | add-note "<title>" [--body "<text>"] | add-directive "<rule>"',
452
+ message: 'Unknown project-memory subcommand. Try: project-memory read [<id>] | index | add-note "<title>" [--body "<text>"] | add-directive "<rule>" | prune-notes --keep <n>|--older-than <days>',
331
453
  };
332
454
  }
333
455
  if (group === "trace") {
@@ -1242,6 +1364,8 @@ async function handleScheduleCommand(argv, json) {
1242
1364
  allowAllTools: hasFlag(argv, "--allow-all-tools"),
1243
1365
  dryRun: hasFlag(argv, "--dry-run"),
1244
1366
  notifyTarget,
1367
+ notifyDesktop: hasFlag(argv, "--notify-desktop"),
1368
+ notifyOpenOmp: hasFlag(argv, "--notify-open-omp"),
1245
1369
  });
1246
1370
  return json
1247
1371
  ? { ok: result.ok, exitCode: result.ok ? 0 : 1, output: result }
@@ -1278,10 +1402,39 @@ async function handleScheduleCommand(argv, json) {
1278
1402
  ? { ok: result.ok, exitCode: result.ok ? 0 : 1, output: result }
1279
1403
  : { ok: result.ok, exitCode: result.ok ? 0 : 1, message: result.message };
1280
1404
  }
1405
+ if (command === "open" && targetId) {
1406
+ const r = mod.openScheduleResult(cwd, targetId);
1407
+ if (!r.ok)
1408
+ return { ok: false, exitCode: 1, output: json ? r : undefined, message: r.error };
1409
+ // --tmux: drop into an interactive omp session (auto-wrapped in tmux by
1410
+ // launchCopilot) rooted at the project. The SessionStart [SCHEDULE RESULTS]
1411
+ // banner surfaces RECENT unseen results across all jobs (not pinned to <id>,
1412
+ // and cursor-gated) — for this id's exact output use `schedule open <id>`
1413
+ // without --tmux. We resolve <id> first only to fail fast on a bad id.
1414
+ if (hasFlag(argv, "--tmux")) {
1415
+ const { launchCopilot } = await import("./copilot/launch.js");
1416
+ const result = await launchCopilot({ args: [], cwd, env: { ...process.env, OMP_FORCE_TMUX_WRAP: "1" } });
1417
+ return {
1418
+ ok: result.ok,
1419
+ exitCode: result.exitCode,
1420
+ message: result.ok ? undefined : `failed to launch omp in tmux (exit ${result.exitCode})`,
1421
+ };
1422
+ }
1423
+ if (json)
1424
+ return { ok: true, output: r };
1425
+ const j = r.job;
1426
+ const header = `${j.id} — ${j.lastStatus ?? "(never run)"} @ ${j.lastRunAt ?? "-"}\n${j.lastSummary ?? ""}`.trim();
1427
+ const body = r.logContent
1428
+ ? `\n\n--- full output (${j.lastLogPath}) ---\n${r.logContent}`
1429
+ : j.lastRunAt
1430
+ ? `\n(log not found: ${j.lastLogPath ?? "n/a"})`
1431
+ : "\n(no run recorded yet)";
1432
+ return { ok: true, message: header + body };
1433
+ }
1281
1434
  return {
1282
1435
  ok: false,
1283
1436
  exitCode: 1,
1284
- message: 'Unknown schedule subcommand. Try: schedule add --id <id> --cron "<expr>" --prompt "<text>" | list | status <id> | remove <id> | run-now <id>',
1437
+ message: 'Unknown schedule subcommand. Try: schedule add --id <id> --cron "<expr>" --prompt "<text>" | list | status <id> | open <id> | remove <id> | run-now <id>',
1285
1438
  };
1286
1439
  }
1287
1440
  function isEntrypoint() {