sigit-code 1.5.5__tar.gz → 1.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/AGENTS.md +64 -13
  2. {sigit_code-1.5.5/.claude → sigit_code-1.5.7/.agents}/skills/agent-client-protocol/SKILL.md +19 -4
  3. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/sigit-code-release/SKILL.md +18 -4
  4. {sigit_code-1.5.5/.agents → sigit_code-1.5.7/.claude}/skills/agent-client-protocol/SKILL.md +19 -4
  5. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/sigit-code-release/SKILL.md +18 -4
  6. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-aur.yml +8 -0
  7. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-crates.yml +34 -6
  8. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-github.yml +8 -0
  9. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-homebrew.yml +8 -0
  10. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-npm.yml +75 -32
  11. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-nuget.yml +37 -6
  12. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-pypi.yml +29 -6
  13. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-scoop.yml +8 -0
  14. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/release-winget.yml +8 -0
  15. {sigit_code-1.5.5 → sigit_code-1.5.7}/.gitignore +1 -0
  16. {sigit_code-1.5.5 → sigit_code-1.5.7}/AGENTS.md +64 -13
  17. {sigit_code-1.5.5 → sigit_code-1.5.7}/CHANGELOG.md +87 -0
  18. {sigit_code-1.5.5 → sigit_code-1.5.7}/CLAUDE.md +64 -13
  19. {sigit_code-1.5.5 → sigit_code-1.5.7}/Cargo.lock +1 -1
  20. {sigit_code-1.5.5 → sigit_code-1.5.7}/Cargo.toml +1 -1
  21. {sigit_code-1.5.5 → sigit_code-1.5.7}/PKG-INFO +1 -1
  22. {sigit_code-1.5.5 → sigit_code-1.5.7}/docs/mcp.md +5 -3
  23. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/README.md.tmpl +1 -1
  24. sigit_code-1.5.7/npm/package-compat.json.tmpl +41 -0
  25. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/package.json.tmpl +1 -1
  26. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/scripts/render-main-package.cjs +8 -7
  27. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/scripts/render-platform-package.cjs +11 -3
  28. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/sigit/README.md +8 -0
  29. sigit_code-1.5.7/npm/sigit/src/index.ts +74 -0
  30. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/backend.rs +328 -2
  31. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/commands.rs +5 -4
  32. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/headless.rs +59 -42
  33. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/hooks.rs +3 -0
  34. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/instructions.rs +51 -5
  35. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/main.rs +333 -104
  36. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/provider.rs +18 -5
  37. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/skills.rs +5 -4
  38. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/subagents.rs +5 -4
  39. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/tools.rs +182 -27
  40. sigit_code-1.5.7/src/workspace.rs +160 -0
  41. sigit_code-1.5.7/tests/acp_endpoint_errors.rs +262 -0
  42. sigit_code-1.5.7/tests/acp_multi_root.rs +254 -0
  43. {sigit_code-1.5.5 → sigit_code-1.5.7}/tests/acp_permissions.rs +231 -7
  44. sigit_code-1.5.7/tests/acp_tool_stdin.rs +235 -0
  45. {sigit_code-1.5.5 → sigit_code-1.5.7}/tests/headless_mode.rs +59 -0
  46. sigit_code-1.5.5/.github/workflows/release-mcp-registry.yml +0 -79
  47. sigit_code-1.5.5/npm/sigit/src/index.ts +0 -43
  48. sigit_code-1.5.5/server.json +0 -39
  49. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
  50. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/branding/SKILL.md +0 -0
  51. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/run-sigit/SKILL.md +0 -0
  52. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/run-sigit/driver.mjs +0 -0
  53. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
  54. {sigit_code-1.5.5 → sigit_code-1.5.7}/.agents/skills/tool-calling/SKILL.md +0 -0
  55. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
  56. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/branding/SKILL.md +0 -0
  57. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/run-sigit/SKILL.md +0 -0
  58. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/run-sigit/driver.mjs +0 -0
  59. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
  60. {sigit_code-1.5.5 → sigit_code-1.5.7}/.claude/skills/tool-calling/SKILL.md +0 -0
  61. {sigit_code-1.5.5 → sigit_code-1.5.7}/.github/workflows/ci.yml +0 -0
  62. {sigit_code-1.5.5 → sigit_code-1.5.7}/.nvmrc +0 -0
  63. {sigit_code-1.5.5 → sigit_code-1.5.7}/LICENSE +0 -0
  64. {sigit_code-1.5.5 → sigit_code-1.5.7}/README.md +0 -0
  65. {sigit_code-1.5.5 → sigit_code-1.5.7}/docs/hooks.md +0 -0
  66. {sigit_code-1.5.5 → sigit_code-1.5.7}/examples/settings-with-hooks.toml +0 -0
  67. {sigit_code-1.5.5 → sigit_code-1.5.7}/examples/skills/README.md +0 -0
  68. {sigit_code-1.5.5 → sigit_code-1.5.7}/examples/skills/commit-message/SKILL.md +0 -0
  69. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/package-main.json.tmpl +0 -0
  70. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/sigit/.gitignore +0 -0
  71. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/sigit/package.json +0 -0
  72. {sigit_code-1.5.5 → sigit_code-1.5.7}/npm/sigit/tsconfig.json +0 -0
  73. {sigit_code-1.5.5 → sigit_code-1.5.7}/nuget/.gitignore +0 -0
  74. {sigit_code-1.5.5 → sigit_code-1.5.7}/nuget/sigit/Program.cs +0 -0
  75. {sigit_code-1.5.5 → sigit_code-1.5.7}/nuget/sigit/README.md +0 -0
  76. {sigit_code-1.5.5 → sigit_code-1.5.7}/nuget/sigit/SiGit.Code.csproj +0 -0
  77. {sigit_code-1.5.5 → sigit_code-1.5.7}/packaging/aur/PKGBUILD.in +0 -0
  78. {sigit_code-1.5.5 → sigit_code-1.5.7}/packaging/nfpm.yaml +0 -0
  79. {sigit_code-1.5.5 → sigit_code-1.5.7}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
  80. {sigit_code-1.5.5 → sigit_code-1.5.7}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
  81. {sigit_code-1.5.5 → sigit_code-1.5.7}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
  82. {sigit_code-1.5.5 → sigit_code-1.5.7}/pypi/README.md +0 -0
  83. {sigit_code-1.5.5 → sigit_code-1.5.7}/pypi/pyproject.toml +0 -0
  84. {sigit_code-1.5.5 → sigit_code-1.5.7}/pyproject.toml +0 -0
  85. {sigit_code-1.5.5 → sigit_code-1.5.7}/rust-toolchain.toml +0 -0
  86. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/account.rs +0 -0
  87. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/browser_auth.rs +0 -0
  88. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/chat.rs +0 -0
  89. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/credentials.rs +0 -0
  90. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/frontmatter.rs +0 -0
  91. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/mcp.rs +0 -0
  92. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/models.rs +0 -0
  93. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/permissions.rs +0 -0
  94. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/session_store.rs +0 -0
  95. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/settings.rs +0 -0
  96. {sigit_code-1.5.5 → sigit_code-1.5.7}/src/setup.rs +0 -0
@@ -26,7 +26,23 @@ from the name alone (e.g. `feature/tool-permission-system`, `fix/glob-mtime-sort
26
26
  a task, ticket, or session id (not `feature/task-q003hm`).
27
27
 
28
28
  **IMPORTANT — pull request target:** Always open pull requests against the `development` branch,
29
- never `main`. `main` is release-only; `development` is where day-to-day work integrates.
29
+ never `main`. `main` is release-only; `development` is where day-to-day work integrates, and the
30
+ release merge (see the `sigit-code-release` skill) is the one thing that ever puts commits on
31
+ `main`.
32
+
33
+ This rule needs help to hold, because the repository's default branch is `main`: both
34
+ `gh pr create` and the GitHub web form pre-select it, so a PR lands on the wrong base unless the
35
+ base is passed explicitly. Pass it every time, and check that it took:
36
+
37
+ ```sh
38
+ gh pr create --base development --head <branch>
39
+ gh pr view <number> --json baseRefName
40
+ ```
41
+
42
+ Retargeting a PR that was opened against `main` costs nothing before it merges:
43
+ `gh pr edit <number> --base development`. After it merges it costs real work — the change sits on
44
+ `main` while `development`, which the next release is cut from, does not have it, and someone has
45
+ to carry it back by hand. PR #65 went in that way.
30
46
 
31
47
  **IMPORTANT — branch off `development`:** Start every working branch from the latest
32
48
  `origin/development`. Exception: when new work *depends on* a feature branch that has not merged
@@ -100,7 +116,13 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
100
116
  smbCloud, and stay general otherwise).
101
117
  - **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
102
118
  `ToolResult`, `TurnResult`). Two impls: `LocalBackend` (on-device via `onde::ChatEngine`) and
103
- `OpenAiBackend` (any OpenAI-compatible HTTP endpoint).
119
+ `OpenAiBackend` (any OpenAI-compatible HTTP endpoint). A `BackendError` is user-facing: the
120
+ ACP prompt handler passes it to the client verbatim and the editor puts it in an error banner,
121
+ so `describe_api_error` unwraps the OpenAI `{"error":{"message":…}}` envelope and shows that
122
+ message on its own, falling back to the status only when there is nothing to unwrap. An
123
+ endpoint can also fail *after* the response is open, reporting it as a `data:` frame holding
124
+ the same envelope; that frame has no `choices`, so `consume_stream` has to check for it
125
+ explicitly or it parses as an empty chunk and the turn ends looking like an empty answer.
104
126
  - **`src/provider.rs`** — decides *which* backend serves inference. Resolution order, first match
105
127
  wins: (1) override via `OPENAI_BASE_URL`+`OPENAI_API_KEY` or active profile in
106
128
  `~/.config/sigit/providers.toml`; (2) siGit Code Cloud when logged in; (3) on-device.
@@ -110,7 +132,13 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
110
132
  spec list (`all_tools`) and the execute `match` (`execute_tool`). `run_command` also enforces
111
133
  commit attribution: when a command creates a new commit that lacks the
112
134
  `Co-Authored-By: siGit Code` trailer (`COMMIT_CO_AUTHOR_TRAILER`), it amends the trailer in —
113
- unless the commit already exists on a remote, which is never rewritten. Also owns the `task`
135
+ unless the commit already exists on a remote, which is never rewritten. Every child process it
136
+ spawns (`spawn_shell`, the `git` helpers, and `hooks.rs`) sets `stdin` to null and never
137
+ inherits it: in ACP mode sigit's stdin is the JSON-RPC pipe from the editor, so a command that
138
+ reads stdin both blocks forever and eats the client's next request, wedging the session past
139
+ any timeout. On Unix the shell also leads its own process group so the timeout kills the whole
140
+ tree, and stdout/stderr are drained on threads while the command runs — waiting first and
141
+ reading after deadlocks as soon as the output outgrows the pipe buffer. Also owns the `task`
114
142
  tool: a nested agent loop in a fresh conversation, offered only when `subagent_available()`
115
143
  (a subagent factory is registered — see `register_subagent_factory_for` in `main.rs`; on-device
116
144
  registers a `None`-returning factory since onde has a single shared history). A subagent's
@@ -129,7 +157,8 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
129
157
  HTTP/JSON-RPC implementation.
130
158
  - **`src/skills.rs`** — [Agent Skills](https://agentskills.io) support. Discovers skill
131
159
  folders (each with a `SKILL.md`: YAML frontmatter `name` + `description`, then Markdown
132
- instructions) from `.sigit/skills/` and `.claude/skills/` in the cwd, `$SIGIT_CONFIG_DIR/skills/`,
160
+ instructions) from `.sigit/skills/` and `.claude/skills/` in every project root (see `src/workspace.rs`),
161
+ `$SIGIT_CONFIG_DIR/skills/`,
133
162
  and `~/.claude/skills/`. Progressive disclosure: the discovery list (name + description) is
134
163
  baked into the dynamically-built `skill` tool's description, and activating a skill (the model
135
164
  calls `skill` with a name) loads the full `SKILL.md` body. The `skill` tool is appended in the
@@ -137,7 +166,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
137
166
  and only when at least one skill exists.
138
167
  - **`src/commands.rs`** — user-defined slash commands. Discovers Markdown files (each an
139
168
  optional YAML frontmatter block — `description`, `argument-hint` — followed by a prompt-template
140
- body) from `.sigit/commands/` and `.claude/commands/` in the cwd, `$SIGIT_CONFIG_DIR/commands/`,
169
+ body) from `.sigit/commands/` and `.claude/commands/` in every project root, `$SIGIT_CONFIG_DIR/commands/`,
141
170
  and `~/.claude/commands/`. A subdirectory namespaces the command with `:`
142
171
  (`.sigit/commands/git/commit.md` → `/git:commit`). Unlike skills there's no tool-call
143
172
  indirection: invoking one works exactly like the built-in `/init` — `commands::render`
@@ -151,7 +180,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
151
180
  - **`src/subagents.rs`** — configurable subagent types for the `task` tool. Discovers Markdown
152
181
  files (YAML frontmatter `name` + `description`, optional comma-separated `tools:` allow-list,
153
182
  then a Markdown body that becomes the subagent's system prompt) from `.sigit/agents/` and
154
- `.claude/agents/` in the cwd, `$SIGIT_CONFIG_DIR/agents/`, and `~/.claude/agents/`. Passing a
183
+ `.claude/agents/` in every project root, `$SIGIT_CONFIG_DIR/agents/`, and `~/.claude/agents/`. Passing a
155
184
  type's `name` as `task`'s `subagent_type` argument swaps in that system prompt and, if `tools:`
156
185
  is set, narrows the offered toolset to its *intersection* with `SUBAGENT_TOOL_NAMES` — the
157
186
  security-relevant narrowing logic lives in `tools.rs` next to that constant, not here; this
@@ -182,10 +211,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
182
211
  `tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
183
212
  (excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
184
213
  server is *also* listed in the public MCP Registry as `si.sigit/sigit` — a **remote**
185
- Streamable-HTTP listing (`server.json` at the repo root, published by
186
- `release-mcp-registry.yml`). Because it's a remote server, the registry's URL-match rule
187
- forces a domain namespace (`si.sigit` ↔ `sigit.si`) verified by a DNS TXT record, not the
188
- GitHub-OIDC scheme `smbcloud-cli` uses for its package listing.
214
+ Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
215
+ (`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
216
+ it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
217
+ `sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
218
+ package listing.
189
219
  - **`src/permissions.rs`** — tool permission policy. Every tool call passes through
190
220
  `decision_for` before executing: read-only tools always run; mutating tools (and all
191
221
  `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
@@ -201,9 +231,22 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
201
231
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
202
232
  walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
203
233
  plus a global file under `$SIGIT_CONFIG_DIR`. Files are ordered outermost-first so the deepest
204
- (most specific) wins. The combined block is injected via `session_context_message` in `main.rs`
205
- — pushed as a system message at every ACP session entry point (new/load/fork + model switch)
206
- and appended to the system prompt on the cloud and TUI-startup paths.
234
+ (most specific) wins. In a multi-root project that walk is repeated for every root
235
+ (`load_workspace_instructions`). The combined block is injected via `session_context_message`
236
+ in `main.rs` — pushed as a system message at every ACP session entry point (new/load/fork +
237
+ model switch) and appended to the system prompt on the cloud and TUI-startup paths.
238
+ - **`src/workspace.rs`** — the directories the session treats as project roots. An editor can
239
+ open several at once (Zed calls it a multi-root project) and ACP carries the extras as
240
+ `additional_directories` on every session request; the headless CLI takes them as repeatable
241
+ `--add-dir` flags. A client only sends those extras to an agent that advertises
242
+ `sessionCapabilities.additionalDirectories` in its `initialize` reply, so that capability in
243
+ `handle_initialize` is what makes the rest of this reachable — without it Zed keeps the first
244
+ root, drops the others, and shows "This agent doesn't currently support multi-root workspaces".
245
+ The process still has one working directory, so the extra roots live in a
246
+ process-global here and `project_dirs()` returns cwd-first, extras after. Project-local
247
+ discovery reads it: skills, slash commands, subagent types, and instruction files all scan
248
+ every root. MCP is deliberately not on that list — `mcp::init` runs once at startup, before
249
+ any session exists, so a second root's `.sigit/mcp.toml` has nobody to tell.
207
250
  - **`src/chat.rs`** — the Unix-only ratatui TUI. Loading-spinner phase then chat; uses
208
251
  `tokio::select!` to multiplex terminal events with streaming tokens.
209
252
  - **`src/setup.rs`** — model cache location, local model discovery, selected-model persistence.
@@ -284,6 +327,14 @@ winget, and the AUR PKGBUILD each need one, and they consume the raw binaries ra
284
327
  tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
285
328
  (`packaging/nfpm.yaml`), packaging the already-built binary rather than re-invoking cargo.
286
329
 
330
+ Re-pushing a tag fires every release workflow a second time, so each one is grouped by tag in a
331
+ `concurrency` block with `cancel-in-progress: false` — the duplicate queues behind the original
332
+ rather than interrupting a publish that is halfway through uploading. What keeps the queued run
333
+ from going red is the "is this version already published?" check each publishing workflow does
334
+ before it uploads. Those checks are load-bearing, not belt-and-braces: v1.5.6 published twice
335
+ because the crates.io one asked the API with curl's default User-Agent, which crates.io answers
336
+ with a 403, which read as "not published yet".
337
+
287
338
  Three of these need credentials or a one-time manual step before they work:
288
339
 
289
340
  - Scoop needs a `getsigit/scoop-bucket` repo and a `SCOOP_BUCKET_TOKEN` secret, mirroring the
@@ -271,8 +271,14 @@ Ok(ForkSessionResponse::new(new_id).config_options(config_options))
271
271
  per-session picker (see Config options below).
272
272
 
273
273
  The session requests carry `cwd: PathBuf` and (with the feature)
274
- `additional_directories: Vec<PathBuf>`. siGit stashes `cwd`, `set_current_dir`s
275
- to it, and pushes a system message telling the model to use absolute paths under it.
274
+ `additional_directories: Vec<PathBuf>` — the other directories of a multi-root
275
+ project, sent only to an agent whose `initialize` reply carries
276
+ `SessionCapabilities::new().additional_directories(SessionAdditionalDirectoriesCapabilities::new())`. `SiGitAgent::enter_session_roots` stashes `cwd`, `set_current_dir`s to
277
+ it, hands the extras to `workspace::set_additional_roots`, and returns the ones
278
+ that survived (a root that isn't a directory, repeats, or just names `cwd` again
279
+ is dropped). `session_context_message` then names every root in the system
280
+ message, and project-local discovery — skills, slash commands, subagent types,
281
+ instruction files — scans all of them, primary root first.
276
282
 
277
283
  ### `PromptRequest` / blocks
278
284
 
@@ -555,8 +561,17 @@ Editor Agent
555
561
  10. **Zed re-fires the last config selection on connect** — make
556
562
  `setConfigOption` a no-op when the requested model is already active, and
557
563
  never start a model switch while a startup load is still in flight (GPU OOM).
558
- 11. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
559
- 12. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
564
+ 11. **`additional_directories` is not decoration** — dropping it means a
565
+ multi-root project silently behaves as if only the first root existed, with
566
+ the other repositories' `AGENTS.md` and skills missing. Route it through
567
+ `workspace.rs`, not just the log line. Handling it is also only half the
568
+ job: Zed gates the field on
569
+ `agentCapabilities.sessionCapabilities.additionalDirectories` being present
570
+ in the `initialize` reply, so an agent that reads the field but never
571
+ advertises the capability is never sent one, and the user gets a banner
572
+ saying the agent has no multi-root support.
573
+ 12. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
574
+ 13. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
560
575
  carries the *rebuilt* options so the picker reflects the new current value.
561
576
 
562
577
  ---
@@ -28,9 +28,16 @@ Use this skill when preparing a release for this repository.
28
28
  - Update the root `sigit` package entry in `Cargo.lock` when the crate version changes.
29
29
  - Add or update the top changelog entry in `CHANGELOG.md` for the release being cut.
30
30
  - Do not treat `npm/sigit/package.json` `0.0.0-dev` as a bug by default. The npm release workflow rewrites it at publish time using `npm/scripts/render-main-package.cjs` and the release tag.
31
+ - Every npm package publishes twice, under `@getsigit` and under `@smbcloud`. `@smbcloud` is the pre-1.5.5 name, mirrored so an install predating the rename keeps updating; `@getsigit` is canonical and is what the README, the Homebrew tap, and the ACP registry entry point at. All fourteen names (two base packages, twelve platform packages) publish over OIDC and each needs its own Trusted Publisher on npm pointing at `release-npm.yml`.
32
+ - The scope is a render-time argument, not a second copy of the packaging: `render-platform-package.cjs` takes it as a fifth argument, and the base package picks it up from `package-main.json.tmpl` versus `package-compat.json.tmpl`. `npm/sigit/src/index.ts` is built once and shipped under both names — it reads its own `package.json` at runtime to decide which scope to resolve the platform binary from, and prints the migration notice only when it finds itself running as `@smbcloud/sigit`.
31
33
  - Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `Cargo.toml`.
32
- - Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, and `release-homebrew.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
34
+ - Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-nuget.yml`, and `release-mcp-registry.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
33
35
  - The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
36
+ - `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
37
+ - Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
38
+ - npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
39
+ - The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
40
+ - The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
34
41
 
35
42
  ## Git release flow
36
43
 
@@ -42,6 +49,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
42
49
  4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
43
50
  5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
44
51
 
52
+ Step 4 is the only way commits are meant to reach `main`. A feature or fix PR targeting `main` breaks that: it puts work on the release branch that `development` does not have, so the next release is cut without it. Such PRs belong on `development` (see the pull request target rule in `AGENTS.md`).
53
+
45
54
  Pushing the `v*.*.*` tag is what fires every release workflow, so create and push it only after the merge into `main` has landed. Do not commit, tag, or push until the user explicitly asks — confirm the version and that they want the release to go out first.
46
55
 
47
56
  ## Typical files to inspect
@@ -51,10 +60,14 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
51
60
  - `CHANGELOG.md`
52
61
  - `README.md`
53
62
  - `npm/sigit/package.json`
63
+ - `npm/package-main.json.tmpl`, `npm/package-compat.json.tmpl`, `npm/package.json.tmpl` (publish-time package metadata for both scopes)
54
64
  - `npm/scripts/render-main-package.cjs`
55
65
  - `npm/`
56
66
  - `pypi/`
57
- - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`)
67
+ - `nuget/sigit/` (bundled-binary `.NET` tool packaging)
68
+ - `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
69
+ - `server.json` (MCP Registry listing)
70
+ - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-scoop.yml`, `release-winget.yml`, `release-aur.yml`, `release-nuget.yml`, `release-mcp-registry.yml`)
58
71
 
59
72
  ## Release checklist
60
73
 
@@ -67,5 +80,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
67
80
  - Git flow followed: bump committed on `release/v<version>`, merged back to `development`, then `development` merged into `main`, with `v<version>` tagged on the `main` merge commit.
68
81
  - Release notes or changelog entries match the actual changes.
69
82
  - CI-equivalent local checks pass for the relevant platform or target.
70
- - Package names, install commands, and branding stay consistent.
71
- - Any known release limitations are called out explicitly.
83
+ - Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
84
+ - Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
85
+ - Any known release limitations are called out explicitly, including a dispatch failure in `release-scoop`/`release-winget`/`release-aur` (non-fatal — logged as a warning by `release-github.yml`) versus a hard failure in a directly tag-triggered workflow like `release-nuget` or `release-mcp-registry`.
@@ -271,8 +271,14 @@ Ok(ForkSessionResponse::new(new_id).config_options(config_options))
271
271
  per-session picker (see Config options below).
272
272
 
273
273
  The session requests carry `cwd: PathBuf` and (with the feature)
274
- `additional_directories: Vec<PathBuf>`. siGit stashes `cwd`, `set_current_dir`s
275
- to it, and pushes a system message telling the model to use absolute paths under it.
274
+ `additional_directories: Vec<PathBuf>` — the other directories of a multi-root
275
+ project, sent only to an agent whose `initialize` reply carries
276
+ `SessionCapabilities::new().additional_directories(SessionAdditionalDirectoriesCapabilities::new())`. `SiGitAgent::enter_session_roots` stashes `cwd`, `set_current_dir`s to
277
+ it, hands the extras to `workspace::set_additional_roots`, and returns the ones
278
+ that survived (a root that isn't a directory, repeats, or just names `cwd` again
279
+ is dropped). `session_context_message` then names every root in the system
280
+ message, and project-local discovery — skills, slash commands, subagent types,
281
+ instruction files — scans all of them, primary root first.
276
282
 
277
283
  ### `PromptRequest` / blocks
278
284
 
@@ -555,8 +561,17 @@ Editor Agent
555
561
  10. **Zed re-fires the last config selection on connect** — make
556
562
  `setConfigOption` a no-op when the requested model is already active, and
557
563
  never start a model switch while a startup load is still in flight (GPU OOM).
558
- 11. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
559
- 12. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
564
+ 11. **`additional_directories` is not decoration** — dropping it means a
565
+ multi-root project silently behaves as if only the first root existed, with
566
+ the other repositories' `AGENTS.md` and skills missing. Route it through
567
+ `workspace.rs`, not just the log line. Handling it is also only half the
568
+ job: Zed gates the field on
569
+ `agentCapabilities.sessionCapabilities.additionalDirectories` being present
570
+ in the `initialize` reply, so an agent that reads the field but never
571
+ advertises the capability is never sent one, and the user gets a banner
572
+ saying the agent has no multi-root support.
573
+ 12. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
574
+ 13. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
560
575
  carries the *rebuilt* options so the picker reflects the new current value.
561
576
 
562
577
  ---
@@ -28,9 +28,16 @@ Use this skill when preparing a release for this repository.
28
28
  - Update the root `sigit` package entry in `Cargo.lock` when the crate version changes.
29
29
  - Add or update the top changelog entry in `CHANGELOG.md` for the release being cut.
30
30
  - Do not treat `npm/sigit/package.json` `0.0.0-dev` as a bug by default. The npm release workflow rewrites it at publish time using `npm/scripts/render-main-package.cjs` and the release tag.
31
+ - Every npm package publishes twice, under `@getsigit` and under `@smbcloud`. `@smbcloud` is the pre-1.5.5 name, mirrored so an install predating the rename keeps updating; `@getsigit` is canonical and is what the README, the Homebrew tap, and the ACP registry entry point at. All fourteen names (two base packages, twelve platform packages) publish over OIDC and each needs its own Trusted Publisher on npm pointing at `release-npm.yml`.
32
+ - The scope is a render-time argument, not a second copy of the packaging: `render-platform-package.cjs` takes it as a fifth argument, and the base package picks it up from `package-main.json.tmpl` versus `package-compat.json.tmpl`. `npm/sigit/src/index.ts` is built once and shipped under both names — it reads its own `package.json` at runtime to decide which scope to resolve the platform binary from, and prints the migration notice only when it finds itself running as `@smbcloud/sigit`.
31
33
  - Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `Cargo.toml`.
32
- - Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, and `release-homebrew.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
34
+ - Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-nuget.yml`, and `release-mcp-registry.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
33
35
  - The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
36
+ - `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
37
+ - Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
38
+ - npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
39
+ - The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
40
+ - The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
34
41
 
35
42
  ## Git release flow
36
43
 
@@ -42,6 +49,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
42
49
  4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
43
50
  5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
44
51
 
52
+ Step 4 is the only way commits are meant to reach `main`. A feature or fix PR targeting `main` breaks that: it puts work on the release branch that `development` does not have, so the next release is cut without it. Such PRs belong on `development` (see the pull request target rule in `AGENTS.md`).
53
+
45
54
  Pushing the `v*.*.*` tag is what fires every release workflow, so create and push it only after the merge into `main` has landed. Do not commit, tag, or push until the user explicitly asks — confirm the version and that they want the release to go out first.
46
55
 
47
56
  ## Typical files to inspect
@@ -51,10 +60,14 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
51
60
  - `CHANGELOG.md`
52
61
  - `README.md`
53
62
  - `npm/sigit/package.json`
63
+ - `npm/package-main.json.tmpl`, `npm/package-compat.json.tmpl`, `npm/package.json.tmpl` (publish-time package metadata for both scopes)
54
64
  - `npm/scripts/render-main-package.cjs`
55
65
  - `npm/`
56
66
  - `pypi/`
57
- - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`)
67
+ - `nuget/sigit/` (bundled-binary `.NET` tool packaging)
68
+ - `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
69
+ - `server.json` (MCP Registry listing)
70
+ - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-scoop.yml`, `release-winget.yml`, `release-aur.yml`, `release-nuget.yml`, `release-mcp-registry.yml`)
58
71
 
59
72
  ## Release checklist
60
73
 
@@ -67,5 +80,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
67
80
  - Git flow followed: bump committed on `release/v<version>`, merged back to `development`, then `development` merged into `main`, with `v<version>` tagged on the `main` merge commit.
68
81
  - Release notes or changelog entries match the actual changes.
69
82
  - CI-equivalent local checks pass for the relevant platform or target.
70
- - Package names, install commands, and branding stay consistent.
71
- - Any known release limitations are called out explicitly.
83
+ - Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
84
+ - Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
85
+ - Any known release limitations are called out explicitly, including a dispatch failure in `release-scoop`/`release-winget`/`release-aur` (non-fatal — logged as a warning by `release-github.yml`) versus a hard failure in a directly tag-triggered workflow like `release-nuget` or `release-mcp-registry`.
@@ -12,6 +12,14 @@ on:
12
12
  description: "Release tag (e.g. v1.5.2)"
13
13
  required: true
14
14
 
15
+ concurrency:
16
+ # A re-pushed or re-dispatched tag fires this a second time. Queue the
17
+ # duplicate behind the original instead of cancelling it: cancelling a run
18
+ # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
19
+ # just hits the "already published" check and exits clean.
20
+ group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
21
+ cancel-in-progress: false
22
+
15
23
  permissions:
16
24
  contents: read
17
25
 
@@ -10,6 +10,14 @@ on:
10
10
  description: "Release tag (e.g. v1.0.2)"
11
11
  required: true
12
12
 
13
+ concurrency:
14
+ # A re-pushed or re-dispatched tag fires this a second time. Queue the
15
+ # duplicate behind the original instead of cancelling it: cancelling a run
16
+ # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
17
+ # just hits the "already published" check and exits clean.
18
+ group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
19
+ cancel-in-progress: false
20
+
13
21
  permissions:
14
22
  contents: read
15
23
 
@@ -56,12 +64,32 @@ jobs:
56
64
  print(next(p['version'] for p in pkgs if p['name']=='sigit'))")
57
65
  echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
58
66
 
59
- if curl -fsS "https://crates.io/api/v1/crates/sigit/${VERSION}" >/dev/null 2>&1; then
60
- echo "sigit ${VERSION} already exists on crates.io, skipping publish"
61
- echo "exists=true" >> "$GITHUB_OUTPUT"
62
- else
63
- echo "exists=false" >> "$GITHUB_OUTPUT"
64
- fi
67
+ # crates.io answers 403 to requests without a descriptive
68
+ # User-Agent, so curl's default one makes this look like a 404 and
69
+ # the publish below runs into "crate already exists on the index".
70
+ # Branch on the status code rather than on curl's exit status, so an
71
+ # answer we do not understand stops the job instead of quietly
72
+ # degrading into that same failure.
73
+ # Retry first, so a timeout or a 5xx costs a few seconds instead of
74
+ # the whole release.
75
+ STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \
76
+ --retry 3 --retry-delay 2 --retry-all-errors \
77
+ -H "User-Agent: ${GITHUB_REPOSITORY} release workflow (https://github.com/${GITHUB_REPOSITORY})" \
78
+ "https://crates.io/api/v1/crates/sigit/${VERSION}")
79
+
80
+ case "$STATUS" in
81
+ 200)
82
+ echo "sigit ${VERSION} already exists on crates.io, skipping publish"
83
+ echo "exists=true" >> "$GITHUB_OUTPUT"
84
+ ;;
85
+ 404)
86
+ echo "exists=false" >> "$GITHUB_OUTPUT"
87
+ ;;
88
+ *)
89
+ echo "Unexpected ${STATUS} from crates.io while checking whether sigit ${VERSION} is published" >&2
90
+ exit 1
91
+ ;;
92
+ esac
65
93
 
66
94
  - name: Publish to crates.io
67
95
  if: steps.crates-check.outputs.exists != 'true'
@@ -10,6 +10,14 @@ on:
10
10
  description: "Release tag (e.g. v0.1.1)"
11
11
  required: true
12
12
 
13
+ concurrency:
14
+ # A re-pushed or re-dispatched tag fires this a second time. Queue the
15
+ # duplicate behind the original instead of cancelling it: cancelling a run
16
+ # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
17
+ # just hits the "already published" check and exits clean.
18
+ group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
19
+ cancel-in-progress: false
20
+
13
21
  permissions:
14
22
  contents: write
15
23
  actions: write
@@ -7,6 +7,14 @@ on:
7
7
  description: "Release tag (e.g. v0.1.1)"
8
8
  required: true
9
9
 
10
+ concurrency:
11
+ # A re-pushed or re-dispatched tag fires this a second time. Queue the
12
+ # duplicate behind the original instead of cancelling it: cancelling a run
13
+ # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
14
+ # just hits the "already published" check and exits clean.
15
+ group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
16
+ cancel-in-progress: false
17
+
10
18
  permissions:
11
19
  contents: read
12
20
 
@@ -10,6 +10,14 @@ on:
10
10
  description: "Release tag (e.g. v0.1.1)"
11
11
  required: true
12
12
 
13
+ concurrency:
14
+ # A re-pushed or re-dispatched tag fires this a second time. Queue the
15
+ # duplicate behind the original instead of cancelling it: cancelling a run
16
+ # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
17
+ # just hits the "already published" check and exits clean.
18
+ group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
19
+ cancel-in-progress: false
20
+
13
21
  permissions:
14
22
  id-token: write
15
23
  contents: read
@@ -108,8 +116,10 @@ jobs:
108
116
  shell: bash
109
117
  run: npm install -g npm@latest
110
118
 
111
- # Authentication is OIDC: each @getsigit/* package has a Trusted
112
- # Publisher pointing at this workflow, so there is no token to leak.
119
+ # Authentication is OIDC: each @getsigit/* and @smbcloud/* package has a
120
+ # Trusted Publisher pointing at this workflow, so there is no token to
121
+ # leak. @smbcloud is the pre-1.5.5 scope, published alongside @getsigit
122
+ # so that an install made before the rename keeps updating.
113
123
  - name: Publish platform package to NPM
114
124
  shell: bash
115
125
  run: |
@@ -141,13 +151,6 @@ jobs:
141
151
  # Create the package directory
142
152
  mkdir -p "${node_pkg}/bin"
143
153
 
144
- # Generate package.json + README for this platform slice
145
- node ./scripts/render-platform-package.cjs \
146
- "${node_pkg}" \
147
- "${node_version}" \
148
- "${node_os}" \
149
- "${node_arch}"
150
-
151
154
  # Windows binaries carry a .exe extension
152
155
  if [ "${{ matrix.build.OS }}" = "windows-2022" ]; then
153
156
  cargo_bin="${cargo_bin}.exe"
@@ -155,18 +158,36 @@ jobs:
155
158
 
156
159
  cp "../target/${{ matrix.build.TARGET }}/release/${cargo_bin}" "${node_pkg}/bin"
157
160
 
158
- cd "${node_pkg}"
159
-
160
- npm_package_name="@getsigit/${node_pkg}"
161
- if npm view "${npm_package_name}@${node_version}" version >/dev/null 2>&1; then
162
- echo "${npm_package_name}@${node_version} already exists on npm, skipping publish"
163
- exit 0
164
- fi
165
-
166
- npm publish --access public
161
+ # One binary, published under both scopes: re-render package.json and
162
+ # README for each, from the same directory.
163
+ for scope in "@getsigit" "@smbcloud"; do
164
+ node ./scripts/render-platform-package.cjs \
165
+ "${node_pkg}" \
166
+ "${node_version}" \
167
+ "${node_os}" \
168
+ "${node_arch}" \
169
+ "${scope}"
170
+
171
+ npm_package_name="${scope}/${node_pkg}"
172
+
173
+ # `npm view` exits non-zero both when the version is genuinely
174
+ # unpublished and when the registry could not be reached, so treat
175
+ # only an E404 as "not published yet". Anything else stops the job
176
+ # instead of publishing over a version that is already there.
177
+ if view_output=$(npm view "${npm_package_name}@${node_version}" version 2>&1); then
178
+ echo "${npm_package_name}@${node_version} already exists on npm, skipping publish"
179
+ continue
180
+ elif ! grep -q "E404" <<<"${view_output}"; then
181
+ echo "npm view failed while checking ${npm_package_name}@${node_version}:" >&2
182
+ echo "${view_output}" >&2
183
+ exit 1
184
+ fi
185
+
186
+ (cd "${node_pkg}" && npm publish --access public)
187
+ done
167
188
 
168
189
  publish-npm-base:
169
- name: Publish base NPM package (@getsigit/sigit)
190
+ name: Publish base NPM packages (@getsigit/sigit, @smbcloud/sigit)
170
191
  needs: publish-npm-binaries
171
192
  runs-on: ubuntu-latest
172
193
 
@@ -200,25 +221,47 @@ jobs:
200
221
  shell: bash
201
222
  run: npm install -g npm@latest
202
223
 
203
- - name: Publish base package to NPM
224
+ - name: Publish base packages to NPM
204
225
  shell: bash
205
226
  run: |
206
227
  cd npm/sigit
207
228
 
208
- # Rewrite package.json with the correct release version and
209
- # matching optionalDependency versions
210
- node ../scripts/render-main-package.cjs \
211
- "./package.json" \
212
- "${{ env.RELEASE_VERSION }}"
213
-
214
- npm_package_name="@getsigit/sigit"
215
229
  npm_package_version="${{ env.RELEASE_VERSION }}"
216
230
 
217
- if npm view "${npm_package_name}@${npm_package_version}" version >/dev/null 2>&1; then
218
- echo "${npm_package_name}@${npm_package_version} already exists on npm, skipping publish"
219
- exit 0
220
- fi
231
+ # Rewrite package.json with the correct release version and matching
232
+ # optionalDependency versions, then build. The compiled launcher is
233
+ # scope-agnostic — it reads its own package.json at runtime to decide
234
+ # which scope to resolve the platform binary from — so both packages
235
+ # ship the same lib/ and it only needs building once.
236
+ node ../scripts/render-main-package.cjs \
237
+ "./package.json" \
238
+ "${npm_package_version}"
221
239
 
222
240
  npm install
223
241
  npm run build
224
- npm publish --access public
242
+
243
+ # @getsigit/sigit is the canonical package; @smbcloud/sigit is the
244
+ # pre-1.5.5 name, kept publishing so installs predating the rename
245
+ # keep updating. Its template points optionalDependencies at the
246
+ # @smbcloud platform packages.
247
+ for template in "package-main.json.tmpl" "package-compat.json.tmpl"; do
248
+ node ../scripts/render-main-package.cjs \
249
+ "./package.json" \
250
+ "${npm_package_version}" \
251
+ "${template}"
252
+
253
+ npm_package_name=$(node -p "require('./package.json').name")
254
+
255
+ # Same E404-only reading as the platform packages above: a registry
256
+ # failure must not look like an unpublished version.
257
+ if view_output=$(npm view "${npm_package_name}@${npm_package_version}" version 2>&1); then
258
+ echo "${npm_package_name}@${npm_package_version} already exists on npm, skipping publish"
259
+ continue
260
+ elif ! grep -q "E404" <<<"${view_output}"; then
261
+ echo "npm view failed while checking ${npm_package_name}@${npm_package_version}:" >&2
262
+ echo "${view_output}" >&2
263
+ exit 1
264
+ fi
265
+
266
+ npm publish --access public
267
+ done