fix: update test snapshots for queryInstruction and complete /sf prefix Phase 2 deprecation
- Fix memory-embeddings-llm-gateway tests: add queryInstruction field to
expected config objects after loadGatewayConfigFromEnv was updated to
return it
- Add STYLEGUIDE.md: SF code standards adapted from ace-coder patterns
(purpose doctrine, principles, anti-patterns STY001-012, thresholds,
naming, patterns, documentation sections)
- Phase 2 /sf prefix removal: update all web components, browser dispatch,
and tests to use direct commands (/autonomous, /stop, /next, /discuss,
/init, /new-milestone) instead of /sf-prefixed forms
- workflow-actions.ts: all command strings updated
- chat-mode.tsx: SF_ACTIONS array updated
- project-welcome.tsx: primaryCommand values updated
- command-surface.tsx: fallback display updated
- remaining-command-panels.tsx: usage examples updated
- browser-slash-command-dispatch.ts: add stop/new-milestone/init to
SF_PASSTHROUGH_COMMANDS so they route correctly to the extension
- recovery-diagnostics-service.ts: suggestion commands updated
- welcome-screen.ts: hint text updated
- All affected tests updated to match new command strings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e4c951ff0c
commit
22cbd83675
14 changed files with 354 additions and 76 deletions
271
STYLEGUIDE.md
Normal file
271
STYLEGUIDE.md
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
# SF Code Standards
|
||||||
|
|
||||||
|
Code patterns for AI-assisted development. Full rules: [AGENTS.md](AGENTS.md) · Planning contract: [docs/adr/0000-purpose-to-software-compiler.md](docs/adr/0000-purpose-to-software-compiler.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Index
|
||||||
|
|
||||||
|
Agent-facing docs are for model consumption first: terse, structured, low-ceremony. Compress wording, not semantics — never remove purpose, value, consumer, consequence, invariants, or action thresholds to save tokens.
|
||||||
|
|
||||||
|
| Section | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| [1. Purpose Doctrine](#1-purpose-doctrine) | The #1 rule: every symbol must answer why it exists |
|
||||||
|
| [2. Principles](#2-principles) | Core coding principles |
|
||||||
|
| [3. Anti-Patterns](#3-anti-patterns) | Blocked patterns and required replacements |
|
||||||
|
| [4. Thresholds](#4-thresholds) | Code quality limits |
|
||||||
|
| [5. Naming](#5-naming) | Naming conventions |
|
||||||
|
| [6. Patterns](#6-patterns) | Architectural patterns |
|
||||||
|
| [7. Documentation](#7-documentation) | JSDoc / comment standards |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose Doctrine
|
||||||
|
|
||||||
|
**Purpose is the most important thing in any symbol.**
|
||||||
|
|
||||||
|
Every exported function, class, constant, and module must answer:
|
||||||
|
|
||||||
|
- **why** it exists (not what it does — the signature shows that)
|
||||||
|
- **what value** it creates or protects
|
||||||
|
- **who** calls it in production (a real consumer, not just tests)
|
||||||
|
- **what breaks** if it returns the wrong answer
|
||||||
|
|
||||||
|
If any answer is missing: `BLOCKED: purpose unclear — [field]`.
|
||||||
|
|
||||||
|
### JSDoc format
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Acquire a unit claim atomically. Returns true on success, false if another
|
||||||
|
* worker already holds an unexpired lease.
|
||||||
|
*
|
||||||
|
* Purpose: prevent two workers from dispatching the same unit when the
|
||||||
|
* run-lock is unavailable — the conditional UPDATE is the safety net.
|
||||||
|
*
|
||||||
|
* Consumer: autonomous dispatch.ts when picking the next eligible unit per
|
||||||
|
* poll tick.
|
||||||
|
*/
|
||||||
|
export function claimUnit(unitId, leaseMs) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Required sections for non-trivial exports:
|
||||||
|
|
||||||
|
- **First line** — what it returns / does, present tense.
|
||||||
|
- **Purpose:** — why it exists; the value it protects.
|
||||||
|
- **Consumer:** — who calls it in production. No consumer = symbol shouldn't exist yet.
|
||||||
|
|
||||||
|
A bare `/** Helper. */` is a code smell. Either write the purpose or delete the symbol.
|
||||||
|
|
||||||
|
### Module-level JSDoc
|
||||||
|
|
||||||
|
```js
|
||||||
|
// session-recorder.js — per-process session lifecycle manager
|
||||||
|
//
|
||||||
|
// Purpose: capture the session/turn/file-touch/ref stream into DB rows so
|
||||||
|
// the memory pipeline has structured data to embed and cross-session search
|
||||||
|
// has rows to query.
|
||||||
|
//
|
||||||
|
// Consumer: bootstrap/register-hooks.js wires all 7 lifecycle events here.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Principles
|
||||||
|
|
||||||
|
| Principle | Rule |
|
||||||
|
|-----------|------|
|
||||||
|
| **Purpose first** | No symbol ships without a clear why, value, consumer, and falsifier. |
|
||||||
|
| **Single responsibility** | One concern per module/function. Adding a second concern = split or extract. |
|
||||||
|
| **DRY** | Single source of truth for mappings, defaults, and shared logic. |
|
||||||
|
| **Self-documenting names** | Names reveal intent. A comment explaining *what* something is = rename it. |
|
||||||
|
| **Constants over magic values** | No raw defaults, timeouts, or limits in logic. Named constants only. |
|
||||||
|
| **Observability** | Failures log at `logWarning` / `logError`. Happy path stays silent. |
|
||||||
|
| **Dead code zero** | No unused exports, no commented-out blocks, no unreachable branches. |
|
||||||
|
| **Small units** | Stay within thresholds (§ 4). Extract or split when approaching limits. |
|
||||||
|
| **Fallbacks only when real** | A fallback that can't deliver working behavior is noise. Omit it. |
|
||||||
|
| **Finish bounded refactors** | Rewire and remove the old path in the same PR. No shims, no dual paths. |
|
||||||
|
| **Single writer** | `sf-db.js` is the only file that issues write SQL. All others call its exports. |
|
||||||
|
| **Spec-first TDD** | Write the failing test before implementing. Test name = contract claim. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-pattern | Why | Required replacement | Rule |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `throw new Error(...)` bare in business logic | Callers can't distinguish failure classes | Throw with a descriptive prefix: `throw new Error("session-recorder.initSessionRecorder: db unavailable")` | **STY001** |
|
||||||
|
| Silent `catch` swallowing | Hides breakage | `logWarning(module, msg)` then decide: re-throw or return explicit failure | **STY002** |
|
||||||
|
| Magic status strings inline | Spreads typo-prone comparisons | Named constant or exported string literal at definition site | **STY003** |
|
||||||
|
| Generic names: `utils`, `helpers`, `common`, `misc` | Unsearchable, no domain signal | Name by capability: `memory-source-store.js`, `embed-circuit.js` | **STY004** |
|
||||||
|
| `// TODO: fix later` without ticket / owner | Permanent invisible debt | Fix now, or add a dated `// TODO(owner): <why>` with `node scripts/tech-debt-scan.mjs` visibility | **STY005** |
|
||||||
|
| Calling `db.prepare(...)` outside `sf-db.js` | Breaks single-writer invariant | Add an exported wrapper in `sf-db.js` | **STY006** |
|
||||||
|
| Embedding logic in hook wiring | Blurs responsibilities; untestable | Extract to a purpose-named module; wire only the call in `register-hooks.js` | **STY007** |
|
||||||
|
| Docstring = "Helper." or no docstring | Purpose is invisible to RAG and reviewers | Full JSDoc with Purpose + Consumer (§ 1) | **STY008** |
|
||||||
|
| Bare `process.env.FOO` scattered in logic | Config not auditable or testable | Named constant + `loadXxxConfigFromEnv()` function with null-guard | **STY009** |
|
||||||
|
| Test name = `"test X"` / `"works"` | Not a contract claim | `what_when_expected` form: `claimUnit_whenLeaseExpired_returnsTrue` | **STY010** |
|
||||||
|
| Mechanical test (counts mocks, not behavior) | Breaks on refactors that don't change behavior | Test what the *consumer receives*; label implementation guards `// guard:` | **STY011** |
|
||||||
|
| Committing to `dist/` or `~/.sf/agent/` | Generated output, not source | `dist/` is gitignored build output; run `npm run copy-resources` to rebuild | **STY012** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Thresholds
|
||||||
|
|
||||||
|
Two-tier: **Warn** = flag in review; **Error** = blocks merge.
|
||||||
|
|
||||||
|
| Metric | Warn | Error |
|
||||||
|
|--------|------|-------|
|
||||||
|
| Function lines | 50 | 75 |
|
||||||
|
| File lines | 800 | 1500 |
|
||||||
|
| Function arguments | 5 | 8 |
|
||||||
|
| Nesting depth | 4 | 6 |
|
||||||
|
| Dead code | 0 tolerance | — |
|
||||||
|
| `TODO`/`FIXME` count | per `tech-debt-scan.mjs` thresholds | — |
|
||||||
|
|
||||||
|
Infrastructure files (`sf-db.js`, generated schemas) may exceed file-line limits when extraction would harm clarity. Add a comment explaining why.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Naming
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| Kind | Convention | Example |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| Module | `kebab-case.js` | `session-recorder.js`, `memory-embeddings-llm-gateway.js` |
|
||||||
|
| Test | `kebab-case.test.mjs` / `.test.ts` | `sf-db-migration.test.mjs` |
|
||||||
|
| Prompt template | `kebab-case.md` | `execute-task.md` |
|
||||||
|
| Bootstrap/wiring | `register-hooks.js`, `init-*.js` | — |
|
||||||
|
|
||||||
|
### Functions and variables
|
||||||
|
|
||||||
|
- **Verb + noun**: `createGatewayEmbedFn`, `recordTurnStart`, `listUnembeddedMemoryIds`
|
||||||
|
- **No vague verbs alone**: not `run`, `do`, `handle` — add the object
|
||||||
|
- **No marketing words**: not `simple`, `unified`, `enhanced`, `smart`
|
||||||
|
- **Verbose over abbreviated**: `embeddingModel` not `embModel`; `queryInstruction` not `queryInstr`
|
||||||
|
- **Predicate booleans**: `embedCircuitIsOpen()`, `isDbAvailable()` — reads as a question
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
| Pattern | Use for | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `DEFAULT_*` | Default values | `DEFAULT_EMBEDDING_MODEL`, `DEFAULT_TIMEOUT_MS` |
|
||||||
|
| `MAX_*`, `MIN_*` | Bounds | `MAX_PER_INVOCATION`, `MIN_INTERVAL_MS` |
|
||||||
|
| `*_THRESHOLD` | Trigger limits | `EMBED_CIRCUIT_THRESHOLD` |
|
||||||
|
| `*_TO_*`, `*_MAP` | Domain A → B mappings | `UNIT_TYPE_TO_LABEL` |
|
||||||
|
| `ENV_*` | Env var name strings | `ENV_KEY`, `ENV_EMBED_MODEL` |
|
||||||
|
| `SCHEMA_VERSION` | Single integer, bumped per migration | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Patterns
|
||||||
|
|
||||||
|
### Single-writer DB
|
||||||
|
|
||||||
|
`sf-db.js` is the only file that prepares and executes write SQL. All other modules call exported wrappers. This makes the write surface auditable, testable, and migration-safe.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ✅ Correct — call the exported wrapper
|
||||||
|
import { upsertSession } from "./sf-db.js";
|
||||||
|
upsertSession({ id, cwd, branch });
|
||||||
|
|
||||||
|
// ❌ Wrong — raw SQL outside sf-db.js
|
||||||
|
const stmt = db.prepare("INSERT INTO sessions ...");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config from env
|
||||||
|
|
||||||
|
Always read env vars through a named `loadXxxConfigFromEnv()` function that returns `null` when required keys are absent (opt-in) or throws with a clear message (required).
|
||||||
|
|
||||||
|
```js
|
||||||
|
export function loadGatewayConfigFromEnv() {
|
||||||
|
const keyEntry = firstEnvEntry(KEY_ALIASES);
|
||||||
|
if (!keyEntry) return null; // opt-in: absent = no-op
|
||||||
|
...
|
||||||
|
return { url, apiKey, embeddingModel, queryInstruction };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Circuit breaker
|
||||||
|
|
||||||
|
When a remote dependency can stall (timeout), implement a circuit breaker that:
|
||||||
|
- Counts consecutive failures
|
||||||
|
- Opens for `CIRCUIT_OPEN_MS` after `THRESHOLD` failures
|
||||||
|
- Logs once per open period (throttled)
|
||||||
|
- Half-opens automatically after cooldown
|
||||||
|
|
||||||
|
See `embedCircuit` in `memory-embeddings-llm-gateway.js` as the reference.
|
||||||
|
|
||||||
|
### Asymmetric embeddings (Qwen3)
|
||||||
|
|
||||||
|
Qwen3-Embedding uses asymmetric retrieval. Always pass `instruction` for queries; omit for documents.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Query embedding — instruction required
|
||||||
|
const embedFn = createGatewayEmbedFn(cfg, { instruction: cfg.queryInstruction });
|
||||||
|
|
||||||
|
// Document/backfill embedding — no instruction
|
||||||
|
const embedFn = createGatewayEmbedFn(cfg);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook wiring
|
||||||
|
|
||||||
|
`bootstrap/register-hooks.js` wires lifecycle events to module functions. Keep each hook body thin: import, call, done. No business logic in hooks.
|
||||||
|
|
||||||
|
```js
|
||||||
|
pi.on("agent_end", async (event) => {
|
||||||
|
const text = event.messages?.at(-1)?.content?.find(b => b.type === "text")?.text ?? "";
|
||||||
|
await recordTurnEnd(text);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test contracts
|
||||||
|
|
||||||
|
Test names are contract claims: `what_when_expected`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ✅ Contract claim
|
||||||
|
test("claimUnit_whenLeaseExpired_returnsTrue", () => { ... });
|
||||||
|
|
||||||
|
// ❌ Not a contract
|
||||||
|
test("claimUnit works", () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
Three tiers:
|
||||||
|
1. **Behaviour contracts** — what the consumer receives. Primary. Spec.
|
||||||
|
2. **Degradation contracts** — what happens when dependencies fail (DB down, gateway unreachable).
|
||||||
|
3. **Implementation guards** — labelled `// guard:` — protect specific failure modes. Refactors may update these.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Documentation
|
||||||
|
|
||||||
|
### When to comment
|
||||||
|
|
||||||
|
- **Always**: exported symbols with non-trivial behavior (full JSDoc per § 1)
|
||||||
|
- **Rarely**: inline comments only when the *why* is genuinely non-obvious from reading the code
|
||||||
|
- **Never**: comments that restate what the code does; comments as TODO parking
|
||||||
|
|
||||||
|
### Keeping docs current
|
||||||
|
|
||||||
|
When you change behavior, update the JSDoc Purpose and Consumer in the same commit. A stale Purpose is worse than no Purpose — it actively misleads the next reader.
|
||||||
|
|
||||||
|
### Module headers
|
||||||
|
|
||||||
|
```js
|
||||||
|
// module-name.js — one-line description
|
||||||
|
//
|
||||||
|
// Purpose: why this module exists as a separable unit.
|
||||||
|
//
|
||||||
|
// Consumer: who imports this at runtime (or "internal" if only tests).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [AGENTS.md](AGENTS.md) — planning conventions, spec-first TDD, test naming
|
||||||
|
- [docs/adr/0000-purpose-to-software-compiler.md](docs/adr/0000-purpose-to-software-compiler.md) — foundational product contract
|
||||||
|
- [docs/SPEC_FIRST_TDD.md](docs/SPEC_FIRST_TDD.md) — test-first constitution
|
||||||
|
- [biome.json](biome.json) — linter config (`npm run lint`)
|
||||||
|
- [scripts/tech-debt-scan.mjs](scripts/tech-debt-scan.mjs) — TODO/FIXME threshold tracking
|
||||||
|
|
@ -750,7 +750,7 @@ Already directionally right:
|
||||||
|
|
||||||
Still needed:
|
Still needed:
|
||||||
|
|
||||||
- Remove `/sf` from docs/web/tests (Phase 2 deprecation)
|
- ~~Remove `/sf` from docs/web/tests (Phase 2 deprecation)~~ ✓ Complete
|
||||||
|
|
||||||
Completed ✓ (RA.Aid Patterns — Phase 2):
|
Completed ✓ (RA.Aid Patterns — Phase 2):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ test("loadGatewayConfigFromEnv accepts SF-prefixed configuration", () => {
|
||||||
urlSource: "SF_LLM_GATEWAY_URL",
|
urlSource: "SF_LLM_GATEWAY_URL",
|
||||||
embeddingModel: "embed-model",
|
embeddingModel: "embed-model",
|
||||||
rerankModel: "rerank-model",
|
rerankModel: "rerank-model",
|
||||||
|
queryInstruction:
|
||||||
|
"Instruct: Retrieve relevant software engineering memories, facts, and project decisions for the given query\nQuery: ",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -59,6 +61,8 @@ test("loadGatewayConfigFromEnv accepts llm-gateway shell aliases", () => {
|
||||||
urlSource: "LLM_GATEWAY_BASE_URL",
|
urlSource: "LLM_GATEWAY_BASE_URL",
|
||||||
embeddingModel: "Qwen/Qwen3-Embedding-4B",
|
embeddingModel: "Qwen/Qwen3-Embedding-4B",
|
||||||
rerankModel: "Qwen/Qwen3-Reranker-0.6B",
|
rerankModel: "Qwen/Qwen3-Reranker-0.6B",
|
||||||
|
queryInstruction:
|
||||||
|
"Instruct: Retrieve relevant software engineering memories, facts, and project decisions for the given query\nQuery: ",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ test("/api/recovery returns structured recovery diagnostics and redacts secrets"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
payload.actions.commands.some((entry: { command: string }) =>
|
payload.actions.commands.some((entry: { command: string }) =>
|
||||||
entry.command.includes("/sf doctor"),
|
entry.command.includes("/doctor"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,83 +23,83 @@ function baseInput(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Group 1: Phase → action mapping ──────────────────────────────────
|
// ─── Group 1: Phase → action mapping ──────────────────────────────────
|
||||||
test("planning + no auto → primary is /sf with label Plan", () => {
|
test("planning + no auto → primary is /discuss with label Plan", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "planning" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "planning" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/discuss");
|
||||||
assert.equal(result.primary.label, "Plan");
|
assert.equal(result.primary.label, "Plan");
|
||||||
assert.equal(result.primary.variant, "default");
|
assert.equal(result.primary.variant, "default");
|
||||||
assert.equal(result.disabled, false);
|
assert.equal(result.disabled, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("executing + no auto → primary is /sf autonomous with label Start Autonomous", () => {
|
test("executing + no auto → primary is /autonomous with label Start Autonomous", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "executing" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "executing" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf autonomous");
|
assert.equal(result.primary.command, "/autonomous");
|
||||||
assert.equal(result.primary.label, "Start Autonomous");
|
assert.equal(result.primary.label, "Start Autonomous");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("summarizing + no auto → primary is /sf autonomous with label Start Autonomous", () => {
|
test("summarizing + no auto → primary is /autonomous with label Start Autonomous", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "summarizing" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "summarizing" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf autonomous");
|
assert.equal(result.primary.command, "/autonomous");
|
||||||
assert.equal(result.primary.label, "Start Autonomous");
|
assert.equal(result.primary.label, "Start Autonomous");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auto active (not paused) → primary is /sf stop with destructive variant", () => {
|
test("auto active (not paused) → primary is /stop with destructive variant", () => {
|
||||||
const result = deriveWorkflowAction(
|
const result = deriveWorkflowAction(
|
||||||
baseInput({ autoActive: true, autoPaused: false }),
|
baseInput({ autoActive: true, autoPaused: false }),
|
||||||
);
|
);
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf stop");
|
assert.equal(result.primary.command, "/stop");
|
||||||
assert.equal(result.primary.label, "Stop Autonomous");
|
assert.equal(result.primary.label, "Stop Autonomous");
|
||||||
assert.equal(result.primary.variant, "destructive");
|
assert.equal(result.primary.variant, "destructive");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auto paused → primary is /sf autonomous with label Resume Autonomous", () => {
|
test("auto paused → primary is /autonomous with label Resume Autonomous", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ autoPaused: true }));
|
const result = deriveWorkflowAction(baseInput({ autoPaused: true }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf autonomous");
|
assert.equal(result.primary.command, "/autonomous");
|
||||||
assert.equal(result.primary.label, "Resume Autonomous");
|
assert.equal(result.primary.label, "Resume Autonomous");
|
||||||
assert.equal(result.primary.variant, "default");
|
assert.equal(result.primary.variant, "default");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pre-planning + no milestones → primary is /sf with label Initialize Project", () => {
|
test("pre-planning + no milestones → primary is /init with label Initialize Project", () => {
|
||||||
const result = deriveWorkflowAction(
|
const result = deriveWorkflowAction(
|
||||||
baseInput({ phase: "pre-planning", hasMilestones: false }),
|
baseInput({ phase: "pre-planning", hasMilestones: false }),
|
||||||
);
|
);
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/init");
|
||||||
assert.equal(result.primary.label, "Initialize Project");
|
assert.equal(result.primary.label, "Initialize Project");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pre-planning + has milestones → primary is /sf with label Continue", () => {
|
test("pre-planning + has milestones → primary is /discuss with label Continue", () => {
|
||||||
const result = deriveWorkflowAction(
|
const result = deriveWorkflowAction(
|
||||||
baseInput({ phase: "pre-planning", hasMilestones: true }),
|
baseInput({ phase: "pre-planning", hasMilestones: true }),
|
||||||
);
|
);
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/discuss");
|
||||||
assert.equal(result.primary.label, "Continue");
|
assert.equal(result.primary.label, "Continue");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("other phases (e.g. researching) without auto → primary is Continue /sf", () => {
|
test("other phases (e.g. researching) without auto → primary is Continue /discuss", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "researching" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "researching" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/discuss");
|
||||||
assert.equal(result.primary.label, "Continue");
|
assert.equal(result.primary.label, "Continue");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("verifying phase without auto → primary is Continue /sf", () => {
|
test("verifying phase without auto → primary is Continue /discuss", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "verifying" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "verifying" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/discuss");
|
||||||
assert.equal(result.primary.label, "Continue");
|
assert.equal(result.primary.label, "Continue");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("complete phase without auto → primary is New Milestone /sf with no step secondary", () => {
|
test("complete phase without auto → primary is New Milestone /new-milestone with no step secondary", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "complete" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "complete" }));
|
||||||
assert.ok(result.primary);
|
assert.ok(result.primary);
|
||||||
assert.equal(result.primary.command, "/sf");
|
assert.equal(result.primary.command, "/new-milestone");
|
||||||
assert.equal(result.primary.label, "New Milestone");
|
assert.equal(result.primary.label, "New Milestone");
|
||||||
assert.equal(result.isNewMilestone, true);
|
assert.equal(result.isNewMilestone, true);
|
||||||
assert.deepEqual(result.secondaries, []);
|
assert.deepEqual(result.secondaries, []);
|
||||||
|
|
@ -109,7 +109,7 @@ test("complete phase without auto → primary is New Milestone /sf with no step
|
||||||
test("secondaries include Step when auto is not active", () => {
|
test("secondaries include Step when auto is not active", () => {
|
||||||
const result = deriveWorkflowAction(baseInput({ phase: "executing" }));
|
const result = deriveWorkflowAction(baseInput({ phase: "executing" }));
|
||||||
assert.ok(result.secondaries.length > 0);
|
assert.ok(result.secondaries.length > 0);
|
||||||
const step = result.secondaries.find((s) => s.command === "/sf next");
|
const step = result.secondaries.find((s) => s.command === "/next");
|
||||||
assert.ok(step, "Expected a Step secondary action");
|
assert.ok(step, "Expected a Step secondary action");
|
||||||
assert.equal(step.label, "Step");
|
assert.equal(step.label, "Step");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ test("renders model and provider", () => {
|
||||||
|
|
||||||
test("renders cwd hint", () => {
|
test("renders cwd hint", () => {
|
||||||
const out = strip(capture({ version: "1.0.0" }));
|
const out = strip(capture({ version: "1.0.0" }));
|
||||||
assert.ok(out.includes("/sf to begin"), "hint line missing");
|
assert.ok(out.includes("/next to step"), "hint line missing");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("skips when not a TTY", (_t) => {
|
test("skips when not a TTY", (_t) => {
|
||||||
|
|
|
||||||
|
|
@ -246,16 +246,16 @@ function buildCommandSuggestions(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (phase === "planning") add("/sf", "Open SF planning");
|
if (phase === "planning") add("/discuss", "Open SF planning");
|
||||||
if (phase === "executing" || phase === "summarizing")
|
if (phase === "executing" || phase === "summarizing")
|
||||||
add("/sf autonomous", "Resume SF autonomous mode");
|
add("/autonomous", "Resume SF autonomous mode");
|
||||||
if (activeScope)
|
if (activeScope)
|
||||||
add(`/sf doctor ${activeScope}`, "Inspect scoped doctor report");
|
add(`/doctor ${activeScope}`, "Inspect scoped doctor report");
|
||||||
if (activeScope)
|
if (activeScope)
|
||||||
add(`/sf doctor fix ${activeScope}`, "Apply scoped doctor fixes");
|
add(`/doctor fix ${activeScope}`, "Apply scoped doctor fixes");
|
||||||
if (validationCount > 0 && activeScope)
|
if (validationCount > 0 && activeScope)
|
||||||
add(`/sf doctor audit ${activeScope}`, "Audit validation diagnostics");
|
add(`/doctor audit ${activeScope}`, "Audit validation diagnostics");
|
||||||
add("/sf status", "Check current-project status");
|
add("/status", "Check current-project status");
|
||||||
|
|
||||||
return [...suggestions.values()];
|
return [...suggestions.values()];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export function printWelcomeScreen(opts: WelcomeScreenOptions): void {
|
||||||
// Tools left, hint right-aligned on the same row
|
// Tools left, hint right-aligned on the same row
|
||||||
const toolsLeft =
|
const toolsLeft =
|
||||||
toolParts.length > 0 ? chalk.dim(" " + toolParts.join(" · ")) : "";
|
toolParts.length > 0 ? chalk.dim(" " + toolParts.join(" · ")) : "";
|
||||||
const hintRight = chalk.dim("/sf to begin · /sf help");
|
const hintRight = chalk.dim("/next to step · /help");
|
||||||
const footerFill = RIGHT_INNER - visLen(toolsLeft) - visLen(hintRight);
|
const footerFill = RIGHT_INNER - visLen(toolsLeft) - visLen(hintRight);
|
||||||
const footerRow = toolsLeft + " ".repeat(Math.max(1, footerFill)) + hintRight;
|
const footerRow = toolsLeft + " ".repeat(Math.max(1, footerFill)) + hintRight;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Top 3 (standalone buttons) ──
|
// ── Top 3 (standalone buttons) ──
|
||||||
{
|
{
|
||||||
label: "Discuss",
|
label: "Discuss",
|
||||||
command: "/sf discuss",
|
command: "/discuss",
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
description: "Start guided milestone/slice discussion",
|
description: "Start guided milestone/slice discussion",
|
||||||
category: "workflow",
|
category: "workflow",
|
||||||
|
|
@ -119,14 +119,14 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Next",
|
label: "Next",
|
||||||
command: "/sf next",
|
command: "/next",
|
||||||
icon: Play,
|
icon: Play,
|
||||||
description: "Execute next task, then pause",
|
description: "Execute next task, then pause",
|
||||||
category: "workflow",
|
category: "workflow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Autonomous",
|
label: "Autonomous",
|
||||||
command: "/sf autonomous",
|
command: "/autonomous",
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
description: "Run all queued product units continuously",
|
description: "Run all queued product units continuously",
|
||||||
category: "workflow",
|
category: "workflow",
|
||||||
|
|
@ -134,14 +134,14 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Workflow ──
|
// ── Overflow: Workflow ──
|
||||||
{
|
{
|
||||||
label: "Stop",
|
label: "Stop",
|
||||||
command: "/sf stop",
|
command: "/stop",
|
||||||
icon: Square,
|
icon: Square,
|
||||||
description: "Stop autonomous mode gracefully",
|
description: "Stop autonomous mode gracefully",
|
||||||
category: "workflow",
|
category: "workflow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pause",
|
label: "Pause",
|
||||||
command: "/sf pause",
|
command: "/pause",
|
||||||
icon: Pause,
|
icon: Pause,
|
||||||
description: "Pause autonomous mode (preserves state)",
|
description: "Pause autonomous mode (preserves state)",
|
||||||
category: "workflow",
|
category: "workflow",
|
||||||
|
|
@ -149,28 +149,28 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Visibility ──
|
// ── Overflow: Visibility ──
|
||||||
{
|
{
|
||||||
label: "Status",
|
label: "Status",
|
||||||
command: "/sf status",
|
command: "/status",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
description: "Show progress dashboard",
|
description: "Show progress dashboard",
|
||||||
category: "visibility",
|
category: "visibility",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Visualize",
|
label: "Visualize",
|
||||||
command: "/sf visualize",
|
command: "/visualize",
|
||||||
icon: LayoutGrid,
|
icon: LayoutGrid,
|
||||||
description: "Interactive TUI (progress, deps, metrics, timeline)",
|
description: "Interactive TUI (progress, deps, metrics, timeline)",
|
||||||
category: "visibility",
|
category: "visibility",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Queue",
|
label: "Queue",
|
||||||
command: "/sf queue",
|
command: "/queue",
|
||||||
icon: ListOrdered,
|
icon: ListOrdered,
|
||||||
description: "Show queued/dispatched units and execution order",
|
description: "Show queued/dispatched units and execution order",
|
||||||
category: "visibility",
|
category: "visibility",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "History",
|
label: "History",
|
||||||
command: "/sf history",
|
command: "/history",
|
||||||
icon: History,
|
icon: History,
|
||||||
description: "View execution history with cost/phase/model details",
|
description: "View execution history with cost/phase/model details",
|
||||||
category: "visibility",
|
category: "visibility",
|
||||||
|
|
@ -178,21 +178,21 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Course correction ──
|
// ── Overflow: Course correction ──
|
||||||
{
|
{
|
||||||
label: "Steer",
|
label: "Steer",
|
||||||
command: "/sf steer",
|
command: "/steer",
|
||||||
icon: Compass,
|
icon: Compass,
|
||||||
description: "Apply user override to active work",
|
description: "Apply user override to active work",
|
||||||
category: "correction",
|
category: "correction",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Capture",
|
label: "Capture",
|
||||||
command: "/sf capture",
|
command: "/capture",
|
||||||
icon: PenLine,
|
icon: PenLine,
|
||||||
description: "Quick-capture a thought to CAPTURES.md",
|
description: "Quick-capture a thought to CAPTURES.md",
|
||||||
category: "correction",
|
category: "correction",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Triage",
|
label: "Triage",
|
||||||
command: "/sf triage",
|
command: "/triage",
|
||||||
icon: Inbox,
|
icon: Inbox,
|
||||||
description: "Classify and route pending captures",
|
description: "Classify and route pending captures",
|
||||||
category: "correction",
|
category: "correction",
|
||||||
|
|
@ -200,14 +200,14 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Skip",
|
label: "Skip",
|
||||||
command: "/sf skip",
|
command: "/skip",
|
||||||
icon: SkipForward,
|
icon: SkipForward,
|
||||||
description: "Prevent a unit from auto-mode dispatch",
|
description: "Prevent a unit from auto-mode dispatch",
|
||||||
category: "correction",
|
category: "correction",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
command: "/sf undo",
|
command: "/undo",
|
||||||
icon: Undo2,
|
icon: Undo2,
|
||||||
description: "Revert last completed unit",
|
description: "Revert last completed unit",
|
||||||
category: "correction",
|
category: "correction",
|
||||||
|
|
@ -215,7 +215,7 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Knowledge ──
|
// ── Overflow: Knowledge ──
|
||||||
{
|
{
|
||||||
label: "Knowledge",
|
label: "Knowledge",
|
||||||
command: "/sf knowledge",
|
command: "/knowledge",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
description: "Add rule, pattern, or lesson to KNOWLEDGE.md",
|
description: "Add rule, pattern, or lesson to KNOWLEDGE.md",
|
||||||
category: "knowledge",
|
category: "knowledge",
|
||||||
|
|
@ -223,14 +223,14 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Configuration ──
|
// ── Overflow: Configuration ──
|
||||||
{
|
{
|
||||||
label: "Mode",
|
label: "Mode",
|
||||||
command: "/sf mode",
|
command: "/mode",
|
||||||
icon: SlidersHorizontal,
|
icon: SlidersHorizontal,
|
||||||
description: "Set workflow mode (solo/team)",
|
description: "Set workflow mode (solo/team)",
|
||||||
category: "config",
|
category: "config",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Prefs",
|
label: "Prefs",
|
||||||
command: "/sf prefs",
|
command: "/prefs",
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
description: "Manage preferences (global/project)",
|
description: "Manage preferences (global/project)",
|
||||||
category: "config",
|
category: "config",
|
||||||
|
|
@ -238,28 +238,28 @@ const SF_ACTIONS: SFActionDef[] = [
|
||||||
// ── Overflow: Maintenance ──
|
// ── Overflow: Maintenance ──
|
||||||
{
|
{
|
||||||
label: "Doctor",
|
label: "Doctor",
|
||||||
command: "/sf doctor",
|
command: "/doctor",
|
||||||
icon: Stethoscope,
|
icon: Stethoscope,
|
||||||
description: "Diagnose and repair .sf/ state",
|
description: "Diagnose and repair .sf/ state",
|
||||||
category: "maintenance",
|
category: "maintenance",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Export",
|
label: "Export",
|
||||||
command: "/sf export",
|
command: "/export",
|
||||||
icon: FileOutput,
|
icon: FileOutput,
|
||||||
description: "Export milestone/slice results (JSON or Markdown)",
|
description: "Export milestone/slice results (JSON or Markdown)",
|
||||||
category: "maintenance",
|
category: "maintenance",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Cleanup",
|
label: "Cleanup",
|
||||||
command: "/sf cleanup",
|
command: "/cleanup",
|
||||||
icon: Trash2,
|
icon: Trash2,
|
||||||
description: "Remove merged branches or snapshots",
|
description: "Remove merged branches or snapshots",
|
||||||
category: "maintenance",
|
category: "maintenance",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Remote",
|
label: "Remote",
|
||||||
command: "/sf remote",
|
command: "/remote",
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
description: "Control remote auto-mode (Slack/Discord)",
|
description: "Control remote auto-mode (Slack/Discord)",
|
||||||
category: "maintenance",
|
category: "maintenance",
|
||||||
|
|
|
||||||
|
|
@ -3056,7 +3056,7 @@ export function CommandSurface() {
|
||||||
data-testid={`sf-surface-${commandSurface.section}`}
|
data-testid={`sf-surface-${commandSurface.section}`}
|
||||||
>
|
>
|
||||||
<p className="font-medium text-foreground">
|
<p className="font-medium text-foreground">
|
||||||
/sf {commandSurface.section.slice(4)}
|
/{commandSurface.section.slice(4)}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1">Unknown SF surface.</p>
|
<p className="mt-1">Unknown SF surface.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ function getVariant(detection: ProjectDetection): WelcomeVariant {
|
||||||
headline: "Existing project detected",
|
headline: "Existing project detected",
|
||||||
body: "SF will map your codebase and ask a few questions about what you want to build. From there it generates structured milestones and deliverable slices.",
|
body: "SF will map your codebase and ask a few questions about what you want to build. From there it generates structured milestones and deliverable slices.",
|
||||||
primaryLabel: "Map & Initialize",
|
primaryLabel: "Map & Initialize",
|
||||||
primaryCommand: "/sf",
|
primaryCommand: "/init",
|
||||||
secondary: {
|
secondary: {
|
||||||
label: "Browse files first",
|
label: "Browse files first",
|
||||||
action: "files-view",
|
action: "files-view",
|
||||||
|
|
@ -59,11 +59,11 @@ function getVariant(detection: ProjectDetection): WelcomeVariant {
|
||||||
detail:
|
detail:
|
||||||
"Your original files will be preserved — migration creates the new structure alongside them.",
|
"Your original files will be preserved — migration creates the new structure alongside them.",
|
||||||
primaryLabel: "Migrate to v2",
|
primaryLabel: "Migrate to v2",
|
||||||
primaryCommand: "/sf migrate",
|
primaryCommand: "/migrate",
|
||||||
secondary: {
|
secondary: {
|
||||||
label: "Start fresh instead",
|
label: "Start fresh instead",
|
||||||
action: "command",
|
action: "command",
|
||||||
command: "/sf",
|
command: "/init",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ function getVariant(detection: ProjectDetection): WelcomeVariant {
|
||||||
headline: "Start a new project",
|
headline: "Start a new project",
|
||||||
body: "This folder is empty. SF will ask what you want to build, then generate a structured plan — milestones broken into deliverable slices with risk-ordered execution.",
|
body: "This folder is empty. SF will ask what you want to build, then generate a structured plan — milestones broken into deliverable slices with risk-ordered execution.",
|
||||||
primaryLabel: "Start Project Setup",
|
primaryLabel: "Start Project Setup",
|
||||||
primaryCommand: "/sf",
|
primaryCommand: "/init",
|
||||||
};
|
};
|
||||||
|
|
||||||
// active-sf and empty-sf shouldn't reach here, but handle gracefully
|
// active-sf and empty-sf shouldn't reach here, but handle gracefully
|
||||||
|
|
@ -85,7 +85,7 @@ function getVariant(detection: ProjectDetection): WelcomeVariant {
|
||||||
headline: "Set up your project",
|
headline: "Set up your project",
|
||||||
body: "Run the SF wizard to get started.",
|
body: "Run the SF wizard to get started.",
|
||||||
primaryLabel: "Get Started",
|
primaryLabel: "Get Started",
|
||||||
primaryCommand: "/sf",
|
primaryCommand: "/init",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ export function QuickPanel() {
|
||||||
Usage
|
Usage
|
||||||
</h4>
|
</h4>
|
||||||
<div className="rounded-md border border-border/50 bg-background/50 px-3 py-2 font-mono text-[11px] text-foreground/80">
|
<div className="rounded-md border border-border/50 bg-background/50 px-3 py-2 font-mono text-[11px] text-foreground/80">
|
||||||
/sf quick <description>
|
/quick <description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ export function QuickPanel() {
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">$</span>
|
<span className="text-muted-foreground">$</span>
|
||||||
<code className="font-mono text-muted-foreground">
|
<code className="font-mono text-muted-foreground">
|
||||||
/sf quick {example}
|
/quick {example}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1603,7 +1603,7 @@ export function StatusPanel() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{milestones.length === 0 && (
|
{milestones.length === 0 && (
|
||||||
<PanelEmpty message="No plan loaded — run /sf to initialize" />
|
<PanelEmpty message="No plan loaded — run /init to initialize" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -141,12 +141,15 @@ const SF_SURFACE_COMMANDS = new Map<string, BrowserSlashCommandSurface>([
|
||||||
const SF_PASSTHROUGH_COMMANDS = new Set<string>([
|
const SF_PASSTHROUGH_COMMANDS = new Set<string>([
|
||||||
"autonomous",
|
"autonomous",
|
||||||
"next",
|
"next",
|
||||||
|
"stop",
|
||||||
"pause",
|
"pause",
|
||||||
"skip",
|
"skip",
|
||||||
"discuss",
|
"discuss",
|
||||||
"run-hook",
|
"run-hook",
|
||||||
"migrate",
|
"migrate",
|
||||||
"remote",
|
"remote",
|
||||||
|
"new-milestone",
|
||||||
|
"init",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SF_HELP_TEXT = `Available SF commands:
|
export const SF_HELP_TEXT = `Available SF commands:
|
||||||
|
|
|
||||||
|
|
@ -83,64 +83,64 @@ export function deriveWorkflowAction(
|
||||||
if (autoActive && !autoPaused) {
|
if (autoActive && !autoPaused) {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Stop Autonomous",
|
label: "Stop Autonomous",
|
||||||
command: "/sf stop",
|
command: "/stop",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
};
|
};
|
||||||
} else if (autoPaused) {
|
} else if (autoPaused) {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Resume Autonomous",
|
label: "Resume Autonomous",
|
||||||
command: "/sf autonomous",
|
command: "/autonomous",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Auto is not active
|
// Auto is not active
|
||||||
if (phase === "complete") {
|
if (phase === "complete") {
|
||||||
// All milestones done — surface a distinct "New Milestone" action
|
// All milestones done — surface a distinct "New Milestone" action
|
||||||
primary = { label: "New Milestone", command: "/sf", variant: "default" };
|
primary = { label: "New Milestone", command: "/new-milestone", variant: "default" };
|
||||||
isNewMilestone = true;
|
isNewMilestone = true;
|
||||||
} else if (phase === "planning") {
|
} else if (phase === "planning") {
|
||||||
primary = { label: "Plan", command: "/sf", variant: "default" };
|
primary = { label: "Plan", command: "/discuss", variant: "default" };
|
||||||
} else if (phase === "executing" || phase === "summarizing") {
|
} else if (phase === "executing" || phase === "summarizing") {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Start Autonomous",
|
label: "Start Autonomous",
|
||||||
command: "/sf autonomous",
|
command: "/autonomous",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
};
|
};
|
||||||
} else if (phase === "pre-planning" && !hasMilestones) {
|
} else if (phase === "pre-planning" && !hasMilestones) {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Initialize Project",
|
label: "Initialize Project",
|
||||||
command: "/sf",
|
command: "/init",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
};
|
};
|
||||||
} else if (phase === "blocked") {
|
} else if (phase === "blocked") {
|
||||||
primary = { label: "Blocked", command: "/sf", variant: "default" };
|
primary = { label: "Blocked", command: "/discuss", variant: "default" };
|
||||||
disabled = true;
|
disabled = true;
|
||||||
disabledReason = "Project is blocked — check blockers";
|
disabledReason = "Project is blocked — check blockers";
|
||||||
} else if (phase === "paused") {
|
} else if (phase === "paused") {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Resume",
|
label: "Resume",
|
||||||
command: "/sf autonomous",
|
command: "/autonomous",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
};
|
};
|
||||||
} else if (phase === "validating-milestone") {
|
} else if (phase === "validating-milestone") {
|
||||||
primary = { label: "Validate", command: "/sf", variant: "default" };
|
primary = { label: "Validate", command: "/discuss", variant: "default" };
|
||||||
} else if (phase === "completing-milestone") {
|
} else if (phase === "completing-milestone") {
|
||||||
primary = {
|
primary = {
|
||||||
label: "Complete Milestone",
|
label: "Complete Milestone",
|
||||||
command: "/sf",
|
command: "/discuss",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
};
|
};
|
||||||
} else if (phase === "needs-discussion") {
|
} else if (phase === "needs-discussion") {
|
||||||
primary = { label: "Discuss", command: "/sf", variant: "default" };
|
primary = { label: "Discuss", command: "/discuss", variant: "default" };
|
||||||
} else if (phase === "replanning-slice") {
|
} else if (phase === "replanning-slice") {
|
||||||
primary = { label: "Replan", command: "/sf", variant: "default" };
|
primary = { label: "Replan", command: "/discuss", variant: "default" };
|
||||||
} else {
|
} else {
|
||||||
primary = { label: "Continue", command: "/sf", variant: "default" };
|
primary = { label: "Continue", command: "/discuss", variant: "default" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add "Step" secondary when auto is not active (not for new milestone — no step concept there)
|
// Add "Step" secondary when auto is not active (not for new milestone — no step concept there)
|
||||||
if (primary.command !== "/sf next" && !isNewMilestone) {
|
if (primary.command !== "/next" && !isNewMilestone) {
|
||||||
secondaries.push({ label: "Step", command: "/sf next" });
|
secondaries.push({ label: "Step", command: "/next" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue