@nebutra/agent-runtime 0.2.1 → 2.0.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.
Files changed (114) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +33 -10
  3. package/dist/adapters/index.d.ts +24 -5
  4. package/dist/adapters/index.js +27 -0
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/{chunk-KCNN4QUQ.js → chunk-5UGIUYJR.js} +4 -4
  7. package/dist/chunk-7ELA4SWE.js +73 -0
  8. package/dist/chunk-7ELA4SWE.js.map +1 -0
  9. package/dist/chunk-Q62VKHIT.js +178 -0
  10. package/dist/chunk-Q62VKHIT.js.map +1 -0
  11. package/dist/chunk-VXILZAXK.js +541 -0
  12. package/dist/chunk-VXILZAXK.js.map +1 -0
  13. package/dist/command-exec.d.ts +45 -0
  14. package/dist/command-exec.js +15 -0
  15. package/dist/command-exec.js.map +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +81 -28
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestration.d.ts +42 -1
  20. package/dist/orchestration.js +7 -3
  21. package/dist/pulsar.js +2 -2
  22. package/dist/sandbox-DfcRttXt.d.ts +219 -0
  23. package/dist/sandbox.d.ts +2 -64
  24. package/dist/sandbox.js +25 -3
  25. package/package.json +81 -30
  26. package/.turbo/turbo-build.log +0 -130
  27. package/.turbo/turbo-test.log +0 -47
  28. package/.turbo/turbo-typecheck.log +0 -4
  29. package/CHANGELOG.md +0 -250
  30. package/dist/chunk-MUF7ZZTO.js +0 -57
  31. package/dist/chunk-MUF7ZZTO.js.map +0 -1
  32. package/dist/chunk-MX2WL43P.js +0 -90
  33. package/dist/chunk-MX2WL43P.js.map +0 -1
  34. package/examples/pulsar-quickstart.ts +0 -35
  35. package/examples/resume-branch.ts +0 -45
  36. package/examples/subagent-fanout.ts +0 -20
  37. package/src/adapters/dispatcher-sse.test.ts +0 -218
  38. package/src/adapters/dispatcher-sse.ts +0 -222
  39. package/src/adapters/index.ts +0 -18
  40. package/src/adapters/mcp-catalog.test.ts +0 -213
  41. package/src/adapters/mcp-catalog.ts +0 -188
  42. package/src/adapters/prisma-rollout.test.ts +0 -153
  43. package/src/adapters/prisma-rollout.ts +0 -104
  44. package/src/agent-runtime.test.ts +0 -176
  45. package/src/artifact-stream.test.ts +0 -330
  46. package/src/artifact-stream.ts +0 -453
  47. package/src/channel-gateway.test.ts +0 -432
  48. package/src/channel-gateway.ts +0 -357
  49. package/src/cli.ts +0 -49
  50. package/src/code-review.test.ts +0 -501
  51. package/src/code-review.ts +0 -495
  52. package/src/command-suggestions.test.ts +0 -251
  53. package/src/command-suggestions.ts +0 -338
  54. package/src/commands.test.ts +0 -184
  55. package/src/commands.ts +0 -140
  56. package/src/commit-message.test.ts +0 -249
  57. package/src/commit-message.ts +0 -180
  58. package/src/context-compaction.test.ts +0 -522
  59. package/src/context-compaction.ts +0 -438
  60. package/src/definitions.test.ts +0 -78
  61. package/src/definitions.ts +0 -190
  62. package/src/deployment-status.test.ts +0 -215
  63. package/src/deployment-status.ts +0 -227
  64. package/src/design-context.test.ts +0 -195
  65. package/src/design-context.ts +0 -198
  66. package/src/dispatcher.test.ts +0 -234
  67. package/src/dispatcher.ts +0 -189
  68. package/src/durable-turn.test.ts +0 -209
  69. package/src/durable-turn.ts +0 -135
  70. package/src/edit-planner.test.ts +0 -204
  71. package/src/edit-planner.ts +0 -325
  72. package/src/fuzzy-match.test.ts +0 -311
  73. package/src/fuzzy-match.ts +0 -444
  74. package/src/hook-pipeline.test.ts +0 -279
  75. package/src/hook-pipeline.ts +0 -373
  76. package/src/inbound-admission.test.ts +0 -394
  77. package/src/inbound-admission.ts +0 -246
  78. package/src/index.ts +0 -49
  79. package/src/loop.test.ts +0 -161
  80. package/src/loop.ts +0 -211
  81. package/src/mcp-bridge.test.ts +0 -165
  82. package/src/mcp-bridge.ts +0 -81
  83. package/src/memory-provider.test.ts +0 -232
  84. package/src/memory-provider.ts +0 -257
  85. package/src/model.ts +0 -168
  86. package/src/orchestration.test.ts +0 -53
  87. package/src/orchestration.ts +0 -146
  88. package/src/permission-ruleset.test.ts +0 -301
  89. package/src/permission-ruleset.ts +0 -1
  90. package/src/policy.ts +0 -151
  91. package/src/project-repo.test.ts +0 -232
  92. package/src/project-repo.ts +0 -311
  93. package/src/protocol.ts +0 -159
  94. package/src/pulsar.test.ts +0 -156
  95. package/src/pulsar.ts +0 -322
  96. package/src/rollout-store-persistent.test.ts +0 -217
  97. package/src/rollout-store-persistent.ts +0 -166
  98. package/src/rollout.ts +0 -150
  99. package/src/sandbox.ts +0 -113
  100. package/src/session-share.test.ts +0 -360
  101. package/src/session-share.ts +0 -310
  102. package/src/skill-distillation.test.ts +0 -177
  103. package/src/skill-distillation.ts +0 -369
  104. package/src/skills.test.ts +0 -277
  105. package/src/skills.ts +0 -277
  106. package/src/subagents.test.ts +0 -290
  107. package/src/subagents.ts +0 -332
  108. package/src/tools.test.ts +0 -12
  109. package/src/tools.ts +0 -129
  110. package/src/workbench.test.ts +0 -0
  111. package/src/workbench.ts +0 -0
  112. package/tsconfig.json +0 -12
  113. package/tsup.config.ts +0 -36
  114. /package/dist/{chunk-KCNN4QUQ.js.map → chunk-5UGIUYJR.js.map} +0 -0
@@ -1,204 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- analyzeEditIntent,
5
- applyEditBlock,
6
- buildEditInstructions,
7
- type EditBlock,
8
- EditType,
9
- type FileManifest,
10
- parseEditBlocks,
11
- planEdit,
12
- selectFilesForEdit,
13
- } from "./edit-planner.js";
14
-
15
- const TENANT = "org_123";
16
-
17
- const manifest: FileManifest = {
18
- files: {
19
- "src/App.tsx": { path: "src/App.tsx", type: "page" },
20
- "src/components/Button.tsx": {
21
- path: "src/components/Button.tsx",
22
- type: "component",
23
- },
24
- "src/components/Header.tsx": {
25
- path: "src/components/Header.tsx",
26
- type: "component",
27
- },
28
- "src/pages/Home.tsx": { path: "src/pages/Home.tsx", type: "page" },
29
- "src/styles/theme.css": { path: "src/styles/theme.css", type: "style" },
30
- "package.json": { path: "package.json", type: "config" },
31
- "src/utils/format.ts": { path: "src/utils/format.ts", type: "util" },
32
- },
33
- };
34
-
35
- describe("analyzeEditIntent", () => {
36
- it("matches UPDATE_STYLE for styling prompts", () => {
37
- const intent = analyzeEditIntent("change the color theme to blue", manifest);
38
- expect(intent.type).toBe(EditType.UPDATE_STYLE);
39
- expect(intent.targetFiles).toContain("src/styles/theme.css");
40
- });
41
-
42
- it("matches ADD_DEPENDENCY for package install prompts", () => {
43
- const intent = analyzeEditIntent("install the axios package", manifest);
44
- expect(intent.type).toBe(EditType.ADD_DEPENDENCY);
45
- expect(intent.targetFiles).toContain("package.json");
46
- });
47
-
48
- it("matches FIX_ISSUE for bug prompts", () => {
49
- const intent = analyzeEditIntent("fix the bug in the Header", manifest);
50
- expect(intent.type).toBe(EditType.FIX_ISSUE);
51
- expect(intent.targetFiles).toContain("src/components/Header.tsx");
52
- });
53
-
54
- it("matches FULL_REBUILD and targets all files", () => {
55
- const intent = analyzeEditIntent("rebuild the entire app from scratch", manifest);
56
- expect(intent.type).toBe(EditType.FULL_REBUILD);
57
- expect(intent.targetFiles.sort()).toEqual(Object.keys(manifest.files).sort());
58
- });
59
-
60
- it("matches REFACTOR", () => {
61
- const intent = analyzeEditIntent("refactor the Button component", manifest);
62
- expect(intent.type).toBe(EditType.REFACTOR);
63
- expect(intent.targetFiles).toContain("src/components/Button.tsx");
64
- });
65
-
66
- it("matches ADD_FEATURE with page insertion points", () => {
67
- const intent = analyzeEditIntent("add a new search feature", manifest);
68
- expect(intent.type).toBe(EditType.ADD_FEATURE);
69
- expect(intent.targetFiles).toContain("src/App.tsx");
70
- });
71
-
72
- it("matches UPDATE_COMPONENT by component name", () => {
73
- const intent = analyzeEditIntent("update the Button component label", manifest);
74
- expect(intent.type).toBe(EditType.UPDATE_COMPONENT);
75
- expect(intent.targetFiles).toContain("src/components/Button.tsx");
76
- });
77
-
78
- it("first-match-wins: fix beats update ordering", () => {
79
- const intent = analyzeEditIntent("fix the broken Button update", manifest);
80
- expect(intent.type).toBe(EditType.FIX_ISSUE);
81
- });
82
-
83
- it("falls back to UPDATE_COMPONENT with empty targets on no match", () => {
84
- const intent = analyzeEditIntent("zzz qqq", manifest);
85
- expect(intent.type).toBe(EditType.UPDATE_COMPONENT);
86
- expect(intent.targetFiles).toEqual([]);
87
- expect(intent.description).toBe("zzz qqq");
88
- });
89
-
90
- it("does not mutate the manifest", () => {
91
- const snapshot = JSON.stringify(manifest);
92
- analyzeEditIntent("rebuild everything", manifest);
93
- expect(JSON.stringify(manifest)).toBe(snapshot);
94
- });
95
- });
96
-
97
- describe("buildEditInstructions", () => {
98
- it("is deterministic per edit type", () => {
99
- expect(buildEditInstructions(EditType.FIX_ISSUE)).toBe(
100
- buildEditInstructions(EditType.FIX_ISSUE),
101
- );
102
- expect(buildEditInstructions(EditType.FIX_ISSUE)).not.toBe(
103
- buildEditInstructions(EditType.ADD_FEATURE),
104
- );
105
- });
106
- });
107
-
108
- describe("selectFilesForEdit", () => {
109
- it("splits primary and context, always includes key files", () => {
110
- const ctx = selectFilesForEdit("update the Button component", manifest);
111
- expect(ctx.primaryFiles).toContain("src/components/Button.tsx");
112
- expect(ctx.contextFiles).toContain("src/App.tsx");
113
- expect(ctx.contextFiles).toContain("package.json");
114
- expect(ctx.contextFiles).not.toContain("src/components/Button.tsx");
115
- });
116
-
117
- it("FULL_REBUILD makes all files primary", () => {
118
- const ctx = selectFilesForEdit("rebuild the whole app", manifest);
119
- expect(ctx.primaryFiles.sort()).toEqual(Object.keys(manifest.files).sort());
120
- expect(ctx.contextFiles).toEqual([]);
121
- });
122
-
123
- it("systemPrompt is deterministic and skips structure for FULL_REBUILD", () => {
124
- const a = selectFilesForEdit("update the Button", manifest);
125
- const b = selectFilesForEdit("update the Button", manifest);
126
- expect(a.systemPrompt).toBe(b.systemPrompt);
127
- expect(a.systemPrompt).toContain("File structure");
128
- const rebuild = selectFilesForEdit("full rebuild now", manifest);
129
- expect(rebuild.systemPrompt).not.toContain("File structure");
130
- });
131
- });
132
-
133
- describe("parseEditBlocks", () => {
134
- it("parses multiple blocks", () => {
135
- const text = `
136
- <<<edit file="a.tsx">>>
137
- do thing one
138
- ---
139
- const a = 1;
140
- <<<end>>>
141
- noise
142
- <<<edit file="b.tsx">>>
143
- do thing two
144
- ---
145
- const b = 2;
146
- <<<end>>>`;
147
- const blocks = parseEditBlocks(text);
148
- expect(blocks).toHaveLength(2);
149
- expect(blocks[0]).toEqual<EditBlock>({
150
- targetFile: "a.tsx",
151
- instructions: "do thing one",
152
- update: "const a = 1;",
153
- });
154
- expect(blocks[1]?.targetFile).toBe("b.tsx");
155
- });
156
-
157
- it("tolerates malformed input", () => {
158
- expect(parseEditBlocks("no blocks here")).toEqual([]);
159
- expect(parseEditBlocks('<<<edit file="x">>> incomplete')).toEqual([]);
160
- });
161
- });
162
-
163
- describe("applyEditBlock", () => {
164
- const block: EditBlock = {
165
- targetFile: "a.tsx",
166
- instructions: "append",
167
- update: "ADDED",
168
- };
169
-
170
- it("uses the injected merger", async () => {
171
- const merge = async (orig: string, upd: string) => `${orig}|${upd}`;
172
- const res = await applyEditBlock("ORIG", block, merge);
173
- expect(res.ok).toBe(true);
174
- expect(res.merged).toBe("ORIG|ADDED");
175
- });
176
-
177
- it("returns error path when merger throws", async () => {
178
- const merge = async () => {
179
- throw new Error("boom");
180
- };
181
- const res = await applyEditBlock("ORIG", block, merge);
182
- expect(res.ok).toBe(false);
183
- expect(res.error).toContain("boom");
184
- });
185
- });
186
-
187
- describe("planEdit (tenant)", () => {
188
- it("fails closed on empty tenant", async () => {
189
- await expect(planEdit("", "update the Button", manifest)).rejects.toThrow();
190
- await expect(planEdit(" ", "update the Button", manifest)).rejects.toThrow();
191
- });
192
-
193
- it("returns a FileContext for a valid tenant", async () => {
194
- const ctx = await planEdit(TENANT, "update the Button component", manifest);
195
- expect(ctx.editIntent.type).toBe(EditType.UPDATE_COMPONENT);
196
- expect(ctx.primaryFiles).toContain("src/components/Button.tsx");
197
- });
198
-
199
- it("does not mutate the input manifest", async () => {
200
- const snapshot = JSON.stringify(manifest);
201
- await planEdit(TENANT, "rebuild everything", manifest);
202
- expect(JSON.stringify(manifest)).toBe(snapshot);
203
- });
204
- });
@@ -1,325 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * Edit planner — a faithful re-expression of a targeted-edit model for an
5
- * app-builder agent. Given a natural-language prompt and a manifest of the
6
- * current project files, it classifies the edit intent, selects the minimal
7
- * set of files to send to a codegen turn, and provides a terse "fast-apply"
8
- * format for re-integrating model output.
9
- *
10
- * Everything here is deterministic and pure aside from `applyEditBlock`,
11
- * whose merge step is provider-specific and injected.
12
- */
13
-
14
- export enum EditType {
15
- UPDATE_COMPONENT = "UPDATE_COMPONENT",
16
- ADD_FEATURE = "ADD_FEATURE",
17
- FIX_ISSUE = "FIX_ISSUE",
18
- REFACTOR = "REFACTOR",
19
- FULL_REBUILD = "FULL_REBUILD",
20
- UPDATE_STYLE = "UPDATE_STYLE",
21
- ADD_DEPENDENCY = "ADD_DEPENDENCY",
22
- }
23
-
24
- export type FileKind = "component" | "page" | "style" | "config" | "util" | "other";
25
-
26
- export interface FileInfo {
27
- path: string;
28
- type: FileKind;
29
- }
30
-
31
- export interface FileManifest {
32
- files: Record<string, FileInfo>;
33
- }
34
-
35
- export interface EditIntent {
36
- type: EditType;
37
- targetFiles: string[];
38
- description: string;
39
- }
40
-
41
- export interface FileContext {
42
- primaryFiles: string[];
43
- contextFiles: string[];
44
- systemPrompt: string;
45
- editIntent: EditIntent;
46
- }
47
-
48
- export interface EditBlock {
49
- targetFile: string;
50
- instructions: string;
51
- update: string;
52
- }
53
-
54
- export type EditMerger = (
55
- originalContent: string,
56
- update: string,
57
- instructions: string,
58
- ) => Promise<string>;
59
-
60
- export interface ApplyEditResult {
61
- ok: boolean;
62
- merged?: string;
63
- error?: string;
64
- }
65
-
66
- const fileManifestSchema = z.object({
67
- files: z.record(
68
- z.string(),
69
- z.object({
70
- path: z.string(),
71
- type: z.enum(["component", "page", "style", "config", "util", "other"]),
72
- }),
73
- ),
74
- });
75
-
76
- // --- pure file resolvers -------------------------------------------------
77
-
78
- function allFiles(manifest: FileManifest): string[] {
79
- return Object.keys(manifest.files);
80
- }
81
-
82
- function filesByType(manifest: FileManifest, type: FileKind): string[] {
83
- return allFiles(manifest).filter((p) => manifest.files[p]?.type === type);
84
- }
85
-
86
- /** Match a component file by its base name appearing in the prompt. */
87
- function resolveByComponentName(prompt: string, manifest: FileManifest): string[] {
88
- const lower = prompt.toLowerCase();
89
- return allFiles(manifest).filter((p) => {
90
- const info = manifest.files[p];
91
- if (!info || (info.type !== "component" && info.type !== "page")) {
92
- return false;
93
- }
94
- const base = (p.split("/").pop() ?? "").replace(/\.[^.]+$/, "");
95
- return base.length > 0 && lower.includes(base.toLowerCase());
96
- });
97
- }
98
-
99
- /** Feature-insertion points: an App/entry file plus pages. */
100
- function resolveFeatureInsertionPoints(_prompt: string, manifest: FileManifest): string[] {
101
- const pages = filesByType(manifest, "page");
102
- const entry = allFiles(manifest).filter((p) => /(^|\/)App\.[tj]sx?$/.test(p));
103
- return Array.from(new Set([...entry, ...pages]));
104
- }
105
-
106
- function resolveStyleFiles(_prompt: string, manifest: FileManifest): string[] {
107
- return filesByType(manifest, "style");
108
- }
109
-
110
- function resolveConfigFiles(_prompt: string, manifest: FileManifest): string[] {
111
- const configs = filesByType(manifest, "config");
112
- const pkg = allFiles(manifest).filter((p) => /(^|\/)package\.json$/.test(p));
113
- return Array.from(new Set([...pkg, ...configs]));
114
- }
115
-
116
- /** For FIX: prefer files named in the prompt, else components + pages. */
117
- function resolveProblemFiles(prompt: string, manifest: FileManifest): string[] {
118
- const named = resolveByComponentName(prompt, manifest);
119
- if (named.length > 0) return named;
120
- return [...filesByType(manifest, "component"), ...filesByType(manifest, "page")];
121
- }
122
-
123
- // --- pattern-driven intent analysis -------------------------------------
124
-
125
- interface IntentPattern {
126
- patterns: RegExp[];
127
- type: EditType;
128
- fileResolver: (prompt: string, manifest: FileManifest) => string[];
129
- }
130
-
131
- // Order matters: first matching pattern wins.
132
- const INTENT_PATTERNS: readonly IntentPattern[] = [
133
- {
134
- patterns: [/\b(rebuild|recreate|start over|from scratch)\b/i],
135
- type: EditType.FULL_REBUILD,
136
- fileResolver: (_p, m) => allFiles(m),
137
- },
138
- {
139
- patterns: [/\b(fix|bug|broken|error|issue|not working|crash)\b/i],
140
- type: EditType.FIX_ISSUE,
141
- fileResolver: resolveProblemFiles,
142
- },
143
- {
144
- patterns: [
145
- /\b(install|add)\b.*\b(package|dependency|dependencies|library|npm|module)\b/i,
146
- /\b(package|dependency|dependencies)\b.*\b(install|add)\b/i,
147
- ],
148
- type: EditType.ADD_DEPENDENCY,
149
- fileResolver: resolveConfigFiles,
150
- },
151
- {
152
- patterns: [/\b(color|colour|theme|style|styling|css|font|spacing|layout look)\b/i],
153
- type: EditType.UPDATE_STYLE,
154
- fileResolver: resolveStyleFiles,
155
- },
156
- {
157
- patterns: [/\b(refactor|clean ?up|reorganize|restructure|simplify)\b/i],
158
- type: EditType.REFACTOR,
159
- fileResolver: resolveByComponentName,
160
- },
161
- {
162
- patterns: [/\b(add|create|implement|introduce)\b.*\b(feature|page|section|flow)\b/i],
163
- type: EditType.ADD_FEATURE,
164
- fileResolver: resolveFeatureInsertionPoints,
165
- },
166
- {
167
- patterns: [/\b(update|change|modify|edit|adjust|tweak)\b/i],
168
- type: EditType.UPDATE_COMPONENT,
169
- fileResolver: resolveByComponentName,
170
- },
171
- ];
172
-
173
- export function analyzeEditIntent(prompt: string, manifest: FileManifest): EditIntent {
174
- const description = prompt.trim();
175
- for (const pattern of INTENT_PATTERNS) {
176
- if (pattern.patterns.some((re) => re.test(prompt))) {
177
- return {
178
- type: pattern.type,
179
- targetFiles: [...pattern.fileResolver(prompt, manifest)],
180
- description,
181
- };
182
- }
183
- }
184
- return {
185
- type: EditType.UPDATE_COMPONENT,
186
- targetFiles: [],
187
- description,
188
- };
189
- }
190
-
191
- // --- targeted context selection -----------------------------------------
192
-
193
- const EDIT_INSTRUCTIONS: Readonly<Record<EditType, string>> = {
194
- [EditType.UPDATE_COMPONENT]:
195
- "Modify only the targeted component(s). Preserve unrelated logic, props, and exports.",
196
- [EditType.ADD_FEATURE]:
197
- "Introduce the new feature with minimal disruption. Wire it into existing entry/page files.",
198
- [EditType.FIX_ISSUE]:
199
- "Diagnose and fix the defect. Make the smallest correct change; do not refactor unrelated code.",
200
- [EditType.REFACTOR]:
201
- "Improve internal structure without changing observable behavior or public API.",
202
- [EditType.FULL_REBUILD]:
203
- "Regenerate the project. Honor the original intent; you may restructure freely.",
204
- [EditType.UPDATE_STYLE]:
205
- "Change styling tokens/rules only. Do not alter component logic or markup structure.",
206
- [EditType.ADD_DEPENDENCY]:
207
- "Add the dependency to the manifest and the minimal wiring required to use it.",
208
- };
209
-
210
- export function buildEditInstructions(type: EditType): string {
211
- return EDIT_INSTRUCTIONS[type];
212
- }
213
-
214
- function isKeyFile(path: string): boolean {
215
- return /(^|\/)App\.[tj]sx?$/.test(path) || /(^|\/)package\.json$/.test(path);
216
- }
217
-
218
- function buildFileStructureSection(manifest: FileManifest): string {
219
- const lines = allFiles(manifest)
220
- .slice()
221
- .sort()
222
- .map((p) => `- ${p} (${manifest.files[p]?.type ?? "other"})`);
223
- return `File structure:\n${lines.join("\n")}`;
224
- }
225
-
226
- export function selectFilesForEdit(prompt: string, manifest: FileManifest): FileContext {
227
- const editIntent = analyzeEditIntent(prompt, manifest);
228
- const everything = allFiles(manifest);
229
-
230
- if (editIntent.type === EditType.FULL_REBUILD) {
231
- const systemPrompt = buildEditInstructions(EditType.FULL_REBUILD);
232
- return {
233
- primaryFiles: [...everything],
234
- contextFiles: [],
235
- systemPrompt,
236
- editIntent,
237
- };
238
- }
239
-
240
- const primarySet = new Set(editIntent.targetFiles);
241
- const primaryFiles = editIntent.targetFiles.slice().sort();
242
-
243
- const contextSet = new Set(everything.filter((p) => !primarySet.has(p)));
244
- for (const p of everything) {
245
- if (isKeyFile(p) && !primarySet.has(p)) {
246
- contextSet.add(p);
247
- }
248
- }
249
- const contextFiles = Array.from(contextSet).sort();
250
-
251
- const systemPrompt = [
252
- buildFileStructureSection(manifest),
253
- "",
254
- buildEditInstructions(editIntent.type),
255
- ].join("\n");
256
-
257
- return { primaryFiles, contextFiles, systemPrompt, editIntent };
258
- }
259
-
260
- // --- fast-apply ----------------------------------------------------------
261
-
262
- /**
263
- * Terse edit-apply format. A model emits zero or more blocks of the form:
264
- *
265
- * <<<edit file="src/App.tsx">>>
266
- * <natural-language instructions describing the change>
267
- * ---
268
- * <the literal updated/replacement snippet>
269
- * <<<end>>>
270
- *
271
- * Text outside well-formed blocks is ignored; malformed blocks are skipped.
272
- */
273
- const EDIT_BLOCK_RE = /<<<edit\s+file="([^"]+)">>>\s*([\s\S]*?)\s*---\s*([\s\S]*?)\s*<<<end>>>/g;
274
-
275
- export function parseEditBlocks(text: string): EditBlock[] {
276
- const blocks: EditBlock[] = [];
277
- for (const match of text.matchAll(EDIT_BLOCK_RE)) {
278
- const targetFile = match[1]?.trim() ?? "";
279
- const instructions = match[2]?.trim() ?? "";
280
- const update = match[3]?.trim() ?? "";
281
- if (targetFile.length === 0) continue;
282
- blocks.push({ targetFile, instructions, update });
283
- }
284
- return blocks;
285
- }
286
-
287
- /**
288
- * A trivial deterministic fallback merger: replaces the original content
289
- * wholesale with the update. Real callers inject an LLM-backed merger.
290
- */
291
- export const fallbackMerger: EditMerger = async (_originalContent, update) => update;
292
-
293
- export async function applyEditBlock(
294
- originalContent: string,
295
- block: EditBlock,
296
- merge: EditMerger = fallbackMerger,
297
- ): Promise<ApplyEditResult> {
298
- try {
299
- const merged = await merge(originalContent, block.update, block.instructions);
300
- return { ok: true, merged };
301
- } catch (error) {
302
- return {
303
- ok: false,
304
- error: error instanceof Error ? error.message : String(error),
305
- };
306
- }
307
- }
308
-
309
- // --- tenant-scoped entry point ------------------------------------------
310
-
311
- function assertTenant(tenantId: string): void {
312
- if (typeof tenantId !== "string" || tenantId.trim().length === 0) {
313
- throw new Error("edit-planner: tenantId is required (fail-closed)");
314
- }
315
- }
316
-
317
- export async function planEdit(
318
- tenantId: string,
319
- prompt: string,
320
- manifest: FileManifest,
321
- ): Promise<FileContext> {
322
- assertTenant(tenantId);
323
- const parsed = fileManifestSchema.parse(manifest);
324
- return selectFilesForEdit(prompt, parsed);
325
- }