Skip to content

[Feature] Add Extensible Lifecycle Hooks for Schemas and Project Config#701

Open
lsmonki wants to merge 34 commits intoFission-AI:mainfrom
lsmonki:feature/custom-lifecycle-hooks
Open

[Feature] Add Extensible Lifecycle Hooks for Schemas and Project Config#701
lsmonki wants to merge 34 commits intoFission-AI:mainfrom
lsmonki:feature/custom-lifecycle-hooks

Conversation

@lsmonki
Copy link

@lsmonki lsmonki commented Feb 12, 2026

Add Extensible Lifecycle Hooks for Schemas and Project Config

Summary

Adds a lifecycle hook system that lets schemas and projects define LLM instructions executed at operation boundaries (pre/post for every skill). Schema-level hooks define workflow-inherent behavior; project-level hooks add project-specific customization. Both are surfaced via openspec instructions --hook.

  • 20 lifecycle points covering all 10 operations: explore, new, continue, ff, apply, verify, sync, archive, bulk-archive, onboard
  • Hook resolution: schema hooks first, then config hooks (additive, not override)
  • Exposed via --hook flag on existing instructions command (no new top-level commands)
  • All skill templates updated with hook execution steps
  • Hooks are LLM instructions only — no shell execution (deferred to future iteration)

Motivation

Addresses #682 (Extensible Hook Capability) directly — the original request for pre/post-archive hooks to inject custom consolidation logic (error log management, ADR generation, notifications).

This implementation goes beyond archive to cover all operations, also enabling use cases raised in other issues:

What Changed

Core

File Change
src/core/artifact-graph/types.ts VALID_LIFECYCLE_POINTS (20 points), HookSchema, HooksSchema Zod types, ResolvedHook interface
src/core/artifact-graph/instruction-loader.ts resolveHooks() — reads schema + config hooks, returns merged array (schema first)
src/core/project-config.ts Optional hooks field with resilient field-by-field parsing and unknown key warnings
src/commands/workflow/hooks.ts Internal module: hook resolution command with JSON and text output

CLI

File Change
src/cli/index.ts --hook <lifecycle-point> option on instructions command, mutual exclusivity with [artifact], --schema rejection in hook mode

Skill Templates

All 10 operations updated in src/core/templates/skill-templates.ts:

Operation Hooks Nesting
explore pre-explore / post-explore
new pre-new / post-new
continue pre-continue / post-continue
ff pre-ff / post-ff Fires pre-continue/post-continue per artifact iteration
apply pre-apply / post-apply
verify pre-verify / post-verify
sync pre-sync / post-sync
archive pre-archive / post-archive
bulk-archive pre-bulk-archive / post-bulk-archive Fires pre-archive/post-archive per individual change
onboard pre-onboard / post-onboard

Docs & Specs

  • docs/cli.md — All three instructions modes documented (artifact, apply, hook)
  • Full change artifacts under openspec/changes/add-lifecycle-hooks/ (proposal, design, specs, tasks)

Tests

  • Unit tests — Schema parsing, config parsing, resolveHooks() ordering and edge cases
  • CLI integration — All 20 lifecycle points validated, JSON output format, mutual exclusivity, schema+config merge ordering
  • 635+ new lines of test code across 4 test files

Hook YAML Structure

# In schema.yaml or config.yaml
hooks:
  post-archive:
    instruction: |
      Review the archived change and generate ADR entries
  pre-verify:
    instruction: |
      Run the full test suite before verification begins

CLI Usage

# With change context (resolves schema from change metadata)
openspec instructions --hook pre-archive --change "add-dark-mode" --json

# Without change (resolves schema from config.yaml default)
openspec instructions --hook post-new --json

JSON Output

{
  "lifecyclePoint": "post-archive",
  "changeName": "add-dark-mode",
  "hooks": [
    { "source": "schema", "instruction": "Generate ADR entries..." },
    { "source": "config", "instruction": "Notify Slack channel..." }
  ]
}

Backward Compatibility

Fully backward-compatible. The hooks field is optional in both schema.yaml and config.yaml. Existing schemas and configs work without modification.

Summary by CodeRabbit

  • New Features

    • Retrieve and run lifecycle hooks via a new --hook mode on the instructions command; workflows now run pre/post hook steps that emit and apply returned instructions (mutually exclusive with artifact/schema options).
  • Documentation

    • Expanded CLI docs, examples, and JSON/text output samples for hook modes and lifecycle points.
  • Tests

    • Extensive unit and CLI tests covering parsing, resolution, ordering, and error cases.
  • Chores

    • Updated .gitignore to exclude Codex and related GitHub-generated files.

…ission-AI#682)

Add hooks system that allows schemas and projects to define LLM instructions
at operation lifecycle points (pre/post for new, archive, sync, apply).

- Schema YAML and project config support optional `hooks` section
- New `openspec hooks` CLI command resolves and returns hooks for a lifecycle point
- Without --change, schema is resolved from config.yaml's default schema field
- Skill templates updated to call hooks at all lifecycle boundaries
- 23 new tests covering parsing, resolution, CLI, and edge cases
…ifecycle points

Consolidate the standalone `openspec hooks` command into `openspec instructions --hook <lifecycle-point>`,
add pre-verify and post-verify lifecycle points (10 total), update all skill templates to use the new
invocation, and document the unified instructions command.
Add 4 new lifecycle points (14 total). The ff skill fires pre-ff/post-ff
around the entire operation and pre-continue/post-continue for each
artifact iteration within it. The continue skill fires pre-continue/post-continue
around each artifact creation.
Complete the lifecycle hooks coverage across all skills (20 total
lifecycle points). Add pre/post hooks for explore, bulk-archive, and
onboard. Bulk-archive also fires pre-archive/post-archive per
individual change within the batch.
- Use trimEnd() instead of trim() to preserve leading whitespace in
  hook instructions
- Reject --schema in hook mode with explicit error instead of silently
  ignoring it
- Clarify pre-new hook wording: schema hooks may apply if config.yaml
  sets a default schema (not "config-only")
- Unify hook ordering note across all skill templates: consistently
  state "schema hooks first, then config hooks"
…mand

The validation is already performed inside resolveHooks() in
instruction-loader.ts. The CLI-level check for missing argument
(undefined) is kept since it's specific to the command interface.
@lsmonki lsmonki requested a review from TabishB as a code owner February 12, 2026 11:20
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds lifecycle hooks support: new hook types and VALID_LIFECYCLE_POINTS, resolveHooks merging schema+config hooks, CLI --hook integration, template changes to invoke hooks pre/post stages, new specs/design/docs/tasks, and comprehensive unit/CLI tests and .gitignore additions.

Changes

Cohort / File(s) Summary
CLI & Commands
src/cli/index.ts, src/commands/workflow/hooks.ts, src/commands/workflow/index.ts, src/commands/workflow/instructions.ts
Add --hook option to instructions, introduce hooksCommand and HooksOptions, enforce mutual exclusivity with artifact/schema, and wire JSON/text output for hooks.
Core types & exports
src/core/artifact-graph/types.ts, src/core/artifact-graph/index.ts, src/core/project-config.ts
Introduce HookSchema/HooksSchema, Hook/ Hooks/ LifecyclePoint types, VALID_LIFECYCLE_POINTS, export types, and extend project config schema with optional hooks plus defensive parsing and warnings.
Hook resolution logic
src/core/artifact-graph/instruction-loader.ts
Add ResolvedHook type and resolveHooks(projectRoot, changeName, lifecyclePoint) that validates lifecycle points, loads schema (change or default), merges schema hooks then config hooks, and returns ordered results.
Skill templates
src/core/templates/skill-templates.ts
Insert pre- and post- hook invocation blocks across skill templates to call openspec instructions --hook <point> --json and apply returned instructions before proceeding.
Specs, design & tasks
openspec/changes/add-lifecycle-hooks/*
Add design.md, proposal.md, tasks.md, multiple spec delta files describing lifecycle points, resolution rules, CLI contract, and skill integrations.
Docs
docs/cli.md
Document --hook behavior, artifact/apply/hook modes, lifecycle points, resolution order, and JSON/text examples.
Tests
test/commands/artifact-workflow.test.ts, test/core/artifact-graph/instruction-loader.test.ts, test/core/artifact-graph/schema.test.ts, test/core/project-config.test.ts
Add tests for parsing hooks (schema/config), resolveHooks ordering and errors, CLI --hook behavior (text/JSON, mutual exclusivity, all lifecycle points), and schema/config integration.
Repo housekeeping
.gitignore, openspec/changes/add-lifecycle-hooks/.openspec.yaml
Append ignores for .codex/ and GitHub-generated prompts/skills directories; add feature metadata file.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "CLI (instructions)"
    participant HooksCmd as "hooksCommand"
    participant Resolver as "resolveHooks"
    participant Schema as "Schema (.openspec or change metadata)"
    participant Config as "ProjectConfig"
    participant Skill as "Skill / Agent"

    User->>CLI: openspec instructions --hook pre-apply --change myChange --json
    CLI->>HooksCmd: hooksCommand(pre-apply, {change: "myChange", json: true})
    HooksCmd->>Resolver: resolveHooks(projectRoot, "myChange", "pre-apply")
    Resolver->>Schema: Load schema for myChange (metadata or config default)
    Schema-->>Resolver: schema hooks (if any)
    Resolver->>Config: Load project config hooks
    Config-->>Resolver: config hooks (if any)
    Resolver->>Resolver: Merge (schema hooks first, then config hooks)
    Resolver-->>HooksCmd: [ResolvedHook{source, instruction}...]
    HooksCmd-->>CLI: JSON output { lifecyclePoint, changeName, hooks }
    CLI-->>User: display JSON

    rect rgba(100, 150, 200, 0.5)
    Note over Skill,CLI: Skill fetches hooks and executes them in order
    Skill->>CLI: openspec instructions --hook pre-apply --change myChange --json
    CLI->>HooksCmd: retrieve hooks
    HooksCmd-->>Skill: return hooks
    Skill->>Skill: Execute hooks sequentially (schema then config)
    Skill->>Skill: Proceed with main workflow step
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • TabishB

"🐰
Hooks sprout before and after each stride,
Schemas whisper, configs walk beside,
Twenty points to nudge the flow,
Agents follow, then onward go,
A nimble hop for every guide!"

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature being added: extensible lifecycle hooks for schemas and project config, which is the primary objective of this comprehensive changeset.
Docstring Coverage ✅ Passed Docstring coverage is 96.30% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

This PR adds a comprehensive lifecycle hook system that enables schemas and projects to inject LLM instructions at 20 operation boundaries (pre/post for all 10 operations). The implementation is well-architected, thoroughly tested, and fully backward-compatible.

Key Changes:

  • Type-safe hook definitions - Added HookSchema and HooksSchema with Zod validation, plus 20 lifecycle point constants in types.ts
  • Hook resolution logic - resolveHooks() function merges schema and config hooks with clear precedence (schema first, config second)
  • CLI integration - Extended instructions command with --hook flag, proper mutual exclusivity checks with artifact argument
  • Resilient config parsing - Project config parser validates hooks against known lifecycle points with helpful warnings
  • Complete skill template updates - All 10 operation templates updated with hook execution steps at appropriate boundaries
  • Comprehensive testing - 635+ lines of test code covering unit tests, integration tests, resolution order, edge cases, and all 20 lifecycle points

Implementation Quality:

  • Clean separation of concerns between types, resolution logic, CLI, and config parsing
  • Proper error handling and validation at all boundaries
  • Helpful error messages for invalid lifecycle points
  • JSON and text output modes for programmatic and human consumption
  • Follows existing OpenSpec patterns and conventions

Testing Coverage:

  • Unit tests verify hook resolution order, schema/config merging, and edge cases
  • CLI integration tests validate all 20 lifecycle points and JSON output format
  • Schema and config parsing tests ensure validation works correctly
  • No bugs or issues identified in the implementation

Confidence Score: 5/5

  • This PR is safe to merge with no issues identified
  • The implementation is exceptional: clean architecture with proper separation of concerns, comprehensive test coverage (635+ new test lines covering all 20 lifecycle points), type-safe with Zod validation, fully backward-compatible, resilient error handling, and follows established patterns. The code is production-ready.
  • No files require special attention

Important Files Changed

Filename Overview
src/core/artifact-graph/types.ts Added hook type definitions and lifecycle point constants - clean type-safe implementation with Zod validation
src/core/artifact-graph/instruction-loader.ts Added resolveHooks function with proper resolution order (schema first, config second) and lifecycle point validation
src/core/project-config.ts Added resilient hooks parsing with lifecycle point validation and helpful warnings for unknown points
src/cli/index.ts Added --hook flag to instructions command with proper mutual exclusivity checks
src/commands/workflow/hooks.ts New command implementation for hook resolution with JSON and text output modes
src/core/templates/skill-templates.ts Updated all 10 skill templates with pre/post hook execution steps at appropriate lifecycle boundaries
test/commands/artifact-workflow.test.ts Added comprehensive CLI integration tests for hook command with JSON output and all lifecycle points
test/core/artifact-graph/instruction-loader.test.ts Added unit tests for resolveHooks covering resolution order, edge cases, and schema/config merging

Sequence Diagram

sequenceDiagram
    participant Agent as LLM Agent
    participant CLI as OpenSpec CLI
    participant Hooks as Hooks Command
    participant Loader as Instruction Loader
    participant Config as Config Parser
    participant Schema as Schema Resolver

    Note over Agent,CLI: Hook Execution at Lifecycle Point
    
    Agent->>CLI: openspec instructions --hook pre-archive --change "my-change" --json
    CLI->>Hooks: hooksCommand("pre-archive", {change: "my-change"})
    
    Hooks->>Loader: resolveHooks(projectRoot, "my-change", "pre-archive")
    
    Note over Loader: Resolution Phase 1: Schema Hooks
    Loader->>Schema: resolveSchema("my-change")
    Schema-->>Loader: schema.yaml with hooks
    Loader->>Loader: Extract schema.hooks["pre-archive"]
    
    Note over Loader: Resolution Phase 2: Config Hooks
    Loader->>Config: readProjectConfig(projectRoot)
    Config-->>Loader: config.yaml with hooks
    Loader->>Loader: Extract config.hooks["pre-archive"]
    
    Loader-->>Hooks: [{source: "schema", instruction: "..."}, {source: "config", instruction: "..."}]
    
    Hooks->>Hooks: Format JSON output
    Hooks-->>CLI: JSON with hooks array
    CLI-->>Agent: {"lifecyclePoint": "pre-archive", "changeName": "my-change", "hooks": [...]}
    
    Note over Agent: Agent executes each hook instruction in order
    
    Agent->>Agent: Execute schema hook instruction
    Agent->>Agent: Execute config hook instruction
    Agent->>Agent: Continue with lifecycle operation
Loading

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@openspec/changes/add-lifecycle-hooks/design.md`:
- Line 239: The design doc's statement "Start with 10 lifecycle points only" is
out of sync with the implementation; update the text in design.md to reflect the
actual count defined in types.ts by changing "10 lifecycle points" to "20
lifecycle points" so it matches the VALID_LIFECYCLE_POINTS array in types.ts
(and keep any explanatory sentence about CLI/LLM cost unchanged).

In `@openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md`:
- Around line 3-4: The Purpose paragraph currently enumerates only "pre/post
archive, sync, new, apply, verify" which is incomplete; update the Purpose
section in specs/lifecycle-hooks/spec.md (the "Purpose" heading and the sentence
referencing operations and the `openspec instructions --hook` flag) to either
list all ten operations (including explore, continue, ff, bulk-archive, onboard)
or replace the explicit list with a general phrase like "pre/post for each
operation" so it matches the full operation list shown later.

In `@openspec/changes/add-lifecycle-hooks/tasks.md`:
- Around line 41-42: The checklist in tasks.md has skipped numbers 6.7 and 8.7
(jumping 6.6→6.8 and 8.6→8.8); update the numbering to remove the gaps so the
sequence is contiguous (e.g., renumber the current 6.8→6.7 and 8.8→8.7), and
ensure the related entries referencing getContinueChangeSkillTemplate,
getOpsxContinueCommandTemplate, getFfChangeSkillTemplate, and
getOpsxFfCommandTemplate remain correct after renumbering.

In `@openspec/config.yaml`:
- Around line 29-69: The config currently contains 20 test scaffolding lifecycle
hooks under the hooks key (e.g., pre-explore, post-explore, pre-new, post-new,
pre-continue, post-continue, pre-ff, post-ff, pre-apply, post-apply, pre-verify,
post-verify, pre-sync, post-sync, pre-archive, post-archive, pre-bulk-archive,
post-bulk-archive, pre-onboard, post-onboard) that instruct the LLM to append to
HOOKSTEST.md; remove these test hooks from the production openspec config and
either (a) delete the entries so no test instructions are shipped, (b) replace
them with real project-specific lifecycle instructions if needed, or (c) move
them into a dedicated test fixture/config used only by acceptance tests; ensure
the hooks root no longer contains the HOOKSTEST.md append instructions and
update any test harness to point at the test fixture if you choose to move them.

In `@src/core/artifact-graph/instruction-loader.ts`:
- Around line 403-405: resolveHooks currently calls readProjectConfig without
error handling, causing filesystem errors to throw; wrap the call to
readProjectConfig in a try/catch (mirroring generateInstructions) so that on
error you set config to undefined (or null) and continue building the hooks
array, optionally logging the error if a logger is available; update the const
config = readProjectConfig(projectRoot) usage in resolveHooks to use the
safely-captured value from the try/catch block.

In `@src/core/templates/skill-templates.ts`:
- Around line 2691-2695: The "post-bulk-archive" post-hook is documented after
the summary which is inconsistent with other post-hooks; move the "Execute
post-bulk-archive hooks" section so it runs before the summary display (i.e.,
swap the current steps 10 and 11) and update the corresponding command template
paragraph that references post-bulk-archive to the same position as other
post-hooks (matching the pattern used by "post-apply", "post-archive", and
"post-sync") so hooks complete before the final summary is shown.
🧹 Nitpick comments (13)
openspec/changes/add-lifecycle-hooks/specs/cli-artifact-workflow/spec.md (1)

24-28: Missing scenario: --schema flag rejected in hook mode.

The PR objectives state that --schema is rejected when using --hook, but this spec only documents mutual exclusivity with the artifact argument. Consider adding a scenario for --schema + --hook rejection to keep the spec complete.

src/core/project-config.ts (1)

165-196: Hooks parsing logic is clean and consistent with existing patterns.

One minor observation: the Zod object z.object({ instruction: z.string().min(1) }) at line 179 is re-created on each loop iteration. You could hoist it above the loop for a tiny efficiency gain, similar to how validPoints is created once.

♻️ Hoist Zod schema outside loop
       const parsedHooks: Record<string, { instruction: string }> = {};
       let hasValidHooks = false;
       const validPoints = new Set<string>(VALID_LIFECYCLE_POINTS);
+      const hookSchema = z.object({ instruction: z.string().min(1) });

       for (const [point, hook] of Object.entries(raw.hooks)) {
         // Warn on unrecognized lifecycle points
         if (!validPoints.has(point)) {
           console.warn(`Unknown lifecycle point in hooks: "${point}". Valid points: ${VALID_LIFECYCLE_POINTS.join(', ')}`);
           continue;
         }

-        const hookResult = z.object({ instruction: z.string().min(1) }).safeParse(hook);
+        const hookResult = hookSchema.safeParse(hook);
test/core/artifact-graph/instruction-loader.test.ts (2)

627-652: Helper createSchemaWithHooks generates valid test schemas — consider a note about quote-safety.

The YAML template at lines 641-643 wraps instruction values in double quotes. This works for current test strings but would produce invalid YAML if an instruction contained " characters. Fine for tests as-is, but worth a brief inline comment if the helper gets reused.


752-774: Test exercises null changeName but only with built-in schema (no hooks).

When changeName is null, the spec says the system resolves the default schema from config.yaml. This test's config references spec-driven (no hooks), so it effectively only tests the "no schema hooks" path. Consider adding a test where the config's default schema does have hooks, to verify that schema hooks from the default schema are also included alongside config hooks.

test/core/artifact-graph/schema.test.ts (1)

207-288: Consider adding a test for unrecognized lifecycle point keys in schema hooks.

The schema's HooksSchema uses z.record(z.string(), ...) and accepts any string key without validation, whereas project config parsing explicitly validates against VALID_LIFECYCLE_POINTS and warns on unknown lifecycle points. A test case like "invalid-point" would verify this behavior and document the design choice that schemas are more permissive than configs.

test/core/project-config.test.ts (1)

615-632: Consider asserting on console.warn behavior for hooks: null.

The analogous rules: null test (Line 145) explicitly checks that a warning is emitted. This test asserts the correct parsed output but doesn't verify whether a warning is or isn't emitted. Adding expect(consoleWarnSpy).toHaveBeenCalled() or expect(consoleWarnSpy).not.toHaveBeenCalled() would make the intent explicit and prevent silent regressions if the warning behavior changes.

openspec/changes/add-lifecycle-hooks/specs/specs-sync-skill/spec.md (1)

1-28: Spec is clear and follows the established pattern.

Minor note: The archive and verify skill specs include a "Scenario: Hook instruction references change context" section (e.g., opsx-archive-skill/spec.md Line 30–33, opsx-verify-skill/spec.md Line 30–33). This spec omits it. Consider adding it for consistency if the sync skill also operates within a change context.

test/commands/artifact-workflow.test.ts (2)

942-952: Sequential loop over 20 lifecycle points may be slow or flaky in CI.

This test spawns 20 separate CLI processes sequentially, each with a 30s timeout, inside a single test with a 60s vitest timeout. If each invocation takes ~3s (process spawn + CLI bootstrap), that's ~60s total — right at the timeout boundary.

Consider either:

  • Increasing the vitest timeout for this specific test (e.g., 120000), or
  • Spot-checking a smaller representative subset (e.g., first, last, and a middle point) instead of all 20.

943-943: Import VALID_LIFECYCLE_POINTS instead of hardcoding the array.

The lifecycle points are hardcoded at line 943, but VALID_LIFECYCLE_POINTS is already exported from src/core/artifact-graph/types.ts and available through the barrel export. If the canonical source is updated, this test won't detect the drift. Import and reuse the constant to keep the test in sync with the source, as test/core/artifact-graph/instruction-loader.test.ts already does.

src/commands/workflow/hooks.ts (1)

92-97: Text output label omits the schema name mentioned in the design doc.

The design document (Line 93 of design.md) shows ### From schema (spec-driven) including the schema name, but the implementation outputs just ### From schema. If the schema name is useful context for the user/LLM, consider including it. This would require passing the resolved schema name through ResolvedHook or HooksOutput.

src/core/artifact-graph/instruction-loader.ts (1)

396-401: Lifecycle point validation creates a new Set on every call.

This is a CLI tool so it's not a real performance concern, but for tidiness you could hoist the Set to module scope as a constant since VALID_LIFECYCLE_POINTS is immutable.

src/cli/index.ts (1)

35-37: Type alias placed between import statements.

InstructionsActionOptions is defined on Line 36, sandwiched between two import blocks. Consider moving it after all imports for clarity, or colocating it near the instructions command definition.

src/core/templates/skill-templates.ts (1)

1595-1784: Consider extracting shared instruction content to reduce duplication, similar to getOnboardInstructions().

The explore skill template (Lines 28–322) and explore command template (Lines 1601–1783) are nearly identical, differing only in a few phrasing details. The same applies to new, continue, apply, ff, sync, archive, bulk-archive, and verify. Only onboard already uses a shared getOnboardInstructions() helper.

With 20 hook blocks now injected across both skill and command variants, maintaining consistency between pairs becomes more error-prone (e.g., a hook fix in one could be missed in the other). Extracting shared instruction strings—as done for onboard—would halve the maintenance surface for hook-related content.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@openspec/changes/add-lifecycle-hooks/design.md`:
- Around line 90-99: The unlabeled fenced code blocks in the design.md snippets
(e.g., the "## Hooks: post-archive (change: add-dark-mode)" section and other
blocks containing "pre-explore       post-explore …", the "openspec instructions
--hook pre-archive --change \"<name>\" --json" command block, and the "ff:
pre-ff …" block) must include language identifiers; update each triple-backtick
fence to use an appropriate tag (e.g., ```text for plain output blocks and
```bash for shell/command examples) so markdownlint MD040 is satisfied and
formatting is consistent across the file.

In `@openspec/changes/add-lifecycle-hooks/specs/lifecycle-hooks/spec.md`:
- Around line 96-130: Update the lifecycle-hooks spec text to explicitly state
(1) the CLI rejects using --schema together with --hook (document the mutual
exclusion and the expected error behavior when both are provided) and (2) JSON
output always returns the full object shape { lifecyclePoint, changeName, hooks
} even when no hooks are found, with hooks: [] and changeName set to null when
no change is provided; ensure these clarifications reference the CLI invocation
form "openspec instructions --hook <lifecycle-point> [--change <name>] [--json]"
and the JSON fields "lifecyclePoint", "changeName", and "hooks" so readers know
to expect an empty array rather than null/omitted fields.

In `@src/core/templates/skill-templates.ts`:
- Around line 2584-2589: Add a fallback note and command for post-archive hooks
in both bulk-archive templates where the docs show running `openspec
instructions --hook post-archive --change "<name>" --json`; after the existing
`mv`/single-archive wording insert a short "Note" that `--change` may not
resolve once the change is moved to archive and instruct to fall back to running
`openspec instructions --hook post-archive --json` if the `--change` invocation
fails, mirroring the single-archive fallback behavior; update both bulk-archive
occurrences (the block around the current post-archive hook example and the
separate occurrence referenced at the later section) so the templates show the
primary command plus the fallback.

@TabishB
Copy link
Contributor

TabishB commented Feb 13, 2026

This feels a little off to me from a design perspective. I'm usually very reluctant to add more branching paths to an agents prompts. We're trying to model a very deterministic flow into a prompt/skill.

We are calling this hooks, but what we actually built is prompt injection that depends on the agent choosing to run extra commands.
Real hooks should be runtime control flow in command execution, not instructions in a skill template.

Also, modeling hooks as a flat root hooks map is the wrong abstraction. We need lifecycle behavior attached to the thing being orchestrated: artifacts and commands/operations.

I would sort of expect this to be linked to a coding agent's hook system too to be called more deterministically when an openspec skill is invoked.

I get what you're trying to do - I was forced to do something similar to make certain things work, but I'm not a fan of cramming more and more instructions into a prompt.

The way I've been thinking about OpenSpec hooks is more through the artifacts generated, where we could either 1) integrate with coding agent hooks 2) have some sort of file watcher system that could watch things in the /openspec folder

Eliminates the type intersection `& { hook?: string }` in cli/index.ts
by adding hook directly to InstructionsOptions.
@lsmonki
Copy link
Author

lsmonki commented Feb 13, 2026

This feels a little off to me from a design perspective. I'm usually very reluctant to add more branching paths to an agents prompts. We're trying to model a very deterministic flow into a prompt/skill.

We are calling this hooks, but what we actually built is prompt injection that depends on the agent choosing to run extra commands. Real hooks should be runtime control flow in command execution, not instructions in a skill template.

Also, modeling hooks as a flat root hooks map is the wrong abstraction. We need lifecycle behavior attached to the thing being orchestrated: artifacts and commands/operations.

I would sort of expect this to be linked to a coding agent's hook system too to be called more deterministically when an openspec skill is invoked.

I get what you're trying to do - I was forced to do something similar to make certain things work, but I'm not a fan of cramming more and more instructions into a prompt.

The way I've been thinking about OpenSpec hooks is more through the artifacts generated, where we could either 1) integrate with coding agent hooks 2) have some sort of file watcher system that could watch things in the /openspec folder

Hey @TabishB, appreciate the detailed feedback. You raised important points and I want to address them directly.

You're right: this is prompt injection

No argument there. These hooks are LLM instructions injected into skill prompts. The agent may or may not follow them perfectly.

But here's the thing — that's what OpenSpec is. Every layer of the system works this way:

  • Artifact instruction in schema.yaml → prompt injection
  • context in config.yaml → prompt injection
  • rules in config.yaml → prompt injection
  • Artifact template files → prompt injection
  • Skill templates themselves → prompt injection

Hooks at operation boundaries are the natural extension of this model to points where no injection mechanism exists today. The compliance guarantees are exactly the same as the rest of the system — no better, no worse.

Two layers, not alternatives

You mentioned coding agent hooks and file watchers as alternatives. I think they're actually a different layer that serves a different purpose:

Coding agent hooks (Claude Code) OpenSpec LLM hooks (this PR)
Nature Shell commands, deterministic LLM instructions, reasoning-based
Guarantee 100% — always executes Best-effort — same as all of OpenSpec
Good for Mechanical tasks Tasks requiring reasoning

Coding agent hooks would handle things like:

  • Run npm test before /opsx:verify (PreToolUse)
  • Auto-format artifacts with Prettier after creation (PostToolUse on Write)
  • Send a Slack webhook after archive completes (Stop hook)
  • Validate the git branch is clean before /opsx:apply (PreToolUse)
  • Call Linear API to move a ticket to Done (PostToolUse)

These are mechanical actions — a shell command can do them. No reasoning needed.

LLM hooks handle things that only a reasoning agent can do:

  • "Review the archived change and generate ADR entries capturing key decisions from design.md"
  • "Compare the implementation against specs and report deviations" (pre-verify)
  • "Consolidate the error log: associate fixed errors with this change and move them to the historical archive" (post-archive)
  • "Review existing specs in openspec/specs/ and identify related capabilities before creating this change" (pre-new)
  • "Check that every requirement in specs has at least one test covering it" (pre-verify)

No shell script can do these — they require reading, analyzing, and reasoning about project artifacts.

On the flat hooks map abstraction

You said: "modeling hooks as a flat root hooks map is the wrong abstraction. We need lifecycle behavior attached to the thing being orchestrated: artifacts and commands/operations."

For artifacts, I agree — and that's already covered. Artifacts already have lifecycle behavior attached directly to them through two layers:

Schema-level (schema.yamlinstruction): the schema author can define before/during/after behavior for each artifact:

# schema.yaml — instruction acts as before + during + after guidance
artifacts:
  - id: proposal
    generates: proposal.md
    template: proposal.md
    instruction: |
      BEFORE writing:
      - Research existing specs in openspec/specs/ to understand current capabilities
      - Check if similar changes have been proposed before in openspec/archive/

      Create the proposal with sections: Why, What Changes, Capabilities, Impact.
      The Capabilities section creates the contract between proposal and specs.

      AFTER writing:
      - Verify every capability listed has a unique kebab-case name
      - Confirm no capability duplicates an existing spec in openspec/specs/

  - id: specs
    generates: "specs/**/*.md"
    template: spec.md
    instruction: |
      BEFORE writing:
      - Read the proposal's Capabilities section — create one spec per capability
      - For MODIFIED capabilities, read the existing spec from openspec/specs/

      Write specs using SHALL/MUST for normative requirements.
      Every requirement MUST have at least one scenario with WHEN/THEN format.

      AFTER writing:
      - Cross-check: every capability in the proposal has a corresponding spec file
      - Verify no orphan specs exist (specs without a matching proposal capability)

Project-level (config.yamlcontext + rules): the project owner can define before/after constraints per artifact without touching the schema:

# config.yaml — rules act as project-specific before/after constraints
schema: spec-driven

context: |
  Tech stack: React 19, TypeScript, Tailwind, PostgreSQL.
  We deploy to AWS ECS. API follows REST conventions.
  All public endpoints require authentication.

rules:
  proposal:
    - "BEFORE starting: use Linear MCP to find the relevant ticket and include its ID"
    - "BEFORE starting: read and follow the instructions in docs/guides/proposal-checklist.md"
    - "BEFORE starting: read and follow the instructions in docs/guides/capability-naming.md"
    - "AFTER writing: update the Linear ticket description with a link to the proposal"
  specs:
    - "BEFORE writing: review the team's spec style guide at docs/spec-conventions.md"
    - "AFTER writing: verify all scenarios are compatible with our test framework format"
    - "AFTER writing: you MUST follow the steps in these files: rules/analyze.md, rules/test.md, rules/conventions.md"
    - "Follow the company naming convention: domain-entity-action (e.g., user-auth-login)"
  design:
    - "BEFORE writing: check ADRs in docs/adr/ for prior decisions on the same area"
    - "BEFORE writing: read and follow docs/guides/design-standards.md"
    - "AFTER writing: if new ADR-worthy decisions were made, flag them for post-archive"
    - "MUST include a rollback strategy for any database migration"
  tasks:
    - "AFTER writing: verify each task group maps to a single PR boundary"

This gives artifacts four layers of behavior already attached to them: template (structure), instruction (guidance from schema author), context (project background), and rules (project constraints). This PR doesn't add hooks to artifacts — they don't need them.

What this PR adds is hooks for operations (archive, verify, sync, apply, explore, etc.) — which are exactly the "commands/operations" you mention. These operations don't have an equivalent to instruction or rules today. The hooks map is keyed by operation lifecycle point (pre-archive, post-verify, etc.), which IS attaching behavior to the operation.

The two-source model (schema hooks + config hooks) mirrors the existing artifact pattern:

  • Schema hooks = workflow-inherent behavior (like instruction for artifacts)
  • Config hooks = project-specific behavior (like rules for artifacts)

Path forward

I see these as two complementary iterations:

  1. LLM instruction hooks (this PR) — for reasoning tasks at operation boundaries. Fits naturally into OpenSpec's existing architecture. Available now.
  2. Coding agent hooks integration (future) — for deterministic execution. Would integrate with Claude Code hooks, Cursor hooks, etc. Requires designing the bridge between OpenSpec operations and agent hook events.

Both are needed. Neither replaces the other. Happy to discuss further.

@lsmonki
Copy link
Author

lsmonki commented Feb 13, 2026

It's worth noting that coding agent hooks also support prompt and agent types — these use LLM reasoning but within a controlled, isolated context with structured output ({ok: true/false, reason: "..."}). For example, an agent-type hook could spawn a subagent with Read/Grep access to verify specs consistency before allowing an edit. I think this is closer to the direction you're pointing at.

However, coding agent hooks operate at the tool level (Bash, Edit, Write, Skill), not at the OpenSpec operation level (archive, verify, sync). The closest event is Stop, which fires when Claude finishes responding — but "finishes responding" doesn't mean the operation completed successfully. Claude could have stopped because it archived correctly, hit an error, got interrupted, or is waiting for user input. There's no way to know from the hook's perspective without parsing the conversation transcript, which is fragile. There's simply no hook event today that says "an /opsx:archive just completed successfully." One potential bridge could be the TaskCompleted hook — if an OpenSpec skill created a task on successful completion, a TaskCompleted hook could react to it deterministically with the task subject as context. But this is still speculative and would require intentional design. Bridging these two layers at the right granularity is a real challenge for a future iteration.

@lsmonki
Copy link
Author

lsmonki commented Feb 13, 2026

One more thing worth considering: not all coding agents have hooks.

Claude Code has a rich hook system (PreToolUse, PostToolUse, Stop, TaskCompleted, prompt/agent types, etc.), but that's specific to Claude Code. OpenSpec is designed to be agent-agnostic — it works with Claude Code, Codex, Cursor, Windsurf, Copilot, Aider, or any agent that can follow markdown instructions.

If we rely on coding agent hooks as the primary extensibility mechanism, we either tie OpenSpec to a specific agent — which breaks a core design principle — or we have to implement and maintain hook integrations for every agent, which is a massive surface area that grows with every new agent in the market.

LLM instruction hooks (openspec instructions --hook) work with any agent that can execute a shell command and follow the returned instructions. That's the common denominator across all agents. It's the only mechanism that's truly portable.

Coding agent hooks are a great additional layer for teams that use a specific agent and want deterministic guarantees. But they can't be the foundation — the foundation needs to work everywhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants