From 0b58c129dcba8453599970d9e3e2d5d35b7fd521 Mon Sep 17 00:00:00 2001 From: Hari Krishnan Date: Wed, 21 Jan 2026 09:52:12 +0530 Subject: [PATCH] docs(lifecycle-hooks): update design to convention-based approach - Replace config-based hooks with convention-based discovery from openspec/hooks/ - Add Decision 5 for integration distribution model - Update spec requirements for directory scanning and file naming convention - Revise tasks for hook discovery implementation Co-Authored-By: Claude Opus 4.5 --- .../changes/add-lifecycle-hooks/design.md | 203 ++++++++++++++++++ .../changes/add-lifecycle-hooks/proposal.md | 32 +++ .../specs/lifecycle-hooks/spec.md | 184 ++++++++++++++++ openspec/changes/add-lifecycle-hooks/tasks.md | 29 +++ 4 files changed, 448 insertions(+) create mode 100644 openspec/changes/add-lifecycle-hooks/design.md create mode 100644 openspec/changes/add-lifecycle-hooks/proposal.md create mode 100644 openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md create mode 100644 openspec/changes/add-lifecycle-hooks/tasks.md diff --git a/openspec/changes/add-lifecycle-hooks/design.md b/openspec/changes/add-lifecycle-hooks/design.md new file mode 100644 index 00000000..37b6e51b --- /dev/null +++ b/openspec/changes/add-lifecycle-hooks/design.md @@ -0,0 +1,203 @@ +## Context + +OpenSpec generates markdown instructions for AI agents at three lifecycle phases: proposal (planning), apply (implementation), and archive (completion). The current implementation hard-codes these instructions in `slash-command-templates.ts`. Users wanting to integrate external tools (issue trackers, CI systems, notification services) must fork OpenSpec and modify these templates directly. + +The Linear MCP integration demonstrated in the `intent-driven-dev` fork shows a common pattern: injecting additional instructions into each phase to call MCP tools for issue status updates. This should be generalizable to any integration. + +**Constraints:** +- Hooks must be markdown instructions (not shell scripts) - they're guidance for AI agents, not executable code +- Must not break existing workflows - hooks are optional +- Must work with all schemas (spec-driven, tdd, custom) +- Should allow project-level configuration without global state + +## Goals / Non-Goals + +**Goals:** +- Enable custom instructions to be injected before/after each lifecycle phase +- Support all three phases: proposal, apply, archive (and any custom phases) +- Discover hooks by convention from `openspec/hooks/` directory +- Support multiple integrations through namespace folders +- Keep hook execution simple - concatenate into phase instructions + +**Non-Goals:** +- Shell script execution (hooks are markdown instructions, not code) +- Global hooks (per-project only) +- Conditional hook execution based on change properties +- Hook dependency ordering (keep it simple) + +## Decisions + +### Decision 1: Hooks as Markdown Instructions + +**Choice:** Hooks are markdown files that get concatenated into phase instructions. + +**Rationale:** OpenSpec generates instructions for AI agents - the agents interpret markdown, not shell scripts. This matches the existing architecture and the pattern demonstrated in the Linear MCP integration. + +**Alternatives considered:** +- Shell scripts: Would require a separate execution model and wouldn't integrate with agent instructions +- MCP tool configuration: Too specific to one integration type; markdown is more flexible +- Schema-level hooks: Would require duplicating hooks across schemas; project-level is simpler + +### Decision 2: Convention-Based Hook Discovery + +**Choice:** Hooks are discovered by convention from `openspec/hooks/` directory using predictable file names. + +**Directory structure:** +``` +openspec/ +├── hooks/ +│ ├── linear/ # Integration namespace +│ │ ├── before-proposal.md # Select Linear story +│ │ ├── after-proposal.md # Move to Todo +│ │ ├── before-apply.md # Update description +│ │ ├── after-apply.md # Move to In Progress +│ │ └── after-archive.md # Move to Done +│ ├── slack/ # Another integration +│ │ └── after-archive.md # Notify channel +│ └── custom/ # Project-specific hooks +│ └── before-apply.md # Custom setup +``` + +**Naming convention:** +- Folder: integration/namespace name (e.g., `linear`, `slack`, `custom`) +- File: `{before|after}-{phase}.md` + +**Composition rules:** +- All hooks for the same phase across ALL folders are concatenated +- Order: alphabetical by folder name, then by file +- Duplicate files within same folder = misconfiguration (warning logged) + +**Rationale:** +- Zero configuration required - just drop files in folders +- Each integration is namespaced - easy to add/remove by folder +- Automatically works with custom schema phases (e.g., `before-review.md`) +- Future-proof: no code changes needed when new phases are added +- Clear ownership: know which integration owns each hook +- Simple to distribute: copy the integration folder + +**Alternatives considered:** +- Flat structure (`hooks/before-proposal.md`): No namespace, conflicts between integrations +- Config-based (`hooks:` in config.yaml): Requires updating config for each hook +- Numbered ordering (`01-before-proposal.md`): More complex, less readable + +### Decision 3: Hook Injection Points + +**Choice:** Inject hooks at these points in the phase instructions: + +| Phase | Before Point | After Point | +|-------|--------------|-------------| +| proposal | After guardrails, before steps | After references | +| apply | After guardrails, before steps | After references | +| archive | After guardrails, before steps | After references | + +**Rationale:** +- "Before" hooks set up context/prerequisites before the main workflow steps +- "After" hooks handle cleanup/follow-up after the workflow completes +- Placing hooks relative to existing sections maintains readability + +### Decision 4: Hook File Resolution + +**Choice:** Hook files are read from `openspec/hooks/{integration}/{timing}-{phase}.md` paths within the project. + +**Path resolution:** +``` +openspec/hooks/linear/before-proposal.md # Reads from project's openspec/hooks/ directory +``` + +**Rationale:** All hooks are centralized in the `openspec/hooks/` directory, keeping integration files organized and separate from other project files. + +**Error handling:** +- Missing `openspec/hooks/` directory: no hooks loaded (not an error) +- Empty integration folder: skipped without warning +- Invalid filenames (e.g., `setup.md`): ignored with debug log +- File read errors: warning logged, hook skipped + +### Decision 5: Integration Distribution Model + +**Choice:** Integrations are distributed as standalone packages containing hook files and optional configuration templates. + +**Integration package structure:** +``` +openspec-linear/ # npm package or git repo +├── README.md # Setup instructions +├── linear/ # Folder name = integration namespace +│ ├── before-proposal.md # Linear story selection +│ ├── after-proposal.md # Move story to Todo +│ ├── before-apply.md # Update story description +│ ├── after-apply.md # Move story to In Progress +│ └── after-archive.md # Move story to Done +├── config/ +│ └── linear.yml.example # Template for openspec/linear.yml +└── install.sh # Optional: copies folder to project +``` + +**Installation methods (user choice):** + +1. **Manual copy** (simplest): + ```bash + # Clone/download the integration + git clone https://github.com/user/openspec-linear + # Copy the integration folder to your project's hooks + cp -r openspec-linear/linear your-project/openspec/hooks/ + ``` + +2. **CLI command** (future enhancement): + ```bash + openspec integration add linear + # Copies integration folder from registry or git URL + ``` + +3. **npm package with postinstall**: + ```bash + npm install openspec-linear --save-dev + # postinstall script copies linear/ folder to openspec/hooks/ + ``` + +**Composing multiple integrations:** + +Automatic! Each integration lives in its own folder: +``` +openspec/hooks/ +├── linear/ # From openspec-linear package +├── slack/ # From openspec-slack package +└── jira/ # From openspec-jira package +``` + +All `before-proposal.md` files from all folders are concatenated into the proposal phase instructions. + +**Rationale:** +- Hooks are just markdown files - trivial to distribute +- No plugin API or runtime to maintain +- Works with any package manager or none at all +- Users can inspect and modify hooks freely + +## Risks / Trade-offs + +| Risk | Mitigation | +|------|------------| +| Large hook files bloating instructions | Add max size check (50KB limit per file) | +| Hook content breaking instruction parsing | Hooks are plain markdown - no special parsing required | +| Users expecting executable hooks | Clear documentation that hooks are agent instructions, not scripts | +| Accidental inclusion of unwanted hooks | Each integration is namespaced - remove folder to remove all its hooks | +| Hook ordering conflicts between integrations | Alphabetical by folder name provides deterministic ordering | +| Discovery overhead scanning directories | Hooks directory is small; scan is fast and cached per command | + +## Migration Plan + +1. **No migration required** - hooks are additive and optional +2. Existing projects continue working unchanged +3. New projects can opt-in by creating `openspec/hooks/{integration}/` directories + +## Open Questions + +1. **Should hooks support schema-level defaults?** (e.g., hooks defined in schema package) + - Current decision: No - start simple with project-level hooks only + - Can add schema-level hooks later if there's demand + +2. **Should there be a way to disable specific integrations without removing the folder?** + - Current decision: No - removing/renaming the folder is simple enough + - Could add `.disabled` marker file or config override later if needed + +3. **Should the CLI provide commands for managing integrations?** + - Current decision: Defer to future enhancement + - Manual copy works for MVP; can add `openspec integration add/remove` later diff --git a/openspec/changes/add-lifecycle-hooks/proposal.md b/openspec/changes/add-lifecycle-hooks/proposal.md new file mode 100644 index 00000000..380214dc --- /dev/null +++ b/openspec/changes/add-lifecycle-hooks/proposal.md @@ -0,0 +1,32 @@ +# Change: Add Lifecycle Hooks for Proposal, Apply, and Archive Phases + +## Why + +OpenSpec's lifecycle phases (proposal, apply, archive) generate instructions for AI agents, but there's no way to inject custom behavior at phase boundaries without modifying OpenSpec's source code. Users integrating external tools (Linear, Jira, GitHub Issues, custom CI pipelines) must fork and modify slash command templates directly. A hooks system would allow users to define custom markdown instructions that get injected before/after each phase, enabling integrations like automatic issue tracking without touching OpenSpec internals. + +## What Changes + +- **New capability**: `lifecycle-hooks` - defines how hooks are discovered and injected +- Discover hooks by convention from `openspec/hooks/{integration}/` directories +- Hook files named `{before|after}-{phase}.md` (e.g., `before-proposal.md`, `after-apply.md`) +- Extend slash command template generation to inject hook instructions at phase boundaries +- Support before/after hooks for any phase (proposal, apply, archive, or custom phases) +- Multiple integrations compose automatically - all hooks for same phase are concatenated +- Hooks are markdown instruction files that get concatenated into the phase instructions + +## Capabilities + +### New Capabilities +- `lifecycle-hooks` - Configuration and injection of custom instructions at lifecycle phase boundaries + +### Modified Capabilities +- `slash-commands` - Extend template generation to discover and inject hooks + +## Impact + +- **Affected code**: + - `src/core/hooks.ts` (new) - Hook discovery and loading from `openspec/hooks/` directory + - `src/core/templates/slash-command-templates.ts` - Inject hook content into phase instructions + - `src/commands/artifact-workflow.ts` - Pass project root to template generation +- **Affected files**: `openspec/hooks/` directory structure for hook files +- **Users**: Can define custom integrations (Linear MCP, Jira, etc.) by adding hook files to `openspec/hooks/{integration}/` without forking OpenSpec diff --git a/openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md b/openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md new file mode 100644 index 00000000..256e5bf6 --- /dev/null +++ b/openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md @@ -0,0 +1,184 @@ +# Spec: Lifecycle Hooks + +## ADDED Requirements + +### Requirement: Hook Directory Discovery + +The system SHALL discover hooks from the `openspec/hooks/` directory by scanning for integration subfolders. + +#### Scenario: Hooks directory exists with integrations +- **WHEN** `openspec/hooks/` contains subfolders (e.g., `linear/`, `slack/`) +- **THEN** system scans each subfolder for hook files + +#### Scenario: Hooks directory does not exist +- **WHEN** `openspec/hooks/` directory does not exist +- **THEN** system returns no hooks (not an error condition) + +#### Scenario: Hooks directory is empty +- **WHEN** `openspec/hooks/` exists but contains no subfolders +- **THEN** system returns no hooks without warning + +#### Scenario: Integration folder is empty +- **WHEN** an integration subfolder exists but contains no files +- **THEN** system skips that integration without warning + +### Requirement: Hook File Naming Convention + +The system SHALL extract timing and phase from hook filenames using the pattern `{before|after}-{phase}.md`. + +#### Scenario: Valid before hook filename +- **WHEN** file is named `before-proposal.md` +- **THEN** system extracts timing=`before` and phase=`proposal` + +#### Scenario: Valid after hook filename +- **WHEN** file is named `after-apply.md` +- **THEN** system extracts timing=`after` and phase=`apply` + +#### Scenario: Custom phase name +- **WHEN** file is named `before-review.md` +- **THEN** system extracts timing=`before` and phase=`review` + +#### Scenario: Invalid filename format +- **WHEN** file is named `setup.md` (no timing prefix) +- **THEN** system ignores the file with debug log + +#### Scenario: Invalid timing prefix +- **WHEN** file is named `during-proposal.md` +- **THEN** system ignores the file with debug log + +#### Scenario: Non-markdown file +- **WHEN** file is named `before-proposal.txt` +- **THEN** system ignores the file (only `.md` files processed) + +### Requirement: Hook Composition + +The system SHALL concatenate hooks from all integration folders for the same phase and timing. + +#### Scenario: Multiple integrations with same hook +- **WHEN** both `linear/before-proposal.md` and `slack/before-proposal.md` exist +- **THEN** both are concatenated into the before-proposal hooks + +#### Scenario: Alphabetical ordering by integration +- **GIVEN** integrations `linear/` and `another/` both have `before-proposal.md` +- **WHEN** hooks are composed +- **THEN** `another/before-proposal.md` appears before `linear/before-proposal.md` + +#### Scenario: Single integration with hook +- **WHEN** only `linear/after-archive.md` exists for that phase/timing +- **THEN** only that hook content is used + +### Requirement: Duplicate Detection + +The system SHALL warn when duplicate filenames exist within the same integration folder. + +#### Scenario: No duplicates +- **WHEN** integration folder contains unique filenames +- **THEN** no warnings are logged + +#### Scenario: Duplicate detected (case sensitivity) +- **WHEN** integration folder contains both `before-proposal.md` and `BEFORE-PROPOSAL.md` on case-insensitive filesystem +- **THEN** system logs a warning about the duplicate + +### Requirement: Supported Lifecycle Phases + +The system SHALL support hooks for any phase name, not a hardcoded list. + +#### Scenario: Standard phase hooks +- **WHEN** files match `before-proposal.md`, `after-apply.md`, `before-archive.md` +- **THEN** system processes them for their respective phases + +#### Scenario: Custom phase hooks +- **WHEN** file matches `before-review.md` (custom phase from schema) +- **THEN** system processes it for the `review` phase + +#### Scenario: Unknown phase with no matching workflow +- **WHEN** file matches `before-unknown.md` but no workflow uses `unknown` phase +- **THEN** hook is loaded but never injected (no error) + +### Requirement: Hook Timing Points + +The system SHALL support `before` and `after` timing for each lifecycle phase. + +#### Scenario: Before hook timing +- **WHEN** a `before` hook exists for a phase +- **THEN** the hook content is injected after guardrails and before the main steps + +#### Scenario: After hook timing +- **WHEN** an `after` hook exists for a phase +- **THEN** the hook content is injected after the references section + +#### Scenario: Both before and after hooks +- **WHEN** both `before` and `after` hooks exist for a phase +- **THEN** both are injected at their respective positions + +### Requirement: Hook Content Size Limit + +The system SHALL enforce a 50KB size limit per hook file to prevent instruction bloat. + +#### Scenario: Hook content within limit +- **WHEN** hook file is under 50KB +- **THEN** content is included in phase instructions + +#### Scenario: Hook content exceeds limit +- **WHEN** hook file exceeds 50KB +- **THEN** system logs a warning with the size and limit, and skips that hook file + +#### Scenario: Combined hooks under total limit +- **WHEN** multiple hook files for same phase are each under 50KB +- **THEN** all are concatenated (no aggregate limit, only per-file) + +### Requirement: Hook Injection into Phase Instructions + +The system SHALL inject resolved hook content into the slash command templates at the appropriate positions. + +#### Scenario: Before hook injection +- **WHEN** generating proposal instructions with `before` hooks discovered +- **THEN** output includes: + 1. Guardrails section + 2. **Hook content** (with integration header: `## linear: Before Proposal`) + 3. Steps section + 4. References section + +#### Scenario: After hook injection +- **WHEN** generating proposal instructions with `after` hooks discovered +- **THEN** output includes: + 1. Guardrails section + 2. Steps section + 3. References section + 4. **Hook content** (with integration header: `## linear: After Proposal`) + +#### Scenario: No hooks discovered +- **WHEN** generating phase instructions with no hooks in `openspec/hooks/` +- **THEN** output is identical to current behavior (no additional sections) + +#### Scenario: Hook section headers include integration name +- **WHEN** hook from `linear/before-proposal.md` is injected +- **THEN** it is prefixed with header: `## linear: Before Proposal` + +#### Scenario: Multiple integrations in same position +- **GIVEN** `another/before-proposal.md` and `linear/before-proposal.md` exist +- **WHEN** before hooks are injected +- **THEN** output includes: + ```markdown + ## another: Before Proposal + [content from another] + + ## linear: Before Proposal + [content from linear] + ``` + +### Requirement: Hook Loading During Template Generation + +The system SHALL discover and load hooks when generating slash command templates, not ahead of time. + +#### Scenario: Hooks discovered during template generation +- **WHEN** slash command templates are generated +- **THEN** system scans `openspec/hooks/` at that time + +#### Scenario: File changes reflected immediately +- **WHEN** a hook file is modified between command invocations +- **THEN** the next template generation uses the updated content + +#### Scenario: Missing project root +- **WHEN** template generation cannot determine project root +- **THEN** hooks are skipped without error diff --git a/openspec/changes/add-lifecycle-hooks/tasks.md b/openspec/changes/add-lifecycle-hooks/tasks.md new file mode 100644 index 00000000..b94a9dc8 --- /dev/null +++ b/openspec/changes/add-lifecycle-hooks/tasks.md @@ -0,0 +1,29 @@ +## 1. Hook Discovery + +- [ ] 1.1 Create `src/core/hooks.ts` with `discoverHooks(projectRoot, phase)` function +- [ ] 1.2 Scan `openspec/hooks/*/` for integration folders +- [ ] 1.3 Parse filenames: extract timing (before/after) and phase name from `{timing}-{phase}.md` +- [ ] 1.4 Concatenate hooks from all folders (alphabetical order by folder name) +- [ ] 1.5 Add file content loading with 50KB size limit per file +- [ ] 1.6 Warn on duplicate files within same integration folder + +## 2. Template Integration + +- [ ] 2.1 Update `slash-command-templates.ts` to accept hooks parameter +- [ ] 2.2 Create `injectHooks()` function for before/after positioning +- [ ] 2.3 Modify `getSlashCommandBody()` to discover and inject hooks +- [ ] 2.4 Add integration folder name as section header when injecting (e.g., `## linear: Before Proposal`) + +## 3. Testing + +- [ ] 3.1 Add unit tests for hook discovery across multiple integration folders +- [ ] 3.2 Add unit tests for filename parsing (valid/invalid patterns) +- [ ] 3.3 Add unit tests for custom phase names (e.g., `before-review.md`) +- [ ] 3.4 Add unit tests for hook concatenation ordering (alphabetical by folder) +- [ ] 3.5 Add integration test: hooks folder → generated template includes hook content + +## 4. Documentation + +- [ ] 4.1 Document hook folder structure and naming convention in AGENTS.md +- [ ] 4.2 Add example integration package structure +- [ ] 4.3 Document how to create and distribute integrations