Skip to content

Commit d833cd5

Browse files
clkaoclaude
andcommitted
Warn on --team-name on host=claude and document the shape flags in build --help
dispatch build is Claude-team-mode-aware: omitting --team-name on host=claude yields the auto-team shape, while passing it selects the legacy TeamCreate- registry shape. That opt-in was invisible (undocumented in --help) and silent (no warning), so a stray --team-name could emit the legacy envelope unnoticed. Add claudeteam.LegacyTeamNameAdvisory, fired from runBuildFields on !bareMode && host=="claude" && teamName!="" just before the envelope emit — after every error guard, so it warns only when the legacy envelope is actually emitted; an error build (no envelope) stays advisory-free. Stderr-only; the dispatch envelope is untouched. Document --host/--team-name/--bare-mode in printBuildUsage with wording aligned to the advisory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e3f85ec commit d833cd5

21 files changed

Lines changed: 165 additions & 0 deletions

internal/claudeteam/claudeteam.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,20 @@ func BareModeAdvisory(w io.Writer) {
7777
"If you intend teams mode, run ToolSearch select:TeamCreate and TeamCreate first. "+
7878
"If bare is intentional, this warning can be ignored.")
7979
}
80+
81+
// LegacyTeamNameAdvisory writes the --team-name-on-claude warning to w. It names
82+
// Claude-only concepts (the auto-team default, the legacy TeamCreate registry), so
83+
// the text lives in the Claude seam beside BareModeAdvisory. The generic build path
84+
// calls it when a non-bare claude dispatch passes a team_name — the teamName != ""
85+
// complement of merged mode — where auto-team is the default and the explicit
86+
// --team-name selects the sunsetting legacy shape. Unlike BareModeAdvisory it needs
87+
// no probe: the trigger is wholly in the CLI args. Stderr-only; the dispatch
88+
// envelope is untouched.
89+
func LegacyTeamNameAdvisory(w io.Writer) {
90+
fmt.Fprintln(w,
91+
"WARN: --team-name selects the legacy TeamCreate-registry dispatch shape "+
92+
"(team_name present, run_in_background absent). On host=claude, auto-team "+
93+
"is the default — omit --team-name to emit the auto-team shape (name + "+
94+
"run_in_background, no team_name). If you mean the legacy team-registry "+
95+
"path, this warning can be ignored.")
96+
}

internal/dispatch/build.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,18 @@ func runBuildFields(probe claudeteam.TeamStateProbe, opts buildOptions, fields m
690690
out.RunInBackground = &runInBackground
691691
}
692692

693+
// Foot-gun guard: a non-bare claude dispatch that passes team_name selects the
694+
// legacy TeamCreate-registry envelope just assembled above (team_name present,
695+
// run_in_background absent) rather than the auto-team default — the teamName != ""
696+
// complement of mergedMode, the same three signals, no new detection and no
697+
// probe. The advisory fires here, after every error guard, so it warns only when
698+
// the legacy envelope is actually being emitted; a build that errors out (no
699+
// envelope) stays advisory-free. Stderr-only: the envelope above is untouched.
700+
// The text lives in the Claude seam beside BareModeAdvisory.
701+
if !bareMode && host == "claude" && teamName != "" {
702+
claudeteam.LegacyTeamNameAdvisory(stderr)
703+
}
704+
693705
return emitBuildJSON(stdout, out)
694706
}
695707

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// ABOUTME: AC-1/AC-2/AC-3 — the --team-name-on-claude stderr advisory fires on the
2+
// ABOUTME: legacy arm, is silent on the merged arm, and the --help docs the shape flags.
3+
package dispatch
4+
5+
import (
6+
"encoding/json"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// teamNameAdvisoryMarker is a stable fragment of the legacy --team-name advisory.
12+
// The gate keys on its presence/absence rather than the full sentence so a future
13+
// wording tweak in claudeteam.LegacyTeamNameAdvisory does not break the test
14+
// (matches the advisoryMarker convention in build_advisory_probe_test.go).
15+
const teamNameAdvisoryMarker = "legacy TeamCreate-registry dispatch shape"
16+
17+
// TestBuildLegacyTeamNameAdvisory is AC-1's behavioral gate: on host=claude, a
18+
// build that passes team_name emits exactly the legacy advisory on stderr while
19+
// the same inputs WITHOUT team_name (the merged shape) emit none. The legacy arm
20+
// also re-asserts the legacy envelope shape on stdout (team_name present,
21+
// run_in_background absent) — AC-3 in the same arm: the advisory is stderr-only
22+
// and does not flip the emitted dispatch envelope.
23+
func TestBuildLegacyTeamNameAdvisory(t *testing.T) {
24+
home := t.TempDir()
25+
t.Setenv("HOME", home)
26+
root := t.TempDir()
27+
wd := writeGood(t, root)
28+
ep := writeFlatEntity(t, wd, "backlog", "")
29+
30+
legacyStdin := mergeStdin(map[string]any{
31+
"schema_version": 2,
32+
"entity_path": ep,
33+
"workflow_dir": wd,
34+
"stage": "backlog",
35+
"checklist": []string{"- a"},
36+
"host": "claude",
37+
"team_name": "fixture-team",
38+
"bare_mode": false,
39+
}, nil)
40+
mergedStdin := mergeStdin(map[string]any{
41+
"schema_version": 2,
42+
"entity_path": ep,
43+
"workflow_dir": wd,
44+
"stage": "backlog",
45+
"checklist": []string{"- a"},
46+
"host": "claude",
47+
"bare_mode": false,
48+
// team_name intentionally absent: the merged .178+ shape.
49+
}, nil)
50+
51+
legacy := runNative(legacyStdin, "build", "--workflow-dir", wd)
52+
if legacy.exit != 0 {
53+
t.Fatalf("legacy --team-name build exit=%d, want 0\nstderr:\n%s", legacy.exit, legacy.stderr)
54+
}
55+
merged := runNative(mergedStdin, "build", "--workflow-dir", wd)
56+
if merged.exit != 0 {
57+
t.Fatalf("merged build exit=%d, want 0\nstderr:\n%s", merged.exit, merged.stderr)
58+
}
59+
60+
// AC-1: advisory PRESENT on the --team-name arm, ABSENT on the merged arm.
61+
if !strings.Contains(legacy.stderr, teamNameAdvisoryMarker) {
62+
t.Errorf("host=claude --team-name build must emit the legacy advisory; stderr=%q", legacy.stderr)
63+
}
64+
if strings.Contains(merged.stderr, teamNameAdvisoryMarker) {
65+
t.Errorf("merged build (no --team-name) must emit NO legacy advisory; stderr=%q", merged.stderr)
66+
}
67+
// Exactly one advisory line on the legacy arm (not one per nothing).
68+
if got := strings.Count(legacy.stderr, teamNameAdvisoryMarker); got != 1 {
69+
t.Errorf("legacy advisory must appear exactly once; got %d:\n%s", got, legacy.stderr)
70+
}
71+
72+
// AC-3 (same arm): the legacy envelope shape is unchanged by the advisory —
73+
// team_name present, run_in_background absent on the --team-name arm; and the
74+
// merged arm keeps run_in_background present, team_name absent.
75+
var legacyOut mergedBuildOutput
76+
if err := json.Unmarshal([]byte(legacy.stdout), &legacyOut); err != nil {
77+
t.Fatalf("legacy stdout is not build JSON: %v\n%s", err, legacy.stdout)
78+
}
79+
if legacyOut.TeamName == nil || *legacyOut.TeamName != "fixture-team" {
80+
t.Errorf("legacy --team-name envelope must keep team_name=%q; got %v", "fixture-team", legacyOut.TeamName)
81+
}
82+
if legacyOut.RunInBackground != nil {
83+
t.Errorf("legacy --team-name envelope must NOT gain run_in_background; got %v", *legacyOut.RunInBackground)
84+
}
85+
var mergedOut mergedBuildOutput
86+
if err := json.Unmarshal([]byte(merged.stdout), &mergedOut); err != nil {
87+
t.Fatalf("merged stdout is not build JSON: %v\n%s", err, merged.stdout)
88+
}
89+
if mergedOut.TeamName != nil {
90+
t.Errorf("merged envelope must omit team_name; got %q", *mergedOut.TeamName)
91+
}
92+
if mergedOut.RunInBackground == nil || !*mergedOut.RunInBackground {
93+
t.Errorf("merged envelope must keep run_in_background=true; got %v", mergedOut.RunInBackground)
94+
}
95+
}
96+
97+
// TestBuildHelpDocumentsShapeFlags is AC-2's golden: dispatch build --help stdout
98+
// documents the shape-selecting flags (--host, --team-name, --bare-mode), with the
99+
// --team-name line naming the legacy shape and the auto-team default. Behavioral:
100+
// it runs the command and reads stdout, not a source grep.
101+
func TestBuildHelpDocumentsShapeFlags(t *testing.T) {
102+
res := runNative("", "build", "--help")
103+
if res.exit != 0 {
104+
t.Fatalf("dispatch build --help exit=%d, want 0\nstderr=%q", res.exit, res.stderr)
105+
}
106+
if res.stderr != "" {
107+
t.Fatalf("dispatch build --help stderr=%q, want empty", res.stderr)
108+
}
109+
assertContainsAll(t, res.stdout,
110+
"--host",
111+
"--team-name",
112+
"--bare-mode",
113+
"legacy TeamCreate-registry dispatch shape",
114+
"auto-team is the default",
115+
)
116+
}

internal/dispatch/dispatch.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ Build an ensign dispatch artifact from stdin JSON and write the JSON envelope to
287287
288288
Flags:
289289
--workflow-dir DIR Workflow definition directory containing README.md.
290+
--host HOST Override the runtime host (claude|codex|pi). Defaults to the detected runtime.
291+
--team-name NAME Select the legacy TeamCreate-registry dispatch shape. On host=claude, auto-team is the default — omit this unless you mean legacy team mode.
292+
--bare-mode Emit the bare sequential shape (no name, no team_name, no run_in_background).
290293
291294
Stdin JSON fields:
292295
schema_version Dispatch schema version. The current supported value is 2.

internal/dispatch/testdata/golden/build-abs-worktree.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

internal/dispatch/testdata/golden/build-crossproduct-feedback+scope+reflow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

internal/dispatch/testdata/golden/build-crossproduct-single+flat+nonworktree+team.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

internal/dispatch/testdata/golden/build-crossproduct-single+flat+worktree+team.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

internal/dispatch/testdata/golden/build-crossproduct-single+folder+worktree+team.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

internal/dispatch/testdata/golden/build-crossproduct-split+flat+worktree+team.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717

1818
===== stderr =====
19+
WARN: --team-name selects the legacy TeamCreate-registry dispatch shape (team_name present, run_in_background absent). On host=claude, auto-team is the default — omit --team-name to emit the auto-team shape (name + run_in_background, no team_name). If you mean the legacy team-registry path, this warning can be ignored.
1920

2021
===== body =====
2122
## First action

0 commit comments

Comments
 (0)