// types.ts - GSDState becomes extensible
export interface GSDState {
activeMilestone: ActiveRef | null;
activeSlice: ActiveRef | null;
activeTask: ActiveRef | null;
phase: Phase;
// ... existing fields ...
// Extension point - hooks store typed data here
extensions: {
[hookName: string]: unknown;
};
}export interface ReviewLoopExtension {
activeReview?: {
taskId: string;
cycle: number; // 1–5
status: 'pending_review' | 'fixing';
lastReviewPath: string; // Path to CODE-REVIEW.md
};
completedReviews: Array<{
taskId: string;
cycles: number;
passed: boolean;
}>;
}Hook state is serialized into the markdown frontmatter of the associated task, slice, or milestone.
---
id: T01
extensions:
review-loop:
activeReview:
taskId: T01
cycle: 2
status: fixing
lastReviewPath: tasks/T01-CODE-REVIEW.md
---dispatchNextUnit runs a middleware chain that allows hooks to override the default dispatch behavior.
// auto.ts
async function dispatchNextUnit(ctx, pi) {
const state = await deriveState(basePath);
// Run middleware chain - hooks can override decision
const middlewareCtx = await runMiddleware(state, { basePath, pi, ctx });
if (middlewareCtx.decision) {
// Hook took over - dispatch hook's unit
await dispatchUnit(middlewareCtx.decision);
} else {
// Default GSD logic
await defaultDispatchLogic(state);
}
}// review-loop-hook.ts
export const reviewLoopHook: GSDMiddleware = async (ctx, next) => {
const { state, basePath } = ctx;
// Only intercept when task just completed
if (state.phase !== 'executing' || !state.activeTask) {
await next(); // Continue to default execution
return;
}
// Check if we have active review state
const reviewState = state.extensions['review-loop'] as ReviewLoopExtension;
if (reviewState?.activeReview?.taskId === state.activeTask.id) {
// Resume review cycle
const { cycle, status } = reviewState.activeReview;
if (status === 'pending_review') {
ctx.decision = {
unitType: 'review-task',
prompt: buildReviewPrompt(state, cycle),
};
return;
}
if (status === 'fixing') {
ctx.decision = {
unitType: 'fix-task',
prompt: buildFixPrompt(state, cycle),
};
return;
}
}
// Check if task needs first review
if (shouldReviewTask(state)) {
ctx.state = {
...state,
extensions: {
...state.extensions,
'review-loop': {
activeReview: {
taskId: state.activeTask.id,
cycle: 1,
status: 'pending_review',
lastReviewPath: '',
},
completedReviews: reviewState?.completedReviews ?? [],
} satisfies ReviewLoopExtension,
},
};
ctx.decision = {
unitType: 'review-task',
prompt: buildReviewPrompt(state, 1),
};
return;
}
// No review needed - proceed with normal execution
await next();
};After a review-task completes, handleAgentEnd in auto.ts triggers middleware again.
async function afterDispatch(result, ctx) {
if (result.unitType === 'review-task') {
const reviewFile = parseReviewOutput(result);
const issues = extractIssues(reviewFile);
if (issues.length === 0) {
// Review passed - clear active review
ctx.state.extensions['review-loop'].activeReview = undefined;
// Allow task execution to proceed
await next();
} else {
// Issues found - increment cycle and schedule fix
const currentCycle =
ctx.state.extensions['review-loop'].activeReview!.cycle;
ctx.state.extensions['review-loop'].activeReview = {
...ctx.state.extensions['review-loop'].activeReview!,
cycle: currentCycle + 1,
status: 'fixing',
lastReviewPath: reviewFile,
};
// Do not call next() - hook schedules fix-task next iteration
}
}
}~/.gsd/agent/extensions/my-review-loop/
├── index.ts
└── gsd-hooks.ts
index.ts: standard Pi extension entry pointgsd-hooks.ts: optional file exporting hook definitions
// index.ts
import { registerHook } from '../gsd/hooks.js';
export default function (pi: ExtensionAPI) {
registerHook('review-loop', reviewLoopHook);
}preferences.md
gsd_hooks:
enabled:
- name: review-loop
source: my-review-loopIf multiple hooks attempt to override dispatch:
- Option A: First registered hook wins
- Option B: Explicit priority ordering
If multiple hooks modify ctx.state.extensions:
- Option A: Shallow merge
- Option B: Hooks must manage their own namespace carefully
Hooks could augment the GSDState extension types.
declare module '../gsd/types.js' {
interface GSDStateExtensions {
'review-loop': ReviewLoopExtension;
}
}Should the cycle counter:
- auto-increment at the framework level, or
- be fully managed by hooks?
- Hooks operate as middleware interceptors in the
/gsd nextdispatch pipeline. - Hook-specific state lives in
GSDState.extensions. - State persists through the existing markdown frontmatter system.
- Hooks can override the next execution unit by setting
ctx.decision. - Review loops become a pure extension, without modifying the core GSD execution engine.