specrails-core 6.2.2 → 6.4.0
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.
- package/README.md +2 -2
- package/dist/agent-runtime/cli.js +1 -1
- package/dist/agent-runtime/compact/environment.d.ts +19 -2
- package/dist/agent-runtime/compact/environment.js +71 -6
- package/dist/agent-runtime/config.js +68 -53
- package/dist/agent-runtime/engine/compiler.js +10 -2
- package/dist/agent-runtime/engine/contracts.d.ts +16 -0
- package/dist/agent-runtime/engine/pieces/control.js +11 -2
- package/dist/agent-runtime/engine/pieces/role-turn.js +35 -2
- package/dist/agent-runtime/engine/pieces/verify.d.ts +10 -3
- package/dist/agent-runtime/engine/pieces/verify.js +107 -30
- package/dist/agent-runtime/executor-types.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +9 -55
- package/dist/agent-runtime/prompts.d.ts +1 -1
- package/dist/agent-runtime/prompts.js +58 -35
- package/dist/agent-runtime/verification-repair.d.ts +26 -0
- package/dist/agent-runtime/verification-repair.js +88 -0
- package/dist/installer/commands/framework.d.ts +7 -3
- package/dist/installer/commands/framework.js +6 -8
- package/dist/installer/commands/init.d.ts +8 -15
- package/dist/installer/commands/init.js +22 -64
- package/dist/installer/phases/install-config.d.ts +8 -2
- package/dist/installer/phases/install-config.js +24 -17
- package/dist/installer/phases/scaffold.d.ts +27 -79
- package/dist/installer/phases/scaffold.js +100 -535
- package/integration-contract.json +7 -10
- package/package.json +1 -1
- package/schemas/agent-runtime.schema.json +9 -7
- package/templates/agents/sr-architect.md +0 -29
- package/templates/agents/sr-developer.md +0 -29
- package/templates/agents/sr-reviewer.md +0 -28
- package/templates/codex-skills/rails/sr-architect/SKILL.md +0 -29
- package/templates/codex-skills/rails/sr-developer/SKILL.md +0 -29
- package/templates/codex-skills/rails/sr-reviewer/SKILL.md +0 -28
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hostPrecondition, installEnvironment } from './compact/environment.js';
|
|
4
|
+
import { guardrailEnabled } from './guardrails.js';
|
|
5
|
+
const BLOCKER_KINDS = new Set(['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope']);
|
|
6
|
+
const BLOCKER_TEXT_LIMIT = 2_000;
|
|
7
|
+
/** Where a missing dependency install belongs: the nearest directory with a lockfile or manifest above the failing command, else the repository root. */
|
|
8
|
+
const INSTALL_MARKERS = ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Cargo.toml'];
|
|
9
|
+
export function installRoots(context, commands) {
|
|
10
|
+
const roots = [];
|
|
11
|
+
for (const command of commands) {
|
|
12
|
+
const repository = context.repositories.find(repo => repo.id === command.repositoryId);
|
|
13
|
+
if (!repository)
|
|
14
|
+
continue;
|
|
15
|
+
let directory = command.cwd;
|
|
16
|
+
let chosen = repository.path;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const relative = path.relative(repository.path, directory);
|
|
19
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
20
|
+
break;
|
|
21
|
+
if (INSTALL_MARKERS.some(name => existsSync(path.join(directory, name)))) {
|
|
22
|
+
chosen = directory;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
if (!relative)
|
|
26
|
+
break;
|
|
27
|
+
directory = path.dirname(directory);
|
|
28
|
+
}
|
|
29
|
+
roots.push(chosen);
|
|
30
|
+
}
|
|
31
|
+
// Every repository root stays a candidate: its plan is a no-op when it is already installed.
|
|
32
|
+
return [...new Set([...roots, ...context.repositories.map(repository => repository.path)])];
|
|
33
|
+
}
|
|
34
|
+
/** A directory as the log should name it: relative to the checkout that contains it. */
|
|
35
|
+
export function checkoutRelative(context, directory) {
|
|
36
|
+
const repository = context.repositories.find(repo => { const relative = path.relative(repo.path, directory); return !relative.startsWith('..') && !path.isAbsolute(relative); });
|
|
37
|
+
return repository ? path.relative(repository.path, directory).split(path.sep).join('/') || '.' : directory;
|
|
38
|
+
}
|
|
39
|
+
/** The legacy host-facing sentence for a blocker; engine v2 hosts read the structured blocker instead. */
|
|
40
|
+
export function hostPreconditionMessage(reason, command, args, directory) {
|
|
41
|
+
return `Verification cannot run in this environment: ${reason} (\`${[command, ...args].join(' ')}\` in ${directory}). This is not a defect in the change, so no correction round was started. Make it available to Specrails (for example in the login shell profile Specrails loads, or with a refreshed registry token), then resume.`;
|
|
42
|
+
}
|
|
43
|
+
/** A failed check the host must unblock (credentials, registry access, a missing variable): no correction round can repair it. */
|
|
44
|
+
export function preconditionBlock(context, receipt) {
|
|
45
|
+
for (const command of receipt.commands) {
|
|
46
|
+
if (command.exitCode === 0)
|
|
47
|
+
continue;
|
|
48
|
+
const precondition = hostPrecondition(command.output);
|
|
49
|
+
if (precondition)
|
|
50
|
+
return boundedBlocker({ kind: precondition.kind, reason: precondition.reason, command: command.command, args: command.args, cwd: checkoutRelative(context, command.cwd),
|
|
51
|
+
requiredAction: precondition.requiredAction, ...(command.evidenceId ? { evidenceId: command.evidenceId } : {}) });
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs the planned installs for the failed commands' roots once, narrating each
|
|
57
|
+
* step through `note` exactly as the legacy graph logs it (`[environment] npm
|
|
58
|
+
* install (app)`, `Environment: installed …`). An install the host cannot
|
|
59
|
+
* complete for a precondition reason becomes `refused`.
|
|
60
|
+
*/
|
|
61
|
+
export function repairEnvironment(context, receipt, guardrails, note, spawn) {
|
|
62
|
+
const failed = receipt.commands.filter(command => command.exitCode !== 0);
|
|
63
|
+
const installs = installEnvironment(installRoots(context, failed), { failureOutput: receipt.commands.map(command => command.output).join('\n'), lockfileRepair: guardrailEnabled(guardrails, 'lockfile-repair'),
|
|
64
|
+
...(spawn ? { spawn } : {}), onEvent: event => note(event.kind === 'text' ? event.text ?? '' : `[environment] ${event.tool ?? ''} ${event.detail ?? ''}`.trim()) });
|
|
65
|
+
const refused = installs.find(outcome => outcome.precondition);
|
|
66
|
+
if (!refused)
|
|
67
|
+
return { installs };
|
|
68
|
+
const precondition = hostPrecondition(refused.detail) ?? { kind: 'environment', reason: refused.precondition, requiredAction: 'Repair the verification environment, then retry the run.' };
|
|
69
|
+
const cwd = checkoutRelative(context, refused.root);
|
|
70
|
+
const command = [refused.command, ...refused.args].join(' ');
|
|
71
|
+
return { installs, refused: boundedBlocker({ kind: precondition.kind, reason: `installing its dependencies failed because ${refused.precondition}`, command: refused.command, args: refused.args, cwd,
|
|
72
|
+
// A network blocker on an install names the exact command to repeat with connectivity; other kinds keep the classifier's action.
|
|
73
|
+
requiredAction: precondition.kind === 'network' ? `Run \`${command}\` in ${cwd} with network access, then retry the run.` : precondition.requiredAction }) };
|
|
74
|
+
}
|
|
75
|
+
/** A blocker as a durable JSON value: known kind, bounded strings, at most 16 arguments. */
|
|
76
|
+
export function boundedBlocker(value) {
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
78
|
+
return undefined;
|
|
79
|
+
const raw = value;
|
|
80
|
+
if (typeof raw.kind !== 'string' || !BLOCKER_KINDS.has(raw.kind))
|
|
81
|
+
return undefined;
|
|
82
|
+
const bounded = (field, limit = BLOCKER_TEXT_LIMIT) => typeof field === 'string' ? field.slice(0, limit) : '';
|
|
83
|
+
const args = Array.isArray(raw.args) ? raw.args.slice(0, 16).map(arg => bounded(arg, 256)) : [];
|
|
84
|
+
// A correction role reports free-text `evidence` instead of a host reason.
|
|
85
|
+
const reason = bounded(raw.reason) || bounded(raw.evidence);
|
|
86
|
+
return { kind: raw.kind, reason, command: bounded(raw.command, 512), args, cwd: bounded(raw.cwd, 512), requiredAction: bounded(raw.requiredAction),
|
|
87
|
+
...(typeof raw.evidenceId === 'string' && raw.evidenceId ? { evidenceId: bounded(raw.evidenceId, 128) } : {}) };
|
|
88
|
+
}
|
|
@@ -37,7 +37,11 @@ export interface AssembleFlags {
|
|
|
37
37
|
provider?: string | boolean;
|
|
38
38
|
version?: string | boolean;
|
|
39
39
|
'code-root'?: string | boolean;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Deprecated since Core 6.3: roles are runtime-defined, so there is nothing
|
|
42
|
+
* to select. Still parsed so older Desktop builds keep working; ignored with
|
|
43
|
+
* a warning.
|
|
44
|
+
*/
|
|
41
45
|
'selected-agents'?: string | boolean;
|
|
42
46
|
}
|
|
43
47
|
interface InstallFrameworkOutcome {
|
|
@@ -77,8 +81,8 @@ export declare function runInstallFramework(flags: InstallFrameworkFlags): Promi
|
|
|
77
81
|
export declare function runSwapCurrent(flags: SwapCurrentFlags): Promise<SwapCurrentOutcome>;
|
|
78
82
|
/**
|
|
79
83
|
* `specrails-core assemble` — SYMLINK the materialized framework subtrees into a
|
|
80
|
-
* project workspace and seed the per-project layer (
|
|
81
|
-
* instruction/settings files
|
|
84
|
+
* project workspace and seed the per-project layer (manifest,
|
|
85
|
+
* instruction/settings files). NO network, NO openspec init.
|
|
82
86
|
*/
|
|
83
87
|
export declare function runAssemble(flags: AssembleFlags): Promise<AssembleOutcome>;
|
|
84
88
|
/** Read the package version (the version the desktop should materialize). */
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { assertNoCoreDowngrade, currentFrameworkVersion, withFrameworkLifecycleLock } from '../util/install-transaction.js';
|
|
4
4
|
import { InstallerError } from '../util/errors.js';
|
|
5
|
-
import { ok, step } from '../util/logger.js';
|
|
5
|
+
import { ok, step, warn } from '../util/logger.js';
|
|
6
6
|
import { pathExists, readTextFile } from '../util/fs.js';
|
|
7
7
|
import { derivedPaths } from '../phases/provider-detect.js';
|
|
8
8
|
import { assertFrameworkVersionComplete, resolveRequiredFrameworkProviders, } from '../phases/framework-lifecycle.js';
|
|
@@ -129,8 +129,8 @@ export async function runSwapCurrent(flags) {
|
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* `specrails-core assemble` — SYMLINK the materialized framework subtrees into a
|
|
132
|
-
* project workspace and seed the per-project layer (
|
|
133
|
-
* instruction/settings files
|
|
132
|
+
* project workspace and seed the per-project layer (manifest,
|
|
133
|
+
* instruction/settings files). NO network, NO openspec init.
|
|
134
134
|
*/
|
|
135
135
|
export async function runAssemble(flags) {
|
|
136
136
|
const scriptDir = resolveScriptDir();
|
|
@@ -153,10 +153,9 @@ export async function runAssemble(flags) {
|
|
|
153
153
|
throw new InstallerError(`framework not materialized: ${currentProviderDir} is missing — run install-framework first`, 41);
|
|
154
154
|
}
|
|
155
155
|
assertFrameworkVersionComplete(frameworkDir, version, [provider]);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: undefined;
|
|
156
|
+
if (flags['selected-agents'] !== undefined) {
|
|
157
|
+
warn('--selected-agents is ignored since Core 6.3: roles are runtime-defined and no role file is installed.');
|
|
158
|
+
}
|
|
160
159
|
step(`Assembling workspace ${workspace} ← framework ${version} (${provider})`);
|
|
161
160
|
assembleProjectWorkspace({
|
|
162
161
|
workspace,
|
|
@@ -166,7 +165,6 @@ export async function runAssemble(flags) {
|
|
|
166
165
|
version,
|
|
167
166
|
codeRoot,
|
|
168
167
|
scriptDir,
|
|
169
|
-
selectedAgents,
|
|
170
168
|
});
|
|
171
169
|
ok(`Linked ${providerDir}/ from framework ${version} + seeded project layer`);
|
|
172
170
|
return { workspace, frameworkDir, provider, version, codeRoot, providerDir };
|
|
@@ -6,7 +6,8 @@ import { type Provider } from '../phases/install-config.js';
|
|
|
6
6
|
* --root-dir <path> Target repo (default: cwd)
|
|
7
7
|
* --yes / -y Non-interactive; auto-init git + accept defaults
|
|
8
8
|
* --provider <name> Force provider (claude, codex, gemini, or kimi)
|
|
9
|
-
* --from-config [<p>] Read provider
|
|
9
|
+
* --from-config [<p>] Read the provider from install-config.yaml (a legacy
|
|
10
|
+
* `agents` section is validated and ignored)
|
|
10
11
|
* --relocate Relocate artifacts to the $HOME workspace (symlinked
|
|
11
12
|
* from the bundled framework) instead of installing them
|
|
12
13
|
* IN-REPO. Default is in-repo so a standalone user's
|
|
@@ -31,12 +32,12 @@ interface InitResult {
|
|
|
31
32
|
}
|
|
32
33
|
type WorkspaceProviderSelections = Partial<Record<Provider, string[]>>;
|
|
33
34
|
/**
|
|
34
|
-
* Snapshot the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `framework/current` must be recreated after `current` moves.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* Snapshot the providers exposed by one live workspace before a global
|
|
36
|
+
* framework version swap. This matters on Windows: directory junctions resolve
|
|
37
|
+
* their target when they are created, so a workspace link made through
|
|
38
|
+
* `framework/current` must be recreated after `current` moves. Roles are
|
|
39
|
+
* runtime-defined since Core 6.3, so every provider reports an empty selection;
|
|
40
|
+
* the keys are what drive the multi-provider reassembly.
|
|
40
41
|
*/
|
|
41
42
|
export declare function snapshotWorkspaceProviderSelections(workspace: string): WorkspaceProviderSelections;
|
|
42
43
|
/**
|
|
@@ -45,20 +46,12 @@ export declare function snapshotWorkspaceProviderSelections(workspace: string):
|
|
|
45
46
|
* on failure.
|
|
46
47
|
*/
|
|
47
48
|
export declare function runInit(flags: InitFlags): Promise<InitResult>;
|
|
48
|
-
/**
|
|
49
|
-
* Warn (once per id) about `agents.selected` entries that have no shipped
|
|
50
|
-
* template — typically a pre-v5 install-config.yaml still listing removed
|
|
51
|
-
* agents. They are skipped at placement; the warning tells the user why and
|
|
52
|
-
* points at the v5 extension path.
|
|
53
|
-
*/
|
|
54
|
-
export declare function warnUnknownSelectedAgents(selected: string[] | undefined): void;
|
|
55
49
|
interface EnsureFrameworkInput {
|
|
56
50
|
scriptDir: string;
|
|
57
51
|
frameworkDir: string;
|
|
58
52
|
provider: Provider;
|
|
59
53
|
providerDir: string;
|
|
60
54
|
version: string;
|
|
61
|
-
selectedAgents?: string[];
|
|
62
55
|
/**
|
|
63
56
|
* Additional providers that must remain available through the global
|
|
64
57
|
* `framework/current` pointer after this version transition.
|
|
@@ -6,13 +6,13 @@ import { recordSuccessfulInstall } from '../util/registry.js';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { InstallerError } from '../util/errors.js';
|
|
8
8
|
import { runCommand } from '../util/exec.js';
|
|
9
|
-
import { info, ok, step
|
|
9
|
+
import { info, ok, step } from '../util/logger.js';
|
|
10
10
|
import { isDir, isSymlink, listDir, pathExists, readTextFile, removePath, writeFileLf, } from '../util/fs.js';
|
|
11
11
|
import { loadInstallConfig, resolveConfigPath, } from '../phases/install-config.js';
|
|
12
12
|
import { checkPrerequisites } from '../phases/prereqs.js';
|
|
13
13
|
import { derivedPaths } from '../phases/provider-detect.js';
|
|
14
14
|
import { materializeFrameworkVersion } from '../phases/framework-lifecycle.js';
|
|
15
|
-
import {
|
|
15
|
+
import { assembleProjectWorkspace, installFramework, } from '../phases/scaffold.js';
|
|
16
16
|
import { frameworkRoot, resolveArtifacts } from '../util/registry.js';
|
|
17
17
|
const WORKSPACE_PROVIDER_ORDER = [
|
|
18
18
|
'claude',
|
|
@@ -21,12 +21,12 @@ const WORKSPACE_PROVIDER_ORDER = [
|
|
|
21
21
|
'kimi',
|
|
22
22
|
];
|
|
23
23
|
/**
|
|
24
|
-
* Snapshot the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `framework/current` must be recreated after `current` moves.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* Snapshot the providers exposed by one live workspace before a global
|
|
25
|
+
* framework version swap. This matters on Windows: directory junctions resolve
|
|
26
|
+
* their target when they are created, so a workspace link made through
|
|
27
|
+
* `framework/current` must be recreated after `current` moves. Roles are
|
|
28
|
+
* runtime-defined since Core 6.3, so every provider reports an empty selection;
|
|
29
|
+
* the keys are what drive the multi-provider reassembly.
|
|
30
30
|
*/
|
|
31
31
|
export function snapshotWorkspaceProviderSelections(workspace) {
|
|
32
32
|
const selections = {};
|
|
@@ -36,44 +36,25 @@ export function snapshotWorkspaceProviderSelections(workspace) {
|
|
|
36
36
|
if (Array.isArray(manifest.providers))
|
|
37
37
|
recorded = manifest.providers.filter((value) => typeof value === 'string');
|
|
38
38
|
}
|
|
39
|
-
catch { /* Legacy manifests
|
|
39
|
+
catch { /* Legacy manifests: a provider counts only through its managed links. */ }
|
|
40
40
|
for (const provider of WORKSPACE_PROVIDER_ORDER) {
|
|
41
41
|
const { providerDir } = derivedPaths(provider);
|
|
42
42
|
const providerRoot = path.join(workspace, providerDir);
|
|
43
43
|
if (!pathExists(providerRoot) ||
|
|
44
44
|
(!workspaceHasManagedProviderLink(providerRoot, provider)
|
|
45
|
-
&& !(recorded.includes(provider) && hasCopiedCoreArtifacts(providerRoot, provider))
|
|
46
|
-
&& !hasCopiedCoreRoles(providerRoot, provider))) {
|
|
45
|
+
&& !(recorded.includes(provider) && hasCopiedCoreArtifacts(providerRoot, provider)))) {
|
|
47
46
|
continue;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
? path.join(workspace, providerDir, 'skills', 'rails')
|
|
51
|
-
: provider === 'kimi'
|
|
52
|
-
? path.join(workspace, providerDir, 'skills')
|
|
53
|
-
: path.join(workspace, providerDir, 'agents');
|
|
54
|
-
const ids = listDir(roleRoot)
|
|
55
|
-
.filter((entry) => provider === 'claude' || provider === 'gemini'
|
|
56
|
-
? path.basename(entry).endsWith('.md')
|
|
57
|
-
: isDir(entry))
|
|
58
|
-
.map((entry) => {
|
|
59
|
-
const name = path.basename(entry);
|
|
60
|
-
return name.endsWith('.md') ? name.slice(0, -3) : name;
|
|
61
|
-
})
|
|
62
|
-
.filter((id) => /^sr-[a-z0-9-]+$/.test(id));
|
|
63
|
-
selections[provider] = [...new Set(ids)].sort();
|
|
48
|
+
selections[provider] = [];
|
|
64
49
|
}
|
|
65
50
|
return selections;
|
|
66
51
|
}
|
|
52
|
+
/** A copied (in-repo) install is recognised by its workflow commands or runtime, never by role files. */
|
|
67
53
|
function hasCopiedCoreArtifacts(providerRoot, provider) {
|
|
68
|
-
return
|
|
54
|
+
return pathExists(provider === 'codex' ? path.join(providerRoot, 'skills', 'implement', 'SKILL.md')
|
|
69
55
|
: provider === 'kimi' ? path.join(providerRoot, 'specrails', 'run-skill.mjs')
|
|
70
56
|
: path.join(providerRoot, 'commands', 'specrails', provider === 'gemini' ? 'implement.toml' : 'implement.md'));
|
|
71
57
|
}
|
|
72
|
-
function hasCopiedCoreRoles(providerRoot, provider) {
|
|
73
|
-
return [...CORE_AGENTS].every((id) => pathExists(provider === 'codex' ? path.join(providerRoot, 'skills', 'rails', id, 'SKILL.md')
|
|
74
|
-
: provider === 'kimi' ? path.join(providerRoot, 'skills', id, 'SKILL.md')
|
|
75
|
-
: path.join(providerRoot, 'agents', id + '.md')));
|
|
76
|
-
}
|
|
77
58
|
function workspaceHasManagedProviderLink(providerRoot, provider) {
|
|
78
59
|
const directoryLinks = provider === 'claude'
|
|
79
60
|
? ['commands', 'skills', 'rules']
|
|
@@ -85,10 +66,11 @@ function workspaceHasManagedProviderLink(providerRoot, provider) {
|
|
|
85
66
|
if (directoryLinks.some((relative) => isSymlink(path.join(providerRoot, relative)))) {
|
|
86
67
|
return true;
|
|
87
68
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
// Kimi links its skills one directory at a time; claude/gemini have no
|
|
70
|
+
// granular region any more (role files are no longer linked per file).
|
|
71
|
+
if (provider !== 'kimi')
|
|
72
|
+
return false;
|
|
73
|
+
return listDir(path.join(providerRoot, 'skills')).some((entry) => isSymlink(entry));
|
|
92
74
|
}
|
|
93
75
|
/**
|
|
94
76
|
* Refresh every provider already represented in the project plus the provider
|
|
@@ -110,9 +92,6 @@ function reassembleWorkspaceProviders(input) {
|
|
|
110
92
|
version: input.version,
|
|
111
93
|
codeRoot: input.codeRoot,
|
|
112
94
|
scriptDir: input.scriptDir,
|
|
113
|
-
selectedAgents: provider === input.selectedProvider
|
|
114
|
-
? input.selectedAgents ?? input.previousSelections[provider]
|
|
115
|
-
: input.previousSelections[provider],
|
|
116
95
|
copyStatics: input.copyStatics,
|
|
117
96
|
});
|
|
118
97
|
}
|
|
@@ -142,10 +121,10 @@ export async function runInit(flags) {
|
|
|
142
121
|
}
|
|
143
122
|
explicitProvider = flags.provider;
|
|
144
123
|
}
|
|
145
|
-
// --from-config: read provider
|
|
124
|
+
// --from-config: read the provider from yaml. The legacy `agents` section is
|
|
125
|
+
// validated (and warned about) by `loadInstallConfig`; roles are runtime-defined.
|
|
146
126
|
const fromConfigFlag = flags['from-config'];
|
|
147
127
|
let providerHint = explicitProvider;
|
|
148
|
-
let selectedAgentsHint;
|
|
149
128
|
if (fromConfigFlag !== undefined) {
|
|
150
129
|
const explicitPath = typeof fromConfigFlag === 'string' ? fromConfigFlag : undefined;
|
|
151
130
|
const resolved = resolveConfigPath(repoRoot, explicitPath);
|
|
@@ -155,9 +134,7 @@ export async function runInit(flags) {
|
|
|
155
134
|
throw new InstallerError(`--provider '${explicitProvider}' conflicts with provider '${config.provider}' in ${resolved}`, 40);
|
|
156
135
|
}
|
|
157
136
|
providerHint = config.provider;
|
|
158
|
-
selectedAgentsHint = config.agents.selected;
|
|
159
137
|
info(`Loaded install config from ${resolved}`);
|
|
160
|
-
warnUnknownSelectedAgents(selectedAgentsHint);
|
|
161
138
|
}
|
|
162
139
|
else {
|
|
163
140
|
info(`install-config.yaml not found at ${resolved} — falling back to ${explicitProvider ? `explicit provider '${explicitProvider}'` : 'auto-detection'}`);
|
|
@@ -199,7 +176,7 @@ export async function runInit(flags) {
|
|
|
199
176
|
// Bundled-framework flow: materialize the provider-INVARIANT framework ONCE
|
|
200
177
|
// under `<home>/.specrails/framework/<version>/<providerDir>/`, point `current`
|
|
201
178
|
// at it, then SYMLINK that copy into the workspace and seed the project layer
|
|
202
|
-
// (
|
|
179
|
+
// (manifest, instruction files). The framework
|
|
203
180
|
// source for standalone npx is the package's templates/+commands/ (scriptDir).
|
|
204
181
|
step('Phase 2 & 3: Installing specrails artifacts');
|
|
205
182
|
const { providerDir } = derivedPaths(prereqs.provider);
|
|
@@ -228,7 +205,6 @@ export async function runInit(flags) {
|
|
|
228
205
|
provider: prereqs.provider,
|
|
229
206
|
providerDir,
|
|
230
207
|
version,
|
|
231
|
-
selectedAgents: selectedAgentsHint,
|
|
232
208
|
requiredProviders: Object.keys(previousSelections),
|
|
233
209
|
});
|
|
234
210
|
reassembleWorkspaceProviders({
|
|
@@ -238,7 +214,6 @@ export async function runInit(flags) {
|
|
|
238
214
|
codeRoot,
|
|
239
215
|
scriptDir,
|
|
240
216
|
selectedProvider: prereqs.provider,
|
|
241
|
-
selectedAgents: selectedAgentsHint,
|
|
242
217
|
previousSelections,
|
|
243
218
|
// In-repo: COPY the framework statics as real, committable files. Relocated:
|
|
244
219
|
// symlink from the framework store (O(1) update on a version swap).
|
|
@@ -255,7 +230,7 @@ export async function runInit(flags) {
|
|
|
255
230
|
if (artifactRoot !== codeRoot)
|
|
256
231
|
recordSuccessfulInstall(repoRoot, { home: process.env.SPECRAILS_REGISTRY_HOME, providers: [prereqs.provider], coreVersion: version });
|
|
257
232
|
step('Installation complete');
|
|
258
|
-
info('
|
|
233
|
+
info('Commands and skills were placed directly — no follow-up step required.');
|
|
259
234
|
// Terminal sentinel for programmatic consumers (specrails-desktop's setup
|
|
260
235
|
// wizard matches this exact line via regex to mark the "init complete"
|
|
261
236
|
// checkpoint). The sentinel line below is FROZEN — the downstream setup
|
|
@@ -268,22 +243,6 @@ export async function runInit(flags) {
|
|
|
268
243
|
});
|
|
269
244
|
});
|
|
270
245
|
}
|
|
271
|
-
/**
|
|
272
|
-
* Warn (once per id) about `agents.selected` entries that have no shipped
|
|
273
|
-
* template — typically a pre-v5 install-config.yaml still listing removed
|
|
274
|
-
* agents. They are skipped at placement; the warning tells the user why and
|
|
275
|
-
* points at the v5 extension path.
|
|
276
|
-
*/
|
|
277
|
-
export function warnUnknownSelectedAgents(selected) {
|
|
278
|
-
if (!selected)
|
|
279
|
-
return;
|
|
280
|
-
for (const id of selected) {
|
|
281
|
-
if (!CORE_AGENTS.has(id)) {
|
|
282
|
-
warn(`install-config.yaml selects agent '${id}', which specrails-core no longer ships — ` +
|
|
283
|
-
`skipping (removed in v5).`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
246
|
/**
|
|
288
247
|
* Materialize the framework for `(version, provider)` if absent and (by default)
|
|
289
248
|
* point `<frameworkDir>/current` at that version. Idempotent — `installFramework`
|
|
@@ -304,7 +263,6 @@ export function ensureFramework(input) {
|
|
|
304
263
|
provider: input.provider,
|
|
305
264
|
providerDir: input.providerDir,
|
|
306
265
|
version: input.version,
|
|
307
|
-
selectedAgents: input.selectedAgents,
|
|
308
266
|
});
|
|
309
267
|
return;
|
|
310
268
|
}
|
|
@@ -20,7 +20,12 @@ interface InstallModelConfig {
|
|
|
20
20
|
interface InstallConfig {
|
|
21
21
|
version: 1;
|
|
22
22
|
provider: Provider;
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Deprecated since Core 6.3: roles are runtime-defined, so the selection has
|
|
25
|
+
* no effect on the installed artifacts. Validated for shape when present and
|
|
26
|
+
* echoed back for consumers that still read it; absent when not supplied.
|
|
27
|
+
*/
|
|
28
|
+
agents?: {
|
|
24
29
|
selected: string[];
|
|
25
30
|
excluded?: string[];
|
|
26
31
|
/** Legacy pre-models-section location, accepted for backward compatibility. */
|
|
@@ -32,7 +37,8 @@ interface InstallConfig {
|
|
|
32
37
|
* Resolve a preset into provider-native identifiers. Kimi currently exposes no
|
|
33
38
|
* Core-defined cost/capability tier mapping, so every named preset resolves to
|
|
34
39
|
* its explicit `k3` default. User-supplied exact identifiers are handled later
|
|
35
|
-
* and are never interpreted as Claude aliases.
|
|
40
|
+
* and are never interpreted as Claude aliases. No preset carries per-role
|
|
41
|
+
* overrides: Core ships no role ids, so `models.overrides` is Desktop's to fill.
|
|
36
42
|
*/
|
|
37
43
|
export declare function resolveProviderModelConfig(provider: Provider, preset?: ModelPreset): InstallModelConfig;
|
|
38
44
|
export declare class InvalidConfigError extends InstallerError {
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import yaml from 'js-yaml';
|
|
3
3
|
import { FilesystemError, InstallerError } from '../util/errors.js';
|
|
4
4
|
import { pathExists, readTextFile, writeFileLf } from '../util/fs.js';
|
|
5
|
+
import { warn } from '../util/logger.js';
|
|
5
6
|
const PROVIDER_DEFAULT_MODELS = {
|
|
6
7
|
claude: 'sonnet',
|
|
7
8
|
codex: 'gpt-5.5-mini',
|
|
@@ -25,7 +26,8 @@ const SAFE_KIMI_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$/;
|
|
|
25
26
|
* Resolve a preset into provider-native identifiers. Kimi currently exposes no
|
|
26
27
|
* Core-defined cost/capability tier mapping, so every named preset resolves to
|
|
27
28
|
* its explicit `k3` default. User-supplied exact identifiers are handled later
|
|
28
|
-
* and are never interpreted as Claude aliases.
|
|
29
|
+
* and are never interpreted as Claude aliases. No preset carries per-role
|
|
30
|
+
* overrides: Core ships no role ids, so `models.overrides` is Desktop's to fill.
|
|
29
31
|
*/
|
|
30
32
|
export function resolveProviderModelConfig(provider, preset = 'balanced') {
|
|
31
33
|
if (provider === 'claude') {
|
|
@@ -33,11 +35,7 @@ export function resolveProviderModelConfig(provider, preset = 'balanced') {
|
|
|
33
35
|
return { preset, defaults: { model: 'haiku' }, overrides: {} };
|
|
34
36
|
}
|
|
35
37
|
if (preset === 'max') {
|
|
36
|
-
return {
|
|
37
|
-
preset,
|
|
38
|
-
defaults: { model: 'sonnet' },
|
|
39
|
-
overrides: { 'sr-architect': 'opus' },
|
|
40
|
-
};
|
|
38
|
+
return { preset, defaults: { model: 'sonnet' }, overrides: {} };
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
return {
|
|
@@ -109,13 +107,18 @@ export function validateInstallConfig(raw) {
|
|
|
109
107
|
// NOTE: a legacy `tier` field (`full`/`quick`) may still be present in pre-v5
|
|
110
108
|
// configs. It is intentionally ignored (not rejected and not carried forward)
|
|
111
109
|
// — v5 has no install tiers.
|
|
110
|
+
// The `agents` section is OPTIONAL and deprecated: roles are runtime-defined
|
|
111
|
+
// since Core 6.3. When present it is still validated for shape (safe ids, no
|
|
112
|
+
// overlap, known preset) so a malformed file fails fast, and a non-empty
|
|
113
|
+
// selection logs one warning instead of being rejected — Desktop builds
|
|
114
|
+
// paired with an older Core still write it.
|
|
112
115
|
const agents = doc.agents;
|
|
113
116
|
let selectedAgents = [];
|
|
114
117
|
let excludedAgents;
|
|
115
|
-
if (!agents || typeof agents !== 'object') {
|
|
116
|
-
errors.push(`
|
|
118
|
+
if (doc.agents !== undefined && (!agents || typeof agents !== 'object' || Array.isArray(agents))) {
|
|
119
|
+
errors.push(`'agents' must be a mapping`);
|
|
117
120
|
}
|
|
118
|
-
else {
|
|
121
|
+
else if (agents) {
|
|
119
122
|
if (!Array.isArray(agents.selected)) {
|
|
120
123
|
errors.push(`'agents.selected' must be a list`);
|
|
121
124
|
}
|
|
@@ -182,6 +185,10 @@ export function validateInstallConfig(raw) {
|
|
|
182
185
|
if (errors.length > 0) {
|
|
183
186
|
throw new InvalidConfigError(errors);
|
|
184
187
|
}
|
|
188
|
+
if (selectedAgents.length > 0) {
|
|
189
|
+
warn(`install-config.yaml 'agents.selected' is ignored since Core 6.3: roles are ` +
|
|
190
|
+
`runtime-defined and no role file is installed (${selectedAgents.join(', ')}).`);
|
|
191
|
+
}
|
|
185
192
|
const preset = (modelPreset ?? agents?.preset ?? 'balanced');
|
|
186
193
|
const resolvedModels = resolveProviderModelConfig(provider, preset);
|
|
187
194
|
if (typeof modelDefaults?.model === 'string') {
|
|
@@ -193,18 +200,18 @@ export function validateInstallConfig(raw) {
|
|
|
193
200
|
const result = {
|
|
194
201
|
version: 1,
|
|
195
202
|
provider,
|
|
196
|
-
agents: {
|
|
197
|
-
selected: selectedAgents,
|
|
198
|
-
},
|
|
199
203
|
models: resolvedModels,
|
|
200
204
|
};
|
|
201
205
|
// NOTE: a legacy `agent_teams` field may still be present in older configs.
|
|
202
206
|
// It is intentionally ignored (not rejected) for backward compatibility.
|
|
203
|
-
if (agents
|
|
204
|
-
result.agents
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
if (agents) {
|
|
208
|
+
result.agents = { selected: selectedAgents };
|
|
209
|
+
if (agents.preset !== undefined) {
|
|
210
|
+
result.agents.preset = agents.preset;
|
|
211
|
+
}
|
|
212
|
+
if (excludedAgents !== undefined) {
|
|
213
|
+
result.agents.excluded = excludedAgents;
|
|
214
|
+
}
|
|
208
215
|
}
|
|
209
216
|
return result;
|
|
210
217
|
}
|