Summary
Extend the BeforeAgentStartEvent payload in @gsd/pi-coding-agent to include two new optional fields: unitType: string (matches the value emitted by emitUnitStart in hook-emitter.ts) and phase: string (matches the Phase enum defined in src/resources/extensions/gsd/types.ts). Both fields are emitted by the auto-dispatch loop in unit-phase.ts (call site setCurrentPhase(unitType, ...) at line 472) but are not currently exposed to extension listeners.
Problem to solve
The gsd-pi-discussion-arena extension (HEAD a371143, v0.8.0) cannot implement true "forced Tier F" integration with gsd-pi runtime because BeforeAgentStartEvent does not carry the dispatch context. Currently the extension must fall back to:
- Polling
getCurrentPhase() from gsd-phase-state.ts (works for GSDExtensionAPI wrappers only).
- Inferring phase from heuristic correlation (e.g.
selectedModelId correlation accuracy ~80%, per spec v2 §4 W1.1 workaround).
- Maintaining an internal
WeakMap<ExtensionAPI, {value: string}> (currentUnitTypeByApi) populated by intercepting unit_start events that gsd-pi never actually emits (verified via grep -rn 'emitUnitStart(' /home/opengsd/repos/open-gsd_gsd-pi/src → 0 call sites, only the definition in src/resources/extensions/gsd/hook-emitter.ts:169).
Without these fields in the event payload, the extension cannot synchronously know which unitType is being dispatched at the moment before_agent_start fires. The user's intent (declared in BeforeAgentStartEventResult via systemPrompt injection) cannot be reliably produced.
This blocks DoD §7.2 ("DoD-capability") of the spec v2 specification which requires W1.1 to be merged in open-gsd/gsd-pi.
Proposed solution
Add two optional fields to the BeforeAgentStartEvent interface in packages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts (current line 691):
export interface BeforeAgentStartEvent {
type: "before_agent_start";
prompt: string;
images?: ImageContent[];
systemPrompt: string;
systemPromptOptions: BuildSystemPromptOptions;
// NEW (additive, backward-compatible):
/** Unit type being dispatched, if known. Matches `unitType` from `UnitStartEvent`. */
unitType?: string;
/** Current GSD phase, if active. One of the 18 values in `Phase` enum. */
phase?: string;
}
The emitter (in packages/pi-agent-core/src/harness/agent-harness.ts line 545, which currently emits BeforeAgentStartEvent with prompt, images, systemPrompt, resources) must be updated to read the current phase from gsd-phase-state.getCurrentPhase() and the current unit-type from unit-phase.ts state, and pass them to the event payload.
This change is:
- Additive (new optional fields, no breaking change).
- Backward-compatible (existing listeners ignore unknown fields).
- Forward-compatible with the existing
gsd-extension-api.ts wrapper which already exposes getPhase() separately.
Alternatives considered
-
Reuse existing systemPromptOptions.BuildSystemPromptOptions payload — rejected because systemPromptOptions is intended for resource discovery metadata, not dispatch context. Coupling unit-type to system-prompt-resource options would create semantic confusion.
-
Add a new event unit_dispatching that fires BEFORE before_agent_start — rejected because it doubles the event surface area and forces every extension to listen to two events. The data belongs in the existing payload.
-
Continue using getCurrentPhase() polling via GSDExtensionAPI — rejected because this requires the wrapper to be installed (not all extensions have it) and introduces race conditions between getCurrentPhase() read and before_agent_start dispatch (the phase can change between read and dispatch in concurrent sessions).
-
Wait for the existing emitUnitStart to actually emit unit_start events — rejected because this requires deeper architectural changes to unit-phase.ts and reviewer feedback (#3338) suggests the maintainer prefers gsd-phase-state as the canonical state, not event emission.
Use cases
-
gsd-pi-discussion-arena forced Tier F integration (primary use case): the extension listens to before_agent_start, reads event.unitType, looks up the corresponding discussion-arena hook module, and injects a systemPrompt instruction to use discussion_arena tool before the agent starts reasoning. This works on ALL gsd-pi runtime versions, not only those with GSDExtensionAPI wrapper.
-
gsd-pi UI status widget (secondary): the GsdProgressState widget (defined in extension-upstream-types.ts:61) could display the active phase and unitType more accurately without cross-extension polling.
-
Cross-extension coordination (tertiary): other community extensions (e.g. observability, cost-tracking) can correlate their work with the dispatch phase without depending on gsd-phase-state module-level singletons.
Impact
- Breaking change: NO (additive fields, optional).
- API surface change: 2 new optional fields on
BeforeAgentStartEvent.
- Performance impact: negligible (2 extra string reads per event emission).
- Migration required for existing extensions: NO (existing listeners ignore unknown fields).
- DoD closed: §7.2 (DoD-capability) becomes fully closed.
- Severity: medium (extension-side workaround exists via
GSDExtensionAPI.getPhase() but requires the wrapper to be installed and propagated; raw pi-coding-agent consumers cannot benefit).
Evidence or prior art
The same payload fields are already used internally by gsd-pi in unit-phase.ts:472:
setCurrentPhase(unitType, {
basePath: s.basePath,
traceId: ic.flowId,
turnId: `iter-${ic.iteration}`,
causedBy: "unit-start",
});
And in gsd-extension-api.ts:54:
export interface GSDExtensionAPI extends ExtensionAPI {
getPhase(): Phase | null;
getActiveUnit(): GSDActiveUnit | null;
}
The GSDExtensionAPI.getActiveUnit() method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.
Additional information
The GSDExtensionAPI.getActiveUnit() method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.
Additional information
- Affected file in gsd-pi:
packages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts:691 (interface declaration) and packages/pi-agent-core/src/harness/agent-harness.ts:545 (event emission).
- Affected extension file (consumer side, already prepared):
src/gsd-api-adapter.ts:73 getRuntimePhase() in gsd-pi-discussion-arena repo. The extension is ready to consume event.unitType if exposed.
- Related discussion in
docs/architecture/runtime-fallbacks.md (W1.1 section) inside the extension repo.
- Related spec v2 §7.2 DoD: this PR is the missing piece to close it.
- Estimated upstream review time: 1-2 working days (additive change, well-documented, low risk).
- Backward compatibility test plan: existing extensions that listen to
before_agent_start must continue to work unchanged. Verify by running tests/integration/extensions-runner.test.ts in open-gsd/gsd-pi
Summary
Extend the
BeforeAgentStartEventpayload in@gsd/pi-coding-agentto include two new optional fields:unitType: string(matches the value emitted byemitUnitStartinhook-emitter.ts) andphase: string(matches thePhaseenum defined insrc/resources/extensions/gsd/types.ts). Both fields are emitted by the auto-dispatch loop inunit-phase.ts(call sitesetCurrentPhase(unitType, ...)at line 472) but are not currently exposed to extension listeners.Problem to solve
The
gsd-pi-discussion-arenaextension (HEADa371143, v0.8.0) cannot implement true "forced Tier F" integration withgsd-piruntime becauseBeforeAgentStartEventdoes not carry the dispatch context. Currently the extension must fall back to:getCurrentPhase()fromgsd-phase-state.ts(works forGSDExtensionAPIwrappers only).selectedModelIdcorrelation accuracy ~80%, per spec v2 §4 W1.1 workaround).WeakMap<ExtensionAPI, {value: string}>(currentUnitTypeByApi) populated by interceptingunit_startevents that gsd-pi never actually emits (verified viagrep -rn 'emitUnitStart(' /home/opengsd/repos/open-gsd_gsd-pi/src→ 0 call sites, only the definition insrc/resources/extensions/gsd/hook-emitter.ts:169).Without these fields in the event payload, the extension cannot synchronously know which
unitTypeis being dispatched at the momentbefore_agent_startfires. The user's intent (declared inBeforeAgentStartEventResultviasystemPromptinjection) cannot be reliably produced.This blocks DoD §7.2 ("DoD-capability") of the spec v2 specification which requires
W1.1to be merged inopen-gsd/gsd-pi.Proposed solution
Add two optional fields to the
BeforeAgentStartEventinterface inpackages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts(current line 691):The emitter (in
packages/pi-agent-core/src/harness/agent-harness.tsline 545, which currently emitsBeforeAgentStartEventwithprompt, images, systemPrompt, resources) must be updated to read the current phase fromgsd-phase-state.getCurrentPhase()and the current unit-type fromunit-phase.tsstate, and pass them to the event payload.This change is:
gsd-extension-api.tswrapper which already exposesgetPhase()separately.Alternatives considered
Reuse existing
systemPromptOptions.BuildSystemPromptOptionspayload — rejected becausesystemPromptOptionsis intended for resource discovery metadata, not dispatch context. Coupling unit-type to system-prompt-resource options would create semantic confusion.Add a new event
unit_dispatchingthat fires BEFOREbefore_agent_start— rejected because it doubles the event surface area and forces every extension to listen to two events. The data belongs in the existing payload.Continue using
getCurrentPhase()polling viaGSDExtensionAPI— rejected because this requires the wrapper to be installed (not all extensions have it) and introduces race conditions betweengetCurrentPhase()read andbefore_agent_startdispatch (the phase can change between read and dispatch in concurrent sessions).Wait for the existing
emitUnitStartto actually emit unit_start events — rejected because this requires deeper architectural changes tounit-phase.tsand reviewer feedback (#3338) suggests the maintainer prefersgsd-phase-stateas the canonical state, not event emission.Use cases
gsd-pi-discussion-arenaforced Tier F integration (primary use case): the extension listens tobefore_agent_start, readsevent.unitType, looks up the correspondingdiscussion-arenahook module, and injects asystemPromptinstruction to usediscussion_arenatool before the agent starts reasoning. This works on ALL gsd-pi runtime versions, not only those withGSDExtensionAPIwrapper.gsd-piUI status widget (secondary): theGsdProgressStatewidget (defined inextension-upstream-types.ts:61) could display the activephaseandunitTypemore accurately without cross-extension polling.Cross-extension coordination (tertiary): other community extensions (e.g. observability, cost-tracking) can correlate their work with the dispatch phase without depending on
gsd-phase-statemodule-level singletons.Impact
BeforeAgentStartEvent.GSDExtensionAPI.getPhase()but requires the wrapper to be installed and propagated; rawpi-coding-agentconsumers cannot benefit).Evidence or prior art
The same payload fields are already used internally by gsd-pi in
unit-phase.ts:472:And in
gsd-extension-api.ts:54:The
GSDExtensionAPI.getActiveUnit()method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.Additional information
The
GSDExtensionAPI.getActiveUnit()method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.Additional information
packages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts:691(interface declaration) andpackages/pi-agent-core/src/harness/agent-harness.ts:545(event emission).src/gsd-api-adapter.ts:73getRuntimePhase()ingsd-pi-discussion-arenarepo. The extension is ready to consumeevent.unitTypeif exposed.docs/architecture/runtime-fallbacks.md(W1.1 section) inside the extension repo.before_agent_startmust continue to work unchanged. Verify by runningtests/integration/extensions-runner.test.tsinopen-gsd/gsd-pi