@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,1473 +0,0 @@
|
|
|
1
|
-
import { sandboxDirectory } from '../config.ts';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join, relative } from 'node:path';
|
|
5
|
-
import { redactSecrets } from '@flowdular/sdk/ai-provider';
|
|
6
|
-
import type { GateResult } from '../gates.ts';
|
|
7
|
-
import {
|
|
8
|
-
basePathOf,
|
|
9
|
-
findSessionModule,
|
|
10
|
-
modulePathOf,
|
|
11
|
-
readChat,
|
|
12
|
-
sessionPaths,
|
|
13
|
-
type SandboxSession,
|
|
14
|
-
} from '../sessions.ts';
|
|
15
|
-
import { readModuleSpecReview, type ModuleSpecReview } from '../spec.ts';
|
|
16
|
-
import { SandboxSetupError } from '../workspace-root.ts';
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_DELIVERY_CONFIGURATION,
|
|
19
|
-
type DeliveryConfiguration,
|
|
20
|
-
} from './configuration.ts';
|
|
21
|
-
import {
|
|
22
|
-
GATES,
|
|
23
|
-
assertEjectCapability,
|
|
24
|
-
countChangedFiles,
|
|
25
|
-
planSessionModules,
|
|
26
|
-
} from './plan.ts';
|
|
27
|
-
import {
|
|
28
|
-
loadPathOwnership,
|
|
29
|
-
loadTaskBudgets,
|
|
30
|
-
matchesPath,
|
|
31
|
-
ownerOf,
|
|
32
|
-
type PathOwnership,
|
|
33
|
-
type TaskBudgets,
|
|
34
|
-
} from './policies.ts';
|
|
35
|
-
import {
|
|
36
|
-
createStepRecorder,
|
|
37
|
-
enableModule,
|
|
38
|
-
exists,
|
|
39
|
-
installWorktree,
|
|
40
|
-
runDeliveryGates,
|
|
41
|
-
stageModules,
|
|
42
|
-
step,
|
|
43
|
-
verifyPlatform,
|
|
44
|
-
type CommandResult,
|
|
45
|
-
type StepRecorder,
|
|
46
|
-
type StepResult,
|
|
47
|
-
} from './steps.ts';
|
|
48
|
-
import type {
|
|
49
|
-
DeliveryAvailability,
|
|
50
|
-
DeliveryBudget,
|
|
51
|
-
DeliveryContext,
|
|
52
|
-
DeliveryGuardrails,
|
|
53
|
-
DeliveryModulePlan,
|
|
54
|
-
DeliveryProvider,
|
|
55
|
-
DeliveryTarget,
|
|
56
|
-
GitDeliveryMode,
|
|
57
|
-
GitDeliveryPlan,
|
|
58
|
-
} from './types.ts';
|
|
59
|
-
|
|
60
|
-
function worktreesDirectory(root: string): string {
|
|
61
|
-
return relative(root, join(sandboxDirectory(root), 'worktrees'));
|
|
62
|
-
}
|
|
63
|
-
/* Written by module enable and pnpm install; they travel in the module's
|
|
64
|
-
commit (.ai/blueprints/new-module/allowed-paths.yaml, cliOwned). */
|
|
65
|
-
const COMPOSITION_PATHS = [
|
|
66
|
-
'flowdular.json',
|
|
67
|
-
'platform/package.json',
|
|
68
|
-
'platform/src/generated/**',
|
|
69
|
-
] as const;
|
|
70
|
-
const LOCKFILE_PATH = 'pnpm-lock.yaml';
|
|
71
|
-
const FALLBACK_BUDGET: DeliveryBudget = {
|
|
72
|
-
maxChangedFiles: 18,
|
|
73
|
-
maxNewDependencies: 0,
|
|
74
|
-
};
|
|
75
|
-
const NO_COMMITS =
|
|
76
|
-
'the repository has no commits yet; make the first commit before delivering as a pull request';
|
|
77
|
-
const OFFENDING_LIMIT = 20;
|
|
78
|
-
const GITHUB_REMOTE =
|
|
79
|
-
/^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com(?::\d+)?\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/;
|
|
80
|
-
|
|
81
|
-
const COMMAND_ENVIRONMENT_KEYS = new Set([
|
|
82
|
-
'HOME',
|
|
83
|
-
'LANG',
|
|
84
|
-
'LC_ALL',
|
|
85
|
-
'LC_CTYPE',
|
|
86
|
-
'LOGNAME',
|
|
87
|
-
'PATH',
|
|
88
|
-
'SHELL',
|
|
89
|
-
'SSH_AUTH_SOCK',
|
|
90
|
-
'SSL_CERT_DIR',
|
|
91
|
-
'SSL_CERT_FILE',
|
|
92
|
-
'TEMP',
|
|
93
|
-
'TMP',
|
|
94
|
-
'TMPDIR',
|
|
95
|
-
'USER',
|
|
96
|
-
'XDG_CACHE_HOME',
|
|
97
|
-
'XDG_CONFIG_HOME',
|
|
98
|
-
'XDG_DATA_HOME',
|
|
99
|
-
'XDG_STATE_HOME',
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
type Run = (
|
|
103
|
-
command: string,
|
|
104
|
-
args: readonly string[],
|
|
105
|
-
cwd: string,
|
|
106
|
-
) => Promise<CommandResult>;
|
|
107
|
-
|
|
108
|
-
interface ChangeSet {
|
|
109
|
-
readonly added: readonly string[];
|
|
110
|
-
readonly modified: readonly string[];
|
|
111
|
-
readonly removed: readonly string[];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface GitDestination {
|
|
115
|
-
readonly repository: string | null;
|
|
116
|
-
readonly mode: GitDeliveryMode;
|
|
117
|
-
readonly pushTarget: string;
|
|
118
|
-
readonly forkOwner: string | null;
|
|
119
|
-
readonly forkRequired: boolean;
|
|
120
|
-
readonly head: string;
|
|
121
|
-
readonly compareUrl: string | null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function sameStrings(
|
|
125
|
-
left: readonly string[],
|
|
126
|
-
right: readonly string[],
|
|
127
|
-
): boolean {
|
|
128
|
-
return (
|
|
129
|
-
left.length === right.length &&
|
|
130
|
-
left.every((value, index) => value === right[index])
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* Apply never trusts an earlier preview blindly. In particular, a spec can be
|
|
135
|
-
edited after the operator opens the plan, which invalidates its approved
|
|
136
|
-
hash. Rebuild the module plan at the last boundary before delivery and fail
|
|
137
|
-
rather than commit a different set of files from the one reviewed. */
|
|
138
|
-
function assertPlanStillCurrent(
|
|
139
|
-
planned: readonly DeliveryModulePlan[],
|
|
140
|
-
current: readonly DeliveryModulePlan[],
|
|
141
|
-
): void {
|
|
142
|
-
const same =
|
|
143
|
-
planned.length === current.length &&
|
|
144
|
-
planned.every((module, index) => {
|
|
145
|
-
const refreshed = current[index];
|
|
146
|
-
return (
|
|
147
|
-
refreshed !== undefined &&
|
|
148
|
-
module.id === refreshed.id &&
|
|
149
|
-
module.directory === refreshed.directory &&
|
|
150
|
-
module.kind === refreshed.kind &&
|
|
151
|
-
module.targetPath === refreshed.targetPath &&
|
|
152
|
-
sameStrings(module.files, refreshed.files) &&
|
|
153
|
-
sameStrings(module.additions, refreshed.additions) &&
|
|
154
|
-
sameStrings(module.overwrites, refreshed.overwrites) &&
|
|
155
|
-
sameStrings(module.removes, refreshed.removes) &&
|
|
156
|
-
sameStrings(module.newPackages, refreshed.newPackages) &&
|
|
157
|
-
module.enable === refreshed.enable
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
if (!same) {
|
|
161
|
-
throw new SandboxSetupError(
|
|
162
|
-
'EJECT_PLAN_STALE',
|
|
163
|
-
'The session changed after this delivery was reviewed. Open a new delivery plan and approve the current specification before trying again.',
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* One delivery per session at a time: two would fight over the same worktree
|
|
169
|
-
path and branch. */
|
|
170
|
-
const inFlight = new Set<string>();
|
|
171
|
-
|
|
172
|
-
/* Delivery commands need only the operator's executable path, local Git
|
|
173
|
-
identity, SSH agent, locale, temporary directory, and certificate paths.
|
|
174
|
-
An allowlist prevents unrelated database URLs and cloud credentials from
|
|
175
|
-
reaching a package script or an external provider command. */
|
|
176
|
-
function commandEnvironment(): NodeJS.ProcessEnv {
|
|
177
|
-
const env: NodeJS.ProcessEnv = {};
|
|
178
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
179
|
-
if (value === undefined || !COMMAND_ENVIRONMENT_KEYS.has(key)) continue;
|
|
180
|
-
env[key] = value;
|
|
181
|
-
}
|
|
182
|
-
Object.assign(env, {
|
|
183
|
-
FORCE_COLOR: '0',
|
|
184
|
-
NO_COLOR: '1',
|
|
185
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
186
|
-
GH_PROMPT_DISABLED: '1',
|
|
187
|
-
GH_NO_UPDATE_NOTIFIER: '1',
|
|
188
|
-
});
|
|
189
|
-
return env;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function githubEnvironment(providerToken: string | null): NodeJS.ProcessEnv {
|
|
193
|
-
const env = commandEnvironment();
|
|
194
|
-
if (providerToken) env.GH_TOKEN = providerToken;
|
|
195
|
-
return env;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function gitEnvironment(providerToken: string | null): NodeJS.ProcessEnv {
|
|
199
|
-
const env = commandEnvironment();
|
|
200
|
-
if (!providerToken) return env;
|
|
201
|
-
/* Keep the credential out of command arguments and remote URLs. Git reads
|
|
202
|
-
this process-local config only for the delivery child process. */
|
|
203
|
-
env.GIT_CONFIG_COUNT = '1';
|
|
204
|
-
env.GIT_CONFIG_KEY_0 = 'http.https://github.com/.extraheader';
|
|
205
|
-
env.GIT_CONFIG_VALUE_0 = `AUTHORIZATION: basic ${Buffer.from(
|
|
206
|
-
`x-access-token:${providerToken}`,
|
|
207
|
-
).toString('base64')}`;
|
|
208
|
-
return env;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function safeExternalText(value: string, providerToken: string | null): string {
|
|
212
|
-
let safe = redactSecrets(value)
|
|
213
|
-
.replace(/\bgithub_pat_[A-Za-z0-9_]{8,}/gi, '[redacted]')
|
|
214
|
-
.replace(/\bgh[pousr]_[A-Za-z0-9_]{8,}/gi, '[redacted]')
|
|
215
|
-
.replace(/https:\/\/[^\s/@]+:[^\s/@]+@/gi, 'https://[redacted]@');
|
|
216
|
-
if (providerToken) {
|
|
217
|
-
safe = safe.split(providerToken).join('[redacted]');
|
|
218
|
-
const header = Buffer.from(`x-access-token:${providerToken}`).toString(
|
|
219
|
-
'base64',
|
|
220
|
-
);
|
|
221
|
-
safe = safe.split(header).join('[redacted]');
|
|
222
|
-
}
|
|
223
|
-
return safe;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function gitDeliveryRunner(
|
|
227
|
-
context: DeliveryContext,
|
|
228
|
-
): Promise<Run> {
|
|
229
|
-
const token = context.gitProviderToken
|
|
230
|
-
? await context.gitProviderToken()
|
|
231
|
-
: null;
|
|
232
|
-
const normalEnv = commandEnvironment();
|
|
233
|
-
const ghEnv = githubEnvironment(token);
|
|
234
|
-
const gitEnv = gitEnvironment(token);
|
|
235
|
-
return async (command, args, cwd) => {
|
|
236
|
-
const result = await context.commands(command, args, cwd, {
|
|
237
|
-
env: command === 'git' ? gitEnv : command === 'gh' ? ghEnv : normalEnv,
|
|
238
|
-
});
|
|
239
|
-
return { ...result, output: safeExternalText(result.output, token) };
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function unavailable(reason: string): DeliveryAvailability {
|
|
244
|
-
return { available: false, reason };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function branchNameFor(
|
|
248
|
-
config: DeliveryConfiguration,
|
|
249
|
-
session: SandboxSession,
|
|
250
|
-
): string {
|
|
251
|
-
return `${config.git.branchPrefix}/${session.moduleSuffix}-${session.id.slice(0, 8)}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function budgetFor(
|
|
255
|
-
config: DeliveryConfiguration,
|
|
256
|
-
budgets: TaskBudgets,
|
|
257
|
-
kind: string,
|
|
258
|
-
): DeliveryBudget {
|
|
259
|
-
const override = budgets.overrides[kind] ?? {};
|
|
260
|
-
return {
|
|
261
|
-
maxChangedFiles:
|
|
262
|
-
config.maxChangedFiles ??
|
|
263
|
-
override.maxChangedFiles ??
|
|
264
|
-
budgets.defaults.maxChangedFiles ??
|
|
265
|
-
FALLBACK_BUDGET.maxChangedFiles,
|
|
266
|
-
maxNewDependencies:
|
|
267
|
-
override.maxNewDependencies ??
|
|
268
|
-
budgets.defaults.maxNewDependencies ??
|
|
269
|
-
FALLBACK_BUDGET.maxNewDependencies,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function ownersOf(
|
|
274
|
-
ownership: PathOwnership,
|
|
275
|
-
paths: readonly string[],
|
|
276
|
-
): readonly string[] {
|
|
277
|
-
const owners = new Set<string>();
|
|
278
|
-
for (const path of paths) {
|
|
279
|
-
const owner = ownerOf(path, ownership);
|
|
280
|
-
if (owner) owners.add(owner);
|
|
281
|
-
}
|
|
282
|
-
return [...owners].sort();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function listSome(items: readonly string[]): string {
|
|
286
|
-
const shown = items.slice(0, OFFENDING_LIMIT).join(', ');
|
|
287
|
-
return items.length > OFFENDING_LIMIT
|
|
288
|
-
? `${shown} and ${items.length - OFFENDING_LIMIT} more`
|
|
289
|
-
: shown;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function evaluateGuardrails(input: {
|
|
293
|
-
readonly offending: readonly string[];
|
|
294
|
-
readonly changedFiles: number;
|
|
295
|
-
readonly newDependencies: readonly string[];
|
|
296
|
-
readonly budget: DeliveryBudget;
|
|
297
|
-
}): DeliveryGuardrails {
|
|
298
|
-
const reasons: string[] = [];
|
|
299
|
-
if (input.offending.length > 0) {
|
|
300
|
-
reasons.push(
|
|
301
|
-
`${input.offending.length} changed file(s) lie outside the allowed paths: ${listSome(input.offending)}.`,
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
if (input.changedFiles > input.budget.maxChangedFiles) {
|
|
305
|
-
reasons.push(
|
|
306
|
-
`${input.changedFiles} changed files exceed the budget of ${input.budget.maxChangedFiles} (sandbox.delivery.maxChangedFiles in flowdular.json, else .ai/policies/task-budgets.yaml).`,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (input.newDependencies.length > input.budget.maxNewDependencies) {
|
|
310
|
-
reasons.push(
|
|
311
|
-
`${input.newDependencies.length} new package(s) (${listSome(input.newDependencies)}) exceed the budget of ${input.budget.maxNewDependencies} from .ai/policies/task-budgets.yaml.`,
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
return { ok: reasons.length === 0, reasons };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function githubRepository(remoteUrl: string): string | null {
|
|
318
|
-
const match = GITHUB_REMOTE.exec(remoteUrl.trim());
|
|
319
|
-
return match ? `${match[1]}/${match[2]}` : null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function compareUrlFor(
|
|
323
|
-
repository: string,
|
|
324
|
-
baseBranch: string,
|
|
325
|
-
head: string,
|
|
326
|
-
): string {
|
|
327
|
-
return `https://github.com/${repository}/compare/${baseBranch}...${head}?expand=1`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function githubRepositoryName(repository: string): string {
|
|
331
|
-
return repository.slice(repository.indexOf('/') + 1);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function githubPushUrl(owner: string, repository: string): string {
|
|
335
|
-
return `https://github.com/${owner}/${githubRepositoryName(repository)}.git`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function outputValue(
|
|
339
|
-
run: Run,
|
|
340
|
-
command: string,
|
|
341
|
-
args: readonly string[],
|
|
342
|
-
cwd: string,
|
|
343
|
-
): Promise<string | null> {
|
|
344
|
-
const result = await run(command, args, cwd);
|
|
345
|
-
return result.code === 0 && result.output.trim() !== ''
|
|
346
|
-
? result.output.trim()
|
|
347
|
-
: null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function assertWorktreeDirectoryIgnored(
|
|
351
|
-
run: Run,
|
|
352
|
-
cwd: string,
|
|
353
|
-
): Promise<void> {
|
|
354
|
-
const ignored = await run(
|
|
355
|
-
'git',
|
|
356
|
-
[
|
|
357
|
-
'check-ignore',
|
|
358
|
-
'--quiet',
|
|
359
|
-
'--no-index',
|
|
360
|
-
`${worktreesDirectory(cwd)}/.probe`,
|
|
361
|
-
],
|
|
362
|
-
cwd,
|
|
363
|
-
);
|
|
364
|
-
if (ignored.code !== 0) {
|
|
365
|
-
throw new SandboxSetupError(
|
|
366
|
-
'GIT_WORKTREE_DIRECTORY_NOT_IGNORED',
|
|
367
|
-
`The ${worktreesDirectory(cwd)} directory is not ignored by Git. Add .flowdular/ to .gitignore before pull request delivery so the active checkout stays unchanged.`,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async function resolveDestination(
|
|
373
|
-
run: Run,
|
|
374
|
-
config: DeliveryConfiguration,
|
|
375
|
-
cwd: string,
|
|
376
|
-
remoteUrl: string,
|
|
377
|
-
provider: DeliveryProvider,
|
|
378
|
-
branch: string,
|
|
379
|
-
): Promise<GitDestination> {
|
|
380
|
-
const detected = githubRepository(remoteUrl);
|
|
381
|
-
const repository = config.git.repository ?? detected;
|
|
382
|
-
if (
|
|
383
|
-
config.git.repository !== null &&
|
|
384
|
-
detected !== null &&
|
|
385
|
-
config.git.repository.toLowerCase() !== detected.toLowerCase()
|
|
386
|
-
) {
|
|
387
|
-
throw new SandboxSetupError(
|
|
388
|
-
'GITHUB_REPOSITORY_MISMATCH',
|
|
389
|
-
`The ${config.git.remote} remote points to ${detected}, not the configured ${config.git.repository}. Choose the matching remote before delivering.`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
let mode: GitDeliveryMode = 'direct';
|
|
393
|
-
if (config.git.mode === 'fork') mode = 'fork';
|
|
394
|
-
if (config.git.mode === 'auto' && repository && provider === 'github') {
|
|
395
|
-
const permission = await outputValue(
|
|
396
|
-
run,
|
|
397
|
-
'gh',
|
|
398
|
-
['api', `repos/${repository}`, '--jq', '.permissions.push // false'],
|
|
399
|
-
cwd,
|
|
400
|
-
);
|
|
401
|
-
if (permission !== 'true') {
|
|
402
|
-
throw new SandboxSetupError(
|
|
403
|
-
'GITHUB_FORK_OPT_IN_REQUIRED',
|
|
404
|
-
'GitHub did not confirm direct push access. Choose Use a fork to create or use a fork, or choose Direct to attempt the repository push explicitly.',
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (mode === 'fork') {
|
|
409
|
-
if (provider !== 'github' || repository === null) {
|
|
410
|
-
throw new SandboxSetupError(
|
|
411
|
-
'GITHUB_FORK_UNAVAILABLE',
|
|
412
|
-
'Fork delivery needs a GitHub repository and an authenticated GitHub integration.',
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
const forkOwner =
|
|
416
|
-
config.git.forkOwner ??
|
|
417
|
-
(await outputValue(run, 'gh', ['api', 'user', '--jq', '.login'], cwd));
|
|
418
|
-
if (!forkOwner) {
|
|
419
|
-
throw new SandboxSetupError(
|
|
420
|
-
'GITHUB_FORK_OWNER_MISSING',
|
|
421
|
-
'GitHub did not return the account that should own the fork.',
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
const forkRepository = `${forkOwner}/${githubRepositoryName(repository)}`;
|
|
425
|
-
const existing = await run(
|
|
426
|
-
'gh',
|
|
427
|
-
[
|
|
428
|
-
'repo',
|
|
429
|
-
'view',
|
|
430
|
-
forkRepository,
|
|
431
|
-
'--json',
|
|
432
|
-
'nameWithOwner,parent',
|
|
433
|
-
'--jq',
|
|
434
|
-
'[.nameWithOwner, (.parent.nameWithOwner // "")] | @tsv',
|
|
435
|
-
],
|
|
436
|
-
cwd,
|
|
437
|
-
);
|
|
438
|
-
if (existing.code === 0) {
|
|
439
|
-
const [, parent = ''] = existing.output.trim().split('\t');
|
|
440
|
-
if (parent.toLowerCase() !== repository.toLowerCase()) {
|
|
441
|
-
throw new SandboxSetupError(
|
|
442
|
-
'GITHUB_FORK_MISMATCH',
|
|
443
|
-
`${forkRepository} exists but is not a fork of ${repository}. Choose another fork owner or use direct delivery.`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
const head = `${forkOwner}:${branch}`;
|
|
448
|
-
return {
|
|
449
|
-
repository,
|
|
450
|
-
mode,
|
|
451
|
-
pushTarget: githubPushUrl(forkOwner, repository),
|
|
452
|
-
forkOwner,
|
|
453
|
-
forkRequired: existing.code !== 0,
|
|
454
|
-
head,
|
|
455
|
-
compareUrl: compareUrlFor(repository, config.git.baseBranch, head),
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
repository,
|
|
460
|
-
mode,
|
|
461
|
-
pushTarget: config.git.remote,
|
|
462
|
-
forkOwner: null,
|
|
463
|
-
forkRequired: false,
|
|
464
|
-
head: branch,
|
|
465
|
-
compareUrl: repository
|
|
466
|
-
? compareUrlFor(repository, config.git.baseBranch, branch)
|
|
467
|
-
: null,
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function probeProvider(
|
|
472
|
-
run: Run,
|
|
473
|
-
config: DeliveryConfiguration,
|
|
474
|
-
cwd: string,
|
|
475
|
-
): Promise<{ readonly kind: DeliveryProvider; readonly note: string }> {
|
|
476
|
-
if (config.git.provider === 'none') {
|
|
477
|
-
return {
|
|
478
|
-
kind: 'none',
|
|
479
|
-
note: 'Pull requests are turned off (sandbox.delivery.git.provider is none); the branch is pushed and the compare link shown.',
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
const status = await run('gh', ['auth', 'status'], cwd);
|
|
483
|
-
if (status.code === 0) return { kind: 'github', note: '' };
|
|
484
|
-
return {
|
|
485
|
-
kind: 'none',
|
|
486
|
-
note:
|
|
487
|
-
status.code === null
|
|
488
|
-
? 'The gh command is not installed. The branch is pushed and a compare link is shown instead of opening a pull request.'
|
|
489
|
-
: 'gh is not signed in. The branch is pushed and a compare link is shown instead of opening a pull request.',
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function porcelainPaths(output: string): readonly string[] {
|
|
494
|
-
const paths: string[] = [];
|
|
495
|
-
for (const line of output.split('\n')) {
|
|
496
|
-
if (line.length < 4) continue;
|
|
497
|
-
let path = line.slice(3);
|
|
498
|
-
const renamed = path.indexOf(' -> ');
|
|
499
|
-
if (renamed >= 0) path = path.slice(renamed + 4);
|
|
500
|
-
if (path.startsWith('"') && path.endsWith('"')) path = path.slice(1, -1);
|
|
501
|
-
paths.push(path);
|
|
502
|
-
}
|
|
503
|
-
return paths;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/* `modules/<dir>/**` becomes the directory pathspec git understands. */
|
|
507
|
-
function pathspecOf(pattern: string): string {
|
|
508
|
-
return pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async function stagedChanges(run: Run, worktree: string): Promise<ChangeSet> {
|
|
512
|
-
const diff = await run(
|
|
513
|
-
'git',
|
|
514
|
-
['diff', '--cached', '--name-status', '--no-renames'],
|
|
515
|
-
worktree,
|
|
516
|
-
);
|
|
517
|
-
const added: string[] = [];
|
|
518
|
-
const modified: string[] = [];
|
|
519
|
-
const removed: string[] = [];
|
|
520
|
-
for (const line of diff.output.split('\n')) {
|
|
521
|
-
const [status, path] = line.split('\t');
|
|
522
|
-
if (!status || !path) continue;
|
|
523
|
-
if (status.startsWith('A')) added.push(path);
|
|
524
|
-
else if (status.startsWith('D')) removed.push(path);
|
|
525
|
-
else modified.push(path);
|
|
526
|
-
}
|
|
527
|
-
return { added, modified, removed };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
async function withTemporaryFile<T>(
|
|
531
|
-
content: string,
|
|
532
|
-
use: (path: string) => Promise<T>,
|
|
533
|
-
): Promise<T> {
|
|
534
|
-
const directory = await mkdtemp(join(tmpdir(), 'flowdular-delivery-'));
|
|
535
|
-
try {
|
|
536
|
-
const path = join(directory, 'message.md');
|
|
537
|
-
await writeFile(path, content, 'utf8');
|
|
538
|
-
return await use(path);
|
|
539
|
-
} finally {
|
|
540
|
-
await rm(directory, { recursive: true, force: true });
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function urlIn(output: string): string | null {
|
|
545
|
-
const matches = output.match(/https?:\/\/\S+/g);
|
|
546
|
-
return matches?.at(-1) ?? null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/* "add a.core and update b.core", in the tense the caller needs. */
|
|
550
|
-
function describeChange(
|
|
551
|
-
modules: readonly DeliveryModulePlan[],
|
|
552
|
-
tense: 'add' | 'adds',
|
|
553
|
-
): string {
|
|
554
|
-
const added = modules.filter((module) => module.kind === 'new');
|
|
555
|
-
const updated = modules.filter((module) => module.kind === 'edit');
|
|
556
|
-
const parts: string[] = [];
|
|
557
|
-
if (added.length > 0) {
|
|
558
|
-
parts.push(
|
|
559
|
-
`${tense === 'adds' ? 'adds' : 'add'} ${added.map((module) => module.id).join(', ')}`,
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
if (updated.length > 0) {
|
|
563
|
-
parts.push(
|
|
564
|
-
`${tense === 'adds' ? 'updates' : 'update'} ${updated.map((module) => module.id).join(', ')}`,
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
return parts.join(' and ');
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function capitalize(text: string): string {
|
|
571
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function pullRequestTitle(modules: readonly DeliveryModulePlan[]): string {
|
|
575
|
-
const complete = capitalize(describeChange(modules, 'add'));
|
|
576
|
-
if (complete.length <= 70) return complete;
|
|
577
|
-
const primary = modules[0]!.id;
|
|
578
|
-
return modules.length === 1
|
|
579
|
-
? `${primary}: sandbox delivery`.slice(0, 70)
|
|
580
|
-
: `${primary}: update ${modules.length} modules`.slice(0, 70);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/* Plain sentences from free text: whitespace collapsed, dashes the repository
|
|
584
|
-
rules forbid replaced, cut to a sentence count and a length. */
|
|
585
|
-
function sentences(text: string, count: number, limit: number): string {
|
|
586
|
-
const clean = safeExternalText(text, null)
|
|
587
|
-
.replace(
|
|
588
|
-
/\b(token|password|secret|api[ _-]?key)\s*[:=]\s*[^\s,;]+/gi,
|
|
589
|
-
'$1: [redacted]',
|
|
590
|
-
)
|
|
591
|
-
.replace(/\s+/g, ' ')
|
|
592
|
-
.replace(/\s*\u2014\s*/g, ', ')
|
|
593
|
-
.replace(/\u2013/g, '-')
|
|
594
|
-
.trim();
|
|
595
|
-
if (clean === '') return '';
|
|
596
|
-
let picked = clean
|
|
597
|
-
.split(/(?<=[.!?])\s+/)
|
|
598
|
-
.slice(0, count)
|
|
599
|
-
.join(' ');
|
|
600
|
-
if (picked.length > limit)
|
|
601
|
-
picked = `${picked.slice(0, limit - 1).trimEnd()}.`;
|
|
602
|
-
return /[.!?]$/.test(picked) ? picked : `${picked}.`;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function gateSummary(gates: readonly GateResult[]): string {
|
|
606
|
-
const modulesByGate = new Map<string, string[]>();
|
|
607
|
-
for (const gate of gates) {
|
|
608
|
-
const modules = modulesByGate.get(gate.id) ?? [];
|
|
609
|
-
if (gate.module) modules.push(gate.module);
|
|
610
|
-
modulesByGate.set(gate.id, modules);
|
|
611
|
-
}
|
|
612
|
-
return [...modulesByGate]
|
|
613
|
-
.map(([id, modules]) =>
|
|
614
|
-
modules.length > 0 ? `${id} (${modules.join(', ')})` : id,
|
|
615
|
-
)
|
|
616
|
-
.join(', ');
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function commitMessage(
|
|
620
|
-
session: SandboxSession,
|
|
621
|
-
modules: readonly DeliveryModulePlan[],
|
|
622
|
-
gates: readonly GateResult[],
|
|
623
|
-
): string {
|
|
624
|
-
return `sandbox: ${describeChange(modules, 'add')}\n\nSession ${session.id}.\nGates passed: ${gateSummary(gates)}.\n`;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
async function lastReviewHandoff(
|
|
628
|
-
workspaceRoot: string,
|
|
629
|
-
session: SandboxSession,
|
|
630
|
-
): Promise<string | null> {
|
|
631
|
-
const entries = await readChat(workspaceRoot, session);
|
|
632
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
633
|
-
const handoff = entries[index]!.handoff;
|
|
634
|
-
if (handoff && (handoff.kind === 'review' || handoff.kind === 'approval')) {
|
|
635
|
-
return handoff.reason;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function pullRequestBody(input: {
|
|
642
|
-
readonly session: SandboxSession;
|
|
643
|
-
readonly modules: readonly DeliveryModulePlan[];
|
|
644
|
-
readonly gates: readonly GateResult[];
|
|
645
|
-
readonly changes: ChangeSet;
|
|
646
|
-
readonly owners: readonly string[];
|
|
647
|
-
readonly requireReviewer: boolean;
|
|
648
|
-
readonly handoff: string | null;
|
|
649
|
-
readonly specs: readonly ModuleSpecReview[];
|
|
650
|
-
}): string {
|
|
651
|
-
const testGates = input.gates.filter((gate) => gate.id === 'tests');
|
|
652
|
-
const summary = [
|
|
653
|
-
`${capitalize(describeChange(input.modules, 'adds'))}.`,
|
|
654
|
-
sentences(input.session.brief, 2, 400),
|
|
655
|
-
input.handoff ? sentences(input.handoff, 1, 300) : '',
|
|
656
|
-
]
|
|
657
|
-
.filter((part) => part !== '')
|
|
658
|
-
.join(' ');
|
|
659
|
-
const lines = [
|
|
660
|
-
summary,
|
|
661
|
-
'',
|
|
662
|
-
`Session ${input.session.id}.`,
|
|
663
|
-
'',
|
|
664
|
-
'## Specification',
|
|
665
|
-
'',
|
|
666
|
-
...input.specs.flatMap((spec) => {
|
|
667
|
-
const before = spec.base?.specVersion ?? 'new';
|
|
668
|
-
const after = spec.draft?.specVersion ?? 'unknown';
|
|
669
|
-
const changes = spec.changes.slice(0, 20).map((change) => {
|
|
670
|
-
const key = change.key ? ` ${change.key}` : '';
|
|
671
|
-
return `- ${change.kind}: ${change.field}${key}`;
|
|
672
|
-
});
|
|
673
|
-
return [
|
|
674
|
-
`### ${spec.moduleId}`,
|
|
675
|
-
'',
|
|
676
|
-
`Version: ${before} to ${after}. Status: ${spec.status ?? 'missing'}.`,
|
|
677
|
-
...(changes.length > 0 ? changes : ['- No field-level spec diff.']),
|
|
678
|
-
...(spec.changes.length > changes.length
|
|
679
|
-
? [`- ${spec.changes.length - changes.length} more spec changes.`]
|
|
680
|
-
: []),
|
|
681
|
-
'',
|
|
682
|
-
];
|
|
683
|
-
}),
|
|
684
|
-
'## Gates',
|
|
685
|
-
'',
|
|
686
|
-
'| Gate | Module | Result |',
|
|
687
|
-
'| --- | --- | --- |',
|
|
688
|
-
...input.gates.map(
|
|
689
|
-
(gate) => `| ${gate.id} | ${gate.module ?? ''} | ${gate.status} |`,
|
|
690
|
-
),
|
|
691
|
-
'',
|
|
692
|
-
`Verification: ${testGates.length} module test gate(s) passed. The platform typecheck passed in the delivery worktree.`,
|
|
693
|
-
'',
|
|
694
|
-
];
|
|
695
|
-
const groups: readonly [string, readonly string[]][] = [
|
|
696
|
-
['Added', input.changes.added],
|
|
697
|
-
['Modified', input.changes.modified],
|
|
698
|
-
['Removed', input.changes.removed],
|
|
699
|
-
];
|
|
700
|
-
lines.push('## Files', '');
|
|
701
|
-
for (const [label, files] of groups) {
|
|
702
|
-
if (files.length === 0) continue;
|
|
703
|
-
lines.push(`${label} (${files.length}):`);
|
|
704
|
-
lines.push(...files.map((file) => `- ${file}`));
|
|
705
|
-
lines.push('');
|
|
706
|
-
}
|
|
707
|
-
lines.push('## Risks', '');
|
|
708
|
-
const migrations = [...input.changes.added, ...input.changes.modified].filter(
|
|
709
|
-
(file) => /\/migrations\//.test(file),
|
|
710
|
-
);
|
|
711
|
-
const removals = input.changes.removed.length;
|
|
712
|
-
const riskLines = [
|
|
713
|
-
...(migrations.length > 0
|
|
714
|
-
? [`- ${migrations.length} migration file(s) require deployment review.`]
|
|
715
|
-
: []),
|
|
716
|
-
...(removals > 0
|
|
717
|
-
? [`- ${removals} file(s) are removed by this change.`]
|
|
718
|
-
: []),
|
|
719
|
-
...(input.requireReviewer && input.owners.length > 1
|
|
720
|
-
? [`- Cross-owner review is required for ${input.owners.join(', ')}.`]
|
|
721
|
-
: []),
|
|
722
|
-
];
|
|
723
|
-
lines.push(
|
|
724
|
-
...(riskLines.length > 0
|
|
725
|
-
? riskLines
|
|
726
|
-
: [
|
|
727
|
-
'- No elevated delivery risks were detected by the sandbox guardrails.',
|
|
728
|
-
]),
|
|
729
|
-
'',
|
|
730
|
-
);
|
|
731
|
-
for (const module of input.modules) {
|
|
732
|
-
lines.push(
|
|
733
|
-
`Post-merge: \`pnpm flowdular auth sync-scopes --module ${module.id} --apply\` against the deployment database.`,
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
if (input.requireReviewer && input.owners.length > 1) {
|
|
737
|
-
lines.push(
|
|
738
|
-
'',
|
|
739
|
-
`Cross-owner change (${input.owners.join(', ')}): .ai/policies/path-ownership.yaml asks for a reviewer from each owner.`,
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
return `${lines.join('\n')}\n`;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
async function specificationEvidence(
|
|
746
|
-
workspaceRoot: string,
|
|
747
|
-
session: SandboxSession,
|
|
748
|
-
): Promise<readonly ModuleSpecReview[]> {
|
|
749
|
-
const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
|
|
750
|
-
return Promise.all(
|
|
751
|
-
session.modules.map((entry) => {
|
|
752
|
-
const module = findSessionModule(session, entry.directory);
|
|
753
|
-
return readModuleSpecReview(
|
|
754
|
-
module,
|
|
755
|
-
modulePathOf(paths, module.directory),
|
|
756
|
-
basePathOf(paths, module.directory),
|
|
757
|
-
);
|
|
758
|
-
}),
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
async function removeWorktree(
|
|
763
|
-
run: Run,
|
|
764
|
-
root: string,
|
|
765
|
-
worktree: string,
|
|
766
|
-
): Promise<StepResult> {
|
|
767
|
-
const removed = await run(
|
|
768
|
-
'git',
|
|
769
|
-
['worktree', 'remove', '--force', worktree],
|
|
770
|
-
root,
|
|
771
|
-
);
|
|
772
|
-
await rm(worktree, { recursive: true, force: true }).catch(() => undefined);
|
|
773
|
-
await run('git', ['worktree', 'prune'], root);
|
|
774
|
-
const gone = !(await exists(worktree));
|
|
775
|
-
return {
|
|
776
|
-
ok: gone,
|
|
777
|
-
output: gone
|
|
778
|
-
? ''
|
|
779
|
-
: `The worktree at ${worktree} is still there: ${removed.output}`,
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function pushDestination(git: GitDeliveryPlan): string {
|
|
784
|
-
if (git.deliveryMode === 'fork') {
|
|
785
|
-
return githubPushUrl(git.forkOwner!, git.repository!);
|
|
786
|
-
}
|
|
787
|
-
return git.remote;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
async function remoteBranchOwner(
|
|
791
|
-
run: Run,
|
|
792
|
-
cwd: string,
|
|
793
|
-
destination: string,
|
|
794
|
-
branch: string,
|
|
795
|
-
): Promise<{
|
|
796
|
-
readonly exists: boolean;
|
|
797
|
-
readonly message: string;
|
|
798
|
-
readonly commit: string | null;
|
|
799
|
-
}> {
|
|
800
|
-
const listed = await run(
|
|
801
|
-
'git',
|
|
802
|
-
['ls-remote', '--heads', destination, `refs/heads/${branch}`],
|
|
803
|
-
cwd,
|
|
804
|
-
);
|
|
805
|
-
if (listed.code !== 0) {
|
|
806
|
-
throw new SandboxSetupError(
|
|
807
|
-
'GIT_REMOTE_UNREACHABLE',
|
|
808
|
-
`The push repository could not be checked before delivery. ${listed.output.trim()}`.trim(),
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
const commit = listed.output.trim().split(/\s+/)[0];
|
|
812
|
-
if (!commit) return { exists: false, message: '', commit: null };
|
|
813
|
-
const fetched = await run(
|
|
814
|
-
'git',
|
|
815
|
-
['fetch', '--quiet', destination, branch],
|
|
816
|
-
cwd,
|
|
817
|
-
);
|
|
818
|
-
if (fetched.code !== 0) {
|
|
819
|
-
throw new SandboxSetupError(
|
|
820
|
-
'GIT_BRANCH_CHECK_FAILED',
|
|
821
|
-
`The existing ${branch} branch could not be inspected.`,
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
const message = await outputValue(
|
|
825
|
-
run,
|
|
826
|
-
'git',
|
|
827
|
-
['show', '-s', '--format=%B', 'FETCH_HEAD'],
|
|
828
|
-
cwd,
|
|
829
|
-
);
|
|
830
|
-
return { exists: true, message: message ?? '', commit };
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function assertSessionBranch(
|
|
834
|
-
branch: string,
|
|
835
|
-
message: string,
|
|
836
|
-
sessionId: string,
|
|
837
|
-
): void {
|
|
838
|
-
if (message.includes(`Session ${sessionId}.`)) return;
|
|
839
|
-
throw new SandboxSetupError(
|
|
840
|
-
'GIT_BRANCH_CONFLICT',
|
|
841
|
-
`The ${branch} branch already exists but was not created for this sandbox session. Choose another branch prefix or remove the conflicting branch yourself.`,
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
async function openPullRequest(
|
|
846
|
-
run: Run,
|
|
847
|
-
worktree: string,
|
|
848
|
-
git: GitDeliveryPlan,
|
|
849
|
-
repository: string | null,
|
|
850
|
-
title: string,
|
|
851
|
-
body: string,
|
|
852
|
-
recorder: StepRecorder,
|
|
853
|
-
): Promise<string | null> {
|
|
854
|
-
const repositoryArgs = repository ? ['--repo', repository] : [];
|
|
855
|
-
const existing = await run(
|
|
856
|
-
'gh',
|
|
857
|
-
[
|
|
858
|
-
'pr',
|
|
859
|
-
'list',
|
|
860
|
-
...repositoryArgs,
|
|
861
|
-
'--head',
|
|
862
|
-
git.branch,
|
|
863
|
-
'--state',
|
|
864
|
-
'open',
|
|
865
|
-
'--limit',
|
|
866
|
-
'1',
|
|
867
|
-
'--json',
|
|
868
|
-
'url,headRepositoryOwner',
|
|
869
|
-
'--jq',
|
|
870
|
-
git.deliveryMode === 'fork'
|
|
871
|
-
? `.[] | select(.headRepositoryOwner.login == ${JSON.stringify(git.forkOwner)}) | .url`
|
|
872
|
-
: '.[0].url',
|
|
873
|
-
],
|
|
874
|
-
worktree,
|
|
875
|
-
);
|
|
876
|
-
const existingUrl = existing.code === 0 ? urlIn(existing.output) : null;
|
|
877
|
-
if (existingUrl) {
|
|
878
|
-
recorder.record('pr', {
|
|
879
|
-
ok: true,
|
|
880
|
-
output: `A pull request for ${git.branch} is already open; the push updated it.`,
|
|
881
|
-
detail: existingUrl,
|
|
882
|
-
});
|
|
883
|
-
return existingUrl;
|
|
884
|
-
}
|
|
885
|
-
const created = await withTemporaryFile(body, (file) =>
|
|
886
|
-
run(
|
|
887
|
-
'gh',
|
|
888
|
-
[
|
|
889
|
-
'pr',
|
|
890
|
-
'create',
|
|
891
|
-
...repositoryArgs,
|
|
892
|
-
'--base',
|
|
893
|
-
git.baseBranch,
|
|
894
|
-
'--head',
|
|
895
|
-
git.head,
|
|
896
|
-
'--title',
|
|
897
|
-
title,
|
|
898
|
-
'--body-file',
|
|
899
|
-
file,
|
|
900
|
-
...(git.reviewers.length > 0
|
|
901
|
-
? ['--reviewer', git.reviewers.join(',')]
|
|
902
|
-
: []),
|
|
903
|
-
],
|
|
904
|
-
worktree,
|
|
905
|
-
),
|
|
906
|
-
);
|
|
907
|
-
const url = created.code === 0 ? urlIn(created.output) : null;
|
|
908
|
-
recorder.record('pr', {
|
|
909
|
-
ok: url !== null,
|
|
910
|
-
output: created.output,
|
|
911
|
-
detail: url ?? '',
|
|
912
|
-
});
|
|
913
|
-
return url;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/* The change lands as a commit on a session branch, built in a separate
|
|
917
|
-
worktree so the operator's working tree and index are never touched, and
|
|
918
|
-
is pushed with a pull request when a provider is usable. */
|
|
919
|
-
export function createGitPullRequestDeliveryTarget(): DeliveryTarget {
|
|
920
|
-
return {
|
|
921
|
-
id: 'git-pr',
|
|
922
|
-
|
|
923
|
-
available: async (context) => {
|
|
924
|
-
const config = context.delivery ?? DEFAULT_DELIVERY_CONFIGURATION;
|
|
925
|
-
const root = context.workspaceRoot;
|
|
926
|
-
const run = await gitDeliveryRunner(context);
|
|
927
|
-
const head = await run(
|
|
928
|
-
'git',
|
|
929
|
-
['rev-parse', '--verify', '--quiet', 'HEAD'],
|
|
930
|
-
root,
|
|
931
|
-
);
|
|
932
|
-
if (head.code === null) {
|
|
933
|
-
return unavailable('git is not installed or not on PATH.');
|
|
934
|
-
}
|
|
935
|
-
if (head.code !== 0) {
|
|
936
|
-
const inside = await run(
|
|
937
|
-
'git',
|
|
938
|
-
['rev-parse', '--is-inside-work-tree'],
|
|
939
|
-
root,
|
|
940
|
-
);
|
|
941
|
-
return unavailable(
|
|
942
|
-
inside.output.trim() === 'true'
|
|
943
|
-
? NO_COMMITS
|
|
944
|
-
: 'this workspace is not inside a git work tree.',
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
try {
|
|
948
|
-
await assertWorktreeDirectoryIgnored(run, root);
|
|
949
|
-
} catch (error) {
|
|
950
|
-
return unavailable(
|
|
951
|
-
error instanceof Error
|
|
952
|
-
? error.message
|
|
953
|
-
: 'the delivery worktree directory is not ignored by Git.',
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
const remote = await run(
|
|
957
|
-
'git',
|
|
958
|
-
['remote', 'get-url', config.git.remote],
|
|
959
|
-
root,
|
|
960
|
-
);
|
|
961
|
-
if (remote.code !== 0) {
|
|
962
|
-
return unavailable(
|
|
963
|
-
`the ${config.git.remote} remote is not configured; add it with git remote add ${config.git.remote} <url>.`,
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
const base = await run(
|
|
967
|
-
'git',
|
|
968
|
-
[
|
|
969
|
-
'ls-remote',
|
|
970
|
-
'--exit-code',
|
|
971
|
-
'--heads',
|
|
972
|
-
config.git.remote,
|
|
973
|
-
`refs/heads/${config.git.baseBranch}`,
|
|
974
|
-
],
|
|
975
|
-
root,
|
|
976
|
-
);
|
|
977
|
-
if (base.code !== 0) {
|
|
978
|
-
return unavailable(
|
|
979
|
-
`the ${config.git.remote}/${config.git.baseBranch} base branch does not exist or cannot be read.`,
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
const detectedRepository = githubRepository(remote.output.trim());
|
|
983
|
-
if (
|
|
984
|
-
config.git.repository !== null &&
|
|
985
|
-
detectedRepository !== null &&
|
|
986
|
-
config.git.repository.toLowerCase() !== detectedRepository.toLowerCase()
|
|
987
|
-
) {
|
|
988
|
-
return unavailable(
|
|
989
|
-
`the ${config.git.remote} remote points to ${detectedRepository}, not ${config.git.repository}.`,
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
const provider = await probeProvider(run, config, root);
|
|
993
|
-
try {
|
|
994
|
-
await resolveDestination(
|
|
995
|
-
run,
|
|
996
|
-
config,
|
|
997
|
-
root,
|
|
998
|
-
remote.output.trim(),
|
|
999
|
-
provider.kind,
|
|
1000
|
-
branchNameFor(config, context.session),
|
|
1001
|
-
);
|
|
1002
|
-
} catch (error) {
|
|
1003
|
-
return unavailable(
|
|
1004
|
-
error instanceof Error
|
|
1005
|
-
? error.message
|
|
1006
|
-
: 'the configured Git destination is not usable.',
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
return { available: true, reason: null };
|
|
1010
|
-
},
|
|
1011
|
-
|
|
1012
|
-
plan: async (context) => {
|
|
1013
|
-
assertEjectCapability(context);
|
|
1014
|
-
const config = context.delivery ?? DEFAULT_DELIVERY_CONFIGURATION;
|
|
1015
|
-
const root = context.workspaceRoot;
|
|
1016
|
-
const modules = await planSessionModules(context);
|
|
1017
|
-
const primary = modules[0]!;
|
|
1018
|
-
const [ownership, budgets] = await Promise.all([
|
|
1019
|
-
loadPathOwnership(root),
|
|
1020
|
-
loadTaskBudgets(root),
|
|
1021
|
-
]);
|
|
1022
|
-
const budget = budgetFor(config, budgets, context.session.kind);
|
|
1023
|
-
const enable = modules.some((module) => module.enable);
|
|
1024
|
-
const newDependencies = [
|
|
1025
|
-
...new Set(modules.flatMap((module) => module.newPackages)),
|
|
1026
|
-
].sort();
|
|
1027
|
-
/* module enable writes the four composition files; install rewrites
|
|
1028
|
-
the lockfile whenever a package is new to the workspace. */
|
|
1029
|
-
const compositionFiles = enable ? 5 : newDependencies.length > 0 ? 1 : 0;
|
|
1030
|
-
const changedFiles = countChangedFiles(modules) + compositionFiles;
|
|
1031
|
-
const owners = ownersOf(ownership, [
|
|
1032
|
-
...modules.map((module) => `modules/${module.directory}/module.json`),
|
|
1033
|
-
...(enable ? ['platform/package.json'] : []),
|
|
1034
|
-
]);
|
|
1035
|
-
const run = await gitDeliveryRunner(context);
|
|
1036
|
-
const remoteUrl = (
|
|
1037
|
-
await run('git', ['remote', 'get-url', config.git.remote], root)
|
|
1038
|
-
).output.trim();
|
|
1039
|
-
const branch = branchNameFor(config, context.session);
|
|
1040
|
-
const provider = await probeProvider(run, config, root);
|
|
1041
|
-
const destination = await resolveDestination(
|
|
1042
|
-
run,
|
|
1043
|
-
config,
|
|
1044
|
-
root,
|
|
1045
|
-
remoteUrl,
|
|
1046
|
-
provider.kind,
|
|
1047
|
-
branch,
|
|
1048
|
-
);
|
|
1049
|
-
return {
|
|
1050
|
-
target: 'git-pr',
|
|
1051
|
-
deliveredBy: 'git-pr',
|
|
1052
|
-
moduleId: primary.id,
|
|
1053
|
-
targetPath: primary.targetPath,
|
|
1054
|
-
files: primary.files,
|
|
1055
|
-
overwrites: primary.overwrites,
|
|
1056
|
-
removes: primary.removes,
|
|
1057
|
-
newPackages: primary.newPackages,
|
|
1058
|
-
enable: primary.enable,
|
|
1059
|
-
modules,
|
|
1060
|
-
changedFiles,
|
|
1061
|
-
gates: [...GATES],
|
|
1062
|
-
platformLocal: false,
|
|
1063
|
-
restartRequired: false,
|
|
1064
|
-
notes: [
|
|
1065
|
-
`Nothing in this workspace changes. The modules are committed on ${branch} from ${config.git.remote}/${config.git.baseBranch} and pushed. ${
|
|
1066
|
-
provider.kind === 'github'
|
|
1067
|
-
? 'A pull request is opened for review.'
|
|
1068
|
-
: provider.note
|
|
1069
|
-
}`,
|
|
1070
|
-
],
|
|
1071
|
-
git: {
|
|
1072
|
-
remote: config.git.remote,
|
|
1073
|
-
repository: destination.repository,
|
|
1074
|
-
baseBranch: config.git.baseBranch,
|
|
1075
|
-
branch,
|
|
1076
|
-
deliveryMode: destination.mode,
|
|
1077
|
-
pushTarget:
|
|
1078
|
-
destination.mode === 'fork'
|
|
1079
|
-
? `${destination.forkOwner}/${githubRepositoryName(destination.repository!)}`
|
|
1080
|
-
: config.git.remote,
|
|
1081
|
-
forkOwner: destination.forkOwner,
|
|
1082
|
-
forkRequired: destination.forkRequired,
|
|
1083
|
-
head: destination.head,
|
|
1084
|
-
worktreePath: join(worktreesDirectory(root), context.session.id),
|
|
1085
|
-
allowedPaths: [
|
|
1086
|
-
...modules.map((module) => `modules/${module.directory}/**`),
|
|
1087
|
-
...(enable ? COMPOSITION_PATHS : []),
|
|
1088
|
-
LOCKFILE_PATH,
|
|
1089
|
-
],
|
|
1090
|
-
budget,
|
|
1091
|
-
changedFiles,
|
|
1092
|
-
newDependencies,
|
|
1093
|
-
owners,
|
|
1094
|
-
requireReviewer: ownership.requireReviewer,
|
|
1095
|
-
reviewers: config.git.reviewers,
|
|
1096
|
-
guardrails: evaluateGuardrails({
|
|
1097
|
-
offending: [],
|
|
1098
|
-
changedFiles,
|
|
1099
|
-
newDependencies,
|
|
1100
|
-
budget,
|
|
1101
|
-
}),
|
|
1102
|
-
provider: provider.kind,
|
|
1103
|
-
providerNote: provider.note,
|
|
1104
|
-
compareUrl: destination.compareUrl,
|
|
1105
|
-
},
|
|
1106
|
-
applied: false,
|
|
1107
|
-
};
|
|
1108
|
-
},
|
|
1109
|
-
|
|
1110
|
-
apply: async (context, plan, emit) => {
|
|
1111
|
-
assertEjectCapability(context);
|
|
1112
|
-
const git = plan.git;
|
|
1113
|
-
if (!git) {
|
|
1114
|
-
throw new SandboxSetupError(
|
|
1115
|
-
'EJECT_PLAN_INVALID',
|
|
1116
|
-
'The plan was not prepared for the git-pr target.',
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
const session = context.session;
|
|
1120
|
-
if (inFlight.has(session.id)) {
|
|
1121
|
-
throw new SandboxSetupError(
|
|
1122
|
-
'EJECT_IN_PROGRESS',
|
|
1123
|
-
'A delivery of this session is already running.',
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
inFlight.add(session.id);
|
|
1127
|
-
const root = context.workspaceRoot;
|
|
1128
|
-
const worktree = join(root, git.worktreePath);
|
|
1129
|
-
const run = await gitDeliveryRunner(context);
|
|
1130
|
-
const recorder = createStepRecorder(emit);
|
|
1131
|
-
const { record } = recorder;
|
|
1132
|
-
const assertOk = (id: string, result: CommandResult): void => {
|
|
1133
|
-
if (result.code !== 0) record(id, step(result));
|
|
1134
|
-
};
|
|
1135
|
-
let worktreeAdded = false;
|
|
1136
|
-
let localBranchCommit: string | null = null;
|
|
1137
|
-
let branchPrepared = false;
|
|
1138
|
-
let remoteBranchExisted = false;
|
|
1139
|
-
let remoteBranchCommit: string | null = null;
|
|
1140
|
-
let pushedCommit: string | null = null;
|
|
1141
|
-
let pushed = false;
|
|
1142
|
-
let delivered = false;
|
|
1143
|
-
try {
|
|
1144
|
-
/* Re-check the approved spec hash and every planned file before any
|
|
1145
|
-
fork, worktree, branch, or remote mutation. */
|
|
1146
|
-
assertPlanStillCurrent(plan.modules, await planSessionModules(context));
|
|
1147
|
-
await assertWorktreeDirectoryIgnored(run, root);
|
|
1148
|
-
const gates = await runDeliveryGates(context, plan.gates, emit);
|
|
1149
|
-
const destination = pushDestination(git);
|
|
1150
|
-
|
|
1151
|
-
if (git.deliveryMode === 'fork') {
|
|
1152
|
-
emit('fork.started', {});
|
|
1153
|
-
if (git.forkRequired) {
|
|
1154
|
-
const signedInOwner = await outputValue(
|
|
1155
|
-
run,
|
|
1156
|
-
'gh',
|
|
1157
|
-
['api', 'user', '--jq', '.login'],
|
|
1158
|
-
root,
|
|
1159
|
-
);
|
|
1160
|
-
if (!signedInOwner) {
|
|
1161
|
-
throw new SandboxSetupError(
|
|
1162
|
-
'GITHUB_IDENTITY_UNAVAILABLE',
|
|
1163
|
-
'GitHub did not return the signed-in account before fork creation.',
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
const organization =
|
|
1167
|
-
signedInOwner.toLowerCase() === git.forkOwner!.toLowerCase()
|
|
1168
|
-
? []
|
|
1169
|
-
: ['--org', git.forkOwner!];
|
|
1170
|
-
record(
|
|
1171
|
-
'fork',
|
|
1172
|
-
step(
|
|
1173
|
-
await run(
|
|
1174
|
-
'gh',
|
|
1175
|
-
[
|
|
1176
|
-
'repo',
|
|
1177
|
-
'fork',
|
|
1178
|
-
git.repository!,
|
|
1179
|
-
'--clone=false',
|
|
1180
|
-
...organization,
|
|
1181
|
-
],
|
|
1182
|
-
root,
|
|
1183
|
-
),
|
|
1184
|
-
git.pushTarget,
|
|
1185
|
-
),
|
|
1186
|
-
);
|
|
1187
|
-
} else {
|
|
1188
|
-
record('fork', {
|
|
1189
|
-
ok: true,
|
|
1190
|
-
output: '',
|
|
1191
|
-
detail: `${git.pushTarget} already exists`,
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const remoteOwner = await remoteBranchOwner(
|
|
1197
|
-
run,
|
|
1198
|
-
root,
|
|
1199
|
-
destination,
|
|
1200
|
-
git.branch,
|
|
1201
|
-
);
|
|
1202
|
-
remoteBranchExisted = remoteOwner.exists;
|
|
1203
|
-
remoteBranchCommit = remoteOwner.commit;
|
|
1204
|
-
if (remoteOwner.exists) {
|
|
1205
|
-
assertSessionBranch(git.branch, remoteOwner.message, session.id);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
emit('fetch.started', {});
|
|
1209
|
-
record(
|
|
1210
|
-
'fetch',
|
|
1211
|
-
step(
|
|
1212
|
-
await run(
|
|
1213
|
-
'git',
|
|
1214
|
-
['fetch', '--quiet', git.remote, git.baseBranch],
|
|
1215
|
-
root,
|
|
1216
|
-
),
|
|
1217
|
-
`${git.remote}/${git.baseBranch}`,
|
|
1218
|
-
),
|
|
1219
|
-
);
|
|
1220
|
-
|
|
1221
|
-
emit('worktree.started', {});
|
|
1222
|
-
await mkdir(join(root, worktreesDirectory(root)), { recursive: true });
|
|
1223
|
-
if (await exists(worktree)) await removeWorktree(run, root, worktree);
|
|
1224
|
-
record(
|
|
1225
|
-
'worktree',
|
|
1226
|
-
step(
|
|
1227
|
-
await run(
|
|
1228
|
-
'git',
|
|
1229
|
-
[
|
|
1230
|
-
'worktree',
|
|
1231
|
-
'add',
|
|
1232
|
-
'--detach',
|
|
1233
|
-
'--quiet',
|
|
1234
|
-
worktree,
|
|
1235
|
-
`${git.remote}/${git.baseBranch}`,
|
|
1236
|
-
],
|
|
1237
|
-
root,
|
|
1238
|
-
),
|
|
1239
|
-
git.worktreePath,
|
|
1240
|
-
),
|
|
1241
|
-
);
|
|
1242
|
-
worktreeAdded = true;
|
|
1243
|
-
|
|
1244
|
-
emit('branch.started', {});
|
|
1245
|
-
const localBranch = await run(
|
|
1246
|
-
'git',
|
|
1247
|
-
['show-ref', '--verify', '--quiet', `refs/heads/${git.branch}`],
|
|
1248
|
-
root,
|
|
1249
|
-
);
|
|
1250
|
-
if (localBranch.code === 0) {
|
|
1251
|
-
localBranchCommit = await outputValue(
|
|
1252
|
-
run,
|
|
1253
|
-
'git',
|
|
1254
|
-
['rev-parse', git.branch],
|
|
1255
|
-
root,
|
|
1256
|
-
);
|
|
1257
|
-
const localMessage =
|
|
1258
|
-
(await outputValue(
|
|
1259
|
-
run,
|
|
1260
|
-
'git',
|
|
1261
|
-
['show', '-s', '--format=%B', git.branch],
|
|
1262
|
-
root,
|
|
1263
|
-
)) ?? '';
|
|
1264
|
-
assertSessionBranch(git.branch, localMessage, session.id);
|
|
1265
|
-
}
|
|
1266
|
-
record(
|
|
1267
|
-
'branch',
|
|
1268
|
-
step(
|
|
1269
|
-
await run('git', ['switch', '--quiet', '-C', git.branch], worktree),
|
|
1270
|
-
git.branch,
|
|
1271
|
-
),
|
|
1272
|
-
);
|
|
1273
|
-
branchPrepared = true;
|
|
1274
|
-
|
|
1275
|
-
const paths = sessionPaths(root, session.id, session.moduleSuffix);
|
|
1276
|
-
const { copied, removed } = await stageModules(
|
|
1277
|
-
paths,
|
|
1278
|
-
worktree,
|
|
1279
|
-
plan.modules,
|
|
1280
|
-
emit,
|
|
1281
|
-
);
|
|
1282
|
-
|
|
1283
|
-
emit('install.started', {});
|
|
1284
|
-
record('install', await installWorktree(run, worktree));
|
|
1285
|
-
|
|
1286
|
-
let enabled = false;
|
|
1287
|
-
for (const module of plan.modules) {
|
|
1288
|
-
if (!module.enable) continue;
|
|
1289
|
-
emit('enable.started', { moduleId: module.id });
|
|
1290
|
-
record('enable', await enableModule(run, worktree, module.id));
|
|
1291
|
-
enabled = true;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
emit('verify.started', {});
|
|
1295
|
-
record('verify', await verifyPlatform(run, worktree));
|
|
1296
|
-
|
|
1297
|
-
emit('guardrails.started', {});
|
|
1298
|
-
const status = await run(
|
|
1299
|
-
'git',
|
|
1300
|
-
['status', '--porcelain', '-uall'],
|
|
1301
|
-
worktree,
|
|
1302
|
-
);
|
|
1303
|
-
assertOk('guardrails', status);
|
|
1304
|
-
const changed = porcelainPaths(status.output);
|
|
1305
|
-
const offending = changed.filter(
|
|
1306
|
-
(path) =>
|
|
1307
|
-
!git.allowedPaths.some((pattern) => matchesPath(path, pattern)),
|
|
1308
|
-
);
|
|
1309
|
-
const guardrails = evaluateGuardrails({
|
|
1310
|
-
offending,
|
|
1311
|
-
changedFiles: changed.length,
|
|
1312
|
-
newDependencies: git.newDependencies,
|
|
1313
|
-
budget: git.budget,
|
|
1314
|
-
});
|
|
1315
|
-
record('guardrails', {
|
|
1316
|
-
ok: guardrails.ok,
|
|
1317
|
-
output: guardrails.ok
|
|
1318
|
-
? changed.join('\n')
|
|
1319
|
-
: guardrails.reasons.join('\n'),
|
|
1320
|
-
detail: `${changed.length} of ${git.budget.maxChangedFiles} files`,
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
emit('commit.started', {});
|
|
1324
|
-
const pathspecs: string[] = [];
|
|
1325
|
-
for (const pattern of git.allowedPaths) {
|
|
1326
|
-
const pathspec = pathspecOf(pattern);
|
|
1327
|
-
if (await exists(join(worktree, pathspec))) pathspecs.push(pathspec);
|
|
1328
|
-
}
|
|
1329
|
-
assertOk(
|
|
1330
|
-
'commit',
|
|
1331
|
-
await run('git', ['add', '-A', '--', ...pathspecs], worktree),
|
|
1332
|
-
);
|
|
1333
|
-
const changes = await stagedChanges(run, worktree);
|
|
1334
|
-
const owners = ownersOf(await loadPathOwnership(root), [
|
|
1335
|
-
...changes.added,
|
|
1336
|
-
...changes.modified,
|
|
1337
|
-
...changes.removed,
|
|
1338
|
-
]);
|
|
1339
|
-
record(
|
|
1340
|
-
'commit',
|
|
1341
|
-
step(
|
|
1342
|
-
await withTemporaryFile(
|
|
1343
|
-
commitMessage(session, plan.modules, gates),
|
|
1344
|
-
(file) =>
|
|
1345
|
-
run('git', ['commit', '--quiet', '--file', file], worktree),
|
|
1346
|
-
),
|
|
1347
|
-
`${changes.added.length + changes.modified.length + changes.removed.length} files on ${git.branch}`,
|
|
1348
|
-
),
|
|
1349
|
-
);
|
|
1350
|
-
|
|
1351
|
-
emit('push.started', {});
|
|
1352
|
-
record(
|
|
1353
|
-
'push',
|
|
1354
|
-
step(
|
|
1355
|
-
await run(
|
|
1356
|
-
'git',
|
|
1357
|
-
[
|
|
1358
|
-
'push',
|
|
1359
|
-
'--quiet',
|
|
1360
|
-
'-u',
|
|
1361
|
-
`--force-with-lease=refs/heads/${git.branch}:${remoteOwner.commit ?? ''}`,
|
|
1362
|
-
destination,
|
|
1363
|
-
git.branch,
|
|
1364
|
-
],
|
|
1365
|
-
worktree,
|
|
1366
|
-
),
|
|
1367
|
-
`${git.pushTarget}/${git.branch}`,
|
|
1368
|
-
),
|
|
1369
|
-
);
|
|
1370
|
-
pushed = true;
|
|
1371
|
-
pushedCommit = await outputValue(
|
|
1372
|
-
run,
|
|
1373
|
-
'git',
|
|
1374
|
-
['rev-parse', 'HEAD'],
|
|
1375
|
-
worktree,
|
|
1376
|
-
);
|
|
1377
|
-
|
|
1378
|
-
emit('pr.started', {});
|
|
1379
|
-
const body = pullRequestBody({
|
|
1380
|
-
session,
|
|
1381
|
-
modules: plan.modules,
|
|
1382
|
-
gates,
|
|
1383
|
-
changes,
|
|
1384
|
-
owners,
|
|
1385
|
-
requireReviewer: git.requireReviewer,
|
|
1386
|
-
handoff: await lastReviewHandoff(root, session),
|
|
1387
|
-
specs: await specificationEvidence(root, session),
|
|
1388
|
-
});
|
|
1389
|
-
let pullRequestUrl: string | null = null;
|
|
1390
|
-
if (git.provider === 'github') {
|
|
1391
|
-
pullRequestUrl = await openPullRequest(
|
|
1392
|
-
run,
|
|
1393
|
-
worktree,
|
|
1394
|
-
git,
|
|
1395
|
-
git.repository,
|
|
1396
|
-
pullRequestTitle(plan.modules),
|
|
1397
|
-
body,
|
|
1398
|
-
recorder,
|
|
1399
|
-
);
|
|
1400
|
-
} else {
|
|
1401
|
-
record('pr', {
|
|
1402
|
-
ok: true,
|
|
1403
|
-
output: body,
|
|
1404
|
-
detail: git.compareUrl ?? `${git.remote}/${git.branch}`,
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
delivered = true;
|
|
1408
|
-
|
|
1409
|
-
return {
|
|
1410
|
-
moduleId: plan.moduleId,
|
|
1411
|
-
targetPath: plan.targetPath,
|
|
1412
|
-
files: copied,
|
|
1413
|
-
removed,
|
|
1414
|
-
enabled,
|
|
1415
|
-
gates,
|
|
1416
|
-
steps: recorder.steps,
|
|
1417
|
-
restartRequired: false,
|
|
1418
|
-
branch: git.branch,
|
|
1419
|
-
pullRequestUrl,
|
|
1420
|
-
compareUrl: git.compareUrl,
|
|
1421
|
-
};
|
|
1422
|
-
} finally {
|
|
1423
|
-
inFlight.delete(session.id);
|
|
1424
|
-
const problems: string[] = [];
|
|
1425
|
-
if (worktreeAdded) {
|
|
1426
|
-
const result = await removeWorktree(run, root, worktree);
|
|
1427
|
-
if (!result.ok) problems.push(result.output);
|
|
1428
|
-
}
|
|
1429
|
-
if (branchPrepared && !delivered) {
|
|
1430
|
-
const result = localBranchCommit
|
|
1431
|
-
? await run(
|
|
1432
|
-
'git',
|
|
1433
|
-
['branch', '-f', git.branch, localBranchCommit],
|
|
1434
|
-
root,
|
|
1435
|
-
)
|
|
1436
|
-
: await run('git', ['branch', '-D', git.branch], root);
|
|
1437
|
-
if (result.code !== 0) problems.push(result.output);
|
|
1438
|
-
}
|
|
1439
|
-
if (pushed && !delivered) {
|
|
1440
|
-
const result = remoteBranchExisted
|
|
1441
|
-
? await run(
|
|
1442
|
-
'git',
|
|
1443
|
-
[
|
|
1444
|
-
'push',
|
|
1445
|
-
'--quiet',
|
|
1446
|
-
`--force-with-lease=refs/heads/${git.branch}:${pushedCommit ?? ''}`,
|
|
1447
|
-
pushDestination(git),
|
|
1448
|
-
`${remoteBranchCommit ?? ''}:refs/heads/${git.branch}`,
|
|
1449
|
-
],
|
|
1450
|
-
root,
|
|
1451
|
-
)
|
|
1452
|
-
: await run(
|
|
1453
|
-
'git',
|
|
1454
|
-
[
|
|
1455
|
-
'push',
|
|
1456
|
-
'--quiet',
|
|
1457
|
-
pushDestination(git),
|
|
1458
|
-
'--delete',
|
|
1459
|
-
git.branch,
|
|
1460
|
-
],
|
|
1461
|
-
root,
|
|
1462
|
-
);
|
|
1463
|
-
if (result.code !== 0) problems.push(result.output);
|
|
1464
|
-
}
|
|
1465
|
-
emit('cleanup.completed', {
|
|
1466
|
-
ok: problems.length === 0,
|
|
1467
|
-
output: problems.join('\n'),
|
|
1468
|
-
detail: delivered ? `${git.branch} kept` : '',
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
},
|
|
1472
|
-
};
|
|
1473
|
-
}
|