fix: gate focus auto-start on task dependencies (#3045)
This commit is contained in:
@@ -240,10 +240,12 @@ func (s *Service) isPassthroughProfile(ctx context.Context, profileID string) bo
|
||||
|
||||
// launchStart creates a new session and launches the agent.
|
||||
// If the request is an auto-start and the task's current workflow step does not
|
||||
// have auto_start_agent, the request is downgraded to a prepare (workspace-only,
|
||||
// no agent) to prevent unwanted auto-starts from the frontend's useAutoStartSession hook.
|
||||
// have auto_start_agent, or the task has unresolved dependencies, the request
|
||||
// is downgraded to a prepare (workspace-only, no agent) to prevent unwanted
|
||||
// auto-starts from the frontend's useAutoStartSession hook.
|
||||
func (s *Service) launchStart(ctx context.Context, req *LaunchSessionRequest) (*LaunchSessionResponse, error) {
|
||||
if req.AutoStart && s.shouldBlockAutoStart(ctx, req) {
|
||||
if req.AutoStart && (s.shouldBlockAutoStart(ctx, req) ||
|
||||
s.dependencyBlocksAutoStart(ctx, req.TaskID, "session.launch")) {
|
||||
req.LaunchWorkspace = true
|
||||
return s.launchPrepare(ctx, req)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kandev/kandev/internal/orchestrator/executor"
|
||||
"github.com/kandev/kandev/internal/task/models"
|
||||
taskservice "github.com/kandev/kandev/internal/task/service"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
)
|
||||
|
||||
type launchDependencyReader struct {
|
||||
blocked bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *launchDependencyReader) DependencyGate(context.Context, string) (bool, string, error) {
|
||||
return r.blocked, "pending", r.err
|
||||
}
|
||||
|
||||
func (r *launchDependencyReader) ListDependentTaskIDs(context.Context, string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *launchDependencyReader) ListPendingDependencyLaunches(
|
||||
context.Context,
|
||||
) ([]taskservice.PendingDependencyLaunch, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// @covers AC-TASKS-TASK-DEPENDENCIES-001.1
|
||||
func TestLaunchSession_AutoStartDependencyGate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blocked bool
|
||||
dependencyError error
|
||||
wantPrepareOnly bool
|
||||
}{
|
||||
{
|
||||
name: "unresolved dependency prepares workspace only",
|
||||
blocked: true,
|
||||
wantPrepareOnly: true,
|
||||
},
|
||||
{
|
||||
name: "resolved dependency follows normal launch path",
|
||||
},
|
||||
{
|
||||
name: "dependency read error prepares workspace only",
|
||||
dependencyError: errors.New("dependency store unavailable"),
|
||||
wantPrepareOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
const taskID = "task-focus-gate"
|
||||
ctx := context.Background()
|
||||
repo := setupTestRepo(t)
|
||||
seedAutoStartGateTask(t, repo, taskID)
|
||||
|
||||
taskRepo := newMockTaskRepo()
|
||||
taskRepo.tasks[taskID] = &v1.Task{
|
||||
ID: taskID,
|
||||
WorkspaceID: "ws1",
|
||||
Title: "Focus gate task",
|
||||
Description: "run the task",
|
||||
State: v1.TaskStateCreated,
|
||||
}
|
||||
|
||||
var agentLaunches atomic.Int32
|
||||
workspaceLaunchDone := make(chan struct{})
|
||||
agentProcessStarted := make(chan struct{})
|
||||
agentManager := &mockAgentManager{
|
||||
launchAgentFunc: func(
|
||||
_ context.Context, req *executor.LaunchAgentRequest,
|
||||
) (*executor.LaunchAgentResponse, error) {
|
||||
if req.StartAgent {
|
||||
agentLaunches.Add(1)
|
||||
} else {
|
||||
close(workspaceLaunchDone)
|
||||
}
|
||||
return &executor.LaunchAgentResponse{
|
||||
AgentExecutionID: "exec-focus-gate",
|
||||
Status: v1.AgentStatusStarting,
|
||||
}, nil
|
||||
},
|
||||
startAgentProcessFunc: func(context.Context, string) error {
|
||||
close(agentProcessStarted)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := createTestServiceWithScheduler(
|
||||
repo, newMockStepGetter(), taskRepo, agentManager,
|
||||
)
|
||||
svc.SetTaskDependencyReader(&launchDependencyReader{
|
||||
blocked: tt.blocked,
|
||||
err: tt.dependencyError,
|
||||
})
|
||||
|
||||
response, err := svc.LaunchSession(ctx, &LaunchSessionRequest{
|
||||
TaskID: taskID,
|
||||
Intent: IntentStart,
|
||||
AgentProfileID: "profile-focus-gate",
|
||||
AutoStart: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("LaunchSession returned error: %v", err)
|
||||
}
|
||||
if response == nil {
|
||||
t.Fatal("LaunchSession returned nil response")
|
||||
}
|
||||
|
||||
if tt.wantPrepareOnly {
|
||||
if response.State != string(models.TaskSessionStateCreated) {
|
||||
t.Fatalf("response state = %q, want CREATED", response.State)
|
||||
}
|
||||
if response.SessionID == "" {
|
||||
t.Fatal("prepare-only response did not include a session ID")
|
||||
}
|
||||
if got := agentLaunches.Load(); got != 0 {
|
||||
t.Fatalf("agent launches = %d, want 0", got)
|
||||
}
|
||||
waitForWorkspaceLaunch(t, workspaceLaunchDone)
|
||||
assertTaskWasNotScheduled(t, repo, taskRepo, taskID)
|
||||
return
|
||||
}
|
||||
|
||||
if got := agentLaunches.Load(); got != 1 {
|
||||
t.Fatalf("agent launches = %d, want 1", got)
|
||||
}
|
||||
if response.AgentExecutionID == "" {
|
||||
t.Fatal("normal launch response did not include an agent execution ID")
|
||||
}
|
||||
select {
|
||||
case <-agentProcessStarted:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("normal launch did not start the agent process")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func seedAutoStartGateTask(t *testing.T, repo interface {
|
||||
CreateWorkspace(context.Context, *models.Workspace) error
|
||||
CreateWorkflow(context.Context, *models.Workflow) error
|
||||
CreateTask(context.Context, *models.Task) error
|
||||
}, taskID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
if err := repo.CreateWorkspace(ctx, &models.Workspace{
|
||||
ID: "ws1", Name: "Focus gate workspace", CreatedAt: now, UpdatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("create workspace: %v", err)
|
||||
}
|
||||
if err := repo.CreateWorkflow(ctx, &models.Workflow{
|
||||
ID: "wf1", WorkspaceID: "ws1", Name: "Focus gate workflow", CreatedAt: now, UpdatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("create workflow: %v", err)
|
||||
}
|
||||
if err := repo.CreateTask(ctx, &models.Task{
|
||||
ID: taskID, WorkspaceID: "ws1", WorkflowID: "wf1",
|
||||
Title: "Focus gate task", Description: "run the task", State: v1.TaskStateCreated,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("create task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForWorkspaceLaunch(t *testing.T, launched <-chan struct{}) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-launched:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("workspace-only prepare did not launch workspace")
|
||||
}
|
||||
}
|
||||
|
||||
func assertTaskWasNotScheduled(t *testing.T, repo interface {
|
||||
GetTask(context.Context, string) (*models.Task, error)
|
||||
}, taskRepo *mockTaskRepo, taskID string) {
|
||||
t.Helper()
|
||||
task, err := repo.GetTask(context.Background(), taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("load task: %v", err)
|
||||
}
|
||||
if task.State == v1.TaskStateScheduling || task.State == v1.TaskStateInProgress {
|
||||
t.Fatalf("task state = %q, must not be scheduled or in progress", task.State)
|
||||
}
|
||||
|
||||
taskRepo.mu.Lock()
|
||||
defer taskRepo.mu.Unlock()
|
||||
for _, state := range taskRepo.stateHistory[taskID] {
|
||||
if state == v1.TaskStateScheduling || state == v1.TaskStateInProgress {
|
||||
t.Fatalf("task state history contains %q", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,23 +362,23 @@ test.describe("Workflow automation", () => {
|
||||
* Kanban board workflow with on_turn_start: move_to_next on Backlog.
|
||||
*
|
||||
* Workflow:
|
||||
* Backlog (on_turn_start: move_to_next, on_turn_complete: move_to_step("review"))
|
||||
* In Progress (on_enter: auto_start_agent, on_turn_complete: move_to_step("review"))
|
||||
* Backlog (on_turn_start: move_to_next, on_turn_complete: malformed move_to_step)
|
||||
* In Progress (on_enter: auto_start_agent, on_turn_complete: malformed move_to_step)
|
||||
* Review (on_turn_start: move_to_previous)
|
||||
* Done (on_turn_start: move_to_step("in-progress"))
|
||||
*
|
||||
* No step has is_start_step, so the task lands at position 0 (Backlog).
|
||||
* The on_turn_complete events use template-level step_id references ("review",
|
||||
* "in-progress") which don't resolve to actual step UUIDs — they're no-ops.
|
||||
* The on_turn_complete events use malformed step_position-only configs, which
|
||||
* the runtime skips because move_to_step requires a step_id.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create task in Backlog (no start_agent) and navigate to the task page
|
||||
* 2. Send a chat message — only the WS message path runs on_turn_start via
|
||||
* dispatchPromptAsync, so the Backlog → In Progress move triggers here
|
||||
* 3. Agent completes → on_turn_complete fires with move_to_step("review")
|
||||
* which is gracefully skipped (invalid step_id) → task stays in In Progress
|
||||
* 3. Agent completes → malformed on_turn_complete move_to_step is skipped
|
||||
* → task stays in In Progress
|
||||
*/
|
||||
test("kanban workflow: on_turn_start moves task, invalid step_id in on_turn_complete is skipped", async ({
|
||||
test("kanban workflow: on_turn_start moves task, malformed on_turn_complete is skipped", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
@@ -396,18 +396,18 @@ test.describe("Workflow automation", () => {
|
||||
const inProgressStep = steps.find((s) => s.name === "In Progress")!;
|
||||
|
||||
// Backlog: on_turn_start moves to next (In Progress),
|
||||
// on_turn_complete uses template-level step_id (no-op in API-created steps)
|
||||
// on_turn_complete has a malformed step_position-only config and is skipped.
|
||||
await apiClient.updateWorkflowStep(backlogStep.id, {
|
||||
events: {
|
||||
on_turn_start: [{ type: "move_to_next" }],
|
||||
on_turn_complete: [{ type: "move_to_step", config: { step_id: "review" } }],
|
||||
on_turn_complete: [{ type: "move_to_step", config: { step_position: 99 } }],
|
||||
},
|
||||
});
|
||||
|
||||
// In Progress: on_turn_complete with invalid step_id (should be skipped)
|
||||
// In Progress: malformed on_turn_complete move_to_step (should be skipped)
|
||||
await apiClient.updateWorkflowStep(inProgressStep.id, {
|
||||
events: {
|
||||
on_turn_complete: [{ type: "move_to_step", config: { step_id: "review" } }],
|
||||
on_turn_complete: [{ type: "move_to_step", config: { step_position: 99 } }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -441,13 +441,13 @@ test.describe("Workflow automation", () => {
|
||||
});
|
||||
|
||||
// Agent response confirms the mock agent ran; on_turn_complete then fires
|
||||
// with move_to_step("review") — invalid step_id → gracefully skipped.
|
||||
// with malformed move_to_step — gracefully skipped because step_id is missing.
|
||||
await expect(session.chat.getByText("simple mock response", { exact: false })).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(session.idleInput()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Task stays in In Progress on the kanban board (invalid step_id is a no-op).
|
||||
// Task stays in In Progress on the kanban board (malformed action is a no-op).
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
const card = kanban.taskCardInColumn("Kanban Flow Task", inProgressStep.id);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
created: 2026-08-25
|
||||
status: done
|
||||
requirements:
|
||||
- REQ-TASKS-TASK-DEPENDENCIES-001
|
||||
system_design:
|
||||
- ../../specs/tasks/system-design/task-dependencies.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Focus Auto-Start Dependency Gate
|
||||
|
||||
## Overview
|
||||
|
||||
Repair the `session.ensure` focus path so every `LaunchSession` request marked
|
||||
as an automatic start observes the existing task-dependency gate. Add the
|
||||
regression test first, then extend `launchStart`'s existing downgrade-to-prepare
|
||||
condition without changing manual starts or dependency-resolution behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Gate `LaunchSession` requests with `IntentStart` and `AutoStart: true` on
|
||||
unresolved task dependencies.
|
||||
- Fail closed when the dependency gate cannot be read.
|
||||
- Preserve the workspace-only `CREATED` session used to inspect a blocked task.
|
||||
- Preserve normal automatic starts after dependencies resolve.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Manual Start agent actions and direct manual `StartTask` calls.
|
||||
- Existing dependency-resolution, deferred-launch, and WIP-promotion behavior.
|
||||
- Frontend changes, API changes, persistence changes, and browser E2E coverage.
|
||||
|
||||
## Technical approach
|
||||
|
||||
Add the dependency predicate to the existing auto-start downgrade in
|
||||
`apps/backend/internal/orchestrator/session_launch.go`. The request remains an
|
||||
automatic request while `launchPrepare` runs, which suppresses passthrough
|
||||
prepare upgrades and prevents a bounce back into `launchStart`.
|
||||
|
||||
Add focused table-driven coverage in a new orchestrator test file. Use a
|
||||
minimal configurable `TaskDependencyReader` fake and the established launch
|
||||
counter/service wiring to prove blocked and read-error requests prepare only,
|
||||
while an unblocked request reaches the agent launch path.
|
||||
|
||||
## Tests
|
||||
|
||||
- `AC-TASKS-TASK-DEPENDENCIES-001.1`: `TestLaunchSession_AutoStartDependencyGate`
|
||||
covers an unresolved dependency, a resolved dependency, and a dependency-read
|
||||
error at the `LaunchSession` boundary.
|
||||
- The blocked and read-error cases assert a `CREATED` workspace-only session,
|
||||
no agent launch, and no transition to `SCHEDULING` or `IN_PROGRESS`.
|
||||
- The unblocked case asserts the normal launch path still reaches a running
|
||||
agent session.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Gate focus-induced auto-start](task-01-gate-focus-autostart.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
- Focused regression passed:
|
||||
`go test -tags fts5 ./internal/orchestrator -run '^TestLaunchSession_AutoStartDependencyGate$' -count=1`.
|
||||
- Focused race regression passed:
|
||||
`go test -race -tags fts5 ./internal/orchestrator -run '^TestLaunchSession_AutoStartDependencyGate$' -count=1`.
|
||||
- `make -C apps/backend lint` passed with zero issues.
|
||||
- `gofmt -l` reported no changes for the modified Go files.
|
||||
- `make -C apps/backend test` passed for `internal/orchestrator` and the other
|
||||
packages reached before two unrelated environment-sensitive packages failed.
|
||||
`internal/common/config` and `internal/launcher` selected the existing
|
||||
`/root/.kandev/config.yaml` instead of each test's temporary home.
|
||||
|
||||
## Risks
|
||||
|
||||
- The downgrade must retain `AutoStart: true`; clearing it would allow a
|
||||
passthrough prepare to upgrade back into an agent start.
|
||||
- The dependency reader is optional in some test/service constructions, so the
|
||||
nil-reader behavior must remain unchanged.
|
||||
- Broad backend validation can expose unrelated repository failures; targeted
|
||||
orchestrator evidence remains the regression proof.
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
id: "01-gate-focus-autostart"
|
||||
title: "Gate focus-induced auto-start"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-TASKS-TASK-DEPENDENCIES-001
|
||||
acceptance_criteria:
|
||||
- AC-TASKS-TASK-DEPENDENCIES-001.1
|
||||
system_design:
|
||||
- ../../specs/tasks/system-design/task-dependencies.md
|
||||
---
|
||||
|
||||
# Task 01: Gate Focus-Induced Auto-Start
|
||||
|
||||
## Summary
|
||||
|
||||
Make `launchStart` apply the task dependency gate to automatic starts before it
|
||||
can call `startTask`. Blocked and unreadable dependency state downgrade to the
|
||||
existing workspace-only prepare flow, while an unblocked task still starts.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add focused `LaunchSession` regression coverage for blocked, unblocked, and
|
||||
dependency-read-error cases.
|
||||
- Extend `launchStart`'s `req.AutoStart` downgrade condition with
|
||||
`dependencyBlocksAutoStart(ctx, req.TaskID, "session.launch")`.
|
||||
- Preserve the request fields needed by `launchPrepare` and its passthrough
|
||||
recursion guard.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Gating manual starts where `AutoStart` is false.
|
||||
- Changing `autoStartTaskForStep`, dependency resolution, WIP admission, or the
|
||||
frontend `session.ensure` message.
|
||||
- Adding a browser test for a backend launch-admission regression.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A blocked automatic `IntentStart` creates only a `CREATED` workspace session,
|
||||
never launches the agent, and never moves the task to `SCHEDULING` or
|
||||
`IN_PROGRESS`.
|
||||
- A dependency-read error has the same fail-closed prepare-only result.
|
||||
- An unblocked automatic `IntentStart` follows the existing normal start path.
|
||||
|
||||
## TDD sequence
|
||||
|
||||
1. Add `TestLaunchSession_AutoStartDependencyGate` with a nearby
|
||||
`@covers AC-TASKS-TASK-DEPENDENCIES-001.1` annotation and three cases:
|
||||
unresolved, resolved, and read error.
|
||||
2. Run only that test. Confirm the unresolved and read-error cases fail because
|
||||
an agent starts or the task leaves `CREATED`; confirm the resolved positive
|
||||
control reaches the existing launch path.
|
||||
3. Add the minimum dependency predicate to `launchStart`'s current automatic
|
||||
downgrade condition.
|
||||
4. Rerun the focused test and keep all three cases green. Refactor only the test
|
||||
setup if needed for clarity, then rerun it after the final production edit.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend
|
||||
go test -tags fts5 ./internal/orchestrator -run '^TestLaunchSession_AutoStartDependencyGate$' -count=1
|
||||
cd ../..
|
||||
make -C apps/backend test
|
||||
make -C apps/backend lint
|
||||
gofmt -l apps/backend/internal/orchestrator/session_launch.go apps/backend/internal/orchestrator/session_launch_dependency_test.go
|
||||
git diff --cached --name-only | grep '\.go$' | xargs -r gofmt -l
|
||||
```
|
||||
|
||||
If either formatting command reports a file, run `gofmt -w` on that exact file,
|
||||
restage it, and repeat both checks before committing.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/orchestrator/session_launch.go`
|
||||
- `apps/backend/internal/orchestrator/session_launch_dependency_test.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- A test that observes only a session row could miss an agent launch; assert
|
||||
both persisted state and the launch counter.
|
||||
- The resolved positive control must use the same harness so a no-op setup
|
||||
cannot make every case appear gated.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-TASKS-TASK-DEPENDENCIES-001` and
|
||||
`AC-TASKS-TASK-DEPENDENCIES-001.1`.
|
||||
- `docs/specs/tasks/system-design/task-dependencies.md`, especially automated
|
||||
launch guards and fail-closed behavior.
|
||||
- `apps/backend/internal/orchestrator/session_launch.go` and existing
|
||||
orchestrator dependency/launch test helpers.
|
||||
|
||||
## Results
|
||||
|
||||
Implemented the dependency gate in `launchStart` and added
|
||||
`TestLaunchSession_AutoStartDependencyGate`. The regression covers unresolved
|
||||
dependencies, resolved dependencies, and dependency-read errors at the
|
||||
`LaunchSession` boundary. Blocked and unreadable dependency state now keeps the
|
||||
request on the workspace-only `CREATED` path; resolved state still launches.
|
||||
|
||||
Validation:
|
||||
|
||||
- Focused test passed.
|
||||
- Focused race test passed.
|
||||
- Backend lint passed with zero issues.
|
||||
- `gofmt -l` reported no changes.
|
||||
- The full backend target reached and passed `internal/orchestrator`, but the
|
||||
target returned failure because unrelated config-discovery tests in
|
||||
`internal/common/config` and `internal/launcher` read the existing
|
||||
`/root/.kandev/config.yaml` instead of their temporary homes.
|
||||
@@ -101,7 +101,7 @@ parallel ones. The reuse is part of the contract because the guarantees below
|
||||
|---|---|
|
||||
| `task_blockers` table + BFS cycle detection (`office/dashboard.AddTaskBlocker`) | The dependency edge store and cycle validator, promoted from Office-only to a core task relationship. |
|
||||
| Deferred launch intent (`tasks.metadata.deferred_launch`, claimed atomically) | Storage and idempotent consumption of "start automatically when unblocked". |
|
||||
| The single auto-start chokepoint (`orchestrator.autoStartTaskForStep`) | Where the dependency gate lives and where dependency-triggered launches enter, so the existing auto-start claim guard prevents double launches. |
|
||||
| Automated launch guards (`orchestrator.autoStartTaskForStep` and `orchestrator.launchStart`) | `autoStartTaskForStep` gates workflow, queue, watcher, and dependency-resolution starts before claims. `launchStart` gates `LaunchSession` auto-starts, including `session.ensure`, and downgrades blocked requests to workspace-only prepare. |
|
||||
| WIP admission and queue promotion ([`tasks/wip-limit-pull-system`](wip-limit-pull-system.md)) | Decides whether an unblocked, auto-start-intent task launches now or on promotion. |
|
||||
|
||||
Two existing behaviors are intentionally *not* changed:
|
||||
@@ -419,8 +419,9 @@ Edges and blocking:
|
||||
Gating:
|
||||
|
||||
- **GIVEN** B depends on an unfinished A and B's workflow step has
|
||||
`on_enter: auto_start_agent`, **WHEN** B is moved into that step, **THEN** no
|
||||
session is created and the skip is logged.
|
||||
`on_enter: auto_start_agent`, **WHEN** B is moved into that step or its task
|
||||
view sends `session.ensure`, **THEN** no agent starts. The move creates no
|
||||
session; focus may create only a workspace-ready `CREATED` session.
|
||||
- **GIVEN** B depends on an unfinished A and B is queued for a WIP-limited
|
||||
auto-start step, **WHEN** capacity opens and B is promoted, **THEN** B is
|
||||
admitted, its queued badge clears, and no session is created.
|
||||
@@ -576,4 +577,4 @@ MCP:
|
||||
|
||||
## Implementation plan
|
||||
|
||||
See [`../../plans/task-dependencies/plan.md`](../../../plans/task-dependencies/plan.md).
|
||||
See [`../../plans/task-dependencies/plan.md`](../../../plans/task-dependencies/plan.md).
|
||||
|
||||
Reference in New Issue
Block a user