specrails-desktop 2.62.0 → 2.63.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.
- package/cli/dist/specrails-desktop.js +39 -5
- package/client/dist/assets/{ActivityFeedPage-C_pqc281.js → ActivityFeedPage-B1bpTy7O.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-vz6rbDtu.js → AgentBrowserCapture-BussBbN3.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BiT1n3BU.js +2 -0
- package/client/dist/assets/AgentModeCodePane-CUjNfeKa.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-99fBBLri.js → AgentModeJobsPane-RVEwyS59.js} +1 -1
- package/client/dist/assets/AgentsPage-DKE-eMC5.js +118 -0
- package/client/dist/assets/AnalyticsPage-Bser2Ly3.js +1 -0
- package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
- package/client/dist/assets/CodePage-BK0grm6e.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-DEBidZbs.js +1 -0
- package/client/dist/assets/{DocsDialog-XUCYKoq0.js → DocsDialog-Dhyo57n9.js} +1 -1
- package/client/dist/assets/{DocsPage-dOLC8bXA.js → DocsPage-BcEkPXd6.js} +1 -1
- package/client/dist/assets/{ExportDropdown-BvKQgwM9.js → ExportDropdown-mRmqdkjr.js} +1 -1
- package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-BlCe3vrU.js +19 -0
- package/client/dist/assets/{JobDetailModal-CkiubrkZ.js → JobDetailModal-CkAejD1F.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CNe4Wn7l.js → JobDetailPage-P-FS_lKz.js} +1 -1
- package/client/dist/assets/JobsPage-CoUXgYse.js +1 -0
- package/client/dist/assets/LoopBuilderPage-iFylNk68.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-Dv3dxHr_.js → LoopPreviewModal-4obS_Ptg.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-DN2lbSnL.js → LoopWindowSurface-CLr5OPqj.js} +1 -1
- package/client/dist/assets/LoopsPage-eKpWFtCp.js +2 -0
- package/client/dist/assets/MinimizedChatsContext-BHIUPVmC.js +1 -0
- package/client/dist/assets/PluginsPage-BgTC2CQx.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-B4chTfU8.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-DAIxIYsl.js +5 -0
- package/client/dist/assets/{RepositoryScopeSelector-DrtPqvc_.js → RepositoryScopeSelector-DeCoUsRN.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-Cktv0-9b.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-DcFlhUxy.js → TemplatePreviewModal-BZTV1TM1.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-DiPngt2g.js +16 -0
- package/client/dist/assets/Trans-ZiXIfj50.js +1 -0
- package/client/dist/assets/{agentRuntime-TKQyg137.js → agentRuntime-BDvbJMmS.js} +1 -1
- package/client/dist/assets/{agentRuntime-DvhEe1um.js → agentRuntime-BKJfv5YD.js} +1 -1
- package/client/dist/assets/{agentRuntime-DxC6-9rN.js → agentRuntime-BxF81z52.js} +1 -1
- package/client/dist/assets/{agentRuntime-C_yLPKDy.js → agentRuntime-BxYS5V7u.js} +1 -1
- package/client/dist/assets/{agentRuntime-WSv8G0pW.js → agentRuntime-BzuwQQgn.js} +1 -1
- package/client/dist/assets/{agentRuntime-D2kYQqNO.js → agentRuntime-Dc9HG3BA.js} +1 -1
- package/client/dist/assets/{agentRuntime-Bjt7Ij6_.js → agentRuntime-DiCldqGG.js} +1 -1
- package/client/dist/assets/{agentRuntime-B-myNBUP.js → agentRuntime-es2ymSl-.js} +1 -1
- package/client/dist/assets/ban-wqfdzoXt.js +1 -0
- package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
- package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
- package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
- package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
- package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
- package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
- package/client/dist/assets/download-C9wP_42h.js +1 -0
- package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
- package/client/dist/assets/file-text-C0blZHLh.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-DlQY1Lm_.js → formatDistanceToNow-CdN5lTyD.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-COUeq65G.js → getTimezoneOffsetInMilliseconds-62h40I4t.js} +1 -1
- package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
- package/client/dist/assets/index-BgbCbIua.css +2 -0
- package/client/dist/assets/{index-CONHvEJw.js → index-CECj3joE.js} +40 -40
- package/client/dist/assets/jira-api-C_D_RDhl.js +1 -0
- package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
- package/client/dist/assets/layers-BCLTgHXO.js +1 -0
- package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
- package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
- package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
- package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
- package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
- package/client/dist/assets/plus-fHDdjBk1.js +1 -0
- package/client/dist/assets/{project-repositories-Cc3oNv8X.js → project-repositories-Be0CWklo.js} +1 -1
- package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
- package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
- package/client/dist/assets/save-LmPSfsvZ.js +1 -0
- package/client/dist/assets/search-BDVp3fkH.js +1 -0
- package/client/dist/assets/{settings-BKJZ2KEM.js → settings-BO2N-oPf.js} +1 -1
- package/client/dist/assets/{settings-LfFnl1UH.js → settings-BQChAs_w.js} +1 -1
- package/client/dist/assets/{settings-BV3jocIx.js → settings-BzmYlcQB.js} +1 -1
- package/client/dist/assets/{settings-BjM2Dw8_.js → settings-CGjXb6TX.js} +1 -1
- package/client/dist/assets/{settings-DA77M48m.js → settings-CMxEWY46.js} +1 -1
- package/client/dist/assets/{settings-P1IQvhnL.js → settings-C_dRSgI2.js} +1 -1
- package/client/dist/assets/{settings-eIMJjyK_.js → settings-Hw7av-B1.js} +1 -1
- package/client/dist/assets/{settings-CKu0LTib.js → settings-nryN6wuS.js} +1 -1
- package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
- package/client/dist/assets/spending-eWiZGhC_.js +1 -0
- package/client/dist/assets/square-D0JTbRSR.js +1 -0
- package/client/dist/assets/trash-D8zYUmTg.js +1 -0
- package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
- package/client/dist/assets/upload-CCHvUkRs.js +1 -0
- package/client/dist/assets/{useDesktop-9Lwoel_8.js → useDesktop-C0mRIlHT.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-DL0xYoZ2.js → useRuntimeRuns-B1a6GChu.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-CcsE5986.js +2 -0
- package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
- package/client/dist/assets/x-Bu7X_eR2.js +1 -0
- package/client/dist/assets/zap-bugHyWzQ.js +1 -0
- package/client/dist/index.html +44 -45
- package/docs/ci-cd.md +8 -1
- package/docs/customizing.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/guide/de/settings/5-project-environment.md +27 -0
- package/docs/guide/en/settings/5-project-environment.md +27 -0
- package/docs/guide/es/settings/5-project-environment.md +27 -0
- package/docs/guide/fr/settings/5-project-environment.md +27 -0
- package/docs/guide/it/settings/5-project-environment.md +27 -0
- package/docs/guide/ja/settings/5-project-environment.md +27 -0
- package/docs/guide/pt/settings/5-project-environment.md +27 -0
- package/docs/guide/zh/settings/5-project-environment.md +27 -0
- package/docs/internals/api-reference.md +3 -1
- package/docs/internals/configuration.md +46 -5
- package/docs/internals/core-runtime-updates.md +3 -3
- package/docs/internals/desktop-owned-workflows.md +10 -0
- package/docs/internals/programmatic-agent-runtime.md +16 -0
- package/docs/internals/safe-pr-review-flow.md +15 -0
- package/docs/internals/source-map.md +6 -1
- package/docs/mcp.md +1 -1
- package/docs/platforms/macos.md +2 -0
- package/docs/platforms/windows-parity.md +4 -4
- package/docs/platforms/windows.md +9 -9
- package/local-runner/dist/specrails-local-runner.js +1 -1
- package/package.json +8 -9
- package/server/dist/mcp/tools/env.js +38 -3
- package/server/dist/mcp/tools/loops.js +1 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +13 -3
- package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +1 -1
- package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
- package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +13 -6
- package/server/dist/modules/loops/runtime/loop-run-manager.js +7 -0
- package/server/dist/modules/loops/runtime/loop-turn-limits.js +64 -0
- package/server/dist/modules/loops/runtime/loops-router.js +3 -1
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -0
- package/server/dist/modules/project-settings/adapters/http.js +24 -0
- package/server/dist/path-resolver.js +135 -20
- package/server/dist/project-env.js +265 -17
- package/server/dist/project-registry.js +14 -0
- package/server/dist/project-router-settings.js +17 -0
- package/server/dist/schemas/agent-runtime.schema.json +1 -1
- package/server/dist/worktree-node-modules.js +95 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +0 -2
- package/client/dist/assets/AgentsPage-CTwm_6T8.js +0 -118
- package/client/dist/assets/AnalyticsPage-YD8MLoPa.js +0 -1
- package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
- package/client/dist/assets/CodePage-CLJn2F2e.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-MWGQVbwp.js +0 -1
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +0 -19
- package/client/dist/assets/JobsPage-DSts_L_d.js +0 -1
- package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +0 -1
- package/client/dist/assets/LoopsPage-DHgIreEW.js +0 -2
- package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +0 -1
- package/client/dist/assets/PluginsPage-D1EJ9oja.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +0 -1
- package/client/dist/assets/RepositoryDeliveries-CW4eKG3x.js +0 -5
- package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +0 -16
- package/client/dist/assets/Trans-CAY2Rd2f.js +0 -1
- package/client/dist/assets/ban-E4EaZe3a.js +0 -1
- package/client/dist/assets/chevron-up-CwGUHd6D.js +0 -1
- package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
- package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
- package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
- package/client/dist/assets/dist-Bq10nS2m.js +0 -1
- package/client/dist/assets/download-CFHyyHcw.js +0 -1
- package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
- package/client/dist/assets/index-BacrO_ub.css +0 -2
- package/client/dist/assets/jira-api-C94VNHNY.js +0 -1
- package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
- package/client/dist/assets/layers-eDMjG41I.js +0 -1
- package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
- package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
- package/client/dist/assets/plus-CsnjfkmX.js +0 -1
- package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
- package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
- package/client/dist/assets/save-B6wGWwBR.js +0 -1
- package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
- package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
- package/client/dist/assets/spending-DemeFfrH.js +0 -1
- package/client/dist/assets/square-Ci-PFcFe.js +0 -1
- package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
- package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
- package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
- package/client/dist/assets/useSharedWebSocket-BEHPeU1Y.js +0 -2
- package/client/dist/assets/x-CBMI9ejn.js +0 -1
- package/client/dist/assets/zap-CHo3aCD1.js +0 -1
|
@@ -133,6 +133,21 @@ Both temporary checkouts use non-force cleanup, including after a Git runner exc
|
|
|
133
133
|
including proofs for historical misplaced links; symlinked destination ancestors confer no
|
|
134
134
|
authority. Registration paths, frozen manifests and Core scope remain unchanged. A failed run
|
|
135
135
|
keeps its old checkout; after updating Desktop, a fresh Relaunch receives the corrected layout.
|
|
136
|
+
- Warm reuse is gated by dependency freshness. Before preparing a new link for a package
|
|
137
|
+
directory, Desktop compares its dependency inputs in the base checkout and in the worktree: the
|
|
138
|
+
nearest lockfile found walking up to the Git top level (first of `pnpm-lock.yaml`, `yarn.lock`,
|
|
139
|
+
`package-lock.json`, `npm-shrinkwrap.json`, `bun.lock`, at the same relative position) and a
|
|
140
|
+
canonical digest of the `package.json` dependency, devDependency, optionalDependency and
|
|
141
|
+
peerDependency fields (scripts and other metadata are ignored). If either input differs, or one
|
|
142
|
+
cannot be read, the directory gets no `node_modules` and the run reports, on the
|
|
143
|
+
`rail.overlay_degraded` channel, for example
|
|
144
|
+
`apps/catalog: yarn.lock differs from the base checkout; dependencies will be installed in the worktree`.
|
|
145
|
+
Core's environment preparation then installs that directory cold from the worktree's own lockfile.
|
|
146
|
+
This prevents a base checkout on another branch, or behind the remote, from supplying packages
|
|
147
|
+
the run's code does not declare. Desktop never checks out, pulls or installs anything in the
|
|
148
|
+
user's checkout to make warm reuse possible. Links already present for a resumed run keep their
|
|
149
|
+
authentication; the guard only applies when a link is prepared. `SPECRAILS_WORKTREE_NODE_MODULES=false`
|
|
150
|
+
still disables warm reuse entirely.
|
|
136
151
|
- Per-unit settlement returns structured execution + delivery results. `onLoopRunFinished` receives
|
|
137
152
|
the engine outcome only; commit/status/ref/provenance/push failures cannot rewrite it.
|
|
138
153
|
- Before staging, settlement blocks active OpenSpec change directories introduced relative to
|
|
@@ -5,7 +5,7 @@ architectural dependency declaration. Search a heading or filename and read only
|
|
|
5
5
|
the relevant source and tests. Feature prefixes group the legacy server files;
|
|
6
6
|
new bounded modules live under `server/modules/`.
|
|
7
7
|
|
|
8
|
-
Includes
|
|
8
|
+
Includes 1047 source/build files. Nearby tests are linked where names
|
|
9
9
|
match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
10
10
|
|
|
11
11
|
## cli
|
|
@@ -384,6 +384,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
384
384
|
- [LoopMigrationPanel.tsx](../../client/src/features/loops/components/LoopMigrationPanel.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopMigrationPanel.test.tsx)
|
|
385
385
|
- [LoopPreviewModal.tsx](../../client/src/features/loops/components/LoopPreviewModal.tsx)
|
|
386
386
|
- [LoopRunModal.tsx](../../client/src/features/loops/components/LoopRunModal.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopRunModal.test.tsx)
|
|
387
|
+
- [LoopTurnLimits.tsx](../../client/src/features/loops/components/LoopTurnLimits.tsx) · [test](../../client/src/features/loops/components/LoopTurnLimits.test.tsx)
|
|
387
388
|
- [LoopWindowSurface.tsx](../../client/src/features/loops/components/LoopWindowSurface.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopWindowSurface.test.tsx)
|
|
388
389
|
- [TemplatePreviewModal.tsx](../../client/src/features/loops/components/TemplatePreviewModal.tsx) · [test](../../client/src/features/loops/components/__tests__/TemplatePreviewModal.test.tsx)
|
|
389
390
|
|
|
@@ -413,6 +414,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
413
414
|
- [loop-export.ts](../../client/src/features/loops/lib/loop-export.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-export.test.ts)
|
|
414
415
|
- [loop-graph-rf.ts](../../client/src/features/loops/lib/loop-graph-rf.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-graph-rf.test.ts)
|
|
415
416
|
- [loop-layout.ts](../../client/src/features/loops/lib/loop-layout.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-layout.test.ts)
|
|
417
|
+
- [loop-role-agents.ts](../../client/src/features/loops/lib/loop-role-agents.ts)
|
|
416
418
|
- [loop-run-models.ts](../../client/src/features/loops/lib/loop-run-models.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-run-models.test.ts)
|
|
417
419
|
- [loop-template-filter.ts](../../client/src/features/loops/lib/loop-template-filter.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-template-filter.test.ts)
|
|
418
420
|
- [loop-ticket-need.ts](../../client/src/features/loops/lib/loop-ticket-need.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-ticket-need.test.ts)
|
|
@@ -848,7 +850,9 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
848
850
|
- [generate-icons.mjs](../../scripts/generate-icons.mjs)
|
|
849
851
|
- [generate-mobile-types.mjs](../../scripts/generate-mobile-types.mjs)
|
|
850
852
|
- [generate-source-map.mjs](../../scripts/generate-source-map.mjs) · [test](../../scripts/generate-source-map.test.mjs)
|
|
853
|
+
- [module-references.mjs](../../scripts/module-references.mjs) · [test](../../scripts/module-references.test.mjs)
|
|
851
854
|
- [obfuscate-chromium.mjs](../../scripts/obfuscate-chromium.mjs)
|
|
855
|
+
- [paired-core-shards.mjs](../../scripts/paired-core-shards.mjs) · [test](../../scripts/paired-core-shards.test.mjs)
|
|
852
856
|
- [patch-node-pty.mjs](../../scripts/patch-node-pty.mjs) · [test](../../scripts/patch-node-pty.test.mjs)
|
|
853
857
|
- [recover-desktop-channel.mjs](../../scripts/recover-desktop-channel.mjs) · [test](../../scripts/recover-desktop-channel.test.mjs)
|
|
854
858
|
- [release-policy.mjs](../../scripts/release-policy.mjs) · [test](../../scripts/release-policy.test.mjs)
|
|
@@ -1330,6 +1334,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
1330
1334
|
- [loop-shell-invocation.ts](../../server/modules/loops/runtime/loop-shell-invocation.ts)
|
|
1331
1335
|
- [loop-step-idle.ts](../../server/modules/loops/runtime/loop-step-idle.ts) · [test](../../server/modules/loops/runtime/loop-step-idle.test.ts)
|
|
1332
1336
|
- [loop-templates.ts](../../server/modules/loops/runtime/loop-templates.ts) · [test](../../server/modules/loops/runtime/loop-templates.test.ts)
|
|
1337
|
+
- [loop-turn-limits.ts](../../server/modules/loops/runtime/loop-turn-limits.ts) · [test](../../server/modules/loops/runtime/loop-turn-limits.test.ts)
|
|
1333
1338
|
- [loops-router.ts](../../server/modules/loops/runtime/loops-router.ts) · [test](../../server/modules/loops/runtime/loops-router.test.ts)
|
|
1334
1339
|
- [loops-store.ts](../../server/modules/loops/runtime/loops-store.ts) · [test](../../server/modules/loops/runtime/loops-store.test.ts)
|
|
1335
1340
|
|
package/docs/mcp.md
CHANGED
|
@@ -48,7 +48,7 @@ reading one description. The tools are:
|
|
|
48
48
|
| `specrails_loops` | Saved loop workflows |
|
|
49
49
|
| `specrails_code` | File discovery, literal content search, bounded line reads, summaries and provenance; AI summary regeneration is capability-gated |
|
|
50
50
|
| `specrails_git` | Repository/branch/worktree information, status, diffs and PR lookup |
|
|
51
|
-
| `specrails_env` | Project environment readiness and environment-file management |
|
|
51
|
+
| `specrails_env` | Project environment readiness and environment-file management; `get`/`status`/`recheck` report each configured name's value-free resolution status |
|
|
52
52
|
| `specrails_support` | Support triage, local diagnostics and specrails-core update workflows |
|
|
53
53
|
| `specrails_setup` | Add-project setup wizard surface |
|
|
54
54
|
| `specrails_analytics` | Per-project spending analytics + budget |
|
package/docs/platforms/macos.md
CHANGED
|
@@ -77,6 +77,8 @@ When the app runs without an active bundle (a runtimes-less build, or `npm run d
|
|
|
77
77
|
|
|
78
78
|
2. **Login-shell merge (async)** — right after the HTTP server starts listening, `augmentPathFromLoginShell()` spawns `$SHELL -l -i` once (1500 ms timeout) and merges whatever `PATH` that login + interactive shell exposes. This recovers the segments that Volta, nvm, fnm, asdf, etc. add only inside your interactive shell — whatever your `$SHELL` produces, no specific rc file is assumed. On timeout or non-zero exit, the fast-path `PATH` stays in effect and a single warning is logged.
|
|
79
79
|
|
|
80
|
+
Project environment names configured in Project Settings (for example `NODE_AUTH_TOKEN`) use a separate, per-project login-shell probe with a 10 s budget (`SPECRAILS_LOGIN_SHELL_TIMEOUT_MS`). It runs in the background, never writes to `process.env`, and reports a status per name. See [configuration](../internals/configuration.md#login-shell-recovery-and-resolution-status).
|
|
81
|
+
|
|
80
82
|
The resolved `PATH` is stored on `process.env.PATH`, so every downstream spawn inherits it (`QueueManager` → provider CLI, `SetupManager` → `npx specrails-core`, `terminalManager` PTYs, etc.).
|
|
81
83
|
|
|
82
84
|
## Broken-symlink detection
|
|
@@ -6,8 +6,8 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
|
|
|
6
6
|
|
|
7
7
|
| Feature | Windows implementation and checks |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| Installation, native runtimes | Native x64/ARM64 NSIS
|
|
10
|
-
| Desktop updates |
|
|
9
|
+
| Installation, native runtimes | Native x64/ARM64 NSIS builds; WebView2 provisioning through the embedded bootstrapper; bundled Node/Git/core/OpenSpec; staged SQLite and full ConPTY/WinPTY dependency validation. |
|
|
10
|
+
| Desktop updates | NSIS-specific and generic Tauri targets both reference the NSIS installer; earlier MSI installs fall back to the generic entry and the NSIS installer removes the WiX installation. Missing, empty or ambiguous artifact/signature pairs fail manifest generation. Publication is serialized, and previous download installers are retained until the remote manifest matches the new release. |
|
|
11
11
|
| Startup and project catalog | Recover the actual Windows profile rather than inventing `C:\Users\Default`; SQLite catalog/repository IDs reopen under the same Unicode/spaced profile. |
|
|
12
12
|
| Projects and multi-repo | Repository membership and canonical identity use real paths; junction and case aliases cannot add the same repository twice. Shared backlog and per-repository jobs retain their existing regression coverage. |
|
|
13
13
|
| Core setup and updates | Offline assembly from the shipped core; preserve and restore the previous active framework if Windows junction replacement fails. Installed-package smoke waits for the real workspace marker. |
|
|
@@ -28,7 +28,7 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
|
|
|
28
28
|
|
|
29
29
|
`.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every pull request and push to `main`, adding `windows-11-arm` on the `main` pushes. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
|
|
30
30
|
|
|
31
|
-
`.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts.
|
|
31
|
+
`.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts. The script fails on any MSI bundle, then installs the NSIS package into a temporary path containing spaces and drives `scripts/smoke-installed-windows.mjs` with the installed Node runtime. The driver uses an isolated user profile and tests:
|
|
32
32
|
|
|
33
33
|
1. The installed pkg sidecar boots with bundled resources and authenticates API access.
|
|
34
34
|
2. A temporary Git repository is registered and core assembly completes offline.
|
|
@@ -53,7 +53,7 @@ The detachable-window implementation has passed native macOS fixtures locally, i
|
|
|
53
53
|
|
|
54
54
|
Before claiming complete Windows parity, record successful x64 and ARM64 CI/release runs and exercise the actual installed UI on supported Windows versions:
|
|
55
55
|
|
|
56
|
-
- Clean NSIS
|
|
56
|
+
- Clean NSIS installs, in-place upgrades from the previous release (including an MSI-installed release migrating through the updater), relaunch, tray quit and uninstall. Verify one installation entry and preserved projects/logs.
|
|
57
57
|
- Native browsing and capture at 100%, 150% and 200% display scaling; resize, maximize, multi-monitor movement, keyboard/clipboard, popup/self-close, and the relevant Okta/SSO tenant. Detach two active missions, minimize main independently, move them between monitors, and reattach into a window with its own open browser; verify both sessions and pending inputs remain intact.
|
|
58
58
|
- Provider authentication and one real mission/rail per enabled provider; steering during a tool call; cancellation; multi-repo integration/checkout with clean and conflicting working trees.
|
|
59
59
|
- Long-running frontend/backend processes, immediate startup failure, stopping nested processes, app quit/update during execution, and persisted logs after restart.
|
|
@@ -15,14 +15,14 @@ Installers are published on every release under:
|
|
|
15
15
|
|
|
16
16
|
> 📥 `https://specrails.dev/downloads/specrails-desktop/latest/`
|
|
17
17
|
|
|
18
|
-
Pick the
|
|
18
|
+
Pick the installer that matches your CPU architecture:
|
|
19
19
|
|
|
20
|
-
| Architecture |
|
|
21
|
-
| --- | --- |
|
|
22
|
-
| x64 | `specrails-desktop-<version>-x64-setup.exe` |
|
|
23
|
-
| ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
|
|
20
|
+
| Architecture | Installer |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| x64 | `specrails-desktop-<version>-x64-setup.exe` |
|
|
23
|
+
| ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Windows ships only the NSIS installer. For unattended or managed deployment, run it silently with `/S` (optionally `/D=<install dir>` as the last argument). Releases up to 2.62 also published an `.msi`; see [Updates](#updates) for how those installations migrate. The installer provisions Microsoft Edge WebView2 when it is missing, downloading it through a small embedded bootstrapper (Windows 11 and current Windows 10 already include it). Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
|
|
26
26
|
|
|
27
27
|
A machine-readable `manifest.json` in `latest/` describes the current release (version, sha256, size) per platform, including `windows-x64` and `windows-arm64` entries — consumers can read it to build download links without hardcoding a version.
|
|
28
28
|
|
|
@@ -59,7 +59,7 @@ When the bundle is present, the Tauri host sets `SPECRAILS_IS_DESKTOP=1` and `SP
|
|
|
59
59
|
|
|
60
60
|
The desktop app self-updates via the Tauri updater plugin. It checks a GitHub Releases `latest.json` endpoint and, on Windows, applies updates with `installMode: "passive"` — the update runs with a minimal progress UI and the app relaunches into the new version.
|
|
61
61
|
|
|
62
|
-
Updates
|
|
62
|
+
Updates use the signed NSIS `-setup.exe` for the same architecture. Installations made from an earlier `.msi` receive that NSIS update too: the installer detects the MSI installation and removes it before installing, so a single installation remains (Windows may ask for administrator approval to remove the machine-wide MSI). The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication.
|
|
63
63
|
|
|
64
64
|
Normal quit and updates request an authenticated graceful shutdown of the owned sidecar first, giving processes and persistent logs time to close before a bounded force-stop fallback.
|
|
65
65
|
|
|
@@ -73,8 +73,8 @@ Reserved paths (`.specrails/profiles/**`, `.claude/agents/custom-*.md`) are pres
|
|
|
73
73
|
|
|
74
74
|
## Uninstall
|
|
75
75
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
76
|
+
- Use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
|
|
77
|
+
- An installation from an earlier `.msi` release that has not updated yet: *Settings → Apps* or `msiexec /x <msi-path>`.
|
|
78
78
|
|
|
79
79
|
## Known limitations
|
|
80
80
|
|
|
@@ -4036,7 +4036,7 @@ var require_core = __commonJS({
|
|
|
4036
4036
|
constructor(opts = {}) {
|
|
4037
4037
|
this.schemas = {};
|
|
4038
4038
|
this.refs = {};
|
|
4039
|
-
this.formats =
|
|
4039
|
+
this.formats = /* @__PURE__ */ Object.create(null);
|
|
4040
4040
|
this._compilations = /* @__PURE__ */ new Set();
|
|
4041
4041
|
this._loading = {};
|
|
4042
4042
|
this._cache = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-desktop",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
58
|
"@tailwindcss/typography": "^0.5.19",
|
|
59
|
-
"ajv": "^8.
|
|
59
|
+
"ajv": "^8.20.0",
|
|
60
60
|
"better-sqlite3": "^12.8.0",
|
|
61
61
|
"chokidar": "^5.0.0",
|
|
62
62
|
"cross-spawn": "^7.0.6",
|
|
@@ -71,29 +71,28 @@
|
|
|
71
71
|
"selfsigned": "^5.5.0",
|
|
72
72
|
"tree-kill": "^1.2.2",
|
|
73
73
|
"werift": "^0.23.0",
|
|
74
|
-
"ws": "^8.
|
|
74
|
+
"ws": "^8.22.0",
|
|
75
75
|
"zod": "^3.25.76"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
+
"@babel/parser": "^7.29.9",
|
|
78
79
|
"@tauri-apps/cli": "^2.0.0",
|
|
79
80
|
"@types/better-sqlite3": "^7.6.0",
|
|
80
|
-
"@types/chokidar": "^1.7.5",
|
|
81
81
|
"@types/cross-spawn": "^6.0.6",
|
|
82
|
-
"@types/diff": "^7.0.2",
|
|
83
82
|
"@types/express": "^5.0.6",
|
|
84
83
|
"@types/js-yaml": "^4.0.9",
|
|
85
84
|
"@types/multer": "^2.1.0",
|
|
86
|
-
"@types/node": "^
|
|
85
|
+
"@types/node": "^26.6.5",
|
|
87
86
|
"@types/pdf-parse": "^1.1.5",
|
|
88
87
|
"@types/supertest": "^7.2.0",
|
|
89
88
|
"@types/ws": "^8.5.0",
|
|
90
|
-
"@vitest/coverage-v8": "^
|
|
89
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
91
90
|
"concurrently": "^9.2.1",
|
|
92
91
|
"esbuild": "^0.28.1",
|
|
93
92
|
"supertest": "^7.2.2",
|
|
94
93
|
"tsx": "^4.23.0",
|
|
95
|
-
"typescript": "^
|
|
96
|
-
"vitest": "^
|
|
94
|
+
"typescript": "^7.0.2",
|
|
95
|
+
"vitest": "^5.0.3"
|
|
97
96
|
},
|
|
98
97
|
"overrides": {
|
|
99
98
|
"ip": "npm:@eggjs/ip@2.1.0",
|
|
@@ -15,6 +15,26 @@ function uniq(names) {
|
|
|
15
15
|
}
|
|
16
16
|
return out;
|
|
17
17
|
}
|
|
18
|
+
/** Names and states only: rebuild each record field by field so nothing but
|
|
19
|
+
* the documented, value-free fields can ever reach an MCP client. */
|
|
20
|
+
function sanitizeStatus(raw) {
|
|
21
|
+
const report = (raw && typeof raw === 'object' ? raw : {});
|
|
22
|
+
const names = Array.isArray(report.names) ? report.names : [];
|
|
23
|
+
return {
|
|
24
|
+
checking: report.checking === true,
|
|
25
|
+
loginShellRecovery: report.loginShellRecovery !== false,
|
|
26
|
+
timeoutMs: typeof report.timeoutMs === 'number' ? report.timeoutMs : null,
|
|
27
|
+
statuses: names
|
|
28
|
+
.filter((entry) => typeof entry?.name === 'string' && typeof entry?.status === 'string')
|
|
29
|
+
.map((entry) => ({
|
|
30
|
+
name: entry.name,
|
|
31
|
+
status: entry.status,
|
|
32
|
+
shell: typeof entry.shell === 'string' ? entry.shell : null,
|
|
33
|
+
checkedAt: typeof entry.checkedAt === 'string' ? entry.checkedAt : null,
|
|
34
|
+
exitCode: typeof entry.exitCode === 'number' ? entry.exitCode : null,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
function envTools() {
|
|
19
39
|
return [
|
|
20
40
|
{
|
|
@@ -22,11 +42,12 @@ function envTools() {
|
|
|
22
42
|
title: 'Project worktree environment',
|
|
23
43
|
description: 'Read, scan, and configure the per-project environment variable NAME allowlist used for rail jobs and isolated loop worktrees. ' +
|
|
24
44
|
'Values are never read or stored by this tool; runs read them from the server environment and can recover configured names from the login shell when the desktop process is missing them. scan inspects safe repo config files such as package.json, .npmrc, .yarnrc.yml and pnpm-workspace.yaml for ${VAR} / process.env.VAR references and private scoped package auth hints. ' +
|
|
25
|
-
'
|
|
45
|
+
'Each configured name reports a value-free resolution status: inherited, recovered (from the login shell), not-defined, probe-timeout or probe-failed. ' +
|
|
46
|
+
'Actions: get (read current names and their statuses), status (statuses only), recheck (probe the login shell now, then return statuses), scan (discover candidates), set (write names), auto_configure (scan then merge discovered names into settings).',
|
|
26
47
|
hintTier: 'read',
|
|
27
48
|
tier: (args) => (args.action === 'set' || args.action === 'auto_configure' ? 'write' : 'read'),
|
|
28
49
|
inputSchema: {
|
|
29
|
-
action: zod_1.z.enum(['get', 'scan', 'set', 'auto_configure']).describe('Operation'),
|
|
50
|
+
action: zod_1.z.enum(['get', 'status', 'recheck', 'scan', 'set', 'auto_configure']).describe('Operation'),
|
|
30
51
|
projectId: zod_1.z.string().optional().describe('Project id (defaults to the active project)'),
|
|
31
52
|
names: zod_1.z.array(zod_1.z.string()).optional().describe('set: env var names to configure'),
|
|
32
53
|
merge: zod_1.z.boolean().optional().describe('set: merge with existing names instead of replacing them (default true)'),
|
|
@@ -36,7 +57,21 @@ function envTools() {
|
|
|
36
57
|
const base = (0, types_1.projectPath)(ctx, args.projectId);
|
|
37
58
|
if (action === 'get') {
|
|
38
59
|
const settings = await (0, types_1.apiCall)(ctx, 'GET', `${base}/settings`);
|
|
39
|
-
|
|
60
|
+
const names = Array.isArray(settings.worktreeEnvPassthrough) ? settings.worktreeEnvPassthrough : [];
|
|
61
|
+
// Statuses are best-effort: an older server without the route still answers names.
|
|
62
|
+
try {
|
|
63
|
+
const { statuses } = sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
|
|
64
|
+
return { names, statuses };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { names };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (action === 'status') {
|
|
71
|
+
return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
|
|
72
|
+
}
|
|
73
|
+
if (action === 'recheck') {
|
|
74
|
+
return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'POST', `${base}/env-passthrough/recheck`));
|
|
40
75
|
}
|
|
41
76
|
if (action === 'scan') {
|
|
42
77
|
const project = (0, types_1.requireProject)(ctx, args.projectId).project;
|
|
@@ -30,7 +30,7 @@ function loopsTools() {
|
|
|
30
30
|
'For a standalone ticket-less run use the run action. ' +
|
|
31
31
|
'Actions: ' +
|
|
32
32
|
'list (all saved loops), ' +
|
|
33
|
-
'get (one loop by id), ' +
|
|
33
|
+
'get (one loop by id, plus turnLimits: each AI step\'s role and effective Maximum turns — set graph.config.agents.roles.<role>.maxTurns, or agents.<role> for built-in roles, then publish; project Agent Runtime settings never change loop runs), ' +
|
|
34
34
|
'templates (built-in template catalog with graphs), ' +
|
|
35
35
|
'factory (the built-in loops — factory:implement, factory:freestyle, factory:sdd-quick-openspec — with the graph a rail launch runs; ' +
|
|
36
36
|
'built-ins are ordinary editable loops whose loopId IS the factory id: change them with update + publish and the change applies to every rail, chat launch and companion that uses that built-in), ' +
|
|
@@ -23,6 +23,7 @@ const agent_runtime_package_1 = require("./agent-runtime-package");
|
|
|
23
23
|
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
24
24
|
const core_node_runtime_1 = require("../../../core-node-runtime");
|
|
25
25
|
const win_spawn_1 = require("../../../util/win-spawn");
|
|
26
|
+
const loop_turn_limits_1 = require("../../loops/runtime/loop-turn-limits");
|
|
26
27
|
function runtimeChangeName(runId) {
|
|
27
28
|
return 'runtime-' + (0, node_crypto_1.createHash)('sha256').update(runId).digest('hex').slice(0, 20);
|
|
28
29
|
}
|
|
@@ -265,9 +266,17 @@ async function runAgentRuntimeInvocation(options) {
|
|
|
265
266
|
}
|
|
266
267
|
if ((0, node_fs_1.existsSync)(frozenPath)) {
|
|
267
268
|
const frozen = JSON.parse((0, node_fs_1.readFileSync)(frozenPath, 'utf8'));
|
|
268
|
-
|
|
269
|
+
// A definition names the roles its steps run (Implement: plan/build/assess/correct); list exactly those with their turn ceiling.
|
|
270
|
+
const definitionRoles = definitionEngine ? JSON.parse((0, node_fs_1.readFileSync)(definitionFile, 'utf8')).roles : undefined;
|
|
271
|
+
const used = Array.isArray(definitionRoles) ? new Set(definitionRoles.filter((role) => typeof role === 'string')) : undefined;
|
|
272
|
+
if (used?.has('developer'))
|
|
273
|
+
used.add('fixer');
|
|
274
|
+
const assignments = [...Object.entries(frozen.agents), ...(frozen.fixer ? [['fixer', frozen.fixer]] : []), ...Object.entries(frozen.roles ?? {})]
|
|
275
|
+
.filter(([role]) => used ? used.has(role) : role in frozen.agents || role === 'fixer');
|
|
276
|
+
for (const [role, assignment] of assignments) {
|
|
277
|
+
const turns = assignment.maxTurns === undefined ? `max ${loop_turn_limits_1.CORE_DEFAULT_MAX_TURNS} turns (Core default)` : `max ${assignment.maxTurns} turns`;
|
|
269
278
|
try {
|
|
270
|
-
options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'}\n`);
|
|
279
|
+
options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'} · ${turns}\n`);
|
|
271
280
|
}
|
|
272
281
|
catch { /* Log observers cannot prevent execution. */ }
|
|
273
282
|
}
|
|
@@ -378,7 +387,8 @@ async function runAgentRuntimeInvocation(options) {
|
|
|
378
387
|
const kind = payload?.type === 'step_succeeded' && payload.outcome === 'fail' ? 'verification_failed' : payload?.type;
|
|
379
388
|
const completionDetail = payload?.type === 'workflow_failed' && Array.isArray(payload.reasons)
|
|
380
389
|
? payload.reasons.filter(reason => typeof reason === 'string' && reason.trim()).join('; ').slice(0, 4_000) : undefined;
|
|
381
|
-
const
|
|
390
|
+
const rawDetail = payload?.error?.message ?? payload?.message ?? completionDetail;
|
|
391
|
+
const detail = payload?.type === 'step_failed' && typeof rawDetail === 'string' ? (0, loop_turn_limits_1.withTurnLimitHint)(rawDetail, payload.stepId) : rawDetail;
|
|
382
392
|
const stepKey = JSON.stringify([payload?.scopeId ?? null, payload?.nodePath ?? payload?.stepId ?? null]);
|
|
383
393
|
if (payload?.type === 'step_failed' && typeof detail === 'string' && detail.trim()) {
|
|
384
394
|
stepFailures.delete(stepKey);
|
|
@@ -447,7 +447,7 @@ function createIsolatedRunSettlement(ports) {
|
|
|
447
447
|
run: a, implementationOutcome, deliveryOutcome: 'blocked',
|
|
448
448
|
initialSha: a.initialSha, finalSha: await readHeadSha(git, a.handle.worktreePath),
|
|
449
449
|
failureCode: 'settlement_interrupted',
|
|
450
|
-
failureDetail: 'Programmatic workflow
|
|
450
|
+
failureDetail: 'Programmatic workflow stopped before delivery; partial changes stay in its original worktree. The run log names the failing step. A resumable step offers Resume on the job page; a raised loop limit (such as a step\'s Maximum turns) needs a new launch.',
|
|
451
451
|
safeToRelease: false,
|
|
452
452
|
};
|
|
453
453
|
}
|
|
@@ -2217,8 +2217,12 @@ class QueueManager {
|
|
|
2217
2217
|
// values are read from the server env (with login-shell recovery for missing
|
|
2218
2218
|
// names) at spawn time. Apply this before Specrails' own env overlays so
|
|
2219
2219
|
// internal SPECRAILS_* control-plane values always win.
|
|
2220
|
+
let environmentNotice = null;
|
|
2220
2221
|
if (this._db) {
|
|
2221
2222
|
spawnEnv = (0, project_env_1.applyWorktreeEnvPassthrough)(this._db, spawnEnv);
|
|
2223
|
+
// One value-free line naming unresolved configured names (written to the
|
|
2224
|
+
// job log once the readers exist). The run proceeds without them.
|
|
2225
|
+
environmentNotice = (0, project_env_1.worktreeEnvPassthroughWarning)(this._db, spawnEnv);
|
|
2222
2226
|
}
|
|
2223
2227
|
const telemetryEnabled = !!(this._projectId && this._db && (0, db_1.getProjectSettings)(this._db).pipelineTelemetryEnabled);
|
|
2224
2228
|
// Resolve the framework version ONCE at spawn time — `framework/current`
|
|
@@ -2605,6 +2609,10 @@ class QueueManager {
|
|
|
2605
2609
|
const stdoutReader = (0, readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
|
|
2606
2610
|
const stderrReader = (0, readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
|
|
2607
2611
|
this._jobReaders.set(jobId, { stdout: stdoutReader, stderr: stderrReader });
|
|
2612
|
+
if (environmentNotice) {
|
|
2613
|
+
persistEvent({ event_type: 'log', source: 'stderr', payload: JSON.stringify({ line: environmentNotice }) }, false);
|
|
2614
|
+
emitLine('stderr', environmentNotice);
|
|
2615
|
+
}
|
|
2608
2616
|
stdoutReader.on('line', (line) => {
|
|
2609
2617
|
if (this._disposed)
|
|
2610
2618
|
return;
|
|
@@ -181,6 +181,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
181
181
|
const completionContexts = new Map();
|
|
182
182
|
const runtimeConfigPath = (cwd) => (0, node_path_1.join)(opts.pluginScope?.().stateRoot ?? cwd, '.specrails', 'agent-runtime.json');
|
|
183
183
|
return {
|
|
184
|
+
...(opts.environmentNotice ? { environmentNotice: opts.environmentNotice } : {}),
|
|
184
185
|
async cancelDefinition({ runId, contextPath, requestId }) {
|
|
185
186
|
const host = (0, agent_runtime_bridge_1.readFrozenRuntimeHost)(contextPath, resolveEnv(), runId);
|
|
186
187
|
await (0, agent_runtime_bridge_1.runAgentRuntimeControl)({ kind: 'cancel', runId, contextPath, requestId, ...host });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.supportsHostBlockers = exports.HOST_BLOCKER_KINDS = void 0;
|
|
3
|
+
exports.supportsHostBlockers = exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS = exports.HOST_BLOCKER_KINDS = void 0;
|
|
4
4
|
exports.configurableImplementGraph = configurableImplementGraph;
|
|
5
5
|
const agent_runtime_settings_1 = require("../../agent-runtime/runtime/agent-runtime-settings");
|
|
6
6
|
const loop_agents_1 = require("./loop-agents");
|
|
@@ -12,6 +12,8 @@ const aspects = { type_correctness: 60, pattern_adherence: 60, test_coverage: 60
|
|
|
12
12
|
const reviewPolicy = 'approved == true; issues must be empty; score >= 70; ' + Object.entries(aspects).map(([aspect, threshold]) => `${aspect} >= ${threshold}`).join('; ');
|
|
13
13
|
exports.HOST_BLOCKER_KINDS = ['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope'];
|
|
14
14
|
/** Fixer/developer structured `blocker`: the host ends the run on it instead of counting a correction round. */
|
|
15
|
+
/** Write roles of a large spec routinely exceed Core's default of 100 turns (one cross-repo Developer used 113). */
|
|
16
|
+
exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS = 300;
|
|
15
17
|
const blocker = objectSchema({ kind: { enum: [...exports.HOST_BLOCKER_KINDS] }, command: string, cwd: string, evidence: string, requiredAction: string }, ['kind', 'requiredAction']);
|
|
16
18
|
/** Host-owned environment repair and structured blockers need the paired Core generation. */
|
|
17
19
|
const supportsHostBlockers = (capabilities) => capabilities?.hostBlockers === 1;
|
|
@@ -24,9 +26,9 @@ function configurableImplementGraph(capabilities) {
|
|
|
24
26
|
const agents = (0, loop_agents_1.defaultLoopAgents)();
|
|
25
27
|
agents.roles = {
|
|
26
28
|
plan: { ...agents.agents.architect, access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'architect') },
|
|
27
|
-
build: { ...agents.agents.developer, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'developer') },
|
|
29
|
+
build: { ...agents.agents.developer, maxTurns: exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'developer') },
|
|
28
30
|
assess: { ...agents.agents.reviewer, access: 'read', artifacts: 'none', openspecSkill: 'openspec-verify-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'reviewer') },
|
|
29
|
-
correct: { ...agents.fixer, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'fixer') },
|
|
31
|
+
correct: { ...agents.fixer, maxTurns: exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'fixer') },
|
|
30
32
|
};
|
|
31
33
|
const nodes = [{ id: 'start', type: 'start', position: { x: 0, y: 0 } }], edges = [];
|
|
32
34
|
const node = (id, label, kind, params, outcomes) => {
|
|
@@ -48,7 +50,7 @@ function configurableImplementGraph(capabilities) {
|
|
|
48
50
|
node('reset-confidence', 'Reset confidence after answer', 'assign', { set: { planInvestigated: false } }, { next: 'architect', failed: 'failed' });
|
|
49
51
|
node('validate', 'Validate OpenSpec', 'openspec-validate', { change }, { pass: 'freeze', fail: 'architect', failed: 'failed' });
|
|
50
52
|
node('freeze', 'Freeze approved artifacts', 'artifact-contract', { change, contractId: 'plan', action: 'freeze' }, { pass: 'initialize-corrections', fail: 'architect', failed: 'failed' });
|
|
51
|
-
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3 } }, { next: 'developer', failed: 'failed' });
|
|
53
|
+
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3, archiveRetries: 0 } }, { next: 'developer', failed: 'failed' });
|
|
52
54
|
const implementation = objectSchema({ summary: string, incomplete: strings, blocker }, ['summary', 'incomplete']);
|
|
53
55
|
turn('developer', 'Developer', 'build', `Implement every approved task for change ${change} using the bound official apply skill. Preserve proposal, design and specs; only tick tasks you completed. Report summary and incomplete task descriptions. If a task cannot be finished because of a host precondition or environment blocker outside this change (for example the sandbox forbids starting a local server, a credential or tool is missing), keep the work done, leave that task unchecked and return the structured blocker (kind, command, cwd, evidence, requiredAction). A failing test, a test you cannot make pass, or unexpected browser or tool behaviour is NOT a host blocker: change the test or the implementation instead. The host confirms a blocker by running its own verification before ending the run; a failure it reproduces comes back as a correction. Prior execution evidence and human answers: {{history.text}}`, implementation, 'developer-blocker', writes);
|
|
54
56
|
node('developer-blocker', 'Developer reported a host blocker', 'condition', { expr: 'exists($outputs.developer.structured.blocker) && $outputs.developer.structured.blocker != null' }, { true: 'developer-blocker-kind', false: 'tasks' });
|
|
@@ -76,9 +78,14 @@ function configurableImplementGraph(capabilities) {
|
|
|
76
78
|
node('correction-progress', 'Check correction progress', 'condition', { expr: 'exists($outputs.fixer.candidateHash) && $outputs.fixer.candidateHash != null && $outputs.fixer.candidateHash != $outputs.verify.candidateHash' }, { true: 'tasks', false: 'correction-stalled' });
|
|
77
79
|
node('approve', 'Approve archive', 'approval', { reason: `Approve archiving the reviewed and verified candidate for change ${change}.`, bindCandidate: true, enabled: agents.approvalBeforeArchive === true }, { next: 'approval-policy' });
|
|
78
80
|
node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
|
|
79
|
-
node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: '
|
|
81
|
+
node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'archive-retry' });
|
|
82
|
+
// Only a stale or unverified candidate is worth re-verifying before another archive attempt.
|
|
83
|
+
// An OpenSpec rejection (e.g. a requirement without SHALL/MUST) repeats identically, so it ends the run
|
|
84
|
+
// instead of cycling verify → review → archive until the transition budget runs out.
|
|
85
|
+
node('archive-retry', 'Archive needs a fresh verification', 'condition', { expr: 'exists($outputs.archive.reason) && ($outputs.archive.reason == "unverified" || $outputs.archive.reason == "candidate_changed") && $vars.archiveRetries < 2' }, { true: 'count-archive-retry', false: 'archive-failed' });
|
|
86
|
+
node('count-archive-retry', 'Count archive re-verification', 'assign', { increment: { archiveRetries: 1 } }, { next: 'verify', failed: 'failed' });
|
|
80
87
|
node('verify-archive', 'Verify archived candidate', 'verify', verifyParams, verifyEnds('done', 'failed', 'host-blocked-archive'));
|
|
81
|
-
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'implementation-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'The developer made no changes while approved tasks are still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'implementation-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Implementation attempt limit reached ({{run.implementationAttempts}}/{{run.implementationLimit}}) with approved tasks still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'developer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Developer reported a host blocker ({{outputs.developer.structured.blocker.kind}}): {{outputs.developer.structured.blocker.requiredAction}} Evidence: {{outputs.developer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'developer' } : {}) } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
|
|
88
|
+
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'archive-failed', type: 'end', position: { x: 360, y: (nodes.length + 1) * 140 }, data: { outcome: 'failure', reason: `OpenSpec could not archive change ${change}. The implementation was verified and reviewed; the archive step in the run log shows OpenSpec's reason (for example a requirement whose first body line has no SHALL or MUST). Fix the spec in the original worktree, then archive it there or relaunch.` } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'implementation-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'The developer made no changes while approved tasks are still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'implementation-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Implementation attempt limit reached ({{run.implementationAttempts}}/{{run.implementationLimit}}) with approved tasks still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'developer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Developer reported a host blocker ({{outputs.developer.structured.blocker.kind}}): {{outputs.developer.structured.blocker.requiredAction}} Evidence: {{outputs.developer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'developer' } : {}) } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
|
|
82
89
|
if (hostBlockers) {
|
|
83
90
|
const hostBlocked = (id, source) => ({ id, type: 'end', position: { x: 1800, y: nodes.length * 140 }, data: { outcome: 'failure', blockerFrom: source, reason: `Host blocker ({{outputs.${source}.blocker.kind}}): {{outputs.${source}.blocker.reason}} Required action: {{outputs.${source}.blocker.requiredAction}}` } });
|
|
84
91
|
nodes.push(hostBlocked('host-blocked', 'verify'), hostBlocked('host-blocked-archive', 'verify-archive'));
|
|
@@ -1009,6 +1009,13 @@ class LoopRunManager {
|
|
|
1009
1009
|
logLine(`▶ Loop "${req.loopName ?? req.loopId}" started${req.spec?.title ? ` — spec: ${req.spec.title}` : ''}`);
|
|
1010
1010
|
if (req.isolation)
|
|
1011
1011
|
logLine(`⎇ Isolated worktree: ${req.isolation.worktreePath} (branch ${req.isolation.branch})`);
|
|
1012
|
+
let environmentNotice = null;
|
|
1013
|
+
try {
|
|
1014
|
+
environmentNotice = this.executors.environmentNotice?.() ?? null;
|
|
1015
|
+
}
|
|
1016
|
+
catch { /* diagnostic only */ }
|
|
1017
|
+
if (environmentNotice)
|
|
1018
|
+
logLine(environmentNotice, 'stderr');
|
|
1012
1019
|
if (!definitionEngine && !usageTelemetryAvailable) {
|
|
1013
1020
|
logLine(`⚠️ ${adapter.displayName} does not report token/cost usage in headless mode; usage totals will remain unavailable.` +
|
|
1014
1021
|
(configuredMaxCostUsd !== undefined
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CORE_DEFAULT_MAX_TURNS = void 0;
|
|
4
|
+
exports.loopStepRole = loopStepRole;
|
|
5
|
+
exports.loopTurnLimits = loopTurnLimits;
|
|
6
|
+
exports.withTurnLimitHint = withTurnLimitHint;
|
|
7
|
+
/** Core's ceiling for one role turn when the role sets no `maxTurns` (Core `cli-executor`). */
|
|
8
|
+
exports.CORE_DEFAULT_MAX_TURNS = 100;
|
|
9
|
+
function object(value) {
|
|
10
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/** The agent role a canvas step runs, mirroring the definition compiler. */
|
|
13
|
+
function loopStepRole(id, kind, params) {
|
|
14
|
+
if (kind === 'prompt')
|
|
15
|
+
return id === 'fix' ? 'fixer' : id === 'prepare' ? 'architect' : params.access === 'read' ? 'reviewer' : 'developer';
|
|
16
|
+
if ((kind === 'role-turn' || kind === 'decider') && typeof params.roleId === 'string')
|
|
17
|
+
return params.roleId;
|
|
18
|
+
if (kind === 'implementation-step' && ['architect', 'developer', 'reviewer', 'fixer'].includes(String(params.phase)))
|
|
19
|
+
return String(params.phase);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function roleEngine(agents, role) {
|
|
23
|
+
if (!agents)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (role === 'fixer')
|
|
26
|
+
return agents.fixer ?? agents.agents.developer;
|
|
27
|
+
if (role === 'architect' || role === 'developer' || role === 'reviewer')
|
|
28
|
+
return agents.agents[role];
|
|
29
|
+
return agents.roles?.[role];
|
|
30
|
+
}
|
|
31
|
+
/** Effective per-step turn ceiling: step engine override, then the loop role, then Core's default. */
|
|
32
|
+
function loopTurnLimits(graph) {
|
|
33
|
+
const agents = graph.config.agents;
|
|
34
|
+
const limits = [];
|
|
35
|
+
const visit = (body) => {
|
|
36
|
+
for (const node of body.nodes) {
|
|
37
|
+
if (node.type !== 'core')
|
|
38
|
+
continue;
|
|
39
|
+
const params = object(node.data?.params) ? node.data.params : {};
|
|
40
|
+
const role = loopStepRole(node.id, node.data?.kind, params);
|
|
41
|
+
if (!role)
|
|
42
|
+
continue;
|
|
43
|
+
const label = typeof node.data?.label === 'string' ? node.data.label : undefined;
|
|
44
|
+
const stepTurns = object(params.engine) ? params.engine.maxTurns : undefined;
|
|
45
|
+
const roleTurns = roleEngine(agents, role)?.maxTurns;
|
|
46
|
+
const [maxTurns, source] = typeof stepTurns === 'number' ? [stepTurns, 'step']
|
|
47
|
+
: typeof roleTurns === 'number' ? [roleTurns, 'role'] : [exports.CORE_DEFAULT_MAX_TURNS, 'core-default'];
|
|
48
|
+
limits.push({ stepId: node.id, ...(label ? { label } : {}), role, maxTurns, source });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
visit(graph);
|
|
52
|
+
for (const component of Object.values(graph.components ?? {}))
|
|
53
|
+
visit(component);
|
|
54
|
+
return limits;
|
|
55
|
+
}
|
|
56
|
+
const TURN_LIMIT = /reached the configured limit of (\d+) turns for ([\w:-]+)/;
|
|
57
|
+
/** Appends where to raise the limit to Core's turn-limit failure; other messages pass through. */
|
|
58
|
+
function withTurnLimitHint(message, stepId) {
|
|
59
|
+
const match = message.match(TURN_LIMIT);
|
|
60
|
+
if (!match)
|
|
61
|
+
return message;
|
|
62
|
+
const step = stepId ? `the "${stepId}" step` : `the step that runs role "${match[2]}"`;
|
|
63
|
+
return `${message} To raise it: Loops ▸ this loop ▸ select ${step} ▸ "Maximum turns" (role "${match[2]}", currently ${match[1]}), save and publish. Project Settings do not change loop runs.`;
|
|
64
|
+
}
|
|
@@ -17,6 +17,7 @@ const builtin_loops_1 = require("./builtin-loops");
|
|
|
17
17
|
const loop_command_catalog_1 = require("./loop-command-catalog");
|
|
18
18
|
const loop_constants_1 = require("./loop-constants");
|
|
19
19
|
const loop_preview_1 = require("./loop-preview");
|
|
20
|
+
const loop_turn_limits_1 = require("./loop-turn-limits");
|
|
20
21
|
const BUILTIN_DELETE_MESSAGE = 'Built-in loops cannot be deleted. Use Restore original to reset it, or Duplicate it to keep a separate copy.';
|
|
21
22
|
async function factoryCapabilities() {
|
|
22
23
|
try {
|
|
@@ -281,7 +282,8 @@ function registerLoopsRoutes(router, deps) {
|
|
|
281
282
|
res.status(404).json({ error: 'Loop not found' });
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
|
-
|
|
285
|
+
// Effective per-step turn ceilings, so readers (UI, MCP) never guess where the limit lives.
|
|
286
|
+
res.json({ loop, turnLimits: (0, loop_turn_limits_1.loopTurnLimits)(loop.graph) });
|
|
285
287
|
});
|
|
286
288
|
// ── Create ───────────────────────────────────────────────────────────────────
|
|
287
289
|
router.post('/loops', (req, res) => {
|
|
@@ -81,6 +81,14 @@ repository members, and runs coordinated AI coding pipelines over their selected
|
|
|
81
81
|
(\`factory:implement\`, \`factory:freestyle\`, \`factory:sdd-quick-openspec\`)
|
|
82
82
|
are editable in place: \`update\` + \`publish\` changes the built-in for every
|
|
83
83
|
rail that uses it, \`restore_builtin\` resets it; \`duplicate\` makes a copy.
|
|
84
|
+
**Turn limits live in the loop, never in Settings.** Each AI step runs an
|
|
85
|
+
agent role with a "Maximum turns" ceiling (Core default 100 when unset).
|
|
86
|
+
\`specrails_loops(get)\` returns \`turnLimits\` (step → role → effective
|
|
87
|
+
value). Implement steps run roles plan/build/assess/correct (Architect,
|
|
88
|
+
Developer, Reviewer, Fixer), stored at \`graph.config.agents.roles.<role>.maxTurns\`.
|
|
89
|
+
To raise one: Loops ▸ the loop ▸ select the step ▸ "Maximum turns", save and
|
|
90
|
+
publish (or \`update\` + \`publish\`). Project Agent Runtime settings do not
|
|
91
|
+
change loop runs, and a failed run keeps its frozen limit: relaunch.
|
|
84
92
|
- **Profile** — per-project agent-chain config (which agents, which models,
|
|
85
93
|
routing). Supported by Claude and Kimi; forced to null on Codex/Gemini rails.
|
|
86
94
|
- **Provider / engine** — claude, codex, gemini or kimi. A project installs one
|