+67
-0
| // 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); |
+1
-1
| { | ||
| "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", |
+57
-0
@@ -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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2642691
0.3%5287
1.5%366
18.45%34
3.03%