feat(skills): /sleep-on-it 6-phase wizard + kei-sleep-queue CRUD + incubation prompt

Priority-scaled time budgets (quick/standard/deep/marathon/weekly),
marathon-mode for hard derivations (skips Phase B REM for one task),
checkpointing every N minutes via partial commits.
This commit is contained in:
Parfii-bot 2026-04-22 02:30:04 +08:00
parent c6c572dcf4
commit 3d928b41db
9 changed files with 1114 additions and 0 deletions

269
_primitives/kei-sleep-queue.sh Executable file
View file

@ -0,0 +1,269 @@
#!/usr/bin/env bash
# kei-sleep-queue.sh — v0.12.0 "sleep on it" queue CRUD helper.
# Commands: add / list / show / done / fail / purge.
# Env: KEI_MEMORY_REPO_PATH (sourced from ~/.claude/secrets/.env).
# Bypass: KEI_SLEEP_GENESIS_BYPASS=1 skips genesis-scan gate on `add`.
set -u
SECRETS_FILE="${HOME}/.claude/secrets/.env"
[ -f "$SECRETS_FILE" ] && [ -z "${KEI_MEMORY_REPO_PATH:-}" ] && \
. "$SECRETS_FILE" 2>/dev/null || true
REPO_PATH="${KEI_MEMORY_REPO_PATH:-}"
QUEUE_DIR="${REPO_PATH}/sleep-queue"
DONE_DIR="${REPO_PATH}/sleep-queue-done"
FAIL_DIR="${REPO_PATH}/sleep-queue-failed"
GENESIS_BIN="${HOME}/.claude/agents/_primitives/_rust/target/release/genesis-scan"
SYNC_SH="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh"
err() { printf 'kei-sleep-queue: %s\n' "$*" >&2; }
die() { err "$*"; exit 1; }
ensure_repo() {
[ -n "$REPO_PATH" ] || die "KEI_MEMORY_REPO_PATH not set (run /sleep-setup)"
[ -d "${REPO_PATH}/.git" ] || die "sync-repo not initialised at $REPO_PATH"
mkdir -p "$QUEUE_DIR" "$DONE_DIR" "$FAIL_DIR" 2>/dev/null || true
}
gen_uuid() {
if command -v uuidgen >/dev/null 2>&1; then uuidgen | tr 'A-Z' 'a-z'
else printf '%s-%s' "$(date -u +%s)" "${RANDOM}${RANDOM}"; fi
}
iso_utc() { date -u +%Y-%m-%dT%H:%M:%SZ; }
push_async() { [ -x "$SYNC_SH" ] && "$SYNC_SH" >/dev/null 2>&1 || true; }
scan_prompt() {
[ "${KEI_SLEEP_GENESIS_BYPASS:-0}" = "1" ] && return 0
[ -x "$GENESIS_BIN" ] || return 0
"$GENESIS_BIN" --stdin --exit-on-hit --format text < "$1" >&2 && return 0
err "genesis-scan flagged the prompt — see scanner output above"
err "bypass (false positives only): KEI_SLEEP_GENESIS_BYPASS=1 $0 add ..."
exit 2
}
# Find a queue file by uuid prefix in dir; echoes path or returns 1.
find_by_uuid() {
local uuid="$1" dir="$2" f
[ -d "$dir" ] || return 1
for f in "$dir/${uuid}-"*.md "$dir/${uuid}.md"; do
[ -f "$f" ] && { printf '%s\n' "$f"; return 0; }
done
return 1
}
# Extract a frontmatter field value (first match) from a file.
fm_field() { awk -F': ' -v k="^$2:" '$0 ~ k {print $2; exit}' "$1"; }
# Parse "<N>m" → N (minutes), or die with context.
parse_minutes() {
local raw="$1" label="$2" stripped="${1%m}"
case "$stripped" in ''|*[!0-9]*) die "bad $label: $raw (expected <N>m)" ;; esac
printf '%s\n' "$stripped"
}
# Priority defaults: TIME_BUDGET_MINUTES, CHECKPOINT_EVERY_MINUTES, MARATHON.
priority_defaults() {
case "$1" in
quick) printf '15 0 false\n' ;;
standard) printf '60 20 false\n' ;;
deep) printf '240 30 false\n' ;;
marathon) printf '480 30 true\n' ;;
weekly) printf '60 20 false\n' ;;
*) die "bad --priority: $1 (expected quick|standard|deep|marathon|weekly)" ;;
esac
}
# Validate add flags; sets ADD_* including time-budget / checkpoint / marathon.
parse_add_flags() {
ADD_TYPE=""; ADD_PRIORITY=""; ADD_FORMAT=""; ADD_PROMPT=""
ADD_TIME_BUDGET=""; ADD_CHECKPOINT=""; ADD_MARATHON=""; ADD_NO_TIMEOUT=0
while [ $# -gt 0 ]; do
case "$1" in
--type) ADD_TYPE="$2"; shift 2 ;;
--priority) ADD_PRIORITY="$2"; shift 2 ;;
--format) ADD_FORMAT="$2"; shift 2 ;;
--prompt-file) ADD_PROMPT="$2"; shift 2 ;;
--time-budget) ADD_TIME_BUDGET="$(parse_minutes "$2" --time-budget)"; shift 2 ;;
--checkpoint-every) ADD_CHECKPOINT="$(parse_minutes "$2" --checkpoint-every)"; shift 2 ;;
--no-timeout) ADD_NO_TIMEOUT=1; shift ;;
--marathon) ADD_MARATHON="true"; shift ;;
*) die "unknown flag: $1" ;;
esac
done
case "$ADD_TYPE" in deep|pipeline|pattern|compare|custom) ;; *) die "bad --type: $ADD_TYPE" ;; esac
case "$ADD_FORMAT" in md|adr|checklist|table) ;; *) die "bad --format: $ADD_FORMAT" ;; esac
[ -n "$ADD_PROMPT" ] && [ -f "$ADD_PROMPT" ] || die "missing --prompt-file"
resolve_priority_fields
}
# Apply priority defaults for any ADD_* fields that weren't overridden.
resolve_priority_fields() {
local defaults budget cp marathon
defaults="$(priority_defaults "$ADD_PRIORITY")"
budget="$(printf '%s' "$defaults" | awk '{print $1}')"
cp="$(printf '%s' "$defaults" | awk '{print $2}')"
marathon="$(printf '%s' "$defaults" | awk '{print $3}')"
[ -z "$ADD_TIME_BUDGET" ] && ADD_TIME_BUDGET="$budget"
[ -z "$ADD_CHECKPOINT" ] && ADD_CHECKPOINT="$cp"
[ -z "$ADD_MARATHON" ] && ADD_MARATHON="$marathon"
[ "$ADD_NO_TIMEOUT" = "1" ] && ADD_TIME_BUDGET="null"
if [ "$ADD_MARATHON" = "true" ] && [ "$ADD_PRIORITY" != "marathon" ]; then
err "warning: --marathon set but --priority=$ADD_PRIORITY (expected marathon)"
fi
}
cmd_add() {
parse_add_flags "$@"
ensure_repo
scan_prompt "$ADD_PROMPT"
local uuid ts file
uuid="$(gen_uuid)"
ts="$(iso_utc)"
file="${QUEUE_DIR}/${uuid}-$(date -u +%s).md"
{
printf -- '---\n'
printf -- 'uuid: %s\n' "$uuid"
printf -- 'submitted_at: %s\n' "$ts"
printf -- 'type: %s\n' "$ADD_TYPE"
printf -- 'priority: %s\n' "$ADD_PRIORITY"
printf -- 'format: %s\n' "$ADD_FORMAT"
printf -- 'time_budget_minutes: %s\n' "$ADD_TIME_BUDGET"
printf -- 'checkpoint_every_minutes: %s\n' "$ADD_CHECKPOINT"
printf -- 'marathon: %s\n' "$ADD_MARATHON"
printf -- 'status: pending\n---\n\n'
cat "$ADD_PROMPT"
printf '\n'
} > "$file" || die "write failed: $file"
printf '%s\n%s\n' "$uuid" "$file"
push_async
}
cmd_list() {
local filter="pending" dir
[ $# -gt 0 ] && case "$1" in
--pending) filter="pending" ;;
--done) filter="done" ;;
--failed) filter="failed" ;;
*) die "unknown filter: $1" ;;
esac
ensure_repo
case "$filter" in
pending) dir="$QUEUE_DIR" ;; done) dir="$DONE_DIR" ;; failed) dir="$FAIL_DIR" ;;
esac
printf '%-36s %-10s %-8s %-9s %s\n' UUID SUBMITTED TYPE PRIORITY FILE
local f u s t p
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
u="$(fm_field "$f" uuid)"
s="$(fm_field "$f" submitted_at | cut -c1-10)"
t="$(fm_field "$f" type)"
p="$(fm_field "$f" priority)"
printf '%-36s %-10s %-8s %-9s %s\n' "${u:--}" "${s:--}" "${t:--}" "${p:--}" "$f"
done
}
cmd_show() {
[ $# -ge 1 ] || die "usage: show <uuid>"
ensure_repo
local uuid="$1" f dir
for dir in "$QUEUE_DIR" "$DONE_DIR" "$FAIL_DIR"; do
f="$(find_by_uuid "$uuid" "$dir" 2>/dev/null)" && { cat "$f"; return 0; }
done
die "uuid not found: $uuid"
}
cmd_done() {
[ $# -ge 1 ] || die "usage: done <uuid>"
ensure_repo
local src dest uuid="$1"
src="$(find_by_uuid "$uuid" "$QUEUE_DIR")" || die "pending uuid not found: $uuid"
dest="${DONE_DIR}/${uuid}.md"
sed 's/^status: pending$/status: done/' "$src" > "$dest" || die "write failed: $dest"
rm -f "$src"
printf 'moved: %s -> %s\n' "$src" "$dest"
push_async
}
cmd_fail() {
local uuid="" reason=""
while [ $# -gt 0 ]; do
case "$1" in
--reason) reason="$2"; shift 2 ;;
*) [ -z "$uuid" ] && { uuid="$1"; shift; } || die "unknown arg: $1" ;;
esac
done
[ -n "$uuid" ] || die "usage: fail <uuid> --reason <text>"
ensure_repo
local src dest
src="$(find_by_uuid "$uuid" "$QUEUE_DIR")" || die "pending uuid not found: $uuid"
dest="${FAIL_DIR}/${uuid}.md"
{
sed 's/^status: pending$/status: failed/' "$src"
printf '\n---\n## Failure reason\n\n%s\n' "${reason:-(no reason given)}"
} > "$dest" || die "write failed: $dest"
rm -f "$src"
printf 'moved: %s -> %s\n' "$src" "$dest"
push_async
}
cmd_purge() {
local days=""
while [ $# -gt 0 ]; do
case "$1" in
--older-than) days="${2%d}"; shift 2 ;;
*) die "unknown flag: $1" ;;
esac
done
case "$days" in ''|*[!0-9]*) die "--older-than <N>d required (N integer)" ;; esac
ensure_repo
local removed=0 f dir
for dir in "$DONE_DIR" "$FAIL_DIR"; do
while IFS= read -r f; do
rm -f "$f" && removed=$((removed + 1))
done < <(find "$dir" -maxdepth 1 -type f -name '*.md' -mtime "+$days" 2>/dev/null)
done
printf 'purged %d file(s) older than %sd\n' "$removed" "$days"
push_async
}
usage() {
cat >&2 <<'EOF'
kei-sleep-queue.sh — v0.12 sleep-on-it queue helper
add --type <deep|pipeline|pattern|compare|custom>
--priority <quick|standard|deep|marathon|weekly>
--format <md|adr|checklist|table>
--prompt-file <path>
[--time-budget <N>m] override minutes from priority default
[--checkpoint-every <M>m] override partial-result cadence
[--no-timeout] time_budget_minutes: null (run until done)
[--marathon] explicit marathon flag
list [--pending|--done|--failed]
show <uuid>
done <uuid>
fail <uuid> --reason <text>
purge --older-than <N>d
Env: KEI_MEMORY_REPO_PATH (required)
KEI_SLEEP_GENESIS_BYPASS=1 skip genesis-scan gate on add
EOF
exit 1
}
main() {
[ $# -ge 1 ] || usage
local sub="$1"; shift
case "$sub" in
add) cmd_add "$@" ;;
list) cmd_list "$@" ;;
show) cmd_show "$@" ;;
done) cmd_done "$@" ;;
fail) cmd_fail "$@" ;;
purge) cmd_purge "$@" ;;
-h|--help|help) usage ;;
*) die "unknown command: $sub" ;;
esac
}
main "$@"

View file

@ -0,0 +1,211 @@
# Nightly incubation — Phase A (KeiSeiKit v0.12.0 "sleep on it")
<!--
Prepended to the v0.11 nightly trigger prompt. The remote agent reads this
block FIRST and processes the queue before running the existing REM
consolidation (Phase B). Phase A and Phase B commit separately so the
morning pull shows two distinct diffs.
-->
## Phase A — Incubation ("sleep on it")
1. **Discover pending tasks.** List `sync-repo/sleep-queue/*.md` files
ordered by the `submitted_at` frontmatter field ascending (FIFO).
2. **Filter by day.** If today is NOT Sunday UTC, skip any file with
`priority: weekly`. On Sunday UTC, include weekly tasks alongside
`quick`, `standard`, `deep`, and `marathon` tasks in the same FIFO
order.
3. **Select tasks for this run (priority-aware).**
Priority resolution for the run:
1. Count `marathon: true` tasks in queue. If ≥ 1 with a this-night
priority, select the OLDEST by `submitted_at` as the sole task
for tonight. **Phase B REM consolidation is SKIPPED this run.**
All other pending tasks are deferred to the next night.
2. Else: pool this-night tasks (`priority ∈ {quick, standard, deep}`)
in FIFO order and **greedy-pack up to 480 minutes total** across
at most 5 tasks. Skip any task whose `time_budget_minutes` would
overflow the remaining budget; it stays pending for the next run.
3. Weekly batch: only processed on Sunday UTC, counted toward the
480-minute greedy-pack budget alongside `standard`/`deep` tasks.
4. **Budget time per task.** Read `time_budget_minutes` from the task's
frontmatter. Default 60 if absent or unparseable. Behavior:
- If `marathon: true`: this task gets the entire night (max 480 min);
other queue items skip this cycle; Phase B is skipped.
- If `checkpoint_every_minutes > 0`: every N minutes, write partial
result to `sleep-results/<uuid>.partial.md` AND commit + push, so
if the run is cut short the user still has the partial.
- If the budget is exhausted and the task is not done: write the
partial with `[TIME-BOXED — <N>min budget exhausted]` at the top
of the body, set `status: timed_out` in the queue-file frontmatter,
and move it to `sleep-queue-failed/<uuid>.md`.
- If `time_budget_minutes: null` (no-timeout): run until done or
until the hard cloud-session cap is hit (still honor checkpointing
so no work is lost).
5. **Dispatch by type.** Read the `type` frontmatter and run the
corresponding tool chain:
- `deep` — ≥ 3 WebSearch queries + ≥ 2 WebFetch page reads +
synthesis section. If the web is unreachable, mark result
`[OFFLINE — web tools unavailable]` and fail the task (step 7).
- `pipeline` — emit 57 phases, each with a one-line
verify-criterion, followed by a tradeoffs matrix. Use the repo's
past reports (`sync-repo/reports/*.md`) as context if relevant.
- `pattern` — grep `sync-repo/reports/*.md` and `sync-repo/backlog.md`
for recurring tokens related to the task text; extract 35 trends;
propose one concrete action.
- `compare` — produce a markdown table with the options the user
listed as columns, weighted criteria as rows, and a weighted-score
recommendation in the final row.
- `custom` — follow the task text verbatim without any fixed
dispatch. Write whatever the task asks for in the chosen format.
6. **Write the result** to
`sync-repo/sleep-results/<uuid>.md` in the chosen `format`:
- `md``# Title` + sections + sources.
- `adr``Context / Decision / Consequences`.
- `checklist``- [ ] item` bullets only (plus a one-line preamble).
- `table` — markdown table + short recommendation paragraph.
Every result MUST end with a `## Sources` section listing the
concrete URLs, files, or tool calls the agent used.
7. **Mark the task done.** Move the queue file:
`sync-repo/sleep-queue/<uuid>-*.md``sync-repo/sleep-queue-done/<uuid>.md`
Also update the `status:` frontmatter line from `pending` to `done`.
8. **On catastrophic failure** (tool error not fixable in the 15-min
budget, missing dependency, corrupted frontmatter): move the file to
`sync-repo/sleep-queue-failed/<uuid>.md`, update `status:` to
`failed`, and append a `## Failure reason` block to the body. Continue
with the next task.
9. **Commit once after Phase A completes** (single commit regardless of
how many tasks were processed). Commit message:
`REM: incubation <YYYY-MM-DD> (<N> task(s))`
10. **Then run Phase B** (see `sleep-trigger-prompt.md`). Phase B gets
its own commit: `REM: consolidation <YYYY-MM-DD>`.
### Phase A time cap
Total wall-clock cap for Phase A is **dynamic**:
- **Marathon run:** up to 480 minutes for the single selected task.
Phase B is SKIPPED this cycle.
- **Regular run:** greedy-pack up to 480 minutes total across at most
5 tasks, driven by each task's `time_budget_minutes`.
If the cap is hit mid-task, commit partial progress (honoring the
task's `checkpoint_every_minutes` cadence) and move the in-flight
task to `sleep-queue-failed/` with reason `phase-a-time-cap`. The
partial result (if any) stays in `sleep-results/<uuid>.partial.md`.
### Checkpointing (intermediate commits)
If a task's `checkpoint_every_minutes` is > 0, the agent commits
partial progress at that cadence:
```
git add sleep-results/<uuid>.partial.md
git commit -m "sleep: checkpoint <uuid> at <N>min"
git push
```
A final "task done" commit rolls the partial into `<uuid>.md` and
deletes the `.partial.md` file. If the run is cut short, the last
partial persists in the repo and the user can read it on morning pull.
---
## Example queue file (input)
```
---
uuid: 8d4f3c1e-7b2a-4f1d-9c8e-0a1b2c3d4e5f
submitted_at: 2026-04-22T14:03:17Z
type: compare
priority: standard
format: table
time_budget_minutes: 60
checkpoint_every_minutes: 20
marathon: false
status: pending
---
Compare SvelteKit, Astro, and Next.js App Router for the kit's landing
page. Criteria: bundle size, SSR ergonomics, build time, ecosystem
depth, hosting footprint.
```
## Example result file (output)
```
# Compare: SvelteKit / Astro / Next.js App Router
| Criterion (weight) | SvelteKit | Astro | Next.js App Router |
|------------------------|-----------|--------|--------------------|
| Bundle size (0.3) | 9/10 | 10/10 | 6/10 |
| SSR ergonomics (0.2) | 8/10 | 7/10 | 9/10 |
| Build time (0.2) | 8/10 | 9/10 | 6/10 |
| Ecosystem depth (0.2) | 7/10 | 6/10 | 10/10 |
| Hosting footprint (0.1)| 8/10 | 9/10 | 5/10 |
| **Weighted score** | **8.1** | **8.2**| **7.3** |
**Recommendation:** Astro narrowly wins for a content-first kit landing
page. Pick SvelteKit if the landing grows into an app; Next.js App
Router only if you already ship a Next.js product suite.
## Sources
- https://svelte.dev/docs/kit
- https://docs.astro.build/en/concepts/why-astro/
- https://nextjs.org/docs/app
- (tool: WebSearch) "SvelteKit vs Astro 2026 bundle size benchmark"
```
---
## Exit reasons (per-task status)
Every task ends in exactly one of:
- `done` — full result in `sleep-results/<uuid>.md`, queue file moved
to `sleep-queue-done/`.
- `time_budget_exhausted` — partial in `sleep-results/<uuid>.partial.md`
with `[TIME-BOXED — <N>min budget exhausted]` marker, queue file
moved to `sleep-queue-failed/` with `status: timed_out`.
- `checkpoint_saved` — intermediate state; the task is still pending
but the latest `.partial.md` is committed and pushed. This is NOT a
terminal status; it upgrades to `done` or `time_budget_exhausted`.
- `failed` — tool error, missing dependency, genesis-scan hit, or
other non-recoverable failure. Queue file moved to
`sleep-queue-failed/` with a `## Failure reason` block.
## Invariants (MUST NOT violate)
- **Never modify or delete `traces/*.jsonl`.** Phase A only touches
`sleep-queue/`, `sleep-queue-done/`, `sleep-queue-failed/`, and
`sleep-results/`. Phase B touches `reports/` and `backlog.md`. Neither
touches `traces/`.
- **Checkpoint commits are mandatory** when `checkpoint_every_minutes
> 0`. Skipping them loses user work on cloud-session eviction.
- **Never delete files outside the queue trees.** Move within the
sync-repo is the only mutation Phase A performs; `rm` is banned
outside `sleep-queue*/` (and even there, only after a successful
move to done/failed).
- **Never paraphrase patent-sensitive terms into the result body.** The
user-side `genesis-scan` gate is line of defense 1; the agent is line
of defense 2. If the agent's best-effort `genesis-scan` run on the
prompt returns non-zero (the scanner's forbidden-pattern list is
embedded in the binary itself — use
`genesis-scan list-patterns` if you need to inspect it), mark the
task failed with reason `patent-term-detected` and skip it entirely
— no partial result, no paraphrasing the matched token.
- **No shell command writes outside `sync-repo/`.** Results land in the
repo, nothing else.
- **No session feedback loop (RULE 0.15).** Results are for the user to
read the next morning. Nothing Phase A writes is auto-consumed by
another Claude Code session.
---
## Failure handling (same as Phase B)
- Clone fails or repo is dirty → exit 1, let the next run retry.
- `sleep-queue/` missing → create it empty + commit, then exit clean
(first run on an older sync-repo).
- Any single task fails → move to `sleep-queue-failed/`, continue.
- Phase A overall fails (total time cap, unhandled tool error) → commit
whatever partial state exists, THEN run Phase B normally. Phase B
must not depend on Phase A succeeding.

117
skills/sleep-on-it/SKILL.md Normal file
View file

@ -0,0 +1,117 @@
---
name: sleep-on-it
description: Defer a hard question, research task, or design comparison to the nightly remote agent (KeiSeiKit v0.12.0 incubation layer). Runs on top of the v0.11 sleep-sync pipeline — user fills one free-text field plus three clicks, task lands in sync-repo/sleep-queue/ and is processed before REM consolidation. Up to 5 tasks per night, 15 minutes each. Pure-click wizard except the single task-description field.
argument-hint: (no arguments)
---
# Sleep On It — Incubation Wizard (index)
Biological analog: the REM-sleep "sleep on it" effect — insight generation
during incubation (Wagner et al. 2004, *Nature*). During the day the user
submits open questions, research tasks, or design comparisons via this
wizard; the nightly cloud agent processes the queue before its existing
REM consolidation pass and writes results to `sync-repo/sleep-results/`.
This `SKILL.md` is the INDEX. Each phase lives in its own file and is
executed in order. Never skip a phase. Never re-order phases.
---
## Prerequisites (hard fail fast if missing)
- v0.11 sleep-sync must be configured (`~/.claude/secrets/.env` contains
`KEI_MEMORY_REPO_PATH`, `KEI_MEMORY_SSH_KEY`, and the sync-repo exists
under that path with a `.git/` subdir).
- `_primitives/kei-sleep-queue.sh` exists at
`~/.claude/agents/_primitives/kei-sleep-queue.sh` and is executable.
If either is missing, print the single line
```
v0.11 sleep-sync not configured — run `/sleep-setup` first, then retry.
```
and exit the wizard. Do not attempt to queue anything offline.
---
## Pipeline overview (6 phases, 5+ AskUserQuestion)
| Phase | File | Purpose | AskUserQuestion |
|---|---|---|---|
| 1 | [phase-1-intake.md](phase-1-intake.md) | One free-text field: the question / task | 0 (prompt, non-empty validate) |
| 2 | [phase-2-type.md](phase-2-type.md) | Task type: deep / pipeline / pattern / compare / custom | 1 (click) |
| 3 | [phase-3-priority.md](phase-3-priority.md) | Priority: tonight / FIFO / weekly | 1 (click) |
| 4 | [phase-4-format.md](phase-4-format.md) | Output format: markdown / ADR / checklist / table | 1 (click) |
| 5 | [phase-5-submit.md](phase-5-submit.md) | Preview frontmatter + body, submit / edit / abort | 1 (click) |
| 6 | [phase-6-ack.md](phase-6-ack.md) | Acknowledgment with UUID + queue path + run ETA | 1 (click) |
**Minimum AskUserQuestion count: 5.** All clicks except the single
free-text task description in Phase 1.
---
## Variables the pipeline produces
| Name | Set in | Meaning |
|---|---|---|
| `TASK_TEXT` | Phase 1 | Free-text task description (non-empty) |
| `TASK_TYPE` | Phase 2 | `deep` / `pipeline` / `pattern` / `compare` / `custom` |
| `PRIORITY` | Phase 3 | `night` / `fifo` / `weekly` |
| `FORMAT` | Phase 4 | `md` / `adr` / `checklist` / `table` |
| `SUBMIT_ACTION` | Phase 5 | `submit` / `edit` / `abort` |
| `QUEUE_PATH` | Phase 5 | Path of the queue file written by `kei-sleep-queue.sh add` |
| `UUID` | Phase 5 | UUID assigned by the helper |
---
## Final report (emit after Phase 6)
```
=== SLEEP-ON-IT REPORT ===
UUID: <UUID>
Queue file: <QUEUE_PATH>
Task type: <TASK_TYPE>
Priority: <PRIORITY>
Output format: <FORMAT>
Next run ETA: <UTC cron time from .keisei-sync.toml>
Results land: sync-repo/sleep-results/<UUID>.md
```
---
## Rules (apply throughout — enforced at every phase)
- **Pure-click contract.** Only Phase 1 asks for free text; every other
decision is an `AskUserQuestion`. No `freeText` outside Phase 1.
- **Idempotent.** Re-running the wizard while a previous task is still
pending is fine — each submission gets its own UUID and its own queue
file. No "one pending at a time" constraint.
- **Genesis-scan on submit (RULE 0.1 second line of defense).** The
helper `kei-sleep-queue.sh add` pipes the task text through
`genesis-scan --stdin --exit-on-hit` when the binary exists. On hit
the submission is rejected with the scanner's stderr surfaced to chat.
Bypass only via `KEI_SLEEP_GENESIS_BYPASS=1` with a visible note.
- **NO DOWNGRADE (RULE -1).** If the helper rejects (genesis hit, invalid
flag, sync push fails), surface 2-3 constructive fix paths — never
"cannot submit".
- **NO HALLUCINATION (RULE 0.4).** Never fabricate a UUID, queue path,
or ETA — always echo the real helper output.
- **RULE 0.8 secrets.** Queue files never embed tokens; env refs live in
`~/.claude/secrets/.env` only.
- **Silent failure (RULE 0.15).** If the post-submit sync push fails,
the queue file still lives locally and will be pushed on the next
session-end dump. The wizard must NOT block on push failure.
- **Constructor Pattern (RULE ZERO).** Every phase file < 100 LOC.
---
## References
- `~/.claude/rules/sleep-layer.md` — RULE 0.15 full text (Phase A added v0.12.0)
- `_primitives/kei-sleep-queue.sh` — the queue CRUD helper
- `_primitives/kei-sleep-sync.sh` — the session-end-dump callback (also
invoked by `kei-sleep-queue.sh add` after write)
- `_primitives/templates/sleep-incubation-prompt.md` — cloud agent Phase A
- `_primitives/templates/sleep-trigger-prompt.md` — cloud agent Phase B
- `skills/sleep-setup/` — v0.11 one-time sync-repo wizard (prerequisite)

View file

@ -0,0 +1,58 @@
# Phase 1 — Task intake (one free-text field)
The single free-text field in the wizard. Everything else is a click.
## 1a — Prerequisite check
Before prompting, verify the v0.11 pipeline is live:
```bash
# Resolve sync-repo path from env or secrets file.
# shellcheck disable=SC1091
[ -f "${HOME}/.claude/secrets/.env" ] && . "${HOME}/.claude/secrets/.env"
REPO_PATH="${KEI_MEMORY_REPO_PATH:-}"
QUEUE_SH="${HOME}/.claude/agents/_primitives/kei-sleep-queue.sh"
if [ -z "$REPO_PATH" ] || [ ! -d "$REPO_PATH/.git" ] || [ ! -x "$QUEUE_SH" ]; then
printf 'v0.11 sleep-sync not configured — run `/sleep-setup` first, then retry.\n'
exit 0
fi
```
If either check fails, exit the wizard. Do not offer offline queue mode.
## 1b — Free-text prompt
Emit a plain chat message (NOT `AskUserQuestion` — a free-text message
is fine when it is the only typed field and has a trivial non-empty
validator):
> What are you sleeping on? One or two sentences — the nightly agent
> will read this verbatim. Examples:
>
> - "Should I pick CfC or a small transformer as the memory re-ranker?"
> - "Compare SvelteKit, Astro, and Next.js App Router for the kit's landing page."
> - "What pattern in recent audit-backlog entries has the highest fix-value-per-effort?"
Store the reply as `TASK_TEXT`.
## 1c — Validate
- Reject if `TASK_TEXT` is empty or only whitespace.
- Reject if `TASK_TEXT` > 4000 characters (keep queue files small; the
agent has 15 minutes wall-clock anyway — a novella does not help).
On reject, print
```
Task description must be non-empty and <= 4000 chars. Try again?
```
and re-prompt. Up to 3 attempts; on the 3rd empty submission abort the
wizard with a short "try again with `/sleep-on-it`" message.
## Verify-criterion
- `TASK_TEXT` is non-empty and <= 4000 chars.
- The v0.11 sync pipeline is wired (REPO_PATH + QUEUE_SH exist).
- Exactly ZERO `AskUserQuestion` in this phase (free-text message only).

View file

@ -0,0 +1,71 @@
# Phase 2 — Task type (click)
Map the free-text task to one of five dispatch categories. The remote
agent's Phase A uses this to pick the right tool chain.
## 2a — Click
Emit ONE `AskUserQuestion`:
```json
{
"questions": [
{
"question": "How should the nightly agent approach this task?",
"header": "Type",
"multiSelect": false,
"options": [
{
"label": "Deep research",
"description": "WebSearch + WebFetch + synthesis — 3+ searches, 2+ page fetches, structured report"
},
{
"label": "Pipeline design",
"description": "Architect + critic sequence — 5-7 phases with verify-criteria and tradeoffs"
},
{
"label": "Pattern analysis",
"description": "Query kei-memory + past reports — extract trends across sessions, propose action"
},
{
"label": "Comparative study",
"description": "Pros/cons matrix across N options the user lists — weighted recommendation"
},
{
"label": "Custom",
"description": "Follow the task text verbatim — no dispatch tool, free-form response"
}
]
}
]
}
```
## 2b — Normalise
Map the clicked label to a compact token the queue file stores:
| Label | Token |
|---|---|
| Deep research | `deep` |
| Pipeline design | `pipeline` |
| Pattern analysis | `pattern` |
| Comparative study | `compare` |
| Custom | `custom` |
Store as `TASK_TYPE`.
## 2c — Soft nudge on mismatch
If `TASK_TYPE == "custom"` AND `TASK_TEXT` contains any of
`should I | compare | trade[- ]off | which is better`, print a soft hint:
> Your task text looks like a comparison — `compare` or `deep` usually
> produce a stronger result than `custom`. Proceeding anyway.
Do NOT re-ask. One nudge, user keeps control.
## Verify-criterion
- `TASK_TYPE ∈ {deep, pipeline, pattern, compare, custom}`.
- Exactly ONE `AskUserQuestion` in this phase.

View file

@ -0,0 +1,130 @@
# Phase 3 — Priority & time budget (click)
Decide how much night-time the remote agent spends on this task.
Priority maps to a wall-clock budget; the pipeline reads the budget
from frontmatter, so a hard equation can own the whole night while a
quick lookup is boxed to 15 minutes.
## 3a — Click
Emit ONE `AskUserQuestion`:
```json
{
"questions": [
{
"question": "How much night-time should this task get?",
"header": "Priority",
"multiSelect": false,
"options": [
{
"label": "Quick",
"description": "15 min, this night — simple questions, fast lookups"
},
{
"label": "Standard",
"description": "60 min, this night — default, medium research"
},
{
"label": "Deep",
"description": "4 hour, this night — serious derivations, thorough prior-art"
},
{
"label": "Marathon",
"description": "Full night, 1 task only — hard equations, full autonomy; Phase B REM skipped this night"
},
{
"label": "Weekly batch",
"description": "60 min, processed next Sunday UTC — non-urgent research"
}
]
}
]
}
```
## 3b — Marathon confirmation
If `LABEL == "Marathon"`, emit ONE more `AskUserQuestion` so the user
consciously accepts the cost:
```json
{
"questions": [
{
"question": "Marathon = this task owns the whole night. Phase B REM consolidation is skipped. Other queue tasks deferred to next night. Confirm?",
"header": "Marathon",
"multiSelect": false,
"options": [
{"label": "Yes, marathon", "description": "Take the full night; defer everything else"},
{"label": "No, downgrade to Deep (4 hour)", "description": "Still a long run but Phase B and other tasks proceed"}
]
}
]
}
```
If the user picks "No, downgrade to Deep (4 hour)", treat the effective
label as `Deep` for the rest of the pipeline.
## 3c — Normalise
Map the final label (after any marathon downgrade) to four variables:
| Label | `PRIORITY_LABEL` | `TIME_BUDGET_MINUTES` | `CHECKPOINT_EVERY_MINUTES` | `MARATHON` |
|---------------|------------------|-----------------------|----------------------------|------------|
| Quick | `quick` | 15 | 0 (off) | `false` |
| Standard | `standard` | 60 | 20 | `false` |
| Deep | `deep` | 240 | 30 | `false` |
| Marathon | `marathon` | 480 | 30 | `true` |
| Weekly batch | `weekly` | 60 | 20 | `false` |
Store all four as phase-scoped variables. They flow to Phase 5, which
passes them to `kei-sleep-queue.sh add`.
## 3d — Cap check (informational)
If `PRIORITY_LABEL ∈ {quick, standard, deep, marathon}` (i.e. this
night), count current this-night pending tasks:
```bash
~/.claude/agents/_primitives/kei-sleep-queue.sh list --pending \
| awk '$4 ~ /^(quick|standard|deep|marathon)$/' \
| wc -l
```
Informational messages:
- **Marathon already queued this night:**
> A marathon task is already pending for tonight. Submitting a second
> marathon — or any this-night task — will be deferred to the next
> night, because the marathon owns the whole window.
- **Greedy-pack near-full (≥ 480 min queued this-night):**
> Tonight's this-night budget is nearly full (≥ 8 hours queued). New
> this-night tasks will still be accepted but may be deferred to the
> next night by the greedy-packing scheduler.
Do NOT re-prompt; the user may explicitly want overflow.
## 3e — Advanced overrides (informational)
After Phase 5 preview, explicit flags override the priority defaults:
```
kei-sleep-queue add --time-budget <N>m # e.g. --time-budget 90m
--checkpoint-every <M>m # e.g. --checkpoint-every 15m
--no-timeout # time_budget_minutes: null
--marathon # explicit marathon flag
```
The wizard does not emit these flags itself; they exist for power users
who call the helper directly.
## Verify-criterion
- `PRIORITY_LABEL ∈ {quick, standard, deep, marathon, weekly}`.
- `TIME_BUDGET_MINUTES ∈ {15, 60, 240, 480}` per the table.
- `CHECKPOINT_EVERY_MINUTES ∈ {0, 20, 30}` per the table.
- `MARATHON` is boolean and `true` iff `PRIORITY_LABEL == "marathon"`.
- At most TWO `AskUserQuestion` calls (second only on marathon path).

View file

@ -0,0 +1,63 @@
# Phase 4 — Output format (click)
Tell the remote agent how to shape `sync-repo/sleep-results/<uuid>.md`.
## 4a — Click
Emit ONE `AskUserQuestion`:
```json
{
"questions": [
{
"question": "What format should the result use?",
"header": "Format",
"multiSelect": false,
"options": [
{
"label": "Structured markdown report",
"description": "Sections + findings + sources — default, best for research / pattern analysis"
},
{
"label": "ADR-style decision record",
"description": "Context / Decision / Consequences — best for pipeline-design output"
},
{
"label": "Checklist / action items",
"description": "`- [ ] item` list — best when you want a morning TODO"
},
{
"label": "Pros/cons table",
"description": "Markdown table with weighted criteria — best for comparative study"
}
]
}
]
}
```
## 4b — Normalise
| Label | Token |
|---|---|
| Structured markdown report | `md` |
| ADR-style decision record | `adr` |
| Checklist / action items | `checklist` |
| Pros/cons table | `table` |
Store as `FORMAT`.
## 4c — Coherence hint
Soft hint only (no re-ask), when type and format drift apart:
- `TASK_TYPE == compare` and `FORMAT != table` → hint that `table` is the usual pick.
- `TASK_TYPE == pipeline` and `FORMAT != adr` → hint that `adr` is the usual pick.
- `TASK_TYPE == pattern` and `FORMAT != checklist` → hint that `checklist` often reads best.
Format: single line, does not block.
## Verify-criterion
- `FORMAT ∈ {md, adr, checklist, table}`.
- Exactly ONE `AskUserQuestion` in this phase.

View file

@ -0,0 +1,121 @@
# Phase 5 — Preview and submit (click)
Show the user exactly what will be written, then submit via the helper.
## 5a — Render preview
Print a fenced block with the frontmatter + body preview:
```
---
uuid: <generated-after-submit>
submitted_at: <generated-after-submit>
type: <TASK_TYPE>
priority: <PRIORITY_LABEL>
format: <FORMAT>
time_budget_minutes: <TIME_BUDGET_MINUTES>
checkpoint_every_minutes: <CHECKPOINT_EVERY_MINUTES>
marathon: <MARATHON>
status: pending
---
<TASK_TEXT>
```
Then print a one-line wall-clock estimate:
```
estimated wall-clock: <TIME_BUDGET_MINUTES> min
```
If `MARATHON == true`, append an explicit warning line beneath the
estimate:
```
marathon: Phase B REM consolidation will be SKIPPED the night this
task runs, and other queue items will be deferred to the next night.
```
Tell the user the `uuid` and `submitted_at` fields are assigned by the
helper on submit — the preview leaves them as placeholders.
## 5b — Click
Emit ONE `AskUserQuestion`:
```json
{
"questions": [
{
"question": "Submit this to the nightly queue?",
"header": "Submit",
"multiSelect": false,
"options": [
{"label": "Submit", "description": "Write queue file + push to memory-repo"},
{"label": "Edit", "description": "Go back to Phase 1 and re-enter the task text"},
{"label": "Abort", "description": "Drop the draft; nothing is written"}
]
}
]
}
```
Store the pick as `SUBMIT_ACTION`.
## 5c — Dispatch
- `SUBMIT_ACTION == "Edit"` → restart from Phase 1 (clears all variables).
- `SUBMIT_ACTION == "Abort"` → print `submission cancelled` and exit.
- `SUBMIT_ACTION == "Submit"` → call the helper (see 5d).
## 5d — Invoke `kei-sleep-queue.sh add`
Write the task text to a temp file, then:
```bash
PROMPT_FILE="$(mktemp)"
printf '%s\n' "$TASK_TEXT" > "$PROMPT_FILE"
MARATHON_FLAG=""
[ "$MARATHON" = "true" ] && MARATHON_FLAG="--marathon"
OUTPUT="$(
~/.claude/agents/_primitives/kei-sleep-queue.sh add \
--type "$TASK_TYPE" \
--priority "$PRIORITY_LABEL" \
--format "$FORMAT" \
--time-budget "${TIME_BUDGET_MINUTES}m" \
--checkpoint-every "${CHECKPOINT_EVERY_MINUTES}m" \
$MARATHON_FLAG \
--prompt-file "$PROMPT_FILE" 2>&1
)"
STATUS=$?
rm -f "$PROMPT_FILE"
```
The helper prints two lines on success:
```
<uuid>
<absolute path of queue file>
```
Capture `UUID = first line`, `QUEUE_PATH = second line`.
On non-zero exit, surface stderr verbatim. Common causes:
- **genesis-scan hit** → task text contains a patent-sensitive term.
Constructive paths (RULE -1):
1. Rewrite the task generically ("pick between <redacted>"
becomes "pick between technology A and B").
2. Set `KEI_SLEEP_GENESIS_BYPASS=1` and re-run if the term is a
false positive (cite the specific word).
3. Abort; keep the question local.
- **write failed** (disk / permissions) → print the error; exit.
- **sync push failed after local write succeeded** → not an error; the
queue file IS committed locally and will push on next session end.
## Verify-criterion
- `SUBMIT_ACTION ∈ {Submit, Edit, Abort}`.
- If `Submit`, `UUID` is a non-empty string and `QUEUE_PATH` ends in
`.md` under `sync-repo/sleep-queue/`.
- Exactly ONE `AskUserQuestion` in this phase.

View file

@ -0,0 +1,74 @@
# Phase 6 — Acknowledgment (click)
Show the user what was written, how to inspect it, and when the agent
picks it up.
## 6a — Resolve next-run ETA
Read the cron expression the `/schedule` trigger uses. The helper keeps
the URL in `sync-repo/.keisei-sync.toml`; the cron target itself is in
the user's Claude Code `/schedule list`. We do not parse the scheduler
from this skill (no portable API) — instead, print the canonical line:
```bash
grep -E '^schedule_utc_cron' "$REPO_PATH/.keisei-sync.toml" 2>/dev/null \
|| printf 'schedule_utc_cron = "unknown — run /schedule list to verify"\n'
```
If the key is absent (older sync-repo), print the fallback line
verbatim. Do not fabricate a time (RULE 0.4).
## 6b — Print acknowledgment block
Emit this block to chat:
```
Queued.
UUID: <UUID>
File: <QUEUE_PATH>
Type: <TASK_TYPE>
Priority: <PRIORITY>
Format: <FORMAT>
Next run: <cron line from 6a>
Results at: <REPO_PATH>/sleep-results/<UUID>.md (after the next run)
Inspect: `kei-sleep-queue show <UUID>`
List all: `kei-sleep-queue list --pending`
Cancel: delete the file at the path above before the next run
```
## 6c — Click (final)
Emit ONE `AskUserQuestion`:
```json
{
"questions": [
{
"question": "What now?",
"header": "Done",
"multiSelect": false,
"options": [
{"label": "Show queue", "description": "Run `kei-sleep-queue list --pending`"},
{"label": "Submit another", "description": "Restart the wizard"},
{"label": "Done", "description": "Close the wizard"}
]
}
]
}
```
Handle each option:
- `Show queue` → shell out to `kei-sleep-queue list --pending` and
print the table; then re-emit this click.
- `Submit another` → restart the wizard from Phase 1.
- `Done` → emit the final report from `SKILL.md` and exit.
## Verify-criterion
- The acknowledgment block in 6b was printed with real values (no `<UUID>`
placeholders left).
- Exactly ONE `AskUserQuestion` in this phase (plus loops on "Show queue").
- The final report block from `SKILL.md` was emitted on `Done`.