@aexol/spectral 0.9.21 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@
6
6
  * ---
7
7
  * name: my-agent
8
8
  * description: What this agent does
9
+ * mode: subagent # or "primary" to inject into the main system prompt
9
10
  * tools: read, grep, find, ls
10
11
  * model: claude-haiku-4-5
11
12
  * ---
@@ -16,9 +17,11 @@
16
17
  * .spectral/agents/*.md — Project-level (opt-in via agentScope)
17
18
  */
18
19
  export type AgentScope = "user" | "project" | "both";
20
+ export type AgentMode = "primary" | "subagent";
19
21
  export interface AgentConfig {
20
22
  name: string;
21
23
  description: string;
24
+ mode: AgentMode;
22
25
  tools?: string[];
23
26
  model?: string;
24
27
  systemPrompt: string;
@@ -29,7 +32,20 @@ export interface AgentDiscoveryResult {
29
32
  agents: AgentConfig[];
30
33
  projectAgentsDir: string | null;
31
34
  }
32
- export declare function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult;
35
+ export declare function discoverAgents(cwd: string, scope: AgentScope, agentDir?: string): AgentDiscoveryResult;
36
+ export declare function discoverPrimaryAgent(cwd: string, scope?: AgentScope, agentDir?: string): AgentConfig | null;
37
+ /**
38
+ * Prepend the primary agent's system prompt above the base system prompt.
39
+ *
40
+ * Used by the per-turn `before_agent_start` hook so the primary-agent
41
+ * toggle takes effect mid-session (on the next user message) rather than
42
+ * only at session start.
43
+ *
44
+ * - Returns the base unchanged when no primary agent is provided.
45
+ * - Idempotent: if the base already starts with the primary agent header,
46
+ * the base is returned unchanged (guards against double-injection).
47
+ */
48
+ export declare function injectPrimaryAgentPrompt(base: string | undefined, primaryAgent: Pick<AgentConfig, "name" | "description" | "systemPrompt"> | null): string | undefined;
33
49
  export declare function formatAgentList(agents: AgentConfig[], maxItems: number): {
34
50
  text: string;
35
51
  remaining: number;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAyED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,oBAAoB,CAoBnF;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAQrC"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA2ED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,oBAAoB,CAoBtB;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,UAAmB,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,WAAW,GAAG,IAAI,CAgBpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC,GAAG,IAAI,GAC9E,MAAM,GAAG,SAAS,CAOpB;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAQrC"}
@@ -6,6 +6,7 @@
6
6
  * ---
7
7
  * name: my-agent
8
8
  * description: What this agent does
9
+ * mode: subagent # or "primary" to inject into the main system prompt
9
10
  * tools: read, grep, find, ls
10
11
  * model: claude-haiku-4-5
11
12
  * ---
@@ -17,6 +18,7 @@
17
18
  */
18
19
  import * as fs from "node:fs";
19
20
  import * as path from "node:path";
21
+ import chalk from "chalk";
20
22
  import { getAgentDir, parseFrontmatter } from "../sdk/coding-agent/index.js";
21
23
  function loadAgentsFromDir(dir, source) {
22
24
  const agents = [];
@@ -51,9 +53,11 @@ function loadAgentsFromDir(dir, source) {
51
53
  ?.split(",")
52
54
  .map((t) => t.trim())
53
55
  .filter(Boolean);
56
+ const mode = frontmatter.mode === "primary" ? "primary" : "subagent";
54
57
  agents.push({
55
58
  name: frontmatter.name,
56
59
  description: frontmatter.description,
60
+ mode,
57
61
  tools: tools && tools.length > 0 ? tools : undefined,
58
62
  model: frontmatter.model,
59
63
  systemPrompt: body,
@@ -83,8 +87,8 @@ function findNearestProjectAgentsDir(cwd) {
83
87
  currentDir = parentDir;
84
88
  }
85
89
  }
86
- export function discoverAgents(cwd, scope) {
87
- const userDir = path.join(getAgentDir(), "agents");
90
+ export function discoverAgents(cwd, scope, agentDir) {
91
+ const userDir = path.join(agentDir ?? getAgentDir(), "agents");
88
92
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
89
93
  const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
90
94
  const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
@@ -105,6 +109,42 @@ export function discoverAgents(cwd, scope) {
105
109
  }
106
110
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
107
111
  }
112
+ export function discoverPrimaryAgent(cwd, scope = "both", agentDir) {
113
+ const { agents } = discoverAgents(cwd, scope, agentDir);
114
+ const primaries = agents.filter((a) => a.mode === "primary");
115
+ if (primaries.length === 0)
116
+ return null;
117
+ if (primaries.length > 1) {
118
+ // User-level primary wins over project-level primary; otherwise first wins.
119
+ const userPrimary = primaries.find((a) => a.source === "user");
120
+ const chosen = userPrimary ?? primaries[0];
121
+ console.warn(chalk.yellow(`Multiple primary agents found (${primaries.map((a) => a.name).join(", ")}). Using "${chosen.name}".`));
122
+ return chosen;
123
+ }
124
+ return primaries[0];
125
+ }
126
+ /**
127
+ * Prepend the primary agent's system prompt above the base system prompt.
128
+ *
129
+ * Used by the per-turn `before_agent_start` hook so the primary-agent
130
+ * toggle takes effect mid-session (on the next user message) rather than
131
+ * only at session start.
132
+ *
133
+ * - Returns the base unchanged when no primary agent is provided.
134
+ * - Idempotent: if the base already starts with the primary agent header,
135
+ * the base is returned unchanged (guards against double-injection).
136
+ */
137
+ export function injectPrimaryAgentPrompt(base, primaryAgent) {
138
+ if (!primaryAgent)
139
+ return base;
140
+ const header = `# Primary Agent: ${primaryAgent.name}\n\n${primaryAgent.description}`;
141
+ const block = `${header}\n\n${primaryAgent.systemPrompt}`;
142
+ if (!base)
143
+ return block;
144
+ if (base.startsWith(header))
145
+ return base;
146
+ return `${block}\n\n---\n\n${base}`;
147
+ }
108
148
  export function formatAgentList(agents, maxItems) {
109
149
  if (agents.length === 0)
110
150
  return { text: "none", remaining: 0 };
@@ -25,5 +25,7 @@
25
25
  * - Access to observational memory via the recall tool
26
26
  */
27
27
  import { type ExtensionAPI } from "../sdk/coding-agent/index.js";
28
- export default function subagentExtension(ext: ExtensionAPI): void;
28
+ export default function subagentExtension(ext: ExtensionAPI, opts?: {
29
+ disabledAgents?: string[];
30
+ }): void;
29
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAqetC,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAqTjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAuetC,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA+UvG"}
@@ -28,6 +28,7 @@ import { agentLoop } from "../sdk/agent-core/index.js";
28
28
  import { StringEnum } from "../sdk/ai/index.js";
29
29
  import { Type } from "typebox";
30
30
  import { discoverAgents } from "./agents.js";
31
+ import { loadAgentSettings } from "../server/handlers/agent-settings.js";
31
32
  // ---------------------------------------------------------------------------
32
33
  // Built-in tool resolution (no spawn needed — tools are registered in-process)
33
34
  // ---------------------------------------------------------------------------
@@ -175,7 +176,8 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
175
176
  // Core — run a single subagent in-process via agentLoop
176
177
  // ---------------------------------------------------------------------------
177
178
  async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, ext, ctx) {
178
- const agent = agents.find((a) => a.name === agentName);
179
+ const normalizedAgentName = agentName.replace(/^@/, "");
180
+ const agent = agents.find((a) => a.name === normalizedAgentName);
179
181
  if (!agent) {
180
182
  const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
181
183
  return {
@@ -355,8 +357,8 @@ const ChainItem = Type.Object({
355
357
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
356
358
  });
357
359
  const AgentScopeSchema = StringEnum(["user", "project", "both"], {
358
- description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
359
- default: "user",
360
+ description: 'Which agent directories to use. Default: "both" (user + project agents).',
361
+ default: "both",
360
362
  });
361
363
  const SubagentParams = Type.Object({
362
364
  agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
@@ -379,15 +381,15 @@ const SubagentParams = Type.Object({
379
381
  // ---------------------------------------------------------------------------
380
382
  // Extension entry point
381
383
  // ---------------------------------------------------------------------------
382
- export default function subagentExtension(ext) {
384
+ export default function subagentExtension(ext, opts) {
383
385
  ext.registerTool({
384
386
  name: "subagent",
385
387
  label: "Subagent",
386
388
  description: [
387
389
  "Delegate tasks to specialized subagents with isolated context.",
388
390
  "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
389
- 'Default agent scope is "user" (from ~/.spectral/agent/agents).',
390
- 'To enable project-local agents in .spectral/agents, set agentScope: "both" (or "project").',
391
+ 'Default agent scope is "both" (user agents from ~/.spectral/agent/agents + project agents from .spectral/agents).',
392
+ 'Set agentScope: "user" or "project" to limit scope.'
391
393
  ].join(" "),
392
394
  promptSnippet: "Run a subagent to investigate, plan, review, or implement code changes",
393
395
  promptGuidelines: [
@@ -395,10 +397,36 @@ export default function subagentExtension(ext) {
395
397
  ],
396
398
  parameters: SubagentParams,
397
399
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
398
- const agentScope = params.agentScope ?? "user";
400
+ // Re-load agent settings from disk on every call so that UI toggles
401
+ // (subagentsEnabled / disabledSubagents) take effect immediately on
402
+ // running sessions, not just on the next session start.
403
+ const liveSettings = loadAgentSettings(ctx.cwd);
404
+ if (!liveSettings.subagentsEnabled) {
405
+ return {
406
+ content: [
407
+ {
408
+ type: "text",
409
+ text: "Subagents are disabled in agent settings. Enable subagents via the agent settings panel to delegate tasks.",
410
+ },
411
+ ],
412
+ details: {
413
+ mode: "single",
414
+ agentScope: (params.agentScope ?? "both"),
415
+ projectAgentsDir: null,
416
+ results: [],
417
+ },
418
+ };
419
+ }
420
+ const agentScope = params.agentScope ?? "both";
399
421
  const discovery = discoverAgents(ctx.cwd, agentScope);
400
- const agents = discovery.agents;
401
- const confirmProjectAgents = params.confirmProjectAgents ?? true;
422
+ // Merge registration-time disabled list (from session start) with the
423
+ // live disabled list (just read from disk) so both are honored.
424
+ const disabledSet = new Set([
425
+ ...(opts?.disabledAgents ?? []),
426
+ ...(liveSettings.disabledSubagents ?? []),
427
+ ]);
428
+ const agents = discovery.agents.filter((a) => !disabledSet.has(a.name));
429
+ const confirmProjectAgents = params.confirmProjectAgents ?? false;
402
430
  const hasChain = (params.chain?.length ?? 0) > 0;
403
431
  const hasTasks = (params.tasks?.length ?? 0) > 0;
404
432
  const hasSingle = Boolean(params.agent && params.task);
@@ -430,12 +458,12 @@ export default function subagentExtension(ext) {
430
458
  const requestedAgentNames = new Set();
431
459
  if (params.chain)
432
460
  for (const step of params.chain)
433
- requestedAgentNames.add(step.agent);
461
+ requestedAgentNames.add(step.agent.replace(/^@/, ""));
434
462
  if (params.tasks)
435
463
  for (const t of params.tasks)
436
- requestedAgentNames.add(t.agent);
464
+ requestedAgentNames.add(t.agent.replace(/^@/, ""));
437
465
  if (params.agent)
438
- requestedAgentNames.add(params.agent);
466
+ requestedAgentNames.add(params.agent.replace(/^@/, ""));
439
467
  const projectAgentsRequested = Array.from(requestedAgentNames)
440
468
  .map((name) => agents.find((a) => a.name === name))
441
469
  .filter((a) => a?.source === "project");
package/dist/cli.js CHANGED
@@ -18,9 +18,15 @@ import { dirname, resolve } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
20
  // ---- Read our own version ----------------------------------------------------
21
- const pkgPath = resolve(__dirname, "..", "package.json");
22
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
23
- const VERSION = pkg.version;
21
+ let VERSION = process.env.npm_package_version ?? "0.0.0";
22
+ try {
23
+ const pkgPath = resolve(__dirname, "..", "package.json");
24
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
25
+ VERSION = pkg.version;
26
+ }
27
+ catch {
28
+ // Standalone Bun binaries do not carry the source package.json.
29
+ }
24
30
  const TAGLINE = "Coding agent that never sleeps";
25
31
  // ---- Branded helpers ---------------------------------------------------------
26
32
  function printVersion() {
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAerE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqWlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAsWlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
@@ -28,7 +28,7 @@
28
28
  * in-process fake backend.
29
29
  */
30
30
  import { readFileSync } from "node:fs";
31
- import { homedir, hostname } from "node:os";
31
+ import { hostname } from "node:os";
32
32
  import { dirname, join, resolve } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { getConfigDir } from "../config.js";
@@ -49,9 +49,17 @@ const DEFAULT_BACKEND_URL = "https://api.aexol.ai";
49
49
  function readPackageVersion() {
50
50
  // dist/commands/serve.js → ../../package.json
51
51
  // src/commands/serve.ts → ../../package.json (tsx / vitest)
52
- const pkgPath = resolve(__dirname, "..", "..", "package.json");
53
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
54
- return pkg.version;
52
+ // In a Bun standalone binary the source tree is not present, so the
53
+ // package.json lookup below fails. Return a fallback version instead of
54
+ // crashing the serve process.
55
+ try {
56
+ const pkgPath = resolve(__dirname, "..", "..", "package.json");
57
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
58
+ return pkg.version;
59
+ }
60
+ catch {
61
+ return process.env.npm_package_version ?? "0.0.0";
62
+ }
55
63
  }
56
64
  /**
57
65
  * Translate a backend HTTP(S) base URL to the relay WS(S) URL.
@@ -118,7 +126,7 @@ export function parseServeArgs(args) {
118
126
  }
119
127
  export async function runServe(opts = {}) {
120
128
  const cfg = await requireLogin();
121
- const cwd = opts.cwd ?? homedir();
129
+ const cwd = opts.cwd ?? process.cwd();
122
130
  const version = readPackageVersion();
123
131
  const installSignalHandlers = opts.installSignalHandlers ?? true;
124
132
  const silent = opts.silent ?? false;
@@ -227,6 +235,7 @@ export async function runServe(opts = {}) {
227
235
  store,
228
236
  manager,
229
237
  cwd,
238
+ agentDir: getAgentDir(),
230
239
  publishMetaEvent: deferredPublishMetaEvent,
231
240
  mcpState: getLiveMcpState() ?? undefined,
232
241
  }).then((response) => {
@@ -48,7 +48,7 @@ import type { RelayClient } from "./client.js";
48
48
  * `id` is populated when the route had an `:id` placeholder.
49
49
  */
50
50
  interface RouteMatch {
51
- route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "get_project_observations" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
51
+ route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "get_project_observations" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "list_agents";
52
52
  id?: string;
53
53
  /** Parsed query params, if the path carried a `?...` suffix. */
54
54
  query?: URLSearchParams;
@@ -79,6 +79,8 @@ export interface RestRequestDeps {
79
79
  manager: SessionStreamManager;
80
80
  /** Working directory for this machine. Used by settings persistence. Defaults to homedir. */
81
81
  cwd?: string;
82
+ /** Agent config dir (`~/.spectral/agent` by default). Passed through to agent-settings persistence. */
83
+ agentDir?: string;
82
84
  /** Optional logger for unexpected errors. Defaults to console.error. */
83
85
  logger?: {
84
86
  error: (...args: unknown[]) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -47,6 +47,7 @@ import { handleSearchProjectFiles } from "../server/handlers/files-search.js";
47
47
  import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleGetProjectObservations, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
48
48
  import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
49
49
  import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
50
+ import { handleGetAgentSettings, handleListAgents, handlePutAgentSettings, } from "../server/handlers/agent-settings.js";
50
51
  import { handleGetSettings, handlePutSettings, } from "../server/handlers/settings.js";
51
52
  import { shutdownState } from "../server/shutdown.js";
52
53
  import { handleAutoResearch } from "./auto-research.js";
@@ -76,6 +77,20 @@ export function matchRoute(method, path) {
76
77
  return { route: "put_settings" };
77
78
  return null;
78
79
  }
80
+ // /api/agent-settings
81
+ if (cleanPath === "/api/agent-settings") {
82
+ if (method === "GET")
83
+ return { route: "get_agent_settings" };
84
+ if (method === "PUT")
85
+ return { route: "put_agent_settings" };
86
+ return null;
87
+ }
88
+ // /api/agents
89
+ if (cleanPath === "/api/agents") {
90
+ if (method === "GET")
91
+ return { route: "list_agents", query };
92
+ return null;
93
+ }
79
94
  // /api/paths/autocomplete
80
95
  if (cleanPath === "/api/paths/autocomplete") {
81
96
  if (method === "GET")
@@ -451,6 +466,14 @@ async function dispatchRoute(match, body, deps) {
451
466
  return handleGetSettings(deps.cwd ?? homedir());
452
467
  case "put_settings":
453
468
  return handlePutSettings(deps.cwd ?? homedir(), asObject(body));
469
+ case "get_agent_settings":
470
+ return handleGetAgentSettings(deps.cwd ?? homedir(), deps.agentDir);
471
+ case "put_agent_settings":
472
+ return handlePutAgentSettings(deps.cwd ?? homedir(), asObject(body), deps.agentDir);
473
+ case "list_agents": {
474
+ const agentCwd = match.query?.get("cwd") || deps.cwd || homedir();
475
+ return handleListAgents(agentCwd, deps.agentDir);
476
+ }
454
477
  }
455
478
  }
456
479
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/sdk/coding-agent/config.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,UAAU,qBAAqB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAsBD,wBAAgB,mBAAmB,IAAI,aAAa,CAcnD;AAyKD,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,iBAAiB,GAAG,SAAS,CAO/B;AAED,wBAAgB,mCAAmC,CAClD,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,MAAM,CAUR;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOhE;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAiBtC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED;;;;+BAI+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAoBD,eAAO,MAAM,YAAY,EAAE,MAA+B,CAAC;AAC3D,eAAO,MAAM,QAAQ,EAAE,MAAyC,CAAC;AACjE,eAAO,MAAM,SAAS,EAAE,MAAmD,CAAC;AAC5E,eAAO,MAAM,eAAe,EAAE,MAAqD,CAAC;AACpF,eAAO,MAAM,OAAO,EAAE,MAA+B,CAAC;AAGtD,eAAO,MAAM,aAAa,QAA+C,CAAC;AAC1E,eAAO,MAAM,eAAe,QAAuD,CAAC;AAEpF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxD;AAMD,gEAAgE;AAChE,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,qCAAqC;AACrC,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/sdk/coding-agent/config.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,UAAU,qBAAqB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAsBD,wBAAgB,mBAAmB,IAAI,aAAa,CAcnD;AAyKD,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,iBAAiB,GAAG,SAAS,CAO/B;AAED,wBAAgB,mCAAmC,CAClD,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,MAAM,CAUR;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOhE;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAiBtC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED;;;;+BAI+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AA4BD,eAAO,MAAM,YAAY,EAAE,MAA+B,CAAC;AAC3D,eAAO,MAAM,QAAQ,EAAE,MAAyC,CAAC;AACjE,eAAO,MAAM,SAAS,EAAE,MAAmD,CAAC;AAC5E,eAAO,MAAM,eAAe,EAAE,MAAqD,CAAC;AACpF,eAAO,MAAM,OAAO,EAAE,MAA+B,CAAC;AAGtD,eAAO,MAAM,aAAa,QAA+C,CAAC;AAC1E,eAAO,MAAM,eAAe,QAAuD,CAAC;AAEpF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxD;AAMD,gEAAgE;AAChE,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,qCAAqC;AACrC,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
@@ -268,7 +268,16 @@ export function getExamplesPath() {
268
268
  export function getChangelogPath() {
269
269
  return resolve(join(getPackageDir(), "CHANGELOG.md"));
270
270
  }
271
- const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
271
+ function readPackageJson() {
272
+ try {
273
+ return JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
274
+ }
275
+ catch {
276
+ // Standalone Bun binaries do not carry the source package.json.
277
+ return {};
278
+ }
279
+ }
280
+ const pkg = readPackageJson();
272
281
  const spectralConfigName = pkg.spectralConfig?.name;
273
282
  export const PACKAGE_NAME = pkg.name || "index.ts";
274
283
  export const APP_NAME = spectralConfigName || "spectral";
@@ -49,6 +49,7 @@
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
51
  import { type AgentSessionEvent } from "../sdk/coding-agent/index.js";
52
+ import { type AgentSettings } from "./handlers/agent-settings.js";
52
53
  import { fetchAllowedModels as defaultFetchAllowedModels } from "../relay/models-fetch.js";
53
54
  import type { SessionMemorySnapshot } from "./storage.js";
54
55
  import type { ImageAttachment, ServerEvent, WireMessage } from "./wire.js";
@@ -116,6 +117,13 @@ export interface AgentBridgeOptions {
116
117
  * can run without a live backend.
117
118
  */
118
119
  fetchAllowedModels?: FetchAllowedModelsFn;
120
+ /**
121
+ * Agent-behavior toggles (primary-agent injection, subagent tool).
122
+ * When omitted, settings are loaded from `settings.json` at `start()`
123
+ * time. Changes take effect on the next session/bridge — a running
124
+ * session is not hot-reloaded.
125
+ */
126
+ agentSettings?: AgentSettings;
119
127
  }
120
128
  export declare class AgentBridge {
121
129
  private session?;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
@@ -55,6 +55,8 @@ import { existsSync, statSync } from "node:fs";
55
55
  import { dirname, join, resolve } from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
57
  import aexolMcpExtension from "../extensions/aexol-mcp.js";
58
+ import { discoverPrimaryAgent, injectPrimaryAgentPrompt } from "../agent/agents.js";
59
+ import { loadAgentSettings } from "./handlers/agent-settings.js";
58
60
  import kanbanBridgeExtension from "../extensions/kanban-bridge.js";
59
61
  import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
60
62
  import subagentExt from "../agent/index.js";
@@ -417,7 +419,30 @@ export class AgentBridge {
417
419
  async start() {
418
420
  if (this.disposed)
419
421
  throw new Error("AgentBridge already disposed");
420
- const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (ext) => { spectralVisionExtension(ext); }, async (ext) => { subagentExt(ext); }, async (ext) => { designerExtension(ext); }, async (ext) => { observationalMemory(ext); }];
422
+ const extensionFactories = [
423
+ // Primary-agent prompt is injected per-turn via before_agent_start so the
424
+ // primaryAgentEnabled toggle takes effect mid-session (next user message).
425
+ async (ext) => {
426
+ ext.on("before_agent_start", (event) => {
427
+ const settings = loadAgentSettings(this.opts.cwd, this.opts.agentDir);
428
+ if (!settings.primaryAgentEnabled)
429
+ return;
430
+ const primaryAgent = discoverPrimaryAgent(this.opts.cwd, "both", this.opts.agentDir);
431
+ const injected = injectPrimaryAgentPrompt(event.systemPrompt, primaryAgent);
432
+ if (injected === event.systemPrompt)
433
+ return;
434
+ return { systemPrompt: injected };
435
+ });
436
+ },
437
+ aexolMcpExtension, kanbanBridgeExtension, async (ext) => { spectralVisionExtension(ext); }, async (ext) => { designerExtension(ext); }, async (ext) => { observationalMemory(ext); }
438
+ ];
439
+ // Agent-behavior toggles. Loaded once at bridge construction so changes
440
+ // take effect on the next session (not mid-turn).
441
+ const agentSettings = this.opts.agentSettings ?? loadAgentSettings(this.opts.cwd, this.opts.agentDir);
442
+ // Only register the subagent tool when the toggle is enabled.
443
+ if (agentSettings.subagentsEnabled) {
444
+ extensionFactories.splice(4, 0, async (ext) => { subagentExt(ext, { disabledAgents: agentSettings.disabledSubagents }); });
445
+ }
421
446
  // Load spectral-mcp-adapter via jiti so tsc never crawls its .ts files in
422
447
  // node_modules. The static `import` was causing tsc to type-check
423
448
  // spectral-mcp-adapter's source and fail the build on its type errors.
@@ -448,6 +473,9 @@ export class AgentBridge {
448
473
  // .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
449
474
  // work out of the box.
450
475
  const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
476
+ // Primary-agent prompt injection happens per-turn via the before_agent_start
477
+ // extension registered above, so the primaryAgentEnabled toggle takes effect
478
+ // mid-session (on the next user message) rather than only at session start.
451
479
  const resourceLoader = new DefaultResourceLoader({
452
480
  cwd: this.opts.cwd,
453
481
  agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Agent-behavior toggles stored under the `agent` namespace in
3
+ * `settings.json`. These control whether the primary-agent system prompt
4
+ * is injected and whether the `subagent` tool is registered.
5
+ *
6
+ * Both default to `true` so existing behaviour is preserved when the
7
+ * namespace is absent.
8
+ */
9
+ export interface AgentSettings {
10
+ primaryAgentEnabled: boolean;
11
+ subagentsEnabled: boolean;
12
+ /** Names of individual subagents that have been explicitly disabled. */
13
+ disabledSubagents: string[];
14
+ }
15
+ export declare const AGENT_SETTINGS_DEFAULTS: AgentSettings;
16
+ export interface PutAgentSettingsInput {
17
+ primaryAgentEnabled?: unknown;
18
+ subagentsEnabled?: unknown;
19
+ disabledSubagents?: unknown;
20
+ }
21
+ export interface PutAgentSettingsOutput extends AgentSettings {
22
+ ok: true;
23
+ }
24
+ /**
25
+ * Load agent settings by merging defaults ← global ← project overrides.
26
+ * Mirrors the precedence pattern used by `loadConfig` for observational
27
+ * memory config.
28
+ */
29
+ export declare function loadAgentSettings(cwd: string, agentDir?: string): AgentSettings;
30
+ export declare function handleGetAgentSettings(cwd: string, agentDir?: string): AgentSettings;
31
+ /**
32
+ * Persist a partial update to the `agent` namespace in the **global**
33
+ * `settings.json` (per-machine, matching the existing memory-settings
34
+ * behaviour). Project-level overrides are read-only from the REST API.
35
+ *
36
+ * Only boolean values are accepted for the two known keys; anything else
37
+ * is a `400 BadRequestError`.
38
+ */
39
+ export declare function handlePutAgentSettings(cwd: string, body: PutAgentSettingsInput, agentDir?: string): PutAgentSettingsOutput;
40
+ export interface AgentInfo {
41
+ name: string;
42
+ description: string;
43
+ mode: string;
44
+ source: string;
45
+ }
46
+ export declare function handleListAgents(cwd: string, agentDir?: string): {
47
+ agents: AgentInfo[];
48
+ };
49
+ //# sourceMappingURL=agent-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/agent-settings.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,eAAO,MAAM,uBAAuB,EAAE,aAIrC,CAAC;AAKF,MAAM,WAAW,qBAAqB;IACpC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAuB,SAAQ,aAAa;IAC3D,EAAE,EAAE,IAAI,CAAC;CACV;AA+BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAQf;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAEf;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,qBAAqB,EAC3B,QAAQ,CAAC,EAAE,MAAM,GAChB,sBAAsB,CAuCxB;AAMD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,CAUzB"}
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir } from "../../sdk/coding-agent/index.js";
4
+ import { discoverAgents } from "../../agent/agents.js";
5
+ import { BadRequestError } from "./errors.js";
6
+ import { withSettingsLock } from "./settings-lock.js";
7
+ export const AGENT_SETTINGS_DEFAULTS = {
8
+ primaryAgentEnabled: true,
9
+ subagentsEnabled: true,
10
+ disabledSubagents: [],
11
+ };
12
+ const AGENT_NAMESPACE = "agent";
13
+ const ALLOWED_KEYS = new Set(["primaryAgentEnabled", "subagentsEnabled", "disabledSubagents"]);
14
+ /**
15
+ * Read the `agent` namespace from a single `settings.json` file.
16
+ * Returns `{}` when the file or namespace is absent or unparseable.
17
+ */
18
+ function readAgentNamespace(path) {
19
+ if (!existsSync(path))
20
+ return {};
21
+ try {
22
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
23
+ const ns = raw[AGENT_NAMESPACE];
24
+ if (!ns || typeof ns !== "object")
25
+ return {};
26
+ const obj = ns;
27
+ const result = {};
28
+ if (typeof obj.primaryAgentEnabled === "boolean") {
29
+ result.primaryAgentEnabled = obj.primaryAgentEnabled;
30
+ }
31
+ if (typeof obj.subagentsEnabled === "boolean") {
32
+ result.subagentsEnabled = obj.subagentsEnabled;
33
+ }
34
+ if (Array.isArray(obj.disabledSubagents)) {
35
+ result.disabledSubagents = obj.disabledSubagents.filter((s) => typeof s === "string");
36
+ }
37
+ return result;
38
+ }
39
+ catch {
40
+ return {};
41
+ }
42
+ }
43
+ /**
44
+ * Load agent settings by merging defaults ← global ← project overrides.
45
+ * Mirrors the precedence pattern used by `loadConfig` for observational
46
+ * memory config.
47
+ */
48
+ export function loadAgentSettings(cwd, agentDir) {
49
+ const globalPath = join(agentDir ?? getAgentDir(), "settings.json");
50
+ const projectPath = join(cwd, ".spectral", "settings.json");
51
+ return {
52
+ ...AGENT_SETTINGS_DEFAULTS,
53
+ ...readAgentNamespace(globalPath),
54
+ ...readAgentNamespace(projectPath),
55
+ };
56
+ }
57
+ export function handleGetAgentSettings(cwd, agentDir) {
58
+ return loadAgentSettings(cwd, agentDir);
59
+ }
60
+ /**
61
+ * Persist a partial update to the `agent` namespace in the **global**
62
+ * `settings.json` (per-machine, matching the existing memory-settings
63
+ * behaviour). Project-level overrides are read-only from the REST API.
64
+ *
65
+ * Only boolean values are accepted for the two known keys; anything else
66
+ * is a `400 BadRequestError`.
67
+ */
68
+ export function handlePutAgentSettings(cwd, body, agentDir) {
69
+ const patch = {};
70
+ for (const key of ["primaryAgentEnabled", "subagentsEnabled"]) {
71
+ if (body[key] !== undefined) {
72
+ if (typeof body[key] !== "boolean") {
73
+ throw new BadRequestError(`agent setting "${key}" must be a boolean, got ${typeof body[key]}`);
74
+ }
75
+ patch[key] = body[key];
76
+ }
77
+ }
78
+ // disabledSubagents is a full-replacement string array
79
+ if (body.disabledSubagents !== undefined) {
80
+ if (!Array.isArray(body.disabledSubagents) ||
81
+ !body.disabledSubagents.every((s) => typeof s === "string")) {
82
+ throw new BadRequestError(`agent setting "disabledSubagents" must be a string array`);
83
+ }
84
+ patch.disabledSubagents = body.disabledSubagents;
85
+ }
86
+ if (Object.keys(patch).length === 0) {
87
+ throw new BadRequestError(`agent settings body must include at least one of: ${[...ALLOWED_KEYS].join(", ")}`);
88
+ }
89
+ // Persist to the global file only. The lock is shared with the
90
+ // observational-memory handler so concurrent writes cannot corrupt
91
+ // each other's namespace.
92
+ withSettingsLock({ agentDir, namespace: AGENT_NAMESPACE }, (current) => {
93
+ return { ...current, ...patch };
94
+ });
95
+ // Return the fully resolved settings (global + project merge) so the
96
+ // caller sees the effective state, not just what we wrote.
97
+ return { ok: true, ...loadAgentSettings(cwd, agentDir) };
98
+ }
99
+ export function handleListAgents(cwd, agentDir) {
100
+ const { agents } = discoverAgents(cwd, "both", agentDir);
101
+ return {
102
+ agents: agents.map((a) => ({
103
+ name: a.name,
104
+ description: a.description,
105
+ mode: a.mode,
106
+ source: a.source,
107
+ })),
108
+ };
109
+ }
@@ -0,0 +1,36 @@
1
+ import { BadRequestError } from "./errors.js";
2
+ /**
3
+ * Acquire a synchronous file lock with bounded retries on `ELOCKED`.
4
+ *
5
+ * Shared by all settings handlers that persist to `settings.json` so
6
+ * concurrent writes from different namespaces (observational-memory,
7
+ * agent) cannot interleave and corrupt the file.
8
+ */
9
+ export declare function acquireLockSyncWithRetry(path: string): () => void;
10
+ /**
11
+ * Resolve the `settings.json` path for a given agent directory.
12
+ * Falls back to the default agent dir (`~/.spectral/agent`) when
13
+ * `agentDir` is omitted.
14
+ */
15
+ export declare function settingsFilePath(agentDir?: string): string;
16
+ /**
17
+ * Read-persist cycle for a single namespace inside `settings.json`,
18
+ * guarded by a file-level lock so concurrent writes across namespaces
19
+ * are serialized.
20
+ *
21
+ * - Reads the **entire** file (preserving all top-level keys).
22
+ * - Extracts `parsed[namespace]` (or `{}` if absent).
23
+ * - Passes the current namespace object to `fn`. If `fn` returns
24
+ * `undefined`, no write occurs. Otherwise the returned object
25
+ * replaces the namespace and the full file is written back.
26
+ *
27
+ * The lock is acquired **before** writing (and also before reading if
28
+ * the file already exists) so we never observe a half-written file.
29
+ */
30
+ export declare function withSettingsLock(opts: {
31
+ agentDir?: string;
32
+ namespace: string;
33
+ }, fn: (current: Record<string, unknown>) => Record<string, unknown> | undefined): void;
34
+ /** Re-exported for handler convenience. */
35
+ export { BadRequestError };
36
+ //# sourceMappingURL=settings-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-lock.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,IAAI,CAsBjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC9C,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5E,IAAI,CA4BN;AAED,2CAA2C;AAC3C,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ import { getAgentDir } from "../../sdk/coding-agent/index.js";
5
+ import { BadRequestError } from "./errors.js";
6
+ /**
7
+ * Acquire a synchronous file lock with bounded retries on `ELOCKED`.
8
+ *
9
+ * Shared by all settings handlers that persist to `settings.json` so
10
+ * concurrent writes from different namespaces (observational-memory,
11
+ * agent) cannot interleave and corrupt the file.
12
+ */
13
+ export function acquireLockSyncWithRetry(path) {
14
+ const maxAttempts = 10;
15
+ const delayMs = 20;
16
+ let lastError;
17
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
18
+ try {
19
+ return lockfile.lockSync(path, { realpath: false });
20
+ }
21
+ catch (error) {
22
+ const code = typeof error === "object" && error !== null && "code" in error
23
+ ? String(error.code)
24
+ : undefined;
25
+ if (code !== "ELOCKED" || attempt === maxAttempts) {
26
+ throw error;
27
+ }
28
+ lastError = error;
29
+ const start = Date.now();
30
+ while (Date.now() - start < delayMs) {
31
+ }
32
+ }
33
+ }
34
+ throw lastError ?? new Error("Failed to acquire settings lock");
35
+ }
36
+ /**
37
+ * Resolve the `settings.json` path for a given agent directory.
38
+ * Falls back to the default agent dir (`~/.spectral/agent`) when
39
+ * `agentDir` is omitted.
40
+ */
41
+ export function settingsFilePath(agentDir) {
42
+ return join(agentDir ?? getAgentDir(), "settings.json");
43
+ }
44
+ /**
45
+ * Read-persist cycle for a single namespace inside `settings.json`,
46
+ * guarded by a file-level lock so concurrent writes across namespaces
47
+ * are serialized.
48
+ *
49
+ * - Reads the **entire** file (preserving all top-level keys).
50
+ * - Extracts `parsed[namespace]` (or `{}` if absent).
51
+ * - Passes the current namespace object to `fn`. If `fn` returns
52
+ * `undefined`, no write occurs. Otherwise the returned object
53
+ * replaces the namespace and the full file is written back.
54
+ *
55
+ * The lock is acquired **before** writing (and also before reading if
56
+ * the file already exists) so we never observe a half-written file.
57
+ */
58
+ export function withSettingsLock(opts, fn) {
59
+ const path = settingsFilePath(opts.agentDir);
60
+ const dir = dirname(path);
61
+ let release;
62
+ try {
63
+ const fileExists = existsSync(path);
64
+ if (fileExists) {
65
+ release = acquireLockSyncWithRetry(path);
66
+ }
67
+ const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
68
+ const parsed = JSON.parse(raw);
69
+ const ns = (parsed[opts.namespace] ?? {});
70
+ const next = fn(ns);
71
+ if (next !== undefined) {
72
+ parsed[opts.namespace] = { ...ns, ...next };
73
+ if (!existsSync(dir)) {
74
+ mkdirSync(dir, { recursive: true });
75
+ }
76
+ if (!release) {
77
+ release = acquireLockSyncWithRetry(path);
78
+ }
79
+ writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
80
+ }
81
+ }
82
+ finally {
83
+ if (release) {
84
+ release();
85
+ }
86
+ }
87
+ }
88
+ /** Re-exported for handler convenience. */
89
+ export { BadRequestError };
@@ -1,14 +1,3 @@
1
- /**
2
- * Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
3
- *
4
- * Settings are stored in the machine's global `settings.json` under the
5
- * `"observational-memory"` namespace (see `src/memory/config.ts`).
6
- *
7
- * GET /api/settings → returns full observational-memory config
8
- * PUT /api/settings → updates a single key via `{ key, value }` body
9
- *
10
- * All paths are machine-level — no session context needed.
11
- */
12
1
  export interface PutSettingsInput {
13
2
  key?: unknown;
14
3
  value?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAsFD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAShE;AAMD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,iBAAiB,CA0BnB"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,iBAAiB;IAChC,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAShE;AACD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,iBAAiB,CAoBnB"}
@@ -1,93 +1,19 @@
1
- /**
2
- * Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
3
- *
4
- * Settings are stored in the machine's global `settings.json` under the
5
- * `"observational-memory"` namespace (see `src/memory/config.ts`).
6
- *
7
- * GET /api/settings → returns full observational-memory config
8
- * PUT /api/settings → updates a single key via `{ key, value }` body
9
- *
10
- * All paths are machine-level — no session context needed.
11
- */
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import lockfile from "proper-lockfile";
15
- import { getAgentDir } from "../../sdk/coding-agent/index.js";
16
1
  import { loadConfig } from "../../memory/config.js";
17
2
  import { BadRequestError } from "./errors.js";
18
- /** Recognised setting keys (dot-separated path within the config object). */
3
+ import { withSettingsLock } from "./settings-lock.js";
19
4
  const ALLOWED_KEYS = new Set([
20
5
  "observationThresholdTokens",
21
6
  "compactionThresholdTokens",
22
7
  "reflectionThresholdTokens",
23
8
  ]);
24
- /** Sane bounds for token thresholds. */
25
9
  const MIN_TOKENS = 1_000;
26
10
  const MAX_TOKENS = 1_000_000;
27
- // ---------------------------------------------------------------------------
28
- // File helpers (inlined to avoid pulling in the full SettingsManager)
29
- // ---------------------------------------------------------------------------
30
- function acquireLockSyncWithRetry(path) {
31
- const maxAttempts = 10;
32
- const delayMs = 20;
33
- let lastError;
34
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
35
- try {
36
- return lockfile.lockSync(path, { realpath: false });
37
- }
38
- catch (error) {
39
- const code = typeof error === "object" && error !== null && "code" in error
40
- ? String(error.code)
41
- : undefined;
42
- if (code !== "ELOCKED" || attempt === maxAttempts) {
43
- throw error;
44
- }
45
- lastError = error;
46
- const start = Date.now();
47
- while (Date.now() - start < delayMs) {
48
- // busy-wait; synchronous by design
49
- }
50
- }
51
- }
52
- throw lastError ?? new Error("Failed to acquire settings lock");
53
- }
54
- /**
55
- * Atomically read-modify-write the global `settings.json` under the
56
- * `"observational-memory"` namespace.
57
- */
58
11
  function withObservationalMemoryLock(fn) {
59
- const path = join(getAgentDir(), "settings.json");
60
- const dir = dirname(path);
61
- let release;
62
- try {
63
- const fileExists = existsSync(path);
64
- if (fileExists) {
65
- release = acquireLockSyncWithRetry(path);
66
- }
67
- const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
68
- const parsed = JSON.parse(raw);
69
- const ns = (parsed["observational-memory"] ?? {});
70
- const next = fn(ns);
71
- if (next !== undefined) {
72
- parsed["observational-memory"] = { ...ns, ...next };
73
- if (!existsSync(dir)) {
74
- mkdirSync(dir, { recursive: true });
75
- }
76
- if (!release) {
77
- release = acquireLockSyncWithRetry(path);
78
- }
79
- writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
80
- }
81
- }
82
- finally {
83
- if (release) {
84
- release();
85
- }
86
- }
12
+ withSettingsLock({ namespace: "observational-memory" }, (current) => {
13
+ const next = fn(current);
14
+ return next;
15
+ });
87
16
  }
88
- // ---------------------------------------------------------------------------
89
- // GET handler
90
- // ---------------------------------------------------------------------------
91
17
  export function handleGetSettings(cwd) {
92
18
  const config = loadConfig(cwd);
93
19
  return {
@@ -98,16 +24,11 @@ export function handleGetSettings(cwd) {
98
24
  debugLog: config.debugLog,
99
25
  };
100
26
  }
101
- // ---------------------------------------------------------------------------
102
- // PUT handler
103
- // ---------------------------------------------------------------------------
104
27
  export function handlePutSettings(cwd, body) {
105
- // Validate key
106
28
  if (typeof body.key !== "string" || !ALLOWED_KEYS.has(body.key)) {
107
29
  throw new BadRequestError(`settings key must be one of: ${[...ALLOWED_KEYS].join(", ")}`);
108
30
  }
109
31
  const key = body.key;
110
- // Validate value
111
32
  if (typeof body.value !== "number" || !Number.isInteger(body.value)) {
112
33
  throw new BadRequestError("settings value must be an integer");
113
34
  }
@@ -115,7 +36,6 @@ export function handlePutSettings(cwd, body) {
115
36
  throw new BadRequestError(`token threshold must be between ${MIN_TOKENS.toLocaleString()} and ${MAX_TOKENS.toLocaleString()}`);
116
37
  }
117
38
  const value = body.value;
118
- // Persist
119
39
  withObservationalMemoryLock((current) => {
120
40
  return { ...current, [key]: value };
121
41
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.21",
3
+ "version": "0.9.25",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,