@@ -83,6 +83,21 @@ type buildOutput struct {
8383 RunInBackground * bool `json:"run_in_background,omitempty"`
8484}
8585
86+ // buildAdvanceOutput is the stdout JSON envelope for `--advance` mode: a pointer
87+ // message for the reuse-advance handle, not a spawn envelope. It carries no
88+ // subagent_type/name/team_name/run_in_background — nothing is spawned, so those
89+ // spawn-only fields are absent from the type entirely (not merely omitempty).
90+ // model stays so the FO's reuse-condition-4 comparator can read
91+ // next_stage.effective_model from this output instead of a separate README read.
92+ type buildAdvanceOutput struct {
93+ SchemaVersion int `json:"schema_version"`
94+ Description string `json:"description"`
95+ FetchCommands []string `json:"fetch_commands"`
96+ DispatchFile string `json:"dispatch_file_path"`
97+ Prompt string `json:"prompt"`
98+ Model * string `json:"model"`
99+ }
100+
86101// buildError prints `error: {msg}` to stderr and returns code (1 by default).
87102func buildError (stderr io.Writer , code int , format string , a ... any ) int {
88103 fmt .Fprintf (stderr , "error: " + format + "\n " , a ... )
@@ -168,6 +183,7 @@ func fieldsFromBuildFlags(opts buildOptions, stderr io.Writer) (map[string]json.
168183 "stage" : rawJSON (opts .Stage ),
169184 "checklist" : rawJSON (checklist ),
170185 "bare_mode" : rawJSON (opts .BareMode ),
186+ "advance" : rawJSON (opts .Advance ),
171187 }
172188 if opts .TeamName != "" {
173189 fields ["team_name" ] = rawJSON (opts .TeamName )
@@ -281,6 +297,13 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
281297 }
282298 bareMode := optBool (fields , "bare_mode" )
283299 isFeedbackReflow := optBool (fields , "is_feedback_reflow" )
300+ advance := optBool (fields , "advance" )
301+
302+ // Rule 13: --advance excludes bare mode. A reuse advance presupposes an
303+ // addressable live worker to message; bare mode dispatches nothing addressable.
304+ if advance && bareMode {
305+ return buildError (stderr , 2 , "--advance is incompatible with bare_mode (a reuse advance presupposes an addressable worker; bare mode has none)" )
306+ }
284307
285308 // Merged mode is the Claude .178+ team shape: a non-bare claude dispatch with
286309 // no team_name. On .178+ TeamCreate/TeamDelete are gone, so team membership is
@@ -526,11 +549,16 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
526549 // --- Prompt assembly ---
527550 var parts []string
528551
529- // 0. Operating-contract first-action directive.
530- parts = append (parts , firstActionBlock (host ))
531-
532- // 1. Header.
533- parts = append (parts , fmt .Sprintf ("You are working on: %s\n \n Stage: %s\n " , entityTitle , stage ))
552+ // 0-1. Operating-contract first-action directive + header. Advance mode
553+ // skips both: the reused worker already holds its operating contract from
554+ // initial dispatch, so the file opens with an advance header instead.
555+ if advance {
556+ parts = append (parts , fmt .Sprintf (
557+ "## Advancing to next stage: %s\n \n You are continuing work on: %s\n " , stage , entityTitle ))
558+ } else {
559+ parts = append (parts , firstActionBlock (host ))
560+ parts = append (parts , fmt .Sprintf ("You are working on: %s\n \n Stage: %s\n " , entityTitle , stage ))
561+ }
534562
535563 // 2. Stage definition — replaced by the show-stage-def fetch line. The native
536564 // fetch line targets `spacedock dispatch show-stage-def` so the dispatch path
@@ -573,14 +601,27 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
573601
574602 // 4. Entity-read instruction. Under split root the entity lives in the state
575603 // checkout; a non-split worktree stage rewrites the path into the worktree.
604+ // Advance mode replaces the "Read ... for the spec" wording with a
605+ // continue-on-entity instruction — the worker already knows the entity, it is
606+ // resuming work on it — using the identical path resolution as fresh dispatch.
576607 if worktreePath != "" && ! splitRoot {
577608 entityRel := pyRelpath (entityPath , gitRoot )
578609 worktreeEntityPath = status .PyJoin (worktreePath , entityRel )
579- parts = append (parts , fmt .Sprintf (
580- "Read the entity file at %s for the full spec. It contains:\n " , worktreeEntityPath ))
610+ if advance {
611+ parts = append (parts , fmt .Sprintf (
612+ "Continue working on the entity at %s.\n " , worktreeEntityPath ))
613+ } else {
614+ parts = append (parts , fmt .Sprintf (
615+ "Read the entity file at %s for the full spec. It contains:\n " , worktreeEntityPath ))
616+ }
581617 } else {
582- parts = append (parts , fmt .Sprintf (
583- "Read the entity file at %s for the current spec.\n " , entityPath ))
618+ if advance {
619+ parts = append (parts , fmt .Sprintf (
620+ "Continue working on the entity at %s.\n " , entityPath ))
621+ } else {
622+ parts = append (parts , fmt .Sprintf (
623+ "Read the entity file at %s for the current spec.\n " , entityPath ))
624+ }
584625 }
585626
586627 // 6. Feedback context (conditional).
@@ -672,6 +713,12 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
672713 dispatchFileName = sessionToken + "-" + derivedName
673714 }
674715 }
716+ if advance {
717+ // -advance suffix so an advance file can never alias a fresh-dispatch file
718+ // for the same slug+stage: a fresh dispatch after a failed advance would
719+ // otherwise collide with the stale advance body at the bare derivedName path.
720+ dispatchFileName += "-advance"
721+ }
675722 if len (dispatchFileName ) > dispatchFileNameMaxLen {
676723 return buildError (stderr , 1 ,
677724 "dispatch filename '%s' exceeds %d characters" , dispatchFileName , dispatchFileNameMaxLen )
@@ -686,7 +733,39 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
686733 return 1
687734 }
688735
689- prompt := dispatchPointerPrompt (host , dispatchFilePath )
736+ var prompt string
737+ if advance {
738+ prompt = dispatchAdvancePointerPrompt (stage , dispatchFilePath )
739+ } else {
740+ prompt = dispatchPointerPrompt (host , dispatchFilePath )
741+ }
742+
743+ // Foot-gun guard: a non-bare claude dispatch that passes team_name selects the
744+ // legacy TeamCreate-registry envelope just assembled above (team_name present,
745+ // run_in_background absent) rather than the auto-team default — the teamName != ""
746+ // complement of mergedMode, the same three signals, no new detection and no
747+ // probe. The advisory fires here, after every error guard, so it warns only when
748+ // the legacy envelope is actually being emitted; a build that errors out (no
749+ // envelope) stays advisory-free. Stderr-only: the envelope above is untouched.
750+ // The text lives in the Claude seam beside BareModeAdvisory.
751+ if ! bareMode && host == "claude" && teamName != "" {
752+ claudeteam .LegacyTeamNameAdvisory (stderr )
753+ }
754+
755+ if advance {
756+ // Nothing is spawned, so the envelope carries no subagent_type/name/
757+ // team_name/run_in_background — only the pointer message and the fields
758+ // the FO's reuse-condition-4 comparator still needs (model).
759+ outAdvance := buildAdvanceOutput {
760+ SchemaVersion : schemaVersion ,
761+ Description : fmt .Sprintf ("%s: %s" , entityTitle , stage ),
762+ FetchCommands : fetchCommands ,
763+ DispatchFile : dispatchFilePath ,
764+ Prompt : prompt ,
765+ Model : effectiveModel ,
766+ }
767+ return emitBuildJSON (stdout , outAdvance )
768+ }
690769
691770 out := buildOutput {
692771 SchemaVersion : schemaVersion ,
@@ -711,18 +790,6 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
711790 out .RunInBackground = & runInBackground
712791 }
713792
714- // Foot-gun guard: a non-bare claude dispatch that passes team_name selects the
715- // legacy TeamCreate-registry envelope just assembled above (team_name present,
716- // run_in_background absent) rather than the auto-team default — the teamName != ""
717- // complement of mergedMode, the same three signals, no new detection and no
718- // probe. The advisory fires here, after every error guard, so it warns only when
719- // the legacy envelope is actually being emitted; a build that errors out (no
720- // envelope) stays advisory-free. Stderr-only: the envelope above is untouched.
721- // The text lives in the Claude seam beside BareModeAdvisory.
722- if ! bareMode && host == "claude" && teamName != "" {
723- claudeteam .LegacyTeamNameAdvisory (stderr )
724- }
725-
726793 return emitBuildJSON (stdout , out )
727794}
728795
@@ -887,6 +954,18 @@ func dispatchPointerPrompt(host, dispatchFilePath string) string {
887954 dispatchFilePath )
888955}
889956
957+ // dispatchAdvancePointerPrompt is the reuse-advance pointer message sent to a
958+ // live worker in place of the hand-assembled verbatim-stage-section template.
959+ // Unlike dispatchPointerPrompt, the wording is host-uniform: a reused worker
960+ // already holds its operating contract from initial dispatch (Claude's
961+ // Skill(...) invocation included), so no host branches on a skill-wrapper
962+ // clause here.
963+ func dispatchAdvancePointerPrompt (stage , dispatchFilePath string ) string {
964+ return fmt .Sprintf (
965+ "Advancing to next stage: %s.\n \n Read %s and treat its content as your next-stage assignment." ,
966+ stage , dispatchFilePath )
967+ }
968+
890969// stateHasOrigin reports whether the state checkout has a named `origin` remote,
891970// the named-remote question the split-root sync contract pushes/pulls against —
892971// true iff `git remote get-url origin` exits 0. Network-free (unlike ls-remote)
@@ -951,7 +1030,8 @@ func stateCommitGuidance(stateCheckout, entityPath, stateBranch string, hasOrigi
9511030// emitBuildJSON writes out as two-space-indented JSON with a trailing newline,
9521031// matching Python json.dumps(indent=2) followed by print() byte-for-byte,
9531032// including its ensure_ascii escaping of any non-ASCII entity title / prompt.
954- func emitBuildJSON (stdout io.Writer , out buildOutput ) int {
1033+ // out is buildOutput for a spawn envelope or buildAdvanceOutput for --advance.
1034+ func emitBuildJSON (stdout io.Writer , out any ) int {
9551035 return claudeteam .EmitPythonJSON (stdout , out )
9561036}
9571037
@@ -994,6 +1074,9 @@ func emitBuildSchema(stdout io.Writer) int {
9941074 "is_feedback_reflow" : map [string ]any {
9951075 "type" : "boolean" ,
9961076 },
1077+ "advance" : map [string ]any {
1078+ "type" : "boolean" ,
1079+ },
9971080 "host" : map [string ]any {
9981081 "type" : "string" ,
9991082 "enum" : []string {"claude" , "codex" , "pi" },
0 commit comments