diff --git a/linear/README.md b/linear/README.md index 790ca33..c4190dc 100644 --- a/linear/README.md +++ b/linear/README.md @@ -206,6 +206,10 @@ cd $WORKSPACE/bitpod-tools python3 linear/scripts/create_linear_issues_from_seed.py ``` +Issue creation note (status/state): +- Linear API issue creation is deterministic when you pass a concrete `stateId` (workflow state ID). +- For Product Development (`team=BIT`), `Backlog` stateId is `162716a8-ffa4-43ea-9e0d-c48fdb8054bc` (BIT-442). + `simulate_e2e.py` runs the feature happy-path sequence: - PR opened -> In Progress - PR ready for review -> `In Review` diff --git a/linear/docs/process/linear_operating_guide_v3.md b/linear/docs/process/linear_operating_guide_v3.md index 2624d3d..8b8627b 100644 --- a/linear/docs/process/linear_operating_guide_v3.md +++ b/linear/docs/process/linear_operating_guide_v3.md @@ -92,7 +92,7 @@ Maintenance update — 2026-04-28: - Treat this assignee/delegate restriction as a temporary safety rule until Codex is decoupled from the user's personal Linear identity. - Do not casually change priority, estimate, due date, or milestone when confidence is low. - Do not create duplicate retroactive issues when an existing issue already owns the scope. -- Temporary Product Development issue-creation workaround: if a new issue lands in `Icebox 🧊` despite being intended for `Backlog`, immediately correct it to `Backlog` using the concrete Backlog status ID `162716a8-ffa4-43ea-9e0d-c48fdb8054bc` and record the evidence. Tracked by BIT-442. +- Temporary Product Development issue-creation workaround (BIT-442): if a new issue lands in `Icebox 🧊` despite being intended for `Backlog`, immediately correct it to `Backlog` using the concrete workflow state ID `162716a8-ffa4-43ea-9e0d-c48fdb8054bc` (Backlog). If a toolchain only accepts IDs, `Icebox 🧊` is `8bc24299-6afe-49e7-a713-42c4ec7f1863`. Record the evidence. 8. Capability degradation handling - If tool behavior is impaired, stop speculative actions and post minimal verified state. diff --git a/linear/scripts/create_linear_issues_from_seed.py b/linear/scripts/create_linear_issues_from_seed.py index d9bd045..a88f3a9 100755 --- a/linear/scripts/create_linear_issues_from_seed.py +++ b/linear/scripts/create_linear_issues_from_seed.py @@ -74,7 +74,46 @@ def resolve_project_id(token: str, project_name: str): raise SystemExit(f"project name not found: {project_name}") -def create_issue(token: str, team_id: str, project_id: Optional[str], title: str, description: str): +def resolve_state_id(token: str, team_id: str, state_name: str) -> str: + query = """ + query TeamStates($id: ID!) { + team(id: $id) { + id + key + name + states { + nodes { + id + name + type + position + } + } + } + } + """ + payload = gql_request(token, query, {"id": team_id}) + errors = payload.get("errors") + if errors: + raise SystemExit(json.dumps(errors, indent=2)) + nodes = payload.get("data", {}).get("team", {}).get("states", {}).get("nodes", []) + if not nodes: + raise SystemExit(f"no workflow states returned for team_id={team_id}") + for node in nodes: + if node.get("name") == state_name: + return node["id"] + options = ", ".join(sorted({n.get("name", "") for n in nodes if n.get("name")})) + raise SystemExit(f"workflow state not found: {state_name!r}. options: {options}") + + +def create_issue( + token: str, + team_id: str, + project_id: Optional[str], + title: str, + description: str, + state_id: Optional[str] = None, +): query = """ mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { @@ -95,6 +134,8 @@ def create_issue(token: str, team_id: str, project_id: Optional[str], title: str } if project_id: input_payload["projectId"] = project_id + if state_id: + input_payload["stateId"] = state_id payload = gql_request(token, query, {"input": input_payload}) errors = payload.get("errors") if errors: @@ -117,6 +158,16 @@ def main(): parser.add_argument("--seed", default=str(DEFAULT_SEED)) parser.add_argument("--team-key", default="BIT") parser.add_argument("--project-name", default="Taylor01") + parser.add_argument( + "--state-name", + default="Backlog", + help="Workflow state name to use on create (resolved to stateId). Defaults to Backlog.", + ) + parser.add_argument( + "--state-id", + default="", + help="Workflow state ID to use on create (overrides --state-name). Recommended when toolchains have name-resolution ambiguity.", + ) parser.add_argument("--live", action="store_true") args = parser.parse_args() @@ -132,6 +183,9 @@ def main(): team_id = resolve_team_id(token, args.team_key) project_id = resolve_project_id(token, args.project_name) + requested_state_id = args.state_id.strip() or None + if requested_state_id is None: + requested_state_id = resolve_state_id(token, team_id, args.state_name) for issue in seed["issues"]: created = create_issue( token=token, @@ -139,6 +193,7 @@ def main(): project_id=project_id, title=issue["title"], description=issue["description"], + state_id=requested_state_id, ) print(f"{created['identifier']}: {created['title']} -> {created['url']}")