Sign In

gspec

Package Overview
Dependencies
Maintainers
1
Versions
39
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gspec - npm Package Compare versions

Comparing version
2.5.1
to
2.6.0
+67
-0
lib/config.js
// Project-local gspec config (.gspec/config.json) — install-time metadata the
// CLI consults later (e.g. `gspec build` defaults its engine to the target the
// project was installed for, instead of blindly assuming Claude).
//
// The same file also carries an optional `models` map (see resolveModel) that
// assigns a model to each build agent; a `~/.gspec/config.json` provides the
// user-global defaults the project file overrides.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';

@@ -19,2 +24,64 @@ export const PROJECT_CONFIG_PATH = join('.gspec', 'config.json');

// User-global config at ~/.gspec/config.json (homedir honors $HOME, so tests
// with an isolated HOME never touch a developer's real file). Same shape as the
// project config; only its `models` map is consulted today.
export async function readGlobalConfig() {
try {
return JSON.parse(await readFile(join(homedir(), '.gspec', 'config.json'), 'utf-8'));
} catch {
return {};
}
}
// --- per-agent model resolution (config `models` map) ----------------------
//
// The `models` map keys a model onto a SELECTOR: an exact agent name, a role
// tier, or `default`. Role tiers let one entry cover a whole class of agents
// without naming each — `qa` is every `*-validator`, `writer` every `*-writer`,
// and the producers that don't follow those suffixes are named below.
const ROLE_BY_NAME = {
'feature-planner': 'planner',
'research-planner': 'planner',
'plan-decomposer': 'planner',
'build-orchestrator': 'planner',
'implementer': 'implementer',
'competitor-researcher': 'researcher',
'codebase-inspector': 'inspector',
};
// The role tier an agent belongs to, or null if it matches no tier (then only an
// exact-name entry or `default` applies). Suffix rules cover the generated
// families (*-writer / *-validator); the table above covers the rest.
export function roleOf(agentName) {
if (ROLE_BY_NAME[agentName]) return ROLE_BY_NAME[agentName];
if (agentName.endsWith('-validator')) return 'qa';
if (agentName.endsWith('-writer')) return 'writer';
return null;
}
// Resolve the model for one agent from the project + global `models` maps, most
// specific selector first, project breaking ties WITHIN a level: exact agent
// name (project→global), then role tier (project→global), then `default`
// (project→global). Returns null when nothing matches (engine/CLI default).
export function resolveModel(agentName, { project = {}, global = {} } = {}) {
const pm = project.models || {};
const gm = global.models || {};
const role = roleOf(agentName);
const candidates = [
pm[agentName], // 1. project, exact agent
gm[agentName], // 2. global, exact agent
role ? pm[role] : undefined, // 3. project, role tier
role ? gm[role] : undefined, // 4. global, role tier
pm.default, // 5. project, default
gm.default, // 6. global, default
];
return candidates.find((m) => typeof m === 'string' && m.trim()) || null;
}
// The distinct selectors configured across both files (project overlaying
// global), for a compact "which models are in play" line at run start.
export function modelSelectors(projectConfig = {}, globalConfig = {}) {
return { ...(globalConfig.models || {}), ...(projectConfig.models || {}) };
}
// Shallow-merges `patch` over the existing config so unrelated keys survive.

@@ -21,0 +88,0 @@ export async function writeProjectConfig(cwd, patch) {

+8
-7

@@ -119,4 +119,5 @@ // gspec build — pluggable execution engines.

// Native agent selection: --agent loads the whole definition (skills/tools/
// model), so we don't inject anything or pass --model.
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb } = {}) {
// model). A per-agent `model` (from the config models map) overrides the
// model the agent definition would otherwise pick.
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb, model } = {}) {
const args = ['-p', prompt, '--agent', agentName, '--output-format', 'text', '--permission-mode', ctx.permissionMode];

@@ -127,3 +128,3 @@ if (needsBash) args.push('--allowedTools', 'Bash');

if (needsWeb) args.push('--allowedTools', 'WebSearch,WebFetch');
if (ctx.model) args.push('--model', ctx.model);
if (model) args.push('--model', model);
if (ctx.dryRun) return dryRun(ctx, 'claude', args, prompt);

@@ -142,3 +143,3 @@ return spawnCapture('claude', args, { cwd: ctx.cwd });

agentFile: (name) => join('.codex', 'agents', `${name}.toml`),
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb } = {}) {
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb, model } = {}) {
// workspace-write auto-approves edits + in-workspace commands but blocks

@@ -150,3 +151,3 @@ // network; stages that run installs/tests (needsBash) get full access.

const net = needsWeb && !needsBash ? ['-c', 'sandbox_workspace_write.network_access=true'] : [];
const head = ['exec', '--sandbox', sandbox, ...net, ...(ctx.model ? ['--model', ctx.model] : [])];
const head = ['exec', '--sandbox', sandbox, ...net, ...(model ? ['--model', model] : [])];
if (ctx.dryRun) return dryRun(ctx, 'codex', [...head, `<${agentName}>`], `<${agentName}>`);

@@ -168,3 +169,3 @@ const instructions = await readInjectedAgent(ctx.cwd, this.agentFile(agentName), codexInstructions);

agentFile: (name) => join('.pi', 'agents', `${name}.md`),
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb } = {}) { // eslint-disable-line no-unused-vars
async runAgent(agentName, prompt, ctx, { needsBash, needsWeb, model } = {}) { // eslint-disable-line no-unused-vars
// -p = print/headless; -a trusts project-local files (skills/agents) for

@@ -176,3 +177,3 @@ // this run. Base Pi does not document a print-mode tool auto-approve flag,

// build runtime docs (§8) for the current status of this gap.
const head = ['-p', '-a', ...(ctx.model ? ['--model', ctx.model] : [])];
const head = ['-p', '-a', ...(model ? ['--model', model] : [])];
if (ctx.dryRun) return dryRun(ctx, 'pi', [...head, `<${agentName}>`], `<${agentName}>`);

@@ -179,0 +180,0 @@ const instructions = await readInjectedAgent(ctx.cwd, this.agentFile(agentName), stripFrontmatter);

{
"name": "gspec",
"version": "2.5.1",
"version": "2.6.0",
"description": "Install gspec specification commands for Claude Code, Cursor, and other AI tools",

@@ -5,0 +5,0 @@ "main": "bin/gspec.js",

@@ -30,2 +30,4 @@ # gspec

The quality loop is tuned to converge and to resume cheaply. A gate fails only on a **blocker/major** finding — minor/nit notes pass with the notes recorded as advisory — so it reaches a finished state instead of polishing forever. Every failing verdict, including ones a self-heal recovers from, is kept in full in `.gspec/build/qa-failures.md` so you can study and tune the loop, and each stage reports its elapsed time. On `--resume`, a stage you left failed is **re-validated in place** (honoring any hand-edit you made to unblock it) rather than restarted from scratch, and feature PRDs that already passed — or were already written — are skipped instead of regenerated.
```bash

@@ -50,2 +52,57 @@ /gspec-build # in your harness — brief interview, then unattended

**Per-agent models (cost control).** The build runs each stage as its own agent, and you can assign each a model — so the checkers and foundations can run on a cheaper model while architecture and implementation keep the strong one. Add a `models` map to `.gspec/config.json` (this project) or `~/.gspec/config.json` (your global default; the project file overrides it). Selectors resolve most-specific-first — exact agent name, then role tier (`writer`, `qa`, `planner`, `implementer`, `researcher`, `inspector`), then `default`:
```jsonc
// ~/.gspec/config.json
{
"models": {
"default": "claude-sonnet-5", // any agent with no better match
"qa": "claude-haiku-4-5", // every *-validator
"architecture-writer": "claude-opus-4-8", // one specific agent
"implementer": "claude-opus-4-8"
}
}
```
With no `models` map, every agent runs on the engine/CLI default, unchanged. The model string passes straight to the engine's `--model`, so any model your CLI accepts works.
**Recommended starting points.** The idea is the same on every engine, expressed with the `writer` and `qa` role tiers plus two overrides:
- **`writer` → a balanced model** — the everyday authoring (profile, stack, practices, style, feature, research PRDs).
- **`qa` → a cheap/fast model** — every `*-validator`; checking a spec needs far less horsepower than writing one.
- **`architecture-writer` and `implementer` → a strong model** — the two load-bearing jobs (the system design and the code), pinned by name so they beat the `writer`/`default` tier.
- **`default` → the balanced model** — catches the planners and anything else.
- **Claude engine** — strong `claude-opus-4-8`, balanced `claude-sonnet-5`, cheap `claude-haiku-4-5`:
```jsonc
// ~/.gspec/config.json
{
"models": {
"default": "claude-sonnet-5", // planners and anything unlisted
"writer": "claude-sonnet-5", // every *-writer (balanced)
"qa": "claude-haiku-4-5", // every *-validator (cheap)
"architecture-writer": "claude-opus-4-8", // override: the system design
"implementer": "claude-opus-4-8" // override: the code
}
}
```
- **Codex engine** — same shape, using the model IDs your `codex` CLI accepts (run `codex --help` / check your Codex config for the exact strings): a strong reasoning model for `architecture-writer`/`implementer`, a mid model for the `writer` tier and `default`, and a small/fast model for `qa`. For example:
```jsonc
// .gspec/config.json (target: codex)
{
"models": {
"default": "gpt-5", // planners and anything unlisted
"writer": "gpt-5", // every *-writer (balanced)
"qa": "gpt-5-mini", // every *-validator (cheap)
"architecture-writer": "gpt-5-codex", // override: the system design
"implementer": "gpt-5-codex" // override: the code
}
}
```
Model names shown are illustrative — use whatever your installed `codex` accepts for `--model`; gspec passes the string through unchanged.
### The spec-by-spec workflow

@@ -52,0 +109,0 @@

Sorry, the diff of this file is too big to display