specrails-desktop 2.57.0 → 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.
@@ -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.57.0",
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 };
@@ -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