Skip to content

Commit c10870a

Browse files
committed
ship: prepare lane for review
1 parent 7ccd831 commit c10870a

143 files changed

Lines changed: 14644 additions & 2705 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/plan/SKILL.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
name: plan
3+
description: Deliberate a feature or change in three locked rounds — functional requirements, then UI design with wireframes, then extras/quirks/out-of-the-box ideas. Each round asks the user clarifying questions via AskUserQuestion before proceeding. New functional scope at any point cascade-restarts the new piece through all three rounds and merges it into the locked plan. Use when the user invokes `/plan`, when the user is in plan mode and asks for a design/spec/feature breakdown, or when a non-trivial change needs structured deliberation before implementation.
4+
metadata:
5+
author: ade
6+
version: "1.0"
7+
---
8+
9+
# /plan — Three-Round Locked Deliberation
10+
11+
A feature plan has three layers: **what it does**, **how it looks**, and **the delightful extras**. Cross-talk between them produces sloppy plans. This skill enforces a strict order: lock functional, then lock UI, then add extras. New functional scope discovered at any point cascades back through all three rounds *for that new piece only*, then merges into the locked plan.
12+
13+
## Activation
14+
15+
Activate when **any** of:
16+
- The user invokes `/plan` (with or without extra context).
17+
- The user is in plan mode and asks for a design, spec, breakdown, or feature plan.
18+
- A non-trivial change request would benefit from structured deliberation (multi-component features, UX-sensitive work, anything spanning >2 files).
19+
20+
## Pre-flight: plan mode gate
21+
22+
Before Round 1, verify plan mode is active. If not:
23+
24+
> This skill is meant to run in plan mode (read-only deliberation). Enter plan mode (Shift+Tab cycles to it) and re-invoke `/plan <your context>`.
25+
26+
Then stop. Do not proceed in edit/full-auto modes — the deliberation contract assumes no files will be touched mid-plan.
27+
28+
## State you must track
29+
30+
Maintain these in your head (or in scratch) across the whole skill:
31+
32+
- **LockedFunctional** — bullet list of functional requirements confirmed so far.
33+
- **LockedUI** — bullet list of UI decisions confirmed so far (with wireframe sketches).
34+
- **LockedExtras** — list of delightful extras confirmed.
35+
- **CurrentRound** — 1 (Functional), 2 (UI), or 3 (Extras).
36+
- **PendingNewPieces** — queue of new functional requirements detected mid-flight that need their own cascade.
37+
38+
When a cascade fires, you snapshot CurrentRound, run the cascade for the new piece, merge results back into the Locked* sets, then resume from the snapshotted round.
39+
40+
## Round 1 — Functional Requirements
41+
42+
Silently deliberate on what the user asked for. Pull out:
43+
- Core capabilities the feature must have.
44+
- Boundaries (what it does *not* do).
45+
- Inputs, outputs, edge cases that change behavior.
46+
- Ambiguities where multiple interpretations exist.
47+
48+
Then call **AskUserQuestion** with 1–4 questions that resolve the meaningful ambiguities. Each option should describe a concrete interpretation with its trade-off. Avoid asking the user to write prose — frame everything as concrete choices.
49+
50+
### Tool reference: AskUserQuestion
51+
52+
Use `AskUserQuestion(questions)` to gather structured choices during Round 1, Round 2, Round 3, and any cascade. `questions` is an array of question objects:
53+
54+
- `id`: stable snake_case identifier.
55+
- `title`: short user-facing label.
56+
- `text`: the actual question.
57+
- `multiSelect`: optional boolean for menus like LockedExtras.
58+
- `allowOther`: optional boolean that permits an "Other" selection.
59+
- `allowAnnotation`: optional boolean that permits free-text annotation alongside a selected option.
60+
- `options`: array of `{ id, label, tradeoffs, description?, preview? }`.
61+
62+
The return value is keyed by question id and contains `selectedOptionIds`, optional `otherText`, and optional `annotations` / `freeText`. Treat selected option ids as the locked choice. Treat `otherText` as a new option authored by the user; if it changes behavior, update **LockedFunctional** and trigger the **Cascade rule**. Treat annotations as refinements to the selected option; if an annotation adds behavior rather than clarifying wording or presentation, it also cascades.
63+
64+
When the user responds:
65+
- Treat selected options as additions to **LockedFunctional**.
66+
- If the user's free-text (the "Other" reply or annotation) adds new scope → see **Cascade rule** below.
67+
- If the user only clarifies an existing item → update **LockedFunctional** and proceed.
68+
69+
Once questions are answered, **do not ask for explicit confirmation**. Silently move to Round 2.
70+
71+
## Round 2 — UI Design
72+
73+
Now deliberate on how the feature presents to the user. Generate 1–3 candidate UI directions. Round 2 candidates may be full-layout wireframes when the whole surface is up for decision, or component-level candidates when only one area is ambiguous. Be explicit which level each candidate represents.
74+
75+
Call **AskUserQuestion** with options that carry **markdown wireframe previews** in the `preview` field. Wireframes are ASCII boxes:
76+
77+
```
78+
┌─────────────────────────────────┐
79+
│ Header · filter ▾ · +new │
80+
├─────────────────────────────────┤
81+
│ ▣ item ⋯ │
82+
│ ▢ item ⋯ │
83+
│ ▢ item ⋯ │
84+
└─────────────────────────────────┘
85+
```
86+
87+
Keep each preview readable in a chat-width column (~70 chars). Show real labels, real density, real affordances — not lorem ipsum.
88+
89+
Cover the meaningful axes (layout type, hierarchy of actions, empty/loading/error states, dense vs roomy) in **at most 4 questions total**. Don't waste a question on a decision that has one obvious answer.
90+
91+
When the user responds:
92+
- Selected wireframes/options → **LockedUI**.
93+
- If candidates were component-level, merge the selected components into **LockedUI** as integration decisions. The Final output still needs one composed/merged inline wireframe derived from **LockedFunctional**, **LockedUI**, and any selected **LockedExtras**.
94+
- Free-text that implies new functional behavior (e.g. "add export button" implies an export *flow*) → see **Cascade rule**.
95+
- Pure UI refinements stay in Round 2.
96+
97+
Silently proceed to Round 3.
98+
99+
## Round 3 — Extras, Quirks, Out-of-the-Box
100+
101+
Now generate 4–8 candidate ideas the user *didn't* ask for but might love: micro-interactions, keyboard shortcuts, empty-state delight, power-user features, animations, accessibility wins, surprise affordances, anti-aesthetics-of-AI touches (favor warmth and specificity over generic monochrome polish).
102+
103+
Call **AskUserQuestion** with `multiSelect: true` for the menu of extras. Each option's `description` should be one sentence explaining the idea and one sentence on the cost/benefit.
104+
105+
Selected items → **LockedExtras**.
106+
107+
If the user adds a new functional idea in free-text → **Cascade rule**.
108+
109+
## Cascade rule (the core contract)
110+
111+
A **new functional requirement** is any input — from the user OR self-detected from your own proposals — that adds, removes, or changes scope of the feature. Not a UI refinement. Not an extras pick. *New behavior the system must perform.*
112+
113+
When detected:
114+
115+
1. **Snapshot** the current round.
116+
2. **Queue** the new piece in **PendingNewPieces** with a one-line description.
117+
3. **Announce briefly**: "New functional piece detected: <X>. Cascading it through R1→R2→R3 before resuming Round <snapshot>."
118+
4. **Run a focused mini-cascade for that piece only**:
119+
- **R1 for new piece**: AskUserQuestion scoped to just the new piece's functional ambiguities.
120+
- **R2 for new piece**: AskUserQuestion with wireframes scoped to just how this new piece integrates into the already-locked UI (do NOT redesign locked layouts — show how the new piece slots in).
121+
- **R3 for new piece**: AskUserQuestion with extras *for this piece only*.
122+
5. **Merge** the results into the appropriate Locked* sets.
123+
6. **Resume** from the snapshotted round.
124+
125+
Multiple cascades may stack. Process them depth-first; never lose the snapshot.
126+
127+
### Self-detecting new functional scope
128+
129+
Before sending a UI option or an extra, ask yourself silently: *does this option require behavior that isn't already in LockedFunctional?* If yes, you have a choice:
130+
- Drop the option (cleanest).
131+
- Or, if it's genuinely a great idea, **fire the cascade yourself** before sending it. Don't sneak new functional scope into UI/extras questions.
132+
133+
## Final output
134+
135+
After Round 3 completes with no pending cascades:
136+
137+
1. Print a tight consolidated plan in chat with three sections:
138+
- **Functional** — bulleted LockedFunctional.
139+
- **UI** — bulleted LockedUI with one inline wireframe of the final composed layout.
140+
- **Extras** — bulleted LockedExtras.
141+
Keep total under ~50 lines. No filler.
142+
2. Call **ExitPlanMode** to formally request approval.
143+
144+
### Tool reference: ExitPlanMode
145+
146+
Use `ExitPlanMode(): void` immediately after the Final output. It has no parameters and returns no value. Calling it signals that **LockedFunctional**, **LockedUI**, and **LockedExtras** are complete, including the final composed wireframe, and asks the user to approve leaving plan mode. Do not call it before all pending cascades are processed.
147+
148+
## Anti-patterns (do not do)
149+
150+
- Asking the user to "describe what they want" in prose. Always frame as concrete options.
151+
- Sending more than 4 questions in a single AskUserQuestion call (tool max).
152+
- Slipping new functional behavior into UI or Extras rounds without cascading.
153+
- Asking explicit "lock in? yes/no" questions between rounds — proceed silently unless interrupted.
154+
- Re-litigating LockedFunctional during R2 or R3 unless a cascade explicitly opens that piece.
155+
- Generic AI aesthetics in wireframes — no `font-mono` aesthetic apologies, no centered-everything, no purple gradients in mocked copy. Be specific and warm.
156+
- Long preamble. The user invoked `/plan`; jump to Round 1.
157+
158+
## Minimal example flow
159+
160+
User: `/plan a markdown note app with tags`
161+
162+
You: silent deliberation → AskUserQuestion (R1):
163+
- Q1: "How should tags be created?" (inline `#tag` parsing / explicit tag field / both)
164+
- Q2: "Search scope when filtering by tag?" (notes containing tag / notes tagged-only / both modes)
165+
- Q3: "Multi-user or single-user?"
166+
167+
User selects + adds "and they sync to iCloud" (new functional scope).
168+
169+
You: "New piece detected: iCloud sync. Cascading."
170+
→ R1-for-sync: conflict resolution? offline-first?
171+
→ R2-for-sync: sync-status indicator placement?
172+
→ R3-for-sync: optimistic UI? merge-conflict modal?
173+
Merge → resume Round 1.
174+
175+
… and so on through R2 and R3 of the main plan, then ExitPlanMode.

.github/workflows/release-core.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,55 @@ jobs:
368368
cp apps/ade-cli/scripts/install-runtime.sh release-assets/runtime/install.sh
369369
chmod 755 release-assets/runtime/install.sh
370370
371+
- name: Validate publish asset manifest
372+
run: |
373+
set -euo pipefail
374+
shopt -s nullglob
375+
376+
require_file() {
377+
local file="$1"
378+
local label="${2:-$1}"
379+
if [ ! -s "$file" ]; then
380+
echo "::error::Missing or empty $label: $file"
381+
exit 1
382+
fi
383+
}
384+
385+
require_glob() {
386+
local pattern="$1"
387+
local label="${2:-$1}"
388+
mapfile -t matches < <(compgen -G "$pattern" || true)
389+
if [ "${#matches[@]}" -eq 0 ]; then
390+
echo "::error::Missing $label matching $pattern"
391+
exit 1
392+
fi
393+
for file in "${matches[@]}"; do
394+
require_file "$file" "$label"
395+
done
396+
}
397+
398+
require_glob 'release-assets/mac/*.dmg' 'macOS DMG'
399+
require_glob 'release-assets/mac/*.zip' 'macOS zip'
400+
require_glob 'release-assets/mac/*-mac.zip.blockmap' 'macOS blockmap'
401+
require_file 'release-assets/mac/latest-mac.yml' 'macOS auto-update metadata'
402+
require_glob 'release-assets/win/*.exe' 'Windows installer'
403+
require_glob 'release-assets/win/*.exe.blockmap' 'Windows blockmap'
404+
require_file 'release-assets/win/latest.yml' 'Windows auto-update metadata'
405+
require_file 'release-assets/runtime/install.sh' 'standalone runtime installer'
406+
if [ ! -x 'release-assets/runtime/install.sh' ]; then
407+
echo "::error::Standalone runtime installer is not executable."
408+
exit 1
409+
fi
410+
411+
for target in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do
412+
require_file "release-assets/runtime/ade-$target" "ADE runtime binary for $target"
413+
require_file "release-assets/runtime/ade-$target.native.tar.gz" "ADE native dependency archive for $target"
414+
tar -tzf "release-assets/runtime/ade-$target.native.tar.gz" | grep -q '^\./node_modules/' || {
415+
echo "::error::ADE native dependency archive for $target is missing node_modules."
416+
exit 1
417+
}
418+
done
419+
371420
- name: Create or update draft GitHub release
372421
env:
373422
GH_TOKEN: ${{ github.token }}

apps/ade-cli/src/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ export async function createAdeRuntime(args: {
980980
logger,
981981
pollIntervalMs: 120_000,
982982
onUpdate: (snapshot) => pushEvent("runtime", { type: "usage", snapshot }),
983+
onThresholdEvent: (event) => pushEvent("runtime", { type: "usage_threshold", event }),
983984
});
984985
const budgetCapService = createBudgetCapService({
985986
db,

apps/ade-cli/src/cli.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10543,9 +10543,11 @@ async function spawnMachineRuntimeDaemon(
1054310543
async function connectMachineRuntimeDaemon(
1054410544
options: GlobalOptions,
1054510545
socketPathOverride?: string | null,
10546+
connectOptions: { allowSpawn?: boolean } = {},
1054610547
): Promise<SocketJsonRpcClient> {
1054710548
const socketPath = await resolveMachineRuntimeSocketPath(socketPathOverride);
1054810549
const label = "ADE runtime daemon socket";
10550+
const allowSpawn = connectOptions.allowSpawn ?? !options.requireSocket;
1054910551
try {
1055010552
const client = await SocketJsonRpcClient.connect(
1055110553
socketPath,
@@ -10557,6 +10559,12 @@ async function connectMachineRuntimeDaemon(
1055710559
options,
1055810560
);
1055910561
if (runtimeVersion && runtimeVersion !== VERSION) {
10562+
if (!allowSpawn) {
10563+
client.close();
10564+
throw new Error(
10565+
`ADE runtime daemon version ${runtimeVersion} does not match CLI version ${VERSION}.`,
10566+
);
10567+
}
1056010568
await shutdownMachineRuntimeDaemon(client);
1056110569
const spawned = await spawnMachineRuntimeDaemon(socketPath, options);
1056210570
if (!spawned) {
@@ -10583,6 +10591,7 @@ async function connectMachineRuntimeDaemon(
1058310591
}
1058410592
return client;
1058510593
} catch (firstError) {
10594+
if (!allowSpawn) throw firstError;
1058610595
const spawned = await spawnMachineRuntimeDaemon(socketPath, options);
1058710596
if (!spawned) throw firstError;
1058810597
try {
@@ -10658,7 +10667,9 @@ async function runRuntimeCommand(
1065810667
}
1065910668

1066010669
if (sub === "start") {
10661-
const client = await connectMachineRuntimeDaemon(options, socketOverride);
10670+
const client = await connectMachineRuntimeDaemon(options, socketOverride, {
10671+
allowSpawn: true,
10672+
});
1066210673
try {
1066310674
const runtimeVersion = await initializeMachineRuntimeDaemon(
1066410675
client,

apps/ade-cli/src/headlessLinearServices.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export type HeadlessGitHubService = {
116116
getStatus: (opts?: {
117117
forceRefresh?: boolean;
118118
}) => Promise<HeadlessGitHubStatus>;
119+
getRemoteStatus: () => Promise<{
120+
repo: { owner: string; name: string } | null;
121+
hasOrigin: boolean;
122+
}>;
119123
detectRepo: () => Promise<{ owner: string; name: string } | null>;
120124
getRepoOrThrow: () => Promise<{ owner: string; name: string }>;
121125
getTokenOrThrow: () => string;
@@ -770,6 +774,12 @@ export function createHeadlessGitHubService(
770774
return status;
771775
}
772776
},
777+
async getRemoteStatus() {
778+
return {
779+
repo: detectGitHubRepo(projectRoot),
780+
hasOrigin: Boolean(readGitOrigin(projectRoot)),
781+
};
782+
},
773783
async detectRepo() {
774784
return detectGitHubRepo(projectRoot);
775785
},

0 commit comments

Comments
 (0)