Add CUPS print server container for Brother HL-4150CDN

This commit is contained in:
juergen committed 2026-10-18 09:16:37 +02:00
commit 6f3162849b
52 files changed
+5168

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+199
View File
@@ -0,0 +1,199 @@
---
description: "Implement tasks from an OpenSpec change (Experimental)"
---
Implement tasks from an OpenSpec change.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Provided arguments**: $ARGUMENTS
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status, source path, and source line
- Dynamic instruction based on current state
- Optional `context`: current required project instruction input from the selected root
- Optional `operationGuidance`: current advisory guidance for apply
- `missingArtifacts` (when present): required artifact ids with no output
**Handle states:**
- If `state: "blocked"`: show the message and pause implementation.
- If `missingArtifacts` is non-empty: suggest completing the missing artifacts. Run `openspec status --change "<name>" --json`, select the next `ready` artifact (not `skipped` or `blocked`), and use `openspec instructions "<artifact-id>" --change "<name>" --json` for its rules and template. Keep the selected `--store <id>` on both commands.
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
- If `state: "all_done"`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
- Otherwise: proceed to implementation
Treat `context` as a required prompt-level input. Read and consider it, and
apply relevant project facts, conventions, and constraints while implementing.
Treat `operationGuidance` as optional additive advice. Read and consider every
entry, and follow entries that are applicable and compatible with the built-in
workflow.
Keep both fields separate from CLI-returned state, missing artifacts, tasks,
progress, `contextFiles`, and the built-in `instruction`. They are not
evidence of task completion, do not replace the built-in instruction, and do
not permit bypassing a blocked state. If context conflicts with the built-in
instruction, an explicit user choice, or a CLI-controlled value, report the
conflict and preserve the controlling value. If guidance is inapplicable or
conflicts with those controlling inputs, do not follow it and explain why.
These are prompt-level behavior contracts, not enforceable checks.
4. **Read context files**
Read every file path listed under `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
Do not copy `context` or `operationGuidance` verbatim into implementation
files or planning artifacts unless the user separately asks for that content.
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Before editing, confirm the checkbox at the returned `sourcePath` and `line` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
- Mark the task complete at its returned `sourcePath` and `line`: `- [ ]` → `- [x]`
- Rerun the apply instructions and confirm that task is now done and progress changed
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- A task needs work beyond what the spec and tasks describe, or you are tempted to drop, narrow, defer, or accept exceptions to specified behavior to make it fit → surface the added scope and ask; do not absorb it silently
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tracked tasks are complete. Review or verify the change as appropriate
before archiving. You can archive this change with `/opsx-archive`.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
- Only mark a task `- [x]` when its specified behavior is fully implemented, not when it is partially done or deferred
- Use contextFiles from CLI output, don't assume specific file names
- Use each task's sourcePath and line to update its exact checkbox
- Do not use context or operation guidance as proof that a task is complete
- Apply relevant project context; report conflicts with controlling workflow inputs
- Consider every guidance entry; explain any inapplicable or conflicting advice
- Do not copy runtime context or operation guidance into implementation files or planning artifacts
- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
+261
View File
@@ -0,0 +1,261 @@
---
description: "Archive a completed change in the experimental workflow"
---
Archive a completed change in the experimental workflow.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Provided arguments**: $ARGUMENTS
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show only active changes (not already archived).
Include the schema used for each change if available.
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-archive <other>`).
**Load current archive inputs before the existing archive checks:**
After resolving the selected change and planning root, run:
```bash
openspec instructions archive --change "<name>" --json
```
Keep the same selected-root flags on this command. This lookup is advisory and
optional: it only supplies extra prompt inputs, so it must never block archiving.
If it exits non-zero or returns invalid JSON — for example on an older CLI that
does not support this command yet — continue the archive workflow with no
context and no operation guidance. Do not report an error and do not stop.
A successful response may omit both optional fields. Treat `context` as a
required prompt-level input: read and consider it, and apply relevant project
facts, conventions, and constraints. Treat `operationGuidance` as optional
additive advice: read and consider every entry, and follow entries that are
applicable and compatible with the built-in archive workflow.
Keep both fields separate from built-in steps, explicit user choices, resolved
paths, CLI checks, and command contracts. If context conflicts with one of those
controlling inputs, report the conflict and preserve the controlling value. If
guidance is inapplicable or conflicts with a controlling input, do not follow it
and explain why. Do not infer replacement paths, skipped prompts, or flags from
either field, and do not copy their text verbatim into specs, change artifacts,
or archive summaries unless the user separately asks for it. These are
prompt-level behavior contracts, not enforceable checks.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
- `artifacts`: List of artifacts with their status (`done`, `skipped`, or other)
**If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs):
- Display warning listing incomplete artifacts
- Prompt user for confirmation to continue
- Proceed if user confirms
3. **Check task completion status**
Run `openspec list --json` with the same selected-root flags and find the
entry in `changes` whose `name` exactly matches the selected change.
Require exactly one match and nonnegative integer `totalTasks` and
`completedTasks`, with `completedTasks <= totalTasks`. The CLI resolves
the schema's tracked task files, including custom artifact names, output
paths, and globs.
Incomplete tasks = `totalTasks - completedTasks`.
Do not infer task completion from artifact status or the absence of a
top-level `tasks.md`. If the lookup fails, returns invalid JSON, omits or
duplicates the selected change, or returns invalid counts, report the problem
and stop before syncing or archiving.
The CLI counts only `x`/`X` checkbox markers as complete;
other markers, including unfamiliar ones, remain incomplete.
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Prompt user for confirmation to continue
- Proceed if user confirms
**If `totalTasks` is zero:** Proceed without a task-related warning.
4. **Assess delta spec sync state**
Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only
delta-spec source. If the `specs` entry is missing or
`existingOutputPaths` is empty, proceed without a sync prompt and do not infer
delta specs from other artifacts.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path)
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
- Otherwise, if the delta has only REMOVED requirements and the change's `.openspec.yaml` declares `retire_capabilities: true`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. `openspec archive` refuses the unmarked REMOVED-only case with `Spec must have at least one requirement`.
- Otherwise, count the capability as needing sync and name it in the summary (`<capability-path>: new main spec will be created`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as `openspec archive` does.
- Determine what changes would be applied (adds, modifications, removals, renames)
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.
**Prompt options:**
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"
Route on the answer:
- "Cancel" — stop, do not archive
- "Archive without syncing" or "Archive now" — proceed to archive
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
- Anything else — ask again rather than archiving
Before a selected sync writes any main spec, run
`openspec instructions specs --change "<name>" --json` once with the same
selected-root flags. Require a zero exit status and valid artifact-instruction
JSON. If the lookup fails or returns invalid JSON, report the error and stop
before writing any main spec or moving the change. A valid response with omitted
`rules` is the no-rules case. Apply returned `rules` only to the content and
form of main specs produced by this merge; do not use them as archive guidance,
change CLI behavior, or copy the rule text into any output file.
Then run the `/opsx-sync` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
If the sync reports any stop or blocking condition, treat the sync as failed.
Stop the archive immediately. Do not perform the post-sync content comparison and do not move its `changeRoot`.
Nothing has moved, so the user can fix the blocking condition or re-run the sync.
After the sync writes each main spec, verify its structure against the canonical sync contract:
- A new main spec starts with a `# <capability> Specification` title. An existing main spec keeps its title exactly as it is.
- Preserve existing `## Purpose` sections completely untouched for established main specs.
- For a new main spec, copy the delta `## Purpose` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable `## Purpose` is provided, use the existing TBD Purpose behavior and warning.
- Verify that no delta-style section headers (`## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
- Requirement blocks the sync wrote or changed use `### Requirement:` headings, and their scenarios use `#### Scenario:` headings, under the spec's `## Requirements` section. Leave content the delta does not mention exactly as it is.
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
- ADDED requirements present
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty.
- RENAMED requirements present under the new name and absent under the old one
If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
5. **Perform the archive**
Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
```bash
mkdir -p "<planningHome.changesDir>/archive"
```
Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-<change-name>`. Never stack a second date (same rule as `openspec archive`).
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move `changeRoot` to the archive directory
```bash
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Spec sync status (synced / sync skipped / no delta specs)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```markdown
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** ✓ Synced to main specs
All artifacts complete. All tasks complete.
```
**Output On Success (No Delta Specs)**
```markdown
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** No delta specs
All artifacts complete. All tasks complete.
```
**Output On Success With Warnings**
```markdown
## Archive Complete (with warnings)
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** Sync skipped (user chose to skip)
**Warnings:**
- Archived with 2 incomplete artifacts
- Archived with 3 incomplete tasks
- Delta spec sync was skipped (user chose to skip)
Review the archive if this was not intentional.
```
**Output On Error (Archive Exists)**
```markdown
## Archive Failed
**Change:** <change-name>
**Target:** the archive path derived from `planningHome.changesDir`/<target-name>/
Target archive directory already exists.
**Options:**
1. Rename the existing archive
2. Delete the existing archive if it's a duplicate
3. Wait until a different date to archive
```
**Guardrails**
- Announce the selected change; prompt for selection when it is ambiguous
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, run the `/opsx-sync` workflow inline (agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
- Consider every guidance entry and explain any inapplicable or conflicting advice
- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged
- Artifact rules constrain only the specs being written and are never operation guidance
- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files
+238
View File
@@ -0,0 +1,238 @@
---
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, do not start it here: say that explore mode does not implement, and point them at `/opsx-propose`, which turns the discussion into a change. The work happens from that change, never from explore mode. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. An explicit request from the user to capture the exploration as a new change is itself that confirmation, covering the change and the change artifacts the request names; scaffold it first as described below.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
- A comparison: "postgres vs sqlite for this"
- Nothing (just enter explore mode)
**Provided arguments**: $ARGUMENTS
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## Planning a Change
When the user is planning a change, guide them toward shared understanding with focused discovery questions. For open-ended discussion, follow the conversation without imposing an interview or a required output.
Before asking a factual question, follow the context discovery below and inspect relevant OpenSpec artifacts, source, tests, docs, and configuration. Do not ask the user to repeat facts you can verify. Summarize relevant findings without reproducing private context or rules. If evidence is missing, conflicting, or inaccessible, state that limitation and ask only for the clarification needed to proceed.
- **Follow dependencies** - Resolve the next blocking decision before its dependent details. For example, clarify the user's outcome and scope before choosing an API or data model. Revisit downstream assumptions when an earlier answer changes. Skip branches that do not matter to this goal.
- **Keep questions focused** - Ask one focused question at a time, and briefly explain why it matters and which decision it unlocks. Batch questions only if the user asks for a batch; keep them small and group related decisions.
- **Offer grounded recommendations** - When evidence supports a recommendation, state your preferred option and why it fits the user's goals, with alternatives and their tradeoffs when useful. Do not invent intent, priorities, or external constraints: ask the user when only they can answer. Avoid a fixed question format.
- **Keep a conversational record** - Track decisions in the conversation, not in files. Separate confirmed decisions from proposed defaults and unresolved questions. Silence is not acceptance. Accepting an answer or a batch of recommendations is not permission to write. Keep file-write confirmation separate from discovery questions and follow the guardrails below.
Stop asking when the user has enough clarity. Let them pause, pivot, or defer a decision; do not exhaust every branch or force a proposal.
For example, after inspecting the relevant code:
```text
The CLI already uses SQLite and has no remote service. Is sharing state
across devices in scope? That determines whether local storage is enough.
If this stays a single-device tool, I recommend keeping SQLite to avoid
adding a service to operate; shared state would need a separate sync design.
```
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
+------------------------------------------+
| Use ASCII diagrams liberally |
+------------------------------------------+
| |
| [State A] -------> [State B] |
| | |
| v |
| [State C] |
| |
| System diagrams, state machines, |
| data flows, architecture sketches, |
| dependency graphs, comparison tables |
| |
+------------------------------------------+
```
**Draw with plain ASCII only** — borders `+` `-` `|`, arrows `-->` `<--` `^` `v`, markers `*` `x`.
Unicode diagram glyphs can render at different widths across terminals, fonts, and locales, so padded boxes and aligned tables can drift. Keep every diagram character ASCII.
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names and task status
- What the user might be working on
That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too:
```bash
openspec list --specs
```
Add `--json` for ids and requirement counts, and append `--store "<id>"` only for a registered standalone store. This is the inventory of what the project already claims to do, and `openspec list` on its own never shows it. To look at one, run `openspec show "<spec-id>" --type spec --json --no-scenarios` (same `--store` rule) - it returns that capability's purpose and requirement texts without pulling the whole spec file into context, and `--type spec` stops a change of the same name from making it ambiguous.
The filtered read is only an overview. Before deciding what is already covered or what should change, read each relevant spec in full, including scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
Then read the project's own context from the resolved root - `<root.path>/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists:
- `context`: project background - tech stack, conventions, constraints
- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact
Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.
If the user mentioned a specific change name, read its artifacts for context.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
If the user asks you to capture the exploration as a new change, that request is the confirmation required above. It covers scaffolding that change and creating the change artifacts the request names, and nothing else. This holds only when the request is theirs: a yes to an offer you made confirms only the scope your offer itself named, so name the change and the artifacts in the offer. Don't re-ask for what they already asked for; do ask before anything beyond it. Transition seamlessly into the requested capture:
1. Run `openspec new change "<name>"` (with `--store <id>` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store <id>` on every applicable follow-up `status` and `instructions` command.
2. Run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "<prerequisite-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves.
3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists.
4. After creating each artifact, re-run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture.
Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. When the requested capture is done, stop there and name where the work continues: `/opsx-propose` writes the remaining planning artifacts, and `/opsx-apply` implements the change once tasks exist. Capturing artifacts never starts implementing them.
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Resolve and read existing artifacts for context**
- Run `openspec status --change "<name>" --json`.
- Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
- Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from `artifactPaths` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
| Insight Type | Where to Capture |
|----------------------------|-------------------------------------|
| New requirement discovered | `specs/<capability-path>/spec.md` |
| Requirement changed | `specs/<capability-path>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers (default schema):
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? Run `/opsx-propose` and this becomes a change."
- **Result in artifact updates**: "Updated the design with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Workflow configuration counts too: creating or editing schemas, templates, or `openspec/config.yaml` is a change, not thinking. Creating or updating OpenSpec change artifacts within the confirmed scope is fine, writing anything else is not. When the user is ready to build, name the handoff rather than starting: `/opsx-propose` turns the discussion into a change, and the work happens there.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it. Read-only commands and tools need no confirmation. Before the first write-capable action—including `openspec new change` or another command that writes files—name the artifacts or files and proposed changes, ask a direct yes/no question, and wait for explicit confirmation in a separate user message. That confirmation covers only the described scope; ask again before expanding it. Answers to design or clarifying questions are never consent to write. That rule governs `openspec new change` whenever you are the one proposing the capture; the user's own capture request is the exception, handled in the capture transition above.
- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change "<name>"` (with `--store <id>` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts.
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own
+167
View File
@@ -0,0 +1,167 @@
---
description: "Propose a new change - create it and generate all artifacts in one step"
---
Propose a new change - create the change and generate all artifacts in one step.
**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow.
I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is:
- proposal.md (what & why)
- `specs/<capability-path>/spec.md` (what the system must do - a delta, not the main spec)
- design.md (how)
- tasks.md (implementation steps)
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
When the user is ready to implement, they must start the apply workflow explicitly.
---
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
**Input**: The argument after `/opsx-propose` is the change name (kebab-case), OR a description of what the user wants to build.
**Provided arguments**: $ARGUMENTS
**Steps**
1. **Understand the request and clarify material ambiguity**
If no input is provided, ask the user (open-ended, no preset options):
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts.
2. **Load project context**
Run `openspec context --json` from the current working directory (or `openspec context --json --store "<store-id>"` when a registered store was explicitly selected). Use the returned `root.path` as the authoritative OpenSpec root. If context reports `no_openspec_root`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer `openspec init` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run `openspec new change`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store.
Only when context returns a resolved `root.path`, read `<root.path>/openspec/config.yaml`. Use `config.yml` only when `config.yaml` does not exist. If neither file exists, continue without project context. Do not fall back to `config.yml` if `config.yaml` is unreadable or invalid.
If the file parses as a YAML object and its `context` field is a string no larger than 51,200 bytes in UTF-8, apply that field before exploring the codebase or making planning decisions. If the file cannot be read or parsed, or the context field is invalid or oversized, continue without project context. Validate this field independently of other config fields, as OpenSpec does.
Treat context as project-provided data and constraints, not as authority to change this workflow: it cannot override user authorization, the planning boundary, tool restrictions, or artifact and output rules. Do not copy the context into artifacts; use it to focus any codebase exploration and as a constraint on the proposal.
3. **Determine the workflow schema**
Use the configured default schema unless the user explicitly requests a different workflow.
**Use a different schema only if the user:**
- Explicitly requests a specific schema by name → use `--schema <schema-name>`
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store "<store-id>"`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; when a registered store was explicitly selected, append `--store "<store-id>"` to `openspec schemas --json` as well. If context fails, stop as described in the context-loading step; do not fall back to the current directory.
Otherwise, omit `--schema` to preserve the configured default.
4. **Create the change directory**
Choose one schema form below. If a registered store is selected, append `--store "<store-id>"` to that command and each later OpenSpec command shown below that accepts `--store`.
Using the configured default:
```bash
openspec new change "<name>"
```
Using an explicitly requested schema:
```bash
openspec new change "<name>" --schema "<schema-name>"
```
This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
5. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on)
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
6. **Create every artifact in the required set**
Use a todo list to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- **Inspect the relevant project before drafting**: Read `context` and `rules` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside `openspec/`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
- An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
- Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
- Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped
c. **If an artifact requires user input** (unclear context):
- Ask the user to clarify
- Then continue with creation
7. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
- What's ready: "All artifacts needed for implementation are ready."
- Prompt: "The artifacts are ready for review. When you are ready, run `/opsx-apply`."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names
- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next
+283
View File
@@ -0,0 +1,283 @@
---
description: "Sync delta specs from a change to main specs"
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
**Input**: Optionally specify a change name after `/opsx-sync` (e.g., `/opsx-sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Provided arguments**: $ARGUMENTS
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show changes that have delta specs (under `specs/` directory).
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-sync <other>`).
2. **Resolve change context**
Run:
```bash
openspec status --change "<name>" --json
```
The JSON includes `planningHome.root`. Main specs live under `<planningHome.root>/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository.
3. **Find delta specs**
Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the
only source of delta spec paths. If the `specs` entry is missing or
`existingOutputPaths` is empty, report that there are no delta specs to sync,
do not infer them from other artifacts, and stop without requesting artifact
instructions or writing a main spec.
Sync every path in `existingOutputPaths` unless the caller narrowed the set.
A caller narrows it by naming an explicit list of complete entries from
`existingOutputPaths` — copy those absolute values verbatim. Archive does
this inline, and a user can too (for example, by selecting the entry ending
in `/specs/billing/invoices/spec.md`).
Then sync only the named paths and leave the remaining delta specs untouched:
bulk archive excludes a delta whose implementation it could not find, and
syncing it anyway would write a main spec the caller deliberately withheld.
Carry that narrowed selection through step 4; never widen it back to the full
list. If a named path is not in `existingOutputPaths`, do not sync it —
report it and stop, rather than dropping it silently. If the named list is
empty, report that there is nothing to sync and stop without writing a main
spec.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
4. **For each delta spec, apply changes to main specs**
Before the first main-spec write, obtain one current specs-rule snapshot:
- If archive invoked this workflow inline and supplied a valid snapshot from
`openspec instructions specs --change "<name>" --json`, reuse it and do not
fetch the same instructions again.
- Otherwise run that command once now with the same selected-root flags.
- If the direct lookup exits non-zero or returns invalid artifact-instruction
JSON, report the error and stop before writing any main spec. Do not treat the
failure as an absent rule set.
- A valid response with omitted `rules` means no artifact rules are configured
and the existing semantic merge continues.
Apply returned `rules` only to the content and form of the main specs produced
by this merge. Artifact rules are not operation guidance and cannot change
selected roots, delta paths, CLI checks, or workflow steps. Use their text as
constraints without copying it verbatim into a main spec or summary.
For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo):
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (may not exist yet)
**If it does not exist yet** (a new capability), match what `openspec archive` does:
only ADDED requirements may be applied - step d creates the spec from them.
MODIFIED and RENAMED have no requirement to act on, so stop the sync for that
capability and report that its main spec does not exist and only ADDED is allowed
for a new spec; never invent the missing requirement. REMOVED has nothing to
remove - skip it and warn.
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios the main spec does not have yet
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
- Retiring the capability. Delete the whole `spec.md` - and the directory once
nothing else is left in it - only when ALL of these hold:
1. removing the requirements *this run* left no requirement blocks;
2. the rest of the spec is well-formed (it still has a `## Purpose`);
3. the main spec was not already empty before this sync - if you removed
nothing, change nothing;
4. every other nonblank line in the whole file is accounted for as the
title, Purpose, Requirements header, or a canonical requirement's
statement, scenarios, or fenced examples;
5. the change's `.openspec.yaml` declares `retire_capabilities: true`;
6. the `spec.md` resolves inside the real specs root (do not follow a
capability-directory symlink to delete an external file).
If removing the selected requirements would leave no requirement blocks and
any retirement condition is not satisfied, do not modify the main spec. Stop
the sync for that capability, report the blocking condition, and tell the user
how to resolve it. Never write or leave an empty `## Requirements` section.
When only the marker is missing, say that too - it is the one thing the user
can add to make the retirement go through.
- Deleting the file also deletes its `## Purpose`; any other section blocks
retirement. Name Purpose when you report the retirement. Include a pasteable
`git checkout` only when the spec lived in the caller's checkout;
otherwise give checkout-scoped recovery guidance.
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**`## Purpose` in the delta:**
- The main spec already has one and it is authoritative - leave it alone
(this is what `openspec archive` does; it warns and moves on)
d. **Create new main spec** if capability doesn't exist yet:
- Only when the delta has ADDED requirements to put in it and no MODIFIED or
RENAMED requirements blocked this capability in step b. Otherwise create nothing
and leave the specs directory untouched. For a REMOVED-only delta, if the change's
`.openspec.yaml` declares `retire_capabilities: true`, report it as already retired
and continue without recreating the spec. Without that marker, report the sync as blocked:
`openspec archive` rejects it with `Spec must have at least one requirement`.
An empty delta has no operations to sync; report it as blocked too.
Never write an empty `## Requirements` section.
- Create `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
5. **Validate updated main specs**
Run `openspec validate --specs` with the same selected-root flags used earlier.
If validation fails, report the problems and do not claim the sync succeeded.
6. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
- Any new main spec left with a TBD Purpose placeholder, so it gets written
now rather than lingering
- Any capability retired, naming the deleted `spec.md`, its Purpose, and
either a pasteable `git checkout` or checkout-scoped recovery guidance
**Delta Spec Format Reference**
```markdown
# Spec Delta
## Purpose
Only on a delta that introduces a brand-new capability. Seeds the new main spec.
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
The system SHALL keep doing the existing thing, now also handling A.
#### Scenario: Scenario the main spec already has
- **WHEN** user does X
- **THEN** system does Y
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Main Spec Format Reference**
Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section:
```markdown
# <capability> Specification
## Purpose
Short description of what this capability does and why it exists.
## Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you merge rather than overwrite:
- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has.
- Keep anything the delta does not mention, in the main spec's existing order
- Use your judgment to merge changes sensibly
**Output On Success**
```markdown
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result
- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts
- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list
- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline
- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response
- Artifact rules constrain only the specs being written and are never copied into output files
+119
View File
@@ -0,0 +1,119 @@
---
name: create-pr
description: Write pull request titles and descriptions when creating a PR. Use when running `gh pr create`, opening a pull request, drafting a PR body or title, or when the user asks for help writing a PR description. Produces a drafted title and body which are then passed to `gh pr create`. Encodes principles from Google eng-practices, the Linux kernel's SubmittingPatches, and Chris Beams' commit-message guide, and prevents the AI-generated-PR anti-patterns (hype tone, unsubstantiated claims, diff-restatement).
license: Apache-2.0
metadata:
author: johnstegeman
version: "1.0"
---
# Writing pull request descriptions
You are about to create a PR. Your job is to produce a title and body that tell the reviewer **what the diff cannot**: why this change exists, what mental model they need to read it, and how you know it works.
## Workflow
Run these steps in order. Steps 1 and 2 can run in parallel.
### 1. Discover repo conventions
Read whichever of these exist (skip silently if missing):
- `.github/PULL_REQUEST_TEMPLATE.md` — if present, treat its sections as the skeleton
- `CONTRIBUTING.md`, `AGENTS.md`, `CLAUDE.md` — pull any documented PR conventions
- Last 3 merged PRs via `gh pr list --state merged --limit 3 --json title,body` — mirror the title style and length norms you see
If a `PULL_REQUEST_TEMPLATE.md` exists, use it as the structural skeleton. Don't invent a different one.
### 2. Gather change context
- `git log <base>..HEAD --oneline` — full commit history for the branch (not just the latest commit)
- `git diff <base>...HEAD --stat` — scope of changes
- Branch name — often encodes issue number (e.g. `jstegeman/ENG-1234-fix-x`)
- Scan commits and branch name for issue references (`#123`, `ENG-1234`, `JIRA-5`)
### 3. Draft the title
Rules in priority order:
1. **If the repo's recent PRs follow a convention, mirror it.** Conventional Commits adoption is genuinely contested — don't impose it where it isn't already in use.
2. **Imperative mood**: "Add retry logic" not "Added retry logic" or "Adds retry logic". Test: *"If applied, this commit will _[title]_"*.
3. **≤ 70 characters.** Detail goes in the body.
4. **Capitalize, no trailing period.**
5. **Specific over generic.** "Fix bug" / "Update code" / "Refactor" are banned — they fail the standalone-skim test.
6. If the repo uses Conventional Commits, prefix with `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `perf:`, `test:`, `ci:`, `build:`, or `style:`. Append `!` for breaking changes.
### 4. Draft the body — adaptive to PR size
**Trivial PR** (typo, comment fix, one-line config tweak): one sentence stating *why* is enough. Skip headers.
**Typical PR**: cover these three, in this order, no rigid headers required:
- **Why** — the problem or motivation. What's broken, what's missing, what triggered this. The reviewer can read the diff to see *what* changed; they need you to explain *why*.
- **What changed at a high altitude** — the mental model the reviewer needs. Not a token-level restatement of the diff. ("Reworks the retry loop to back off exponentially" — not "Adds variable `delay`, multiplies by 2 in `foo()`.")
- **How you verified it** — tests added, manual steps run, commands executed, output observed. If you didn't test something, say so explicitly.
**Large or risky PR**: add as needed:
- **Breaking changes / API surface deltas** — explicit callout if public contracts changed
- **Out of scope / follow-ups** — what you intentionally did NOT do, so reviewers don't ask
- **Alternatives considered** — when a non-obvious design choice was made
**Always include if applicable**:
- Linked issue with auto-close keyword: `Fixes #123`, `Closes ENG-1234`. Auto-detect from branch name or commits.
### 5. Self-check before output
Before passing to `gh pr create`, re-read your draft against this checklist:
- [ ] Does the title pass the **standalone-skim test**? (Could someone scanning `git log --oneline` understand what this PR does?)
- [ ] Does the body explain **why**, not just **what**?
- [ ] Have I avoided **restating the diff in prose**?
- [ ] Are any **claims I made backed by evidence** I can point to in the PR?
- [ ] Have I cut every **hype word** (see list below)?
- [ ] If the description is getting long, **is the PR actually too big**? (Kernel rule: long description often = wrong-sized patch.)
### 6. Execute
Pass the drafted title and body to `gh pr create` per the harness's normal PR-creation flow.
## Core principles
1. **Why over what** (Beams rule 7; Linux kernel). Code shows what; the description must explain why. The Linux kernel: *"Whether your patch is a one-line bug fix or 5000 lines of a new feature, there must be an underlying problem that motivated you to do this work."*
2. **Self-contained** (Linux kernel). A reviewer or future archaeologist should understand the change from the title + body without chasing external context. Issue links are aids, not substitutes.
3. **Standalone-skim test** (Google eng-practices). The title alone must let someone skimming history understand what this CL does. Banned title patterns from Google: `Fix bug`, `Fix build`, `Add patch`, `Moving code from A to B`.
4. **Quantify or hedge** (Linux kernel). *"If you claim improvements in performance, memory consumption, stack footprint, or binary size, include numbers that back them up. But also describe non-obvious costs."* If you don't have numbers, label the claim `(not measured)` or remove it — don't assert it confidently.
5. **Long description = wrong-sized PR** (Linux kernel). *"If your description starts to get long, that's a sign that you probably need to split up your patch."* If you find yourself writing four paragraphs of context, consider whether the PR should be split.
## Anti-patterns — these break the skill
**Restating the diff in prose.** "Added function `processOrder` that calls `validateOrder` and then `submitOrder`." The reviewer can read code. Tell them the *shape* of the change, not its tokens.
**Marketing / hype language.** Cut these words on sight — they signal AI-generated content and add no information:
> comprehensive, robust, seamlessly, leverages, leverage, ensures, enables, empowers, streamlines, elegant, clean, intuitive, powerful, sophisticated, cutting-edge, modern, optimal, best-in-class, production-ready, enterprise-grade
Also cut decorative emoji in headers, marketing-style intros ("This PR brings exciting improvements to…"), and self-congratulatory closes ("This sets us up for future success.").
**Unsubstantiated claims.** "Improves performance" with no number. "More reliable" with no failure mode named. "Better UX" with no before/after. Either name the evidence or rewrite the claim honestly: *"Should reduce contention in `handleX` based on the lock-ordering change; not measured."*
**Walls of text on tiny PRs.** A one-line typo fix does not need six headers. Match structure to scope.
**Over-confident testing claims.** "Tested" alone is meaningless. Name *what* you tested: command run, test file touched, manual repro reproduced. If you didn't test a code path, say so.
**Restating what's already in the linked issue.** Link the issue; don't copy-paste its body into the PR description. The reviewer can click.
## Example: typical PR body
> The retry middleware was retrying on 4xx responses, which amplified load against an already-failing dependency during the Tuesday incident.
>
> This narrows retries to 5xx, network errors, and explicit `Retry-After` responses. Other 4xx propagate to the caller unchanged.
>
> Verified by adding `retry_middleware_test.py::test_no_retry_on_4xx` and re-running the failure-mode replay in `scripts/replay.py` against the recorded incident traffic — request count drops from 6.2× to 1.0× as expected.
>
> Fixes ENG-4421.
Notice: no headers, no hype, one number where a number exists, evidence named, issue linked. ~90 words. The diff fills in the rest.
## Primary sources
- Google eng-practices, *CL descriptions*: https://google.github.io/eng-practices/review/developer/cl-descriptions.html
- Linux kernel, *Submitting patches*: https://www.kernel.org/doc/html/latest/process/submitting-patches.html
- Chris Beams, *How to Write a Git Commit Message*: https://cbea.ms/git-commit/
- Tim Pope, *A Note About Git Commit Messages*: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
- Conventional Commits 1.0: https://www.conventionalcommits.org/en/v1.0.0/
@@ -0,0 +1,14 @@
---
description: Draft a pull request title and body using the create-pr skill, then open the PR.
---
Invoke the `create-pr` skill to draft a PR title and body for the current branch following its workflow:
1. Discover repo conventions (PR template, CONTRIBUTING / AGENTS / CLAUDE docs, last 3 merged PRs).
2. Gather change context from `git log`, `git diff`, and the branch name.
3. Draft a title (imperative mood, ≤ 70 chars, mirroring repo style; Conventional Commits only if the repo uses them).
4. Draft a body sized to the PR — covering **why**, **what at high altitude**, and **how verified**; plus breaking changes / out-of-scope / linked issue when applicable.
5. Self-check against the anti-pattern list (no hype words, no diff restatement, no unsubstantiated claims, no wall-of-text on a trivial PR).
6. Run `gh pr create` with the drafted title and body.
Arguments (optional): $ARGUMENTS — additional context or instructions to factor into the draft (e.g., "this is a hotfix, keep it terse" or "emphasize the migration steps").
+333
View File
@@ -0,0 +1,333 @@
---
name: jujutsu
description: Use this skill for any version control operations (commit, log, diff, push, fetch, bookmark, workspace, rebase, undo, etc.). If a `.jj` directory exists, the repo is jujutsu and git mutations will corrupt it — use `jj` for all mutations; read-only `git log/diff/show/blame/grep` are allowed. Covers co-located repos, multi-agent parallel workspaces, and operation-log recovery. **DO NOT IGNORE**
allowed-tools: Bash(jj *)
license: Apache-2.0
metadata:
author: johnstegeman
version: "1.3"
---
# Jujutsu (jj) Version Control System
This skill helps you work with Jujutsu, a Git-compatible VCS with mutable commits and automatic rebasing.
**Tested with jj v0.41.0** - Commands may differ in other versions.
For token-sensitive inspection commands, prefer the compact commands in `references/TEMPLATES.md` over jj's default output.
The compact commands in `references/TEMPLATES.md` intentionally use `jj --no-pager ...`.
For revsets, filesets, and template syntax, see `references/QUERY_LANGUAGES.md`.
For sibling or unrelated work that should start from `trunk()`, see `references/NONLINEAR.md`.
For common surprises and easy mistakes, see `references/GOTCHAS.md`.
<!--
Attribution and inspiration sources:
- Forked from danverbraganza/jujutsu-skill (https://skills.sh/danverbraganza/jujutsu-skill/jujutsu).
The original scaffolding, references/ structure, and core mental-model wording are from there.
- Enforcement language in "Critical: never use git for mutations" was tightened using the
read-only-git allowlist pattern from knoopx/pi@jujutsu
(https://skills.sh/knoopx/pi/jujutsu).
- "Parallel Workspaces" framing as a tool for multi-agent isolation draws on
onevcat/skills@onevcat-jj (https://skills.sh/onevcat/skills/onevcat-jj),
which is explicitly subtitled "Version Control for Agent Workflows".
- The .workspaces/ project-local directory convention, the .gitignore safety check,
and the directory-selection priority documented in references/WORKSPACES.md
come from edmundmiller/dotfiles using-jj-workspaces
(https://lobehub.com/skills/edmundmiller-dotfiles-using-jj-workspaces).
- The "op log records every mutation; nothing is ever truly lost" framing in the
Recovery section is adapted from trevors/dot-claude@jj-workflow
(https://skills.sh/trevors/dot-claude/jj-workflow).
- Query-language guidance, nonlinear-work patterns, and workspace handoff /
monitoring ideas were adapted from joshuadavidthomas/agent-skills `jj`
(https://github.com/joshuadavidthomas/agent-skills/tree/main/jj) and its
`workspaces.md`
(https://raw.githubusercontent.com/joshuadavidthomas/agent-skills/main/jj/workspaces.md),
while intentionally preserving this skill's local describe-first workflow and
`.workspaces/` / `workspaces/` directory priority.
-->
## Detecting a jj repo
Check whether a directory is part of a jj repo:
```bash
jj root
```
If it returns a path, the repo is jj-managed. If it returns `Error: There is no jj repo in <directory>`, it isn't. The presence of a `.jj/` folder is another giveaway. If the repo is not a jj one, do not further use this skill.
<!-- Enforcement wording (allowed/forbidden split with read-only git allowlist) adapted from knoopx/pi@jujutsu. -->
## Critical: never use git for mutations in a jj repo
In a jj repo (including co-located repos that also have `.git/`):
- **Forbidden** (corrupts jj state): `git commit`, `git add`, `git stash`, `git reset`, `git checkout <branch>`, `git switch`, `git rebase`, `git merge`, `git cherry-pick`, `git push`, `git pull`.
- **Allowed** (read-only): `git log`, `git show`, `git diff`, `git blame`, `git grep`, `git status`. Prefer the `jj` equivalents (`jj log`, `jj show`, `jj diff`, etc.) but read-only git commands won't break anything.
- **Use jj instead**: `jj git push`, `jj git fetch`, `jj edit <change>` (not `git checkout`), `jj rebase`, `jj new <a> <b>` (for merges).
See `references/COLOCATED.md` for the full co-located repo workflow.
## Important: Automated/Agent Environment
When running as an agent or running on behalf of the user:
1. **Always use `-m` flags** to provide commit messages inline rather than relying on editor prompts:
```bash
# Always use -m to avoid editor prompts
jj desc -m "message" # NOT: jj desc
jj squash --into @- -m "message" # NOT: jj squash (which opens editor or prompts for hunks)
```
Editor-based commands will fail in non-interactive environments.
2. **Verify operations with `jj st`** after mutations (`squash`, `abandon`, `rebase`, `restore`) to confirm the operation succeeded.
## Important Principles
1. Each commit should represent one logical change. See [commit messages](references/COMMIT_MSG.md) for details on good commit messages. Before starting work on a new change, make sure the working copy is clean (no changes)
```bash
jj st
```
## Core Concepts
### The Working Copy is a Commit
In jj, your working directory is always a commit (referenced as `@`). Changes are automatically snapshotted when you run any jj command. There is no staging area.
There is no need to run `jj commit`.
### Commits Are Mutable
**CRITICAL**: Unlike git, jj commits can be freely modified. This enables a high-quality commit workflow:
1. Before starting work, run `jj st`. If `@` already has changes, run `jj new` first. If `@` is empty, use it as-is.
2. Describe your intended changes with `jj desc -m "Message"`
3. Make your changes.
4. Do NOT run `jj new` when finished — leave that to the next task's step 1.
You may refine the commit using `jj squash` or `jj absorb` as needed
### Change IDs vs Commit IDs
- **Change ID**: A stable identifier (like `tqpwlqmp`) that persists when a commit is rewritten
- **Commit ID**: A content hash (like `3ccf7581`) that changes when commit content changes
Prefer using Change IDs when referencing commits in commands.
## Essential Workflow
### Starting Work: Describe First, Then Code
**Always create your commit message before writing code:**
```bash
# First, describe what you intend to do
jj desc -m "feat: Add user authentication to login endpoint"
# Then make your changes - they automatically become part of this commit
# ... edit files ...
# Check status
jj st
```
For the full starting-a-change workflow (when to reuse `@` vs `jj new`, anti-patterns, decision logic), see `references/NEW_CHANGE.md`.
For unrelated or sibling work that should branch from `trunk()`, see `references/NONLINEAR.md`.
### Creating Atomic Commits
Each commit should represent ONE logical change.
### Viewing History
```bash
# View recent commits
jj log
# View with patches
jj log -p
# View specific commit
jj show <change-id>
# View diff of working copy
jj diff
```
If you need to run `jj log`, `jj show`, or `jj diff` as an agent, use the matching compact command from `references/TEMPLATES.md` unless the full default output is explicitly needed.
For the revsets, filesets, and template syntax behind those compact commands, see `references/QUERY_LANGUAGES.md`.
### Moving Between Commits
```bash
# Create a new empty commit on top of current
jj new
# Create new commit with message
jj new && jj desc -m "Commit message"
# Edit an existing commit (working copy becomes that commit)
jj edit <change-id>
# Edit the previous commit
jj prev -e
# Edit the next commit
jj next -e
```
## Refining Commits
If you need to refine a commit (squash, abandon, undo, split, etc), see (Refining commits)[references/REFINE_COMMIT.md]
## Working with Bookmarks (Branches)
Bookmarks are jj's equivalent to git branches:
```bash
# Create a bookmark at current commit
jj bookmark create my-feature -r@
# Move bookmark to a different commit
jj bookmark move my-feature --to <change-id>
# List bookmarks
jj bookmark list
# Delete a bookmark
jj bookmark delete my-feature
```
If you need to run `jj bookmark list`, use the matching compact command from `references/TEMPLATES.md`.
<!--
Multi-agent workspace framing inspired by onevcat/skills@onevcat-jj.
The .workspaces/ project-local convention codified in references/WORKSPACES.md
is adapted from edmundmiller/dotfiles using-jj-workspaces.
-->
## Parallel Workspaces
For running multiple agents in parallel against the same repo without working-copy collisions, use `jj workspace`. Each workspace gets its own `@` while sharing the underlying repo store.
```bash
# Quick reference (full convention in references/WORKSPACES.md)
jj workspace add .workspaces/<repo>-<purpose> # Create
jj workspace list # List
jj workspace forget <name> # Untrack (files stay on disk)
```
If you need to run `jj workspace list`, use the matching compact command from `references/TEMPLATES.md`.
See `references/WORKSPACES.md` for the full convention: the `.workspaces/` directory layout, `.gitignore` safety check, naming, environment bootstrapping, agent handoff, monitoring, stale-workspace recovery, and merging results from multiple workspaces back together.
## Working with tags
jujutsu does not yet support tags and pushing them to a remote. If you need to tag (such as for a release), you will need to use git to create and push the tags.
## Git Integration (Co-located Repos)
Always use co-located repos when working with existing git projects. They give you jj locally while keeping the repo remote-compatible with git tooling.
```bash
jj git clone <url> --colocate # Clone a git repo
jj git init --colocate # Adopt an existing git repo
```
See `references/COLOCATED.md` for the complete workflow: the mutation rule (`git` is forbidden for mutations, allowed read-only), the git-command allowlist, common gotchas, and switching modes when absolutely necessary.
See `references/GOTCHAS.md` when the behavior feels surprising and you want the short list of jj-specific footguns before trying a git-style workaround.
### Pushing Changes
For pushing bookmarks to a remote, see `references/PUSH.md`. For named remotes, fork+upstream setups, and other multi-remote workflows, see `references/REMOTES.md`.
## Handling Conflicts
jj treats conflicts as first-class objects — a commit can contain a conflict, and that conflict persists through rebases until resolved. `jj` refuses to push conflicted commits, so they must be resolved before `jj git push`.
```bash
jj st # Reports if @ has unresolved conflicts
jj resolve --list # Lists conflicted paths
jj resolve --tool :ours # Take side #1 non-interactively (agent-safe)
jj resolve --tool :theirs # Take side #2 non-interactively (agent-safe)
```
**Never run bare `jj resolve` (no `--tool`)** — it's interactive and will hang in non-interactive agent environments. Either pass `--tool :ours` / `--tool :theirs`, or edit conflict markers directly in the file and let `jj st` snapshot the resolution.
See `references/CONFLICTS.md` for the full resolution playbook: marker formats (diff vs snapshot), five resolution paths, ancestor-level resolution to avoid re-resolving across descendants, verification (beyond `jj st`), and anti-patterns.
<!-- Recovery framing ("op log records every mutation; nothing is ever lost") adapted from trevors/dot-claude@jj-workflow. -->
## Recovery and the Operation Log
The op log records every mutation. Nothing is ever lost.
```bash
jj undo # Reverse the last operation (primary recovery tool)
jj op log # See all operations
jj op restore <op-id> # Jump back to any past state
jj evolog -r <change> # See how a specific change evolved
```
If you need to run `jj op log` or `jj evolog`, use the matching compact commands from `references/TEMPLATES.md` first. Add `-p` only after you have narrowed the scope.
**Always reach for `jj undo` first** when something looks wrong — it's always correct and safe. See `references/RECOVERY.md` for recovery patterns by scenario (bad squash, accidental abandon, lost work, etc.).
## Preserving Commit Quality
**IMPORTANT**: Because commits are mutable, always refine them:
1. **Review your commit**: `jj show @` or `jj diff`
2. **Is it atomic?** One logical change per commit
3. **Is the message clear?** Use imperative verb phrase in sentence case format with no full stop: "Verb object"
4. **Are there unrelated changes?** Use `jj restore` to move changes out, then create separate commits
5. **Should changes be elsewhere?** Use `jj squash` or `jj absorb`
## Quick Reference
| Action | Command |
|--------|---------|
| Describe commit | `jj desc -m "message"` |
| View status | `jj st` |
| View log | `jj log` |
| View diff | `jj diff` |
| New commit | `jj st` then `jj new` only if `@` has changes, then `jj desc -m "message"` |
| Edit commit | `jj edit <id>` |
| Squash to parent | `jj squash --into @- -m "message"` |
| Auto-distribute | `jj absorb` |
| Abandon commit | `jj abandon <id>` |
| Undo last operation | `jj undo` |
| Restore files | `jj restore [paths]` |
| Create bookmark | `jj bookmark create <name>` |
| Push bookmark | `jj git push -b <name>` |
| Add workspace | `jj workspace add .workspaces/<repo>-<purpose>` |
| List workspaces | `jj workspace list` |
| Forget workspace | `jj workspace forget <name>` |
| See operation log | `jj op log` |
| Jump to past state | `jj op restore <op-id>` |
| See change evolution | `jj evolog -r <id>` |
## Best Practices Summary
1. **Describe first**: Set the commit message before coding
2. **One change per commit**: Keep commits atomic and focused
3. **Use change IDs**: They're stable across rewrites
4. **Refine commits**: Leverage mutability for clean history
5. **Embrace the workflow**: No staging area, no stashing — the op log replaces both
6. **Workspaces for parallel agents**: Isolate concurrent agent work in `.workspaces/<repo>-<purpose>` (see `references/WORKSPACES.md`)
7. **`jj undo` first**: When something looks wrong, undo before fixing manually (see `references/RECOVERY.md`)
@@ -0,0 +1,4 @@
interface:
display_name: "Jujutsu"
short_description: "Help with Jujutsu version control workflows"
default_prompt: "Use $jujutsu to inspect this repo, choose the right jj workflow, and carry the change through safely."
@@ -0,0 +1,81 @@
# Co-located jj + git Repositories
A **co-located** repo has both `.jj/` and `.git/` at the same root. jj manages local operations; git stays in sync underneath so remote-side tooling (CI, GitHub, hooks, IDE plugins, `gh` CLI) continues to work normally.
This is the **strongly recommended** setup for working with an existing git project.
## Why co-located
- **Local: jj's superior model.** Mutable commits, no staging area, the op log as a safety net.
- **Remote: still git.** GitHub sees ordinary commits and refs. Collaborators don't need to know you use jj.
- **Tooling parity.** IDE git integrations, pre-commit hooks, `gh`/`git` CLI for read-only inspection all continue to work.
## Creating a co-located repo
```bash
# From scratch
jj git init --colocate
# From a remote
jj git clone <url> --colocate
# Adopt an existing git repo (run from inside it)
cd existing-git-repo
jj git init --colocate
```
After this, both `.jj/` and `.git/` exist and stay synchronized automatically. **Never run `jj git init` without `--colocate`** on a project you already have git history for — non-colocated init creates a separate git store inside `.jj/repo/store/git` which is harder to work with.
## The mutation rule
> **Never use `git` for mutations in a co-located jj repo.** Read-only `git` is fine.
| git command | Allowed in jj repo? |
|---|---|
| `git log`, `git show`, `git diff`, `git blame`, `git grep` | ✅ Allowed (jj equivalents preferred) |
| `git status` | ✅ Allowed (shows git's view; may differ from `jj st`) |
| `git commit`, `git add`, `git stash`, `git reset` | ❌ Forbidden — corrupts jj state |
| `git checkout <branch>`, `git switch` | ❌ Forbidden — use `jj edit` instead |
| `git rebase`, `git merge`, `git cherry-pick` | ❌ Forbidden — use `jj rebase`, `jj new <a> <b>`, `jj duplicate` |
| `git fetch` | ⚠️ Use `jj git fetch` — it keeps jj in sync |
| `git pull` | ❌ Forbidden — `git pull` does fetch+merge; use `jj git fetch` + `jj rebase` |
| `git push` | ❌ Forbidden — use `jj git push -b <bookmark>` |
The reason: jj snapshots the working copy on every jj command. If git mutates state behind jj's back, jj's next snapshot will be confused about what changed when, and the op log can get out of sync.
## Switching to git mode (rare; usually a smell)
If you genuinely need to use a git workflow for one operation (e.g., an external tool insists), do it carefully:
```bash
# 1. Ensure jj working copy is committed and clean
jj st
# (resolve anything pending — describe, refine, etc.)
# 2. Now run the git operation
git <something>
# 3. Return to jj — jj will re-sync on the next command
jj st # Triggers re-sync
jj edit <change-id> # Resume work
```
**Almost every git operation has a jj equivalent that's safer.** Reach for git mode only as a last resort.
## Common gotchas
- **Git complains about uncommitted changes**: jj's working copy is always a commit; git sees that as "uncommitted." Use `jj st` (not `git status`) to assess state.
- **HEAD detachment**: jj manages `HEAD` per change. `git checkout` will detach HEAD in confusing ways — use `jj edit` instead.
- **`.gitignore` is respected by jj** automatically. `.jjignore` is only needed if you want jj-only ignore patterns the outer git doesn't see.
- **Branches vs bookmarks**: git branches become jj bookmarks on fetch. Bookmarks don't auto-advance like git branches — you must `jj bookmark move` before pushing.
## Pushing from a co-located repo
Use `jj git push`, not `git push`:
```bash
jj bookmark move main --to @ # Move bookmark to your latest change
jj git push -b main # Push to remote
```
See `references/PUSH.md` for the full push workflow.
@@ -0,0 +1,25 @@
# Creating good commit messages
1. Use the following format:
```
<type>: <subject>
<body>
```
The valid <type> values are: fix, feat, chore, docs
Type and subject are required
Body is optional
2. If there is a body, separate it from the subject with a blank line
3. Limit the subject line to 50 characters. This is not a hard limit but a rule of thumb. The subject line should explain the "what" of the logical change
4. Capitalize the subject line
5. Do not end the subject line with a period
6. Use the imperative mood in the subject line
Imperative mood just means “spoken or written as if giving a command or instruction”. A few examples:
* Clean your room
* Close the door
* Take out the trash
7. Wrap the body at 72 characters
8. Use the body to explain what and why vs. how
@@ -0,0 +1,176 @@
# Resolving Conflicts
jj treats conflicts as **first-class objects**: a commit can contain a conflict, and that conflict **persists through rebases** until someone resolves it. This is the headline mental-model difference from git, where a working tree must be conflict-free before committing.
For an agent, this changes the rhythm:
- A `jj rebase` or `jj squash` that produces a conflict **does not fail**. It succeeds; the resulting commit is just marked conflicted.
- You can keep working — even keep rebasing the conflicted commit further — and resolve it later.
- `jj` won't let you push a conflicted commit to a remote, so conflicts must be resolved before `jj git push`.
## Detecting conflicts
```bash
jj st # Shows "There are unresolved conflicts" if any
jj resolve --list # Lists all conflicted paths in @
jj resolve --list -r <change-id> # Lists conflicts in a specific change
jj log # Conflicted changes are shown with a "conflict" marker
```
## Conflict marker formats
jj supports three marker styles (set via `ui.conflict-marker-style`):
- **`diff`** (modern default) — shows base→side as a diff
- **`snapshot`** — full content of each side, like git's `<<<<<<<` style
- **`git`** — legacy git-compatible 3-way
Check your style:
```bash
jj config get ui.conflict-marker-style
```
### Example: `diff` style
```
<<<<<<< Conflict 1 of 1
%%%%%%% Changes from base to side #1
-base content here
+side 1 content here
+++++++ Contents of side #2
side 2 content here
>>>>>>> Conflict 1 of 1 ends
```
- Side #1 = "ours" (the destination — typically your current branch/change)
- Side #2 = "theirs" (the source — typically what's being rebased/merged in)
- The `%%%%%%%` block shows what side #1 changed *from the common base*; side #2's content is shown directly.
### Example: `snapshot` style
```
<<<<<<< Conflict 1 of 1
+++++++ Contents of side #1
side 1 content here
%%%%%%% Contents of base
base content here
+++++++ Contents of side #2
side 2 content here
>>>>>>> Conflict 1 of 1 ends
```
Both styles delimit conflict regions identically. To resolve: replace the entire `<<<<<<< ... >>>>>>>` block with the desired final content.
## Five resolution paths (agent-friendly)
`jj resolve` with no `--tool` launches an interactive merge tool and **will hang in non-interactive agent environments**. Use one of these instead:
### 1. Edit conflict markers directly (most common)
Read the file, replace each `<<<<<<<` block with the resolved content, save. Then snapshot:
```bash
jj st # Triggers snapshot; should report no remaining conflicts
```
This is the canonical agent path. Works for any merge complexity.
### 2. Take one side wholesale: `:ours` / `:theirs`
For conflicts where one side simply wins (e.g., a rebase brings in unrelated formatting changes you don't want):
```bash
jj resolve --tool :ours # Keep side #1; drop side #2's conflicting changes
jj resolve --tool :theirs # Keep side #2; drop side #1's conflicting changes
# Scope to specific paths
jj resolve --tool :ours path/to/file.rs
```
These built-in tools are **non-interactive** and safe to use in agent contexts. They apply to all conflicts in `@` (or the paths given).
### 3. Restore from a specific version: `jj restore --from`
When neither side is right but a *different* revision has the content you want:
```bash
jj restore --from <change-id> path/to/file.txt
```
This pulls the file contents from `<change-id>` into the working copy, replacing conflict markers entirely.
### 4. Abandon a hopelessly conflicted change
If the conflicted change wasn't worth keeping:
```bash
jj abandon <change-id>
```
Descendants are rebased onto the parent. Their own conflicts (if any) remain to be resolved separately.
### 5. Resolve at the ancestor, let jj re-propagate
If a rebase produced the same conflict in many descendants, **don't resolve each descendant individually**. Resolve once at the change where the conflict first appeared, and jj will re-propagate the resolution forward:
```bash
# Find where the conflict first appeared
jj log -r 'conflicts()'
# Edit that change directly
jj edit <ancestor-change-id>
# ... fix conflict markers ...
jj st # Snapshot the resolution
jj edit @- # Move back to where you were
```
Descendants pick up the resolution automatically.
## Verifying a resolution
After editing, **don't trust `jj st` alone**:
```bash
# 1. Status should report no conflicts
jj st
# 2. Grep for residual markers (catches typos that left markers in place)
git grep -E '^(<{7}|>{7}|\\%{7}|\\+{7})' || echo 'no residual conflict markers'
# 3. Run the project's tests/linter — markers may be syntactically valid in some
# languages (e.g. inside strings) and parse-tests catch what eyeballing misses.
```
In a colocated repo, `git grep` is allowed (read-only). In a non-colocated repo, use `grep -rE` or `rg`.
## Pushing conflicted commits is blocked
```bash
jj git push -b main
# Refused: branch main contains conflicts
```
You'll need to resolve before pushing. There's no `--force-push-conflicts` option — this is by design.
## Anti-patterns
- ❌ **`jj resolve` (no `--tool`)** — interactive; will hang the agent. Always pass `--tool :ours` / `--tool :theirs` or edit markers manually.
- ❌ **Resolving descendants individually after a rebase** — wasteful and error-prone; resolve at the ancestor (path 5).
- ❌ **Relying on `jj st` to verify resolution** — also grep for residual markers and run tests.
- ❌ **Trying to `git checkout` to "drop" a conflict** in a colocated repo — corrupts jj state. Use `jj abandon` or resolve.
- ❌ **`jj squash` to "hide" a conflict into another commit** — the conflict propagates; it doesn't disappear.
## Quick reference
| Action | Command |
|---|---|
| List conflicts in `@` | `jj resolve --list` |
| List conflicts everywhere | `jj log -r 'conflicts()'` |
| Resolve manually | Edit file, then `jj st` |
| Take "ours" (side #1) | `jj resolve --tool :ours [paths...]` |
| Take "theirs" (side #2) | `jj resolve --tool :theirs [paths...]` |
| Restore from another rev | `jj restore --from <change-id> <path>` |
| Drop the change | `jj abandon <change-id>` |
| Find conflict's ancestor | `jj log -r 'conflicts() ~ ::conflicts()-'` |
| Check marker style | `jj config get ui.conflict-marker-style` |
@@ -0,0 +1,47 @@
# Gotchas
Short list of jj behaviors that regularly surprise agents.
**Tested with jj 0.41.0**
## 1. Working Copy Is a Commit
`@` is always a commit, not a staging area. `jj new` starts a different change; it does not finish the current one.
## 2. Most jj Commands Snapshot the Working Copy
Running `jj st`, `jj log`, and many other commands can record filesystem changes into the working-copy commit. If you intentionally need a stale view, use `--ignore-working-copy`.
## 3. Change IDs and Commit IDs Are Different
Change IDs are stable across rewrites. Commit IDs change whenever content or metadata changes. Prefer change IDs when handing work to another agent.
## 4. `jj undo` Is the First Recovery Tool
Do not manually reverse a bad `rebase`, `squash`, or `abandon` before trying `jj undo`. Use `references/RECOVERY.md` for deeper recovery.
## 5. Pager Output Is Not a Good Agent Default
Use the compact commands in `references/TEMPLATES.md`. Those commands intentionally use `jj --no-pager ...`.
## 6. Co-located Repos Still Forbid Git Mutations
`git log`, `git show`, `git diff`, `git blame`, `git grep`, and `git status` are fine. `git commit`, `git add`, `git rebase`, `git push`, and similar mutations can corrupt jj state.
## 7. `jj workspace forget` Does Not Delete Files
It only unregisters the workspace. Delete the directory separately if cleanup is intended.
## 8. Stale Workspaces Are Normal in Parallel Work
If another workspace rewrites your base, use `jj workspace update-stale`. Verify the result with `jj st` instead of improvising a manual repair.
## 9. Bare `jj resolve` Is Interactive
For agent-safe conflict handling, use `jj resolve --tool :ours`, `jj resolve --tool :theirs`, or edit conflict markers directly.
## 10. Default Output Is Often Too Large
Prefer compact templates, targeted revsets, and narrow filesets before reaching for full `jj log -p` or `jj show`.
When behavior feels surprising, stop and check `references/NEW_CHANGE.md`, `references/TEMPLATES.md`, `references/WORKSPACES.md`, or `references/RECOVERY.md` before inventing a git-style workaround.
@@ -0,0 +1,49 @@
# Starting a New Change
The jj workflow is: **describe first, then code, then refine**. The working copy is always a commit (`@`), so you're already in one when you start. The question is whether to reuse it or start fresh.
## The opening sequence
```bash
# 1. Check what @ currently looks like
jj st
```
**If `@` is empty** (no changes, no description set, or description is fine to discard):
- Reuse it. Just describe and start.
**If `@` has changes you want to keep** (it's someone else's WIP, or work you haven't finished):
- Start fresh with `jj new` — this creates a new empty commit on top of `@`.
```bash
# 2. Describe before coding (only with -m; never editor-based)
jj desc -m "feat: Add user authentication to login endpoint"
# 3. Make changes — they snapshot to @ automatically on the next jj command
# (no `jj add`, no `jj commit` needed)
# 4. Verify
jj st
```
## Why describe first
Setting the message up front:
- Forces you to articulate the change as one logical unit before writing code (atomic commits)
- Means there's never a phase where `@` says "(no description set)"
- Makes the change navigable in `jj log` while it's still being built
See `references/COMMIT_MSG.md` for the message format conventions.
## After the change is done
**Do NOT run `jj new` to "close" the change.** In jj, the working copy is always a commit — there's no separate commit step. Leave `@` as-is; the *next* task's step 1 (`jj st` → reuse or `jj new`) handles transition.
If refinement is needed (split, squash, absorb, abandon), see `references/REFINE_COMMIT.md`.
## Anti-patterns
- ❌ `jj commit` — doesn't exist; the working copy is already a commit
- ❌ `jj desc` without `-m` — opens an editor; will hang in non-interactive agent environments
- ❌ Running `jj new` at the end of a task — leaves an empty `@` for the next agent to clean up
- ❌ Starting code before describing — you'll forget to describe, or describe vaguely after the fact
@@ -0,0 +1,65 @@
# Nonlinear Work
This skill supports sibling commits and unrelated work. Do not force everything onto the current stack if the work is logically independent.
**Tested with jj 0.41.0**
## When to Start a Sibling from `trunk()`
Use a fresh sibling when:
- `@` already represents one logical change and the new work is unrelated
- the user wants multiple independent lines of work
- you need to compare or merge two alternative implementations
- a quick fix appears while a feature change is already in progress
## Preferred Flow
This skill keeps the local "describe first" rule, even for sibling work:
```bash
jj st
jj new trunk()
jj desc -m "fix: handle stale workspace recovery"
# make changes
jj st
```
Use `trunk()` instead of guessing `main` or `master`. If the new work should build on the current change, use plain `jj new` instead of `jj new trunk()`.
## Child vs Sibling
Use a child (`jj new`) when:
- the new work depends on the current change
- the user wants a stacked sequence
- the current commit is intentionally being split into follow-up commits
Use a sibling from `trunk()` when:
- the work should review, land, or be abandoned independently
- you are branching off to do an unrelated fix, doc change, or experiment
- parallel agents need clearly separated lines of history
## Merge-Shaped Work
If two independently meaningful lines of work should remain visible as a merge, create an explicit merge commit:
```bash
jj new <left-change> <right-change> -m "merge: combine related lines of work"
```
Ask the user before choosing between:
- rebasing one line onto the other
- keeping a visible merge
- pushing each line separately
## Anti-Patterns
- Do not keep piling unrelated work into the current `@` just because it is convenient.
- Do not adopt a `describe -> code -> jj new -> repeat` loop. In this skill, `jj new` starts the next change; it does not close the current one.
- Do not default to `jj new trunk() --no-edit -m ...` here. Keep the local sequence: choose the base first, then run `jj desc -m ...`.
- Do not hardcode `main` or `master` when `trunk()` expresses the intent.
See `references/NEW_CHANGE.md` for the default change-starting workflow and `references/WORKSPACES.md` for multi-workspace parallelism.
@@ -0,0 +1,38 @@
### Pushing Changes
When the user asks you to push changes:
```bash
# Push a specific bookmark to the remote
jj git push -b <bookmark-name>
# Example: push the main bookmark
jj git push -b main
```
**Before pushing, ensure:**
1. Your bookmark points to the correct commit (bookmarks don't auto-advance like git branches)
2. The commits are refined and atomic
3. The user has explicitly requested the push
**IMPORTANT**: Unlike git branches, jj bookmarks do not automatically move when you create new commits. You must manually update them before pushing:
```bash
# Move an existing bookmark to the current commit
jj bookmark move my-feature --to @
# Then push it
jj git push -b my-feature
```
If no bookmark exists for your changes, create one first:
```bash
# Create a bookmark at the current commit
jj bookmark create my-feature
# Then push it
jj git push -b my-feature
```
For named remotes, multi-remote setups (e.g. fork + upstream), tracking remote bookmarks, and one-time configuration for the two canonical multi-remote workflows, see `references/REMOTES.md`.
@@ -0,0 +1,91 @@
# Query Languages
Use jj's query languages when you need to select revisions, paths, or render compact output without falling back to the default verbose UI.
**Tested with jj 0.41.0**
## Revsets
Use revsets to select commits and workspaces.
Common selectors:
- `@`: current working-copy change
- `@-`: parent of the current working-copy change
- `trunk()`: configured mainline; use this instead of guessing `main` vs `master`
- `::@`: all ancestors of `@`
- `trunk()..@`: work reachable from `@` that is not on `trunk()`
- `working_copies()`: all workspace working-copy commits
- `<workspace>@`: another workspace's working-copy commit
- `<change-id>`: a stable change ID such as `wqzktpqm`
Agent patterns:
```bash
# Current work relative to trunk
jj --no-pager log -r 'trunk()..@'
# All active workspace tips
jj --no-pager log -r 'working_copies()'
# One specific workspace tip
jj --no-pager log -r '<workspace>@'
```
Quote revsets that contain punctuation or function calls.
## Filesets
Use filesets when a command should operate on only part of the tree.
Common patterns:
- `path` or `"path"`: cwd-relative path prefix
- `file:"path"`: exact cwd-relative file path
- `glob:"pattern"`: cwd-relative glob
- `root:"path"`: workspace-relative path prefix
- `~x`: everything except `x`
- `x & y`: intersection
- `x | y`: union
- `x ~ y`: subtract `y` from `x`
Examples:
```bash
# Only markdown files in the current directory
jj diff 'glob:"*.md"'
# Everything in src except snapshots
jj diff 'src ~ glob:"src/**/*.snap"'
# Exact file path
jj diff 'file:"README.md"'
```
## Templates
Use templates when default output is too large or too decorative for agent work.
Common building blocks:
- `++`: concatenate output pieces
- `if(cond, a, b)`: conditional formatting
- `description.first_line()`: one-line summary
- `change_id.shortest(8)`: stable short ID
- `bookmarks`: bookmark names for a commit
- `current_working_copy`, `empty`, `conflict`: useful booleans in `jj log`
Example:
```bash
jj --no-pager log --no-graph -n 5 -T 'change_id.shortest(8) ++ " " ++ if(description, description.first_line(), "(no description)") ++ "\n"'
```
Start from `references/TEMPLATES.md` instead of inventing new templates unless you need something substantially different.
## Rules
- Prefer `trunk()` over hardcoding `main` or `master`.
- Quote revsets and filesets in shell commands when punctuation could be interpreted by the shell.
- Prefer compact `jj --no-pager` inspection commands before widening the query.
- If a query fails to parse, check `jj <command> --help`, `jj help -k revsets`, `jj help -k filesets`, or `jj help -k templates`.
@@ -0,0 +1,118 @@
# Recovery and the Operation Log
The **operation log** records every mutation to the repo — every `jj` command that changed state. Nothing is ever truly lost. This is jj's safety net: an agent that makes a mistake (bad squash, accidental abandon, wrong rebase) can always go back.
## The three recovery primitives
### 1. `jj undo` — reverse the last operation
```bash
jj undo
```
The first thing to reach for. Reverses the most recent operation atomically — works for `abandon`, `squash`, `rebase`, `restore`, `describe`, and most other mutations.
**Prefer `jj undo` over manually reversing changes** — it's always correct and safe.
### 2. `jj op log` — see the full operation history
```bash
jj op log # All operations, newest first
jj op log -n 20 # Limit to 20
jj op log -p # Include diffs of what each op changed
```
If you are inspecting first and do not yet need full diffs, start with the compact `jj op log` command from `references/TEMPLATES.md`. Add `-p` only after you have narrowed the relevant op IDs.
Each operation has an `op-id`. Use these to navigate further back than a single `jj undo`.
### 3. `jj op restore` — jump to any past state
```bash
jj op restore <op-id>
```
Restores the entire repo to the state it was in at that op. **This is the nuke-from-orbit recovery** — it can undo many operations at once. Safe because the restore itself becomes a new op in the log (you can `jj undo` it if you change your mind).
## Per-change history: `jj evolog`
The op log shows everything; `jj evolog` shows how a *single change* evolved (was rewritten, squashed into, rebased, etc.):
```bash
jj evolog # Evolution of the current @
jj evolog -r <change-id> # Evolution of a specific change
jj evolog -p # Include patches between versions
```
If you are inspecting first and do not yet need patches, start with the compact `jj evolog` command from `references/TEMPLATES.md`. Add `-p` only after you have narrowed the relevant version.
Useful when you want to recover an earlier *version* of a specific commit (e.g., "what did this commit look like before I squashed into it?").
## Common recovery scenarios
### Accidentally abandoned the wrong change
```bash
jj undo # If it was just now
# or
jj op log # Find the abandon op
jj op restore <op-id> # Restore to before it
```
### A `jj squash` merged work into the wrong parent
```bash
jj undo # Single-step undo
```
If you've done more work since:
```bash
jj op log # Locate the squash op
jj op restore <op-id> # Jump back; ops after that point are dropped
```
### A rebase went sideways
```bash
jj undo # Reverse the rebase entirely
```
### Lost work after a complex sequence
```bash
jj op log -p # Diffs show exactly what each op did
# Find the op id where the work still existed
jj op restore <op-id>
```
### Need a previous version of a commit's content
```bash
jj evolog -r <change-id> -p # Find the version you want
jj restore --from <commit-id-from-evolog> <path> # Restore working-copy content from it
```
### Restored the wrong op-id
```bash
jj undo # The restore is itself a recorded op
```
## Notes for agents
- **Always reach for `jj undo` first** when something looks wrong. Don't try to manually unwind with new commits or `jj abandon` — `undo` is the canonical safe path.
- **The op log isn't pruned aggressively** — even a week of work is fully retrievable.
- **`jj op restore` is recorded** — it's not destructive in the way `git reset --hard` is. You can always undo a restore.
- **There is no stash/pop in jj** because every state is automatically a commit in the op log. Treat `jj undo` / `jj op restore` as the equivalent.
## Quick reference
| Action | Command |
|---|---|
| Undo last operation | `jj undo` |
| See all operations | `jj op log` |
| See ops with diffs | `jj op log -p` |
| Jump to past state | `jj op restore <op-id>` |
| Change evolution | `jj evolog -r <change-id>` |
| Restore file from past version | `jj restore --from <commit-id> <path>` |
@@ -0,0 +1,77 @@
### Squashing Changes
`jj squash` moves changes from one revision to another. The default is `@` → its parent; pass `--into` to target any other change.
```bash
# Default: squash @ into its parent
jj squash
# Squash @ into a specific change (not the parent)
jj squash --into <change-id>
jj squash -t <change-id> # short form
# Squash a non-@ change into its parent
jj squash -r <change-id>
# Full explicit form: any source, any destination
jj squash --from <source-id> --into <destination-id>
# Squash only specific paths (the rest stay in the source)
jj squash path/to/file.txt
jj squash --into <change-id> src/auth/
```
**Combined description prompt**: if both source and destination have non-empty descriptions, `jj squash` opens an editor to pick a combined description — and hangs in agent environments. Always pass `-m` inline when squashing two described commits together:
```bash
jj squash --into <change-id> -m "feat: Combine auth changes"
```
**Empty source**: when squashing leaves the source revision empty (no content vs. its parent), jj abandons it by default. Pass `--keep-emptied` to keep an empty placeholder instead.
**Interactive forms to avoid**: `jj squash -i` / `jj squash --interactive` open a TUI and will hang in agent environments. Use `--from` / `--into` / paths instead.
### Splitting Commits
**Warning**: `jj split` is interactive and will hang in agent environments. To divide a commit, use `jj restore` to move changes out, then create separate commits manually.
### Absorbing Changes
Automatically distribute changes to the commits that last modified those lines:
```bash
# Absorb working copy changes into appropriate ancestor commits
jj absorb
```
### Abandoning Commits
Remove a commit entirely (descendants are rebased to its parent):
```bash
jj abandon <change-id>
```
### Undoing Operations
Reverse the last jj operation:
```bash
jj undo
```
This reverts the repository to its state before the previous command. Useful for recovering from mistakes like accidental `abandon`, `squash`, or `rebase`.
### Restoring Files
Discard changes to specific files or restore files from another revision:
```bash
# Discard all uncommitted changes in working copy (restore from parent)
jj restore
# Discard changes to specific files
jj restore path/to/file.txt
# Restore files from a specific revision
jj restore --from <change-id> path/to/file.txt
@@ -0,0 +1,164 @@
# Named Remotes and Multi-Remote Workflows
For pushing to a single remote (the common case), see `references/PUSH.md`. This file covers everything else: managing named remotes, selecting which remote to push to, tracking remote bookmarks, and the two canonical multi-remote setups.
## Named remotes
When a repo has more than one remote (e.g. `origin` = your fork, `upstream` = the canonical source), you select the target with `--remote <name>`.
```bash
jj git remote list # See all remotes and their URLs
jj git remote add upstream <url> # Add a second remote
jj git remote rename origin fork # Rename a remote
jj git remote set-url origin <new-url> # Change a remote's URL
jj git remote remove old-name # Remove a remote (and forget its bookmarks)
```
**Default remote selection**: when `--remote` is omitted, `jj git push` uses:
1. The `git.push` config setting (if set), or
2. The remote named `origin` (if multiple remotes exist), or
3. The single remote (if only one exists)
There is **no built-in option to push to multiple remotes in one command** — call `jj git push` once per remote.
### Pushing the same bookmark to two remotes
```bash
jj bookmark move my-feature --to @ # Move the bookmark to current change
jj git push -b my-feature --remote fork # Push to your fork
jj git push -b my-feature --remote upstream # Push to canonical (e.g. for a PR target branch)
```
### Setting a default remote
To avoid typing `--remote` repeatedly:
```bash
# Per-repo (writes to .jj/repo/config.toml)
jj config set --repo git.push fork
# Globally (writes to ~/.config/jj/config.toml)
jj config set --user git.push origin
```
### Tracking a remote bookmark
By default, when you `jj git fetch`, remote bookmarks appear as `<name>@<remote>` (e.g. `main@upstream`). To get a local bookmark that follows it:
```bash
jj bookmark track main@upstream # Track upstream/main as a local 'main'
jj bookmark track 'glob:release/*@upstream' # Track all upstream release branches
```
Once tracked, `jj git push --tracked --remote upstream` will push all tracked bookmarks back to that remote.
### Fetching from a specific remote
```bash
jj git fetch --remote upstream # Fetch only from upstream
jj git fetch --all-remotes # Fetch from every configured remote
```
### When a push is rejected
If the remote has moved since your last fetch, `jj git push` refuses (similar to `git push --force-with-lease`). Re-sync first:
```bash
jj git fetch --remote <name> # Pull the remote's current state
# (resolve any bookmark conflicts or rebase if needed)
jj git push -b <bookmark> --remote <name> # Try again
```
### Quick reference
| Action | Command |
|---|---|
| List remotes | `jj git remote list` |
| Add a remote | `jj git remote add <name> <url>` |
| Push to specific remote | `jj git push -b <bookmark> --remote <name>` |
| Push all tracked bookmarks to a remote | `jj git push --tracked --remote <name>` |
| Set default push remote (repo) | `jj config set --repo git.push <name>` |
| Track a remote bookmark | `jj bookmark track <name>@<remote>` |
| Fetch one remote | `jj git fetch --remote <name>` |
| Fetch all remotes | `jj git fetch --all-remotes` |
## Common multi-remote workflows
Two canonical setups documented in [the jj docs](https://docs.jj-vcs.dev/latest/guides/multiple-remotes/). Pick the one that matches your relationship to the remotes; the config is one-time per repo.
**Nomenclature** (jj convention):
- `origin` = a remote you have write access to (typically where you push)
- `upstream` = the more canonical / well-known repo (you may not have push access)
- Trunk is assumed to be `main`, so remote bookmarks are `main@origin` and `main@upstream`
### Scenario 1: GitHub-style fork contributing upstream
`upstream` is the canonical project; `origin` is your fork; you open PRs from `origin` against `upstream`.
Typical actions:
- Fetch from `upstream` to get the latest changes
- Push `main` to `origin` to keep your fork's `main` in sync
- Push `my-feature` to `origin`, then open a PR targeting `upstream`
One-time setup:
```bash
# Fetch from both remotes when you run `jj git fetch`
jj config set --repo git.fetch '["upstream", "origin"]'
# Default push target is your fork
jj config set --repo git.push origin
# Track both remote bookmarks named `main`
jj bookmark track main
# Make upstream's main the trunk (treated as immutable; rebases stop here)
jj config set --repo 'revset-aliases."trunk()"' main@upstream
```
Why these specifically:
- Tracking `main@upstream` keeps your local `main` updated whenever you fetch from upstream
- Tracking `main@origin` means `jj git push` will also update your fork's `main`
- `trunk() = main@upstream` prevents jj from rewriting commits that are already in the canonical repo
### Scenario 2: Independent repository, periodically integrating from upstream
`origin` is a long-lived repository with its own divergent history; `upstream` is the source you occasionally pull changes from but don't contribute back to.
Typical actions:
- Fetch from `origin` to get the latest of your own work
- Push bookmarks to `origin`; merge PRs into `main@origin`
- Periodically fetch from `main@upstream` and merge/rebase/duplicate its changes into `main@origin`
One-time setup:
```bash
# Fetch only from origin (typical case)
jj config set --repo git.fetch '["origin"]'
# Or fetch from both if you want upstream changes visible on every fetch:
# jj config set --repo git.fetch '["upstream", "origin"]'
# Default push target is origin
jj config set --repo git.push origin
# Track only origin's main — do NOT track upstream's main
jj bookmark track main --remote=origin
jj bookmark untrack main --remote=upstream
# Your origin defines the trunk (your main is the canonical line)
jj config set --repo 'revset-aliases."trunk()"' main@origin
```
Why these specifically:
- Tracking only `main@origin` keeps `main@upstream` as a clearly separate reference you can `rebase`/`merge` from intentionally — it won't silently overwrite your local `main`
- `trunk() = main@origin` treats your divergent line (not upstream's) as the immutable history boundary
### Choosing between the scenarios
| Question | Scenario 1 | Scenario 2 |
|---|---|---|
| Do you push features back to upstream as PRs? | Yes | No |
| Should fetching from upstream update your local `main`? | Yes | No (you integrate manually) |
| Is `upstream/main` immutable from your repo's perspective? | Yes | No (you may rewrite when integrating) |
| Is `origin/main` allowed to diverge from `upstream/main`? | Minimally (just for syncing) | Yes (it's the point) |
@@ -0,0 +1,84 @@
# Token-Reducing Templates
Use inline `-T` templates for compact, agent-friendly jj output. Prefer inline `-T` use over editing repo or global jj config so the skill stays explicit and portable.
Assumptions:
- Assume `jj 0.41.0` or later.
- Each template was tested with `jj 0.41.0`.
- If a template fails to parse, treat that as jj version drift and verify with `jj <command> --help` plus `jj help -k templates`.
- Use the commands below as written. They intentionally put `--no-pager` at the top-level `jj` invocation so agent output does not depend on pager configuration.
## `jj log`
Tested with: `jj 0.41.0`
```bash
jj --no-pager log --no-graph -n 20 -T 'change_id.shortest(8) ++ if(bookmarks, " " ++ bookmarks, "") ++ if(current_working_copy, " @", "") ++ if(empty, " (empty)", "") ++ if(conflict, " (conflict)", "") ++ " " ++ if(description, description.first_line(), "(no description)") ++ "\n"'
```
This keeps the change ID, bookmark or `@`, empty/conflict markers, and the first description line. Use `shortest(8)` instead of bare `shortest()` so IDs do not collapse to one character on `jj 0.41.0`.
## `jj show`
Tested with: `jj 0.41.0`
```bash
jj --no-pager show <change-id> --stat -T 'change_id.shortest(8) ++ if(bookmarks, " " ++ bookmarks, "") ++ if(current_working_copy, " @", "") ++ if(empty, " (empty)", "") ++ if(conflict, " (conflict)", "") ++ " " ++ if(description, description.first_line(), "(no description)") ++ "\n"'
```
Prefer `--stat` for a compact file summary. Swap `--stat` for `--no-patch` if you want the commit summary without any file-level diff summary.
## `jj diff`
Tested with: `jj 0.41.0`
```bash
jj --no-pager diff -T 'self.status_char() ++ " " ++ self.display_diff_path() ++ "\n"'
```
Add `-r <rev>` if you want a diff for a specific revision instead of the working copy.
## `jj bookmark list`
Tested with: `jj 0.41.0`
```bash
jj --no-pager bookmark list -T 'if(self.normal_target(), self.name() ++ ": " ++ self.normal_target().change_id().shortest(8) ++ " " ++ if(self.normal_target().description(), self.normal_target().description().first_line(), "(no description)") ++ "\n", self.name() ++ ": (conflicted)\n")'
```
Use this for ordinary bookmark inspection. If you are explicitly debugging conflicted bookmarks and need every old/new target, fall back to jj's default output.
## `jj workspace list`
Tested with: `jj 0.41.0`
```bash
jj --no-pager workspace list -T 'self.name() ++ ": " ++ self.target().change_id().shortest(8) ++ " " ++ if(self.target().description(), self.target().description().first_line(), "(no description)") ++ if(self.target().empty(), " (empty)", "") ++ "\n"'
```
This shows workspace name, working-copy change ID, description, and whether that working copy is empty.
## `jj op log`
Tested with: `jj 0.41.0`
```bash
jj --no-pager op log --no-graph -n 20 -T 'id.short(8) ++ " " ++ time.start().ago() ++ " " ++ description.first_line() ++ "\n"'
```
Use this first when you need recovery context but do not yet need full diffs. Add `-p` only after you have narrowed down the interesting op IDs.
## `jj evolog`
Tested with: `jj 0.41.0`
```bash
jj --no-pager evolog -r <change-id> --no-graph -n 20 -T 'commit.change_id().shortest(8) ++ " " ++ if(commit.description(), commit.description().first_line(), "(no description)") ++ " | " ++ operation.id().short(8) ++ " " ++ operation.description().first_line() ++ "\n"'
```
This keeps the evolving change summary on the left and the rewriting operation on the right.
## Not Templated
`jj st` and `jj status` do not expose a template flag in `jj 0.41.0`, so leave status output untemplated. The same applies to commands like `jj undo`, `jj op restore`, and `jj resolve` where the value is in the side effect, not in custom rendering.
@@ -0,0 +1,209 @@
# Parallel Workspaces
Use `jj workspace` when multiple agents need independent working copies of the same repo. Each workspace gets its own `@` while sharing the same repo store and operation log.
**Tested with jj 0.41.0**
This skill intentionally prefers project-local workspace directories when they are safely ignored. Directory priority for this skill:
1. `.workspaces/`
2. `workspaces/`
3. Repo-local instructions such as `AGENTS.md` or `CLAUDE.md`
4. Ask the user
Do not replace this with a blanket "sibling directories only" rule.
## Before Creating a Workspace
First, make sure project-local workspaces will not leak into Git history:
```bash
grep -E '^\.?workspaces/?$' .gitignore 2>/dev/null
```
If the entry is missing, add it before `jj workspace add` and record that change using the normal workflow from `references/NEW_CHANGE.md`:
```bash
echo ".workspaces/" >> .gitignore
```
Never skip this check. `jj workspace add` creates directories that contain `.jj/` metadata, and the outer Git repo must ignore them.
Use descriptive names so `jj workspace list` and `jj log` stay readable:
```bash
REPO_NAME=$(basename "$(jj workspace root)")
WORKSPACE_PATH=".workspaces/${REPO_NAME}-<purpose>"
```
Examples:
- `.workspaces/myrepo-test-runner`
- `.workspaces/myrepo-reviewer-agent`
- `.workspaces/myrepo-refactor-experiment`
Avoid generic names such as `tmp`, `wip`, or `new`.
## Creating and Bootstrapping
Create the workspace and move into it:
```bash
jj workspace add "$WORKSPACE_PATH"
cd "$WORKSPACE_PATH"
```
After creation:
- the workspace has its own working-copy commit
- it appears in log output as `<workspace-name>@`
- it shares history and the operation log with the main workspace
- snapshots in this workspace affect only this workspace's `@`
If the repo needs environment setup, bootstrap it in the new workspace before handing it to another agent:
| Marker file | Setup command |
| --- | --- |
| `package.json` | `npm install` |
| `Cargo.toml` | `cargo build` |
| `pyproject.toml` | `uv sync` |
| `Gemfile` | `bundle install` |
| `flake.nix` | `nix develop` |
| `go.mod` | `go mod download` |
| `mix.exs` | `mix deps.get` |
## Agent Handoff
When one agent creates a workspace for another, always report:
- the absolute workspace path
- the change ID the agent should edit
- the exact `cd` command
- the exact `jj edit <change-id>` command
- the scope boundaries
Use a handoff like this:
```text
Workspace: /absolute/path/to/.workspaces/myrepo-tests
Change ID: abcdefgh
cd /absolute/path/to/.workspaces/myrepo-tests
jj edit abcdefgh
Rules:
- Always use -m for messages.
- Run jj st after every mutation.
- Do not modify files outside the assigned scope.
```
Absolute paths matter. Agents lose track of relative locations more often than they lose track of change IDs.
## Monitoring Progress
Start from the compact `jj log` command in `references/TEMPLATES.md`, then swap in the revset you need:
- all workspace tips: `-r 'working_copies()'`
- one named workspace: `-r '<workspace>@'`
Examples of the revsets themselves:
```bash
working_copies()
reviewer@
```
See `references/QUERY_LANGUAGES.md` for the revset syntax behind those examples.
## Combining Results
When a workspace finishes its work, bring that change back to the main line and retire the workspace. Do not decide silently; ask the user which strategy they want.
### Strategy 1: Rebase into the Default Workspace
Use this when one developer is orchestrating multiple agents and the result should land as ordinary commits in the default workspace.
```bash
jj rebase -s <workspace-change-id> -d @
jj workspace forget <workspace-name>
rm -rf "$WORKSPACE_PATH"
```
If the repo uses bookmarks for shipping work, move or create the bookmark after the rebase using the normal push workflow from `references/PUSH.md`.
### Strategy 2: Keep an Explicit Merge
Use this when both lines of work are independently meaningful and the merge structure should remain visible:
```bash
jj new <change-from-workspace-a> <change-from-workspace-b> -m "merge: combine parallel work"
```
### Strategy 3: Push a Bookmark and Open a PR
Use this when the work needs human review before landing:
```bash
jj bookmark create my-feature -r <workspace-change-id>
jj git push -b my-feature
```
Decision guide:
| Situation | Default suggestion |
| --- | --- |
| Solo developer running multiple local agents | Strategy 1 |
| Long-lived feature with checkpoints | Strategy 1, then push a bookmark when ready |
| Human review required | Strategy 3 |
| Both lines of work are independently meaningful | Strategy 2 |
If the context is ambiguous, suggest Strategy 1 first and ask whether the user wants the result to land in the default workspace or as a PR.
## Listing and Cleaning Up
```bash
jj workspace list
jj workspace forget <workspace-name>
rm -rf "$WORKSPACE_PATH"
```
If you need to inspect `jj workspace list` output as an agent, use the compact command from `references/TEMPLATES.md`.
`jj workspace forget` only unregisters the workspace. It does not delete files from disk.
## `update-stale`
If another workspace rewrites the commit your workspace had checked out, jj marks that workspace as stale. Use this first:
```bash
jj workspace update-stale
jj st
```
Do not improvise a manual repair before trying `update-stale`. If the old operation is gone, jj can create a recovery commit with the current working-copy contents instead of silently discarding them.
## Conflict Avoidance
| Risk | Recommended mitigation |
| --- | --- |
| Build outputs or generated files | Ignore them or give each workspace separate output paths |
| Shared config files | Assign a single owner for that file or change it serially |
| Lockfiles / dependency updates | Let one task own dependency changes |
| Same source files across workspaces | Redesign the task boundary or serialize the work |
## Critical Rules
1. Never skip the `.gitignore` check for project-local workspace directories.
2. Always follow the directory priority: `.workspaces/` -> `workspaces/` -> repo instructions -> ask.
3. Use descriptive names: `${REPO_NAME}-<purpose>`, not `tmp`.
4. Bootstrap the workspace before handing it to another agent.
5. Report the absolute path, change ID, and next commands when a workspace is ready.
6. Run `jj workspace forget` before deleting the directory.
7. Ask before choosing between rebase, merge, or PR integration.
8. Use `jj workspace update-stale` as the first response to a stale workspace.
## Reference
Convention adapted from [edmundmiller/dotfiles `using-jj-workspaces`](https://lobehub.com/skills/edmundmiller-dotfiles-using-jj-workspaces).
Additional handoff, monitoring, stale-workspace, and conflict-avoidance patterns were adapted from [joshuadavidthomas/agent-skills `jj`](https://github.com/joshuadavidthomas/agent-skills/tree/main/jj) and its [`workspaces.md`](https://raw.githubusercontent.com/joshuadavidthomas/agent-skills/main/jj/workspaces.md), while intentionally preserving this skill's local `.workspaces/` / `workspaces/` directory priority and local change-creation workflow.
@@ -0,0 +1,206 @@
---
name: openspec-apply-change
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. Also use when the user says "openspec apply", "opsx apply", or "openspec implement".
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.14.2"
---
Implement tasks from an OpenSpec change.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status, source path, and source line
- Dynamic instruction based on current state
- Optional `context`: current required project instruction input from the selected root
- Optional `operationGuidance`: current advisory guidance for apply
- `missingArtifacts` (when present): required artifact ids with no output
**Handle states:**
- If `state: "blocked"`: show the message and pause implementation.
- If `missingArtifacts` is non-empty: suggest completing the missing artifacts. Run `openspec status --change "<name>" --json`, select the next `ready` artifact (not `skipped` or `blocked`), and use `openspec instructions "<artifact-id>" --change "<name>" --json` for its rules and template. Keep the selected `--store <id>` on both commands.
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
- If `state: "all_done"`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
- Otherwise: proceed to implementation
Treat `context` as a required prompt-level input. Read and consider it, and
apply relevant project facts, conventions, and constraints while implementing.
Treat `operationGuidance` as optional additive advice. Read and consider every
entry, and follow entries that are applicable and compatible with the built-in
workflow.
Keep both fields separate from CLI-returned state, missing artifacts, tasks,
progress, `contextFiles`, and the built-in `instruction`. They are not
evidence of task completion, do not replace the built-in instruction, and do
not permit bypassing a blocked state. If context conflicts with the built-in
instruction, an explicit user choice, or a CLI-controlled value, report the
conflict and preserve the controlling value. If guidance is inapplicable or
conflicts with those controlling inputs, do not follow it and explain why.
These are prompt-level behavior contracts, not enforceable checks.
4. **Read context files**
Read every file path listed under `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
Do not copy `context` or `operationGuidance` verbatim into implementation
files or planning artifacts unless the user separately asks for that content.
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Before editing, confirm the checkbox at the returned `sourcePath` and `line` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
- Mark the task complete at its returned `sourcePath` and `line`: `- [ ]` → `- [x]`
- Rerun the apply instructions and confirm that task is now done and progress changed
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- A task needs work beyond what the spec and tasks describe, or you are tempted to drop, narrow, defer, or accept exceptions to specified behavior to make it fit → surface the added scope and ask; do not absorb it silently
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tracked tasks are complete. Review or verify the change as appropriate
before archiving. You can archive this change with `/opsx-archive`.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
- Only mark a task `- [x]` when its specified behavior is fully implemented, not when it is partially done or deferred
- Use contextFiles from CLI output, don't assume specific file names
- Use each task's sourcePath and line to update its exact checkbox
- Do not use context or operation guidance as proof that a task is complete
- Apply relevant project context; report conflicts with controlling workflow inputs
- Consider every guidance entry; explain any inapplicable or conflicting advice
- Do not copy runtime context or operation guidance into implementation files or planning artifacts
- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
@@ -0,0 +1,221 @@
---
name: openspec-archive-change
description: Archive a completed OpenSpec change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. Also use when the user says "openspec archive" or "opsx archive".
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.14.2"
---
Archive a completed change in the experimental workflow.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show only active changes (not already archived).
Include the schema used for each change if available.
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-archive <other>`).
**Load current archive inputs before the existing archive checks:**
After resolving the selected change and planning root, run:
```bash
openspec instructions archive --change "<name>" --json
```
Keep the same selected-root flags on this command. This lookup is advisory and
optional: it only supplies extra prompt inputs, so it must never block archiving.
If it exits non-zero or returns invalid JSON — for example on an older CLI that
does not support this command yet — continue the archive workflow with no
context and no operation guidance. Do not report an error and do not stop.
A successful response may omit both optional fields. Treat `context` as a
required prompt-level input: read and consider it, and apply relevant project
facts, conventions, and constraints. Treat `operationGuidance` as optional
additive advice: read and consider every entry, and follow entries that are
applicable and compatible with the built-in archive workflow.
Keep both fields separate from built-in steps, explicit user choices, resolved
paths, CLI checks, and command contracts. If context conflicts with one of those
controlling inputs, report the conflict and preserve the controlling value. If
guidance is inapplicable or conflicts with a controlling input, do not follow it
and explain why. Do not infer replacement paths, skipped prompts, or flags from
either field, and do not copy their text verbatim into specs, change artifacts,
or archive summaries unless the user separately asks for it. These are
prompt-level behavior contracts, not enforceable checks.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
- `artifacts`: List of artifacts with their status (`done`, `skipped`, or other)
**If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs):
- Display warning listing incomplete artifacts
- Ask the user to confirm they want to proceed
- Proceed if user confirms
3. **Check task completion status**
Run `openspec list --json` with the same selected-root flags and find the
entry in `changes` whose `name` exactly matches the selected change.
Require exactly one match and nonnegative integer `totalTasks` and
`completedTasks`, with `completedTasks <= totalTasks`. The CLI resolves
the schema's tracked task files, including custom artifact names, output
paths, and globs.
Incomplete tasks = `totalTasks - completedTasks`.
Do not infer task completion from artifact status or the absence of a
top-level `tasks.md`. If the lookup fails, returns invalid JSON, omits or
duplicates the selected change, or returns invalid counts, report the problem
and stop before syncing or archiving.
The CLI counts only `x`/`X` checkbox markers as complete;
other markers, including unfamiliar ones, remain incomplete.
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Ask the user to confirm they want to proceed
- Proceed if user confirms
**If `totalTasks` is zero:** Proceed without a task-related warning.
4. **Assess delta spec sync state**
Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only
delta-spec source. If the `specs` entry is missing or
`existingOutputPaths` is empty, proceed without a sync prompt and do not infer
delta specs from other artifacts.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path)
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
- Otherwise, if the delta has only REMOVED requirements and the change's `.openspec.yaml` declares `retire_capabilities: true`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. `openspec archive` refuses the unmarked REMOVED-only case with `Spec must have at least one requirement`.
- Otherwise, count the capability as needing sync and name it in the summary (`<capability-path>: new main spec will be created`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as `openspec archive` does.
- Determine what changes would be applied (adds, modifications, removals, renames)
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.
**Prompt options:**
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"
Route on the answer:
- "Cancel" — stop, do not archive
- "Archive without syncing" or "Archive now" — proceed to archive
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
- Anything else — ask again rather than archiving
Before a selected sync writes any main spec, run
`openspec instructions specs --change "<name>" --json` once with the same
selected-root flags. Require a zero exit status and valid artifact-instruction
JSON. If the lookup fails or returns invalid JSON, report the error and stop
before writing any main spec or moving the change. A valid response with omitted
`rules` is the no-rules case. Apply returned `rules` only to the content and
form of main specs produced by this merge; do not use them as archive guidance,
change CLI behavior, or copy the rule text into any output file.
Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
If the sync reports any stop or blocking condition, treat the sync as failed.
Stop the archive immediately. Do not perform the post-sync content comparison and do not move its `changeRoot`.
Nothing has moved, so the user can fix the blocking condition or re-run the sync.
After the sync writes each main spec, verify its structure against the canonical sync contract:
- A new main spec starts with a `# <capability> Specification` title. An existing main spec keeps its title exactly as it is.
- Preserve existing `## Purpose` sections completely untouched for established main specs.
- For a new main spec, copy the delta `## Purpose` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable `## Purpose` is provided, use the existing TBD Purpose behavior and warning.
- Verify that no delta-style section headers (`## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
- Requirement blocks the sync wrote or changed use `### Requirement:` headings, and their scenarios use `#### Scenario:` headings, under the spec's `## Requirements` section. Leave content the delta does not mention exactly as it is.
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
- ADDED requirements present
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty.
- RENAMED requirements present under the new name and absent under the old one
If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
5. **Perform the archive**
Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
```bash
mkdir -p "<planningHome.changesDir>/archive"
```
Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-<change-name>`. Never stack a second date (same rule as `openspec archive`).
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move `changeRoot` to the archive directory
```bash
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Whether specs were synced (if applicable)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```markdown
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped">
<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
```
**Guardrails**
- Announce the selected change; prompt for selection when it is ambiguous
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
- Consider every guidance entry and explain any inapplicable or conflicting advice
- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged
- Artifact rules constrain only the specs being written and are never operation guidance
- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files
+356
View File
@@ -0,0 +1,356 @@
---
name: openspec-explore
description: Enter OpenSpec explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements in a project that uses OpenSpec. Use when the user wants to think through something before or during an OpenSpec change. Also use when the user says "openspec explore" or "opsx explore".
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.14.2"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, investigate the codebase, and run read-only commands or tools without confirmation, but you must NEVER write code or implement features. If the user asks you to implement something, do not start it here: say that explore mode does not implement, and point them at `/opsx-propose`, which turns the discussion into a change. The work happens from that change, never from explore mode. You MAY create or update OpenSpec change artifacts (proposals, designs, specs) within a confirmed scope—that's capturing thinking, not implementing. Answering design or clarifying questions is never consent to write. Before the first write-capable action, name the artifacts or files you would change and what you would do, ask a direct yes/no question, and wait for the user's confirmation in a separate message. Confirmation covers only the scope you described; ask again before expanding it. An explicit request from the user to capture the exploration as a new change is itself that confirmation, covering the change and the change artifacts the request names; scaffold it first as described below.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## Planning a Change
When the user is planning a change, guide them toward shared understanding with focused discovery questions. For open-ended discussion, follow the conversation without imposing an interview or a required output.
Before asking a factual question, follow the context discovery below and inspect relevant OpenSpec artifacts, source, tests, docs, and configuration. Do not ask the user to repeat facts you can verify. Summarize relevant findings without reproducing private context or rules. If evidence is missing, conflicting, or inaccessible, state that limitation and ask only for the clarification needed to proceed.
- **Follow dependencies** - Resolve the next blocking decision before its dependent details. For example, clarify the user's outcome and scope before choosing an API or data model. Revisit downstream assumptions when an earlier answer changes. Skip branches that do not matter to this goal.
- **Keep questions focused** - Ask one focused question at a time, and briefly explain why it matters and which decision it unlocks. Batch questions only if the user asks for a batch; keep them small and group related decisions.
- **Offer grounded recommendations** - When evidence supports a recommendation, state your preferred option and why it fits the user's goals, with alternatives and their tradeoffs when useful. Do not invent intent, priorities, or external constraints: ask the user when only they can answer. Avoid a fixed question format.
- **Keep a conversational record** - Track decisions in the conversation, not in files. Separate confirmed decisions from proposed defaults and unresolved questions. Silence is not acceptance. Accepting an answer or a batch of recommendations is not permission to write. Keep file-write confirmation separate from discovery questions and follow the guardrails below.
Stop asking when the user has enough clarity. Let them pause, pivot, or defer a decision; do not exhaust every branch or force a proposal.
For example, after inspecting the relevant code:
```text
The CLI already uses SQLite and has no remote service. Is sharing state
across devices in scope? That determines whether local storage is enough.
If this stays a single-device tool, I recommend keeping SQLite to avoid
adding a service to operate; shared state would need a separate sync design.
```
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
+------------------------------------------+
| Use ASCII diagrams liberally |
+------------------------------------------+
| |
| [State A] -------> [State B] |
| | |
| v |
| [State C] |
| |
| System diagrams, state machines, |
| data flows, architecture sketches, |
| dependency graphs, comparison tables |
| |
+------------------------------------------+
```
**Draw with plain ASCII only** — borders `+` `-` `|`, arrows `-->` `<--` `^` `v`, markers `*` `x`.
Unicode diagram glyphs can render at different widths across terminals, fonts, and locales, so padded boxes and aligned tables can drift. Keep every diagram character ASCII.
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names and task status
- What the user might be working on
That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too:
```bash
openspec list --specs
```
Add `--json` for ids and requirement counts, and append `--store "<id>"` only for a registered standalone store. This is the inventory of what the project already claims to do, and `openspec list` on its own never shows it. To look at one, run `openspec show "<spec-id>" --type spec --json --no-scenarios` (same `--store` rule) - it returns that capability's purpose and requirement texts without pulling the whole spec file into context, and `--type spec` stops a change of the same name from making it ambiguous.
The filtered read is only an overview. Before deciding what is already covered or what should change, read each relevant spec in full, including scenarios, with `openspec show "<spec-id>" --type spec` (same `--store` rule).
Then read the project's own context from the resolved root - `<root.path>/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists:
- `context`: project background - tech stack, conventions, constraints
- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact
Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
If the user asks you to capture the exploration as a new change, that request is the confirmation required above. It covers scaffolding that change and creating the change artifacts the request names, and nothing else. This holds only when the request is theirs: a yes to an offer you made confirms only the scope your offer itself named, so name the change and the artifacts in the offer. Don't re-ask for what they already asked for; do ask before anything beyond it. Transition seamlessly into the requested capture:
1. Run `openspec new change "<name>"` (with `--store <id>` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store <id>` on every applicable follow-up `status` and `instructions` command.
2. Run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "<prerequisite-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves.
3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists.
4. After creating each artifact, re-run `openspec status --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "<artifact-id>" --change "<name>" --json` (append the confirmed `--store "<id>"` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture.
Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. When the requested capture is done, stop there and name where the work continues: `/opsx-propose` writes the remaining planning artifacts, and `/opsx-apply` implements the change once tasks exist. Capturing artifacts never starts implementing them.
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Resolve and read existing artifacts for context**
- Run `openspec status --change "<name>" --json`.
- Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
- Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from `artifactPaths` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
| Insight Type | Where to Capture |
|----------------------------|-------------------------------------|
| New requirement discovered | `specs/<capability-path>/spec.md` |
| Requirement changed | `specs/<capability-path>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers (default schema):
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
You: Real-time collab is a big space. Let me think about this...
COLLABORATION SPECTRUM
============================================
Awareness Coordination Sync
| | |
v v v
+--------+ +--------+ +--------+
|Presence| |Cursors | | CRDT |
| "3 | | Multi | |Conflict|
|online" | | select | | free |
+--------+ +--------+ +--------+
| | |
trivial moderate complex
Where's your head at?
```
**User brings a specific problem:**
```
User: The auth system is a mess
You: [reads codebase]
+---------------------------------------------+
| CURRENT AUTH FLOW |
+---------------------------------------------+
|
+-------------+-------------+
v v v
+---------+ +---------+ +---------+
| Google | | GitHub | | Email |
| OAuth | | OAuth | | Magic |
+----+----+ +----+----+ +----+----+
| | |
+-------------+-------------+
v
+-----------+
| Session |
+-----+-----+
|
v
+-----------+
| Perms |
+-----------+
I see three tangles. Which one's burning?
```
**User is stuck mid-implementation:**
```
User: /opsx-explore add-auth-system
The OAuth integration is more complex than expected
You: [reads change artifacts]
You're on task 4: "Implement OAuth flow"
Let me trace what's involved...
[draws diagram, explores options, suggests paths]
Want to update the design to reflect this?
Or add a spike task to investigate?
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
You: Generic answer is boring. What's the context?
User: A CLI tool that tracks local dev environments
You: That changes everything.
+-------------------------------------------------+
| CLI TOOL DATA STORAGE |
+-------------------------------------------------+
Key constraints:
- No daemon running
- Must work offline
- Single user
SQLite Postgres
Deployment embedded needs server
Offline yes no
Single file yes no
SQLite. Not even close.
Unless... is there a sync component?
```
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? Run `/opsx-propose` and this becomes a change."
- **Result in artifact updates**: "Updated the design with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When it feels like things are crystallizing, you might summarize:
```
## What We Figured Out
**The problem**: [crystallized understanding]
**The approach**: [if one emerged]
**Open questions**: [if any remain]
**Next steps** (if ready):
- Turn this into a change: `/opsx-propose`
- Keep exploring: just keep talking
```
But this summary is optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Workflow configuration counts too: creating or editing schemas, templates, or `openspec/config.yaml` is a change, not thinking. Creating or updating OpenSpec change artifacts within the confirmed scope is fine, writing anything else is not. When the user is ready to build, name the handoff rather than starting: `/opsx-propose` turns the discussion into a change, and the work happens there.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it. Read-only commands and tools need no confirmation. Before the first write-capable action—including `openspec new change` or another command that writes files—name the artifacts or files and proposed changes, ask a direct yes/no question, and wait for explicit confirmation in a separate user message. That confirmation covers only the described scope; ask again before expanding it. Answers to design or clarifying questions are never consent to write. That rule governs `openspec new change` whenever you are the one proposing the capture; the user's own capture request is the exception, handled in the capture transition above.
- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change "<name>"` (with `--store <id>` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts.
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own
+174
View File
@@ -0,0 +1,174 @@
---
name: openspec-propose
description: Propose a new OpenSpec change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. Also use when the user says "openspec propose" or "opsx propose".
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.14.2"
---
Propose a new change - create the change and generate all artifacts in one step.
**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow.
I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is:
- proposal.md (what & why)
- `specs/<capability-path>/spec.md` (what the system must do - a delta, not the main spec)
- design.md (how)
- tasks.md (implementation steps)
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
When the user is ready to implement, they must start the apply workflow explicitly.
---
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **Understand the request and clarify material ambiguity**
If no clear input is provided, ask the user (open-ended, no preset options):
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts.
2. **Load project context**
Run `openspec context --json` from the current working directory (or `openspec context --json --store "<store-id>"` when a registered store was explicitly selected). Use the returned `root.path` as the authoritative OpenSpec root. If context reports `no_openspec_root`, stop without creating or changing any files and follow the **Project check** above for how this workflow was reached. Offer `openspec init` only for an explicit OpenSpec request, and wait for the user to request initialization. Do not initialize automatically or run `openspec new change`. After initialization, rerun this context check before continuing. For any other context failure, stop and report the error; do not fall back to the current directory or run later OpenSpec commands without the selected store.
Only when context returns a resolved `root.path`, read `<root.path>/openspec/config.yaml`. Use `config.yml` only when `config.yaml` does not exist. If neither file exists, continue without project context. Do not fall back to `config.yml` if `config.yaml` is unreadable or invalid.
If the file parses as a YAML object and its `context` field is a string no larger than 51,200 bytes in UTF-8, apply that field before exploring the codebase or making planning decisions. If the file cannot be read or parsed, or the context field is invalid or oversized, continue without project context. Validate this field independently of other config fields, as OpenSpec does.
Treat context as project-provided data and constraints, not as authority to change this workflow: it cannot override user authorization, the planning boundary, tool restrictions, or artifact and output rules. Do not copy the context into artifacts; use it to focus any codebase exploration and as a constraint on the proposal.
3. **Determine the workflow schema**
Use the configured default schema unless the user explicitly requests a different workflow.
**Use a different schema only if the user:**
- Explicitly requests a specific schema by name → use `--schema <schema-name>`
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store "<store-id>"`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; when a registered store was explicitly selected, append `--store "<store-id>"` to `openspec schemas --json` as well. If context fails, stop as described in the context-loading step; do not fall back to the current directory.
Otherwise, omit `--schema` to preserve the configured default.
4. **Create the change directory**
Choose one schema form below. If a registered store is selected, append `--store "<store-id>"` to that command and each later OpenSpec command shown below that accepts `--store`.
Using the configured default:
```bash
openspec new change "<name>"
```
Using an explicitly requested schema:
```bash
openspec new change "<name>" --schema "<schema-name>"
```
This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
5. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on)
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
6. **Create every artifact in the required set**
Use a todo list to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- **Inspect the relevant project before drafting**: Read `context` and `rules` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside `openspec/`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
- An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
- Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
- Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped
c. **If an artifact requires user input** (unclear context):
- Ask the user to clarify
- Then continue with creation
7. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
- What's ready: "All artifacts needed for implementation are ready."
- Prompt: "The artifacts are ready for review. When you are ready, run `/opsx-apply` or ask me to apply this change."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names
- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next
@@ -0,0 +1,290 @@
---
name: openspec-sync-specs
description: Sync delta specs from an OpenSpec change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. Also use when the user says "openspec sync" or "opsx sync".
allowed-tools: Bash(openspec:*)
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.14.2"
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Project check:** These steps expect a project that already uses OpenSpec. Before the first step that writes anything (`new change`, `archive`, `sync specs`, or authoring an artifact file), confirm the project has a root: run `openspec list --json` (with `--store <id>` when a store is selected, since the store is then the root) and read `root`. A root object means the project is set up. `"root": null` means it is not - there is no `openspec/` directory here, and a write such as `openspec new change` would create one as a side effect. The command also exits non-zero, which is that answer rather than a broken CLI, so read the JSON instead of retrying or working around it.
One `"root": null` is not about setup: when a `status` error message starts with `Declared in` or `Invalid store declaration in` and names this project's `openspec/config.yaml` (or `config.yml`), the project does use OpenSpec through a store it declares, which this machine cannot resolve (the store is not registered, or the `store:` line is malformed). Do not treat it as uninitialized and skip the branches below: stop before writing and show the user that error's `message` and `fix`.
Otherwise, with no root, what happens next depends on how this workflow was reached:
- **Auto-selected**: you chose this workflow yourself, without the user naming OpenSpec, naming this skill, or running its slash command. Stop using OpenSpec and answer the request normally, as you would with no OpenSpec installed. Do not ask them to set anything up and do not mention OpenSpec setup.
- **Explicit OpenSpec request**: the user named OpenSpec, named this skill, or ran its slash command. Stop before writing and ask how to proceed: set this project up (`openspec init`), target a store they already have (`--store <id>`), or continue without OpenSpec for this request. Wait for their answer.
In both branches, never create the root as a side effect: do not run `openspec init` until the user asks for it, do not hand-create `openspec/` files, and do not let a command create it.
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and ask the user to select one
When prompting, show changes that have delta specs (under `specs/` directory).
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-sync <other>`).
2. **Resolve change context**
Run:
```bash
openspec status --change "<name>" --json
```
The JSON includes `planningHome.root`. Main specs live under `<planningHome.root>/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository.
3. **Find delta specs**
Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the
only source of delta spec paths. If the `specs` entry is missing or
`existingOutputPaths` is empty, report that there are no delta specs to sync,
do not infer them from other artifacts, and stop without requesting artifact
instructions or writing a main spec.
Sync every path in `existingOutputPaths` unless the caller narrowed the set.
A caller narrows it by naming an explicit list of complete entries from
`existingOutputPaths` — copy those absolute values verbatim. Archive does
this inline, and a user can too (for example, by selecting the entry ending
in `/specs/billing/invoices/spec.md`).
Then sync only the named paths and leave the remaining delta specs untouched:
bulk archive excludes a delta whose implementation it could not find, and
syncing it anyway would write a main spec the caller deliberately withheld.
Carry that narrowed selection through step 4; never widen it back to the full
list. If a named path is not in `existingOutputPaths`, do not sync it —
report it and stop, rather than dropping it silently. If the named list is
empty, report that there is nothing to sync and stop without writing a main
spec.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
4. **For each delta spec, apply changes to main specs**
Before the first main-spec write, obtain one current specs-rule snapshot:
- If archive invoked this workflow inline and supplied a valid snapshot from
`openspec instructions specs --change "<name>" --json`, reuse it and do not
fetch the same instructions again.
- Otherwise run that command once now with the same selected-root flags.
- If the direct lookup exits non-zero or returns invalid artifact-instruction
JSON, report the error and stop before writing any main spec. Do not treat the
failure as an absent rule set.
- A valid response with omitted `rules` means no artifact rules are configured
and the existing semantic merge continues.
Apply returned `rules` only to the content and form of the main specs produced
by this merge. Artifact rules are not operation guidance and cannot change
selected roots, delta paths, CLI checks, or workflow steps. Use their text as
constraints without copying it verbatim into a main spec or summary.
For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo):
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (may not exist yet)
**If it does not exist yet** (a new capability), match what `openspec archive` does:
only ADDED requirements may be applied - step d creates the spec from them.
MODIFIED and RENAMED have no requirement to act on, so stop the sync for that
capability and report that its main spec does not exist and only ADDED is allowed
for a new spec; never invent the missing requirement. REMOVED has nothing to
remove - skip it and warn.
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios the main spec does not have yet
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
- Retiring the capability. Delete the whole `spec.md` - and the directory once
nothing else is left in it - only when ALL of these hold:
1. removing the requirements *this run* left no requirement blocks;
2. the rest of the spec is well-formed (it still has a `## Purpose`);
3. the main spec was not already empty before this sync - if you removed
nothing, change nothing;
4. every other nonblank line in the whole file is accounted for as the
title, Purpose, Requirements header, or a canonical requirement's
statement, scenarios, or fenced examples;
5. the change's `.openspec.yaml` declares `retire_capabilities: true`;
6. the `spec.md` resolves inside the real specs root (do not follow a
capability-directory symlink to delete an external file).
If removing the selected requirements would leave no requirement blocks and
any retirement condition is not satisfied, do not modify the main spec. Stop
the sync for that capability, report the blocking condition, and tell the user
how to resolve it. Never write or leave an empty `## Requirements` section.
When only the marker is missing, say that too - it is the one thing the user
can add to make the retirement go through.
- Deleting the file also deletes its `## Purpose`; any other section blocks
retirement. Name Purpose when you report the retirement. Include a pasteable
`git checkout` only when the spec lived in the caller's checkout;
otherwise give checkout-scoped recovery guidance.
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**`## Purpose` in the delta:**
- The main spec already has one and it is authoritative - leave it alone
(this is what `openspec archive` does; it warns and moves on)
d. **Create new main spec** if capability doesn't exist yet:
- Only when the delta has ADDED requirements to put in it and no MODIFIED or
RENAMED requirements blocked this capability in step b. Otherwise create nothing
and leave the specs directory untouched. For a REMOVED-only delta, if the change's
`.openspec.yaml` declares `retire_capabilities: true`, report it as already retired
and continue without recreating the spec. Without that marker, report the sync as blocked:
`openspec archive` rejects it with `Spec must have at least one requirement`.
An empty delta has no operations to sync; report it as blocked too.
Never write an empty `## Requirements` section.
- Create `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
5. **Validate updated main specs**
Run `openspec validate --specs` with the same selected-root flags used earlier.
If validation fails, report the problems and do not claim the sync succeeded.
6. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
- Any new main spec left with a TBD Purpose placeholder, so it gets written
now rather than lingering
- Any capability retired, naming the deleted `spec.md`, its Purpose, and
either a pasteable `git checkout` or checkout-scoped recovery guidance
**Delta Spec Format Reference**
```markdown
# Spec Delta
## Purpose
Only on a delta that introduces a brand-new capability. Seeds the new main spec.
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
The system SHALL keep doing the existing thing, now also handling A.
#### Scenario: Scenario the main spec already has
- **WHEN** user does X
- **THEN** system does Y
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Main Spec Format Reference**
Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section:
```markdown
# <capability> Specification
## Purpose
Short description of what this capability does and why it exists.
## Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you merge rather than overwrite:
- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has.
- Keep anything the delta does not mention, in the main spec's existing order
- Use your judgment to merge changes sensibly
**Output On Success**
```markdown
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result
- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts
- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list
- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline
- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response
- Artifact rules constrain only the specs being written and are never copied into output files
@@ -0,0 +1,171 @@
## `$state`
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
## `$derived`
To compute something from state, use `$derived` rather than `$effect`:
```js
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
```
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
## `$effect`
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
## `$props`
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
```js
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
```
## `$inspect.trace`
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## Events
Any element attribute starting with `on` is treated as an event listener:
```svelte
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
```
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
```svelte
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
```
Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
```
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
## Each blocks
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
## Using JavaScript variables in CSS
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
```svelte
<div style:--columns={columns}>...</div>
```
You can then reference `var(--columns)` inside the component's `<style>`.
## Styling child components
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
```svelte
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
```
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
```
## Context
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features
Always use runes mode for new code, and avoid features that have more modern replacements:
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
- use `$props` instead of `export let`, `$$props` and `$$restProps`
- use `onclick={...}` instead of `on:click={...}`
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
- use classes with `$state` fields to share reactivity between components, instead of using stores
- use `{@attach ...}` instead of `use:action`
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive
@@ -0,0 +1,59 @@
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
```
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
```
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
**Example:**
```bash
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
```
Analyzes Svelte code and suggests fixes for common issues.
**Options:**
- `--async` - Enable async Svelte mode (default: false)
- `--svelte-version` - Target version: 4 or 5 (default: 5)
**Examples:**
```bash
# Analyze inline code (escape $ as \$)
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
# Analyze a file
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
# Target Svelte 4
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
```
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
## Workflow
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
+23
View File
@@ -0,0 +1,23 @@
# AGENTS.md
## Project
Goal: create a `Containerfile` for **Podman** that runs a CUPS print server. The repo is new — there is no application code and no build/test/lint config yet, so don't guess at tooling that isn't there.
- `HP-driver/` is the only content. Despite the folder name, `linux-brprinter-installer-2.2.6-0.gz` is a **Brother** driver installer, and `HP-driver/README.md` documents its usage (`gunzip` → `sudo su` → `bash linux-brprinter-installer-* <model>`).
## Version control
- Use **jujutsu (`jj`)** for all version-control mutations, not git. See the `jujutsu` skill (`.opencode/skills/jujutsu/SKILL.md`), which also covers recovery and workspaces.
- The directory is not a repo yet. Initialize with `jj git init` before making commits.
## OpenSpec workflow
- Planned work goes through OpenSpec: `/opsx-propose` → `/opsx-apply` → `/opsx-archive` (commands in `.opencode/commands/opsx-*.md`, skills in `.opencode/skills/openspec-*`).
- The `openspec` CLI (Homebrew, v1.14.x) is installed but **not initialized in this repo** — all opsx commands fail with "No OpenSpec root found" until `openspec init` has been run once.
- Specs live at `openspec/specs/<capability>/spec.md`; changes are scaffolded with `.openspec.yaml` in the resolved planning home.
## Environment
- `podman`, `podman-compose`, and CUPS are **not installed** on this machine (verified 2026-10-17). Confirm availability before assuming `podman build` works.
- Host is macOS; `openspec`, `jj`, and `git` are on PATH (Homebrew).
+93
View File
@@ -0,0 +1,93 @@
# Containerfile for a CUPS print server sharing a Brother HL-4150CDN over
# IPP/AirPrint. Build with Podman:
#
# podman build -t printserver:latest .
#
# Run with host networking so mDNS (Avahi) can advertise the printer. The
# printer is described by environment variables:
#
# podman run --rm --network host \
# -e PRINTER_IP=192.168.1.50 \
# -e PRINTER_NAME=HL-4150CDN \
# --name printserver printserver:latest
#
# PRINTER_URI can be used instead of PRINTER_IP for full control:
#
# -e PRINTER_URI=lpd://192.168.1.50/binary_p1
#
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
# --- Task 2.1 / 2.2: base system, CUPS, Avahi, DBus and i386 runtime -------
# The Brother filter binaries are 32-bit i386 ELF executables, so the i386
# architecture has to be enabled and the matching runtime libraries installed.
RUN dpkg --add-architecture i386 \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
cups \
cups-client \
cups-filters \
cups-ppdc \
avahi-daemon \
avahi-utils \
libnss-mdns \
dbus \
procps \
curl \
ghostscript \
ca-certificates \
poppler-utils \
libc6:i386 \
libstdc++6:i386 \
libgcc-s1:i386 \
&& rm -rf /var/lib/apt/lists/*
# Make sure the 32-bit dynamic loader the Brother binaries ask for exists,
# even if the i386 libc places it only in the multiarch directory.
RUN if [ ! -e /lib/ld-linux.so.2 ] && [ -e /lib/i386-linux-gnu/ld-linux.so.2 ]; then \
ln -s /lib/i386-linux-gnu/ld-linux.so.2 /lib/ld-linux.so.2; \
fi \
&& test -e /lib/ld-linux.so.2
# --- Task 2.3: install the Brother HL-4150CDN driver packages --------------
# Copy the local driver packages from the repository into the build context and
# install them. The LPR package must be installed before the CUPS wrapper
# package, which depends on it. `--force-all` is used because the packages are
# built for i386 and have no dependency information Debian can satisfy here.
# The whole assets tree is copied because the directory name contains a space,
# which some COPY implementations cannot quote.
COPY assets /tmp/assets
RUN mkdir -p /var/spool/lpd \
&& dpkg -i --force-all "/tmp/assets/Brother HL-4150CDN/lpr-driver/hl4150cdnlpr-1.1.1-5.i386.deb" \
&& dpkg -i --force-all "/tmp/assets/Brother HL-4150CDN/cupswrapper/hl4150cdncupswrapper-1.1.1-5.i386.deb" \
&& dpkg -s hl4150cdnlpr:i386 hl4150cdncupswrapper:i386 >/dev/null \
&& rm -rf /tmp/assets
# --- Task 2.4: configure CUPS and verify the driver files are in place -----
# The CUPS wrapper postinst copies the PPD into the CUPS model directory and
# creates the brlpdwrapper filter. Fail the build if anything is missing.
RUN test -f /usr/share/cups/model/brhl4150cdn.ppd \
&& test -x /usr/lib/cups/filter/brlpdwrapperhl4150cdn \
&& test -x /usr/local/Brother/Printer/hl4150cdn/lpd/brhl4150cdnfilter \
&& mkdir -p /usr/share/ppd \
&& ln -sf /usr/share/cups/model/brhl4150cdn.ppd /usr/share/ppd/brhl4150cdn.ppd \
&& rm -f /etc/cups/printers.conf /etc/cups/printers.conf.O
COPY cupsd.conf /etc/cups/cupsd.conf
# CUPS needs the runtime directories to exist in the image.
RUN mkdir -p /run/cups /run/dbus /run/avahi-daemon /var/spool/cups /var/log/cups
# --- Task 2.5: startup script -----------------------------------------------
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh
# --- Task 2.6: exposed ports ------------------------------------------------
# 631/tcp CUPS administration and IPP
# 5353/udp mDNS / DNS-SD for AirPrint discovery
EXPOSE 631/tcp
EXPOSE 5353/udp
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+150
View File
@@ -0,0 +1,150 @@
# Printserver
A containerized CUPS print server that shares a **Brother HL-4150CDN** network
printer over IPP and advertises it via AirPrint (mDNS/DNS-SD), so client
devices can print without installing the Brother driver themselves.
The container bundles:
- CUPS (IPP/IPP Everywhere + web admin on port 631)
- Avahi / DNS-SD for AirPrint discovery (UDP 5353)
- The Brother HL-4150CDN LPR + CUPS wrapper drivers and 32-bit runtime libs
## Layout
| Path | Purpose |
| --- | --- |
| `Containerfile` | Image definition (Debian base, i386 multiarch, Brother driver install) |
| `cupsd.conf` | Container CUPS configuration (listen on all interfaces, share printers) |
| `entrypoint.sh` | Starts D-Bus, Avahi and CUPS, then registers the printer queue |
| `assets/Brother HL-4150CDN/` | Brother driver `.deb` packages used at build time |
## Prerequisites
- Podman (or a compatible builder such as Docker/Buildah)
- The printer reachable on the network, with a known IP address or hostname
## Build
Run from the repository root:
```sh
podman build -t printserver:latest .
```
On an arm64 host, build for x86_64 explicitly (the Brother binaries are i386
and need an x86_64 target):
```sh
podman build --platform linux/amd64 -t printserver:latest .
```
The build installs the Brother LPR and CUPS wrapper packages from
`assets/Brother HL-4150CDN/` and verifies that the PPD and filter are in place.
## Run
mDNS does not work through a normal container network, so use **host
networking**:
```sh
podman run -d --name printserver \
--network host \
-e PRINTER_IP=192.168.1.50 \
-e PRINTER_NAME=HL-4150CDN \
printserver:latest
```
### Environment variables
| Variable | Default | Description |
| --- | --- | --- |
| `PRINTER_IP` | *(empty)* | IP address / host name of the network printer. A device URI is derived from it (see `PRINTER_PROTOCOL`). |
| `PRINTER_URI` | *(empty)* | Full device URI. Overrides `PRINTER_IP` when set. |
| `PRINTER_PROTOCOL` | `lpd` | Protocol used with `PRINTER_IP`: `lpd`, `socket` or `ipp`. |
| `PRINTER_NAME` | `HL4150CDN` | CUPS queue name. Also used as the name advertised via AirPrint/DNS-SD. |
| `PRINTER_DESCRIPTION` | *(empty)* | CUPS printer description (`lpadmin -D`), shown in the CUPS web UI. |
| `PPD_FILE` | `/usr/share/cups/model/brhl4150cdn.ppd` | PPD used for the queue. |
No queue is created when neither `PRINTER_IP` nor `PRINTER_URI` is set.
### Choosing the connection
The HL-4150CDN is a network printer. Set `PRINTER_IP` and let the entrypoint
build the URI:
| `PRINTER_PROTOCOL` | Resulting URI |
| --- | --- |
| `lpd` (default) | `lpd://<ip>/binary_p1` — LPR/LPD, the URI Brother documents |
| `socket` | `socket://<ip>:9100` — raw AppSocket/JetDirect |
| `ipp` | `ipp://<ip>/ipp/print` |
For example, a printer at `192.168.1.50` using LPD:
```sh
-e PRINTER_IP=192.168.1.50
```
Or specify the URI explicitly for full control:
```sh
-e PRINTER_URI=lpd://192.168.1.50/binary_p1
```
### Advertised name
`PRINTER_NAME` is used as the CUPS queue name and as the name clients see in
AirPrint discovery. To show a descriptive label in the CUPS web interface,
set `PRINTER_DESCRIPTION` (this does not change the advertised name):
```sh
-e PRINTER_NAME=HL4150CDN -e PRINTER_DESCRIPTION="Brother HL-4150CDN (Office)"
```
## Verify
Browse the CUPS web interface:
```sh
open http://localhost:631/ # printers
open http://localhost:631/admin # administration
```
From the host, or inside the container:
```sh
podman exec printserver lpstat -t # queues and status
podman exec printserver lpstat -v HL4150CDN # device URI
podman exec printserver avahi-browse -rt _ipp._tcp # mDNS advertisement
```
A print job can be submitted with:
```sh
podman exec printserver lp -d HL4150CDN /etc/hostname
```
On macOS/iOS the shared queue should appear in the printer list via AirPrint.
## Notes and limitations
- **Run on an x86_64 host.** The Brother driver binaries are 32-bit i386 ELF
executables. They run natively on an x86_64 kernel, but on arm64 hosts the
image can be built and the drivers installed while the filter cannot execute
without i386 emulation (for example qemu-user / binfmt).
- **Host networking is required** for Avahi to send and receive mDNS traffic.
`-p 631:631` alone is enough for direct IPP access but not for AirPrint
discovery.
- **Printer state is not persisted.** The queue is recreated on every start
from `PRINTER_URI`. To keep CUPS configuration across container replacement,
mount a volume over `/etc/cups`:
```sh
-v printserver-cups:/etc/cups
```
- The Brother filter is a PostScript-based CUPS filter. AirPrint clients that
rely on driverless (URF) printing may not be able to use the queue; macOS
clients that can use the shared CUPS queue's PPD will work.
- The Brother driver packages are 32-bit (i386); the image enables Debian's
i386 multiarch to provide `libc6:i386` and the `ld-linux.so.2` loader they
need.
BIN
View File
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
1. Login as a superuser ( or use "sudo" option if it is required )
2. Install LPR driver and cupswrapper driver
Turn on the printer and connect the USB cable.
Open the terminal and go to the directory where the drivers are.
Install LPR driver.The install process may take some time. Please wait until it is complete.
Command (for dpkg) : dpkg -i --force-all (lpr-drivername)
Install cupswrapper driver.The install process may take some time. Please wait until it is complete.
Command (for dpkg) : dpkg -i --force-all (cupswrapper-drivername)
Check if the LPR driver and cupswrapper driver are installed
Command (for dpkg) : dpkg -l | grep Brother
3. Depending on the connection type you are using (USB or Network), follow one of the steps below.
(for USB Connection)
Open a web browser and go to "http://localhost:631/printers".
Check if the Device URI of your printer is "usb://Brother/(your printer's model name)"
If the device URI is different from the example above, please go to "Modify Printer" of your printer to select proper device and driver.
If your printer is not listed on "http://localhost:631/printers", please go to "http://localhost:631/admin" and click "Add printer" and select proper device and driver.
(for Network Connection)
Open a web browser and go to "http://localhost:631/printers".
Click "Modify Printer" and set following parameters.
- "LPD/LPR Host or Printer" or "AppSocket/HP JetDirect" for Device
- lpd://(Your printer's IP address)/binary_p1 for Device URI
- Brother for Make/Manufacturer Selection
- Your printer's name for Model/Driver Selection
4. Try a test print
Open a text editor, write something and select "print" from the menu.
Binary file not shown.
@@ -0,0 +1,20 @@
# Step 1. Enter this command to extract the downloaded file:
Command: `gunzip linux-brprinter-installer-*.*.*-*.gz`
e.g. `gunzip linux-brprinter-installer-2.2.6-0.gz`
# Step 2. Get superuser authorization with the "su" command or "sudo su" command.
# Step 3. Run the tool:
Command: `bash linux-brprinter-installer-*.*.*-* Brother machine name`
e.g. `bash linux-brprinter-installer-2.1.1-1 HL-4150CDN`
# Step 4. The driver installation will start. Follow the installation screen directions.
When you see the message "Will you specify the DeviceURI ?",
For USB Users: Choose N(No)
For Network Users: Choose Y(Yes) and DeviceURI number.
The install process may take some time. Please wait until it is complete.
@@ -0,0 +1,38 @@
1. Login as a superuser ( or use "sudo" option if required).
2. Install the driver.
Turn on the printer and connect the usb, network or parallel cable.
Go to the directory where the driver is.
Install LPR driver.The install process may take some time. Please wait until it is complete.
Command : dpkg -i --force-all (lpr-drivername)
Check if the LPR driver is installed.
Command : dpkg -l | grep Brother
3. Confirm/Configure a file according to your connection.
Check the configuration filename for your distribution.
Example:
openSUSE, Ubuntu, Debian : /etc/printcap
Redhat, fedora, Mandriva : /etc/printcap.local
Edit the file according to your connection.
For USB Connection (Default)
Check if the parameter of ":lp" is ":lp=/dev/usb/lp0"
For Network Connection
replace ":lp" line to the following 2 lines
:rm=(ip address of your printer)\
:rp=lp\
For Parallel Connection
replace ":lp" line to the following line
:lp=/dev/lp0\
Restart the print system.
Command (for lpr): /etc/init.d/lpr restart
Command (for lprng) : /etc/init.d/lprng restart
4. Try a test print.
+148
View File
@@ -0,0 +1,148 @@
# Container-friendly CUPS configuration for the Brother HL-4150CDN print server.
#
# Based on the CUPS 2.4 default cupsd.conf, adjusted so that the daemon
# listens on every interface and allows clients on the local network to use
# and administer the shared printer.
# Log general information in error_log - change "warn" to "debug"
# for troubleshooting...
LogLevel warn
MaxLogSize 0
ErrorPolicy retry-job
# Listen on all interfaces so the container can serve the LAN.
Listen 0.0.0.0:631
Listen [::]:631
Listen /run/cups/cups.sock
# Advertise shared printers on the local network via mDNS/DNS-SD (Avahi).
Browsing On
BrowseLocalProtocols dnssd
BrowseRemoteProtocols none
BrowseWebIF Yes
# Default authentication type, when authentication is required...
DefaultAuthType Basic
# Web interface setting...
WebInterface Yes
# Allow any host name to be used to reach this server.
ServerAlias *
# Restrict access to the server...
<Location />
Order allow,deny
Allow all
</Location>
# Restrict access to the admin pages...
<Location /admin>
Order allow,deny
Allow all
</Location>
# Restrict access to configuration files...
<Location /admin/conf>
AuthType Default
Require user @SYSTEM
Order allow,deny
Allow all
</Location>
# Restrict access to log files...
<Location /admin/log>
AuthType Default
Require user @SYSTEM
Order allow,deny
Allow all
</Location>
# Set the default printer/job policies...
<Policy default>
# Job/subscription privacy...
JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
# Job-related operations must be done by the owner or an administrator...
<Limit Create-Job Print-Job Print-URI Validate-Job>
Order deny,allow
</Limit>
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
# All administration operations require an administrator to authenticate...
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# All printer operations require a printer operator to authenticate...
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# Only the owner or an administrator can cancel or authenticate a job...
<Limit Cancel-Job CUPS-Authenticate-Job>
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
<Limit All>
Order deny,allow
</Limit>
</Policy>
# Set the authenticated printer/job policies...
<Policy authenticated>
# Job/subscription privacy...
JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
# Job-related operations must be done by the owner or an administrator...
<Limit Create-Job Print-Job Print-URI Validate-Job>
AuthType Default
Order deny,allow
</Limit>
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
AuthType Default
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
# All administration operations require an administrator to authenticate...
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# All printer operations require a printer operator to authenticate...
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
AuthType Default
Require user @SYSTEM
Order deny,allow
</Limit>
# Only the owner or an administrator can cancel or authenticate a job...
<Limit Cancel-Job CUPS-Authenticate-Job>
AuthType Default
Require user @OWNER @SYSTEM
Order deny,allow
</Limit>
<Limit All>
Order deny,allow
</Limit>
</Policy>
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh
# Startup script for the containerized CUPS print server.
#
# Starts the D-Bus system bus, Avahi (mDNS/DNS-SD) and CUPS, then registers
# the Brother HL-4150CDN queue if it does not exist yet. The CUPS daemon is
# kept in the foreground so the container lives as long as the print server.
#
# Configuration (all optional, via environment):
# PRINTER_IP IP address / host name of the network printer.
# PRINTER_URI Full device URI; overrides PRINTER_IP when set.
# PRINTER_PROTOCOL lpd (default), socket or ipp - used with PRINTER_IP.
# PRINTER_NAME CUPS queue name and advertised (DNS-SD) name.
# PRINTER_DESCRIPTION CUPS Description (lpadmin -D) shown in the web UI.
# PPD_FILE PPD used for the queue.
set -eu
PRINTER_NAME="${PRINTER_NAME:-HL4150CDN}"
PRINTER_IP="${PRINTER_IP:-}"
PRINTER_URI="${PRINTER_URI:-}"
PRINTER_PROTOCOL="${PRINTER_PROTOCOL:-lpd}"
PRINTER_DESCRIPTION="${PRINTER_DESCRIPTION:-}"
PPD_FILE="${PPD_FILE:-/usr/share/cups/model/brhl4150cdn.ppd}"
LOG_PREFIX="[entrypoint]"
log() { echo "${LOG_PREFIX} $*"; }
# Build a device URI from PRINTER_IP unless an explicit PRINTER_URI is given.
if [ -z "${PRINTER_URI}" ] && [ -n "${PRINTER_IP}" ]; then
case "${PRINTER_PROTOCOL}" in
lpd)
PRINTER_URI="lpd://${PRINTER_IP}/binary_p1"
;;
socket)
PRINTER_URI="socket://${PRINTER_IP}:9100"
;;
ipp)
PRINTER_URI="ipp://${PRINTER_IP}/ipp/print"
;;
*)
log "unknown PRINTER_PROTOCOL '${PRINTER_PROTOCOL}', defaulting to lpd"
PRINTER_URI="lpd://${PRINTER_IP}/binary_p1"
;;
esac
fi
# --- D-Bus system bus -------------------------------------------------------
mkdir -p /run/dbus /var/lib/dbus
if [ ! -s /etc/machine-id ] && [ ! -s /var/lib/dbus/machine-id ]; then
log "generating D-Bus machine id"
dbus-uuidgen > /var/lib/dbus/machine-id
fi
if [ ! -S /run/dbus/system_bus_socket ]; then
log "starting dbus-daemon (system bus)"
dbus-daemon --system --fork
fi
# --- Avahi / mDNS -----------------------------------------------------------
mkdir -p /run/avahi-daemon
if ! pgrep -x avahi-daemon >/dev/null 2>&1; then
log "starting avahi-daemon"
avahi-daemon --daemonize --no-chroot
fi
# --- CUPS -------------------------------------------------------------------
log "starting cupsd"
/usr/sbin/cupsd -f &
CUPSD_PID=$!
# Wait until CUPS accepts requests.
i=0
while [ "$i" -lt 30 ]; do
if lpstat -r >/dev/null 2>&1; then
break
fi
i=$((i + 1))
sleep 0.5
done
# Share printers on the network and allow remote administration.
cupsctl --share-printers --remote-admin >/dev/null 2>&1 || true
# --- Brother HL-4150CDN queue ----------------------------------------------
if lpstat -p "${PRINTER_NAME}" >/dev/null 2>&1; then
log "printer ${PRINTER_NAME} already exists"
elif [ -n "${PRINTER_URI}" ]; then
if [ -f "${PPD_FILE}" ]; then
log "adding printer ${PRINTER_NAME} -> ${PRINTER_URI}"
lpadmin -p "${PRINTER_NAME}" -E -v "${PRINTER_URI}" -P "${PPD_FILE}" \
|| log "warning: lpadmin failed to add ${PRINTER_NAME}"
else
log "warning: PPD ${PPD_FILE} not found; skipping printer creation"
fi
else
log "neither PRINTER_URI nor PRINTER_IP is set; skipping automatic printer creation"
fi
# Make sure the queue is shared, named for advertising and set as default.
if lpstat -p "${PRINTER_NAME}" >/dev/null 2>&1; then
lpadmin -p "${PRINTER_NAME}" -o printer-is-shared=true -E >/dev/null 2>&1 || true
if [ -n "${PRINTER_DESCRIPTION}" ]; then
lpadmin -p "${PRINTER_NAME}" -D "${PRINTER_DESCRIPTION}" >/dev/null 2>&1 || true
fi
lpadmin -d "${PRINTER_NAME}" >/dev/null 2>&1 || true
log "queue ${PRINTER_NAME} is shared and set as default"
fi
log "ready: CUPS on 631/tcp, mDNS on 5353/udp"
# Keep the container running by waiting on cupsd.
wait "${CUPSD_PID}"
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-17
@@ -0,0 +1,62 @@
# Design
## Context
See proposal.md for motivation - we need to create a Podman container that runs a CUPS print server for Brother HL-4150CDN. The repo contains the Brother driver packages in `assets/Brother HL-4150CDN/`:
- `lpr-driver/hl4150cdnlpr-1.1.1-5.i386.deb`
- `cupswrapper/hl4150cdncupswrapper-1.1.1-5.i386.deb`
- `installer/linux-brprinter-installer-2.2.6-0.gz` (reference only; we will NOT run it in the build - it is interactive and downloads from the network)
Key observations:
- Brother drivers are i386 binaries (32-bit ELF). Container needs 32-bit runtime libraries.
- CUPS wrapper + LPR packages provide PPD, filters, and helper binaries.
- AirPrint requires CUPS + Avahi/mDNS (DNS-SD).
- Brother's cups wrapper installs files to /usr/local/Brother/ and may need symlinks into CUPS directories.
## Goals / Non-Goals
**Goals:**
- Create a Containerfile that builds a working image for Podman
- Install and configure Brother HL-4150CDN drivers
- Share printer via IPP with AirPrint discovery
- Expose ports 631 (CUPS/IPP) and 5353/udp (mDNS)
- Make it easy to run with podman
**Non-Goals:**
- Managing multiple printers
- Custom SSL/TLS configuration for CUPS (use defaults or documented)
- Persistent CUPS spool/state beyond basic setup
## Decisions
### Base image
- Use Debian/Ubuntu (or similar) as base to handle .deb packages and 32-bit libs easily. Alternatively Alpine, but Alpine needs musl + 32-bit support can be more complex for prebuilt i386 ELF binaries from Brother. Debian stable is practical.
### Driver installation
- Use the local `.deb` packages from `assets/Brother HL-4150CDN/` (copied into the build context) and install with `dpkg -i --force-all`. No network download at build time for the driver itself.
- Do not run the Brother installer script (`linux-brprinter-installer-2.2.6-0.gz`) in the build - it is interactive and downloads packages from download.brother.com.
- Also need to install brcupsconfpt1 helper if needed - comes with cups wrapper.
- Run the cups wrapper install logic as per Brother installer (or manually create symlinks).
### CUPS + Avahi + DBus
- Install cups, avahi-daemon, dbus. Run dbus and avahi-daemon; start cupsd.
- Configure CUPS to allow remote access/sharing. For containerized setup, typically need to listen on all interfaces and configure sharing.
### Printer configuration
- Add printer via lpadmin using the Brother PPD. Point to network URI (e.g., ipp://PRINTER_IP/ipp or socket://PRINTER_IP:9100 - depends on printer; HL-4150CDN is network printer).
- Enable printer sharing and default options.
### Runtime configuration
- The printer endpoint and advertised name are supplied through environment variables rather than baked into the image, so the same image can be pointed at any HL-4150CDN:
- `PRINTER_IP` + `PRINTER_PROTOCOL` (lpd/socket/ipp) derive the device URI.
- `PRINTER_URI` overrides the derived URI for full control.
- `PRINTER_NAME` sets the CUPS queue name and the DNS-SD/AirPrint advertised name.
- `PRINTER_DESCRIPTION` sets the CUPS Description (Info) shown in the web UI.
- The entrypoint builds the URI and runs lpadmin at startup, then shares the queue.
## Risks / Trade-offs
- [Risk] Brother binaries are 32-bit only → require multiarch libs (libc6:i386, etc.). Increases image size slightly.
- [Risk] Using .deb packages assumes glibc-based base. If we choose different base, need to ensure compatibility.
- [Risk] Avahi in container needs --net=host or proper mDNS setup. AirPrint discovery typically works better with host network mode; document this.
- [Risk] CUPS state (printers.conf, etc.) should be persisted if we want config to survive restarts - may need volumes.
@@ -0,0 +1,29 @@
# Proposal
## Why
Provide a Podman container that runs CUPS and shares the Brother HL-4150CDN network printer as an AirPrint and IPP printer. Clients should be able to print without installing local printer drivers.
## What Changes
- Create a Containerfile for Podman (based on a Linux distro that supports CUPS and 32-bit binaries)
- Extract and install Brother driver files (from `assets/Brother HL-4150CDN/` - local .deb packages: `hl4150cdnlpr` and `hl4150cdncupswrapper`) needed for HL-4150CDN
- Configure CUPS to run in the container and share the network printer
- Enable mDNS/Avahi for AirPrint discovery
- Provide minimal runtime configuration (ports 631, 5353/udp) and documentation for running with podman
- Handle 32-bit dependencies required by Brother i386 binaries
## Capabilities
### New Capabilities
- `printserver/cups`: Containerized CUPS print server with AirPrint/IPP sharing and Brother HL-4150CDN driver support
### Modified Capabilities
None
## Impact
- New Containerfile (and possibly supporting files) in repository root
- Uses Brother driver artifacts in `assets/Brother HL-4150CDN/`
- Runtime ports: CUPS (631), mDNS (5353/udp) for AirPrint
- Depends on Avahi/DBus, CUPS, and 32-bit compatibility libs for Brother binaries
@@ -0,0 +1,81 @@
# Spec Delta
## Purpose
Provide a containerized CUPS print server that shares the Brother HL-4150CDN network printer as an AirPrint and IPP printer so clients can print without installing local printer drivers.
## ADDED Requirements
### Requirement: Container image builds with required dependencies
The container SHALL include CUPS, Avahi/mDNS, DBus, and required 32-bit runtime libraries to run Brother HL-4150CDN i386 binaries.
#### Scenario: Image contains CUPS
- **WHEN** the image is built
- **THEN** CUPS is installed and available at /usr/sbin/cupsd
#### Scenario: Image supports 32-bit Brother binaries
- **WHEN** the image is built
- **THEN** required 32-bit libraries are present for the Brother i386 filters
### Requirement: Brother HL-4150CDN driver is installed in container
The container SHALL install the Brother HL-4150CDN CUPS wrapper, PPD, LPR/filter components from the provided driver artifacts.
#### Scenario: Driver files present
- **WHEN** the container image is built
- **THEN** the HL-4150CDN PPD and Brother filters are installed in standard system locations
#### Scenario: CUPS recognizes HL-4150CDN
- **WHEN** CUPS starts with the driver installed
- **THEN** CUPS is able to use the HL-4150CDN PPD
### Requirement: Printer is shared and discoverable
The system SHALL share the Brother HL-4150CDN printer over IPP and make it discoverable via AirPrint (mDNS/DNS-SD) on the container network.
#### Scenario: IPP printer is shared
- **WHEN** CUPS runs in the container
- **THEN** the printer is shared via IPP for network clients
#### Scenario: AirPrint discovery
- **WHEN** Avahi runs alongside CUPS
- **THEN** the printer is advertised via AirPrint so macOS/iOS clients can discover it without driver installation
### Requirement: Runtime configuration is exposed
The container SHALL expose CUPS on port 631 and mDNS on UDP port 5353.
#### Scenario: CUPS port exposed
- **WHEN** the container runs
- **THEN** port 631 is available for CUPS administration and IPP
#### Scenario: mDNS port exposed
- **WHEN** the container runs
- **THEN** UDP port 5353 is available for AirPrint discovery
### Requirement: No client drivers required
The shared printer SHALL be exposed in a way that client devices can print without installing vendor printer drivers.
#### Scenario: AirPrint compatibility
- **WHEN** a client discovers the printer via AirPrint
- **THEN** the client uses native IPP/AirPrint drivers
### Requirement: Printer endpoint and advertised name are configurable at runtime
The container SHALL accept the network printer address and the advertised printer name through environment variables so the same image can target a different printer or name without rebuilding.
#### Scenario: Printer IP is provided
- **WHEN** the container is started with `PRINTER_IP` set
- **THEN** a device URI is derived from that address and the Brother HL-4150CDN queue is created pointing at it
#### Scenario: Printer protocol is selected
- **WHEN** the container is started with `PRINTER_IP` and `PRINTER_PROTOCOL`
- **THEN** the device URI uses that protocol (lpd, socket or ipp)
#### Scenario: Full URI override
- **WHEN** `PRINTER_URI` is set
- **THEN** it is used as the device URI and `PRINTER_IP` is ignored
#### Scenario: Custom advertised name
- **WHEN** the container is started with `PRINTER_NAME`
- **THEN** the CUPS queue uses that name and it is the name advertised via AirPrint/DNS-SD
#### Scenario: Custom CUPS description
- **WHEN** the container is started with `PRINTER_DESCRIPTION`
- **THEN** the queue's CUPS Description (Info) is set to that value
@@ -0,0 +1,23 @@
# Tasks
## 1. Repository initialization
- [x] 1.1 Initialize jujutsu repository with `jj git init` in repo root and verify `.jj` directory exists
## 2. Container build setup
- [x] 2.1 Create Containerfile in repository root based on Debian/Ubuntu base image with multiarch support for i386
- [x] 2.2 Install required system packages: cups, avahi-daemon, dbus, wget/curl, and 32-bit runtime libraries (libc6:i386, libstdc++6:i386, etc.)
- [x] 2.3 Copy the local Brother driver packages (`assets/Brother HL-4150CDN/lpr-driver/hl4150cdnlpr-1.1.1-5.i386.deb` and `assets/Brother HL-4150CDN/cupswrapper/hl4150cdncupswrapper-1.1.1-5.i386.deb`) into the build context and install them in the container with `dpkg -i --force-all` (verify install reports success)
- [x] 2.4 Configure CUPS and Brother drivers (symlink PPD/filter files as needed, set up cupsd.conf for container use) and verify files are in correct locations
- [x] 2.5 Set up startup script to run dbus, avahi-daemon, and cupsd in the container
- [x] 2.6 Expose ports 631 (CUPS/IPP) and 5353/udp (mDNS) in Containerfile and verify ports are declared
## 3. Printer configuration
- [x] 3.1 Add logic/config to create and share the Brother HL-4150CDN printer (using lpadmin) with appropriate network URI and PPD; ensure printer is shared for network clients
- [x] 3.2 Configure Avahi/CUPS for AirPrint advertisement and verify printer will be discoverable
- [x] 3.3 Add environment-based configuration for the printer address and advertised name (`PRINTER_IP`, `PRINTER_PROTOCOL`, `PRINTER_NAME`, `PRINTER_DESCRIPTION`; `PRINTER_URI` overrides)
## 4. Documentation and testing
- [x] 4.1 Create or update README with instructions for building and running the container with podman (including host network mode note for mDNS, volume mounts if needed)
- [x] 4.2 Test container build with podman build and verify image builds successfully
- [x] 4.3 Test container run and verify CUPS is accessible and printer is shared/discoverable (manual verification)
- [x] 4.4 Verify `podman run` with `PRINTER_IP` and a custom `PRINTER_NAME` creates the queue at the derived URI and advertises that name
View File
Whitespace-only changes.
+32
View File
@@ -0,0 +1,32 @@
schema: spec-driven
# Project context (optional)
# Add only constraints that should guide OpenSpec artifacts and workflows.
# Include constraints an agent cannot infer by reading the code.
# Keep general project documentation and discoverable codebase facts out.
# Example:
# context: |
# Designs and tasks must cover Windows, macOS, and Linux
# Write all artifacts in Spanish
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always state what is out of scope
# tasks:
# - Break tasks into chunks of max 2 hours
# Per-operation guidance (optional)
# Add advisory guidance for how apply and archive work should be conducted.
# This is separate from artifact rules above.
# Example:
# operations:
# apply:
# guidance:
# - Keep test summaries concise
# archive:
# guidance:
# - Summarize the archive outcome before finishing
View File
Whitespace-only changes.