sigit-code 1.5.5__tar.gz → 1.5.6__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 (90) hide show
  1. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/AGENTS.md +22 -5
  2. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/sigit-code-release/SKILL.md +15 -4
  3. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/sigit-code-release/SKILL.md +15 -4
  4. {sigit_code-1.5.5 → sigit_code-1.5.6}/AGENTS.md +22 -5
  5. {sigit_code-1.5.5 → sigit_code-1.5.6}/CHANGELOG.md +29 -0
  6. {sigit_code-1.5.5 → sigit_code-1.5.6}/CLAUDE.md +22 -5
  7. {sigit_code-1.5.5 → sigit_code-1.5.6}/Cargo.lock +1 -1
  8. {sigit_code-1.5.5 → sigit_code-1.5.6}/Cargo.toml +1 -1
  9. {sigit_code-1.5.5 → sigit_code-1.5.6}/PKG-INFO +1 -1
  10. {sigit_code-1.5.5 → sigit_code-1.5.6}/docs/mcp.md +5 -3
  11. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/backend.rs +202 -0
  12. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/headless.rs +17 -37
  13. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/main.rs +194 -43
  14. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/provider.rs +18 -5
  15. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/tools.rs +40 -12
  16. {sigit_code-1.5.5 → sigit_code-1.5.6}/tests/acp_permissions.rs +231 -7
  17. {sigit_code-1.5.5 → sigit_code-1.5.6}/tests/headless_mode.rs +59 -0
  18. sigit_code-1.5.5/.github/workflows/release-mcp-registry.yml +0 -79
  19. sigit_code-1.5.5/server.json +0 -39
  20. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
  21. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
  22. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/branding/SKILL.md +0 -0
  23. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/SKILL.md +0 -0
  24. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/driver.mjs +0 -0
  25. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
  26. {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/tool-calling/SKILL.md +0 -0
  27. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
  28. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
  29. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/branding/SKILL.md +0 -0
  30. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/SKILL.md +0 -0
  31. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/driver.mjs +0 -0
  32. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
  33. {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/tool-calling/SKILL.md +0 -0
  34. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/ci.yml +0 -0
  35. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-aur.yml +0 -0
  36. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-crates.yml +0 -0
  37. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-github.yml +0 -0
  38. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-homebrew.yml +0 -0
  39. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-npm.yml +0 -0
  40. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-nuget.yml +0 -0
  41. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-pypi.yml +0 -0
  42. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-scoop.yml +0 -0
  43. {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-winget.yml +0 -0
  44. {sigit_code-1.5.5 → sigit_code-1.5.6}/.gitignore +0 -0
  45. {sigit_code-1.5.5 → sigit_code-1.5.6}/.nvmrc +0 -0
  46. {sigit_code-1.5.5 → sigit_code-1.5.6}/LICENSE +0 -0
  47. {sigit_code-1.5.5 → sigit_code-1.5.6}/README.md +0 -0
  48. {sigit_code-1.5.5 → sigit_code-1.5.6}/docs/hooks.md +0 -0
  49. {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/settings-with-hooks.toml +0 -0
  50. {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/skills/README.md +0 -0
  51. {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/skills/commit-message/SKILL.md +0 -0
  52. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/README.md.tmpl +0 -0
  53. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/package-main.json.tmpl +0 -0
  54. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/package.json.tmpl +0 -0
  55. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/scripts/render-main-package.cjs +0 -0
  56. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/scripts/render-platform-package.cjs +0 -0
  57. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/.gitignore +0 -0
  58. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/README.md +0 -0
  59. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/package.json +0 -0
  60. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/src/index.ts +0 -0
  61. {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/tsconfig.json +0 -0
  62. {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/.gitignore +0 -0
  63. {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/Program.cs +0 -0
  64. {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/README.md +0 -0
  65. {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/SiGit.Code.csproj +0 -0
  66. {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/aur/PKGBUILD.in +0 -0
  67. {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/nfpm.yaml +0 -0
  68. {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
  69. {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
  70. {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
  71. {sigit_code-1.5.5 → sigit_code-1.5.6}/pypi/README.md +0 -0
  72. {sigit_code-1.5.5 → sigit_code-1.5.6}/pypi/pyproject.toml +0 -0
  73. {sigit_code-1.5.5 → sigit_code-1.5.6}/pyproject.toml +0 -0
  74. {sigit_code-1.5.5 → sigit_code-1.5.6}/rust-toolchain.toml +0 -0
  75. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/account.rs +0 -0
  76. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/browser_auth.rs +0 -0
  77. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/chat.rs +0 -0
  78. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/commands.rs +0 -0
  79. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/credentials.rs +0 -0
  80. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/frontmatter.rs +0 -0
  81. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/hooks.rs +0 -0
  82. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/instructions.rs +0 -0
  83. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/mcp.rs +0 -0
  84. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/models.rs +0 -0
  85. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/permissions.rs +0 -0
  86. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/session_store.rs +0 -0
  87. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/settings.rs +0 -0
  88. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/setup.rs +0 -0
  89. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/skills.rs +0 -0
  90. {sigit_code-1.5.5 → sigit_code-1.5.6}/src/subagents.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
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
182
198
  `tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
183
199
  (excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
184
200
  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.
201
+ Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
202
+ (`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
203
+ it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
204
+ `sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
205
+ package listing.
189
206
  - **`src/permissions.rs`** — tool permission policy. Every tool call passes through
190
207
  `decision_for` before executing: read-only tools always run; mutating tools (and all
191
208
  `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
@@ -29,8 +29,13 @@ Use this skill when preparing a release for this repository.
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
31
  - 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.
32
+ - 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
33
  - 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.
34
+ - `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`.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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
39
 
35
40
  ## Git release flow
36
41
 
@@ -42,6 +47,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
42
47
  4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
43
48
  5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
44
49
 
50
+ 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`).
51
+
45
52
  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
53
 
47
54
  ## Typical files to inspect
@@ -54,7 +61,10 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
54
61
  - `npm/scripts/render-main-package.cjs`
55
62
  - `npm/`
56
63
  - `pypi/`
57
- - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`)
64
+ - `nuget/sigit/` (bundled-binary `.NET` tool packaging)
65
+ - `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
66
+ - `server.json` (MCP Registry listing)
67
+ - `.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
68
 
59
69
  ## Release checklist
60
70
 
@@ -67,5 +77,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
67
77
  - 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
78
  - Release notes or changelog entries match the actual changes.
69
79
  - 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.
80
+ - Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
81
+ - Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
82
+ - 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`.
@@ -29,8 +29,13 @@ Use this skill when preparing a release for this repository.
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
31
  - 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.
32
+ - 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
33
  - 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.
34
+ - `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`.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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
39
 
35
40
  ## Git release flow
36
41
 
@@ -42,6 +47,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
42
47
  4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
43
48
  5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
44
49
 
50
+ 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`).
51
+
45
52
  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
53
 
47
54
  ## Typical files to inspect
@@ -54,7 +61,10 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
54
61
  - `npm/scripts/render-main-package.cjs`
55
62
  - `npm/`
56
63
  - `pypi/`
57
- - `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`)
64
+ - `nuget/sigit/` (bundled-binary `.NET` tool packaging)
65
+ - `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
66
+ - `server.json` (MCP Registry listing)
67
+ - `.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
68
 
59
69
  ## Release checklist
60
70
 
@@ -67,5 +77,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
67
77
  - 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
78
  - Release notes or changelog entries match the actual changes.
69
79
  - 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.
80
+ - Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
81
+ - Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
82
+ - 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`.
@@ -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
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
182
198
  `tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
183
199
  (excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
184
200
  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.
201
+ Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
202
+ (`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
203
+ it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
204
+ `sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
205
+ package listing.
189
206
  - **`src/permissions.rs`** — tool permission policy. Every tool call passes through
190
207
  `decision_for` before executing: read-only tools always run; mutating tools (and all
191
208
  `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.6
4
+
5
+ ### What changed
6
+
7
+ - **Switching models mid-conversation no longer restarts it.** Both switching
8
+ to a local GGUF model and picking a different cloud tier now carry the live
9
+ conversation history across, repairing any half-finished tool call so
10
+ strict OpenAI-compatible endpoints don't reject the session
11
+ - **Long-running tools now show progress in ACP clients.** Synchronous tools
12
+ (shell commands, file operations, `read_website`, ...) run on a blocking
13
+ thread pool instead of the ACP connection's own task, so a slow command's
14
+ `in_progress` update reaches the client — and Zed's spinner — while the
15
+ command is still running instead of arriving with the result
16
+ - **Text from consecutive tool rounds no longer runs together.** ACP and
17
+ headless clients concatenate consecutive message chunks, so a reply that
18
+ continued after a tool call used to glue onto the previous round's last
19
+ sentence with no space. A blank line now opens the next round's text
20
+ - **New onde-cloud model tiers.** The model picker (`/models` in the TUI, the
21
+ config panel in editors) now offers five additional siGit Code Cloud tiers —
22
+ `flux`, `apex`, `aura`, `orbit`, and `nova` — alongside the existing Fast,
23
+ Balanced, Large, Mini, Air, Pro, and KKK options. The tier list mirrors
24
+ onde-cloud's router catalogue, so all three sides (the picker, the router,
25
+ and the cloud catalogue) have to be updated together when a tier is added
26
+ - **MCP Registry listing moved to `getsigit/si`.** The `server.json` manifest
27
+ and its `release-mcp-registry.yml` publish workflow now live in the
28
+ [`getsigit/si`](https://github.com/getsigit/si) repo alongside the si CLI's
29
+ other code, since the registry entry describes the si platform rather than
30
+ the coding agent
31
+
3
32
  ## 1.5.5
4
33
 
5
34
  A distribution release. siGit Code itself is unchanged. What moved is how it
@@ -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
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
182
198
  `tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
183
199
  (excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
184
200
  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.
201
+ Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
202
+ (`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
203
+ it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
204
+ `sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
205
+ package listing.
189
206
  - **`src/permissions.rs`** — tool permission policy. Every tool call passes through
190
207
  `decision_for` before executing: read-only tools always run; mutating tools (and all
191
208
  `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
@@ -5778,7 +5778,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
5778
5778
 
5779
5779
  [[package]]
5780
5780
  name = "sigit"
5781
- version = "1.5.5"
5781
+ version = "1.5.6"
5782
5782
  dependencies = [
5783
5783
  "agent-client-protocol",
5784
5784
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sigit"
3
- version = "1.5.5"
3
+ version = "1.5.6"
4
4
  edition = "2024"
5
5
  description = "siGit Code — ACP-compatible AI coding agent. Sí, git."
6
6
  documentation = "https://github.com/getsigit/sigit"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sigit-code
3
- Version: 1.5.5
3
+ Version: 1.5.6
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -63,6 +63,8 @@ The server is published to the
63
63
  [official MCP Registry](https://registry.modelcontextprotocol.io) as
64
64
  **`si.sigit/sigit`**, a remote Streamable-HTTP listing, so registry-aware
65
65
  clients can add it in one click. The registry only stores metadata; the
66
- `server.json` and publish workflow live in this repo, and namespace ownership is
67
- proven by a DNS TXT record on `sigit.si`. The internal setup and release runbook
68
- live with the server (private `sigit-si` repo).
66
+ `server.json` and publish workflow live in the
67
+ [`getsigit/si`](https://github.com/getsigit/si) repo (the `si` CLI, also an
68
+ MCP client of this server), and namespace ownership is proven by a DNS TXT
69
+ record on `sigit.si`. The internal setup and release runbook live with the
70
+ server (private `sigit-si` repo).
@@ -880,6 +880,89 @@ struct StreamFunctionDelta {
880
880
  arguments: Option<String>,
881
881
  }
882
882
 
883
+ // ── Carrying a conversation across a model switch ────────────────────────────
884
+
885
+ /// The part of a history snapshot that survives a model switch: every
886
+ /// non-system message, with half-finished tool plumbing repaired.
887
+ ///
888
+ /// System messages are dropped because the backend being switched *to* seeds
889
+ /// its own — a different model's prompt, or a freshly pushed session-context
890
+ /// message. A switch can land mid-turn, after the assistant asked for a tool
891
+ /// but before the results came back; a tool call with no matching result (or a
892
+ /// result with no matching call) makes strict OpenAI-compatible endpoints
893
+ /// reject every later request in the session, so those halves are stripped
894
+ /// instead of carried.
895
+ pub fn carryover_history(snapshot: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
896
+ let answered: std::collections::HashSet<String> = snapshot
897
+ .iter()
898
+ .filter(|message| message["role"] == "tool")
899
+ .filter_map(|message| message["tool_call_id"].as_str().map(str::to_string))
900
+ .collect();
901
+
902
+ let mut carried: Vec<serde_json::Value> = Vec::with_capacity(snapshot.len());
903
+ let mut called: std::collections::HashSet<String> = std::collections::HashSet::new();
904
+
905
+ for mut message in snapshot {
906
+ match message["role"].as_str().unwrap_or_default() {
907
+ "system" => continue,
908
+ "assistant" => {
909
+ if let Some(calls) = message["tool_calls"].as_array() {
910
+ let kept: Vec<serde_json::Value> = calls
911
+ .iter()
912
+ .filter(|call| call["id"].as_str().is_some_and(|id| answered.contains(id)))
913
+ .cloned()
914
+ .collect();
915
+ if kept.is_empty() {
916
+ // Only unanswered requests: keep whatever text came
917
+ // with them, drop the message if there was none.
918
+ if let Some(object) = message.as_object_mut() {
919
+ object.remove("tool_calls");
920
+ }
921
+ if message["content"].as_str().unwrap_or_default().is_empty() {
922
+ continue;
923
+ }
924
+ } else {
925
+ for call in &kept {
926
+ called.insert(call["id"].as_str().unwrap_or_default().to_string());
927
+ }
928
+ message["tool_calls"] = serde_json::Value::Array(kept);
929
+ }
930
+ }
931
+ carried.push(message);
932
+ }
933
+ "tool" => {
934
+ let answers_a_kept_call = message["tool_call_id"]
935
+ .as_str()
936
+ .is_some_and(|id| called.contains(id));
937
+ if answers_a_kept_call {
938
+ carried.push(message);
939
+ }
940
+ }
941
+ _ => carried.push(message),
942
+ }
943
+ }
944
+
945
+ carried
946
+ }
947
+
948
+ /// Replay `carried` (from [`carryover_history`]) into `backend`, on top of the
949
+ /// system messages `backend` seeded for itself. Used when a model switch
950
+ /// installs a new backend — or reloads the on-device engine, which wipes its
951
+ /// history — so the thread continues under the new model instead of restarting.
952
+ pub async fn adopt_carryover(backend: &dyn InferenceBackend, carried: Vec<serde_json::Value>) {
953
+ if carried.is_empty() {
954
+ return;
955
+ }
956
+ let mut rebuilt: Vec<serde_json::Value> = backend
957
+ .history_snapshot()
958
+ .await
959
+ .into_iter()
960
+ .take_while(|message| message["role"] == "system")
961
+ .collect();
962
+ rebuilt.extend(carried);
963
+ backend.restore_history(rebuilt).await;
964
+ }
965
+
883
966
  #[cfg(test)]
884
967
  mod tests {
885
968
  use super::*;
@@ -968,6 +1051,125 @@ mod tests {
968
1051
  assert_eq!(last["content"], "cancelled by the user");
969
1052
  }
970
1053
 
1054
+ #[test]
1055
+ fn carryover_drops_system_messages_and_keeps_the_turns() {
1056
+ let snapshot = vec![
1057
+ serde_json::json!({ "role": "system", "content": "old prompt" }),
1058
+ serde_json::json!({ "role": "user", "content": "hello" }),
1059
+ serde_json::json!({ "role": "assistant", "content": "hi" }),
1060
+ ];
1061
+
1062
+ let carried = carryover_history(snapshot);
1063
+
1064
+ assert_eq!(carried.len(), 2);
1065
+ assert_eq!(carried[0]["role"], "user");
1066
+ assert_eq!(carried[1]["content"], "hi");
1067
+ }
1068
+
1069
+ #[test]
1070
+ fn carryover_strips_a_tool_call_that_never_got_a_result() {
1071
+ // Switching mid-turn: the assistant asked for a tool, nothing answered.
1072
+ let snapshot = vec![
1073
+ serde_json::json!({ "role": "user", "content": "read a.rs" }),
1074
+ streamed_assistant_history(
1075
+ "on it",
1076
+ &[ToolCall {
1077
+ id: "call_1".to_string(),
1078
+ name: "read_file".to_string(),
1079
+ arguments: "{}".to_string(),
1080
+ }],
1081
+ ),
1082
+ ];
1083
+
1084
+ let carried = carryover_history(snapshot);
1085
+
1086
+ assert_eq!(carried.len(), 2);
1087
+ assert_eq!(carried[1]["content"], "on it");
1088
+ assert!(
1089
+ carried[1].get("tool_calls").is_none(),
1090
+ "an unanswered tool call must not survive the switch"
1091
+ );
1092
+ }
1093
+
1094
+ #[test]
1095
+ fn carryover_drops_a_textless_unanswered_tool_call_and_its_late_result() {
1096
+ let snapshot = vec![
1097
+ streamed_assistant_history(
1098
+ "",
1099
+ &[ToolCall {
1100
+ id: "call_1".to_string(),
1101
+ name: "read_file".to_string(),
1102
+ arguments: "{}".to_string(),
1103
+ }],
1104
+ ),
1105
+ // An orphan: its assistant message is gone with the line above.
1106
+ serde_json::json!({
1107
+ "role": "tool",
1108
+ "tool_call_id": "call_other",
1109
+ "content": "file contents",
1110
+ }),
1111
+ ];
1112
+
1113
+ assert!(carryover_history(snapshot).is_empty());
1114
+ }
1115
+
1116
+ #[test]
1117
+ fn carryover_keeps_an_answered_tool_call_paired_with_its_result() {
1118
+ let snapshot = vec![
1119
+ streamed_assistant_history(
1120
+ "",
1121
+ &[ToolCall {
1122
+ id: "call_1".to_string(),
1123
+ name: "read_file".to_string(),
1124
+ arguments: "{}".to_string(),
1125
+ }],
1126
+ ),
1127
+ serde_json::json!({
1128
+ "role": "tool",
1129
+ "tool_call_id": "call_1",
1130
+ "content": "file contents",
1131
+ }),
1132
+ ];
1133
+
1134
+ let carried = carryover_history(snapshot);
1135
+
1136
+ assert_eq!(carried.len(), 2);
1137
+ assert_eq!(carried[0]["tool_calls"][0]["id"], "call_1");
1138
+ assert_eq!(carried[1]["tool_call_id"], "call_1");
1139
+ }
1140
+
1141
+ #[tokio::test]
1142
+ async fn adopt_carryover_replays_the_thread_under_the_new_system_prompt() {
1143
+ let new_backend =
1144
+ OpenAiBackend::new("http://localhost", "", "m", Some("new prompt".into()));
1145
+
1146
+ let carried = carryover_history(vec![
1147
+ serde_json::json!({ "role": "system", "content": "old prompt" }),
1148
+ serde_json::json!({ "role": "user", "content": "hello" }),
1149
+ serde_json::json!({ "role": "assistant", "content": "hi" }),
1150
+ ]);
1151
+ adopt_carryover(&new_backend, carried).await;
1152
+
1153
+ let history = new_backend.history_snapshot().await;
1154
+ assert_eq!(history.len(), 3);
1155
+ assert_eq!(history[0]["role"], "system");
1156
+ assert_eq!(history[0]["content"], "new prompt");
1157
+ assert_eq!(history[1]["content"], "hello");
1158
+ assert_eq!(history[2]["content"], "hi");
1159
+ }
1160
+
1161
+ #[tokio::test]
1162
+ async fn adopt_carryover_leaves_a_fresh_backend_alone() {
1163
+ let new_backend =
1164
+ OpenAiBackend::new("http://localhost", "", "m", Some("new prompt".into()));
1165
+
1166
+ adopt_carryover(&new_backend, Vec::new()).await;
1167
+
1168
+ let history = new_backend.history_snapshot().await;
1169
+ assert_eq!(history.len(), 1);
1170
+ assert_eq!(history[0]["content"], "new prompt");
1171
+ }
1172
+
971
1173
  #[test]
972
1174
  fn estimate_tokens_scales_with_serialized_size() {
973
1175
  assert_eq!(estimate_tokens(&[]), 0);
@@ -228,16 +228,12 @@ async fn run_prompt(
228
228
  // sink is passed and only the final message is printed.
229
229
  let (sink, mut sink_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
230
230
  let sink_opt = if config.quiet { None } else { Some(&sink) };
231
- let mut assembled = String::new();
232
- let mut sent = String::new();
233
- let mut streamed_any = false;
231
+ let mut reply = crate::StreamedReply::default();
234
232
 
235
233
  let mut result = drain_to_stdout(
236
234
  backend.send_message_with_tools(&config.prompt, &tools, sink_opt),
237
235
  &mut sink_rx,
238
- &mut assembled,
239
- &mut sent,
240
- &mut streamed_any,
236
+ &mut reply,
241
237
  )
242
238
  .await?;
243
239
 
@@ -300,12 +296,14 @@ async fn run_prompt(
300
296
  None // last round: force text
301
297
  };
302
298
 
299
+ // Whatever this round says starts a new paragraph rather than
300
+ // continuing the sentence the tool calls interrupted.
301
+ reply.interrupt();
302
+
303
303
  result = drain_to_stdout(
304
304
  backend.send_tool_results(tool_results, next_tools, sink_opt),
305
305
  &mut sink_rx,
306
- &mut assembled,
307
- &mut sent,
308
- &mut streamed_any,
306
+ &mut reply,
309
307
  )
310
308
  .await?;
311
309
  }
@@ -318,7 +316,7 @@ async fn run_prompt(
318
316
  if !final_visible.is_empty() {
319
317
  let _ = writeln!(stdout, "{final_visible}");
320
318
  }
321
- } else if streamed_any {
319
+ } else if reply.streamed_any {
322
320
  // The reply is already on stdout; end the line for the shell.
323
321
  let _ = writeln!(stdout);
324
322
  } else if !final_visible.is_empty() {
@@ -335,9 +333,7 @@ async fn run_prompt(
335
333
  async fn drain_to_stdout<F>(
336
334
  fut: F,
337
335
  sink_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
338
- assembled: &mut String,
339
- sent: &mut String,
340
- streamed_any: &mut bool,
336
+ reply: &mut crate::StreamedReply,
341
337
  ) -> Result<TurnResult, backend::BackendError>
342
338
  where
343
339
  F: std::future::Future<Output = Result<TurnResult, backend::BackendError>>,
@@ -347,39 +343,23 @@ where
347
343
  tokio::select! {
348
344
  done = &mut fut => break done,
349
345
  Some(piece) = sink_rx.recv() => {
350
- emit_visible_chunk(&piece, assembled, sent, streamed_any);
346
+ emit_visible_chunk(&piece, reply);
351
347
  }
352
348
  }
353
349
  };
354
350
  // Flush tokens that landed between the last poll and the future resolving.
355
351
  while let Ok(piece) = sink_rx.try_recv() {
356
- emit_visible_chunk(&piece, assembled, sent, streamed_any);
352
+ emit_visible_chunk(&piece, reply);
357
353
  }
358
354
  result
359
355
  }
360
356
 
361
- /// Append a streamed fragment, strip `<think>` reasoning from the running
362
- /// text, and print only the newly revealed visible suffix. Tracking the
363
- /// assembled text (not just deltas) keeps think-block stripping correct even
364
- /// when a tag spans chunk boundaries — same approach as the ACP path.
365
- fn emit_visible_chunk(
366
- piece: &str,
367
- assembled: &mut String,
368
- sent: &mut String,
369
- streamed_any: &mut bool,
370
- ) {
371
- assembled.push_str(piece);
372
- let (_think, visible) = crate::chat::strip_think_blocks(assembled);
373
- match visible.strip_prefix(sent.as_str()) {
374
- Some(extra) if !extra.is_empty() => {
375
- print!("{extra}");
376
- let _ = std::io::stdout().flush();
377
- *sent = visible;
378
- *streamed_any = true;
379
- }
380
- // No new visible text, or the visible prefix changed retroactively
381
- // (rare, e.g. a late-closing think tag): resync without reprinting.
382
- _ => *sent = visible,
357
+ /// Fold a streamed fragment into `reply` and print whatever it newly reveals —
358
+ /// the stdio counterpart of `SiGitAgent::emit_visible_chunk`.
359
+ fn emit_visible_chunk(piece: &str, reply: &mut crate::StreamedReply) {
360
+ if let Some(extra) = reply.push(piece) {
361
+ print!("{extra}");
362
+ let _ = std::io::stdout().flush();
383
363
  }
384
364
  }
385
365