diff --git a/docs/opsx.md b/docs/opsx.md
index bebe0a51d..e0d825d68 100644
--- a/docs/opsx.md
+++ b/docs/opsx.md
@@ -95,6 +95,10 @@ rules:
- Use Given/When/Then format for scenarios
design:
- Include sequence diagrams for complex flows
+ apply:
+ - Run tests before marking tasks done
+ archive:
+ - Verify specs are synced before archiving
```
### Config Fields
@@ -103,7 +107,7 @@ rules:
|-------|------|-------------|
| `schema` | string | Default schema for new changes (e.g., `spec-driven`) |
| `context` | string | Project context injected into all artifact instructions |
-| `rules` | object | Per-artifact rules, keyed by artifact ID |
+| `rules` | object | Per-artifact and workflow-phase rules, keyed by artifact ID or workflow phase (`apply`, `archive`) |
### How It Works
@@ -114,14 +118,15 @@ rules:
4. Default (`spec-driven`)
**Context injection:**
-- Context is prepended to every artifact's instructions
-- Wrapped in `...` tags
+- Context is injected into artifact instructions, apply instructions, and archive instructions
+- Wrapped in `...` tags
- Helps AI understand your project's conventions
**Rules injection:**
-- Rules are only injected for matching artifacts
+- Artifact keys (e.g. `proposal`, `tasks`) are injected only into matching artifact instructions
+- Workflow keys (`apply`, `archive`) are injected into the corresponding workflow instruction surface
- Wrapped in `...` tags
-- Appear after context, before the template
+- Appear after the built-in instruction content
### Artifact IDs by Schema
@@ -131,6 +136,22 @@ rules:
- `design` — Technical design
- `tasks` — Implementation tasks
+### Workflow Phase Keys
+
+In addition to artifact IDs, the `rules` field accepts workflow phase keys that apply during that phase rather than during artifact creation:
+
+- `apply` — Rules applied during `/opsx:apply` (implementation phase)
+- `archive` — Rules applied during `/opsx:archive` and `/opsx:bulk-archive`
+
+These keys do not generate "unknown artifact" warnings. The workflow agent reads them via:
+
+```bash
+openspec instructions apply --change "" --json
+openspec instructions archive --json
+```
+
+Both commands return a JSON object with optional `context` and `rules` fields alongside the phase-specific template. When present, the agent applies them as behavioral constraints without copying them into any output file.
+
### Config Validation
- Unknown artifact IDs in `rules` generate warnings
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/.openspec.yaml b/openspec/changes/extend-config-injection-to-apply-archive/.openspec.yaml
new file mode 100644
index 000000000..8d87be18e
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-07
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/design.md b/openspec/changes/extend-config-injection-to-apply-archive/design.md
new file mode 100644
index 000000000..f2265b383
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/design.md
@@ -0,0 +1,98 @@
+## Context
+
+Project config (`openspec/config.yaml`) supports two injection fields: a top-level `context` string and a `rules` map keyed by artifact ID. Currently, `generateInstructions()` in `instruction-loader.ts` reads both fields and surfaces them in artifact instruction output as `` and `` blocks.
+
+`generateApplyInstructions()` in `src/commands/workflow/instructions.ts` builds apply output from the schema's `apply` block and task progress, but does not read or surface project config. The archive instruction surface is a static template in `src/core/templates/workflows/archive-change.ts`; it is not generated dynamically and receives no config input.
+
+Rule target validation runs in `validateConfigRules()` (`project-config.ts`), which checks each key in `rules` against the schema's known artifact IDs and emits a warning for any unrecognized key. `apply` and `archive` are not currently registered as valid targets, so a user who writes `rules.apply` today gets a spurious warning.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Inject project `context` into apply and archive instruction output alongside artifacts.
+- Inject `rules.apply` into apply instruction output, and `rules.archive` into archive instruction output.
+- Register `apply` and `archive` as known workflow rule targets so `validateConfigRules()` accepts them without warning.
+- Keep built-in apply/archive safety behavior (task-progress checks, archive readiness checks) unaffected and higher-priority than injected config guidance.
+- Preserve full backward compatibility for all existing artifact rule keys and existing config shapes.
+
+**Non-Goals:**
+- Extending injection to `verify` or `sync`.
+- Changing the `context` field structure or adding new config fields.
+- Making built-in safety checks configurable or bypassable via rules.
+
+## Decisions
+
+### D1: Filter workflow keys before artifact rule validation
+
+Define `WORKFLOW_RULE_TARGETS = new Set(['apply', 'archive'])` in `project-config.ts`, importing `WorkflowId` from `profiles.ts`. At the `validateConfigRules()` call site in `instruction-loader.ts`, strip workflow keys from `rules` before passing the map to the validator, so only artifact-targeted keys are checked against schema artifact IDs.
+
+`validateConfigRules()` itself is unchanged and never receives workflow keys. The `WorkflowId` type parameter gives compile-time safety: if `'apply'` or `'archive'` is ever removed or renamed in `ALL_WORKFLOWS`, the Set initializer becomes a type error.
+
+*Alternative considered:* adding `'apply'` and `'archive'` into `validArtifactIds` at the call site. Rejected — `validArtifactIds` becomes a misnomer and the validator's error message would list workflow phases as if they were artifacts.
+
+### D2: Extend apply instructions to carry context and rules
+
+The full apply instruction path has three layers that all need updating:
+
+1. **`generateApplyInstructions()`** — add `context` and `rules` to the return type. Call `readProjectConfig()` and extract `context` and `rules.apply`. Config guidance is appended after the built-in content (task list, progress state, schema instruction) so built-in content remains the leading section.
+
+2. **`applyInstructionsCommand()`** — already does `JSON.stringify(instructions)`, so `context` and `rules` appear in JSON output automatically once the return type includes them. No additional change needed here.
+
+3. **`printApplyInstructionsText()`** — add rendering of `` and `` blocks using the same pattern as `printInstructionsText()` for artifacts.
+
+*Alternative considered:* inline config text directly into the instruction string inside `generateApplyInstructions()`. Rejected — mixes data with presentation, prevents callers from filtering or reformatting independently.
+
+### D3: Add archive instruction generation as a new CLI path
+
+`openspec instructions archive` currently falls into the CLI handler's else branch and calls `instructionsCommand('archive', ...)`, which looks for an artifact named `archive` in the schema and fails. Archive needs its own generation path analogous to apply:
+
+1. **CLI handler** — add an `archive` branch alongside the existing `apply` branch, routing to a new `archiveInstructionsCommand()`.
+
+2. **`generateArchiveInstructions(projectRoot)`** — reads project config, retrieves the static archive template via `getArchiveChangeSkillTemplate()`, and returns `{ template, context?, rules? }` with `context` and `rules.archive` as separate fields. No `changeName` parameter is needed because the output depends only on the project config, not on any specific change.
+
+3. **`archiveInstructionsCommand()`** — mirrors `applyInstructionsCommand()`: calls `generateArchiveInstructions()`, serializes to JSON with `--json`, or calls `printArchiveInstructionsText()` for text output.
+
+4. **`printArchiveInstructionsText()`** — renders the static template content followed by `` and `` blocks, keeping the built-in template text as the leading section.
+
+*Alternative considered:* embed placeholders inside the static template string. Rejected — brittle string interpolation in a long template is harder to test and couples the template to the injection mechanism.
+
+### D4: Config guidance placement preserves built-in priority
+
+Built-in behavior (archive readiness checks in `ArchiveCommand.execute()`, apply state computation in `generateApplyInstructions()`) runs first and its output is emitted before any config guidance. Config context and rules are a trailing addendum. This means:
+
+- Built-in safety preconditions (all tasks done, specs synced) are hard checks in `ArchiveCommand.execute()` — config rules cannot suppress or override them.
+- The instruction text the user sees leads with the built-in contract, followed by project-specific guidance.
+
+### D5: Skill templates explicitly surface injected fields as AI constraints
+
+The three workflow skill templates need to tell the AI agent what to do with the `context` and `rules` fields returned by instruction commands. The templates are the authoritative source of agent behavior — generated command files are derived from them.
+
+- **`apply-change.ts`** — add `context` and `rules` to the Step 3 JSON field list so the agent knows to expect them; add a constraint stating the agent must apply them as behavioral guidance without copying their content into any output file.
+- **`archive-change.ts`** — add a new step before the main archive steps: call `openspec instructions archive --json` and consume the returned `context` and `rules` as constraints for the entire workflow. Built-in readiness checks (artifact completion, task completion, spec sync) are still executed regardless.
+- **`bulk-archive-change.ts`** — add a one-time call to `openspec instructions archive --json` at the start of the batch; apply the returned `context` and `rules` as constraints across all changes. Call is made once, not once per change.
+
+*Alternative considered:* patch generated command files directly. Rejected — generated files are overwritten by `openspec sync` and would lose the changes.
+
+### D6: Workflow targets documented inline in generated config
+
+`config-prompts.ts` generates the initial `openspec/config.yaml` via `serializeConfig()`. The `rules` comment block currently shows only artifact-key examples (`proposal`, `tasks`). Adding `apply` and `archive` as commented examples in the same block ensures users discover workflow targets alongside artifact targets without requiring separate documentation.
+
+The same comment block is the single place to update; no separate documentation file or migration guide change is needed for discoverability.
+
+## Risks / Trade-offs
+
+**Validation warning surfacing for apply/archive** — the proposal requires validation failures to be surfaced to callers of apply/archive instruction generation. For artifacts, `generateInstructions()` logs warnings via `console.warn` and continues. The same approach applies here: `generateApplyInstructions()` and `generateArchiveInstructions()` log warnings for malformed rule values (e.g., non-array entries) and continue with the valid subset. Truly unknown rule keys (not artifact IDs and not workflow targets) are caught upstream at the `validateConfigRules()` call in `instruction-loader.ts` and do not reach apply/archive generation.
+→ No new warning mechanism needed; reuse existing `console.warn` pattern.
+
+**`validateConfigRules` call site timing** — validation currently runs at instruction-generation time when artifact IDs are known. Passing `WORKFLOW_RULE_TARGETS` into the same call is sufficient; no config-read-time validation change is needed.
+→ No mitigation needed beyond the constant.
+
+**`readProjectConfig` called independently for apply and archive** — config is read per instruction generation call. Two separate invocations each read the file; this matches existing artifact behavior and is acceptable for typical usage.
+→ No change needed.
+
+**Static archive template length** — the archive template is a long string. Appending config guidance as a separate labeled section (rather than modifying the template internals) keeps the template itself stable and avoids formatting surprises.
+→ Design already accounts for this via the wrapper approach in D3.
+
+## Open Questions
+
+None — proposal scope and alfred's implementation constraints (no artifact-rule warnings for workflow targets; built-in safety prompts outrank config guidance) are fully addressed by D1–D4.
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/proposal.md b/openspec/changes/extend-config-injection-to-apply-archive/proposal.md
new file mode 100644
index 000000000..adcb5197e
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/proposal.md
@@ -0,0 +1,49 @@
+## Why
+
+Project config currently applies `context` and `rules` only when generating artifact instructions. Teams lose those same project constraints once they move into `/opsx:apply` and `/opsx:archive`, which makes the workflow inconsistent and forces users to restate guidance that is already present in `openspec/config.yaml`.
+
+We should reuse the existing config model so project-level guidance stays available across the most common post-artifact actions, without requiring a new config format or changing existing artifact behavior.
+
+## What Changes
+
+- Extend `context` injection so the existing project context is available in apply instructions and archive workflow instructions, not only artifact instructions.
+- Extend `rules` injection so the existing `rules` object can also provide workflow-specific guidance for `apply` and `archive` while preserving current artifact-targeted behavior.
+- Keep existing artifact instruction behavior unchanged and backward compatible.
+- Preserve the current archive command safety checks and prompts; this change is about instruction surfaces, not new archive command enforcement semantics.
+- Do not expand this change to `verify` or `sync`; this proposal only covers `apply` and `archive`.
+
+### Config Structure
+
+- Continue using the existing top-level `context` field, and inject that project context into `apply` and `archive` instruction surfaces in addition to artifact instructions.
+- Reserve `rules.apply` for workflow guidance injected into `/opsx:apply` instructions.
+- Reserve `rules.archive` for workflow guidance injected into `/opsx:archive` instructions.
+- Keep existing artifact keys such as `rules.specs`, `rules.design`, and `rules.tasks` unchanged for backward compatibility.
+
+### Validation and Error Handling
+
+- Extend config validation so reserved workflow targets are accepted alongside artifact keys.
+- Keep malformed or unknown rule targets as validation errors with actionable messages so users can correct `openspec/config.yaml` before running workflow instructions.
+- Apply artifact rules only to matching artifact instructions, and apply workflow rules only to their corresponding workflow instruction surfaces.
+- Surface validation failures to callers of apply/archive instruction generation and cover those cases with unit and integration tests.
+
+## Capabilities
+
+### New Capabilities
+- `cli-archive-instructions`: `openspec instructions archive` becomes a valid CLI call, returning a JSON object with `template`, `context`, and `rules` fields analogous to apply instructions.
+
+### Modified Capabilities
+- `context-injection`: extend project context injection from artifact-only instructions to apply and archive instruction surfaces
+- `rules-injection`: extend the existing rules model so it can guide apply and archive in addition to matching artifacts; update generated config examples to document `rules.apply` and `rules.archive` as valid targets
+- `cli-artifact-workflow`: include injected context and rules in apply instruction output; update apply skill template to consume and apply context/rules from instruction JSON
+- `opsx-archive-skill`: include injected context and rules in archive workflow guidance while preserving current archive readiness checks and prompts
+- `opsx-bulk-archive-skill`: fetch archive instructions once at workflow start to obtain project context and rules as constraints for the entire batch
+
+## Impact
+
+- `src/core/project-config.ts` and related validation will need to recognize workflow targets while remaining backward compatible for artifact keys.
+- `src/commands/workflow/instructions.ts` and related types will need to surface injected context/rules for apply instructions, and a new archive instruction generation path will be added.
+- `src/cli/index.ts` will need an `archive` branch in the `instructions` command handler to route to the new archive instruction path.
+- `src/core/templates/workflows/apply-change.ts`, `archive-change.ts`, and `bulk-archive-change.ts` will need to consume `context` and `rules` from instruction output and treat them as AI constraints.
+- `src/core/config-prompts.ts` will need updated comments and examples to show `rules.apply` and `rules.archive` as valid targets alongside artifact keys.
+- `docs/opsx.md`, config examples, and relevant specs/tests will need updates so the documented behavior matches the new instruction surfaces.
+- User-facing docs should include examples showing `rules.apply` and `rules.archive` alongside existing artifact rules, plus migration guidance for teams deciding when to keep artifact-specific guidance versus move workflow guidance into phase-specific targets.
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-archive-instructions/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-archive-instructions/spec.md
new file mode 100644
index 000000000..455e4b46c
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-archive-instructions/spec.md
@@ -0,0 +1,43 @@
+## ADDED Requirements
+
+### Requirement: Archive Instructions Command
+
+The system SHALL accept `archive` as a valid subcommand of `openspec instructions`, returning archive workflow instructions alongside any injected project config.
+
+#### Scenario: Generate archive instructions with no project config
+
+- **WHEN** user runs `openspec instructions archive`
+- **AND** no `openspec/config.yaml` exists
+- **THEN** the system outputs archive workflow instructions without context or rules sections
+
+#### Scenario: Generate archive instructions with project context
+
+- **WHEN** user runs `openspec instructions archive`
+- **AND** `openspec/config.yaml` contains a `context` field
+- **THEN** the system outputs archive workflow instructions followed by a `` section containing the config context
+
+#### Scenario: Generate archive instructions with workflow rules
+
+- **WHEN** user runs `openspec instructions archive`
+- **AND** `openspec/config.yaml` contains `rules.archive` with one or more entries
+- **THEN** the system outputs archive workflow instructions followed by a `` section listing each rule
+
+#### Scenario: Archive instructions JSON output
+
+- **WHEN** user runs `openspec instructions archive --json`
+- **THEN** the system outputs JSON with:
+ - `template`: the archive workflow instruction text
+ - `context`: project context string if present in config, omitted otherwise
+ - `rules`: array of rule strings from `rules.archive` if present, omitted otherwise
+
+#### Scenario: Archive instructions JSON with no workflow rules
+
+- **WHEN** user runs `openspec instructions archive --json`
+- **AND** config has `rules` with only artifact keys (e.g. `rules.specs`) but no `rules.archive`
+- **THEN** JSON output omits the `rules` field
+
+#### Scenario: Archive instructions JSON with no context
+
+- **WHEN** user runs `openspec instructions archive --json`
+- **AND** config has no `context` field
+- **THEN** JSON output omits the `context` field
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-artifact-workflow/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-artifact-workflow/spec.md
new file mode 100644
index 000000000..369c58181
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/cli-artifact-workflow/spec.md
@@ -0,0 +1,69 @@
+## ADDED Requirements
+
+### Requirement: Apply Skill Template Consumes Context and Rules
+
+The apply skill template SHALL instruct the AI to read `context` and `rules` from the apply instructions JSON output and apply them as behavioral constraints during implementation, without copying them into any output file.
+
+#### Scenario: Apply skill reads context from instructions JSON
+
+- **WHEN** the apply skill fetches `openspec instructions apply --change --json`
+- **AND** the JSON includes a `context` field
+- **THEN** the skill applies that context as background knowledge during task implementation
+
+#### Scenario: Apply skill reads rules from instructions JSON
+
+- **WHEN** the apply skill fetches `openspec instructions apply --change --json`
+- **AND** the JSON includes a `rules` field
+- **THEN** the skill applies those rules as constraints during task implementation
+
+#### Scenario: Context and rules are not written into implementation output
+
+- **WHEN** the apply skill uses context and rules from the instructions JSON
+- **THEN** the context and rules content does not appear in any code files, task file updates, or other implementation output
+
+## MODIFIED Requirements
+
+### Requirement: Apply Instructions Command
+
+The system SHALL generate schema-aware apply instructions via `openspec instructions apply`, including injected project context and workflow rules when present in config.
+
+#### Scenario: Generate apply instructions
+
+- **WHEN** user runs `openspec instructions apply --change `
+- **AND** all required artifacts (per schema's `apply.requires`) exist
+- **THEN** the system outputs:
+ - `contextFiles` mapping artifact IDs to arrays of concrete paths for all existing artifacts
+ - Schema-specific instruction text
+ - Progress information derived from the tracking file (if `apply.tracks` is set)
+
+#### Scenario: Apply instructions include project context when configured
+
+- **WHEN** user runs `openspec instructions apply --change `
+- **AND** `openspec/config.yaml` contains a `context` field
+- **THEN** output includes a `` section after the built-in instruction content
+
+#### Scenario: Apply instructions include workflow rules when configured
+
+- **WHEN** user runs `openspec instructions apply --change `
+- **AND** `openspec/config.yaml` contains `rules.apply` with one or more entries
+- **THEN** output includes a `` section after the built-in instruction content
+
+#### Scenario: Apply blocked by missing artifacts
+
+- **WHEN** user runs `openspec instructions apply --change `
+- **AND** required artifacts are missing
+- **THEN** the system indicates apply is blocked
+- **AND** lists which artifacts must be created first
+
+#### Scenario: Apply instructions JSON output
+
+- **WHEN** user runs `openspec instructions apply --change --json`
+- **THEN** the system outputs JSON with:
+ - `contextFiles`: object mapping artifact IDs to arrays of concrete paths for existing artifacts
+ - `instruction`: the apply instruction text
+ - `progress`: task progress summary derived from the tracking file when present
+ - `tasks`: parsed task list with completion state when a tracking file exists
+ - `state`: current apply state (`blocked`, `ready`, or `all_done`)
+ - `missingArtifacts`: list of required artifacts when apply is blocked by missing artifacts, omitted otherwise
+ - `context`: project context string if present in config, omitted otherwise
+ - `rules`: array of rule strings from `rules.apply` if present, omitted otherwise
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/context-injection/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/context-injection/spec.md
new file mode 100644
index 000000000..32f055241
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/context-injection/spec.md
@@ -0,0 +1,37 @@
+## MODIFIED Requirements
+
+### Requirement: Inject context into all artifact instructions
+
+The system SHALL inject the context field from project config into instructions for all artifacts and workflow instruction surfaces, wrapped in XML-style `` tags.
+
+#### Scenario: Config has context field
+
+- **WHEN** config contains `context: "Tech stack: TypeScript, React"`
+- **THEN** instruction output includes `\nTech stack: TypeScript, React\n`
+
+#### Scenario: Config has no context field
+
+- **WHEN** config omits the context field or context is undefined
+- **THEN** instruction output does not include `` tags
+
+#### Scenario: Context is multi-line string
+
+- **WHEN** config contains context with multiple lines
+- **THEN** instruction output preserves line breaks within `` tags
+
+#### Scenario: Context applied to all artifacts
+
+- **WHEN** instructions are loaded for any artifact (proposal, specs, design, tasks)
+- **THEN** context section appears in all instruction outputs
+
+#### Scenario: Context applied to apply instruction surface
+
+- **WHEN** user runs `openspec instructions apply --change `
+- **AND** config contains a `context` field
+- **THEN** apply instruction output includes the context in a `` section
+
+#### Scenario: Context applied to archive instruction surface
+
+- **WHEN** user runs `openspec instructions archive`
+- **AND** config contains a `context` field
+- **THEN** archive instruction output includes the context in a `` section
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-archive-skill/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-archive-skill/spec.md
new file mode 100644
index 000000000..ce6064f2c
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-archive-skill/spec.md
@@ -0,0 +1,34 @@
+## ADDED Requirements
+
+### Requirement: Config Injection into Archive Workflow
+
+The `/opsx:archive` skill SHALL fetch archive instructions via `openspec instructions archive --json` before executing the workflow, and apply any returned `context` and `rules` as constraints throughout the archive process.
+
+#### Scenario: Archive instructions fetched before main workflow steps
+
+- **WHEN** agent executes `/opsx:archive` with a change name
+- **THEN** the agent calls `openspec instructions archive --json` after the built-in readiness checks (artifact completion, task completion) but before executing the main workflow steps (spec sync, archive)
+- **AND** uses the returned `context` and `rules` as behavioral constraints for the remaining workflow
+
+#### Scenario: Project context guides archive workflow
+
+- **WHEN** `openspec instructions archive --json` returns a `context` field
+- **THEN** the agent applies that context as background knowledge during the archive workflow
+- **AND** the context does not override or suppress the built-in readiness checks
+
+#### Scenario: Workflow rules guide archive decisions
+
+- **WHEN** `openspec instructions archive --json` returns a `rules` field
+- **THEN** the agent follows those rules as additional constraints during the archive workflow
+- **AND** the rules do not override or suppress the built-in artifact completion, task completion, or spec sync checks
+
+#### Scenario: No config does not block archive
+
+- **WHEN** `openspec instructions archive --json` returns no `context` and no `rules`
+- **THEN** the agent proceeds with the archive workflow using only the built-in steps
+
+#### Scenario: Built-in safety checks remain enforced regardless of config rules
+
+- **WHEN** `rules.archive` contains guidance that conflicts with built-in safety behavior
+- **THEN** the built-in artifact completion check, task completion check, and spec sync prompt are still executed
+- **AND** config rules are applied as supplementary guidance only
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-bulk-archive-skill/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-bulk-archive-skill/spec.md
new file mode 100644
index 000000000..c9e890523
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/opsx-bulk-archive-skill/spec.md
@@ -0,0 +1,27 @@
+## ADDED Requirements
+
+### Requirement: Config Injection into Bulk Archive Workflow
+
+The `/opsx:bulk-archive` skill SHALL fetch archive instructions once at the start of the workflow via `openspec instructions archive --json` and apply any returned `context` and `rules` as constraints throughout the entire batch operation.
+
+#### Scenario: Archive instructions fetched once after batch validation
+
+- **WHEN** agent executes `/opsx:bulk-archive`
+- **THEN** the agent calls `openspec instructions archive --json` once after batch validation (artifact status, task completion, delta spec collection) but before conflict detection and archive execution
+- **AND** the returned `context` and `rules` are applied as constraints for all changes in the batch
+
+#### Scenario: Single fetch covers the entire batch
+
+- **WHEN** the bulk archive workflow processes multiple changes
+- **THEN** `openspec instructions archive --json` is called only once, not once per change
+- **AND** the same `context` and `rules` apply to every change in the batch
+
+#### Scenario: No config does not block bulk archive
+
+- **WHEN** `openspec instructions archive --json` returns no `context` and no `rules`
+- **THEN** the agent proceeds with the bulk archive workflow using only the built-in steps
+
+#### Scenario: Built-in safety checks remain enforced for each change
+
+- **WHEN** processing each change in the batch
+- **THEN** the built-in per-change checks (artifact completion, task completion, spec conflict resolution) are still executed regardless of config rules
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/specs/rules-injection/spec.md b/openspec/changes/extend-config-injection-to-apply-archive/specs/rules-injection/spec.md
new file mode 100644
index 000000000..1711249a0
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/specs/rules-injection/spec.md
@@ -0,0 +1,88 @@
+## ADDED Requirements
+
+### Requirement: Inject workflow rules into apply instruction surface
+
+The system SHALL inject rules from `rules.apply` in project config into apply instruction output.
+
+#### Scenario: Rules exist for apply workflow
+
+- **WHEN** running `openspec instructions apply --change `
+- **AND** config has `rules: { apply: ["Run tests before marking tasks done"] }`
+- **THEN** apply instruction output includes a `` section with that rule
+
+#### Scenario: No rules for apply workflow
+
+- **WHEN** running `openspec instructions apply --change `
+- **AND** config has no `rules.apply` key
+- **THEN** apply instruction output does not include a `` section
+
+#### Scenario: Apply workflow rules appear after built-in instruction content
+
+- **WHEN** apply instructions are generated with `rules.apply` present
+- **THEN** `` section appears after the built-in task list and schema instruction content
+
+### Requirement: Inject workflow rules into archive instruction surface
+
+The system SHALL inject rules from `rules.archive` in project config into archive instruction output.
+
+#### Scenario: Rules exist for archive workflow
+
+- **WHEN** running `openspec instructions archive`
+- **AND** config has `rules: { archive: [...] }`
+- **THEN** archive instruction output includes a `` section with that rule
+
+#### Scenario: No rules for archive workflow
+
+- **WHEN** running `openspec instructions archive`
+- **AND** config has no `rules.archive` key
+- **THEN** archive instruction output does not include a `` section
+
+#### Scenario: Archive workflow rules appear after built-in template content
+
+- **WHEN** archive instructions are generated with `rules.archive` present
+- **THEN** `` section appears after the built-in archive workflow template content
+
+### Requirement: Document workflow rule targets in generated config
+
+The system SHALL include `rules.apply` and `rules.archive` as commented examples in the config file generated by `openspec init`, so users discover these targets alongside artifact rule keys.
+
+#### Scenario: Generated config shows workflow rule examples
+
+- **WHEN** user runs `openspec init` and a new `openspec/config.yaml` is created
+- **THEN** the rules section comment block includes examples of `rules.apply` and `rules.archive` alongside artifact key examples
+
+#### Scenario: Workflow examples appear in the same rules block as artifact examples
+
+- **WHEN** the generated config.yaml rules section is rendered
+- **THEN** `rules.apply` and `rules.archive` examples appear in the same commented block as artifact rule examples such as `rules.proposal` and `rules.tasks`
+
+## MODIFIED Requirements
+
+### Requirement: Validate artifact IDs during instruction loading
+
+The system SHALL validate artifact IDs in rules against the schema when instructions are loaded and emit warnings for unknown IDs. Reserved workflow targets (`apply`, `archive`) SHALL be accepted without warning.
+
+#### Scenario: All artifact IDs are valid
+
+- **WHEN** instructions loaded and config has `rules: { proposal: [...], specs: [...] }` for schema with those artifacts
+- **THEN** no validation warnings are emitted
+
+#### Scenario: Unknown artifact ID in rules
+
+- **WHEN** instructions loaded and config has `rules: { unknownartifact: [...] }`
+- **THEN** warning emitted: "Unknown artifact ID in rules: 'unknownartifact'. Valid IDs for schema 'spec-driven': design, proposal, specs, tasks"
+
+#### Scenario: Workflow targets are not treated as unknown artifact IDs
+
+- **WHEN** instructions loaded and config has `rules: { apply: [...], archive: [...] }`
+- **THEN** no validation warning is emitted for `apply` or `archive`
+
+#### Scenario: Multiple unknown artifact IDs
+
+- **WHEN** instructions loaded and config has multiple unknown artifact IDs
+- **THEN** separate warning emitted for each unknown artifact ID
+
+#### Scenario: Validation warnings shown once per session
+
+- **WHEN** instructions loaded multiple times in same CLI session
+- **THEN** each unique validation warning is shown only once (cached)
diff --git a/openspec/changes/extend-config-injection-to-apply-archive/tasks.md b/openspec/changes/extend-config-injection-to-apply-archive/tasks.md
new file mode 100644
index 000000000..f4cc18370
--- /dev/null
+++ b/openspec/changes/extend-config-injection-to-apply-archive/tasks.md
@@ -0,0 +1,42 @@
+## 1. Validation Layer (D1)
+
+- [x] 1.1 In `src/core/project-config.ts`, define and export `WORKFLOW_RULE_TARGETS = new Set(['apply', 'archive'])`, importing `WorkflowId` from `profiles.ts`
+- [x] 1.2 In `src/core/artifact-graph/instruction-loader.ts`, strip keys present in `WORKFLOW_RULE_TARGETS` from `projectConfig.rules` before passing the map to `validateConfigRules()`
+- [x] 1.3 Write unit tests for the call site confirming `apply` and `archive` keys in `rules` produce no validation warning, and that unknown keys still do
+
+## 2. Apply Instruction Extension (D2)
+
+- [x] 2.1 In `generateApplyInstructions()` (`src/commands/workflow/instructions.ts`), call `readProjectConfig()` and add optional `context` and `rules` fields (from `rules.apply`) to the return type
+- [x] 2.2 In `printApplyInstructionsText()`, render `` and `` blocks after built-in content, matching the pattern in `printInstructionsText()`
+- [x] 2.3 Write unit tests for `generateApplyInstructions()`: context present, rules.apply present, both absent; confirm JSON output carries the new fields
+- [x] 2.4 Write unit tests for `printApplyInstructionsText()`: verify rendered text includes blocks when fields are set and omits them when absent
+
+## 3. Archive Instruction Generation (D3)
+
+- [x] 3.1 Add `generateArchiveInstructions(projectRoot)` in `src/commands/workflow/instructions.ts`: read project config, call `getArchiveChangeSkillTemplate()`, return `{ template, context?, rules? }` using `path.join()` / `path.resolve()` for all path operations
+- [x] 3.2 Add `archiveInstructionsCommand(options)`: call `generateArchiveInstructions()`, serialize to JSON with `--json`, or call `printArchiveInstructionsText()` for text
+- [x] 3.3 Add `printArchiveInstructionsText()`: render template content followed by `` and `` blocks
+- [x] 3.4 In `src/cli/index.ts`, add an `archive` branch alongside the existing `apply` branch in the `instructions` command handler, routing to `archiveInstructionsCommand()`
+- [x] 3.5 Write unit tests for `generateArchiveInstructions()`: no config, context only, `rules.archive` only, both; confirm `rules` is absent when only artifact keys exist in config
+- [x] 3.6 Write integration test for `openspec instructions archive --json` end-to-end
+
+## 4. Skill Template Updates (D5)
+
+- [x] 4.1 In `src/core/templates/workflows/apply-change.ts`, add `context` and `rules` to the Step 3 JSON field list and add a constraint that the agent must apply them as behavioral guidance without copying them into any output file
+- [x] 4.2 In `src/core/templates/workflows/archive-change.ts`, add a new step before the main workflow steps: call `openspec instructions archive --json` and consume returned `context` and `rules` as constraints; note that built-in readiness checks run regardless
+- [x] 4.3 In `src/core/templates/workflows/bulk-archive-change.ts`, add a one-time call to `openspec instructions archive --json` at the start of the batch; apply returned `context` and `rules` as constraints for all changes (single call, not once per change)
+- [x] 4.4 Run `openspec sync` to regenerate `.claude/commands/opsx/*.md` from the updated templates; verify generated files reflect template changes
+
+## 5. Config Documentation (D6)
+
+- [x] 5.1 In `src/core/config-prompts.ts`, add `rules.apply` and `rules.archive` as commented examples in the same `rules` block as existing artifact key examples (`proposal`, `tasks`)
+
+## 6. Integration Tests
+
+- [x] 6.1 Write integration test for `openspec instructions apply --json` confirming `context` and `rules` appear in output when configured
+- [x] 6.2 Write integration test confirming `rules.apply` and `rules.archive` in `config.yaml` produce no validation warning during instruction generation
+- [x] 6.3 Verify `openspec init` generates a `config.yaml` whose `rules` comment block includes `apply` and `archive` examples
+
+## 7. Docs
+
+- [x] 7.1 Update `docs/opsx.md` to document `openspec instructions archive` as a valid command and show `rules.apply` / `rules.archive` alongside existing artifact rule examples
diff --git a/openspec/config.yaml b/openspec/config.yaml
index 0b7ad5176..b187d2b84 100644
--- a/openspec/config.yaml
+++ b/openspec/config.yaml
@@ -34,3 +34,8 @@ rules:
- Use existing constants and lists - don't invent detection mechanisms
- Prefer explicit lookups over pattern matching or regex
- If we generate it, we track it by name in a constant
+ apply:
+ - Read context files, work through pending tasks, mark complete as you go
+ - Pause if you hit blockers or need clarification
+ archive:
+ - Verify specs are synced before archiving
\ No newline at end of file
diff --git a/src/cli/index.ts b/src/cli/index.ts
index f1278dbd7..6a1308691 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -21,6 +21,7 @@ import {
statusCommand,
instructionsCommand,
applyInstructionsCommand,
+ archiveInstructionsCommand,
templatesCommand,
schemasCommand,
newChangeCommand,
@@ -447,9 +448,11 @@ program
.option('--json', 'Output as JSON')
.action(async (artifactId: string | undefined, options: InstructionsOptions) => {
try {
- // Special case: "apply" is not an artifact, but a command to get apply instructions
+ // Special cases: "apply" and "archive" are workflow commands, not artifact IDs
if (artifactId === 'apply') {
await applyInstructionsCommand(options);
+ } else if (artifactId === 'archive') {
+ await archiveInstructionsCommand(options);
} else {
await instructionsCommand(artifactId, options);
}
diff --git a/src/commands/workflow/index.ts b/src/commands/workflow/index.ts
index 232b2dbe3..a340ad7d7 100644
--- a/src/commands/workflow/index.ts
+++ b/src/commands/workflow/index.ts
@@ -7,8 +7,8 @@
export { statusCommand } from './status.js';
export type { StatusOptions } from './status.js';
-export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
-export type { InstructionsOptions } from './instructions.js';
+export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand } from './instructions.js';
+export type { InstructionsOptions, ArchiveInstructionsOptions } from './instructions.js';
export { templatesCommand } from './templates.js';
export type { TemplatesOptions } from './templates.js';
diff --git a/src/commands/workflow/instructions.ts b/src/commands/workflow/instructions.ts
index 7afba1475..fd0bf5062 100644
--- a/src/commands/workflow/instructions.ts
+++ b/src/commands/workflow/instructions.ts
@@ -21,6 +21,8 @@ import {
type TaskItem,
type ApplyInstructions,
} from './shared.js';
+import { readProjectConfig } from '../../core/project-config.js';
+import { getArchiveChangeSkillTemplate } from '../../core/templates/workflows/archive-change.js';
// -----------------------------------------------------------------------------
// Types
@@ -326,6 +328,12 @@ export async function generateApplyInstructions(
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
}
+ // Read project config for context and rules.apply
+ const projectConfig = readProjectConfig(projectRoot);
+ const configContext = projectConfig?.context?.trim() || undefined;
+ const applyRules = projectConfig?.rules?.['apply'];
+ const configRules = applyRules && applyRules.length > 0 ? applyRules : undefined;
+
return {
changeName,
changeDir,
@@ -336,6 +344,8 @@ export async function generateApplyInstructions(
state,
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
instruction,
+ ...(configContext !== undefined && { context: configContext }),
+ ...(configRules !== undefined && { rules: configRules }),
};
}
@@ -369,7 +379,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions
}
export function printApplyInstructionsText(instructions: ApplyInstructions): void {
- const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
+ const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules } = instructions;
console.log(`## Apply: ${changeName}`);
console.log(`Schema: ${schemaName}`);
@@ -420,4 +430,115 @@ export function printApplyInstructionsText(instructions: ApplyInstructions): voi
// Instruction
console.log('### Instruction');
console.log(instruction);
+ console.log();
+
+ // Project context (AI constraint - do not include in output)
+ if (context) {
+ console.log('');
+ console.log('');
+ console.log(context);
+ console.log('');
+ console.log();
+ }
+
+ // Rules (AI constraint - do not include in output)
+ if (rules && rules.length > 0) {
+ console.log('');
+ console.log('');
+ for (const rule of rules) {
+ console.log(`- ${rule}`);
+ }
+ console.log('');
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Archive Instructions Command
+// -----------------------------------------------------------------------------
+
+export interface ArchiveInstructions {
+ template: string;
+ /** Project context from config (AI constraint, not to be included in output) */
+ context?: string;
+ /** Workflow rules from rules.archive in config (AI constraints, not to be included in output) */
+ rules?: string[];
+}
+
+export interface ArchiveInstructionsOptions {
+ change?: string;
+ json?: boolean;
+}
+
+/**
+ * Generates archive instructions including injected project context and rules.archive.
+ */
+export async function generateArchiveInstructions(
+ projectRoot: string
+): Promise {
+ const template = getArchiveChangeSkillTemplate().instructions;
+
+ const projectConfig = readProjectConfig(projectRoot);
+ const configContext = projectConfig?.context?.trim() || undefined;
+ const archiveRules = projectConfig?.rules?.['archive'];
+ const configRules = archiveRules && archiveRules.length > 0 ? archiveRules : undefined;
+
+ return {
+ template,
+ ...(configContext !== undefined && { context: configContext }),
+ ...(configRules !== undefined && { rules: configRules }),
+ };
+}
+
+export async function archiveInstructionsCommand(options: ArchiveInstructionsOptions): Promise {
+ const spinner = options.json ? undefined : ora('Generating archive instructions...').start();
+
+ try {
+ const projectRoot = process.cwd();
+ if (options.change) {
+ // Validate the optional change name so archive instructions aren't requested for a nonexistent change.
+ await validateChangeExists(options.change, projectRoot);
+ }
+
+ const instructions = await generateArchiveInstructions(projectRoot);
+
+ spinner?.stop();
+
+ if (options.json) {
+ console.log(JSON.stringify(instructions, null, 2));
+ return;
+ }
+
+ printArchiveInstructionsText(instructions);
+ } catch (error) {
+ spinner?.stop();
+ throw error;
+ }
+}
+
+export function printArchiveInstructionsText(instructions: ArchiveInstructions): void {
+ const { template, context, rules } = instructions;
+
+ // Template content (built-in archive workflow steps)
+ console.log(template.trim());
+ console.log();
+
+ // Project context (AI constraint - do not include in output)
+ if (context) {
+ console.log('');
+ console.log('');
+ console.log(context);
+ console.log('');
+ console.log();
+ }
+
+ // Rules (AI constraint - do not include in output)
+ if (rules && rules.length > 0) {
+ console.log('');
+ console.log('');
+ for (const rule of rules) {
+ console.log(`- ${rule}`);
+ }
+ console.log('');
+ console.log();
+ }
}
diff --git a/src/commands/workflow/shared.ts b/src/commands/workflow/shared.ts
index 43c9aa46c..62f743c02 100644
--- a/src/commands/workflow/shared.ts
+++ b/src/commands/workflow/shared.ts
@@ -35,6 +35,10 @@ export interface ApplyInstructions {
state: 'blocked' | 'all_done' | 'ready';
missingArtifacts?: string[];
instruction: string;
+ /** Project context from config (AI constraint, not to be included in output) */
+ context?: string;
+ /** Workflow rules from rules.apply in config (AI constraints, not to be included in output) */
+ rules?: string[];
}
// -----------------------------------------------------------------------------
diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts
index b8b2675bb..23d68e37d 100644
--- a/src/core/artifact-graph/instruction-loader.ts
+++ b/src/core/artifact-graph/instruction-loader.ts
@@ -5,7 +5,7 @@ import { ArtifactGraph } from './graph.js';
import { detectCompleted } from './state.js';
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
import { FileSystemUtils } from '../../utils/file-system.js';
-import { readProjectConfig, validateConfigRules } from '../project-config.js';
+import { readProjectConfig, validateConfigRules, WORKFLOW_RULE_TARGETS } from '../project-config.js';
import type { Artifact, CompletedSet } from './types.js';
// Session-level cache for validation warnings (avoid repeating same warnings)
@@ -243,8 +243,12 @@ export function generateInstructions(
// Validate rules artifact IDs if config has rules (only once per session)
if (projectConfig?.rules) {
const validArtifactIds = new Set(context.graph.getAllArtifacts().map((a) => a.id));
+ // Strip workflow-reserved keys before artifact ID validation so rules.apply/archive don't warn
+ const artifactOnlyRules = Object.fromEntries(
+ Object.entries(projectConfig.rules).filter(([key]) => !WORKFLOW_RULE_TARGETS.has(key))
+ );
const warnings = validateConfigRules(
- projectConfig.rules,
+ artifactOnlyRules,
validArtifactIds,
context.schemaName
);
diff --git a/src/core/config-prompts.ts b/src/core/config-prompts.ts
index d3bb029e2..5141b1354 100644
--- a/src/core/config-prompts.ts
+++ b/src/core/config-prompts.ts
@@ -25,8 +25,10 @@ export function serializeConfig(config: Partial): string {
lines.push('');
// Rules section with comments
- lines.push('# Per-artifact rules (optional)');
- lines.push('# Add custom rules for specific artifacts.');
+ lines.push('# Per-artifact and workflow rules (optional)');
+ lines.push('# Add custom rules for specific artifacts or workflow phases.');
+ lines.push('# Artifact keys (e.g. proposal, specs, design, tasks) apply during artifact creation.');
+ lines.push('# Workflow keys (apply, archive) apply during those workflow phases.');
lines.push('# Example:');
lines.push('# rules:');
lines.push('# proposal:');
@@ -34,6 +36,10 @@ export function serializeConfig(config: Partial): string {
lines.push('# - Always include a "Non-goals" section');
lines.push('# tasks:');
lines.push('# - Break tasks into chunks of max 2 hours');
+ lines.push('# apply:');
+ lines.push('# - Run tests before marking tasks done');
+ lines.push('# archive:');
+ lines.push('# - Verify specs are synced before archiving');
return lines.join('\n') + '\n';
}
diff --git a/src/core/project-config.ts b/src/core/project-config.ts
index 6c1ea04a5..58d4034f6 100644
--- a/src/core/project-config.ts
+++ b/src/core/project-config.ts
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from 'fs';
import path from 'path';
import { parse as parseYaml } from 'yaml';
import { z } from 'zod';
+import type { WorkflowId } from './profiles.js';
/**
* Zod schema for project configuration.
@@ -42,6 +43,15 @@ export const ProjectConfigSchema = z.object({
export type ProjectConfig = z.infer;
+/**
+ * Workflow rule targets that are valid in the `rules` config key but are not artifact IDs.
+ * Filtered out before passing rules to validateConfigRules() so the validator only sees artifact keys.
+ */
+export const WORKFLOW_RULE_TARGETS = new Set([
+ 'apply',
+ 'archive',
+] as const satisfies readonly WorkflowId[]);
+
const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit
/**
diff --git a/src/core/templates/workflows/apply-change.ts b/src/core/templates/workflows/apply-change.ts
index be60210a7..174e6be54 100644
--- a/src/core/templates/workflows/apply-change.ts
+++ b/src/core/templates/workflows/apply-change.ts
@@ -44,6 +44,8 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
+ - \`context\`: project background (if present in config) — apply as background knowledge, do NOT copy to any output file
+ - \`rules\`: workflow constraints (if present in config) — apply as behavioral guidance, do NOT copy to any output file
**Handle states:**
- If \`state: "blocked"\` (missing artifacts): show message, suggest using openspec-continue-change
@@ -201,6 +203,8 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
+ - \`context\`: project background (if present in config) — apply as background knowledge, do NOT copy to any output file
+ - \`rules\`: workflow constraints (if present in config) — apply as behavioral guidance, do NOT copy to any output file
**Handle states:**
- If \`state: "blocked"\` (missing artifacts): show message, suggest using \`/opsx:continue\`
diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts
index 1c37ffde0..97054472e 100644
--- a/src/core/templates/workflows/archive-change.ts
+++ b/src/core/templates/workflows/archive-change.ts
@@ -16,9 +16,11 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate {
**Steps**
-1. **If no change name provided, prompt for selection**
+1. **Determine the change**
- Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+ If a change name was provided, use it directly.
+
+ Otherwise, run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
@@ -51,7 +53,16 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate {
**If no tasks file exists:** Proceed without task-related warning.
-4. **Assess delta spec sync state**
+4. **Fetch archive instructions**
+
+ Run:
+ \`\`\`bash
+ openspec instructions archive --json
+ \`\`\`
+
+ Parse the JSON. If a \`context\` field is present, apply it as project background knowledge for the remaining steps. If a \`rules\` field is present, apply those rules as additional behavioral constraints throughout. Do NOT copy \`context\` or \`rules\` content into any output.
+
+5. **Assess delta spec sync state**
Check for delta specs at \`openspec/changes//specs/\`. If none exist, proceed without sync prompt.
@@ -66,7 +77,7 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate {
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice.
-5. **Perform the archive**
+6. **Perform the archive**
Create the archive directory if it doesn't exist:
\`\`\`bash
@@ -83,7 +94,7 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate {
mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD-
\`\`\`
-6. **Display summary**
+7. **Display summary**
Show archive completion summary including:
- Change name
@@ -166,7 +177,16 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate {
**If no tasks file exists:** Proceed without task-related warning.
-4. **Assess delta spec sync state**
+4. **Fetch archive instructions**
+
+ Run:
+ \`\`\`bash
+ openspec instructions archive --json
+ \`\`\`
+
+ Parse the JSON. If a \`context\` field is present, apply it as project background knowledge for the remaining steps. If a \`rules\` field is present, apply those rules as additional behavioral constraints throughout. Do NOT copy \`context\` or \`rules\` content into any output.
+
+5. **Assess delta spec sync state**
Check for delta specs at \`openspec/changes//specs/\`. If none exist, proceed without sync prompt.
@@ -181,7 +201,7 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate {
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice.
-5. **Perform the archive**
+6. **Perform the archive**
Create the archive directory if it doesn't exist:
\`\`\`bash
@@ -198,7 +218,7 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate {
mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD-
\`\`\`
-6. **Display summary**
+7. **Display summary**
Show archive completion summary including:
- Change name
diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts
index ed6d14452..46e186dad 100644
--- a/src/core/templates/workflows/bulk-archive-change.ts
+++ b/src/core/templates/workflows/bulk-archive-change.ts
@@ -49,7 +49,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- List which capability specs exist
- For each, extract requirement names (lines matching \`### Requirement: \`)
-4. **Detect spec conflicts**
+4. **Fetch archive instructions (once, before processing any changes)**
+
+ Run:
+ \`\`\`bash
+ openspec instructions archive --json
+ \`\`\`
+
+ Parse the JSON once. If a \`context\` field is present, apply it as project background knowledge for the entire batch. If a \`rules\` field is present, apply those rules as behavioral constraints across all changes. Do NOT copy \`context\` or \`rules\` content into any output.
+
+ This call is made **once** at the start, not once per change. The same context and rules apply to every change in the batch. If neither field is present, proceed normally.
+
+5. **Detect spec conflicts**
Build a map of \`capability -> [changes that touch it]\`:
@@ -60,7 +71,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
A conflict exists when 2+ selected changes have delta specs for the same capability.
-5. **Resolve conflicts agentically**
+6. **Resolve conflicts agentically**
**For each conflict**, investigate the codebase:
@@ -80,7 +91,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- In what order (if both)
- Rationale (what was found in codebase)
-6. **Show consolidated status table**
+7. **Show consolidated status table**
Display a table summarizing all changes:
@@ -105,7 +116,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
\`\`\`
-7. **Confirm batch operation**
+8. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
@@ -117,7 +128,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
If there are incomplete changes, make clear they'll be archived with warnings.
-8. **Execute archive for each confirmed change**
+9. **Execute archive for each confirmed change**
Process changes in the determined order (respecting conflict resolution):
@@ -137,7 +148,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
-9. **Display summary**
+10. **Display summary**
Show final results:
@@ -296,7 +307,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- List which capability specs exist
- For each, extract requirement names (lines matching \`### Requirement: \`)
-4. **Detect spec conflicts**
+4. **Fetch archive instructions (once, before processing any changes)**
+
+ Run:
+ \`\`\`bash
+ openspec instructions archive --json
+ \`\`\`
+
+ Parse the JSON once. If a \`context\` field is present, apply it as project background knowledge for the entire batch. If a \`rules\` field is present, apply those rules as behavioral constraints across all changes. Do NOT copy \`context\` or \`rules\` content into any output.
+
+ This call is made **once** at the start, not once per change. The same context and rules apply to every change in the batch. If neither field is present, proceed normally.
+
+5. **Detect spec conflicts**
Build a map of \`capability -> [changes that touch it]\`:
@@ -307,7 +329,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
A conflict exists when 2+ selected changes have delta specs for the same capability.
-5. **Resolve conflicts agentically**
+6. **Resolve conflicts agentically**
**For each conflict**, investigate the codebase:
@@ -327,7 +349,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- In what order (if both)
- Rationale (what was found in codebase)
-6. **Show consolidated status table**
+7. **Show consolidated status table**
Display a table summarizing all changes:
@@ -352,7 +374,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
\`\`\`
-7. **Confirm batch operation**
+8. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
@@ -364,7 +386,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
If there are incomplete changes, make clear they'll be archived with warnings.
-8. **Execute archive for each confirmed change**
+9. **Execute archive for each confirmed change**
Process changes in the determined order (respecting conflict resolution):
@@ -384,7 +406,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
-9. **Display summary**
+10. **Display summary**
Show final results:
diff --git a/test/commands/apply-archive-instructions.test.ts b/test/commands/apply-archive-instructions.test.ts
new file mode 100644
index 000000000..660f060c4
--- /dev/null
+++ b/test/commands/apply-archive-instructions.test.ts
@@ -0,0 +1,373 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import * as fsSync from 'node:fs';
+import * as fs from 'node:fs/promises';
+import * as path from 'node:path';
+import * as os from 'node:os';
+import {
+ generateApplyInstructions,
+ printApplyInstructionsText,
+ generateArchiveInstructions,
+ printArchiveInstructionsText,
+} from '../../src/commands/workflow/instructions.js';
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+async function createProjectConfig(projectRoot: string, content: string): Promise {
+ const configDir = path.join(projectRoot, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(path.join(configDir, 'config.yaml'), content);
+}
+
+async function createChange(projectRoot: string, changeName: string): Promise {
+ const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
+ await fs.mkdir(changeDir, { recursive: true });
+ await fs.writeFile(path.join(changeDir, 'proposal.md'), '## Why\nTest.\n\n## What Changes\n- **test:** placeholder');
+ await fs.writeFile(path.join(changeDir, 'design.md'), '# Design');
+
+ const specsDir = path.join(changeDir, 'specs');
+ await fs.mkdir(specsDir, { recursive: true });
+ await fs.writeFile(path.join(specsDir, 'test.md'), '# Spec');
+
+ await fs.writeFile(
+ path.join(changeDir, 'tasks.md'),
+ '## 1. Tasks\n\n- [ ] 1.1 Do thing\n- [ ] 1.2 Do another thing\n'
+ );
+ return changeDir;
+}
+
+// ---------------------------------------------------------------------------
+// generateApplyInstructions
+// ---------------------------------------------------------------------------
+
+describe('generateApplyInstructions', () => {
+ let tempDir: string;
+
+ beforeEach(async () => {
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-test-apply-'));
+ await createChange(tempDir, 'my-change');
+ });
+
+ afterEach(async () => {
+ await fs.rm(tempDir, { recursive: true, force: true });
+ });
+
+ it('omits context and rules when no config exists', async () => {
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.context).toBeUndefined();
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('includes context when config has a context field', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\ncontext: |\n Tech stack: TypeScript\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.context).toContain('Tech stack: TypeScript');
+ });
+
+ it('includes rules from rules.apply when present', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n apply:\n - Run tests before marking tasks done\n - Keep PRs small\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.rules).toEqual(['Run tests before marking tasks done', 'Keep PRs small']);
+ });
+
+ it('includes both context and rules.apply when both configured', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\ncontext: Project context\nrules:\n apply:\n - Rule 1\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.context).toBe('Project context');
+ expect(result.rules).toEqual(['Rule 1']);
+ });
+
+ it('omits rules when config has only artifact rules (no rules.apply)', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n proposal:\n - Artifact rule\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('omits rules when rules.apply is an empty array', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n apply: []\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('carries context and rules in JSON output (auto via JSON.stringify)', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\ncontext: My context\nrules:\n apply:\n - My rule\n'
+ );
+
+ const result = await generateApplyInstructions(tempDir, 'my-change');
+ const json = JSON.parse(JSON.stringify(result));
+
+ expect(json.context).toBe('My context');
+ expect(json.rules).toEqual(['My rule']);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// printApplyInstructionsText
+// ---------------------------------------------------------------------------
+
+describe('printApplyInstructionsText', () => {
+ let consoleSpy: ReturnType;
+
+ beforeEach(() => {
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ consoleSpy.mockRestore();
+ });
+
+ function captureOutput(instructions: Parameters[0]): string {
+ consoleSpy.mockReset();
+ printApplyInstructionsText(instructions);
+ return consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
+ }
+
+ const baseInstructions = {
+ changeName: 'test-change',
+ changeDir: '/tmp/test',
+ schemaName: 'spec-driven',
+ contextFiles: {},
+ progress: { total: 2, complete: 0, remaining: 2 },
+ tasks: [],
+ state: 'ready' as const,
+ instruction: 'Work through tasks.',
+ };
+
+ it('renders project_context block when context is present', () => {
+ const output = captureOutput({ ...baseInstructions, context: 'Project background' });
+
+ expect(output).toContain('');
+ expect(output).toContain('Project background');
+ expect(output).toContain('');
+ });
+
+ it('omits project_context block when context is absent', () => {
+ const output = captureOutput({ ...baseInstructions });
+
+ expect(output).not.toContain('');
+ });
+
+ it('renders rules block when rules are present', () => {
+ const output = captureOutput({ ...baseInstructions, rules: ['Run tests', 'Keep PRs small'] });
+
+ expect(output).toContain('');
+ expect(output).toContain('- Run tests');
+ expect(output).toContain('- Keep PRs small');
+ expect(output).toContain('');
+ });
+
+ it('omits rules block when rules are absent', () => {
+ const output = captureOutput({ ...baseInstructions });
+
+ expect(output).not.toContain('');
+ });
+
+ it('renders both context and rules blocks when both present', () => {
+ const output = captureOutput({
+ ...baseInstructions,
+ context: 'My context',
+ rules: ['Rule A'],
+ });
+
+ expect(output).toContain('');
+ expect(output).toContain('My context');
+ expect(output).toContain('');
+ expect(output).toContain('- Rule A');
+ });
+
+ it('includes instruction section regardless of context/rules', () => {
+ const output = captureOutput({ ...baseInstructions });
+
+ expect(output).toContain('### Instruction');
+ expect(output).toContain('Work through tasks.');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// generateArchiveInstructions
+// ---------------------------------------------------------------------------
+
+describe('generateArchiveInstructions', () => {
+ let tempDir: string;
+
+ beforeEach(async () => {
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-test-archive-'));
+ });
+
+ afterEach(async () => {
+ await fs.rm(tempDir, { recursive: true, force: true });
+ });
+
+ it('returns template content when no config exists', async () => {
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.template).toBeTruthy();
+ expect(result.context).toBeUndefined();
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('includes context when config has a context field', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\ncontext: Archive context\n'
+ );
+
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.context).toBe('Archive context');
+ });
+
+ it('includes rules from rules.archive when present', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n archive:\n - Verify specs are synced\n - Check task completion\n'
+ );
+
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.rules).toEqual(['Verify specs are synced', 'Check task completion']);
+ });
+
+ it('includes both context and rules.archive when both configured', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\ncontext: My project\nrules:\n archive:\n - Rule 1\n'
+ );
+
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.context).toBe('My project');
+ expect(result.rules).toEqual(['Rule 1']);
+ });
+
+ it('omits rules when config has only artifact rules (no rules.archive)', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n specs:\n - Artifact rule\n'
+ );
+
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('omits rules when config has rules.apply but not rules.archive', async () => {
+ await createProjectConfig(
+ tempDir,
+ 'schema: spec-driven\nrules:\n apply:\n - Apply rule\n'
+ );
+
+ const result = await generateArchiveInstructions(tempDir);
+
+ expect(result.rules).toBeUndefined();
+ });
+
+ it('omits context and rules fields from JSON when absent', async () => {
+ const result = await generateArchiveInstructions(tempDir);
+ const json = JSON.parse(JSON.stringify(result));
+
+ expect(Object.prototype.hasOwnProperty.call(json, 'context')).toBe(false);
+ expect(Object.prototype.hasOwnProperty.call(json, 'rules')).toBe(false);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// printArchiveInstructionsText
+// ---------------------------------------------------------------------------
+
+describe('printArchiveInstructionsText', () => {
+ let consoleSpy: ReturnType;
+
+ beforeEach(() => {
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ consoleSpy.mockRestore();
+ });
+
+ function captureOutput(instructions: Parameters[0]): string {
+ consoleSpy.mockReset();
+ printArchiveInstructionsText(instructions);
+ return consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
+ }
+
+ it('renders the archive template content', () => {
+ const output = captureOutput({ template: 'Archive workflow steps here.' });
+
+ expect(output).toContain('Archive workflow steps here.');
+ });
+
+ it('renders project_context block when context is present', () => {
+ const output = captureOutput({ template: 'Template', context: 'Project background' });
+
+ expect(output).toContain('');
+ expect(output).toContain('Project background');
+ expect(output).toContain('');
+ });
+
+ it('omits project_context block when context is absent', () => {
+ const output = captureOutput({ template: 'Template' });
+
+ expect(output).not.toContain('');
+ });
+
+ it('renders rules block when rules are present', () => {
+ const output = captureOutput({ template: 'Template', rules: ['Verify sync', 'Check tasks'] });
+
+ expect(output).toContain('');
+ expect(output).toContain('- Verify sync');
+ expect(output).toContain('- Check tasks');
+ expect(output).toContain('');
+ });
+
+ it('omits rules block when rules are absent', () => {
+ const output = captureOutput({ template: 'Template' });
+
+ expect(output).not.toContain('');
+ });
+
+ it('renders template before context/rules sections', () => {
+ const output = captureOutput({
+ template: 'Template content',
+ context: 'Context content',
+ rules: ['Rule A'],
+ });
+
+ const templateIdx = output.indexOf('Template content');
+ const contextIdx = output.indexOf('');
+ const rulesIdx = output.indexOf('');
+
+ expect(templateIdx).toBeLessThan(contextIdx);
+ expect(templateIdx).toBeLessThan(rulesIdx);
+ });
+});
diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts
index 613e37f98..661684652 100644
--- a/test/commands/artifact-workflow.test.ts
+++ b/test/commands/artifact-workflow.test.ts
@@ -634,6 +634,165 @@ artifacts:
});
});
+ describe('instructions apply with config injection', () => {
+ it('includes context in JSON output when config.yaml has context field', async () => {
+ await createTestChange('apply-ctx-test', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\ncontext: My project context\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'apply', '--change', 'apply-ctx-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json.context).toBe('My project context');
+ });
+
+ it('includes rules in JSON output when config.yaml has rules.apply', async () => {
+ await createTestChange('apply-rules-test', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\nrules:\n apply:\n - Run tests first\n - Keep PRs small\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'apply', '--change', 'apply-rules-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json.rules).toEqual(['Run tests first', 'Keep PRs small']);
+ });
+
+ it('omits context and rules fields from JSON when not configured', async () => {
+ await createTestChange('apply-noconfig-test', ['proposal', 'design', 'specs', 'tasks']);
+
+ const result = await runCLI(
+ ['instructions', 'apply', '--change', 'apply-noconfig-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json).not.toHaveProperty('context');
+ expect(json).not.toHaveProperty('rules');
+ });
+
+ it('rules.apply and rules.archive produce no validation warnings', async () => {
+ await createTestChange('apply-wf-rules-test', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\nrules:\n apply:\n - Apply rule\n archive:\n - Archive rule\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'apply', '--change', 'apply-wf-rules-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).not.toContain('Unknown artifact ID');
+ });
+ });
+
+ describe('instructions archive command', () => {
+ it('outputs JSON with template field', async () => {
+ await createTestChange('archive-json-test', ['proposal', 'design', 'specs', 'tasks']);
+
+ const result = await runCLI(
+ ['instructions', 'archive', '--change', 'archive-json-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toBe('');
+ const json = JSON.parse(result.stdout);
+ expect(json.template).toBeTruthy();
+ });
+
+ it('includes context in JSON when config.yaml has context field', async () => {
+ await createTestChange('archive-ctx-test', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\ncontext: Archive project context\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'archive', '--change', 'archive-ctx-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json.context).toBe('Archive project context');
+ });
+
+ it('includes rules in JSON when config.yaml has rules.archive', async () => {
+ await createTestChange('archive-rules-test', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\nrules:\n archive:\n - Verify specs synced\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'archive', '--change', 'archive-rules-test', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json.rules).toEqual(['Verify specs synced']);
+ });
+
+ it('omits rules field when config has only artifact rules but no rules.archive', async () => {
+ await createTestChange('archive-noarch-rules', ['proposal', 'design', 'specs', 'tasks']);
+ const configDir = path.join(tempDir, 'openspec');
+ await fs.mkdir(configDir, { recursive: true });
+ await fs.writeFile(
+ path.join(configDir, 'config.yaml'),
+ 'schema: spec-driven\nrules:\n specs:\n - Artifact rule only\n'
+ );
+
+ const result = await runCLI(
+ ['instructions', 'archive', '--change', 'archive-noarch-rules', '--json'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ const json = JSON.parse(result.stdout);
+ expect(json).not.toHaveProperty('rules');
+ });
+
+ it('renders text output with template content', async () => {
+ await createTestChange('archive-text-test', ['proposal', 'design', 'specs', 'tasks']);
+
+ const result = await runCLI(
+ ['instructions', 'archive', '--change', 'archive-text-test'],
+ { cwd: tempDir }
+ );
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toContain('Archive');
+ });
+ });
+
+ describe('openspec init generates config with workflow rule examples', () => {
+ it('generated config.yaml includes rules.apply and rules.archive examples', async () => {
+ const { serializeConfig } = await import('../../src/core/config-prompts.js');
+ const yaml = serializeConfig({ schema: 'spec-driven' });
+ expect(yaml).toContain('apply:');
+ expect(yaml).toContain('archive:');
+ expect(yaml).toContain('Run tests before marking tasks done');
+ expect(yaml).toContain('Verify specs are synced before archiving');
+ });
+ });
+
describe('help text', () => {
it('status command help shows description', async () => {
const result = await runCLI(['status', '--help']);
diff --git a/test/core/artifact-graph/instruction-loader.test.ts b/test/core/artifact-graph/instruction-loader.test.ts
index 9d8f612cd..e50c83922 100644
--- a/test/core/artifact-graph/instruction-loader.test.ts
+++ b/test/core/artifact-graph/instruction-loader.test.ts
@@ -504,6 +504,86 @@ rules:
expect(consoleWarnSpy).not.toHaveBeenCalled();
});
+
+ it('should not warn for rules.apply (reserved workflow target)', () => {
+ const configDir = path.join(tempDir, 'openspec');
+ fs.mkdirSync(configDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(configDir, 'config.yaml'),
+ `schema: spec-driven
+rules:
+ apply:
+ - Run tests before marking tasks done
+`
+ );
+
+ const context = loadChangeContext(tempDir, 'my-change');
+ generateInstructions(context, 'proposal', tempDir);
+
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not warn for rules.archive (reserved workflow target)', () => {
+ const configDir = path.join(tempDir, 'openspec');
+ fs.mkdirSync(configDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(configDir, 'config.yaml'),
+ `schema: spec-driven
+rules:
+ archive:
+ - Verify specs are synced before archiving
+`
+ );
+
+ const context = loadChangeContext(tempDir, 'my-change');
+ generateInstructions(context, 'proposal', tempDir);
+
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not warn for mixed artifact and workflow targets', () => {
+ const configDir = path.join(tempDir, 'openspec');
+ fs.mkdirSync(configDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(configDir, 'config.yaml'),
+ `schema: spec-driven
+rules:
+ proposal:
+ - Artifact rule
+ apply:
+ - Workflow rule
+ archive:
+ - Another workflow rule
+`
+ );
+
+ const context = loadChangeContext(tempDir, 'my-change');
+ generateInstructions(context, 'proposal', tempDir);
+
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
+ });
+
+ it('should still warn for unknown keys even when workflow targets are present', () => {
+ const configDir = path.join(tempDir, 'openspec');
+ fs.mkdirSync(configDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(configDir, 'config.yaml'),
+ `schema: spec-driven
+rules:
+ apply:
+ - Valid workflow rule
+ unknownkey:
+ - Bad rule
+`
+ );
+
+ const context = loadChangeContext(tempDir, 'my-change');
+ generateInstructions(context, 'proposal', tempDir);
+
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Unknown artifact ID in rules: "unknownkey"')
+ );
+ });
});
});
diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts
index 9c2f798c7..89afd9407 100644
--- a/test/core/templates/skill-templates-parity.test.ts
+++ b/test/core/templates/skill-templates-parity.test.ts
@@ -33,22 +33,22 @@ const EXPECTED_FUNCTION_HASHES: Record = {
getExploreSkillTemplate: '3f73b4d7ab189ef6367fccc9d99308bee35c6a89dae4c8044582a01cb01b335b',
getNewChangeSkillTemplate: '5989672758eccf54e3bb554ab97f2c129a192b12bbb7688cc1ffcf6bccb1ae9d',
getContinueChangeSkillTemplate: 'f2e413f0333dfd6641cc2bd1a189273fdea5c399eecdde98ef528b5216f097b3',
- getApplyChangeSkillTemplate: '6238712ba8cd2fd099c4f3bac13436f758fc6ac776fb8be19547f2b195240bfd',
+ getApplyChangeSkillTemplate: '942ca9e156560bca5214917516d18c0862149a8aed0141ebb90fe3632429ace7',
getFfChangeSkillTemplate: 'a7332fb14c8dc3f9dec71f5d332790b4a8488191e7db4ab6132ccbefecf9ded9',
getSyncSpecsSkillTemplate: 'bded184e4c345619148de2c0ad80a5b527d4ffe45c87cc785889b9329e0f465b',
getOnboardSkillTemplate: 'c9e719a02d2ae7f74a0e978f9ad4e767c1921248a9e3724c3321c58a15c38ba9',
getOpsxExploreCommandTemplate: 'b421b88c7a532385f7b1404736d7893eb35a05573b4a04a96f72379ac1bbf148',
getOpsxNewCommandTemplate: '62eee32d6d81a376e7be845d0891e28e6262ad07482f9bfe6af12a9f0366c364',
getOpsxContinueCommandTemplate: '8bbaedcc95287f9e822572608137df4f49ad54cedfb08d3342d0d1c4e9716caa',
- getOpsxApplyCommandTemplate: 'f59cfe9482a1b29f64b9cd7396397991a2f00a5cb1abde4ab8b4757acf1678b9',
+ getOpsxApplyCommandTemplate: '58ac0f3370e2747e96dc82a1a8f0aa8750b2f2664db389608cdcb974dfd6cf15',
getOpsxFfCommandTemplate: 'cdebe872cc8e0fcc25c8864b98ffd66a93484c0657db94bd1285b8113092702a',
- getArchiveChangeSkillTemplate: '6f8ca383fdb5a4eb9872aca81e07bf0ba7f25e4de8617d7a047ca914ca7f14b9',
- getBulkArchiveChangeSkillTemplate: '8049897ce1ddb2ff6c0d4b72e22636f9ecfd083b5f2c2a30cf3bb1cb828a2f93',
+ getArchiveChangeSkillTemplate: '7cb780a95b4f98b5daa5add394a2fc7e81887b53022d899dd370e43985573810',
+ getBulkArchiveChangeSkillTemplate: 'c663dd7fed63e1daab689ffe657a8d5d897d356a7f82fa0cbced5ce985a077d3',
getOpsxSyncCommandTemplate: '378d035fe7cc30be3e027b66dcc4b8afc78ef1c8369c39479c9b05a582fb5ccf',
getVerifyChangeSkillTemplate: '40dde29051a0ba204295b74e49e87b6e9ff30c8b89ff0e791b4f955b4595de59',
- getOpsxArchiveCommandTemplate: 'b44cc9748109f61687f9f596604b037bc3ea803abc143b22f09a76aebd98b493',
+ getOpsxArchiveCommandTemplate: '8a273ddfbcb1b7dfe156d0a6ae472c7008a6a61321d5459a016a363d9c2d4bc5',
getOpsxOnboardCommandTemplate: 'fce531f952e939ee85a41848fc21e4cc720b0f3eb62737adc3a51ee6ad2dfc57',
- getOpsxBulkArchiveCommandTemplate: '0d77c82de43840a28c74f5181cb21e33b9a9d00454adf4bc92bdc9e69817d6f5',
+ getOpsxBulkArchiveCommandTemplate: '0aac2f5a01748bf1789b10b6e899d7c4d3d102abf9aa87006e143b8b866986d9',
getOpsxVerifyCommandTemplate: 'd7c0444863faabb16abb091bc40ee56d985ae4bfa9a4db1e622ca8ba03c32fed',
getOpsxProposeSkillTemplate: 'd67f937d44650e9c61d2158c865309fbab23cb3f50a3d4868a640a97776e3999',
getOpsxProposeCommandTemplate: '41ad59b37eafd7a161bab5c6e41997a37368f9c90b194451295ede5cd42e4d46',
@@ -59,11 +59,11 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = {
'openspec-explore': '08e1ec9958eb04653707dd3e198c3fd69cf1b3acd3cf95a1022693cca83c60fc',
'openspec-new-change': 'c324a7ace1f244aa3f534ac8e3370a2c11190d6d1b85a315f26a211398310f0f',
'openspec-continue-change': '463cf0b980ec9c3c24774414ef2a3e48e9faa8577bc8748990f45ab3d5efe960',
- 'openspec-apply-change': '38ad2cb645827eda555f20e1ac9d483e1d75bae4c817c0669474aaa8c12c0421',
+ 'openspec-apply-change': '5a24c5eb686a07abd84cd1832c7e72bc76c68b7eb50375003c5acd63529c3e70',
'openspec-ff-change': '672c3a5b8df152d959b15bd7ae2be7a75ab7b8eaa2ec1e0daa15c02479b27937',
'openspec-sync-specs': 'b8859cf454379a19ca35dbf59eedca67306607f44a355327f9dc851114e50bde',
- 'openspec-archive-change': 'f83c85452bd47de0dee6b8efbcea6a62534f8a175480e9044f3043f887cebf0f',
- 'openspec-bulk-archive-change': '10477399bb07c7ba67f78e315bd68fb1901af8866720545baf4c62a6a679493b',
+ 'openspec-archive-change': 'a4ded919ff8f3e71226bbfb36c470899b92cdb5f9e0588d5e877c3fd3d0335a6',
+ 'openspec-bulk-archive-change': '1f932060f161582ba82cf7e3440c478b432d7ded067c6fddf8fbd97aa0e57ee0',
'openspec-verify-change': 'b6dc1b87940be9d6125b834831c8619019aec9a9748995f72bf981b6f08b67f8',
'openspec-onboard': 'c1444e026028210efd699110f7e9079bcb486d85ccf27f743213a81cb1084303',
'openspec-propose': '20e36dabefb90e232bad0667292bd5007ec280f8fc4fc995dbc4282bf45a22e7',