feat(v0.6): AgentSpec field on TaskMeta with backward-compat unmarshal
Replace TaskMeta.Agent (string) with TaskMeta.Agent (AgentSpec) carrying type/command/args/env. Custom UnmarshalYAML preserves the legacy scalar form: a built-in name (claude, opencode) maps to that type; any other scalar maps to type=custom with the scalar as command. A missing agent field leaves Type empty so the resolver fills in default_agent at launch. ValidateAgentSpec enforces: known type (claude|opencode|custom), type=custom requires command, command must be an executable name or path with no whitespace or shell metacharacters. Launch-path wiring (Task 3) and the --agent flag rework (Task 4) are intentionally not part of this commit; cmd/* call sites are patched to the minimum needed for the build to compile.
This commit is contained in:
+10
-10
@@ -22,16 +22,16 @@ func makeArchiveWs(t *testing.T, root, category, dirName string) string {
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
slug := dirName[11:]
|
||||
meta := &workspace.TaskMeta{
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: category,
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: category,
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
||||
return dir
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func runAttach(cmd *cobra.Command, args []string) error {
|
||||
|
||||
agent := attachAgent
|
||||
if agent == "" {
|
||||
agent = ws.Meta.Agent
|
||||
agent = ws.Meta.Agent.Type
|
||||
}
|
||||
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ func TestAttachForwardsToEntryHelperWithAlwaysPersistent(t *testing.T) {
|
||||
ID: "t", Slug: "attach-fwd-demo", Title: "attach-fwd-demo",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
|
||||
@@ -27,7 +27,7 @@ func completionTestEnv(t *testing.T) string {
|
||||
ID: "t", Slug: slug, Title: slug,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: status, Category: category, Type: taskType,
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if status == "archived" {
|
||||
meta.ArchivedAt = &now
|
||||
|
||||
+9
-9
@@ -21,15 +21,15 @@ func createTestWs(t *testing.T, root, category, dirName, status string) string {
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
slug := dirName[11:] // skip "YYYY-MM-DD_"
|
||||
meta := &workspace.TaskMeta{
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Mode: "local",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
||||
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ func TestRunWorkspaceEntryIsInjectable(t *testing.T) {
|
||||
want := WorkspaceEntryOptions{
|
||||
WsPath: "/tmp/ws",
|
||||
WsRoot: "/tmp",
|
||||
WsMeta: &workspace.TaskMeta{Slug: "demo", Category: "projects", Mode: "local", Agent: "claude"},
|
||||
WsMeta: &workspace.TaskMeta{Slug: "demo", Category: "projects", Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}},
|
||||
Agent: "claude",
|
||||
Shell: true,
|
||||
CommandName: "test",
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ func runInfo(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Category: %s\n", m.Category)
|
||||
fmt.Printf("Status: %s\n", m.Status)
|
||||
fmt.Printf("Mode: %s\n", m.Mode)
|
||||
fmt.Printf("Agent: %s\n", agentLineWithSource(m.Agent, resolver))
|
||||
fmt.Printf("Agent: %s\n", agentLineWithSource(m.Agent.Type, resolver))
|
||||
fmt.Printf("Launch session mode: %s (%s)\n",
|
||||
resolver.SessionMode().Value, infoSourceLabel(resolver.SessionMode()))
|
||||
// v0.5.1: display timestamps in local time. task.yaml stores UTC;
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestInfoAgentSourceWorkspace(t *testing.T) {
|
||||
ID: "t", Slug: "agentwsdemo", Title: "agentwsdemo",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "opencode",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "opencode"},
|
||||
})
|
||||
|
||||
out, err := runInfoCapture(t, root, "agentwsdemo")
|
||||
@@ -70,7 +70,7 @@ func TestInfoAgentSourceDefaultForLegacy(t *testing.T) {
|
||||
ID: "t", Slug: "legacyagent", Title: "legacy agent",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "", // legacy: no agent recorded
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: ""}, // legacy: no agent recorded
|
||||
})
|
||||
|
||||
out, err := runInfoCapture(t, root, "legacyagent")
|
||||
@@ -107,7 +107,7 @@ func TestInfoLaunchSessionModeFromConfig(t *testing.T) {
|
||||
ID: "t", Slug: "modesrc", Title: "mode src",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
})
|
||||
|
||||
out, err := runInfoCapture(t, root, "modesrc")
|
||||
@@ -138,7 +138,7 @@ func TestInfoLaunchSessionModeBuiltinDefault(t *testing.T) {
|
||||
ID: "t", Slug: "modedefault", Title: "mode default",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
})
|
||||
|
||||
out, err := runInfoCapture(t, root, "modedefault")
|
||||
@@ -167,7 +167,7 @@ func TestInfoLaunchSessionModeAfterAgentBeforeCreated(t *testing.T) {
|
||||
ID: "t", Slug: "placement", Title: "placement",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
})
|
||||
|
||||
out, err := runInfoCapture(t, root, "placement")
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestInfoShowsLaunchFieldsWhenLaunchDirSet(t *testing.T) {
|
||||
ID: "t", Slug: "demo", Title: "demo",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "projects", Type: "project",
|
||||
Mode: "local", Agent: "claude", LaunchDir: "demo",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}, LaunchDir: "demo",
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInfoOmitsLaunchFieldsForTask(t *testing.T) {
|
||||
ID: "t", Slug: "regular", Title: "regular",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
@@ -104,8 +104,8 @@ func TestInfoFormatsTimestampsInLocalZone(t *testing.T) {
|
||||
ID: "t", Slug: "tz-test", Title: "tz-test",
|
||||
CreatedAt: fixedUTC, UpdatedAt: fixedUTC,
|
||||
ArchivedAt: &archived,
|
||||
Status: "archived", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Status: "archived", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestInfoFindsArchivedWorkspaceWithoutFlag(t *testing.T) {
|
||||
Category: "general",
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestInfoShowsDirExistsNoWhenLaunchDirMissing(t *testing.T) {
|
||||
ID: "t", Slug: "renamed", Title: "renamed",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "projects", Type: "project",
|
||||
Mode: "local", Agent: "claude", LaunchDir: "renamed",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}, LaunchDir: "renamed",
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func makeInfoSessionWorkspace(t *testing.T, root, slug string) string {
|
||||
ID: "t", Slug: slug, Title: slug,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
|
||||
@@ -26,7 +26,7 @@ func makeListSessionWorkspace(t *testing.T, root, category, dirName, slug, statu
|
||||
ID: "t", Slug: slug, Title: slug,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: status, Category: category, Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if status == "archived" {
|
||||
meta.ArchivedAt = &now
|
||||
|
||||
+10
-10
@@ -24,16 +24,16 @@ func listTestEnv(t *testing.T) string {
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
slug := dirName[11:]
|
||||
meta := &workspace.TaskMeta{
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Type: taskType,
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Type: taskType,
|
||||
Mode: "local",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
||||
}
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ func runNew(cmd *cobra.Command, args []string) error {
|
||||
Title: title,
|
||||
Category: category,
|
||||
Mode: "local",
|
||||
Agent: agent,
|
||||
AgentSpec: workspace.AgentSpec{Type: agent},
|
||||
IsProject: newProject,
|
||||
SeedDir: config.ResolveSeedDir(),
|
||||
SkipCategoryDir: skipCategoryDir,
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ func makeNotesWs(t *testing.T, root, category, dirName, status, notesBody string
|
||||
ID: "t", Slug: slug, Title: slug,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: status, Category: category, Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if status == "archived" {
|
||||
meta.ArchivedAt = &now
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func runOpen(cmd *cobra.Command, args []string) error {
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
Agent: ws.Meta.Agent,
|
||||
Agent: ws.Meta.Agent.Type,
|
||||
Shell: true, // open always launches a shell
|
||||
Force: openForce,
|
||||
Direct: openDirect,
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestOpenForwardsToEntryHelperWithShellTrue(t *testing.T) {
|
||||
ID: "t", Slug: "open-fwd-demo", Title: "open-fwd-demo",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ func TestPathPrintsAbsolutePath(t *testing.T) {
|
||||
ID: "t", Slug: "path-active", Title: "path-active",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestPathWorksOnArchivedWorkspace(t *testing.T) {
|
||||
Category: "general",
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ func makeArchivedWs(t *testing.T, root, category, dirName string) string {
|
||||
Category: category,
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
||||
return dir
|
||||
@@ -149,7 +149,7 @@ func TestRestoreRefusesAlreadyActive(t *testing.T) {
|
||||
ID: "t", Slug: "already", Title: "already",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ func doResume(query string, container, useShell, force bool, agentOverride strin
|
||||
|
||||
agent := agentOverride
|
||||
if agent == "" {
|
||||
agent = ws.Meta.Agent
|
||||
agent = ws.Meta.Agent.Type
|
||||
}
|
||||
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestResumeArchivedHintNoDuplicateError(t *testing.T) {
|
||||
Category: "general",
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestRunResumeForwardsToEntryHelperWithCommandNameResume(t *testing.T) {
|
||||
ID: "t", Slug: "resume-fwd-demo", Title: "resume-fwd-demo",
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
Status: "active", Category: "general", Type: "task",
|
||||
Mode: "local", Agent: "claude",
|
||||
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ func TestResumeArchivedWorkspaceShowsRestoreHint(t *testing.T) {
|
||||
Category: "general",
|
||||
Type: "task",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
Agent: workspace.AgentSpec{Type: "claude"},
|
||||
}
|
||||
workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user