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',