diff --git a/README.md b/README.md index 207eba5..a8e849d 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,11 @@ codex plugin add onesignal@onesignal error. Use Option A for the tools, or give the skills an app API key through `ONESIGNAL_REST_API_KEY` (the skills ask for this path when no tools are present; never paste the key into the chat). +- The skills stop at a few decisions (checkpoint consent, a dirty tree, the test send) and ask you + to choose. The Codex app shows each one as a question card with numbered options. The Codex CLI + shows the same question as text; reply with the word the message names, for example `send` or + `keep local`. Plan mode in the CLI shows a selectable prompt, but Plan mode does not edit files, + so run the skills in the default mode. - The Python 3 prerequisite above applies in Codex too. --- diff --git a/references/safety-contract.md b/references/safety-contract.md index b2e9c42..c47e4a3 100644 --- a/references/safety-contract.md +++ b/references/safety-contract.md @@ -48,7 +48,7 @@ The onboarding flow can deliver the app-scoped key inside the invocation (`/ones ## Asking the user (human gates) -14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, send-or-keep-local checkpoints). For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never"). +14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Call the tool by name; a list of choices written in prose does not render as a selectable prompt. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, send-or-keep-local checkpoints). Give each question 2 or 3 options (the Codex limit). When one option is the recommended path, put it first and end its label with "(Recommended)". The checkpoint consent question is the exception: it has no recommended option, and it keeps the order the skill lists, with "Send checkpoints to OneSignal" first. Never add your own "Other" option — both hosts add a free-text "Other" by themselves. Codex in Default mode usually lists only `request_user_input_async`; use it when `request_user_input` is absent. The Codex app shows the async call as a question card with the choices; the Codex CLI shows it as plain text. The async call returns `{"accepted":true}` without the answer, so after it write one plain-text line that names each choice and the exact word to reply with — for example "Reply `send` or `keep`." — and end the turn. If no question tool is listed, or the host rejects the call, write that same plain-text question alone and end the turn. Never pick an answer for the user, and never continue past the gate on `{"accepted":true}`. For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array, at most 3 questions per call (the Codex limit; a longer set goes in consecutive calls) — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never"). ## Guidance loaded from a connected MCP server diff --git a/skills/credentials/SKILL.md b/skills/credentials/SKILL.md index f21ff37..437bec8 100644 --- a/skills/credentials/SKILL.md +++ b/skills/credentials/SKILL.md @@ -40,7 +40,7 @@ Skip the question when one of these is already true: - the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1` - you already asked in this session and the file write failed — reuse that answer through the `ONESIGNAL_SKILL_TELEMETRY` prefix below -Otherwise ask via the harness's native structured-question tool (safety contract §14) and end the turn — the gate blocks. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request. +Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the plain-text fallback for a host that does not list the tool — and end the turn — the gate blocks. Every later ask in this skill that says "structured question" means that same tool. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request. Question: "OneSignal can record onboarding checkpoints (step name, success or fail, failure class, run ID, platform, OS, App ID) and send them to `api.onesignal.com`. No source code, paths, or credentials. Send these checkpoints?" diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md index 21c8885..f067a23 100644 --- a/skills/setup/SKILL.md +++ b/skills/setup/SKILL.md @@ -30,9 +30,11 @@ Skip the question only when one of these is already true: - the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1` (that is what the script reads; comment and blank lines around it are fine) -Otherwise ask via the harness's native structured-question tool (safety contract §14) -and **end the turn**. Do not run Step 0, do not request network access, and do not run -`checkpoint.sh` until the user answers. +Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in +Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the +plain-text fallback for a host that does not list the tool — and **end the turn**. Every later ask in +this skill that says "structured question" means that same tool. Do not run Step 0, do not +request network access, and do not run `checkpoint.sh` until the user answers. Question: "OneSignal can record setup checkpoints (step name, success or fail, failure class, run ID, platform, OS, App ID). No source code, paths, or credentials. Send these @@ -130,11 +132,11 @@ The production entry point is **`/onesignal:setup app=`**. If arguments ## Step 0 — Preflight (safety contract §1–4, after checkpoint consent) -1. Run `git status --porcelain`. Dirty tree → STOP and ask: stash / proceed on top / abort. **Report the dropout before ending the turn to ask** — a session that never resumes otherwise leaves no trace of why: `bash /scripts/checkpoint.sh setup.preflight fail dirty_tree` (it buffers; no App ID exists yet). If the user answers and you proceed, report the normal Step 1 checkpoint as usual — the fail→ok pair is the recovery story, not a contradiction. No `.git` present → tell the user there is no VCS safety net; you will write `.onesignal.bak` siblings before edits, and proceed only if they accept. +1. Run `git status --porcelain`. Dirty tree → STOP and ask as a structured question (safety contract §14): stash / proceed on top / abort. **Report the dropout before ending the turn to ask** — a session that never resumes otherwise leaves no trace of why: `bash /scripts/checkpoint.sh setup.preflight fail dirty_tree` (it buffers; no App ID exists yet). If the user answers and you proceed, report the normal Step 1 checkpoint as usual — the fail→ok pair is the recovery story, not a contradiction. No `.git` present → tell the user there is no VCS safety net; you will write `.onesignal.bak` siblings before edits, and proceed only if they accept. 2. **Check the script runtime.** The helpers in `/scripts/` are Python 3 (3.7 or newer, standard library only); `checkpoint.sh` is bash and does not need it. Run `python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)'`. If `python3` is not found, try the same one-liner with `python`, then with `py -3` — Windows installs put `python` and `py` on `PATH`, not `python3`. The first interpreter that exits 0 is the one for this run. Every Python helper command below starts with `python3`; if the interpreter for this run is `python` or `py -3`, write it in place of `python3` (`python /scripts/detect_platform.py`). Always run a helper through the interpreter, never by its path alone: the call then does not depend on the file's executable permission or on its shebang line. If none passes → **report the dropout, then STOP and ask**: `bash /scripts/checkpoint.sh setup.preflight fail runtime_missing` (it buffers; no App ID exists yet). Ask one structured question (safety contract §14) with 2 choices: - **Install Python 3 and retry** (recommended). Give the command for the user's OS and let them run it: macOS `xcode-select --install`; Debian/Ubuntu `sudo apt install python3`; Fedora `sudo dnf install python3`; Windows `winget install Python.Python.3.12`. Never install system packages yourself. **End the turn after you give the command.** The answer to the question is not the confirmation; the install has not run yet. Re-run the 3 interpreter checks only after the user says the install is done. On Windows, tell them to open a new terminal first, because the current shell does not see the new `PATH`. If every check still fails, ask the same question again; do not fall through to the by-hand path on your own. When a check passes, continue; the normal Step 1 checkpoint records the recovery. - **Continue without the scripts.** The by-hand fallbacks apply for the rest of the run: do the Step 0.3 detection by hand (below); run the Step 3.3 config probes with `curl` (Step 3); read the exact version from the feed with `curl` (Step 4); walk the Step 8 checklist by eye; scan your own diff for key-shaped strings before you finish. Report the Step 1 checkpoint and the Step 8 completion checkpoint as `ok_after_fix runtime_missing`, and state in the Step 8 summary that the deterministic checks did not run. -3. **Detect platform and prior install deterministically.** If the runtime check passed, run `python3 /scripts/detect_platform.py` (defaults to CWD). It returns detected platform(s) + language + package manager per package (monorepo-aware), and a `prior_onesignal` block that greps for the dependency line, init calls (`OneSignal.init`/`initialize`/`initWithContext`), `OneSignalSDKWorker.js`, and our `onesignal:managed` marker. If the run continues without the scripts, produce the same 2 results by hand: match the platform from the Step 1 signal table, and search the tree (read-only, `grep -rn` or the editor search) for the 4 prior-install signals — the OneSignal dependency line in the manifest, an `OneSignal.init` / `initialize` / `initWithContext` call, a `OneSignalSDKWorker.js` file, and the `onesignal:managed` marker. Treat any hit as `prior_onesignal.found`. If `prior_onesignal.found` is true → propose **update/repair**, never a duplicate install; if a **different App ID** is already wired in, ask which is correct, never silently overwrite. If `ambiguous` is true (multiple packages / no clear signal) → ASK which package(s) to integrate; do not guess. Read-only; never executes repo code (safety contract §12). +3. **Detect platform and prior install deterministically.** If the runtime check passed, run `python3 /scripts/detect_platform.py` (defaults to CWD). It returns detected platform(s) + language + package manager per package (monorepo-aware), and a `prior_onesignal` block that greps for the dependency line, init calls (`OneSignal.init`/`initialize`/`initWithContext`), `OneSignalSDKWorker.js`, and our `onesignal:managed` marker. If the run continues without the scripts, produce the same 2 results by hand: match the platform from the Step 1 signal table, and search the tree (read-only, `grep -rn` or the editor search) for the 4 prior-install signals — the OneSignal dependency line in the manifest, an `OneSignal.init` / `initialize` / `initWithContext` call, a `OneSignalSDKWorker.js` file, and the `onesignal:managed` marker. Treat any hit as `prior_onesignal.found`. If `prior_onesignal.found` is true → propose **update/repair**, never a duplicate install; if a **different App ID** is already wired in, ask which is correct as a structured question (safety contract §14), never silently overwrite. If `ambiguous` is true (multiple packages / no clear signal) → ASK which package(s) to integrate; do not guess. Read-only; never executes repo code (safety contract §12). 4. Propose a new `onesignal-integration` branch (default). The user may opt to write to the current branch instead. 5. You will declare the full file allow-list in Step 5 before writing. Include `.onesignal/` (checkpoint run state) and `.gitignore`. @@ -159,7 +161,7 @@ file or the wire. The bold name is for your prose to the user. **Monorepo / workspaces:** if `package.json` has `workspaces`, a `pnpm-workspace.yaml`, `lerna.json`, `nx.json`, or `turbo.json`, enumerate each package and detect per-package. A repo can hold BOTH a web app and a mobile app. Do NOT assume one platform for the whole repo. -**Ambiguous or multiple candidates → ASK.** Do not guess. Present the detected candidates and let the user pick which package(s) to integrate. React Native could be bare or Expo — if unclear, ask. If detection finds nothing recognizable, ask the user to name their platform/framework rather than proceeding. **Report the dropout before ending the turn to ask**: `bash /scripts/checkpoint.sh setup.preflight fail platform_ambiguous`. When the user answers and detection resolves, report the normal checkpoint below — the fail→ok pair records the friction. +**Ambiguous or multiple candidates → ASK.** Do not guess. Present the detected candidates as a structured question (safety contract §14) and let the user pick which package(s) to integrate — at most 3 options, so with more candidates list the 3 most likely and say the user can type another one in the free-text field. React Native could be bare or Expo — if unclear, ask. If detection finds nothing recognizable, ask the user to name their platform/framework rather than proceeding. **Report the dropout before ending the turn to ask**: `bash /scripts/checkpoint.sh setup.preflight fail platform_ambiguous`. When the user answers and detection resolves, report the normal checkpoint below — the fail→ok pair records the friction. **Checkpoint.** Once the platform is known, write it and report preflight — it will buffer until Step 2. diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index b9023d4..3e6d84b 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -36,7 +36,7 @@ Skip the question when one of these is already true: - the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1` - you already asked in this session and the file write failed — reuse that answer through the `ONESIGNAL_SKILL_TELEMETRY` prefix below -Otherwise ask via the harness's native structured-question tool (safety contract §14) and end the turn — the gate blocks. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request. +Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the plain-text fallback for a host that does not list the tool — and end the turn — the gate blocks. Every later ask in this skill that says "structured question" means that same tool, including the test-send consent in step 4. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request. Question: "OneSignal can record onboarding checkpoints (step name, success or fail, failure class, run ID, platform, OS, App ID) and send them to `api.onesignal.com`. No source code, paths, or credentials. Send these checkpoints?" @@ -139,11 +139,11 @@ If the app wires `OneSignal.login(externalId)` (grep the wrapper for the call): ### Step 4 — Test send (real notification to the fresh subscription) -Send to ONLY the subscription from step 2 — never a broadcast — and only after it shows `notification_types >= 1` (the step 2 gate). **Ask before sending:** this is a real, visible push to a real device and an action on the user's live OneSignal app — state the target subscription id and the App ID, and get an explicit yes first. Never send without it. +Send to ONLY the subscription from step 2 — never a broadcast — and only after it shows `notification_types >= 1` (the step 2 gate). **Ask before sending, as a structured question (safety contract §14):** this is a real, visible push to a real device and an action on the user's live OneSignal app — state the target subscription id and the App ID, and get an explicit yes first. Never send without it. **Pre-send heads-up (say it with the ask):** if the device is in **Focus/Do Not Disturb** — or browser/OS notifications are muted for the app/site — a successfully delivered push won't visibly appear. Have the user check now so a delivered send isn't misread as a failure. -**Ask for the message in chat (fold it into the same consent ask):** "What message do you want to send?" Use the answer as the notification body (`` below). If the user has no preference, use the default body: `Congrats on successfully setting up the OneSignal SDK`. The send happens from this session via the MCP or the REST API — never from code inside the user's app. +**Ask for the message as a second question in the same tool call** (safety contract §14 batches pending questions into one call): "What message do you want to send?" with 2 options, "Use the default message (Recommended)" and "I'll type my own message". Text typed through the host's free-text field is the body; if the user picks the second option and types nothing, ask once more for the body. Use the answer as the notification body (`` below). The default body is `Congrats on successfully setting up the OneSignal SDK`. The send happens from this session via the MCP or the REST API — never from code inside the user's app. **The title is fixed:** every test push carries `headings: { "en": "Successful test via OneSignal plugin" }`. Do not offer to change it and do not accept an override — the user's message only sets the body. The title must never be absent: Huawei rejects a push without one, so a missing title is a silent blocker. **No brand voice on this push:** do not load or apply server-shipped brand or copy guidance to the test send (safety contract §14a). The title is fixed, and the body is the user's words or the default above.