Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ jobs:
cache-dependency-path: web/package-lock.json

# `make web-deps`, not a bare `npm ci`: `npm ci` refuses to run at all when
# web/package-lock.json does not match web/package.json, and the lock in this
# tree predates the vitest/eslint devDependencies. web-deps tries `npm ci`
# first and falls back to `npm install`, printing the real fix either way.
# web/package-lock.json does not match web/package.json. web-deps tries
# `npm ci` first, reports any failure, then falls back to `npm install`.
# Dependency changes must include the updated lockfile.
- name: Install web deps
run: make web-deps

Expand Down Expand Up @@ -125,9 +125,9 @@ jobs:
cache-dependency-path: web/package-lock.json

# `make web-deps`, not a bare `npm ci`: `npm ci` refuses to run at all when
# web/package-lock.json does not match web/package.json, and the lock in this
# tree predates the vitest/eslint devDependencies. web-deps tries `npm ci`
# first and falls back to `npm install`, printing the real fix either way.
# web/package-lock.json does not match web/package.json. web-deps tries
# `npm ci` first, reports any failure, then falls back to `npm install`.
# Dependency changes must include the updated lockfile.
- name: Install web deps
run: make web-deps

Expand Down Expand Up @@ -164,9 +164,9 @@ jobs:
python-version: "3.12"

# `make web-deps`, not a bare `npm ci`: `npm ci` refuses to run at all when
# web/package-lock.json does not match web/package.json, and the lock in this
# tree predates the vitest/eslint devDependencies. web-deps tries `npm ci`
# first and falls back to `npm install`, printing the real fix either way.
# web/package-lock.json does not match web/package.json. web-deps tries
# `npm ci` first, reports any failure, then falls back to `npm install`.
# Dependency changes must include the updated lockfile.
- name: Install web deps
run: make web-deps

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Go, SLM-first coding harness. Deeper: `CONTRIBUTING.md`, `docs/`.

## Build & test

- `make bootstrap` — npm deps + Studio UI build into `cmd/slmcode/ui/`. `go build` alone embeds no SPA; the server then serves a placeholder page (`pkg/server/placeholder.go`) and `slmcode studio` warns on startup. `web/package-lock.json` is **stale**, so `npm ci` fails and bootstrap falls back to `npm install`, which regenerates the lock — **commit it**.
- `make bootstrap` — npm deps + Studio UI build into `cmd/slmcode/ui/`. `go build` alone embeds no SPA; the server then serves a placeholder page (`pkg/server/placeholder.go`) and `slmcode studio` warns on startup. Keep `web/package-lock.json` in sync with `web/package.json`. If `npm ci` fails, inspect the error; bootstrap can fall back to `npm install`. **Commit any resulting lockfile changes** alongside dependency updates.
- `make check` — the single gate: `tidy-check`, gofmt, vet, lint, `cover` (`go test ./...` + the coverage floor), `-race ./pkg/...`, `web-check` (lint + typecheck:test + test + build). CI runs this. The two steps that need the network (module proxy, npm registry) **skip with a named reason** instead of failing, so it runs offline.
- Lint baseline is **zero**: `make lint` (and `make lint-strict`, its alias) fail on any finding. Fix it, or add `//nolint:<linter> // <why this site is a false positive>`.
- `make e2e` offline; `RUN_E2E=1 make e2e` adds live-model tests. Two suites run under plain `make test` and are the ones to keep green: `test/e2e/harness_smoke_test.go` drives the harness in-process against a fake OpenAI server, and `test/e2e/binary_acceptance_test.go` builds the **real binary** plus `test/fakemodel` and drives `init → doctor → run → task show → diff → apply` against a Go and a TypeScript fixture, asserting the bytes on disk.
Expand Down
26 changes: 8 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ make bootstrap # installs web/ npm deps and builds the Studio UI → cmd/
make build # → ./bin/slmcode
```

Needs Go 1.23+. `make bootstrap` additionally needs Node.js 22+ on your PATH — nothing else
Needs Go 1.25+ (the module selects the Go 1.26.7 toolchain). `make bootstrap` additionally needs Node.js 22.22.2+ or 24.15+ on your PATH — nothing else
in SLMCode does; it fails with an actionable message if `npm` is missing. `make install-user`
puts the binary in `~/.local/bin`; `make install-system` installs system-wide.

### How the Studio UI gets into the binary

The Studio SPA is React 18 + Vite + TypeScript in `web/`. `make ui-react` builds it and copies
The Studio SPA is React 19 + Vite + TypeScript in `web/`. `make ui-react` builds it and copies
`web/dist/*` into `cmd/slmcode/ui/`, which `cmd/slmcode/root.go` embeds with
`//go:embed all:ui`. `make bootstrap` is `web-deps` + `ui-react`: it always ensures the npm
dependencies are current and then builds. It is a bootstrap, not a cache check — it used to
Expand All @@ -41,23 +41,13 @@ command — and `slmcode studio` says the same thing on startup. Both use one pr
So: `go build` alone always works and produces a usable binary (CLI, TUI and the whole Studio API
are unaffected); only the web page is missing until you run `make bootstrap`.

### `web/package-lock.json` is currently out of date — and how that is fixed
### Keep the web lockfile reproducible

`web/package.json` gained `vitest`, `@testing-library/*`, `eslint` and the rest of the test
toolchain, and **`web/package-lock.json` predates them**. `npm ci` installs strictly from the
lock and refuses to run at all when the two disagree:

```
npm ci can only install packages when your package.json and package-lock.json are in sync
```

`make bootstrap` handles this: `scripts/web-deps.sh` tries `npm ci`, and on failure explains why
and falls back to **`npm install`**, which resolves from `package.json` and **rewrites
`web/package-lock.json`**.

> **Commit the regenerated `web/package-lock.json`.** That is the actual fix. Until it lands,
> every clone and every CI run pays for the fallback; once it does, `npm ci` works again and is
> both faster and reproducible.
`web/package-lock.json` must match `web/package.json`. `npm ci` installs exactly
that lockfile. If dependency edits cause a mismatch, run `npm install` in `web/`
and commit the updated lockfile with the manifest. `make bootstrap` reports an
`npm ci` failure and falls back to `npm install`; inspect that output instead of
assuming the failure is always a lockfile mismatch.

### Test files never block the app build

Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ test: ## Run unit tests
go test ./...

race: ## Run unit tests under the Go race detector (pkg/... — the engine core)
go test -race -count=1 ./pkg/...
go test -race -count=1 -timeout 30m ./pkg/...

# The integration suite races the parts unit tests cannot: a full run has
# parallel workers and background probes emitting while the run goroutine
Expand All @@ -347,11 +347,16 @@ cover: ## Run tests with coverage and fail if total coverage drops below the flo
@./scripts/coverage-check.sh

govulncheck: ## Scan all packages for known vulnerabilities
@if ! command -v govulncheck >/dev/null 2>&1; then \
@set -e; \
task_vuln_tool="$$(command -v govulncheck || true)"; \
if [ -z "$$task_vuln_tool" ]; then \
echo "govulncheck not found — installing (go install golang.org/x/vuln/cmd/govulncheck@latest)…"; \
go install golang.org/x/vuln/cmd/govulncheck@latest; \
fi
govulncheck ./...
task_go_bin="$$(go env GOBIN)"; \
if [ -z "$$task_go_bin" ]; then task_go_bin="$$(go env GOPATH)/bin"; fi; \
task_vuln_tool="$$task_go_bin/govulncheck"; \
fi; \
"$$task_vuln_tool" ./...

e2e: ## Run e2e tests (set RUN_E2E=1 for live oMLX tests)
go test ./test/e2e/ -count=1 -timeout 30m
Expand Down
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,17 @@ Details: **[docs/install-offline.md](docs/install-offline.md)**

```bash
git clone https://github.com/UnicoLab/smlcode.git && cd smlcode
make bootstrap # needs Node 22+: installs web/ deps and builds the Studio UI in
make bootstrap # needs Node 22.22.2+ or 24.15+: installs web/ deps and builds the Studio UI in
make install-user # → ~/.local/bin/slmcode
```

`make bootstrap` is the one step that needs Node. It installs `web/`'s npm dependencies and runs
the Vite build into `cmd/slmcode/ui/`, which is `go:embed`ed into the binary. Note that
`web/package-lock.json` is currently out of date with `web/package.json`, so `npm ci` cannot run;
`make bootstrap` says so and falls back to `npm install`, which regenerates the lock — **commit
the regenerated `web/package-lock.json`**. Full story: [CONTRIBUTING.md](CONTRIBUTING.md#build).
the Vite build into `cmd/slmcode/ui/`, which is `go:embed`ed into the binary.
Keep `web/package-lock.json` in sync with `web/package.json`; commit any lockfile
changes after updating dependencies. Full story: [CONTRIBUTING.md](CONTRIBUTING.md#build).

Explore the [feature index](docs/features.md) for the full documentation map,
including [external decision models](docs/laya.md).

No Node? `go build ./cmd/slmcode` works on its own — everything except the Studio SPA. The binary
then serves a built-in placeholder page that tells you to run `make bootstrap`, and `slmcode
Expand Down Expand Up @@ -185,9 +187,9 @@ Go, Python, JavaScript and JSON and return **in-band**. Full reference with fail

### Specialists

20 built-in roles: `coordinator`, `orchestrator`, `context`, `explorer`, `docs`, `architect`,
22 built-in roles: `coordinator`, `orchestrator`, `context`, `explorer`, `docs`, `architect`,
`planner`, `splitter`, `worker`, `deep`, `reviewer`, `reviewer-strict`, `corrector`, `tester`,
`placeholder`, `escalate`, `memory`, `composer`, `describer`, `editor`.
`placeholder`, `escalate`, `memory`, `manager`, `triage`, `composer`, `describer`, `editor`.

`describer`/`editor` is the architect/editor split: the describer reasons in prose with no
tools and no format constraints, the editor only formats, with constrained decoding and tools.
Expand All @@ -198,13 +200,13 @@ Custom and per-language specialists come from YAML blocks. → **[docs/agents.md

| Kind | Purpose | Built-in |
|---|---|---|
| **Pack** | Composes pipeline + quality + agents + skills | 13 |
| **Pipeline** | Phase graph with language-specific slots | 13 |
| **Agent** | Custom specialist or built-in override | 35 (`go-worker`, `ts-reviewer`, `kotlin-tester`, …) |
| **Quality** | Lint/test/build commands per language | 13 |
| **Pack** | Composes pipeline + quality + agents + skills | 15 |
| **Pipeline** | Phase graph with language-specific slots | 15 |
| **Agent** | Custom specialist or built-in override | 46 (`go-worker`, `ts-reviewer`, `kotlin-tester`, …) |
| **Quality** | Lint/test/build commands per language | 15 |

The thirteen packs: `go`, `python`, `react`, `typescript`, `web`, `rust`, `java`, `kotlin`,
`dotnet`, `ruby`, `php`, `swift`, `cpp`. Also shipped: 29 skills and 13 provider stacks.
The fifteen packs: `go`, `python`, `react`, `typescript`, `web`, `rust`, `java`, `kotlin`,
`dotnet`, `ruby`, `php`, `swift`, `cpp`, `shadcn`, `untitledui`. Also shipped: 32 skills and 13 provider stacks.

`slmcode init` picks the pack for you. Detection is scored, not first-match: a marker file in the
root counts, a `detect.contains` proof of the file's *content* counts more, stray source files
Expand Down
34 changes: 20 additions & 14 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Cutting a SLMCode release

The exact sequence for shipping a version, written for the person running it. It assumes
a clean checkout of `main`, push access to `UnicoLab/smlcode`, Go, Node 18+ and `make`.
a clean checkout of `main`, push access to `UnicoLab/smlcode`, Go 1.25+ with the module-selected toolchain, Node 22.22.2+ or 24.15+ and `make`.

The short version: **you tag; CI does the rest.** Everything between the tag landing and
the release appearing is `.github/workflows/release.yml`. Your job is to make sure the
Expand All @@ -28,23 +28,17 @@ git switch main && git pull --ff-only
git status --porcelain # must be empty
```

**Regenerate the web lockfile if it is stale.** `web/package-lock.json` currently predates
several `devDependencies` in `web/package.json`, so a strict `npm ci` refuses to run. CI
falls back to `npm install` and still builds, but the lockfile is the fix:

```bash
cd web && npm install && cd ..
git diff --stat web/package-lock.json
# if it changed:
git add web/package-lock.json && git commit -m "chore(web): regenerate package-lock.json"
```
**Verify reproducible dependencies.** `web/package-lock.json` must match
`web/package.json`. Run `npm ci` in `web/`; if dependency changes require
`npm install`, inspect and commit the lockfile update before releasing.

**Build the Studio UI and run the full gate.** `make bootstrap` is what puts the real SPA
into `cmd/slmcode/ui/`; without it every binary you build locally serves the placeholder
page from `pkg/server`.

```bash
make bootstrap # npm deps + vite build + sync into cmd/slmcode/ui/
make docs-build # strict navigation and documentation build
make check # gofmt, vet, golangci-lint (0 issues), coverage floor, -race, web lint+build
./scripts/check-version.sh # version.go == Makefile == Formula
./scripts/check-repo-refs.sh # no UnicoLab/slmcode download URLs
Expand All @@ -60,6 +54,18 @@ ls cmd/slmcode/ui/assets/*.js # must list at least one bundle
# the real Studio, not "Studio not built"
```

**Complete the runtime qualification.** Run `make e2e-release` against the
actual coding provider/checkpoint used for the release. For an enabled decision
provider, follow `docs/laya.md` and run its live smoke test. Record the exact
server/model, results and any skipped checks; a green fake-server suite does not
establish real-model quality. Also run `make govulncheck` and review findings.
`make check` can explicitly skip missing tooling/network checks; a release must
complete those checks in a provisioned environment.

**Confirm documentation coverage.** Walk `docs/features.md`, compare the binary's
`--help` and `config schema` output with the CLI/config guides, and verify the
setup and fallback instructions for changed features.

**Confirm the changelog is written.** `docs/changelog.md` needs a real `## vX.Y.Z` entry
with a **Breaking behaviour changes** section, cross-linked to `docs/migration.md`. The
generated commit-subject dump is a fallback for patch releases, not a substitute.
Expand Down Expand Up @@ -147,9 +153,9 @@ The `bump` dropdown forces the level when you disagree with the commits:

| Commits since the last tag | Version chosen |
|---|---|
| any `feat!:` / `BREAKING CHANGE:` | major — but **held inside 0.x** (0.27.1 → 0.27.1) |
| any `feat:` | minor (0.27.1 → 0.27.1) |
| only `fix:` / `perf:` | patch (0.27.1 → 0.24.1) |
| any `feat!:` / `BREAKING CHANGE:` | major policy, held to a minor bump while on 0.x |
| any `feat:` | minor |
| only `fix:` / `perf:` | patch |
| only `chore:`, `docs:`, `ci:` … | patch |
| subjects that are not conventional at all | counted, but they drive nothing |

Expand Down
12 changes: 10 additions & 2 deletions cmd/slmcode/cmd_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,12 @@ is present — never to a local port that merely might be a model server.`,

if !res.Found {
if asJSON {
return emitJSON(map[string]any{
if err := emitJSON(map[string]any{
"ok": false, "reason": res.NothingFound(), "tried": findingsJSON(res),
})
}); err != nil {
return err
}
return failf(1, "%s", res.NothingFound())
}
fmt.Println()
return failf(1, "%s", res.NothingFound())
Expand All @@ -131,6 +134,11 @@ is present — never to a local port that merely might be a model server.`,
}

if asJSON {
if !dryRun {
if err := writeChoice(cfg, choice, toUser); err != nil {
return err
}
}
return emitJSON(map[string]any{
"ok": true, "choice": choice, "tried": findingsJSON(res),
"written": !dryRun, "scope": scopeName(toUser),
Expand Down
37 changes: 37 additions & 0 deletions cmd/slmcode/cmd_configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,40 @@ func TestConfigureRejectsABadTimeout(t *testing.T) {
t.Error("an unparsable timeout must be refused rather than ignored")
}
}

func TestConfigureJSONPersistsBeforeReportingWritten(t *testing.T) {
root := configureWorkspace(t)
srv := modelServer(t, "Qwen2.5-Coder-14B-Instruct")
if _, err := runConfigure(t, "--json", "--yes", "--endpoint", srv.URL+"/v1"); err != nil {
t.Fatal(err)
}
cfg, err := config.Load(root)
if err != nil {
t.Fatal(err)
}
if cfg.Endpoint != srv.URL+"/v1" || cfg.Model != "Qwen2.5-Coder-14B-Instruct" {
t.Fatal("JSON success did not persist its choice")
}
}

func TestConfigureJSONFailureIsNonzeroAndDoesNotWrite(t *testing.T) {
root := configureWorkspace(t)
before, err := os.ReadFile(filepath.Join(root, ".slmcode", "config.yaml"))
if err != nil {
t.Fatal(err)
}
srv := modelServer(t)
endpoint := srv.URL + "/v1"
srv.Close()
_, err = runConfigure(t, "--json", "--yes", "--endpoint", endpoint, "--timeout", "100ms")
if err == nil || exitCodeFor(err) == 0 {
t.Fatal("JSON failure exited successfully")
}
after, err := os.ReadFile(filepath.Join(root, ".slmcode", "config.yaml"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, after) {
t.Fatal("failed discovery changed config")
}
}
Loading
Loading