Skip to content

feat(loop): carry typed phase context through lifecycle hooks #4030

Description

@davdittrich

Pre-submission checklist

  • I have searched existing issues and discussions — this has not been proposed and declined before
  • I have read CONTRIBUTING.md and understand that I must wait for approved-feature before writing any code
  • I have read the existing GSD commands and workflows and confirmed this feature does not duplicate existing behavior
  • This feature solves a problem for solo developers using AI coding tools, not a personal preference or workflow I happen to like

Feature name

Typed phase context for lifecycle hooks

Type of addition

Other — an additive typed field on the existing loop-extension-point contract and its workflow dispatch calls.

The solo developer problem

When a solo developer plans or reviews Phase 2 while Phase 1 remains the project's current executing phase, a phase-scoped capability receives only the lifecycle point and project cwd. It cannot identify the task-local phase. Extensions then either infer from STATE.md and operate on Phase 1, or invent artifact-order/mtime heuristics.

This is observed, not hypothetical: davdittrich/gsd-beads#5 reproduces plan:pre refreshing Phase 1's Beads recall while Phase 2 is being planned. That can hide relevant tickets and can send phase-scoped issue mutation or reconciliation to the wrong phase.

The existing resolver returns { point, activeHooks, rendered } and the workflow calls loop render-hooks <point> --raw without phase context. src/loop-resolver.cts at b811ea1, lines 478-619 and plan-phase.md, lines 353-357 show the missing seam.

Claude Code's documented PostToolUse input provides generic session/cwd fields plus the completed tool's input and response; it has no GSD phase field. A plugin cannot recover an authoritative task-local phase from that host payload alone: https://code.claude.com/docs/en/hooks#posttooluse-input

What this feature adds

Add an optional, typed phase context to phase-scoped loop render-hooks resolution and dispatch:

gsd_run loop render-hooks plan:pre \
  --phase "$PHASE" \
  --phase-dir "$PHASE_DIR" \
  --raw

The JSON envelope gains an additive field:

{
  "point": "plan:pre",
  "context": {
    "phase": "02",
    "phaseDir": "/project/.planning/phases/02-forecasting"
  },
  "activeHooks": [],
  "rendered": ""
}

The names above are a concrete proposal, not a demand for that exact spelling. The contract is:

  1. Context is supplied by the invoking workflow, which already owns PHASE/PHASE_DIR.
  2. Core validates the phase token and confines the resolved directory to the active project's .planning/phases/<direct-child> before publishing it.
  3. Generic step/contribution dispatch makes the resolved typed context available to the invoked capability handler. Runtime adapters may project it into their native hook/tool payload, but extensions never scrape prose, STATE.md, directory order, or mtimes.
  4. Omitted context preserves the current envelope and behavior for non-phase-scoped callers.
  5. The field represents invocation context, not project lifecycle status; it does not update STATE.md.

Full scope of changes

Files/systems expected to change (exact generated-file handling should follow repository conventions):

  • src/loop-resolver.cts — define/validate the optional phase-context value and add it to the resolved/output envelope.
  • gsd-core/bin/gsd-tools.cjs — parse the additive typed arguments and pass them to cmdLoopRenderHooks.
  • gsd-core/workflows/plan-phase.md — pass the phase being planned at plan:pre and plan:post call sites.
  • gsd-core/workflows/execute-phase.md — pass the phase being executed at execute:wave:pre and execute:wave:post call sites.
  • gsd-core/workflows/verify-work.md and other phase-scoped verify:* consumers — pass the verified phase.
  • gsd-core/references/loop-hook-dispatch.md — specify how generic step/contribution handlers receive typed invocation context.
  • tests/loop-render-hooks.test.cjs — pure resolver, CLI, validation, omission/backward-compatibility, and hostile-path cases.
  • Existing workflow/loop-hook end-to-end suites — prove the selected phase reaches a capability handler at plan, wave, and verify points.
  • Runtime adapter fixtures where the generic context is projected to a native hook surface.
  • User-facing capability/extension documentation and inventory/generated attribution metadata required by repository checks.

Systems affected: loop resolver JSON schema, phase-scoped workflow call sites, generic capability dispatch, runtime hook projections, extension-author documentation. No new command, dependency, planning artifact, or persistent state field.

User stories

  1. As a solo developer planning Phase 2 while Phase 1 is still executing, I want phase-scoped extensions to receive Phase 2 explicitly so they cannot read or mutate Phase 1 by mistake.
  2. As a capability author, I want one validated core-owned invocation-context field so my extension works across runtimes without scraping workflow prose or inferring from mutable project state.
  3. As a runtime adapter maintainer, I want phase context in the generic loop contract so I can project it through the host's native hook shape without capability-specific code.

Acceptance criteria

  • Phase-scoped loop render-hooks accepts an explicit phase token and phase directory and returns them in a typed context field.
  • The phase token follows GSD's existing phase-number contract, including decimal phases.
  • phaseDir resolves to one direct child beneath the active project's .planning/phases; traversal, symlink escape, nonexistent paths, and foreign-project paths are rejected before hook dispatch.
  • At every phase-scoped plan, execute-wave, and verify call site, the workflow-supplied phase reaches a synthetic third-party step handler unchanged.
  • With STATE.current_phase: 01 and invocation context for Phase 2, the synthetic handler receives only Phase 2; STATE.md remains byte-identical.
  • Omitting context preserves the current { point, activeHooks, rendered } behavior and does not invent a fallback context field.
  • Non-phase-scoped loop points remain unchanged.
  • Runtime adapter tests prove the context survives at least Claude Code and Codex projection; the core contract is runtime-neutral.
  • Existing loop resolver, host contract, workflow, runtime, and capability suites pass.
  • Extension-author documentation states that invocation context outranks recovery/project-status artifacts for task-local routing.

Which area does this primarily affect?

Multiple areas — core workflow, runtime integration, and the capability/loop extension-point contract.

Applicable runtimes

  • All runtimes

Breaking changes assessment

None intended. The CLI arguments and JSON context member are additive and optional. Existing callers that omit them keep current output and behavior. Existing capability handlers continue to run; handlers may adopt context incrementally. No STATE, ROADMAP, plan, or capability-manifest schema is changed.

Maintenance burden

  • No external dependency.
  • One small typed context structure, argument parser branch, and validation path.
  • Phase-scoped workflow call sites must keep passing the phase they already resolve.
  • Runtime adapters must preserve the generic field when projecting loop dispatch; the field must be included in future parity tests.
  • If canonical phase-token/path rules change, this validator must reuse that single source rather than fork it.
  • Documentation and end-to-end fixtures add ongoing but bounded maintenance.

The maintenance cost is lower than each capability independently implementing state inference or runtime-specific transcript parsing.

Alternatives considered

  1. Update STATE.current_phase before planning another phase. Rejected: it falsifies project lifecycle status to steer one invocation and breaks concurrent work.
  2. Infer the phase from newest artifacts, directory order, PLAN presence, or mtimes. Rejected: nondeterministic under concurrent work, preserved artifacts, clock skew, and partial writes.
  3. Capability-specific sidecars/environment variables. Rejected: runtime-specific, collision-prone, stale after crashes, and duplicates plumbing in every extension.
  4. Typed core invocation context. Preferred. It is deterministic, validates once at the trust boundary, and is reusable by every capability/runtime.

Decision ranking:

  1. Performance — one already-known pair of scalar fields; no filesystem scan or transcript parse.
  2. Simplicity/LOC — one generic seam replaces per-capability inference and sidecars.
  3. Ecosystem support — runtime-neutral core field can be projected through each host adapter.
  4. Maintenance — additive schema with centralized phase validation; no new dependency or persistent state.

Prior art and references

Additional context

This proposal deliberately excludes Beads-specific behavior. Beads exposed the missing generic contract, but the field belongs to core because any phase-scoped capability can otherwise target stale project state.

No gsd-core implementation or PR will be started before approved-feature.

Metadata

Metadata

Assignees

No one assigned

    Labels

    approved-featureFeature approved — contributor may begin coding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions