specrails-desktop 2.60.0 → 2.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/ActivityFeedPage-C_pqc281.js +1 -0
- package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-vz6rbDtu.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +2 -0
- package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-99fBBLri.js} +1 -1
- package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CTwm_6T8.js} +3 -3
- package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-YD8MLoPa.js} +1 -1
- package/client/dist/assets/CodePage-CLJn2F2e.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-MWGQVbwp.js} +1 -1
- package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-XUCYKoq0.js} +1 -1
- package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-dOLC8bXA.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-BvKQgwM9.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +19 -0
- package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-CkiubrkZ.js} +1 -1
- package/client/dist/assets/JobDetailPage-CNe4Wn7l.js +1 -0
- package/client/dist/assets/JobsPage-DSts_L_d.js +1 -0
- package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-Dv3dxHr_.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-DN2lbSnL.js} +1 -1
- package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-DHgIreEW.js} +2 -2
- package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +1 -0
- package/client/dist/assets/PluginsPage-D1EJ9oja.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-CW4eKG3x.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-DrtPqvc_.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-DcFlhUxy.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +16 -0
- package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-CAY2Rd2f.js} +1 -1
- package/client/dist/assets/agent-5bSixVzz.js +1 -0
- package/client/dist/assets/agent-BR4EHfhq.js +1 -0
- package/client/dist/assets/agent-C0IKkJP8.js +1 -0
- package/client/dist/assets/agent-C7li9ayC.js +1 -0
- package/client/dist/assets/agent-D0dRmEen.js +1 -0
- package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
- package/client/dist/assets/agent-Dip4h2YE.js +1 -0
- package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
- package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
- package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
- package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
- package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
- package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
- package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
- package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
- package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
- package/client/dist/assets/ban-E4EaZe3a.js +1 -0
- package/client/dist/assets/chevron-up-CwGUHd6D.js +1 -0
- package/client/dist/assets/dist-Bq10nS2m.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DlQY1Lm_.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-COUeq65G.js} +1 -1
- package/client/dist/assets/index-BacrO_ub.css +2 -0
- package/client/dist/assets/index-CONHvEJw.js +83 -0
- package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-C94VNHNY.js} +1 -1
- package/client/dist/assets/layers-eDMjG41I.js +1 -0
- package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
- package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
- package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
- package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
- package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
- package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
- package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
- package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
- package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
- package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
- package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
- package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
- package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
- package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
- package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
- package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
- package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-Cc3oNv8X.js} +1 -1
- package/client/dist/assets/save-B6wGWwBR.js +1 -0
- package/client/dist/assets/{settings-Dn-pXukr.js → settings-BKJZ2KEM.js} +1 -1
- package/client/dist/assets/{settings-BBj_kZaD.js → settings-BV3jocIx.js} +1 -1
- package/client/dist/assets/{settings-COlH_P33.js → settings-BjM2Dw8_.js} +1 -1
- package/client/dist/assets/settings-CKu0LTib.js +1 -0
- package/client/dist/assets/{settings-D77mC_Lb.js → settings-DA77M48m.js} +1 -1
- package/client/dist/assets/{settings-ByVE7jdZ.js → settings-LfFnl1UH.js} +1 -1
- package/client/dist/assets/{settings-DZKy7MqN.js → settings-P1IQvhnL.js} +1 -1
- package/client/dist/assets/{settings-B5Hy_Kol.js → settings-eIMJjyK_.js} +1 -1
- package/client/dist/assets/{spending-Oov7p52A.js → spending-DemeFfrH.js} +1 -1
- package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-9Lwoel_8.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-DL0xYoZ2.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-BEHPeU1Y.js} +2 -2
- package/client/dist/assets/zap-CHo3aCD1.js +1 -0
- package/client/dist/index.html +26 -22
- package/docs/guide/de/integrations/6-agent-chat.md +11 -0
- package/docs/guide/en/integrations/6-agent-chat.md +11 -0
- package/docs/guide/es/integrations/6-agent-chat.md +11 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
- package/docs/guide/it/integrations/6-agent-chat.md +11 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/agent-sessions.md +157 -0
- package/docs/internals/api-reference.md +33 -4
- package/docs/internals/configuration.md +36 -0
- package/docs/internals/core-runtime-updates.md +10 -3
- package/docs/internals/operations-runbook.md +11 -0
- package/docs/internals/programmatic-agent-runtime.md +2 -0
- package/docs/internals/source-map.md +40 -1
- package/mcp-bridge/dist/specrails-mcp.js +82 -15
- package/package.json +1 -1
- package/server/dist/agent-mcp-config.js +41 -0
- package/server/dist/core-compat.js +2 -0
- package/server/dist/desktop-db.js +101 -4
- package/server/dist/desktop-router.js +31 -2
- package/server/dist/index.js +58 -0
- package/server/dist/mcp/tools/app-settings.js +11 -0
- package/server/dist/mcp/tools/mission.js +45 -2
- package/server/dist/mcp/tools/specs.js +2 -2
- package/server/dist/modules/accounting/runtime/result-event.js +9 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
- package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
- package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
- package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
- package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
- package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
- package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
- package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
- package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
- package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
- package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
- package/server/dist/modules/agent-sessions/index.js +31 -0
- package/server/dist/modules/agent-sessions/ports.js +2 -0
- package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
- package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
- package/server/dist/modules/agents/runtime/agent-store.js +3 -1
- package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
- package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
- package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
- package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +41 -11
- package/server/dist/modules/loops/runtime/loop-run-manager.js +4 -0
- package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
- package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
- package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
- package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
- package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
- package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
- package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
- package/server/dist/modules/project-settings/adapters/http.js +14 -2
- package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
- package/server/dist/modules/project-settings/domain.js +24 -0
- package/server/dist/modules/project-settings/index.js +2 -1
- package/server/dist/modules/specs/runtime/spec-models.js +1 -1
- package/server/dist/plugins/codex-spawn.js +15 -7
- package/server/dist/project-registry.js +32 -0
- package/server/dist/project-router-settings.js +7 -1
- package/server/dist/providers/claude-adapter.js +6 -1
- package/server/dist/schemas/agent-runtime.schema.json +9 -7
- package/client/dist/assets/ActivityFeedPage-3J7fR8yB.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-Dps30tk9.js +0 -2
- package/client/dist/assets/AgentModeCodePane-CvLimey8.js +0 -2
- package/client/dist/assets/CodePage-D981zRHy.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
- package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +0 -1
- package/client/dist/assets/JobsPage-BSrl8P7u.js +0 -1
- package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-BD2bsRBe.js +0 -1
- package/client/dist/assets/PluginsPage-B8f49Mvn.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-D7q-O6h3.js +0 -1
- package/client/dist/assets/ReviewPacketPage-DpZSzF-Q.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-BHmtj1As.js +0 -16
- package/client/dist/assets/agent--8QAfoXL.js +0 -1
- package/client/dist/assets/agent-3HxXkIWX.js +0 -1
- package/client/dist/assets/agent-CpFCz32T.js +0 -1
- package/client/dist/assets/agent-DVnw6ybO.js +0 -1
- package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
- package/client/dist/assets/agent-hv57-zyG.js +0 -1
- package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
- package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
- package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
- package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
- package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
- package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
- package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
- package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
- package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
- package/client/dist/assets/index-Bfunvi6w.css +0 -2
- package/client/dist/assets/index-g8S62tpS.js +0 -83
- package/client/dist/assets/save-OsUqVn99.js +0 -1
- package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
- package/client/dist/assets/zap-CUOlvtT1.js +0 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readSelectedSessionContract = readSelectedSessionContract;
|
|
7
|
+
exports.coreSessionsAvailability = coreSessionsAvailability;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const agent_runtime_loader_1 = require("../../agent-runtime/runtime/agent-runtime-loader");
|
|
11
|
+
const availability_1 = require("../domain/availability");
|
|
12
|
+
const protocol_1 = require("../domain/protocol");
|
|
13
|
+
/** Read `agentRuntime.sessions` from the selected Core package next to its runtime CLI. */
|
|
14
|
+
function readSelectedSessionContract(cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)()) {
|
|
15
|
+
if (!cli)
|
|
16
|
+
return null;
|
|
17
|
+
const contract = node_path_1.default.resolve(node_path_1.default.dirname(cli), '..', '..', 'integration-contract.json');
|
|
18
|
+
if (!node_fs_1.default.existsSync(contract))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(node_fs_1.default.readFileSync(contract, 'utf8')).agentRuntime?.sessions ?? null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide once per call whether Core sessions are usable now. Any failure to
|
|
29
|
+
* probe Core degrades to legacy transports with an explicit reason.
|
|
30
|
+
*/
|
|
31
|
+
async function coreSessionsAvailability(env = process.env) {
|
|
32
|
+
const flag = (0, availability_1.parseCoreSessionsFlag)(env.SPECRAILS_CORE_SESSIONS);
|
|
33
|
+
if (flag === 'off')
|
|
34
|
+
return (0, availability_1.resolveCoreSessionsAvailability)(flag, null);
|
|
35
|
+
try {
|
|
36
|
+
const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
|
|
37
|
+
const contract = (0, protocol_1.checkSessionContract)(readSelectedSessionContract());
|
|
38
|
+
return (0, availability_1.resolveCoreSessionsAvailability)(flag, runtime.api?.capabilities, contract.compatible);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return { enabled: false, flag, reason: `Core runtime probe failed: ${error.message}` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionHostRegistry = void 0;
|
|
4
|
+
const session_event_pump_1 = require("../application/session-event-pump");
|
|
5
|
+
const errors_1 = require("../domain/errors");
|
|
6
|
+
const host_state_1 = require("../domain/host-state");
|
|
7
|
+
/** Failures a restart cannot fix: the scope degrades at once instead of looping. */
|
|
8
|
+
const FATAL_CODES = new Set(['journal_locked', 'protocol_mismatch', 'driver_unavailable']);
|
|
9
|
+
/**
|
|
10
|
+
* One supervised Core session host per scope (project key or `global`).
|
|
11
|
+
* Hosts start lazily, restart with backoff after unexpected exits, re-open and
|
|
12
|
+
* replay tracked sessions, and become degraded after repeated failures so
|
|
13
|
+
* callers can fall back to legacy transports for new turns.
|
|
14
|
+
*/
|
|
15
|
+
class SessionHostRegistry {
|
|
16
|
+
options;
|
|
17
|
+
scopes = new Map();
|
|
18
|
+
stopped = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
status(scope) {
|
|
23
|
+
return this.scopes.get(scope)?.state.status ?? 'absent';
|
|
24
|
+
}
|
|
25
|
+
/** Every known scope with its status (operator view, UI notices). */
|
|
26
|
+
hosts() {
|
|
27
|
+
return [...this.scopes].map(([scope, entry]) => ({ scope, status: entry.state.status, detail: entry.degradedReason, code: entry.degradedCode }));
|
|
28
|
+
}
|
|
29
|
+
/** Why a degraded scope is unavailable, when known. */
|
|
30
|
+
degradedCode(scope) {
|
|
31
|
+
return this.scopes.get(scope)?.degradedCode ?? null;
|
|
32
|
+
}
|
|
33
|
+
/** Whether new turns in this scope should use Core sessions now. */
|
|
34
|
+
available(scope) {
|
|
35
|
+
return !this.stopped && (0, host_state_1.acceptsSessions)(this.scopes.get(scope)?.state ?? host_state_1.INITIAL_HOST_STATE);
|
|
36
|
+
}
|
|
37
|
+
/** A ready client for the scope, starting the host when needed. */
|
|
38
|
+
async acquire(scope) {
|
|
39
|
+
if (this.stopped)
|
|
40
|
+
throw new errors_1.SessionRequestError('Session hosts are shutting down', { code: 'busy', retryable: true });
|
|
41
|
+
const entry = this.entry(scope);
|
|
42
|
+
if (entry.client && !this.isClosed(entry.client))
|
|
43
|
+
return entry.client;
|
|
44
|
+
if (entry.state.status === 'degraded') {
|
|
45
|
+
throw new errors_1.SessionRequestError(`Core sessions are unavailable for ${scope}: ${entry.degradedReason ?? 'repeated host failures'}`, { code: 'host_degraded', retryable: false });
|
|
46
|
+
}
|
|
47
|
+
if (entry.starting)
|
|
48
|
+
return entry.starting;
|
|
49
|
+
if (entry.state.status === 'ready') {
|
|
50
|
+
// The client closed before its close event was processed: count it as a failure.
|
|
51
|
+
this.fail(scope, entry, 'Session host connection closed');
|
|
52
|
+
if (this.status(scope) === 'degraded')
|
|
53
|
+
return this.acquire(scope);
|
|
54
|
+
}
|
|
55
|
+
if (entry.state.status === 'restarting') {
|
|
56
|
+
// A caller needs the host now: start immediately instead of waiting for the backoff timer.
|
|
57
|
+
entry.restartTimer?.cancel();
|
|
58
|
+
entry.restartTimer = null;
|
|
59
|
+
entry.state = (0, host_state_1.onRestartDue)(entry.state).state;
|
|
60
|
+
}
|
|
61
|
+
else if (entry.state.status === 'absent') {
|
|
62
|
+
entry.state = (0, host_state_1.onDemand)(entry.state).state;
|
|
63
|
+
}
|
|
64
|
+
return this.start(scope, entry);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Follow a session: its events are applied to `sink` and reported to
|
|
68
|
+
* `listener`. Survives host restarts (resume + replay from the sink cursor).
|
|
69
|
+
*/
|
|
70
|
+
async track(scope, sessionId, sink, listener) {
|
|
71
|
+
const client = await this.acquire(scope);
|
|
72
|
+
const entry = this.entry(scope);
|
|
73
|
+
const tracked = entry.tracked.get(sessionId) ?? { sessionId, sink, listener, pump: null };
|
|
74
|
+
entry.tracked.set(sessionId, tracked);
|
|
75
|
+
tracked.pump?.detach();
|
|
76
|
+
tracked.pump = new session_event_pump_1.SessionEventPump(sessionId, client, sink, listener);
|
|
77
|
+
await tracked.pump.attach();
|
|
78
|
+
return tracked.pump;
|
|
79
|
+
}
|
|
80
|
+
untrack(scope, sessionId) {
|
|
81
|
+
const entry = this.scopes.get(scope);
|
|
82
|
+
const tracked = entry?.tracked.get(sessionId);
|
|
83
|
+
tracked?.pump?.detach();
|
|
84
|
+
entry?.tracked.delete(sessionId);
|
|
85
|
+
}
|
|
86
|
+
/** Manual retry of a degraded scope. */
|
|
87
|
+
async retry(scope) {
|
|
88
|
+
const entry = this.entry(scope);
|
|
89
|
+
const result = (0, host_state_1.onRetry)(entry.state);
|
|
90
|
+
if (result.decision.action !== 'start')
|
|
91
|
+
return;
|
|
92
|
+
entry.state = result.state;
|
|
93
|
+
entry.degradedReason = null;
|
|
94
|
+
entry.degradedCode = null;
|
|
95
|
+
await this.start(scope, entry);
|
|
96
|
+
}
|
|
97
|
+
/** Graceful stop of one scope (project removed). */
|
|
98
|
+
async stop(scope) {
|
|
99
|
+
const entry = this.scopes.get(scope);
|
|
100
|
+
if (!entry)
|
|
101
|
+
return;
|
|
102
|
+
entry.restartTimer?.cancel();
|
|
103
|
+
entry.state = (0, host_state_1.onStop)(entry.state);
|
|
104
|
+
for (const tracked of entry.tracked.values())
|
|
105
|
+
tracked.pump?.detach();
|
|
106
|
+
for (const unsubscribe of entry.unsubscribe.splice(0))
|
|
107
|
+
unsubscribe();
|
|
108
|
+
const client = entry.client ?? (entry.starting ? await entry.starting.catch(() => null) : null);
|
|
109
|
+
entry.client = null;
|
|
110
|
+
this.options.onStatus?.(scope, 'stopped');
|
|
111
|
+
if (client)
|
|
112
|
+
await client.close();
|
|
113
|
+
this.scopes.delete(scope);
|
|
114
|
+
}
|
|
115
|
+
/** App shutdown. */
|
|
116
|
+
async stopAll() {
|
|
117
|
+
this.stopped = true;
|
|
118
|
+
await Promise.allSettled([...this.scopes.keys()].map((scope) => this.stop(scope)));
|
|
119
|
+
}
|
|
120
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
121
|
+
entry(scope) {
|
|
122
|
+
let entry = this.scopes.get(scope);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
entry = { state: host_state_1.INITIAL_HOST_STATE, client: null, starting: null, restartTimer: null, tracked: new Map(), unsubscribe: [], degradedReason: null, degradedCode: null };
|
|
125
|
+
this.scopes.set(scope, entry);
|
|
126
|
+
}
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
isClosed(client) {
|
|
130
|
+
return client.closed === true;
|
|
131
|
+
}
|
|
132
|
+
start(scope, entry) {
|
|
133
|
+
this.options.onStatus?.(scope, 'starting');
|
|
134
|
+
entry.starting = (async () => {
|
|
135
|
+
try {
|
|
136
|
+
const client = await this.options.launcher.launch(scope);
|
|
137
|
+
if (entry.state.status === 'stopped') {
|
|
138
|
+
await client.close();
|
|
139
|
+
throw new errors_1.SessionRequestError('Scope stopped', { code: 'busy', retryable: false });
|
|
140
|
+
}
|
|
141
|
+
entry.client = client;
|
|
142
|
+
entry.state = (0, host_state_1.onReady)(entry.state);
|
|
143
|
+
this.wire(scope, entry, client);
|
|
144
|
+
this.options.onStatus?.(scope, 'ready');
|
|
145
|
+
await this.reattach(entry, client);
|
|
146
|
+
return client;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (entry.state.status !== 'stopped') {
|
|
150
|
+
if ((0, errors_1.isSessionRequestError)(error) && FATAL_CODES.has(error.code))
|
|
151
|
+
this.fatal(scope, entry, error.code, error.message);
|
|
152
|
+
else
|
|
153
|
+
this.fail(scope, entry, error.message);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
entry.starting = null;
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
return entry.starting;
|
|
162
|
+
}
|
|
163
|
+
wire(scope, entry, client) {
|
|
164
|
+
for (const unsubscribe of entry.unsubscribe.splice(0))
|
|
165
|
+
unsubscribe();
|
|
166
|
+
entry.unsubscribe.push(client.onEvent((envelope) => { void entry.tracked.get(envelope.sessionId)?.pump?.push(envelope).catch((error) => this.options.onStatus?.(scope, entry.state.status, `projection failed: ${error.message}`)); }), client.onLagged((sessionId) => { void entry.tracked.get(sessionId)?.pump?.catchUp().catch(() => undefined); }), client.onClose((reason, code) => {
|
|
167
|
+
if (entry.client !== client || entry.state.status === 'stopped')
|
|
168
|
+
return;
|
|
169
|
+
entry.client = null;
|
|
170
|
+
if (code && FATAL_CODES.has(code))
|
|
171
|
+
this.fatal(scope, entry, code, reason);
|
|
172
|
+
else
|
|
173
|
+
this.fail(scope, entry, reason);
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
/** After a (re)start: resume every tracked session and replay from its cursor. */
|
|
177
|
+
async reattach(entry, client) {
|
|
178
|
+
for (const tracked of entry.tracked.values()) {
|
|
179
|
+
tracked.pump?.detach();
|
|
180
|
+
try {
|
|
181
|
+
await client.request('session.open', { resume: { sessionId: tracked.sessionId } });
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// Closed sessions replay as history; a session Core never opened has nothing to resume.
|
|
185
|
+
if (!(error instanceof errors_1.SessionRequestError) || (error.code !== 'session_closed' && error.code !== 'session_not_found'))
|
|
186
|
+
throw error;
|
|
187
|
+
if (error.code === 'session_not_found')
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
tracked.pump = new session_event_pump_1.SessionEventPump(tracked.sessionId, client, tracked.sink, tracked.listener);
|
|
191
|
+
await tracked.pump.attach();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
fatal(scope, entry, code, reason) {
|
|
195
|
+
const result = (0, host_state_1.onFatal)(entry.state, reason);
|
|
196
|
+
entry.state = result.state;
|
|
197
|
+
if (result.decision.action !== 'degrade')
|
|
198
|
+
return;
|
|
199
|
+
entry.restartTimer?.cancel();
|
|
200
|
+
entry.restartTimer = null;
|
|
201
|
+
entry.degradedReason = reason;
|
|
202
|
+
entry.degradedCode = code;
|
|
203
|
+
this.options.onStatus?.(scope, 'degraded', reason, code);
|
|
204
|
+
}
|
|
205
|
+
fail(scope, entry, reason) {
|
|
206
|
+
const result = (0, host_state_1.onFailure)(entry.state, this.options.clock.now(), this.options.policy ?? host_state_1.DEFAULT_SUPERVISION);
|
|
207
|
+
entry.state = result.state;
|
|
208
|
+
if (result.decision.action === 'degrade') {
|
|
209
|
+
entry.degradedReason = `${result.decision.reason}; last: ${reason}`;
|
|
210
|
+
this.options.onStatus?.(scope, 'degraded', entry.degradedReason);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (result.decision.action === 'restart') {
|
|
214
|
+
this.options.onStatus?.(scope, 'restarting', reason);
|
|
215
|
+
entry.restartTimer?.cancel();
|
|
216
|
+
entry.restartTimer = this.options.clock.after(result.decision.delayMs, () => {
|
|
217
|
+
entry.restartTimer = null;
|
|
218
|
+
const due = (0, host_state_1.onRestartDue)(entry.state);
|
|
219
|
+
entry.state = due.state;
|
|
220
|
+
if (due.decision.action === 'start')
|
|
221
|
+
void this.start(scope, entry).catch(() => undefined);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.SessionHostRegistry = SessionHostRegistry;
|
|
@@ -47,6 +47,8 @@ function mapMessage(row) {
|
|
|
47
47
|
attachment_ids: ids,
|
|
48
48
|
context_refs: parseJsonArray(row.context_refs, isContextRef),
|
|
49
49
|
intents: parseIntents(row.intent),
|
|
50
|
+
...(row.core_turn_id ? { core_turn_id: row.core_turn_id } : {}),
|
|
51
|
+
...(row.turn_origin === 'user' || row.turn_origin === 'subagent' || row.turn_origin === 'system' ? { turn_origin: row.turn_origin } : {}),
|
|
50
52
|
created_at: row.created_at,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -128,7 +130,7 @@ function addAgentMessage(db, input) {
|
|
|
128
130
|
const id = (0, crypto_1.randomUUID)();
|
|
129
131
|
const attachmentIds = input.attachmentIds && input.attachmentIds.length ? JSON.stringify(input.attachmentIds) : null;
|
|
130
132
|
const contextRefs = input.contextRefs && input.contextRefs.length ? JSON.stringify(input.contextRefs) : null;
|
|
131
|
-
db.prepare('INSERT INTO agent_messages (id, conversation_id, role, content, attachment_ids, context_refs) VALUES (?, ?, ?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content, attachmentIds, contextRefs);
|
|
133
|
+
db.prepare('INSERT INTO agent_messages (id, conversation_id, role, content, attachment_ids, context_refs, turn_origin, core_turn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content, attachmentIds, contextRefs, input.turnOrigin ?? null, input.coreTurnId ?? null);
|
|
132
134
|
// Touch the parent so the list stays ordered by latest activity.
|
|
133
135
|
db.prepare("UPDATE agent_conversations SET updated_at = datetime('now') WHERE id = ?").run(input.conversationId);
|
|
134
136
|
return mapMessage(db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id));
|
|
@@ -4,9 +4,9 @@ exports.coreFactoryGraph = coreFactoryGraph;
|
|
|
4
4
|
const loop_implement_recipe_1 = require("./loop-implement-recipe");
|
|
5
5
|
const loop_agents_1 = require("./loop-agents");
|
|
6
6
|
/** App-owned definition graphs. Core supplies schemas, execution and canonical versions. */
|
|
7
|
-
function coreFactoryGraph(mode, independent = false, configurable = false) {
|
|
7
|
+
function coreFactoryGraph(mode, independent = false, configurable = false, capabilities) {
|
|
8
8
|
if (mode === 'implement' && configurable)
|
|
9
|
-
return (0, loop_implement_recipe_1.configurableImplementGraph)();
|
|
9
|
+
return (0, loop_implement_recipe_1.configurableImplementGraph)(capabilities);
|
|
10
10
|
const nodes = [{ id: 'start', type: 'start', position: { x: 0, y: 0 } }];
|
|
11
11
|
const edges = [];
|
|
12
12
|
const config = { maxIterations: 12, maxTransitions: 120, timeoutMinutes: 0, aiStepTimeoutMinutes: 0,
|
|
@@ -41,6 +41,7 @@ function compileLoopToDefinition(graph, launch) {
|
|
|
41
41
|
: {}),
|
|
42
42
|
...(typeof node.data?.exit === 'string' ? { exit: node.data.exit } : {}),
|
|
43
43
|
...(typeof node.data?.reason === 'string' ? { reason: node.data.reason } : {}),
|
|
44
|
+
...(typeof node.data?.blockerFrom === 'string' ? { blockerFrom: node.data.blockerFrom } : {}),
|
|
44
45
|
}
|
|
45
46
|
: node.data.params;
|
|
46
47
|
const promptRole = node.id === 'fix' ? 'fixer' : node.id === 'prepare' ? 'architect' : rawParams.access === 'read' ? 'reviewer' : 'developer';
|
|
@@ -94,7 +94,7 @@ const FACTORY_BY_ID = new Map([
|
|
|
94
94
|
function factoryLoopsForCapabilities(capabilities) {
|
|
95
95
|
if (capabilities?.engineV2 !== 1 || capabilities.workflowDefinitions !== 1)
|
|
96
96
|
return exports.FACTORY_LOOPS;
|
|
97
|
-
return exports.FACTORY_LOOPS.map(factory => ({ ...factory, graph: (0, loop_core_factory_1.coreFactoryGraph)(factory.id === 'factory:implement' ? 'implement' : factory.id === 'factory:freestyle' ? 'freestyle' : 'quick-sdd', capabilities.implementationSteps === 1, capabilities.workflowAgentSteps === 1) }));
|
|
97
|
+
return exports.FACTORY_LOOPS.map(factory => ({ ...factory, graph: (0, loop_core_factory_1.coreFactoryGraph)(factory.id === 'factory:implement' ? 'implement' : factory.id === 'factory:freestyle' ? 'freestyle' : 'quick-sdd', capabilities.implementationSteps === 1, capabilities.workflowAgentSteps === 1, capabilities) }));
|
|
98
98
|
}
|
|
99
99
|
function getFactoryLoop(id, capabilities) {
|
|
100
100
|
const legacy = FACTORY_BY_ID.get(id);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CORE_NODE_KINDS = void 0;
|
|
4
|
+
exports.effectivePieceOutcomes = effectivePieceOutcomes;
|
|
4
5
|
exports.isDefinitionReviewerPath = isDefinitionReviewerPath;
|
|
5
6
|
exports.isDefinitionGraph = isDefinitionGraph;
|
|
6
7
|
exports.assertDefinitionGraph = assertDefinitionGraph;
|
|
@@ -16,6 +17,12 @@ exports.interpolateSpec = interpolateSpec;
|
|
|
16
17
|
exports.CORE_NODE_KINDS = ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell',
|
|
17
18
|
'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
|
|
18
19
|
'component', 'implementation', 'implementation-step', 'end'];
|
|
20
|
+
/** Mirror of Core's `getOutcomes(params)`: `verify` advertises `blocked` only when the node opts into host blockers. */
|
|
21
|
+
function effectivePieceOutcomes(descriptor, params) {
|
|
22
|
+
if (descriptor.kind === 'verify' && params?.hostBlockers !== true)
|
|
23
|
+
return descriptor.outcomes.filter(outcome => outcome !== 'blocked');
|
|
24
|
+
return descriptor.outcomes;
|
|
25
|
+
}
|
|
19
26
|
function isDefinitionReviewerPath(graph, value) {
|
|
20
27
|
if (typeof value !== 'string' || !value || value.length > 2048)
|
|
21
28
|
return false;
|
|
@@ -201,7 +208,7 @@ function validateLoopGraph(graph, catalog) {
|
|
|
201
208
|
const labels = new Set();
|
|
202
209
|
for (const edge of out) {
|
|
203
210
|
if (typeof edge.label !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(edge.label) || labels.has(edge.label) ||
|
|
204
|
-
(descriptor && !descriptor.
|
|
211
|
+
(descriptor && !effectivePieceOutcomes(descriptor, node.data?.params).includes(edge.label))) {
|
|
205
212
|
errors.push({ code: 'INVALID_BRANCH', nodeId: node.id, edgeId: edge.id, message: 'Core outcome edges need unique labels advertised by the piece.' });
|
|
206
213
|
}
|
|
207
214
|
if (edge.label)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.supportsHostBlockers = exports.HOST_BLOCKER_KINDS = void 0;
|
|
3
4
|
exports.configurableImplementGraph = configurableImplementGraph;
|
|
4
5
|
const agent_runtime_settings_1 = require("../../agent-runtime/runtime/agent-runtime-settings");
|
|
5
6
|
const loop_agents_1 = require("./loop-agents");
|
|
@@ -9,8 +10,17 @@ const strings = { type: 'array', items: string, maxItems: 100 };
|
|
|
9
10
|
const score = { type: 'number', minimum: 0, maximum: 100 };
|
|
10
11
|
const aspects = { type_correctness: 60, pattern_adherence: 60, test_coverage: 60, security: 75, architectural_alignment: 60 };
|
|
11
12
|
const reviewPolicy = 'approved == true; issues must be empty; score >= 70; ' + Object.entries(aspects).map(([aspect, threshold]) => `${aspect} >= ${threshold}`).join('; ');
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
exports.HOST_BLOCKER_KINDS = ['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope'];
|
|
14
|
+
/** Fixer/developer structured `blocker`: the host ends the run on it instead of counting a correction round. */
|
|
15
|
+
const blocker = objectSchema({ kind: { enum: [...exports.HOST_BLOCKER_KINDS] }, command: string, cwd: string, evidence: string, requiredAction: string }, ['kind', 'requiredAction']);
|
|
16
|
+
/** Host-owned environment repair and structured blockers need the paired Core generation. */
|
|
17
|
+
const supportsHostBlockers = (capabilities) => capabilities?.hostBlockers === 1;
|
|
18
|
+
exports.supportsHostBlockers = supportsHostBlockers;
|
|
19
|
+
/** The recipe, roles, schemas and decisions are Desktop data, never Core phases.
|
|
20
|
+
* `capabilities` are the installed Core's advertised runtime capabilities: without `hostBlockers`
|
|
21
|
+
* the verify nodes compile exactly as before (no `setup`, no `blocked` edge, no `blockerFrom`). */
|
|
22
|
+
function configurableImplementGraph(capabilities) {
|
|
23
|
+
const hostBlockers = (0, exports.supportsHostBlockers)(capabilities);
|
|
14
24
|
const agents = (0, loop_agents_1.defaultLoopAgents)();
|
|
15
25
|
agents.roles = {
|
|
16
26
|
plan: { ...agents.agents.architect, access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'architect') },
|
|
@@ -24,7 +34,11 @@ function configurableImplementGraph() {
|
|
|
24
34
|
for (const [outcome, target] of Object.entries(outcomes))
|
|
25
35
|
edges.push({ id: `e-${id}-${outcome}`, source: id, target, label: outcome });
|
|
26
36
|
};
|
|
27
|
-
const turn = (id, label, roleId, prompt, schema, next) => node(id, label, 'role-turn', { roleId, prompt, structuredOutput: schema, sessionContinuity: 'none' }, { next, invalid: 'failed', failed: 'failed' });
|
|
37
|
+
const turn = (id, label, roleId, prompt, schema, next, extra = {}) => node(id, label, 'role-turn', { roleId, prompt, structuredOutput: schema, sessionContinuity: 'none', ...extra }, { next, invalid: 'failed', failed: 'failed' });
|
|
38
|
+
const verifyParams = { commands: 'configured', additionalCommandsFrom: 'architect', ...(hostBlockers ? { setup: 'configured', hostBlockers: true } : {}) };
|
|
39
|
+
const verifyEnds = (passed, failed, blockedEnd) => ({ pass: passed, fail: failed, failed: 'failed', ...(hostBlockers ? { blocked: blockedEnd } : {}) });
|
|
40
|
+
// Same Core generation as host blockers: older role-turn schemas reject unknown params.
|
|
41
|
+
const writes = hostBlockers ? { verificationProposalsFrom: 'architect' } : {};
|
|
28
42
|
const change = '{{run.changeId}}';
|
|
29
43
|
turn('architect', 'Architect', 'plan', `Plan the frozen ticket requirements for OpenSpec change ${change}. Use the bound official fast-forward skill. Author complete proposal, design, delta specs and actionable tasks. Propose existing repository verification commands for repositories with no configured checks. Return confidence, question (empty when none), and verification [{repositoryId,command,args,cwd?}]. When revisiting low confidence, investigate the missing repository evidence before asking again. Context and human answers: {{history.text}}`, objectSchema({ confidence: { enum: ['high', 'medium', 'low'] }, question: string, verification: { type: 'array', maxItems: 20, items: objectSchema({ repositoryId: string, command: string, args: strings, cwd: string }, ['repositoryId', 'command', 'args']) } }), 'confidence');
|
|
30
44
|
node('confidence', 'Planning confidence', 'condition', { expr: '$outputs.architect.structured.confidence != "low"' }, { true: 'validate', false: 'investigate' });
|
|
@@ -34,11 +48,22 @@ function configurableImplementGraph() {
|
|
|
34
48
|
node('reset-confidence', 'Reset confidence after answer', 'assign', { set: { planInvestigated: false } }, { next: 'architect', failed: 'failed' });
|
|
35
49
|
node('validate', 'Validate OpenSpec', 'openspec-validate', { change }, { pass: 'freeze', fail: 'architect', failed: 'failed' });
|
|
36
50
|
node('freeze', 'Freeze approved artifacts', 'artifact-contract', { change, contractId: 'plan', action: 'freeze' }, { pass: 'initialize-corrections', fail: 'architect', failed: 'failed' });
|
|
37
|
-
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3 } }, { next: 'developer', failed: 'failed' });
|
|
38
|
-
const implementation = objectSchema({ summary: string, incomplete: strings });
|
|
39
|
-
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. Prior execution evidence and human answers: {{history.text}}`, implementation, '
|
|
40
|
-
node('
|
|
41
|
-
|
|
51
|
+
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3 } }, { next: 'developer', failed: 'failed' });
|
|
52
|
+
const implementation = objectSchema({ summary: string, incomplete: strings, blocker }, ['summary', 'incomplete']);
|
|
53
|
+
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
|
+
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' });
|
|
55
|
+
// Credentials, network and variables are the person's to provide. Any other
|
|
56
|
+
// blocker is confirmed by the host's own verification: a host precondition
|
|
57
|
+
// ends at host-blocked, a reproduced failure becomes a correction, and
|
|
58
|
+
// passing checks reach the reviewer, whose rejection of unfinished tasks is
|
|
59
|
+
// corrected within the same budget.
|
|
60
|
+
node('developer-blocker-kind', 'Blocker needs the person', 'condition', { expr: ['credential', 'network', 'environment-variable'].map(kind => `$outputs.developer.structured.blocker.kind == "${kind}"`).join(' || ') }, { true: 'developer-blocked', false: 'verify' });
|
|
61
|
+
node('tasks', 'Check artifacts and completed tasks', 'artifact-contract', { change, contractId: 'plan', action: 'check', requireCompletedTasks: true }, { pass: 'verify', fail: 'implementation-budget', failed: 'failed' });
|
|
62
|
+
// Incomplete tasks send the developer back, but only while each attempt changes the candidate and within budget.
|
|
63
|
+
node('implementation-budget', 'Check implementation attempt budget', 'condition', { expr: '$vars.implementationAttempts < $vars.implementationLimit' }, { true: 'implementation-progress', false: 'implementation-exhausted' });
|
|
64
|
+
node('implementation-progress', 'Check implementation progress', 'condition', { expr: '!exists($vars.lastImplementationHash) || $outputs.developer.candidateHash != $vars.lastImplementationHash' }, { true: 'begin-implementation', false: 'implementation-stalled' });
|
|
65
|
+
node('begin-implementation', 'Count implementation attempt', 'assign', { increment: { implementationAttempts: 1 }, set: { lastImplementationHash: '{{outputs.developer.candidateHash}}' } }, { next: 'developer', failed: 'failed' });
|
|
66
|
+
node('verify', 'Verify candidate', 'verify', verifyParams, verifyEnds('reviewer', 'correction-context', 'host-blocked'));
|
|
42
67
|
turn('reviewer', 'Reviewer', 'assess', `Review change ${change} read-only against all approved artifacts and frozen ticket acceptance criteria. Inspect real code and the host verification evidence: {{outputs.verify}}. Return approved, summary, issues, score (0-100), and aspects. Acceptance policy: ${reviewPolicy}. For any rejection or below-threshold score, explain the concrete unmet requirement and actionable repair in summary and issues; passing commands do not prove every acceptance obligation. Prior execution: {{history.text}}`, objectSchema({ approved: { type: 'boolean' }, summary: string, issues: strings, score, aspects: objectSchema(Object.fromEntries(Object.keys(aspects).map(aspect => [aspect, score]))) }), 'review-policy');
|
|
43
68
|
node('review-policy', 'Review acceptance policy', 'condition', { expr: ['$outputs.reviewer.structured.approved == true', '$outputs.reviewer.structured.issues.length == 0', '$outputs.reviewer.structured.score >= 70', ...Object.entries(aspects).map(([aspect, threshold]) => `$outputs.reviewer.structured.aspects.${aspect} >= ${threshold}`)].join(' && ') }, { true: 'approve', false: 'correction-context' });
|
|
44
69
|
node('correction-context', 'Select current review findings', 'condition', { expr: '$outputs.verify.valid == true && exists($outputs.reviewer.candidateHash) && $outputs.reviewer.candidateHash == $outputs.verify.candidateHash' }, { true: 'capture-review', false: 'clear-review' });
|
|
@@ -46,13 +71,18 @@ function configurableImplementGraph() {
|
|
|
46
71
|
node('clear-review', 'Clear outdated review findings', 'assign', { set: { latestReview: 'No reviewer findings for the current verified candidate. Diagnose the current host verification failure first.' } }, { next: 'correction-budget', failed: 'failed' });
|
|
47
72
|
node('correction-budget', 'Check automatic correction budget', 'condition', { expr: '$vars.correctionAttempts < $vars.correctionLimit' }, { true: 'begin-correction', false: 'correction-exhausted' });
|
|
48
73
|
node('begin-correction', 'Count automatic correction attempt', 'assign', { increment: { correctionAttempts: 1 } }, { next: 'fixer', failed: 'failed' });
|
|
49
|
-
turn('fixer', 'Fixer', 'correct', `Correct the reported implementation, test or review failure for change ${change} using the bound apply skill and preserving approved artifacts. Read failureSummary, command, cwd, exitCode and evidenceId first; use read_verification_evidence for truncated evidence. If Host verification is valid, repair the concrete rejected review obligation or below-threshold aspect instead of only rerunning green commands. Acceptance policy: ${reviewPolicy}. Reproduce the exact focused failure, inspect the assertion and tested code, then apply the smallest repair that preserves the required behavior. An unchanged filename does not prove an unrelated failure. A mandatory test inside the admitted workspace may receive a proven formatting/compatibility repair while retaining every required guard. Report diagnosis, focused commands with original exit codes, repair and unresolved failures in summary and incomplete. If the cause
|
|
74
|
+
turn('fixer', 'Fixer', 'correct', `Correct the reported implementation, test or review failure for change ${change} using the bound apply skill and preserving approved artifacts. Read failureSummary, command, cwd, exitCode and evidenceId first; use read_verification_evidence for truncated evidence. If Host verification is valid, repair the concrete rejected review obligation or below-threshold aspect instead of only rerunning green commands. Acceptance policy: ${reviewPolicy}. Reproduce the exact focused failure, inspect the assertion and tested code, then apply the smallest repair that preserves the required behavior. An unchanged filename does not prove an unrelated failure. A mandatory test inside the admitted workspace may receive a proven formatting/compatibility repair while retaining every required guard. Report diagnosis, focused commands with original exit codes, repair and unresolved failures in summary and incomplete. If the cause is a host precondition or environment blocker outside this change, make no edits and return the structured blocker (kind, command, cwd, evidence, requiredAction): the host ends the run with your required action. Any other unchanged correction stops this run even when host checks pass. Do not make unrelated edits merely to change the candidate hash. Host verification: {{outputs.verify}}. Latest review findings for this candidate: {{run.latestReview}}. Prior execution: {{history.text}}`, implementation, 'correction-blocker', writes);
|
|
75
|
+
node('correction-blocker', 'Fixer reported a host blocker', 'condition', { expr: 'exists($outputs.fixer.structured.blocker) && $outputs.fixer.structured.blocker != null' }, { true: 'fixer-blocked', false: 'correction-progress' });
|
|
50
76
|
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' });
|
|
51
77
|
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' });
|
|
52
78
|
node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
|
|
53
79
|
node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'verify' });
|
|
54
|
-
node('verify-archive', 'Verify archived candidate', 'verify',
|
|
55
|
-
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}}` } });
|
|
80
|
+
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' } : {}) } });
|
|
82
|
+
if (hostBlockers) {
|
|
83
|
+
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
|
+
nodes.push(hostBlocked('host-blocked', 'verify'), hostBlocked('host-blocked-archive', 'verify-archive'));
|
|
85
|
+
}
|
|
56
86
|
edges.unshift({ id: 'e-start', source: 'start', target: 'architect' });
|
|
57
87
|
return { nodes, edges, config: { maxIterations: 12, maxTransitions: 200, timeoutMinutes: 0, aiStepTimeoutMinutes: 0, change: 'new', journal: 'ledger-only', ticketScope: 'all', agents, reviewerStepId: 'reviewer' } };
|
|
58
88
|
}
|
|
@@ -756,6 +756,10 @@ class LoopRunManager {
|
|
|
756
756
|
throw new Error('runtime_run_completed: Fork a completed workflow to continue');
|
|
757
757
|
this._cancelled.delete(runId);
|
|
758
758
|
this.claimDefinition({ ...frozen.request, runId });
|
|
759
|
+
// Cards and chips that showed the restart/interruption pause follow the resumed run live.
|
|
760
|
+
const request = frozen.request;
|
|
761
|
+
this._emit({ type: 'loop.run_resumed', projectId: request.projectId, loopRunId: runId, railIndex: request.railIndex ?? null,
|
|
762
|
+
ticketIds: request.spec?.ticketIds ?? (request.ticketId == null ? [] : [request.ticketId]) });
|
|
759
763
|
const task = this._run(frozen.request, { resume: true, ...input }).finally(() => { this.releaseDefinition(runId); this._definitionTasks.delete(runId); });
|
|
760
764
|
this._definitionTasks.set(runId, task);
|
|
761
765
|
return task;
|
|
@@ -341,10 +341,10 @@ const CORE_STARTER_SHAPES = {
|
|
|
341
341
|
};
|
|
342
342
|
/** Core definition graph for one of the eight starters. Deterministic: same id ⇒
|
|
343
343
|
* structurally identical graph (positions included). */
|
|
344
|
-
function coreStarterGraph(id, independent = false, configurable = false) {
|
|
344
|
+
function coreStarterGraph(id, independent = false, configurable = false, capabilities) {
|
|
345
345
|
const shape = CORE_STARTER_SHAPES[id];
|
|
346
346
|
if (shape === 'ship' && configurable)
|
|
347
|
-
return (0, loop_core_factory_1.coreFactoryGraph)('implement', true, true);
|
|
347
|
+
return (0, loop_core_factory_1.coreFactoryGraph)('implement', true, true, capabilities);
|
|
348
348
|
const { prompt, goal, maxIterations } = STARTER_TEXT[id];
|
|
349
349
|
const nodes = [{ id: 'start', type: 'start', position: { x: COL_X, y: 0 } }];
|
|
350
350
|
const edges = [];
|
|
@@ -438,7 +438,7 @@ function loopTemplatesForCapabilities(capabilities) {
|
|
|
438
438
|
...template,
|
|
439
439
|
description: CORE_STARTER_DESCRIPTIONS[template.id],
|
|
440
440
|
...(readOnly ? { readOnly: true } : {}),
|
|
441
|
-
graph: coreStarterGraph(template.id, capabilities?.implementationSteps === 1, capabilities?.workflowAgentSteps === 1),
|
|
441
|
+
graph: coreStarterGraph(template.id, capabilities?.implementationSteps === 1, capabilities?.workflowAgentSteps === 1, capabilities),
|
|
442
442
|
};
|
|
443
443
|
});
|
|
444
444
|
}
|