@@ -92,6 +92,8 @@ const (
9292 MarkerPlanningCheckPlan = deliverycontrol .TransitionID ("planning.check_plan" )
9393 MarkerPlanningActivate = deliverycontrol .TransitionID ("planning.activate" )
9494 MarkerPlanningWorkspace = deliverycontrol .TransitionID ("planning.workspace_cut" )
95+ MarkerPlanningWrite = deliverycontrol .TransitionID ("planning.planning_write" )
96+ MarkerPlanningApproval = deliverycontrol .TransitionID ("planning.record_approval" )
9597 MarkerRecoveryDoctor = deliverycontrol .TransitionID ("recovery.doctor" )
9698 MarkerRecoveryDiscard = deliverycontrol .TransitionID ("recovery.discard_delivery" )
9799 MarkerRecoveryRepair = deliverycontrol .TransitionID ("recovery.repair_state" )
@@ -130,6 +132,12 @@ type FlowNext struct {
130132 // completion state and prescribes no command — coding work is never a modeled
131133 // transition, only an ordered pointer.
132134 SubAction * FlowTask `json:"sub_action,omitempty"`
135+ // Alternatives are the other admissible next commands from this position —
136+ // the computed solution set minus the single Prescribed primary. They let a
137+ // caller PICK a legal move instead of deriving one from the law's prose.
138+ // Advisory, never a second primary: the rendering keeps exactly one Run line.
139+ // control-law: solution-set-derives-from-guard-declarations
140+ Alternatives []PrescribedCommand `json:"alternatives,omitempty"`
133141}
134142
135143// PrescribedCommand is the exact next command that makes the oracle's lowest-cost
@@ -188,9 +196,30 @@ func prescribeCommand(repo, feature string, status NextStatus, transition delive
188196 preview := filepath .Join (WorkspaceFor (repo ).GeneratedRoot (), "features" , feature , "pr.md" )
189197 cmd .Args = append (repoArgs , "--preview" , preview , "--action" , "open" )
190198 cmd .RequiresHumanInput = []string {"--preview-fingerprint" }
199+ case deliverycontrol .TransitionID ("delivery.record_change" ):
200+ if feature == "" {
201+ return nil , false
202+ }
203+ // Rework: the correction facts (what changed, where it was observed, and
204+ // its classification) are human knowledge; owe them, never fabricate them.
205+ cmd .Args = append (repoArgs , "--feature" , feature )
206+ if status .ActiveSlice != "" {
207+ cmd .Args = append (cmd .Args , "--slice" , status .ActiveSlice )
208+ }
209+ cmd .RequiresHumanInput = []string {"--message" , "--source-stage" , "--classification" }
210+ case deliverycontrol .TransitionID ("delivery.undo" ):
211+ // The mutation id names WHICH receipt to reverse — a human decision.
212+ cmd .Args = repoArgs
213+ cmd .RequiresHumanInput = []string {"--mutation" }
214+ case deliverycontrol .TransitionID ("delivery.discard_delivery" ):
215+ if feature == "" {
216+ return nil , false
217+ }
218+ cmd .Args = append (repoArgs , "--feature" , feature )
191219 default :
192- // Recovery/observe/rework transitions are not prescribed as a forward move;
193- // emit nothing rather than a command whose arguments we cannot derive.
220+ // Recovery/observe transitions outside the set above are not prescribed as
221+ // a forward move; emit nothing rather than a command whose arguments we
222+ // cannot derive.
194223 return nil , false
195224 }
196225 cmd .AutoDerivable = len (cmd .RequiresHumanInput ) == 0
@@ -209,12 +238,18 @@ func prescribeCommand(repo, feature string, status NextStatus, transition delive
209238// state"), not an execution grant: markers are off the auto-drive allowlist
210239// and have no executor, so the driver always prescribes-and-stops on them.
211240// control-law: prescriptive-closure-every-stage-names-a-runnable-command
241+ // planningFeatureDir is the single joined form of a feature's planning
242+ // directory used by the prescription layer and the solution-set enumerator.
243+ func planningFeatureDir (repo , feature string ) string {
244+ return filepath .Join (repo , ".product-loop" , "features" , feature )
245+ }
246+
212247func prescribePlanning (repo string , status NextStatus ) (* PrescribedCommand , string ) {
213248 var repoArgs []string
214249 if repo != "" && repo != "." {
215250 repoArgs = []string {"--repo" , repo }
216251 }
217- featureDir := filepath . Join (repo , ".product-loop" , "features" , status .Feature )
252+ featureDir := planningFeatureDir (repo , status .Feature )
218253 finish := func (cmd * PrescribedCommand , followUp string ) (* PrescribedCommand , string ) {
219254 cmd .AutoDerivable = len (cmd .RequiresHumanInput ) == 0
220255 return cmd , followUp
@@ -233,32 +268,19 @@ func prescribePlanning(repo string, status NextStatus) (*PrescribedCommand, stri
233268 }, "Then run auto-plan with the validated SOURCE_PLAN path; author every feature artifact through `boatstack-helper planning-write` (document on stdin)." )
234269 case "DRAFT_PLAN" :
235270 return finish (& PrescribedCommand {
236- Verb : "check-plan" ,
237- Args : []string {"--plan" , filepath .Join (featureDir , "plan.md" )},
271+ Verb : "check-plan" ,
272+ Args : []string {"--plan" , filepath .Join (featureDir , "plan.md" )},
238273 Transition : MarkerPlanningCheckPlan ,
239274 }, "After the check passes, present the plan for approval and record it with `record-approval` using the exact PLAN_FINGERPRINT it printed." )
240275 case "APPROVED" , "POLICY_READY" :
241276 // ResolveNext already ordered the move: a fresh workspace cut when one is
242277 // needed, otherwise activation. "build" is an operation name, not a verb;
243278 // activate-plan is the build operation's first concrete command.
244279 if status .NextOperation == "workspace-cut" {
245- return finish (& PrescribedCommand {
246- Verb : "workspace-cut" ,
247- Args : append (repoArgs , "--feature" , status .Feature ),
248- Transition : MarkerPlanningWorkspace ,
249- }, "Then activate the plan from the fresh workspace with `activate-plan`." )
280+ return finish (buildWorkspaceCut (repoArgs , status .Feature ),
281+ "Then activate the plan from the fresh workspace with `activate-plan`." )
250282 }
251- args := []string {
252- "--plan" , filepath .Join (featureDir , "plan.md" ),
253- "--out-dir" , filepath .Join (featureDir , "compiled" ),
254- "--output" , filepath .Join (featureDir , "plan.lock.json" ),
255- }
256- if status .ObservedStage == "APPROVED" {
257- args = append (args , "--approval" , filepath .Join (featureDir , "approval.md" ))
258- }
259- return finish (& PrescribedCommand {
260- Verb : "activate-plan" , Args : args , Transition : MarkerPlanningActivate ,
261- }, "" )
283+ return finish (buildActivatePlan (featureDir , status .ObservedStage ), "" )
262284 case "INVALID_STATE" :
263285 switch status .NextOperation {
264286 case "doctor" :
@@ -296,6 +318,30 @@ func prescribePlanning(repo string, status NextStatus) (*PrescribedCommand, stri
296318 }
297319}
298320
321+ // buildWorkspaceCut and buildActivatePlan are the single assembly points for
322+ // their commands, shared by prescribePlanning (the primary) and the solution-set
323+ // enumerator (the alternatives) so the two can never drift apart.
324+ // control-law: solution-set-derives-from-guard-declarations
325+ func buildWorkspaceCut (repoArgs []string , feature string ) * PrescribedCommand {
326+ return & PrescribedCommand {
327+ Verb : "workspace-cut" ,
328+ Args : append (append ([]string {}, repoArgs ... ), "--feature" , feature ),
329+ Transition : MarkerPlanningWorkspace ,
330+ }
331+ }
332+
333+ func buildActivatePlan (featureDir , stage string ) * PrescribedCommand {
334+ args := []string {
335+ "--plan" , filepath .Join (featureDir , "plan.md" ),
336+ "--out-dir" , filepath .Join (featureDir , "compiled" ),
337+ "--output" , filepath .Join (featureDir , "plan.lock.json" ),
338+ }
339+ if stage == "APPROVED" {
340+ args = append (args , "--approval" , filepath .Join (featureDir , "approval.md" ))
341+ }
342+ return & PrescribedCommand {Verb : "activate-plan" , Args : args , Transition : MarkerPlanningActivate }
343+ }
344+
299345// NextControl composes the authoritative read-only recommendation (ResolveNext)
300346// with the deterministic oracle to advise the lowest-cost next move toward a
301347// published delivery. It performs no mutation and is safe to call at any time.
@@ -330,6 +376,7 @@ func nextControlFromStatus(repo string, status NextStatus) (FlowNext, error) {
330376 out .Prescribed = cmd
331377 out .FollowUp = followUp
332378 }
379+ out .Alternatives = alternativesFor (repo , status , out )
333380 return out , nil
334381 }
335382 out .State = state
@@ -354,6 +401,7 @@ func nextControlFromStatus(repo string, status NextStatus) (FlowNext, error) {
354401 }
355402 }
356403 }
404+ out .Alternatives = alternativesFor (repo , status , out )
357405 return out , nil
358406}
359407
@@ -389,9 +437,32 @@ func FormatFlowNext(next FlowNext) string {
389437 } else {
390438 fmt .Fprintf (& b , "Flow state: unresolved (no oracle advisory; follow the recommended operation above)\n " )
391439 }
440+ writeAlternatives (& b , next .Alternatives )
392441 return b .String ()
393442}
394443
444+ // writeAlternatives renders the solution set's other legal moves as ONE line of
445+ // verbs with a short purpose gloss — never a second Run line, so the response
446+ // contract's single primary action holds.
447+ // control-law: solution-set-derives-from-guard-declarations
448+ func writeAlternatives (b * strings.Builder , alternatives []PrescribedCommand ) {
449+ if len (alternatives ) == 0 {
450+ return
451+ }
452+ shown := alternatives
453+ if len (shown ) > solutionSetTextCap {
454+ shown = shown [:solutionSetTextCap ]
455+ }
456+ labels := make ([]string , 0 , len (shown ))
457+ for _ , alt := range shown {
458+ labels = append (labels , alt .Verb + " (" + solutionGloss (alt .Transition )+ ")" )
459+ }
460+ fmt .Fprintf (b , "Also legal from here: %s\n " , strings .Join (labels , ", " ))
461+ if len (alternatives ) > len (shown ) {
462+ fmt .Fprintf (b , " (%d more in `flow next --json` under alternatives)\n " , len (alternatives )- len (shown ))
463+ }
464+ }
465+
395466// writePrescribed renders the Run line and its owed-input annotation for a
396467// prescribed command, shared by the oracle and pre-activation branches.
397468func writePrescribed (b * strings.Builder , p * PrescribedCommand ) {
0 commit comments