gestalt-mobile 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/client/assets/index-5-CmBBPG.css +1 -0
- package/dist/client/assets/index-BLgr7z1T.js +107 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +2 -3
- package/dist/server/server/composition.contracts.js +27 -0
- package/dist/server/server/composition.js +10 -2
- package/dist/server/server/features/agent-activity/registry.js +4 -0
- package/dist/server/server/features/autopilot/application/policy.js +42 -1
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +11 -0
- package/dist/server/server/features/catalog/register-routes.js +13 -0
- package/dist/server/server/features/catalog/session-defaults/endpoint.js +16 -0
- package/dist/server/server/features/files/check-references/endpoint.js +36 -0
- package/dist/server/server/features/files/read-entry/endpoint.js +28 -0
- package/dist/server/server/features/files/register-routes.js +4 -0
- package/dist/server/server/features/plans/application/parse-supervised-plan.js +11 -0
- package/dist/server/server/features/plans/archive-workspace-plan/endpoint.js +26 -0
- package/dist/server/server/features/plans/list-workspace-plans/endpoint.js +1 -1
- package/dist/server/server/features/plans/register-routes.js +6 -0
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/start-session/endpoint.js +6 -0
- package/dist/server/server/features/sessions/start-session/request.js +4 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +12 -0
- package/dist/server/server/platform/catalog/filesystem-workspace-catalog.js +3 -0
- package/dist/server/server/platform/codex/recent-thread-lister.js +2 -1
- package/dist/server/server/platform/codex/session-model-config.js +104 -0
- package/dist/server/server/platform/codex/session-runtime.js +33 -13
- package/dist/server/server/platform/filesystem/filesystem-workspace-files.js +99 -0
- package/dist/server/server/platform/persistence/filesystem-session-defaults.js +42 -0
- package/dist/server/server/platform/persistence/migrate.js +2 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +7 -2
- package/dist/server/server/platform/plans/filesystem-plan-archiver.js +109 -0
- package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +96 -68
- package/dist/server/server/platform/plans/org-plan-archive-tag.js +17 -0
- package/dist/server/server/platform/plans/org-plan-discovery.js +53 -0
- package/dist/server/shared/contracts/session-defaults.js +20 -0
- package/dist/server/shared/contracts/session-model-settings.js +6 -0
- package/package.json +3 -1
- package/dist/client/assets/index-BRly-q56.js +0 -38
- package/dist/client/assets/index-D57mr3B1.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -16,8 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
16
16
|
<link rel="icon" href="/icons/gestalt-mobile-192.png" />
|
|
17
17
|
<link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
|
|
18
18
|
<title>Gestalt Mobile</title>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BLgr7z1T.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-5-CmBBPG.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="app"></div>
|
|
@@ -7,7 +7,7 @@ import fastifyStatic from '@fastify/static';
|
|
|
7
7
|
import fastifyCookie from '@fastify/cookie';
|
|
8
8
|
import fastify, {} from 'fastify';
|
|
9
9
|
import { registerGetHealth } from './features/health/get-health/endpoint.js';
|
|
10
|
-
import {
|
|
10
|
+
import { registerCatalogRoutes } from './features/catalog/register-routes.js';
|
|
11
11
|
import { registerAuthRoutes } from './features/auth/register-routes.js';
|
|
12
12
|
import { registerGitRoutes } from './features/git/register-routes.js';
|
|
13
13
|
import { registerPlanRoutes } from './features/plans/register-routes.js';
|
|
@@ -45,8 +45,7 @@ export async function buildApp(deps) {
|
|
|
45
45
|
});
|
|
46
46
|
registerGetHealth(app, deps.health);
|
|
47
47
|
registerAuthRoutes(app, deps);
|
|
48
|
-
|
|
49
|
-
registerGetBootstrap(app, deps.bootstrap);
|
|
48
|
+
registerCatalogRoutes(app, { bootstrap: deps.bootstrap, sessionDefaults: deps.sessionDefaults });
|
|
50
49
|
registerSessionRoutes(app, deps);
|
|
51
50
|
if (deps.autopilot)
|
|
52
51
|
registerAutopilotRoutes(app, deps.autopilot, deps.sessionRoutes?.idempotency, deps.sessionRoutes ? (id) => deps.sessionRoutes?.find(id)?.provider : undefined);
|
|
@@ -2886,6 +2886,9 @@ describe('production composition', () => {
|
|
|
2886
2886
|
['DELETE', '/api/auth/devices/:deviceId', '/api/auth/devices/device-1', 'protected'],
|
|
2887
2887
|
['GET', '/api/bootstrap', '/api/bootstrap', 'protected'],
|
|
2888
2888
|
['HEAD', '/api/bootstrap', '/api/bootstrap', 'protected'],
|
|
2889
|
+
['GET', '/api/session-defaults', '/api/session-defaults', 'protected'],
|
|
2890
|
+
['HEAD', '/api/session-defaults', '/api/session-defaults', 'protected'],
|
|
2891
|
+
['PUT', '/api/session-defaults', '/api/session-defaults', 'protected'],
|
|
2889
2892
|
['POST', '/api/session-models/:provider', '/api/session-models/codex', 'protected'],
|
|
2890
2893
|
['POST', '/api/sessions', '/api/sessions', 'protected'],
|
|
2891
2894
|
['GET', '/api/sessions', '/api/sessions', 'protected'],
|
|
@@ -2914,6 +2917,24 @@ describe('production composition', () => {
|
|
|
2914
2917
|
['GET', '/api/sessions/:id/plan', '/api/sessions/session-1/plan', 'protected'],
|
|
2915
2918
|
['HEAD', '/api/sessions/:id/plan', '/api/sessions/session-1/plan', 'protected'],
|
|
2916
2919
|
['DELETE', '/api/sessions/:id/plan', '/api/sessions/session-1/plan', 'protected'],
|
|
2920
|
+
[
|
|
2921
|
+
'POST',
|
|
2922
|
+
'/api/workspaces/:workspaceId/files/references',
|
|
2923
|
+
'/api/workspaces/workspace-1/files/references',
|
|
2924
|
+
'protected',
|
|
2925
|
+
],
|
|
2926
|
+
[
|
|
2927
|
+
'GET',
|
|
2928
|
+
'/api/workspaces/:workspaceId/files/preview',
|
|
2929
|
+
'/api/workspaces/workspace-1/files/preview?path=README.md',
|
|
2930
|
+
'protected',
|
|
2931
|
+
],
|
|
2932
|
+
[
|
|
2933
|
+
'HEAD',
|
|
2934
|
+
'/api/workspaces/:workspaceId/files/preview',
|
|
2935
|
+
'/api/workspaces/workspace-1/files/preview?path=README.md',
|
|
2936
|
+
'protected',
|
|
2937
|
+
],
|
|
2917
2938
|
[
|
|
2918
2939
|
'GET',
|
|
2919
2940
|
'/api/workspaces/:workspaceId/files',
|
|
@@ -2974,6 +2995,12 @@ describe('production composition', () => {
|
|
|
2974
2995
|
'/api/workspaces/workspace-1/plans/plan.org',
|
|
2975
2996
|
'protected',
|
|
2976
2997
|
],
|
|
2998
|
+
[
|
|
2999
|
+
'POST',
|
|
3000
|
+
'/api/workspaces/:workspaceId/plans/:planName/archive',
|
|
3001
|
+
'/api/workspaces/workspace-1/plans/.gestalt%2Fplan.org/archive',
|
|
3002
|
+
'protected',
|
|
3003
|
+
],
|
|
2977
3004
|
[
|
|
2978
3005
|
'GET',
|
|
2979
3006
|
'/api/git/repositories/:workspaceId',
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
+
import { FilesystemSessionDefaults } from './platform/persistence/filesystem-session-defaults.js';
|
|
10
|
+
import { SessionModelConfig } from './platform/codex/session-model-config.js';
|
|
9
11
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
10
12
|
import { debuglog } from 'node:util';
|
|
11
13
|
import { buildApp } from './app.js';
|
|
@@ -59,6 +61,7 @@ import { compileSkillOverride } from './features/skills/model/skill-profile.js';
|
|
|
59
61
|
import { SupervisedPlanRegistry } from './features/plans/application/supervised-plan-registry.js';
|
|
60
62
|
import { FilesystemPlanStatusSource } from './platform/plans/filesystem-plan-status-source.js';
|
|
61
63
|
import { FilesystemWorkspacePlanCatalog } from './platform/plans/filesystem-workspace-plan-catalog.js';
|
|
64
|
+
import { FilesystemPlanArchiver } from './platform/plans/filesystem-plan-archiver.js';
|
|
62
65
|
import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-command.js';
|
|
63
66
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
64
67
|
import { SqliteAutopilotStore } from './platform/persistence/sqlite-autopilot-store.js';
|
|
@@ -389,6 +392,8 @@ export async function composeRelayApp(options) {
|
|
|
389
392
|
list: (provider) => (provider === 'kimi' ? kimiModels.listForSession() : models.list(provider)),
|
|
390
393
|
};
|
|
391
394
|
const skillProfiles = new FilesystemSkillProfileStore(options.homeDirectory ?? homedir());
|
|
395
|
+
const sessionDefaults = new FilesystemSessionDefaults(options.homeDirectory ?? homedir());
|
|
396
|
+
const sessionModelConfig = new SessionModelConfig(process.env.CODEX_HOME ?? join(options.homeDirectory ?? homedir(), '.codex-gestalt'), dirname(databasePath));
|
|
392
397
|
// kimi discovery is workspace-scoped, not profile-scoped: the profile only
|
|
393
398
|
// decides which discovered skills a session's server materializes.
|
|
394
399
|
const kimiSkillCatalog = new KimiSkillCatalog(kimiManager ?? null, kimiManager != null);
|
|
@@ -397,6 +402,7 @@ export async function composeRelayApp(options) {
|
|
|
397
402
|
: new CodexSkillCatalog(profile, options.launchAppServer ?? launchCodexAppServer);
|
|
398
403
|
const editorSkillCatalog = new CachedSkillCatalog((provider, profile, workspace) => skillCatalog(provider, profile).list(workspace));
|
|
399
404
|
const workspacePlanCatalog = new FilesystemWorkspacePlanCatalog();
|
|
405
|
+
const planArchiver = new FilesystemPlanArchiver();
|
|
400
406
|
const resolveSkills = async (session) => {
|
|
401
407
|
const catalog = session.provider === 'kimi'
|
|
402
408
|
? await kimiSkillCatalog.list(session.workspacePath)
|
|
@@ -907,7 +913,7 @@ export async function composeRelayApp(options) {
|
|
|
907
913
|
failPendingCheckpointHandoffs(sessionId, false);
|
|
908
914
|
dismissPendingInteractions(sessionId, new Date().toISOString(), 'failed');
|
|
909
915
|
recoverExitedSession(sessionId);
|
|
910
|
-
}, resolveSkills, planStatusSource, acceptPlanUpdate, options.planMeasurementBaseUrl, 30_000, 64, root)
|
|
916
|
+
}, resolveSkills, planStatusSource, acceptPlanUpdate, options.planMeasurementBaseUrl, 30_000, 64, root, (session) => sessionModelConfig.resolve(session))
|
|
911
917
|
: null;
|
|
912
918
|
const handleKimiNotification = (sessionId, event) => {
|
|
913
919
|
const occurredAt = new Date().toISOString();
|
|
@@ -1155,7 +1161,9 @@ export async function composeRelayApp(options) {
|
|
|
1155
1161
|
staticDir: options.staticDir,
|
|
1156
1162
|
pwaIcon: options.pwaIcon,
|
|
1157
1163
|
recentThreads: recentThreadsForRelay,
|
|
1164
|
+
sessionDefaults,
|
|
1158
1165
|
bootstrap: {
|
|
1166
|
+
sessionDefaults,
|
|
1159
1167
|
workspaces,
|
|
1160
1168
|
profiles: options.profiles,
|
|
1161
1169
|
models,
|
|
@@ -1390,7 +1398,7 @@ export async function composeRelayApp(options) {
|
|
|
1390
1398
|
events.publish(journal.append(id, 'plan.closed', {}, occurredAt));
|
|
1391
1399
|
},
|
|
1392
1400
|
},
|
|
1393
|
-
workspacePlanRoutes: { workspaces, plans: workspacePlanCatalog },
|
|
1401
|
+
workspacePlanRoutes: { workspaces, plans: workspacePlanCatalog, archiver: planArchiver },
|
|
1394
1402
|
workspaceFileRoutes: { workspaces, files: workspaceFiles },
|
|
1395
1403
|
autopilot,
|
|
1396
1404
|
...(runtime
|
|
@@ -102,6 +102,10 @@ export class AgentActivityRegistry {
|
|
|
102
102
|
kind: 'collaboration',
|
|
103
103
|
childId: child.id,
|
|
104
104
|
childStatus: 'notLoaded',
|
|
105
|
+
// The bounded roster/process read is authoritative as a unit. If a
|
|
106
|
+
// child disappeared, its previous runtime-local process projection
|
|
107
|
+
// cannot remain eligible as live work after this reconciliation.
|
|
108
|
+
childOwnedProcesses: [],
|
|
105
109
|
});
|
|
106
110
|
// A malformed/unknown `thread/list` row is not proof that a child is
|
|
107
111
|
// healthy. Preserve it as disconnected but keep the aggregate qualified
|
|
@@ -49,6 +49,47 @@ export function classifyAgentActivity(activity) {
|
|
|
49
49
|
const subagentsSettled = activity.aggregateSubagents === 'idle' || activity.aggregateSubagents === 'blocked';
|
|
50
50
|
return rootSettled && subagentsSettled ? 'settled' : 'observe';
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Classifies actor state against the plan position that can legally run next.
|
|
54
|
+
* Completed canonical executors remain visible evidence, but their terminal
|
|
55
|
+
* topology cannot indefinitely turn a fresh read into a reconciliation loop.
|
|
56
|
+
*/
|
|
57
|
+
function classifySupervisedActivity(activity, plan) {
|
|
58
|
+
const selected = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
|
|
59
|
+
const index = selected >= 0 ? selected : plan.steps.findIndex((step) => step.state !== 'DONE');
|
|
60
|
+
if (index < 0)
|
|
61
|
+
return classifyAgentActivity(activity);
|
|
62
|
+
const position = index + 1;
|
|
63
|
+
const retained = activity.subagents.filter((child) => {
|
|
64
|
+
const match = /^L([1-9]\d*)$/.exec(child.canonicalPosition ?? '');
|
|
65
|
+
if (!match)
|
|
66
|
+
return true;
|
|
67
|
+
const historical = Number(match[1]) < position;
|
|
68
|
+
const supervisedProcess = child.ownedProcesses?.some((process) => process.ownership === 'supervisor' &&
|
|
69
|
+
(process.state === 'running' || process.state === 'detached-active'));
|
|
70
|
+
return !historical || supervisedProcess;
|
|
71
|
+
});
|
|
72
|
+
// An aggregate-only observation cannot prove which child is historical.
|
|
73
|
+
if (retained.length === activity.subagents.length)
|
|
74
|
+
return classifyAgentActivity(activity);
|
|
75
|
+
if (activity.root.state === 'awaitingHuman' ||
|
|
76
|
+
retained.some((child) => child.state === 'awaitingHuman'))
|
|
77
|
+
return 'attention';
|
|
78
|
+
if (activity.root.state === 'working' ||
|
|
79
|
+
retained.some((child) => child.state === 'working' ||
|
|
80
|
+
child.state === 'awaitingAgent' ||
|
|
81
|
+
child.ownedProcesses?.some((process) => (child.state !== 'disconnected' || process.ownership === 'supervisor') &&
|
|
82
|
+
(process.state === 'running' || process.state === 'detached-active'))))
|
|
83
|
+
return 'active';
|
|
84
|
+
if (activity.root.state === 'disconnected' ||
|
|
85
|
+
retained.some((child) => child.state === 'disconnected'))
|
|
86
|
+
return 'reconcile';
|
|
87
|
+
const rootSettled = activity.root.state === 'idle' ||
|
|
88
|
+
activity.root.state === 'blocked' ||
|
|
89
|
+
activity.root.state === 'awaitingAgent';
|
|
90
|
+
const childrenSettled = retained.every((child) => child.state === 'idle' || child.state === 'blocked');
|
|
91
|
+
return rootSettled && childrenSettled ? 'settled' : 'observe';
|
|
92
|
+
}
|
|
52
93
|
/** A deliberately pure, exhaustive safety gate. Adapters may only enact this result. */
|
|
53
94
|
export function decideAutopilot(input) {
|
|
54
95
|
const { state, plan, activity, hasPendingInteraction, now, policy } = input;
|
|
@@ -93,7 +134,7 @@ export function decideAutopilot(input) {
|
|
|
93
134
|
return { kind: 'reconcile' };
|
|
94
135
|
if (Date.parse(now) - Date.parse(activity.root.observedAt) > policy.staleAfterMs)
|
|
95
136
|
return { kind: 'reconcile' };
|
|
96
|
-
const disposition =
|
|
137
|
+
const disposition = classifySupervisedActivity(activity, plan);
|
|
97
138
|
if (disposition === 'attention')
|
|
98
139
|
return { kind: 'observe' };
|
|
99
140
|
if (disposition === 'reconcile')
|
|
@@ -10,7 +10,18 @@ export async function getBootstrap(deps) {
|
|
|
10
10
|
deps.models?.list('codex').catch(() => []) ?? [],
|
|
11
11
|
deps.models?.list('kimi').catch(() => []) ?? [],
|
|
12
12
|
]);
|
|
13
|
+
let sessionDefaults = null;
|
|
14
|
+
let sessionDefaultsError = false;
|
|
15
|
+
if (deps.sessionDefaults) {
|
|
16
|
+
try {
|
|
17
|
+
sessionDefaults = await deps.sessionDefaults.read();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
sessionDefaultsError = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
return {
|
|
24
|
+
...(deps.sessionDefaults ? { sessionDefaults, sessionDefaultsError } : {}),
|
|
14
25
|
workspaces,
|
|
15
26
|
profiles,
|
|
16
27
|
models: { codex: codexModels, kimi: kimiModels },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { registerGetBootstrap } from './get-bootstrap/endpoint.js';
|
|
7
|
+
import { registerSessionDefaults } from './session-defaults/endpoint.js';
|
|
8
|
+
export function registerCatalogRoutes(app, deps) {
|
|
9
|
+
if (deps.bootstrap)
|
|
10
|
+
registerGetBootstrap(app, deps.bootstrap);
|
|
11
|
+
if (deps.sessionDefaults)
|
|
12
|
+
registerSessionDefaults(app, deps.sessionDefaults);
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { sessionDefaultsSchema, } from '../../../../shared/contracts/session-defaults.js';
|
|
7
|
+
export function registerSessionDefaults(app, store) {
|
|
8
|
+
app.get('/api/session-defaults', async () => ({ defaults: await store.read() }));
|
|
9
|
+
app.put('/api/session-defaults', async (request, reply) => {
|
|
10
|
+
const parsed = sessionDefaultsSchema.safeParse(request.body);
|
|
11
|
+
if (!parsed.success)
|
|
12
|
+
return reply.code(400).send({ error: 'INVALID_SESSION_DEFAULTS' });
|
|
13
|
+
await store.save(parsed.data);
|
|
14
|
+
return { defaults: parsed.data };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerCheckReferences(app, deps) {
|
|
7
|
+
if (!deps.files.exists)
|
|
8
|
+
return;
|
|
9
|
+
app.post('/api/workspaces/:workspaceId/files/references', async (request, reply) => {
|
|
10
|
+
const body = request.body;
|
|
11
|
+
const paths = body?.paths;
|
|
12
|
+
if (!Array.isArray(paths) ||
|
|
13
|
+
paths.length > 64 ||
|
|
14
|
+
!paths.every((path) => typeof path === 'string' && path.length <= 4096 && !path.includes('\0')))
|
|
15
|
+
return reply.code(400).send({ code: 'INVALID_FILE_REFERENCES' });
|
|
16
|
+
try {
|
|
17
|
+
const workspace = await deps.workspaces.resolve(request.params.workspaceId);
|
|
18
|
+
const unique = [...new Set(paths)];
|
|
19
|
+
const existing = [];
|
|
20
|
+
for (let index = 0; index < unique.length; index += 8) {
|
|
21
|
+
const group = unique.slice(index, index + 8);
|
|
22
|
+
const results = await Promise.all(group.map((path) => deps.files.exists(workspace.realPath, path)));
|
|
23
|
+
group.forEach((path, offset) => {
|
|
24
|
+
if (results[offset])
|
|
25
|
+
existing.push(path);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return reply.send({ paths: existing });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND')
|
|
32
|
+
return reply.code(404).send({ code: 'WORKSPACE_NOT_FOUND' });
|
|
33
|
+
return reply.code(503).send({ code: 'WORKSPACE_FILES_UNAVAILABLE' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function registerReadEntry(app, deps) {
|
|
2
|
+
if (!deps.files.read)
|
|
3
|
+
return;
|
|
4
|
+
app.get('/api/workspaces/:workspaceId/files/preview', async (request, reply) => {
|
|
5
|
+
const { path } = request.query;
|
|
6
|
+
if (typeof path !== 'string' || path.includes('\0') || path.length > 4096)
|
|
7
|
+
return reply.code(400).send({ code: 'INVALID_FILE_PREVIEW' });
|
|
8
|
+
try {
|
|
9
|
+
const workspace = await deps.workspaces.resolve(request.params.workspaceId);
|
|
10
|
+
const result = await deps.files.read(workspace.realPath, path);
|
|
11
|
+
if (result.kind === 'available')
|
|
12
|
+
return reply.send(result.preview);
|
|
13
|
+
const errors = {
|
|
14
|
+
missing: [404, 'FILE_PREVIEW_NOT_FOUND'],
|
|
15
|
+
unreadable: [403, 'FILE_PREVIEW_UNREADABLE'],
|
|
16
|
+
unsupported: [415, 'FILE_PREVIEW_UNSUPPORTED'],
|
|
17
|
+
'too-large': [413, 'FILE_PREVIEW_TOO_LARGE'],
|
|
18
|
+
};
|
|
19
|
+
const [status, code] = errors[result.kind];
|
|
20
|
+
return reply.code(status).send({ code });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND')
|
|
24
|
+
return reply.code(404).send({ code: 'WORKSPACE_NOT_FOUND' });
|
|
25
|
+
return reply.code(503).send({ code: 'WORKSPACE_FILES_UNAVAILABLE' });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
+
import { registerCheckReferences } from './check-references/endpoint.js';
|
|
7
|
+
import { registerReadEntry } from './read-entry/endpoint.js';
|
|
6
8
|
import { registerListDirectory } from './list-directory/endpoint.js';
|
|
7
9
|
import { registerCopyEntry } from './copy-entry/endpoint.js';
|
|
8
10
|
import { registerMoveEntry } from './move-entry/endpoint.js';
|
|
@@ -11,6 +13,8 @@ import { registerDeleteEntry } from './delete-entry/endpoint.js';
|
|
|
11
13
|
export function registerFileRoutes(app, deps) {
|
|
12
14
|
if (!deps.workspaceFileRoutes)
|
|
13
15
|
return;
|
|
16
|
+
registerCheckReferences(app, deps.workspaceFileRoutes);
|
|
17
|
+
registerReadEntry(app, deps.workspaceFileRoutes);
|
|
14
18
|
registerListDirectory(app, deps.workspaceFileRoutes);
|
|
15
19
|
registerCopyEntry(app, deps.workspaceFileRoutes);
|
|
16
20
|
registerMoveEntry(app, deps.workspaceFileRoutes);
|
|
@@ -54,6 +54,8 @@ export function parseSupervisedPlan(input) {
|
|
|
54
54
|
if (!closed)
|
|
55
55
|
return unavailable('MALFORMED_ORG');
|
|
56
56
|
const descriptions = new Map();
|
|
57
|
+
let descriptionKey;
|
|
58
|
+
let blankLines = 0;
|
|
57
59
|
while (index < lines.length && !lines[index].startsWith('*')) {
|
|
58
60
|
const description = descriptionPattern.exec(lines[index]);
|
|
59
61
|
if (description) {
|
|
@@ -61,6 +63,15 @@ export function parseSupervisedPlan(input) {
|
|
|
61
63
|
if (descriptions.has(key))
|
|
62
64
|
return unavailable('MALFORMED_ORG');
|
|
63
65
|
descriptions.set(key, description[2] ?? '');
|
|
66
|
+
descriptionKey = key;
|
|
67
|
+
blankLines = 0;
|
|
68
|
+
}
|
|
69
|
+
else if (descriptionKey && /^[ \t]+\S/.test(lines[index])) {
|
|
70
|
+
descriptions.set(descriptionKey, `${descriptions.get(descriptionKey)}${'\n'.repeat(blankLines + 1)}${lines[index].replace(/^(?: |\t)/, '')}`);
|
|
71
|
+
blankLines = 0;
|
|
72
|
+
}
|
|
73
|
+
else if (lines[index].trim() === '') {
|
|
74
|
+
blankLines += 1;
|
|
64
75
|
}
|
|
65
76
|
else if (lines[index].trim() !== '') {
|
|
66
77
|
return unavailable('MALFORMED_ORG');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function registerArchiveWorkspacePlan(app, deps) {
|
|
7
|
+
app.post('/api/workspaces/:workspaceId/plans/:planName/archive', async (request, reply) => {
|
|
8
|
+
try {
|
|
9
|
+
const { workspaceId, planName } = request.params;
|
|
10
|
+
const workspace = await deps.workspaces.resolve(workspaceId);
|
|
11
|
+
const result = await deps.archiver.archive(workspace.realPath, planName);
|
|
12
|
+
if (result.kind === 'archived')
|
|
13
|
+
return reply.send({ archived: true });
|
|
14
|
+
if (result.kind === 'missing')
|
|
15
|
+
return reply.code(404).send({ code: 'PLAN_NOT_FOUND' });
|
|
16
|
+
if (result.kind === 'conflict')
|
|
17
|
+
return reply.code(409).send({ code: 'PLAN_ARCHIVE_CONFLICT' });
|
|
18
|
+
return reply.code(422).send({ code: 'PLAN_ARCHIVE_UNAVAILABLE' });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND')
|
|
22
|
+
return reply.code(404).send({ code: 'WORKSPACE_NOT_FOUND' });
|
|
23
|
+
return reply.code(503).send({ code: 'PLAN_ARCHIVE_UNAVAILABLE' });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -7,7 +7,7 @@ export function registerListWorkspacePlans(app, deps) {
|
|
|
7
7
|
app.get('/api/workspaces/:workspaceId/plans', async (request, reply) => {
|
|
8
8
|
try {
|
|
9
9
|
const workspace = await deps.workspaces.resolve(request.params.workspaceId);
|
|
10
|
-
return reply.send(await deps.plans.list(workspace.realPath));
|
|
10
|
+
return reply.send(await deps.plans.list(workspace.realPath, true));
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
13
|
if (error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND')
|
|
@@ -9,6 +9,7 @@ import { registerGetPlan } from './get-plan/endpoint.js';
|
|
|
9
9
|
import { registerGetWorkspacePlan } from './get-workspace-plan/endpoint.js';
|
|
10
10
|
import { registerListWorkspacePlans } from './list-workspace-plans/endpoint.js';
|
|
11
11
|
import { registerOpenPlan } from './open-plan/endpoint.js';
|
|
12
|
+
import { registerArchiveWorkspacePlan } from './archive-workspace-plan/endpoint.js';
|
|
12
13
|
export function registerPlanRoutes(app, deps) {
|
|
13
14
|
if (deps.planRoutes) {
|
|
14
15
|
registerGetPlan(app, deps.planRoutes);
|
|
@@ -18,6 +19,11 @@ export function registerPlanRoutes(app, deps) {
|
|
|
18
19
|
if (deps.workspacePlanRoutes) {
|
|
19
20
|
registerListWorkspacePlans(app, deps.workspacePlanRoutes);
|
|
20
21
|
registerGetWorkspacePlan(app, deps.workspacePlanRoutes);
|
|
22
|
+
if (deps.workspacePlanRoutes.archiver)
|
|
23
|
+
registerArchiveWorkspacePlan(app, {
|
|
24
|
+
workspaces: deps.workspacePlanRoutes.workspaces,
|
|
25
|
+
archiver: deps.workspacePlanRoutes.archiver,
|
|
26
|
+
});
|
|
21
27
|
}
|
|
22
28
|
if (deps.planMeasurementRoutes)
|
|
23
29
|
registerGetPlanMeasurement(app, deps.planMeasurementRoutes);
|
|
@@ -59,6 +59,7 @@ export class RelaySession {
|
|
|
59
59
|
provider: input.provider,
|
|
60
60
|
profile: profileName(input.profile),
|
|
61
61
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
62
|
+
...(input.modelSettings === undefined ? {} : { modelSettings: { ...input.modelSettings } }),
|
|
62
63
|
...(input.branch === undefined ? {} : { branch: input.branch }),
|
|
63
64
|
effectiveSkillSelection: createEffectiveSkillSelection(input.effectiveSkillSelection),
|
|
64
65
|
executionPolicy: createSessionExecutionPolicy(input),
|
|
@@ -173,6 +174,7 @@ export class RelaySession {
|
|
|
173
174
|
function copy(snapshot) {
|
|
174
175
|
return {
|
|
175
176
|
...snapshot,
|
|
177
|
+
...(snapshot.modelSettings ? { modelSettings: { ...snapshot.modelSettings } } : {}),
|
|
176
178
|
...(snapshot.effectiveSkillSelection === undefined
|
|
177
179
|
? {}
|
|
178
180
|
: {
|
|
@@ -30,6 +30,12 @@ export function registerStartSession(app, deps) {
|
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
32
|
deps.reportFailure?.('start-session', error);
|
|
33
|
+
if (error instanceof Error &&
|
|
34
|
+
['ORG_EXECUTOR_PROFILE_UNAVAILABLE', 'ORG_EXECUTOR_PROFILE_INVALID'].includes(error.message))
|
|
35
|
+
return reply
|
|
36
|
+
.code(503)
|
|
37
|
+
.type('application/problem+json')
|
|
38
|
+
.send(problem(error.message, 503, 'The installed Org-plan executor profile could not be configured. Restore the Gestalt agent profile and retry.'));
|
|
33
39
|
if (error instanceof SkillProfileError)
|
|
34
40
|
return reply
|
|
35
41
|
.code(400)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { thinkingLevels } from '../../../../shared/contracts/session-model-settings.js';
|
|
7
8
|
const schema = z.object({
|
|
8
9
|
workspaceId: z.string().min(1),
|
|
9
10
|
provider: z.enum(['codex', 'kimi']),
|
|
@@ -14,6 +15,9 @@ const schema = z.object({
|
|
|
14
15
|
.trim()
|
|
15
16
|
.optional()
|
|
16
17
|
.transform((value) => value || undefined),
|
|
18
|
+
reasoningEffort: z.enum(thinkingLevels).optional(),
|
|
19
|
+
executorModel: z.string().trim().min(1).max(256).optional(),
|
|
20
|
+
executorReasoningEffort: z.enum(thinkingLevels).optional(),
|
|
17
21
|
sandbox: z
|
|
18
22
|
.enum(['workspace-git', 'read-only', 'workspace-write', 'danger-full-access'])
|
|
19
23
|
.optional(),
|
|
@@ -62,6 +62,18 @@ export async function startSession(input, deps) {
|
|
|
62
62
|
provider: input.provider,
|
|
63
63
|
profile: input.profile,
|
|
64
64
|
model,
|
|
65
|
+
...(input.provider === 'codex' &&
|
|
66
|
+
(input.reasoningEffort || input.executorModel || input.executorReasoningEffort)
|
|
67
|
+
? {
|
|
68
|
+
modelSettings: {
|
|
69
|
+
...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
|
|
70
|
+
...(input.executorModel ? { executorModel: input.executorModel } : {}),
|
|
71
|
+
...(input.executorReasoningEffort
|
|
72
|
+
? { executorReasoningEffort: input.executorReasoningEffort }
|
|
73
|
+
: {}),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
65
77
|
...(branch ? { branch } : {}),
|
|
66
78
|
sandbox: input.sandbox,
|
|
67
79
|
approvalPolicy: input.approvalPolicy,
|
|
@@ -15,6 +15,9 @@ export class FilesystemWorkspaceCatalog {
|
|
|
15
15
|
return [(await this.tree()).option];
|
|
16
16
|
}
|
|
17
17
|
async resolve(id) {
|
|
18
|
+
const root = await this.root;
|
|
19
|
+
if (id === this.id(root))
|
|
20
|
+
return { id, name: '/', realPath: root };
|
|
18
21
|
const found = this.find(await this.tree(), id);
|
|
19
22
|
if (!found)
|
|
20
23
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
@@ -47,7 +47,8 @@ async function listProfileThreads(launch, profile, cwd) {
|
|
|
47
47
|
limit: 20,
|
|
48
48
|
sortKey: 'recency_at',
|
|
49
49
|
sortDirection: 'desc',
|
|
50
|
-
sourceKinds: ['cli', 'appServer'],
|
|
50
|
+
sourceKinds: ['cli', 'vscode', 'appServer'],
|
|
51
|
+
modelProviders: [],
|
|
51
52
|
}));
|
|
52
53
|
return (result.data ?? []).flatMap((thread) => {
|
|
53
54
|
if (typeof thread.id !== 'string' || typeof thread.cwd !== 'string')
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
/** Finds root assignments without treating instruction strings or TOML tables as settings. */
|
|
10
|
+
function replaceRootSetting(profile, key, value) {
|
|
11
|
+
const lines = profile.split('\n');
|
|
12
|
+
let stringDelimiter = null;
|
|
13
|
+
let rootEnd = lines.length;
|
|
14
|
+
const assignments = [];
|
|
15
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
16
|
+
const line = lines[index];
|
|
17
|
+
if (!stringDelimiter) {
|
|
18
|
+
if (/^\s*\[/.test(line)) {
|
|
19
|
+
rootEnd = index;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (new RegExp(`^\\s*${key}\\s*=`).test(line))
|
|
23
|
+
assignments.push(index);
|
|
24
|
+
}
|
|
25
|
+
for (let offset = 0; offset < line.length; offset += 1) {
|
|
26
|
+
const character = line[offset];
|
|
27
|
+
if (stringDelimiter) {
|
|
28
|
+
if (stringDelimiter.startsWith('"') && character === '\\') {
|
|
29
|
+
offset += 1;
|
|
30
|
+
}
|
|
31
|
+
else if (line.startsWith(stringDelimiter, offset)) {
|
|
32
|
+
offset += stringDelimiter.length - 1;
|
|
33
|
+
stringDelimiter = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (character === '#') {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
else if (character === '"' || character === "'") {
|
|
40
|
+
stringDelimiter = line.startsWith(character.repeat(3), offset)
|
|
41
|
+
? character.repeat(3)
|
|
42
|
+
: character;
|
|
43
|
+
offset += stringDelimiter.length - 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (stringDelimiter?.length === 1)
|
|
47
|
+
throw new Error('ORG_EXECUTOR_PROFILE_INVALID');
|
|
48
|
+
}
|
|
49
|
+
if (stringDelimiter || assignments.length > 1)
|
|
50
|
+
throw new Error('ORG_EXECUTOR_PROFILE_INVALID');
|
|
51
|
+
const assignment = `${key} = ${JSON.stringify(value)}`;
|
|
52
|
+
if (assignments.length)
|
|
53
|
+
lines[assignments[0]] = assignment;
|
|
54
|
+
else
|
|
55
|
+
lines.splice(rootEnd, 0, assignment);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
/** Keeps installed Org role instructions while pinning settings only for the owning session. */
|
|
59
|
+
export class SessionModelConfig {
|
|
60
|
+
codexHome;
|
|
61
|
+
stateDirectory;
|
|
62
|
+
constructor(codexHome, stateDirectory) {
|
|
63
|
+
this.codexHome = codexHome;
|
|
64
|
+
this.stateDirectory = stateDirectory;
|
|
65
|
+
}
|
|
66
|
+
async resolve(session) {
|
|
67
|
+
const settings = session.modelSettings;
|
|
68
|
+
if (!settings)
|
|
69
|
+
return {};
|
|
70
|
+
const config = settings.reasoningEffort
|
|
71
|
+
? { model_reasoning_effort: settings.reasoningEffort }
|
|
72
|
+
: {};
|
|
73
|
+
if (!settings.executorModel && !settings.executorReasoningEffort)
|
|
74
|
+
return config;
|
|
75
|
+
let profile;
|
|
76
|
+
try {
|
|
77
|
+
profile = await readFile(join(this.codexHome, 'agents', 'org-plan-executor.toml'), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error('ORG_EXECUTOR_PROFILE_UNAVAILABLE');
|
|
81
|
+
}
|
|
82
|
+
for (const [key, value] of [
|
|
83
|
+
['model', settings.executorModel],
|
|
84
|
+
['model_reasoning_effort', settings.executorReasoningEffort],
|
|
85
|
+
]) {
|
|
86
|
+
if (!value)
|
|
87
|
+
continue;
|
|
88
|
+
profile = replaceRootSetting(profile, key, value);
|
|
89
|
+
}
|
|
90
|
+
const directory = join(this.stateDirectory, 'agent-profiles');
|
|
91
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
92
|
+
const name = createHash('sha256').update(session.id).digest('hex');
|
|
93
|
+
const path = join(directory, `${name}.toml`);
|
|
94
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
95
|
+
try {
|
|
96
|
+
await writeFile(temporary, profile, { flag: 'wx', mode: 0o600 });
|
|
97
|
+
await rename(temporary, path);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await rm(temporary, { force: true });
|
|
101
|
+
}
|
|
102
|
+
return { ...config, agents: { 'org-plan-executor': { config_file: path } } };
|
|
103
|
+
}
|
|
104
|
+
}
|