@flowdular/sdk 0.1.0 → 0.2.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/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +1 -1
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- package/packages/sandbox/vite.config.ts +0 -133
|
@@ -1,993 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
access,
|
|
5
|
-
cp,
|
|
6
|
-
mkdtemp,
|
|
7
|
-
readFile,
|
|
8
|
-
readdir,
|
|
9
|
-
rm,
|
|
10
|
-
stat,
|
|
11
|
-
} from 'node:fs/promises';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join, relative } from 'node:path';
|
|
14
|
-
import {
|
|
15
|
-
composeInstruction,
|
|
16
|
-
findRole,
|
|
17
|
-
parseHandoff,
|
|
18
|
-
selectTaskSkill,
|
|
19
|
-
type AgentRoleDefinition,
|
|
20
|
-
type CodingAgentEvent,
|
|
21
|
-
type CodingAgentMessage,
|
|
22
|
-
type CodingAgentRegistry,
|
|
23
|
-
type HandoffDeclaration,
|
|
24
|
-
} from '@flowdular/sdk/coding-agent';
|
|
25
|
-
import {
|
|
26
|
-
invalidateAutoReview,
|
|
27
|
-
prepareAutoReview,
|
|
28
|
-
moduleReviewRevision,
|
|
29
|
-
recordAutoReview,
|
|
30
|
-
} from './auto-review.ts';
|
|
31
|
-
import { attachmentInstruction } from './attachments.ts';
|
|
32
|
-
import { captureCheckpoint } from './checkpoints.ts';
|
|
33
|
-
import { guardAgentPaths } from './path-guard.ts';
|
|
34
|
-
import type { SandboxConfiguration } from './config.ts';
|
|
35
|
-
import { diffTrees, type FileDiff } from './diff.ts';
|
|
36
|
-
import {
|
|
37
|
-
GATE_IDS,
|
|
38
|
-
formatDirectory,
|
|
39
|
-
isGateId,
|
|
40
|
-
runGates,
|
|
41
|
-
type GateId,
|
|
42
|
-
type GateResult,
|
|
43
|
-
} from './gates.ts';
|
|
44
|
-
import {
|
|
45
|
-
SPEC_OWNER_ROLE,
|
|
46
|
-
planHandoff,
|
|
47
|
-
planSpecGateHandoff,
|
|
48
|
-
routeRole,
|
|
49
|
-
type RoutingContext,
|
|
50
|
-
} from './planning.ts';
|
|
51
|
-
import type { PlatformClient } from './platform-client.ts';
|
|
52
|
-
import { listSkills, writeAgentPointer } from './reference.ts';
|
|
53
|
-
import {
|
|
54
|
-
appendChatEntry,
|
|
55
|
-
basePathOf,
|
|
56
|
-
findSessionModule,
|
|
57
|
-
installSessionDependencies,
|
|
58
|
-
modulePathOf,
|
|
59
|
-
readChat,
|
|
60
|
-
readSession,
|
|
61
|
-
sessionPaths,
|
|
62
|
-
updateSession,
|
|
63
|
-
type ChatEntry,
|
|
64
|
-
type HandoffPlan,
|
|
65
|
-
type SandboxSession,
|
|
66
|
-
type SessionModule,
|
|
67
|
-
type SessionPaths,
|
|
68
|
-
} from './sessions.ts';
|
|
69
|
-
import { isSpecApproved, readSpecGate, type SpecGate } from './spec.ts';
|
|
70
|
-
import type { InstallResult } from './workspace-install.ts';
|
|
71
|
-
import { SandboxSetupError } from './workspace-root.ts';
|
|
72
|
-
|
|
73
|
-
export interface TurnContext {
|
|
74
|
-
readonly workspaceRoot: string;
|
|
75
|
-
readonly configuration: SandboxConfiguration;
|
|
76
|
-
readonly registry: CodingAgentRegistry;
|
|
77
|
-
readonly roles: readonly AgentRoleDefinition[];
|
|
78
|
-
readonly platform: PlatformClient | null;
|
|
79
|
-
/* Installs the session workspace; defaults to pnpm. Tests replace it. */
|
|
80
|
-
readonly installDependencies?: (
|
|
81
|
-
session: SandboxSession,
|
|
82
|
-
) => Promise<InstallResult>;
|
|
83
|
-
/* Runs deterministic sandbox gates. Tests may replace the process boundary
|
|
84
|
-
while keeping routing and approval behaviour unchanged. */
|
|
85
|
-
readonly executeGates?: (input: {
|
|
86
|
-
readonly session: SandboxSession;
|
|
87
|
-
readonly gates: readonly GateId[];
|
|
88
|
-
readonly modules?: readonly SessionModule[];
|
|
89
|
-
}) => Promise<readonly GateResult[]>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface TurnInput {
|
|
93
|
-
readonly sessionId: string;
|
|
94
|
-
readonly message: string;
|
|
95
|
-
readonly role?: string;
|
|
96
|
-
/* The draft module directory this turn works in. Defaults to the module the
|
|
97
|
-
last handoff named, then to the primary module. */
|
|
98
|
-
readonly module?: string;
|
|
99
|
-
readonly driver?: string;
|
|
100
|
-
readonly signal?: AbortSignal;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* A diff of one draft module, named by the module directory it belongs to. */
|
|
104
|
-
export interface SessionFileDiff extends FileDiff {
|
|
105
|
-
readonly module: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface TurnOutcome {
|
|
109
|
-
readonly session: SandboxSession;
|
|
110
|
-
readonly diffs: readonly SessionFileDiff[];
|
|
111
|
-
readonly gates: readonly GateResult[];
|
|
112
|
-
readonly handoff: HandoffPlan;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const MAX_HISTORY_MESSAGES = 20;
|
|
116
|
-
|
|
117
|
-
function historyFrom(entries: readonly ChatEntry[]): CodingAgentMessage[] {
|
|
118
|
-
/* Orchestrator feedback, gate failures included, is replayed as user input
|
|
119
|
-
so the next turn can act on it. */
|
|
120
|
-
return entries
|
|
121
|
-
.filter(
|
|
122
|
-
(entry) =>
|
|
123
|
-
entry.kind !== 'event' &&
|
|
124
|
-
typeof entry.text === 'string' &&
|
|
125
|
-
entry.text.trim().length > 0,
|
|
126
|
-
)
|
|
127
|
-
.slice(-MAX_HISTORY_MESSAGES)
|
|
128
|
-
.map((entry) => ({
|
|
129
|
-
role: entry.kind === 'agent' ? ('assistant' as const) : ('user' as const),
|
|
130
|
-
text: entry.text!,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function exists(path: string): Promise<boolean> {
|
|
135
|
-
try {
|
|
136
|
-
await access(path);
|
|
137
|
-
return true;
|
|
138
|
-
} catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/* Most modules declare their endpoints in src/api/endpoints.ts, but a module
|
|
144
|
-
may compose its server from src/platform.ts or several files under
|
|
145
|
-
src/server. Any of the three means the server surface exists. */
|
|
146
|
-
async function hasServerSurface(modulePath: string): Promise<boolean> {
|
|
147
|
-
if (await exists(join(modulePath, 'src/api/endpoints.ts'))) return true;
|
|
148
|
-
if (await exists(join(modulePath, 'src/platform.ts'))) return true;
|
|
149
|
-
try {
|
|
150
|
-
return (await readdir(join(modulePath, 'src/server'))).length > 0;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function routingContext(
|
|
157
|
-
context: TurnContext,
|
|
158
|
-
session: SandboxSession,
|
|
159
|
-
paths: SessionPaths,
|
|
160
|
-
modulePath: string,
|
|
161
|
-
message: string,
|
|
162
|
-
entries: readonly ChatEntry[],
|
|
163
|
-
specApproved: boolean | null,
|
|
164
|
-
): Promise<RoutingContext> {
|
|
165
|
-
return {
|
|
166
|
-
session,
|
|
167
|
-
paths,
|
|
168
|
-
roles: context.roles,
|
|
169
|
-
message,
|
|
170
|
-
hasSpec: await exists(join(modulePath, 'spec/module.yaml')),
|
|
171
|
-
hasManifest: await exists(join(modulePath, 'module.json')),
|
|
172
|
-
hasServer: await hasServerSurface(modulePath),
|
|
173
|
-
hasClient: await exists(join(modulePath, 'src/client/contribution.tsrx')),
|
|
174
|
-
specApproved,
|
|
175
|
-
lastHandoff:
|
|
176
|
-
entries.filter((entry) => entry.handoff).at(-1)?.handoff ?? null,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* The last handoff names the module the work continues in, so a chained turn
|
|
181
|
-
stays where the previous one left off. An explicit request wins, and one that
|
|
182
|
-
names a module the session does not have is refused rather than silently
|
|
183
|
-
redirected to the primary. */
|
|
184
|
-
function activeModule(
|
|
185
|
-
session: SandboxSession,
|
|
186
|
-
requested: string | undefined,
|
|
187
|
-
entries: readonly ChatEntry[],
|
|
188
|
-
): SessionModule {
|
|
189
|
-
if (requested) return findSessionModule(session, requested);
|
|
190
|
-
const named = entries.filter((entry) => entry.handoff).at(-1)
|
|
191
|
-
?.handoff?.module;
|
|
192
|
-
return (
|
|
193
|
-
session.modules.find((module) => module.directory === named) ??
|
|
194
|
-
session.modules[0]!
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/* Nobody but the specification owner works in a module whose specification the
|
|
199
|
-
operator has not approved. A missing owner role is a configuration error, not
|
|
200
|
-
permission to bypass the gate. */
|
|
201
|
-
function refusesUnapproved(gate: SpecGate, roleId: string): boolean {
|
|
202
|
-
return gate.approved !== true && roleId !== SPEC_OWNER_ROLE;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/* The refused turn: the operator hears why nothing ran, with a stable code, and
|
|
206
|
-
the session stops on the move that unblocks it. */
|
|
207
|
-
async function* refuseUnapproved(
|
|
208
|
-
context: TurnContext,
|
|
209
|
-
session: SandboxSession,
|
|
210
|
-
active: SessionModule,
|
|
211
|
-
role: AgentRoleDefinition,
|
|
212
|
-
gate: SpecGate,
|
|
213
|
-
input: { readonly brief: string; readonly driver: string },
|
|
214
|
-
): AsyncGenerator<ChatEntry, TurnOutcome> {
|
|
215
|
-
const handoff = planSpecGateHandoff({
|
|
216
|
-
roles: context.roles,
|
|
217
|
-
module: active.directory,
|
|
218
|
-
refusedRole: role.id,
|
|
219
|
-
changed: gate.changed,
|
|
220
|
-
brief: input.brief,
|
|
221
|
-
});
|
|
222
|
-
const reason = `${role.name} did not take this turn: the specification of ${active.id} is not approved. ${
|
|
223
|
-
gate.changed
|
|
224
|
-
? 'Review the change below and approve it, ask for changes, or edit the specification yourself.'
|
|
225
|
-
: `${handoff.roleName} writes the specification change first.`
|
|
226
|
-
}`;
|
|
227
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
228
|
-
kind: 'system',
|
|
229
|
-
role: role.id,
|
|
230
|
-
module: active.directory,
|
|
231
|
-
text: reason,
|
|
232
|
-
event: {
|
|
233
|
-
type: 'error',
|
|
234
|
-
code: 'SPEC_NOT_APPROVED',
|
|
235
|
-
message: reason,
|
|
236
|
-
} as CodingAgentEvent,
|
|
237
|
-
});
|
|
238
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
239
|
-
kind: 'system',
|
|
240
|
-
role: handoff.role,
|
|
241
|
-
module: active.directory,
|
|
242
|
-
text: handoffText(handoff),
|
|
243
|
-
handoff,
|
|
244
|
-
});
|
|
245
|
-
const updated = await updateSession(context.workspaceRoot, session.id, {
|
|
246
|
-
state: handoff.kind === 'approval' ? 'awaiting-approval' : 'planned',
|
|
247
|
-
driver: input.driver,
|
|
248
|
-
});
|
|
249
|
-
return {
|
|
250
|
-
session: updated,
|
|
251
|
-
diffs: await collectDiffs(context.workspaceRoot, updated),
|
|
252
|
-
gates: [],
|
|
253
|
-
handoff,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function handoffText(handoff: HandoffPlan): string {
|
|
258
|
-
if (handoff.kind === 'continue') {
|
|
259
|
-
return `Handing off to ${handoff.roleName}. ${handoff.reason}`;
|
|
260
|
-
}
|
|
261
|
-
if (handoff.kind === 'approval') {
|
|
262
|
-
return `${handoff.reason} Approve it to let ${handoff.roleName} implement it.`;
|
|
263
|
-
}
|
|
264
|
-
if (handoff.kind === 'question') {
|
|
265
|
-
return `${handoff.reason} Answer below to continue.`;
|
|
266
|
-
}
|
|
267
|
-
return handoff.reason;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function runCli(
|
|
271
|
-
workspaceRoot: string,
|
|
272
|
-
args: readonly string[],
|
|
273
|
-
): Promise<{ code: number | null; output: string }> {
|
|
274
|
-
return new Promise((resolvePromise) => {
|
|
275
|
-
const child = spawn(
|
|
276
|
-
'pnpm',
|
|
277
|
-
['--dir', workspaceRoot, '--silent', 'flowdular', ...args],
|
|
278
|
-
{
|
|
279
|
-
cwd: workspaceRoot,
|
|
280
|
-
env: { ...process.env, FORCE_COLOR: '0' },
|
|
281
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
282
|
-
},
|
|
283
|
-
);
|
|
284
|
-
let output = '';
|
|
285
|
-
child.stdout.setEncoding('utf8');
|
|
286
|
-
child.stderr.setEncoding('utf8');
|
|
287
|
-
const append = (chunk: string) => {
|
|
288
|
-
output = (output + chunk).slice(0, 8_000);
|
|
289
|
-
};
|
|
290
|
-
child.stdout.on('data', append);
|
|
291
|
-
child.stderr.on('data', append);
|
|
292
|
-
child.on('error', (error) =>
|
|
293
|
-
resolvePromise({ code: null, output: error.message }),
|
|
294
|
-
);
|
|
295
|
-
child.on('close', (code) => resolvePromise({ code, output }));
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function listFiles(root: string, directory = root): Promise<string[]> {
|
|
300
|
-
let entries;
|
|
301
|
-
try {
|
|
302
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
303
|
-
} catch {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
const files: string[] = [];
|
|
307
|
-
for (const entry of entries) {
|
|
308
|
-
if (entry.name === 'node_modules') continue;
|
|
309
|
-
const path = join(directory, entry.name);
|
|
310
|
-
if (entry.isDirectory()) files.push(...(await listFiles(root, path)));
|
|
311
|
-
else files.push(relative(root, path));
|
|
312
|
-
}
|
|
313
|
-
return files;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/* The scaffold refuses a target directory that holds anything but the
|
|
317
|
-
specification, while a business manager may already have written
|
|
318
|
-
translations there. Those files step aside for the scaffold and come back
|
|
319
|
-
over the skeleton, so what the specialist wrote wins over the generated
|
|
320
|
-
placeholder. */
|
|
321
|
-
async function withScaffoldClearance<T>(
|
|
322
|
-
modulePath: string,
|
|
323
|
-
run: () => Promise<T>,
|
|
324
|
-
): Promise<T> {
|
|
325
|
-
const extras = (await listFiles(modulePath)).filter(
|
|
326
|
-
(file) => file !== 'spec/module.yaml',
|
|
327
|
-
);
|
|
328
|
-
if (extras.length === 0) return run();
|
|
329
|
-
const stash = await mkdtemp(join(tmpdir(), 'flowdular-scaffold-'));
|
|
330
|
-
try {
|
|
331
|
-
for (const file of extras) {
|
|
332
|
-
await cp(join(modulePath, file), join(stash, file));
|
|
333
|
-
await rm(join(modulePath, file), { force: true });
|
|
334
|
-
}
|
|
335
|
-
for (const directory of await readdir(modulePath)) {
|
|
336
|
-
if (directory === 'spec') continue;
|
|
337
|
-
await rm(join(modulePath, directory), { recursive: true, force: true });
|
|
338
|
-
}
|
|
339
|
-
return await run();
|
|
340
|
-
} finally {
|
|
341
|
-
for (const file of extras) {
|
|
342
|
-
await cp(join(stash, file), join(modulePath, file), { force: true });
|
|
343
|
-
}
|
|
344
|
-
await rm(stash, { recursive: true, force: true });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/* A new module is created by the same capability the CLI exposes, from the
|
|
349
|
-
specification the business manager wrote. The sandbox never hand-writes a
|
|
350
|
-
module skeleton of its own, and it formats the skeleton right away so the
|
|
351
|
-
format gate reports the specialist's files, not the generator's. */
|
|
352
|
-
export async function scaffoldFromSpec(
|
|
353
|
-
context: TurnContext,
|
|
354
|
-
session: SandboxSession,
|
|
355
|
-
): Promise<string | null> {
|
|
356
|
-
const paths = sessionPaths(
|
|
357
|
-
context.workspaceRoot,
|
|
358
|
-
session.id,
|
|
359
|
-
session.moduleSuffix,
|
|
360
|
-
);
|
|
361
|
-
const notes: string[] = [];
|
|
362
|
-
for (const module of session.modules) {
|
|
363
|
-
if (module.kind !== 'new') continue;
|
|
364
|
-
const modulePath = modulePathOf(paths, module.directory);
|
|
365
|
-
const specPath = join(modulePath, 'spec', 'module.yaml');
|
|
366
|
-
if (!(await exists(specPath))) continue;
|
|
367
|
-
if (await exists(join(modulePath, 'module.json'))) continue;
|
|
368
|
-
const spec = await readFile(specPath, 'utf8');
|
|
369
|
-
if (!isSpecApproved(module, spec)) {
|
|
370
|
-
notes.push(
|
|
371
|
-
`The ${module.id} specification is not approved by the operator yet, so the module skeleton was not created.`,
|
|
372
|
-
);
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const result = await withScaffoldClearance(modulePath, () =>
|
|
376
|
-
runCli(context.workspaceRoot, [
|
|
377
|
-
'module',
|
|
378
|
-
'new',
|
|
379
|
-
module.id,
|
|
380
|
-
'--spec',
|
|
381
|
-
join('modules', module.directory, 'spec', 'module.yaml'),
|
|
382
|
-
'--apply',
|
|
383
|
-
'--root',
|
|
384
|
-
paths.workspace,
|
|
385
|
-
'--json',
|
|
386
|
-
]),
|
|
387
|
-
);
|
|
388
|
-
if (result.code !== 0) {
|
|
389
|
-
notes.push(
|
|
390
|
-
`The module scaffold for ${module.id} failed (exit ${result.code ?? 'signal'}): ${result.output.slice(0, 1_500) || 'the command produced no output.'}`,
|
|
391
|
-
);
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
await formatDirectory(context.workspaceRoot, modulePath);
|
|
395
|
-
notes.push(
|
|
396
|
-
`Created the module skeleton for ${module.id} from its approved specification.`,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
return notes.length > 0 ? notes.join('\n') : null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/* Diffs are recomputed only when a file under the base or the draft changed.
|
|
403
|
-
The signature is one stat per file, the diff is an LCS per changed file. */
|
|
404
|
-
interface DiffCacheEntry {
|
|
405
|
-
readonly signature: string;
|
|
406
|
-
readonly diffs: readonly SessionFileDiff[];
|
|
407
|
-
}
|
|
408
|
-
const diffCache = new Map<string, DiffCacheEntry>();
|
|
409
|
-
const DIFF_CACHE_LIMIT = 64;
|
|
410
|
-
|
|
411
|
-
async function treeSignature(
|
|
412
|
-
hash: ReturnType<typeof createHash>,
|
|
413
|
-
root: string,
|
|
414
|
-
): Promise<void> {
|
|
415
|
-
for (const file of (await listFiles(root)).sort()) {
|
|
416
|
-
try {
|
|
417
|
-
const info = await stat(join(root, file));
|
|
418
|
-
hash.update(`${file}\0${info.mtimeMs}\0${info.size}\n`);
|
|
419
|
-
} catch {
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function collectDiffs(
|
|
426
|
-
workspaceRoot: string,
|
|
427
|
-
session: SandboxSession,
|
|
428
|
-
): Promise<readonly SessionFileDiff[]> {
|
|
429
|
-
const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
|
|
430
|
-
const hash = createHash('sha1');
|
|
431
|
-
for (const module of session.modules) {
|
|
432
|
-
hash.update(`${module.directory}\n`);
|
|
433
|
-
await treeSignature(hash, basePathOf(paths, module.directory));
|
|
434
|
-
await treeSignature(hash, modulePathOf(paths, module.directory));
|
|
435
|
-
}
|
|
436
|
-
const signature = hash.digest('hex');
|
|
437
|
-
const cached = diffCache.get(session.id);
|
|
438
|
-
if (cached && cached.signature === signature) return cached.diffs;
|
|
439
|
-
|
|
440
|
-
const diffs: SessionFileDiff[] = [];
|
|
441
|
-
for (const module of session.modules) {
|
|
442
|
-
for (const diff of await diffTrees(
|
|
443
|
-
basePathOf(paths, module.directory),
|
|
444
|
-
modulePathOf(paths, module.directory),
|
|
445
|
-
)) {
|
|
446
|
-
diffs.push({ ...diff, module: module.directory });
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (diffCache.size >= DIFF_CACHE_LIMIT) {
|
|
450
|
-
diffCache.delete(diffCache.keys().next().value!);
|
|
451
|
-
}
|
|
452
|
-
diffCache.set(session.id, { signature, diffs });
|
|
453
|
-
return diffs;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export function forgetDiffs(sessionId: string): void {
|
|
457
|
-
diffCache.delete(sessionId);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export async function runSessionGates(
|
|
461
|
-
context: TurnContext,
|
|
462
|
-
session: SandboxSession,
|
|
463
|
-
gates: readonly string[],
|
|
464
|
-
modules?: readonly SessionModule[],
|
|
465
|
-
signal?: AbortSignal,
|
|
466
|
-
): Promise<readonly GateResult[]> {
|
|
467
|
-
const selected = gates.filter(isGateId) as GateId[];
|
|
468
|
-
if (selected.length === 0) return [];
|
|
469
|
-
if (context.executeGates) {
|
|
470
|
-
return context.executeGates({
|
|
471
|
-
session,
|
|
472
|
-
gates: selected,
|
|
473
|
-
...(modules ? { modules } : {}),
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
return runGates({
|
|
477
|
-
workspaceRoot: context.workspaceRoot,
|
|
478
|
-
paths: sessionPaths(
|
|
479
|
-
context.workspaceRoot,
|
|
480
|
-
session.id,
|
|
481
|
-
session.moduleSuffix,
|
|
482
|
-
),
|
|
483
|
-
session,
|
|
484
|
-
gates: selected,
|
|
485
|
-
...(modules ? { modules } : {}),
|
|
486
|
-
...(signal ? { signal } : {}),
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/* The dependency install is a gate in everything but name: when a declared
|
|
491
|
-
package cannot be installed, the specialist must hear it as a failure with
|
|
492
|
-
the installer's output, not as a typecheck error about a missing module. */
|
|
493
|
-
async function installGate(
|
|
494
|
-
context: TurnContext,
|
|
495
|
-
session: SandboxSession,
|
|
496
|
-
): Promise<GateResult | null> {
|
|
497
|
-
const install =
|
|
498
|
-
context.installDependencies ??
|
|
499
|
-
((target: SandboxSession) =>
|
|
500
|
-
installSessionDependencies(context.workspaceRoot, target));
|
|
501
|
-
const result = await install(session);
|
|
502
|
-
if (!result.ran) return null;
|
|
503
|
-
return {
|
|
504
|
-
id: 'dependencies',
|
|
505
|
-
status: result.ok ? 'passed' : 'failed',
|
|
506
|
-
durationMs: result.durationMs,
|
|
507
|
-
command: 'pnpm install (session workspace)',
|
|
508
|
-
output: result.ok
|
|
509
|
-
? `Installed the session workspace in ${result.durationMs} ms.`
|
|
510
|
-
: `The session workspace could not install the declared dependencies:\n${result.output}`,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/* A turn writes in one module, so the role's paths resolve against that
|
|
515
|
-
module's directory and nowhere else. */
|
|
516
|
-
function allowedPathsFor(
|
|
517
|
-
role: AgentRoleDefinition,
|
|
518
|
-
module: SessionModule,
|
|
519
|
-
): readonly string[] {
|
|
520
|
-
return role.allowedPaths.map((path) => `modules/${module.directory}/${path}`);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function gateSummary(gate: GateResult): string {
|
|
524
|
-
const label = gate.module ? `${gate.id} (modules/${gate.module})` : gate.id;
|
|
525
|
-
return gate.status === 'failed'
|
|
526
|
-
? `Gate ${label} failed.\nCommand: ${gate.command}\n\n${gate.output}`
|
|
527
|
-
: `Gate ${label}: ${gate.status}`;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/* One chat turn: the selected role drives the selected driver inside the
|
|
531
|
-
session workspace, every event is appended to the durable chat log, and the
|
|
532
|
-
diff plus the role's gates are collected when the driver finishes. */
|
|
533
|
-
export async function* runTurn(
|
|
534
|
-
context: TurnContext,
|
|
535
|
-
input: TurnInput,
|
|
536
|
-
): AsyncGenerator<ChatEntry, TurnOutcome> {
|
|
537
|
-
const session = await readSession(context.workspaceRoot, input.sessionId);
|
|
538
|
-
const paths = sessionPaths(
|
|
539
|
-
context.workspaceRoot,
|
|
540
|
-
session.id,
|
|
541
|
-
session.moduleSuffix,
|
|
542
|
-
);
|
|
543
|
-
const message = input.message.trim();
|
|
544
|
-
if (message.length < 1 || message.length > 20_000) {
|
|
545
|
-
throw new SandboxSetupError(
|
|
546
|
-
'INVALID_MESSAGE',
|
|
547
|
-
'A turn needs a message of 1 to 20000 characters.',
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
const transcript = await readChat(context.workspaceRoot, session);
|
|
551
|
-
const active = activeModule(session, input.module, transcript);
|
|
552
|
-
const modulePath = modulePathOf(paths, active.directory);
|
|
553
|
-
const gate = await readSpecGate(
|
|
554
|
-
active,
|
|
555
|
-
modulePath,
|
|
556
|
-
basePathOf(paths, active.directory),
|
|
557
|
-
);
|
|
558
|
-
const requested = input.role ?? 'auto';
|
|
559
|
-
const routed =
|
|
560
|
-
requested === 'auto'
|
|
561
|
-
? routeRole(
|
|
562
|
-
await routingContext(
|
|
563
|
-
context,
|
|
564
|
-
session,
|
|
565
|
-
paths,
|
|
566
|
-
modulePath,
|
|
567
|
-
message,
|
|
568
|
-
transcript,
|
|
569
|
-
gate.approved,
|
|
570
|
-
),
|
|
571
|
-
)
|
|
572
|
-
: { role: requested, reason: '' };
|
|
573
|
-
const roleId = routed.role;
|
|
574
|
-
const role = findRole(context.roles, roleId);
|
|
575
|
-
const driverId = input.driver ?? session.driver;
|
|
576
|
-
const driver = await context.registry.resolve(driverId);
|
|
577
|
-
|
|
578
|
-
const attachmentNote = attachmentInstruction(session.attachments);
|
|
579
|
-
|
|
580
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
581
|
-
kind: 'user',
|
|
582
|
-
role: roleId,
|
|
583
|
-
module: active.directory,
|
|
584
|
-
text: message,
|
|
585
|
-
...(session.attachments.length > 0
|
|
586
|
-
? { attachments: session.attachments }
|
|
587
|
-
: {}),
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
if (refusesUnapproved(gate, roleId)) {
|
|
591
|
-
return yield* refuseUnapproved(context, session, active, role, gate, {
|
|
592
|
-
brief: session.brief || message,
|
|
593
|
-
driver: driverId,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
await updateSession(context.workspaceRoot, session.id, {
|
|
598
|
-
state: 'editing',
|
|
599
|
-
role: roleId,
|
|
600
|
-
driver: driverId,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
if (routed.reason) {
|
|
604
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
605
|
-
kind: 'system',
|
|
606
|
-
role: roleId,
|
|
607
|
-
module: active.directory,
|
|
608
|
-
text: `Routed to ${role.name}. ${routed.reason}`,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/* An approved specification produces the module skeleton before the turn
|
|
613
|
-
starts, so an engineer never has to invent the module layout. */
|
|
614
|
-
const preparation = await scaffoldFromSpec(context, session);
|
|
615
|
-
if (preparation) {
|
|
616
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
617
|
-
kind: 'system',
|
|
618
|
-
role: roleId,
|
|
619
|
-
text: preparation,
|
|
620
|
-
});
|
|
621
|
-
const install = await installGate(context, session);
|
|
622
|
-
if (install && install.status === 'failed') {
|
|
623
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
624
|
-
kind: 'system',
|
|
625
|
-
role: roleId,
|
|
626
|
-
text: gateSummary(install),
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const skill = selectTaskSkill({
|
|
632
|
-
role: role.id,
|
|
633
|
-
sessionKind: active.kind === 'new' ? 'new-module' : 'edit-module',
|
|
634
|
-
blueprint: session.blueprint,
|
|
635
|
-
task: message,
|
|
636
|
-
available: await listSkills(context.workspaceRoot),
|
|
637
|
-
});
|
|
638
|
-
const reviewing = skill === 'auto-review';
|
|
639
|
-
if (reviewing) await prepareAutoReview(context.workspaceRoot, paths, active);
|
|
640
|
-
const reviewRevision = reviewing
|
|
641
|
-
? await moduleReviewRevision(modulePath)
|
|
642
|
-
: null;
|
|
643
|
-
if (reviewing) await invalidateAutoReview(paths, active);
|
|
644
|
-
let reviewPassed = false;
|
|
645
|
-
const turnAllowedPaths = reviewing ? [] : allowedPathsFor(role, active);
|
|
646
|
-
const team = context.roles.filter((mate) => role.handoff.includes(mate.id));
|
|
647
|
-
const instruction = composeInstruction(role, {
|
|
648
|
-
moduleId: active.id,
|
|
649
|
-
modulePath: `modules/${active.directory}`,
|
|
650
|
-
sessionKind: active.kind === 'new' ? 'new-module' : 'edit-module',
|
|
651
|
-
blueprint: session.blueprint,
|
|
652
|
-
allowedPaths: turnAllowedPaths,
|
|
653
|
-
skill,
|
|
654
|
-
team: team.map((mate) => `${mate.id}: ${mate.purpose}`),
|
|
655
|
-
notes: [
|
|
656
|
-
...(reviewing
|
|
657
|
-
? [
|
|
658
|
-
'Read the complete active-module change against reference/auto-review-base/. This turn is read-only. Preserve the intended next-specialist handoff from the implementation turn after a passing review.',
|
|
659
|
-
]
|
|
660
|
-
: []),
|
|
661
|
-
...(session.modules.length > 1
|
|
662
|
-
? [
|
|
663
|
-
`This session works on several modules: ${session.modules
|
|
664
|
-
.map(
|
|
665
|
-
(module) =>
|
|
666
|
-
`${module.id} (modules/${module.directory}, ${module.kind === 'new' ? 'new' : 'existing'})`,
|
|
667
|
-
)
|
|
668
|
-
.join(
|
|
669
|
-
', ',
|
|
670
|
-
)}. This turn is yours in modules/${active.directory} only; another specialist takes the turn for the others. Each module is a project of this pnpm workspace, so a draft that imports another draft resolves the session copy.`,
|
|
671
|
-
]
|
|
672
|
-
: []),
|
|
673
|
-
'reference/ is read-only. Consult only the code and references needed for this task; do not preload its catalog.',
|
|
674
|
-
'Other module.json files under modules/ describe the dependency graph. Only the draft module directories have sources you may change.',
|
|
675
|
-
...(active.kind === 'edit'
|
|
676
|
-
? [
|
|
677
|
-
'The module already exists. Read it before changing it and keep every existing behavior that the request does not ask you to change.',
|
|
678
|
-
]
|
|
679
|
-
: []),
|
|
680
|
-
],
|
|
681
|
-
});
|
|
682
|
-
await writeAgentPointer(
|
|
683
|
-
paths.workspace,
|
|
684
|
-
driverId === 'claude-code' ? 'CLAUDE.md' : 'AGENTS.md',
|
|
685
|
-
role.name,
|
|
686
|
-
skill,
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
const history = historyFrom(await readChat(context.workspaceRoot, session));
|
|
690
|
-
const resumeId = session.resumeIds[driverId] ?? null;
|
|
691
|
-
let nextResumeId = resumeId;
|
|
692
|
-
let failed = false;
|
|
693
|
-
let closing = '';
|
|
694
|
-
/* This snapshot is the enforcement point for role ownership. The workspace
|
|
695
|
-
remains readable to the agent, but changes outside its module allowlist are
|
|
696
|
-
quarantined and restored before any formatter, gate, checkpoint or delivery
|
|
697
|
-
can observe them. */
|
|
698
|
-
const pathGuard = await guardAgentPaths({
|
|
699
|
-
workspace: paths.workspace,
|
|
700
|
-
sessionRoot: paths.root,
|
|
701
|
-
allowedPaths: turnAllowedPaths,
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
for await (const event of driver.run({
|
|
706
|
-
workspacePath: paths.workspace,
|
|
707
|
-
allowedPaths: turnAllowedPaths,
|
|
708
|
-
role: roleId,
|
|
709
|
-
systemInstruction: instruction,
|
|
710
|
-
prompt: attachmentNote ? `${attachmentNote}\n\n${message}` : message,
|
|
711
|
-
resumeId,
|
|
712
|
-
history: history.slice(0, -1),
|
|
713
|
-
model: session.model,
|
|
714
|
-
signal: input.signal,
|
|
715
|
-
})) {
|
|
716
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
717
|
-
kind: eventKind(event),
|
|
718
|
-
role: roleId,
|
|
719
|
-
module: active.directory,
|
|
720
|
-
...(event.type === 'assistant.message' ? { text: event.text } : {}),
|
|
721
|
-
event,
|
|
722
|
-
});
|
|
723
|
-
if (event.type === 'assistant.message') closing = event.text;
|
|
724
|
-
if (event.type === 'turn.completed') {
|
|
725
|
-
nextResumeId = event.resumeId ?? nextResumeId;
|
|
726
|
-
failed = event.finishReason === 'error';
|
|
727
|
-
}
|
|
728
|
-
if (event.type === 'error') failed = true;
|
|
729
|
-
}
|
|
730
|
-
} catch (error) {
|
|
731
|
-
failed = true;
|
|
732
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
733
|
-
kind: 'system',
|
|
734
|
-
role: roleId,
|
|
735
|
-
module: active.directory,
|
|
736
|
-
text:
|
|
737
|
-
error instanceof Error
|
|
738
|
-
? `The coding agent stopped: ${error.message}`
|
|
739
|
-
: 'The coding agent stopped unexpectedly.',
|
|
740
|
-
event: {
|
|
741
|
-
type: 'error',
|
|
742
|
-
code: 'AGENT_TURN_FAILED',
|
|
743
|
-
message: 'The coding agent stopped unexpectedly.',
|
|
744
|
-
} as CodingAgentEvent,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const pathResult = await pathGuard.verify();
|
|
749
|
-
if (pathResult.violations.length > 0) {
|
|
750
|
-
failed = true;
|
|
751
|
-
const evidence = pathResult.violations
|
|
752
|
-
.map((violation) => `- ${violation.path}: ${violation.reason}`)
|
|
753
|
-
.join('\n');
|
|
754
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
755
|
-
kind: 'system',
|
|
756
|
-
role: roleId,
|
|
757
|
-
module: active.directory,
|
|
758
|
-
text: `This turn wrote outside its allowed paths and was failed. The sandbox restored those paths before validation.\n${evidence}${pathResult.quarantine ? `\nEvidence was quarantined at ${pathResult.quarantine}.` : ''}`,
|
|
759
|
-
event: {
|
|
760
|
-
type: 'error',
|
|
761
|
-
code: 'ALLOWED_PATHS_VIOLATION',
|
|
762
|
-
message: evidence.slice(0, 500),
|
|
763
|
-
} as CodingAgentEvent,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const scaffoldNote =
|
|
768
|
-
failed || reviewing ? null : await scaffoldFromSpec(context, session);
|
|
769
|
-
if (scaffoldNote) {
|
|
770
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
771
|
-
kind: 'system',
|
|
772
|
-
role: roleId,
|
|
773
|
-
text: scaffoldNote,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/* Prettier is deterministic and not a correctness signal, so the sandbox
|
|
778
|
-
formats the draft modules itself after each turn rather than bouncing the
|
|
779
|
-
format gate back to the agent over whitespace. */
|
|
780
|
-
if (!failed && !reviewing) {
|
|
781
|
-
const turnPaths = sessionPaths(
|
|
782
|
-
context.workspaceRoot,
|
|
783
|
-
session.id,
|
|
784
|
-
session.moduleSuffix,
|
|
785
|
-
);
|
|
786
|
-
for (const module of session.modules) {
|
|
787
|
-
await formatDirectory(
|
|
788
|
-
context.workspaceRoot,
|
|
789
|
-
modulePathOf(turnPaths, module.directory),
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const diffs = await collectDiffs(context.workspaceRoot, session);
|
|
795
|
-
const gates: GateResult[] = [];
|
|
796
|
-
if ((diffs.length > 0 || reviewing) && !failed) {
|
|
797
|
-
/* Only the modules that hold changes are gated: a module nobody touched
|
|
798
|
-
has nothing to check and its gates would cost minutes for no signal. */
|
|
799
|
-
const changed = new Set(diffs.map((diff) => diff.module));
|
|
800
|
-
const gated = session.modules.filter((module) =>
|
|
801
|
-
changed.has(module.directory),
|
|
802
|
-
);
|
|
803
|
-
/* The install runs whenever a package.json changed and counts as the
|
|
804
|
-
dependencies gate; the role's own gates follow, plus the declared
|
|
805
|
-
dependency check the session cannot do without. */
|
|
806
|
-
const install = await installGate(context, session);
|
|
807
|
-
if (install) gates.push(install);
|
|
808
|
-
if (!install || install.status !== 'failed') {
|
|
809
|
-
gates.push(
|
|
810
|
-
...(await runSessionGates(
|
|
811
|
-
context,
|
|
812
|
-
session,
|
|
813
|
-
[
|
|
814
|
-
...(reviewing
|
|
815
|
-
? GATE_IDS.filter((id) => id !== 'auto-review')
|
|
816
|
-
: role.gates),
|
|
817
|
-
...(role.gates.includes('dependencies') ? [] : ['dependencies']),
|
|
818
|
-
],
|
|
819
|
-
reviewing ? [active] : gated,
|
|
820
|
-
input.signal,
|
|
821
|
-
)),
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
if (
|
|
826
|
-
reviewing &&
|
|
827
|
-
!failed &&
|
|
828
|
-
!input.signal?.aborted &&
|
|
829
|
-
reviewRevision !== null
|
|
830
|
-
) {
|
|
831
|
-
const verified = GATE_IDS.filter((id) => id !== 'auto-review').every((id) =>
|
|
832
|
-
gates.some(
|
|
833
|
-
(gate) =>
|
|
834
|
-
gate.id === id &&
|
|
835
|
-
gate.status === 'passed' &&
|
|
836
|
-
(gate.module === active.directory ||
|
|
837
|
-
((id === 'spec-schema' || id === 'module-schema') &&
|
|
838
|
-
gate.module === undefined)),
|
|
839
|
-
),
|
|
840
|
-
);
|
|
841
|
-
if (verified && gates.every((gate) => gate.status === 'passed')) {
|
|
842
|
-
reviewPassed = await recordAutoReview(
|
|
843
|
-
paths,
|
|
844
|
-
active,
|
|
845
|
-
reviewRevision,
|
|
846
|
-
closing,
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
gates.push(
|
|
850
|
-
...(await runSessionGates(
|
|
851
|
-
context,
|
|
852
|
-
session,
|
|
853
|
-
['auto-review'],
|
|
854
|
-
[active],
|
|
855
|
-
input.signal,
|
|
856
|
-
)),
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (reviewing && !reviewPassed && !failed) {
|
|
861
|
-
const index = gates.findIndex(
|
|
862
|
-
(gate) => gate.id === 'auto-review' && gate.status === 'failed',
|
|
863
|
-
);
|
|
864
|
-
if (index >= 0 && /"verdict"\s*:\s*"fail"/.test(closing)) {
|
|
865
|
-
gates[index] = {
|
|
866
|
-
...gates[index]!,
|
|
867
|
-
output:
|
|
868
|
-
'Use $module-update to fix the findings in your preceding review. Preserve unrelated behavior, add regression tests, and finish implementation before requesting another review.',
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
const declared: HandoffDeclaration | null = closing
|
|
873
|
-
? parseHandoff(closing)
|
|
874
|
-
: null;
|
|
875
|
-
/* The turn may have written the specification, so the gate is read again:
|
|
876
|
-
what the next role may do depends on the document as it is now. */
|
|
877
|
-
const closingGate = await readSpecGate(
|
|
878
|
-
active,
|
|
879
|
-
modulePath,
|
|
880
|
-
basePathOf(paths, active.directory),
|
|
881
|
-
);
|
|
882
|
-
const handoffContext = {
|
|
883
|
-
routing: await routingContext(
|
|
884
|
-
context,
|
|
885
|
-
session,
|
|
886
|
-
paths,
|
|
887
|
-
modulePath,
|
|
888
|
-
message,
|
|
889
|
-
[],
|
|
890
|
-
closingGate.approved,
|
|
891
|
-
),
|
|
892
|
-
role: roleId,
|
|
893
|
-
module: active.directory,
|
|
894
|
-
declared,
|
|
895
|
-
gates,
|
|
896
|
-
failed,
|
|
897
|
-
changed: diffs.length > 0,
|
|
898
|
-
specApproved: closingGate.approved,
|
|
899
|
-
brief: session.brief || message,
|
|
900
|
-
reviewing,
|
|
901
|
-
};
|
|
902
|
-
let handoff = planHandoff(handoffContext);
|
|
903
|
-
if (handoff.kind === 'review') {
|
|
904
|
-
// Let intermediate specialists finish their handoffs before a full review.
|
|
905
|
-
// Every delivered module, including unchanged drafts, needs a current record.
|
|
906
|
-
const remaining = session.modules.filter(
|
|
907
|
-
(module) =>
|
|
908
|
-
!gates.some(
|
|
909
|
-
(gate) =>
|
|
910
|
-
gate.id === 'auto-review' && gate.module === module.directory,
|
|
911
|
-
),
|
|
912
|
-
);
|
|
913
|
-
if (remaining.length > 0) {
|
|
914
|
-
gates.push(
|
|
915
|
-
...(await runSessionGates(
|
|
916
|
-
context,
|
|
917
|
-
session,
|
|
918
|
-
['auto-review'],
|
|
919
|
-
remaining,
|
|
920
|
-
input.signal,
|
|
921
|
-
)),
|
|
922
|
-
);
|
|
923
|
-
handoff = planHandoff(handoffContext);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
for (const gate of gates) {
|
|
928
|
-
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
929
|
-
kind: 'system',
|
|
930
|
-
role: roleId,
|
|
931
|
-
...(gate.module ? { module: gate.module } : {}),
|
|
932
|
-
text: gateSummary(gate),
|
|
933
|
-
event: {
|
|
934
|
-
type: gate.status === 'failed' ? 'error' : 'tool.completed',
|
|
935
|
-
...(gate.status === 'failed'
|
|
936
|
-
? {
|
|
937
|
-
code: `GATE_${gate.id.toUpperCase()}`,
|
|
938
|
-
message: gate.output.slice(0, 500),
|
|
939
|
-
}
|
|
940
|
-
: { tool: `gate:${gate.id}`, detail: gate.command, ok: true }),
|
|
941
|
-
} as CodingAgentEvent,
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const handoffEntry = await appendChatEntry(context.workspaceRoot, session, {
|
|
946
|
-
kind: 'system',
|
|
947
|
-
role: handoff.role,
|
|
948
|
-
...(handoff.module ? { module: handoff.module } : {}),
|
|
949
|
-
text: handoffText(handoff),
|
|
950
|
-
handoff,
|
|
951
|
-
});
|
|
952
|
-
yield handoffEntry;
|
|
953
|
-
|
|
954
|
-
/* A turn that changed files becomes a restore point, keyed by the handoff
|
|
955
|
-
entry so the transcript line and its checkpoint share one sequence. */
|
|
956
|
-
if (diffs.length > 0) {
|
|
957
|
-
await captureCheckpoint(
|
|
958
|
-
context.workspaceRoot,
|
|
959
|
-
session,
|
|
960
|
-
handoffEntry.sequence,
|
|
961
|
-
{ label: role.name, role: roleId },
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
const updated = await updateSession(context.workspaceRoot, session.id, {
|
|
966
|
-
resumeIds: nextResumeId
|
|
967
|
-
? { ...session.resumeIds, [driverId]: nextResumeId }
|
|
968
|
-
: session.resumeIds,
|
|
969
|
-
state: failed
|
|
970
|
-
? 'failed'
|
|
971
|
-
: handoff.kind === 'approval' || handoff.kind === 'question'
|
|
972
|
-
? 'awaiting-approval'
|
|
973
|
-
: gates.some((gate) => gate.status !== 'passed')
|
|
974
|
-
? 'validating'
|
|
975
|
-
: diffs.length > 0
|
|
976
|
-
? 'previewing'
|
|
977
|
-
: 'planned',
|
|
978
|
-
role: roleId,
|
|
979
|
-
driver: driverId,
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
if (context.platform && updated.registeredWithPlatform) {
|
|
983
|
-
await context.platform
|
|
984
|
-
.updateSessionState(updated.id, updated.state)
|
|
985
|
-
.catch(() => undefined);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return { session: updated, diffs, gates, handoff };
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function eventKind(event: CodingAgentEvent): ChatEntry['kind'] {
|
|
992
|
-
return event.type === 'assistant.message' ? 'agent' : 'event';
|
|
993
|
-
}
|