Skip to content

Commit 06a9eee

Browse files
feat: productionize SLM harness UX
1 parent 965ddfc commit 06a9eee

62 files changed

Lines changed: 5391 additions & 801 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
| Concept | Purpose |
1313
|---------|---------|
1414
| **Pipeline** | YAML-configurable execution graph of 16 phases |
15-
| **Agents** | 15 built-in role experts with SLM-optimized prompts |
15+
| **Agents** | 17 built-in role experts with SLM-optimized prompts |
1616
| **Blocks** | Marketplace-ready YAML packages: pipelines, agents, quality, packs |
1717
| **Stacks** | Provider/model presets (omlx-local, deepseek, openai, …) |
1818
| **Skills** | Claude Code–compatible `SKILL.md` convention packs |
@@ -292,7 +292,7 @@ Slots are user-inserted agent calls around phase anchors:
292292

293293
---
294294

295-
## 5. Built-in Specialists (16 total)
295+
## 5. Built-in Specialists (17 total)
296296

297297
Defined in `pkg/agents/prompts.go` + `pkg/agents/factory.go`:
298298

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MODULE := github.com/UnicoLab/slmcode
22
BIN := slmcode
3-
VERSION ?= 0.14.0
3+
VERSION ?= 0.15.0
44
PREFIX ?= $(HOME)/.local
55
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
66
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

‎README.md‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<p align="center">
1515
<a href="https://unicolab.ai"><img alt="UnicoLab" src="https://img.shields.io/badge/Made%20with%20%E2%99%A5%20by-UnicoLab-0f6e8c?style=flat-square" /></a>
16-
<a href="https://github.com/UnicoLab/smlcode/releases/latest"><img alt="release" src="https://img.shields.io/github/v/release/UnicoLab/smlcode?style=flat-square&color=2dd4bf&label=v0.13.1" /></a>
16+
<a href="https://github.com/UnicoLab/smlcode/releases/latest"><img alt="release" src="https://img.shields.io/github/v/release/UnicoLab/smlcode?style=flat-square&color=2dd4bf&label=v0.15.0" /></a>
1717
<a href="https://github.com/UnicoLab/smlcode/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/UnicoLab/smlcode/ci.yml?branch=main&style=flat-square&label=CI" /></a>
1818
<img alt="go" src="https://img.shields.io/badge/go-1.23+-00ADD8?style=flat-square&logo=go&logoColor=white" />
1919
<img alt="license" src="https://img.shields.io/badge/license-MIT-0ea5e9?style=flat-square" />
@@ -130,15 +130,15 @@ Deep dive: **[docs/PROVIDERS.md](docs/PROVIDERS.md)**
130130
| 💨 **Wave fast-path** | When ALL tasks have clean QA + disk evidence, skip reviewer LLM entirely |
131131
| 🏎️ **`fast_model`** | Dual-model routing — 8B for light agents (reviewer, planner), 30B for heavy (worker, tester) |
132132

133-
### 🧩 Agents (19 specialists)
133+
### 🧩 Agents (17 specialists)
134134

135135
| Agent | Role | Tools |
136136
|-------|------|-------|
137137
| 🧭 `explorer` | Codebase deep-dive | ✅ |
138138
| 🏗️ `architect` | Design structure & components | ❌ |
139139
| 📋 `planner` | High-level execution plan | ❌ |
140140
| ✂️ `splitter` | Break plan into atomic tasks | ❌ |
141-
| 🎤 `interviewer` | Ask clarifying questions (HITL) | ❌ |
141+
| 🎯 `composer` | Assemble task-specific dynamic pipelines | ❌ |
142142
| 🛠️ `worker` | Implement scoped changes | ✅ |
143143
| 🔨 `deep` | Multi-step complex worker | ✅ |
144144
| 👁️ `reviewer` | Self-critic / approve | ❌ |
@@ -148,7 +148,6 @@ Deep dive: **[docs/PROVIDERS.md](docs/PROVIDERS.md)**
148148
| 📝 `context` | Maintain CONTEXT.md | ❌ |
149149
| 📚 `docs` | Read documentation | ✅ |
150150
| 🧠 `memory` | Distill MEMORY.md | ❌ |
151-
| 🎓 `learner` | Wave lessons for future packs | ❌ |
152151
| 🗂️ `coordinator` | Manage board & task flow | ❌ |
153152
| 🎼 `orchestrator` | High-level coordination | ❌ |
154153
| 🚨 `escalate` | Arbitrate max-retry failures | ❌ |
@@ -197,8 +196,8 @@ slmcode studio --port-auto → auto-switch if port is busy
197196

198197
| Gate | Default | What it does |
199198
|------|---------|-------------|
200-
| 🎤 **Clarify** | `auto` | Interview agent asks about language/stack/framework |
201-
| ✅ **Plan approve** | `auto` | Human reviews plan before workers execute |
199+
| 🎤 **Clarify** | `ask` | Interview agent asks about language/stack/framework |
200+
| ✅ **Plan approve** | `ask` | Human reviews plan before workers execute |
202201
| 🔄 **Continue** | `ask` | Ask when retries exhausted — another wave or stop? |
203202
| 🚨 **Escalate** | `ask` | Task hit max retries — retry / re-scope / abort? |
204203
| 🐚 **Shell** | `allow` | Approve shell commands before execution |
@@ -237,7 +236,7 @@ static_quality: true # reject stub/placeholder code
237236
| 🐘 Large-model habit | 🐭 SLMCode approach |
238237
|----------------------|---------------------|
239238
| Stuff the repo into chat | Incremental `.slmcode/*.md` memory |
240-
| One free-form agent | Plan → atomic tasks → 19 specialists |
239+
| One free-form agent | Plan → atomic tasks → 17 specialists |
241240
| Re-scan every turn | Reuse CONTEXT/MEMORY; skip deep explore |
242241
| Hope the model self-corrects | Reviewer ↔ corrector + multipass |
243242
| Opaque progress | Live CLI + Studio SSE stream |
@@ -254,6 +253,9 @@ slmcode init # auto-detects language & applies pack
254253
255254
slmcode # premium TUI
256255
slmcode run -v "add JWT validation"
256+
slmcode compose "add JWT validation" # preview selected phases/agents first
257+
slmcode readiness --fix # apply safe local-model defaults
258+
slmcode status # plan gate, diagnostics, latest composition
257259
slmcode board # live kanban
258260
slmcode studio # http://127.0.0.1:7420
259261
```
@@ -264,7 +266,8 @@ Useful knobs:
264266
slmcode stack list
265267
slmcode stack apply deepseek # switch to DeepSeek
266268
slmcode config set fast_model LFM2.5-8B-A1B-MLX-4bit # speed boost!
267-
slmcode run --parallel 6 --think-passes 2 "refactor auth"
269+
slmcode run --dynamic --parallel 6 --think-passes 2 "refactor auth"
270+
slmcode run --no-dynamic "fix README typo" # force static pipeline for tiny jobs
268271
slmcode config set plan_approve ask # require human plan approval
269272
slmcode blocks apply python # apply Python language pack
270273
```
@@ -280,7 +283,10 @@ slmcode blocks apply python # apply Python language pack
280283
| `agent list` / `agent show` | Inspect agent specialists |
281284
| `blocks list` / `blocks apply` | Browse & apply building blocks |
282285
| `skills list` / `skills new` | Manage skill packs |
286+
| `compose` | Preview dynamic phases, specialists, handoff, and SLM fit |
287+
| `readiness` / `ready` | Score and optionally fix local SLM production settings |
283288
| `run -v` | Full pipeline + live stream |
289+
| `status` | Query, dynamic pipeline, plan gate, diagnostics, board counts |
284290
| `tui` / bare `slmcode` | Premium interactive TUI |
285291
| `chat` | Classic REPL |
286292
| `board` / `watch` | Colored kanban |
@@ -298,9 +304,11 @@ TUI: `/compact`, `/models`, `/mcp`, `/auth`, `/schema`, `/sessions`, `/stats`, `
298304
|---------|-----------|
299305
| ⚡ **Parallel execution** | 6 concurrent paths: workers, QA, critique, review, phases, speculative races |
300306
| 🏎️ **Dual-model** | `fast_model` routes light agents (reviewer, planner) to smaller/faster LLM |
307+
| 🎯 **Dynamic composition** | `compose` previews task-specific phases, agents, slots, handoff, and SLM fit |
301308
| 💨 **Wave fast-path** | Tasks with clean QA + disk evidence skip reviewer LLM entirely |
302309
| 🔄 **QA gate** | Single-round gate, auto-fixes gofmt/ruff, skips when no test files |
303-
| 🧪 **Smart smoke** | Uses `go vet` (instant) when no `*_test.go` files exist |
310+
| 🧪 **Smart smoke** | Uses fast local Go/Python/TypeScript checks when configured |
311+
| 🩺 **Readiness** | Scores provider/model reachability and safe SLM defaults before long runs |
304312
| 📦 **Auto-pack** | Detects `go.mod` / `pyproject.toml` / `package.json` on `init` |
305313

306314
---

‎cmd/slmcode/cmd_compose.go‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,13 @@ With no query, prints the latest saved runtime composition from .slmcode/.`),
6969
}
7070

7171
func readLatestComposition(slmDir string) (composer.Composition, error) {
72-
var comp composer.Composition
73-
path := filepath.Join(slmDir, composer.DynamicFileName)
74-
b, err := os.ReadFile(path)
72+
comp, ok, err := composer.LoadDynamic(slmDir)
7573
if err != nil {
76-
if os.IsNotExist(err) {
77-
return comp, fmt.Errorf("no saved composition at %s — run `slmcode compose \"your task\"` to preview one", path)
78-
}
7974
return comp, err
8075
}
81-
if err := json.Unmarshal(b, &comp); err != nil {
82-
return comp, fmt.Errorf("read latest composition: %w", err)
76+
if !ok {
77+
return comp, fmt.Errorf("no saved composition at %s — run `slmcode compose \"your task\"` to preview one", filepath.Join(slmDir, composer.DynamicFileName))
8378
}
84-
comp.Normalize()
8579
return comp, nil
8680
}
8781

@@ -113,6 +107,16 @@ func formatCompositionCLI(resp compositionCLIResponse) string {
113107
b.WriteString(cli.Warn("dynamic_pipeline is disabled; this is an inspection preview"))
114108
b.WriteString("\n")
115109
}
110+
if hints := compositionSLMFit(resp); len(hints) > 0 {
111+
b.WriteString("\n")
112+
b.WriteString(cli.Bold("SLM Fit"))
113+
b.WriteString("\n")
114+
for _, hint := range hints {
115+
b.WriteString(" - ")
116+
b.WriteString(hint)
117+
b.WriteString("\n")
118+
}
119+
}
116120

117121
if len(c.Handoff) > 0 {
118122
b.WriteString("\n")
@@ -188,6 +192,10 @@ func formatCompositionCLI(resp compositionCLIResponse) string {
188192
return b.String()
189193
}
190194

195+
func compositionSLMFit(resp compositionCLIResponse) []string {
196+
return composer.FitHints(resp.Composition, resp.DynamicEnabled, resp.ModelProfile.ContextLimit)
197+
}
198+
191199
func writeCLIKeyVal(b *strings.Builder, k, v string) {
192200
if strings.TrimSpace(v) == "" {
193201
v = "-"

‎cmd/slmcode/cmd_compose_test.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func TestFormatCompositionCLIShowsAgentsHandoffAndLoop(t *testing.T) {
3838
for _, want := range []string{
3939
"Composition Preview",
4040
"focused Go pipeline",
41+
"SLM Fit",
42+
"3 enabled phases selected",
43+
"2 handoff bullets",
4144
"Touch pkg/foo.go only",
4245
"execute",
4346
"go-worker",
@@ -60,6 +63,49 @@ func TestFormatCompositionCLIWarnsWhenDynamicDisabled(t *testing.T) {
6063
if !strings.Contains(out, "dynamic_pipeline is disabled") {
6164
t.Fatalf("missing disabled warning:\n%s", out)
6265
}
66+
if !strings.Contains(out, "enable dynamic_pipeline") {
67+
t.Fatalf("missing SLM fit dynamic hint:\n%s", out)
68+
}
69+
}
70+
71+
func TestCompositionSLMFitFlagsWeakLocalComposition(t *testing.T) {
72+
resp := compositionCLIResponse{
73+
Mode: "latest",
74+
DynamicEnabled: true,
75+
ModelProfile: config.ModelProfile{ContextLimit: 4096},
76+
Composition: composer.Composition{
77+
Phases: []composer.PhaseChoice{
78+
{ID: "context", Enabled: true},
79+
{ID: "explore", Enabled: true},
80+
{ID: "docs", Enabled: true},
81+
{ID: "architect", Enabled: true},
82+
{ID: "plan", Enabled: true},
83+
{ID: "split", Enabled: true},
84+
{ID: "coord", Enabled: true},
85+
{ID: "execute", Agent: "worker", Enabled: true},
86+
{ID: "test", Agent: "tester", Enabled: true},
87+
{ID: "polish", Enabled: true},
88+
{ID: "memory", Enabled: true},
89+
},
90+
Execute: composer.ExecuteChoice{DefaultRole: "worker", MaxWaves: 5},
91+
Slots: []pipeline.Slot{
92+
{ID: "a"}, {ID: "b"}, {ID: "c"}, {ID: "d"},
93+
},
94+
},
95+
}
96+
out := strings.Join(compositionSLMFit(resp), "\n")
97+
for _, want := range []string{
98+
"11 enabled phases",
99+
"handoff is empty",
100+
"team is empty",
101+
"worker role is generic",
102+
"max_waves=5",
103+
"4 slots",
104+
} {
105+
if !strings.Contains(out, want) {
106+
t.Fatalf("hint missing %q:\n%s", want, out)
107+
}
108+
}
63109
}
64110

65111
func TestReadLatestComposition(t *testing.T) {

‎cmd/slmcode/cmd_core.go‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ Examples:
326326
func statusCmd() *cobra.Command {
327327
return &cobra.Command{
328328
Use: "status",
329-
Short: "Snapshot of query, plan head, board counts",
329+
Short: "Snapshot of query, dynamic pipeline, plan gate, diagnostics, and board counts",
330330
RunE: func(cmd *cobra.Command, args []string) error {
331331
ws, err := openWorkspace()
332332
if err != nil {
@@ -337,6 +337,10 @@ func statusCmd() *cobra.Command {
337337
cli.KeyVal("provider", ws.Config.Provider)
338338
cli.KeyVal("model", ws.Config.Model)
339339
cli.KeyVal("backend", ws.Config.Backend)
340+
fmt.Print(formatPipelineStatus(ws.Config))
341+
if comp := formatLatestCompositionStatus(ws.Config); comp != "" {
342+
fmt.Print(comp)
343+
}
340344
b := ws.Board.Snapshot()
341345
by := b.ByColumn()
342346
fmt.Println()
@@ -350,6 +354,9 @@ func statusCmd() *cobra.Command {
350354
fmt.Println()
351355
q, _ := ws.Store.Read("QUERY.md")
352356
fmt.Println(cli.Dim(q))
357+
if diag := formatLatestRunDiagnostics(ws.Config.SlmDir()); diag != "" {
358+
fmt.Print(diag)
359+
}
353360
return nil
354361
},
355362
}

‎cmd/slmcode/cmd_doctor_test.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/UnicoLab/slmcode/pkg/readiness"
8+
)
9+
10+
func TestFormatDoctorProviderProbeReportsModelAvailability(t *testing.T) {
11+
out := formatDoctorProviderProbe(readiness.Check{
12+
ID: "provider_model",
13+
OK: true,
14+
Message: `omlx model "local-coder" available (2 listed)`,
15+
Latency: 17,
16+
})
17+
for _, want := range []string{"LLM ok", "local-coder", "2 listed", "17 ms"} {
18+
if !strings.Contains(out, want) {
19+
t.Fatalf("output missing %q:\n%s", want, out)
20+
}
21+
}
22+
}
23+
24+
func TestFormatDoctorProviderProbeExplainsMissingModel(t *testing.T) {
25+
out := formatDoctorProviderProbe(readiness.Check{
26+
ID: "provider_model",
27+
OK: false,
28+
Severity: "critical",
29+
Message: `omlx model "missing" not listed; available: other`,
30+
Endpoint: "http://127.0.0.1:8000/v1",
31+
FixLabel: "Select an installed model or update the stack",
32+
FixHint: "Use Settings -> Model Stack, or set model to one of the listed local models.",
33+
})
34+
for _, want := range []string{
35+
"LLM check failed",
36+
"missing",
37+
"endpoint: http://127.0.0.1:8000/v1",
38+
"tip: Use Settings",
39+
"fix: Select an installed model",
40+
} {
41+
if !strings.Contains(out, want) {
42+
t.Fatalf("output missing %q:\n%s", want, out)
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)