specrails-desktop 2.56.1 → 2.57.1

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.
@@ -7,14 +7,16 @@ read-only discovery; it does not execute a PATH shim or install a global package
7
7
  An explicit `SPECRAILS_CORE_BIN` wins when it resolves to a usable compatible
8
8
  package. Otherwise Desktop selects the newest compatible package from its
9
9
  activated managed installation, bundled resources, local dependency and external
10
- CLI on PATH. Core 4 and 5 remain supported. A runtime older than the active
10
+ CLI on PATH. Supported Core majors are 4, 5 and 6 (`SUPPORTED_CORE_MAJORS` in
11
+ `server/core-package.ts`). A runtime older than the active
11
12
  framework is rejected rather than silently replacing an update.
12
13
 
13
14
  ## Persistence and publication
14
15
 
15
- The release bundle pins Core 5.6.0 in `desktop-release.yml` and
16
- `scripts/assemble-bundled-core.lock.json`. This includes scoped runtime recovery
17
- and the Opus 5.5 alias. Update both pins together and check compatibility against
16
+ The release bundle pins Core 6.0.0 in `desktop-release.yml` and
17
+ `scripts/assemble-bundled-core.lock.json`. Core 6 is the Desktop-only engine: it
18
+ publishes integration contract 5.0 (no standalone `update`) and ships only the
19
+ implement, batch-implement and retry workflows. Update both pins together and check compatibility against
18
20
  the staged published package; retained runs still use their original runtime.
19
21
 
20
22
  Desktop updates retain the complete npm installation, including dependencies,
@@ -64,6 +64,8 @@ Desktop negotiates `runtime api` and sends configuration to `runtime validate --
64
64
 
65
65
  Settings are saved at `<project execution .specrails directory>/agent-runtime.json`. Missing configuration uses the default agent runtime. The retired enabled flag cannot select another engine. Malformed configuration blocks admission; it is not ignored. Saving verifies that Core exposes the expected API. Connections are stored globally in `~/.specrails/runtime-providers.json`; project files retain role references, models, limits and verification. Existing embedded connections migrate once, with stable disambiguated IDs on endpoint conflicts. New runs freeze the resolved configuration, while saved runs retain their original snapshot.
66
66
 
67
+ **A package of a larger checkout.** A repository registered as a subdirectory of its git checkout (for example `apps/web` inside a monorepo) is isolated in a worktree of the whole checkout, so Desktop passes that directory to Core as the repository `scope` in `desktop-context.json` (Core capability `repositoryScope`). Configured checks run inside the package: a check without `cwd`, or with a `cwd` relative to the registered directory, never runs at the checkout root, where a monorepo test script fans out to every workspace. Core keeps the change inside the scope: after each developer or fixer turn it undoes edits outside it, the reviewer judges only the git change set measured against the run's base, a check that fails for missing credentials, variables or registry access stops the run for the host instead of starting a correction round, and a correction loop that stops converging stops with the reason instead of repeating the same checks. Runs admitted before scopes existed keep their frozen whole-checkout context.
68
+
67
69
  Core owns role instructions and permissions. The developer role edits and runs commands inside its CLI sandbox (the same autonomy as the legacy Implement step); architect and reviewer are read-only. A legacy rail profile/model selection does not override the runtime's per-role provider configuration. The JSON schema is [server/schemas/agent-runtime.schema.json](../../server/schemas/agent-runtime.schema.json), mirrored from Core. For a complete configuration, custom executor examples, Kimi capabilities and API tooling details, see [Core's runtime guide](https://github.com/fjpulidop/specrails-core/blob/main/docs/agent-runtime.md) in the paired revision.
68
70
 
69
71
  The built-in Claude adapter supports its native dollar cap. Built-in Codex, Gemini, Kimi and OpenAI-compatible adapters reject `maxCostUsd`; remove that limit for mixed-provider/local runs. Kimi also rejects token caps because its usage is unavailable. Unknown cost/tokens remain unknown in accounting, rather than becoming zero. Attempt, timeout and tool limits remain available. Existing provider services, licenses and inference costs are separate from the free open-source orchestration runtime.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.56.1",
3
+ "version": "2.57.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkoutSubdirectory = checkoutSubdirectory;
3
4
  exports.prepareCoreExecution = prepareCoreExecution;
4
5
  exports.coreVerificationContext = coreVerificationContext;
5
6
  exports.checkCoreCompletion = checkCoreCompletion;
@@ -25,6 +26,45 @@ function canonicalPath(path) {
25
26
  return (0, node_path_1.join)(canonicalPath(parent), (0, node_path_1.basename)(path));
26
27
  }
27
28
  }
29
+ /** Where a directory sits inside its git checkout: '' for the checkout root or a directory outside git. */
30
+ function checkoutSubdirectory(directory) {
31
+ let start;
32
+ try {
33
+ start = (0, node_fs_1.realpathSync)(directory);
34
+ }
35
+ catch {
36
+ return '';
37
+ }
38
+ for (let current = start;; current = (0, node_path_1.dirname)(current)) {
39
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(current, '.git')))
40
+ return (0, node_path_1.relative)(current, start).split(node_path_1.sep).join('/');
41
+ if ((0, node_path_1.dirname)(current) === current)
42
+ return '';
43
+ }
44
+ }
45
+ /**
46
+ * A registered repository may be one package of a larger git checkout (a
47
+ * monorepo app). An isolated worktree always mounts the whole checkout, so the
48
+ * registered directory travels to Core as the repository scope; otherwise Core
49
+ * verifies, edits and reviews every workspace of the checkout.
50
+ */
51
+ function registeredScope(checkout, sourcePath) {
52
+ if (!sourcePath)
53
+ return undefined;
54
+ const prefix = checkoutSubdirectory(sourcePath);
55
+ // Core already receives the registered directory itself when it is not a checkout root.
56
+ if (!prefix || checkoutSubdirectory(checkout) !== '')
57
+ return undefined;
58
+ try {
59
+ return (0, node_fs_1.statSync)((0, node_path_1.join)(checkout, ...prefix.split('/'))).isDirectory() ? [prefix] : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ function withoutScope(context) {
66
+ return { ...context, repositories: context.repositories.map(({ scope: _scope, ...repository }) => repository) };
67
+ }
28
68
  /** Stable run identity and frozen inputs are shared by every provider and retry.
29
69
  * No cwd/global singleton: two rails in the same workspace get separate files.
30
70
  */
@@ -34,9 +74,13 @@ function prepareCoreExecution(input) {
34
74
  throw new Error('Invalid Core execution run id');
35
75
  const cwd = (0, node_fs_1.realpathSync)(input.cwd);
36
76
  (0, core_update_state_1.assertWorkspaceCoreReady)(cwd);
77
+ const scoped = (repository, sourcePath) => {
78
+ const scope = registeredScope(repository.path, sourcePath);
79
+ return scope ? { ...repository, scope } : repository;
80
+ };
37
81
  const repositories = manifest
38
- ? manifest.repositories.map(repo => ({ id: repo.repositoryId, name: repo.name, path: (0, node_fs_1.realpathSync)(repo.worktreePath), baseSha: repo.baseSha }))
39
- : [{ id: run.repositoryId ?? (run.spec?.repositoryIds?.length === 1 ? run.spec.repositoryIds[0] : run.projectId ? `primary-${run.projectId}` : 'primary'), name: (0, node_path_1.basename)(input.repoDir ?? cwd), path: (0, node_fs_1.realpathSync)(input.repoDir ?? cwd) }];
82
+ ? manifest.repositories.map(repo => scoped({ id: repo.repositoryId, name: repo.name, path: (0, node_fs_1.realpathSync)(repo.worktreePath), baseSha: repo.baseSha }, repo.sourcePath))
83
+ : [scoped({ id: run.repositoryId ?? (run.spec?.repositoryIds?.length === 1 ? run.spec.repositoryIds[0] : run.projectId ? `primary-${run.projectId}` : 'primary'), name: (0, node_path_1.basename)(input.repoDir ?? cwd), path: (0, node_fs_1.realpathSync)(input.repoDir ?? cwd) }, input.sourcePath)];
40
84
  if (!repositories.length || repositories.some(repo => !SAFE_ID.test(repo.id)))
41
85
  throw new Error('Invalid Core repository scope');
42
86
  const artifactRepositoryId = manifest?.artifactRepositoryId ?? repositories[0].id;
@@ -76,7 +120,9 @@ function prepareCoreExecution(input) {
76
120
  catch (error) {
77
121
  if (error.code !== 'EEXIST')
78
122
  throw error;
79
- if ((0, node_fs_1.readFileSync)(contextPath, 'utf8') !== serialized)
123
+ const frozen = (0, node_fs_1.readFileSync)(contextPath, 'utf8');
124
+ // A run admitted before repository scopes existed keeps its frozen whole-checkout context.
125
+ if (frozen !== serialized && frozen !== JSON.stringify(withoutScope(context), null, 2) + '\n')
80
126
  throw new Error('Core execution context changed during an active run; start a new run for changed scope');
81
127
  }
82
128
  const env = { ...input.env, SPECRAILS_EXECUTION_CONTEXT: contextPath };
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CORE_PACKAGE_SPEC = void 0;
4
- /** Online fallback selects the supported Core 5 lifecycle; 5.1.0 is the floor
5
- * because it quotes the command path in its Windows shell runner (OpenSpec init
6
- * from an install directory with spaces). The desktop bundles the same version.
7
- * Bundled Core 4 remains readable for installed apps; the runtime resolver
8
- * selects the newest usable compatible package and never downgrades a newer
9
- * activated framework. */
10
- exports.CORE_PACKAGE_SPEC = 'specrails-core@^5.1.0';
3
+ exports.CORE_PACKAGE_SPEC = exports.SUPPORTED_CORE_MAJORS = void 0;
4
+ exports.isSupportedCoreVersion = isSupportedCoreVersion;
5
+ /** Core majors this Desktop can drive. Core 6 publishes integration contract
6
+ * 5.0 (no standalone `update`); Cores 4 and 5 remain readable for installed
7
+ * apps. The runtime resolver selects the newest usable compatible package and
8
+ * never downgrades a newer activated framework. */
9
+ exports.SUPPORTED_CORE_MAJORS = [4, 5, 6];
10
+ function isSupportedCoreVersion(version) {
11
+ return exports.SUPPORTED_CORE_MAJORS.includes(Number(version.split('.')[0]));
12
+ }
13
+ /** Online fallback selects the Core 6 lifecycle, the same major the desktop bundles. */
14
+ exports.CORE_PACKAGE_SPEC = 'specrails-core@^6.0.0';
@@ -15,6 +15,7 @@ const fs_1 = __importDefault(require("fs"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const artifact_registry_1 = require("./artifact-registry");
17
17
  const bundled_core_1 = require("./bundled-core");
18
+ const core_package_1 = require("./core-package");
18
19
  const semver_lite_1 = require("./semver-lite");
19
20
  const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
20
21
  function managedCoreRoot(home) {
@@ -114,8 +115,8 @@ function resolveCoreRuntime(home, external) {
114
115
  const runtime = packageForBinary(override, 'override');
115
116
  if (!runtime)
116
117
  throw new Error('SPECRAILS_CORE_BIN does not resolve to a usable specrails-core package.');
117
- if (![4, 5].includes(Number(runtime.version.split('.')[0])))
118
- throw new Error('The explicit Core package is not compatible with this Desktop. Supported Core majors: 4 and 5.');
118
+ if (!(0, core_package_1.isSupportedCoreVersion)(runtime.version))
119
+ throw new Error(`The explicit Core package is not compatible with this Desktop. Supported Core majors: ${core_package_1.SUPPORTED_CORE_MAJORS.join(', ')}.`);
119
120
  if (current && (0, semver_lite_1.compareVersions)(runtime.version, current) < 0)
120
121
  throw new Error(`The explicit Core ${runtime.version} is older than active framework ${current}; Desktop will not downgrade it.`);
121
122
  return runtime;
@@ -135,10 +136,7 @@ function resolveCoreRuntime(home, external) {
135
136
  candidates.push(managed);
136
137
  }
137
138
  candidates.push(...(external ?? (home === undefined ? discoverExternalCoreRuntimes() : [])));
138
- const compatible = candidates.filter(candidate => {
139
- const major = Number(candidate.version.split('.')[0]);
140
- return major >= 4 && major <= 5;
141
- }).sort((a, b) => (0, semver_lite_1.compareVersions)(b.version, a.version));
139
+ const compatible = candidates.filter(candidate => (0, core_package_1.isSupportedCoreVersion)(candidate.version)).sort((a, b) => (0, semver_lite_1.compareVersions)(b.version, a.version));
142
140
  const selected = compatible[0] ?? null;
143
141
  if (current && (!selected || (0, semver_lite_1.compareVersions)(selected.version, current) < 0)) {
144
142
  throw new Error(`Core framework ${current} is installed, but its runtime package is unavailable. Reinstall Core ${current} or a newer compatible version; Desktop will not downgrade it.`);
@@ -13,6 +13,7 @@ const semver_lite_1 = require("./semver-lite");
13
13
  const win_spawn_1 = require("./util/win-spawn");
14
14
  const bundled_core_1 = require("./bundled-core");
15
15
  const core_runtime_1 = require("./core-runtime");
16
+ const core_package_1 = require("./core-package");
16
17
  const artifact_registry_1 = require("./artifact-registry");
17
18
  /** Voluntary Core updates retain the complete npm package and dependencies,
18
19
  * publish its framework, then await every project refresh. Partial refreshes
@@ -117,8 +118,8 @@ class CoreUpdateManager {
117
118
  if (!requested || !(0, semver_lite_1.isValidVersion)(requested)) {
118
119
  return { ok: false, error: 'No valid target version to update to. Check for updates first.' };
119
120
  }
120
- if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requested) || ![4, 5].includes(Number(requested.split('.')[0]))) {
121
- return { ok: false, error: 'This Desktop supports Core 4 and 5. Update Desktop before installing another major version.' };
121
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(requested) || !(0, core_package_1.isSupportedCoreVersion)(requested)) {
122
+ return { ok: false, error: `This Desktop supports Core ${core_package_1.SUPPORTED_CORE_MAJORS.join(', ')}. Update Desktop before installing another major version.` };
122
123
  }
123
124
  const current = (0, framework_manager_1.readCurrentFrameworkVersion)(this.home);
124
125
  if (current && !(0, semver_lite_1.isNewer)(requested, current) && !(requested === current && ((0, core_runtime_1.getCoreRuntimeStatus)(this.home).error || this.pendingVersion === requested))) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RUNTIME_HOST_ENV_KEYS = void 0;
4
4
  exports.runtimeChangeName = runtimeChangeName;
5
+ exports.scopedHostChecks = scopedHostChecks;
5
6
  exports.runAgentRuntimeInvocation = runAgentRuntimeInvocation;
6
7
  const agent_runtime_history_1 = require("./agent-runtime-history");
7
8
  const agent_runtime_repositories_1 = require("./agent-runtime-repositories");
@@ -20,6 +21,22 @@ const win_spawn_1 = require("../../../util/win-spawn");
20
21
  function runtimeChangeName(runId) {
21
22
  return 'runtime-' + (0, node_crypto_1.createHash)('sha256').update(runId).digest('hex').slice(0, 20);
22
23
  }
24
+ /**
25
+ * Host checks belong to the directory the user registered: for a package of a
26
+ * larger checkout (Core scope) they run in that package, never at the checkout
27
+ * root where a monorepo test script fans out to every workspace. An explicit
28
+ * cwd is relative to the registered directory; one already inside the scope is kept.
29
+ */
30
+ function scopedHostChecks(checks, repositories = []) {
31
+ return checks.map(check => {
32
+ const scope = repositories.find(repository => repository.id === check.repositoryId)?.scope?.[0];
33
+ if (!scope || (check.cwd !== undefined && (0, node_path_1.isAbsolute)(check.cwd)))
34
+ return check;
35
+ const cwd = (check.cwd ?? '.').replace(/\\/g, '/');
36
+ const inside = cwd === scope || cwd.startsWith(scope + '/');
37
+ return { ...check, cwd: inside ? cwd : node_path_1.posix.normalize(node_path_1.posix.join(scope, cwd)) };
38
+ });
39
+ }
23
40
  exports.RUNTIME_HOST_ENV_KEYS = [
24
41
  'SPECRAILS_REPO_DIR', 'SPECRAILS_GIT_AUTO', 'SPECRAILS_REPO_MAP_PATH',
25
42
  'SPECRAILS_EXECUTION_CONTEXT', 'SPECRAILS_EXECUTION_MANIFEST', 'SPECRAILS_REPOSITORIES',
@@ -61,6 +78,7 @@ async function runAgentRuntimeInvocation(options) {
61
78
  const { config, origins } = (0, agent_runtime_effective_config_1.resolveEffectiveRuntimeConfig)((0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider), {
62
79
  repositoryIds: admittedContext.repositories.map(repo => repo.id), source, providerOverride: options.providerOverride,
63
80
  });
81
+ config.verification = scopedHostChecks(config.verification, admittedContext.repositories);
64
82
  config.rolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)() };
65
83
  // Core rejects unknown connection keys: drop the desktop-only local-engine
66
84
  // fields (label/defaultModel/rates/supportsReasoningEffort) before Core sees it.
@@ -237,7 +237,7 @@ function createLoopExecutors(opts = {}) {
237
237
  const existingContext = coreRun ? runtimeContextPath(cwd, coreRun.runId, baseEnv) : undefined;
238
238
  const programmatic = coreRun && existingContext && (coreRun.implementation || (coreRun.verificationStep && existsRuntimeRequest(existingContext)));
239
239
  const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
240
- const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
240
+ const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv, sourcePath: executionManifest ? undefined : opts.sourcePath?.() }) : undefined;
241
241
  const stepEnv = core?.env ?? baseStepEnv;
242
242
  if (coreRun?.implementation && core) {
243
243
  completionContexts.set(coreRun.runId, { cwd, contextPath: core.contextPath, env: stepEnv, runId: coreRun.runId });
@@ -476,7 +476,7 @@ function createLoopExecutors(opts = {}) {
476
476
  if (!adapter.capabilities.persistentStdin)
477
477
  return null;
478
478
  const baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
479
- const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
479
+ const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv, sourcePath: executionManifest ? undefined : opts.sourcePath?.() }) : undefined;
480
480
  const stepEnv = core?.env ?? baseStepEnv;
481
481
  const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir, executionManifest);
482
482
  if (repoDir) {
@@ -532,7 +532,7 @@ function createLoopExecutors(opts = {}) {
532
532
  const baseEnv = resolveEnv();
533
533
  const programmatic = existsRuntimeRequest(runtimeContextPath(cwd, coreRun.runId, baseEnv));
534
534
  const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
535
- const core = (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv });
535
+ const core = (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv, sourcePath: executionManifest ? undefined : opts.sourcePath?.() });
536
536
  if (existsRuntimeRequest(core.contextPath))
537
537
  return (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, coreRun.runId);
538
538
  const env = (0, runtime_1.buildProviderEnv)((0, providers_1.getAdapter)(provider), { prompt: '', model, reasoning_effort: effort }, core.env);
@@ -1106,6 +1106,8 @@ class ProjectRegistry {
1106
1106
  // See resolveLoopBaseEnv.
1107
1107
  const loopRunManager = new loop_run_manager_1.LoopRunManager(db, boundBroadcast, (0, loop_executors_1.createLoopExecutors)({
1108
1108
  pluginScope: () => ({ stateRoot: (0, workspace_resolution_1.resolveProjectExecution)(project).cwd, legacyProviderId: project.provider }),
1109
+ // A project registered as a package of a larger checkout scopes Core to that package in isolated worktrees.
1110
+ sourcePath: () => project.path,
1109
1111
  env: () => (0, workspace_resolution_1.resolveLoopBaseEnv)({ slug: project.slug, path: project.path }, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
1110
1112
  // Named rail profiles and global per-agent defaults ride the same
1111
1113
  // SPECRAILS_PROFILE_PATH snapshot in both AI transports. An explicit null