forgeos 0.1.0-alpha.21 → 0.1.0-alpha.22
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/AGENTS.md +1 -1
- package/CHANGELOG.md +17 -2
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/docs/changelog.md +13 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/types.ts +3 -0
- package/src/forge/agent-memory/bridge.ts +12 -0
- package/src/forge/agent-memory/context-pack.ts +106 -8
- package/src/forge/agent-memory/types.ts +4 -1
- package/src/forge/cli/commands.ts +47 -0
- package/src/forge/cli/main.ts +4 -0
- package/src/forge/cli/new.ts +3 -1
- package/src/forge/cli/parse.ts +64 -11
- package/src/forge/cli/studio.ts +54 -0
- package/src/forge/cli/verify.ts +2 -0
- package/src/forge/compiler/frontend-graph/build.ts +58 -2
- package/src/forge/delta/index.ts +12 -0
- package/src/forge/delta/recorder.ts +60 -0
- package/src/forge/delta/status.ts +639 -2
- package/src/forge/delta/store.ts +204 -5
- package/src/forge/delta/timeline.ts +75 -1
- package/src/forge/version.ts +1 -1
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +33 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +17 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +3 -0
package/src/forge/cli/studio.ts
CHANGED
|
@@ -139,6 +139,15 @@ export interface StudioSnapshotResult {
|
|
|
139
139
|
commands: string[];
|
|
140
140
|
diffPlan?: DevConsoleDiffPlan;
|
|
141
141
|
};
|
|
142
|
+
handoff: {
|
|
143
|
+
previewUrl: string;
|
|
144
|
+
currentSession?: { id?: string; title?: string; status?: string; confidence?: number };
|
|
145
|
+
changedFiles?: number;
|
|
146
|
+
generatedState?: string;
|
|
147
|
+
deltaHealth?: string;
|
|
148
|
+
agentContextCommand: string;
|
|
149
|
+
recommendedCommands: string[];
|
|
150
|
+
};
|
|
142
151
|
proofs: {
|
|
143
152
|
preview: StudioAttachResult["preview"]["status"];
|
|
144
153
|
generated: StudioAttachResult["posture"]["generated"];
|
|
@@ -981,6 +990,13 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
981
990
|
: undefined;
|
|
982
991
|
const delta = await runDeltaStatus(appRoot);
|
|
983
992
|
const contextPacket = contextPacketFor({ appRoot, posture, commands });
|
|
993
|
+
const handoff = studioHandoffFor({
|
|
994
|
+
previewUrl: preview.url,
|
|
995
|
+
posture,
|
|
996
|
+
changed: changed.data,
|
|
997
|
+
delta,
|
|
998
|
+
commands,
|
|
999
|
+
});
|
|
984
1000
|
const gitState = (changed.data as { git?: { available?: boolean } }).git;
|
|
985
1001
|
const changedReadable = changed.ok || gitState?.available === false;
|
|
986
1002
|
const ok = posture.state !== "needs-attention" && changedReadable &&
|
|
@@ -1006,6 +1022,7 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
1006
1022
|
changed: changed.data,
|
|
1007
1023
|
commands,
|
|
1008
1024
|
contextPacket,
|
|
1025
|
+
handoff,
|
|
1009
1026
|
proofs: {
|
|
1010
1027
|
preview: preview.status,
|
|
1011
1028
|
generated: posture.generated,
|
|
@@ -1019,6 +1036,43 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
1019
1036
|
};
|
|
1020
1037
|
}
|
|
1021
1038
|
|
|
1039
|
+
function studioHandoffFor(input: {
|
|
1040
|
+
previewUrl: string;
|
|
1041
|
+
posture: StudioSnapshotResult["posture"];
|
|
1042
|
+
changed: Record<string, unknown>;
|
|
1043
|
+
delta: unknown;
|
|
1044
|
+
commands: StudioSnapshotResult["commands"];
|
|
1045
|
+
}): StudioSnapshotResult["handoff"] {
|
|
1046
|
+
const changedSummary = input.changed.summary as { changedFiles?: unknown } | undefined;
|
|
1047
|
+
const deltaRecord = input.delta && typeof input.delta === "object" && !Array.isArray(input.delta)
|
|
1048
|
+
? input.delta as { workSession?: { id?: unknown; title?: unknown; status?: unknown; confidence?: unknown }; details?: { health?: { status?: unknown } } }
|
|
1049
|
+
: undefined;
|
|
1050
|
+
const workSession = deltaRecord?.workSession;
|
|
1051
|
+
return {
|
|
1052
|
+
previewUrl: input.previewUrl,
|
|
1053
|
+
...(workSession
|
|
1054
|
+
? {
|
|
1055
|
+
currentSession: {
|
|
1056
|
+
...(typeof workSession.id === "string" ? { id: workSession.id } : {}),
|
|
1057
|
+
...(typeof workSession.title === "string" ? { title: workSession.title } : {}),
|
|
1058
|
+
...(typeof workSession.status === "string" ? { status: workSession.status } : {}),
|
|
1059
|
+
...(typeof workSession.confidence === "number" ? { confidence: workSession.confidence } : {}),
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
: {}),
|
|
1063
|
+
...(typeof changedSummary?.changedFiles === "number" ? { changedFiles: changedSummary.changedFiles } : {}),
|
|
1064
|
+
generatedState: input.posture.generated?.state,
|
|
1065
|
+
deltaHealth: typeof deltaRecord?.details?.health?.status === "string" ? deltaRecord.details.health.status : undefined,
|
|
1066
|
+
agentContextCommand: "forge agent context --handoff --json",
|
|
1067
|
+
recommendedCommands: [
|
|
1068
|
+
input.commands.handoff,
|
|
1069
|
+
"forge agent context --handoff --json",
|
|
1070
|
+
input.commands.changed,
|
|
1071
|
+
input.commands.doctor,
|
|
1072
|
+
],
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1022
1076
|
export async function runStudioWatchCommand(options: StudioAttachOptions): Promise<StudioWatchResult> {
|
|
1023
1077
|
const snapshot = await runStudioSnapshotCommand(options);
|
|
1024
1078
|
const intervalMs = Math.max(1000, Math.floor(options.intervalMs ?? 5000));
|
package/src/forge/cli/verify.ts
CHANGED
|
@@ -695,6 +695,7 @@ const STRICT_TEST_FALLBACK_MS_BY_PATH: Array<{ pattern: RegExp; estimatedMs: num
|
|
|
695
695
|
{ pattern: /^tests\/templates\/new-b2b-support-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
696
696
|
{ pattern: /^tests\/templates\/new-agent-workroom\.test\.ts$/, estimatedMs: 12_000 },
|
|
697
697
|
{ pattern: /^tests\/templates\/new-minimal-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
698
|
+
{ pattern: /^tests\/templates\/new-nuxt-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
698
699
|
{ pattern: /^tests\/templates\/create-forge-app\.test\.ts$/, estimatedMs: 8_000 },
|
|
699
700
|
];
|
|
700
701
|
const STRICT_ISOLATED_TEST_PATTERNS = [
|
|
@@ -730,6 +731,7 @@ const STRICT_ISOLATED_TEST_PATTERNS = [
|
|
|
730
731
|
/^tests\/templates\/new-b2b-support-web\.test\.ts$/,
|
|
731
732
|
/^tests\/templates\/new-agent-workroom\.test\.ts$/,
|
|
732
733
|
/^tests\/templates\/new-minimal-web\.test\.ts$/,
|
|
734
|
+
/^tests\/templates\/new-nuxt-web\.test\.ts$/,
|
|
733
735
|
/^tests\/telemetry\/telemetry-dev-server\.test\.ts$/,
|
|
734
736
|
];
|
|
735
737
|
const STRICT_SERIAL_TEST_PATTERNS: RegExp[] = [];
|
|
@@ -229,6 +229,58 @@ function detectRouteUses(file: string, text: string, clientManifest: ClientManif
|
|
|
229
229
|
return uses;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function localComposableNames(file: string, text: string): string[] {
|
|
233
|
+
const relName = componentNameForFile(file);
|
|
234
|
+
const names = [relName.startsWith("use") ? relName : `use${relName.slice(0, 1).toUpperCase()}${relName.slice(1)}`];
|
|
235
|
+
for (const match of text.matchAll(/export\s+function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g)) {
|
|
236
|
+
if (match[1]) {
|
|
237
|
+
names.push(match[1]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return uniqueSorted(names);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildComposableUseIndex(
|
|
244
|
+
webRoot: string,
|
|
245
|
+
sourceFiles: string[],
|
|
246
|
+
clientManifest: ClientManifest,
|
|
247
|
+
): Map<string, ReturnType<typeof detectUses>> {
|
|
248
|
+
const index = new Map<string, ReturnType<typeof detectUses>>();
|
|
249
|
+
for (const file of sourceFiles) {
|
|
250
|
+
const rel = toPosix(relative(webRoot, file));
|
|
251
|
+
if (!rel.startsWith("composables/") && !rel.startsWith("src/composables/")) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (rel.endsWith("/forge.ts") || rel === "composables/forge.ts" || rel === "src/composables/forge.ts") {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const text = nodeFileSystem.readText(file) ?? "";
|
|
258
|
+
const uses = detectUses(text, clientManifest);
|
|
259
|
+
for (const name of localComposableNames(file, text)) {
|
|
260
|
+
index.set(name, uses);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return index;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function detectLocalComposableUses(
|
|
267
|
+
text: string,
|
|
268
|
+
composableUseIndex: Map<string, ReturnType<typeof detectUses>>,
|
|
269
|
+
): ReturnType<typeof detectUses> {
|
|
270
|
+
let uses: ReturnType<typeof detectUses> = {
|
|
271
|
+
usesCommands: [],
|
|
272
|
+
usesQueries: [],
|
|
273
|
+
usesLiveQueries: [],
|
|
274
|
+
rawForgeFetches: [],
|
|
275
|
+
};
|
|
276
|
+
for (const [name, composableUses] of composableUseIndex) {
|
|
277
|
+
if (new RegExp(`\\b${name}\\s*\\(`).test(text)) {
|
|
278
|
+
uses = mergeUses(uses, composableUses);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return uses;
|
|
282
|
+
}
|
|
283
|
+
|
|
232
284
|
function devCommandFor(webRoot: string, framework: FrontendGraph["framework"]): string {
|
|
233
285
|
const pkg = readJson<{ scripts?: Record<string, string> }>(join(webRoot, "package.json"));
|
|
234
286
|
if (pkg?.scripts?.dev) {
|
|
@@ -347,6 +399,7 @@ export function buildFrontendGraph(input: {
|
|
|
347
399
|
const bridgeFiles: string[] = [];
|
|
348
400
|
const diagnostics: FrontendGraph["diagnostics"] = [];
|
|
349
401
|
const textByRel = new Map<string, string>();
|
|
402
|
+
const composableUseIndex = buildComposableUseIndex(webRoot, sourceFiles, input.clientManifest);
|
|
350
403
|
|
|
351
404
|
for (const file of sourceFiles) {
|
|
352
405
|
const rel = toPosix(relative(input.workspaceRoot, file));
|
|
@@ -359,7 +412,10 @@ export function buildFrontendGraph(input: {
|
|
|
359
412
|
rel === "web/composables/forge.ts" ||
|
|
360
413
|
rel === "web/plugins/forge.ts";
|
|
361
414
|
textByRel.set(rel, text);
|
|
362
|
-
const uses =
|
|
415
|
+
const uses = mergeUses(
|
|
416
|
+
detectUses(text, input.clientManifest),
|
|
417
|
+
detectLocalComposableUses(text, composableUseIndex),
|
|
418
|
+
);
|
|
363
419
|
if (isComponentFile(webRoot, file, text)) {
|
|
364
420
|
components.push({ name: componentNameForText(file, text), file: rel, ...uses });
|
|
365
421
|
}
|
|
@@ -469,7 +525,7 @@ export function buildFrontendGraph(input: {
|
|
|
469
525
|
? "Nuxt app does not expose a Forge plugin; generated composables may not be wired"
|
|
470
526
|
: "web app does not expose a ForgeProvider; generated hooks may not be wired",
|
|
471
527
|
fixHint: framework === "nuxt"
|
|
472
|
-
? "Create web/plugins/forge.ts and
|
|
528
|
+
? "Create web/plugins/forge.client.ts and web/plugins/forge.server.ts and install ForgeVuePlugin with runtimeConfig.public.forgeUrl and devAuth for local development."
|
|
473
529
|
: "Mount ForgeProvider once in the web app root/provider layer and pass devAuth for local development.",
|
|
474
530
|
suggestedCommands: ["forge inspect frontend --json", `forge make ui --framework ${framework === "nuxt" ? "nuxt" : "vite"} --dry-run --json`],
|
|
475
531
|
docs: ["src/forge/_generated/frontendGraph.json", "AGENTS.md"],
|
package/src/forge/delta/index.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
export {
|
|
2
2
|
runDeltaStatus,
|
|
3
3
|
runDeltaRepair,
|
|
4
|
+
runDeltaCompact,
|
|
5
|
+
runDeltaPrune,
|
|
6
|
+
runDeltaExport,
|
|
7
|
+
runDeltaDoctor,
|
|
4
8
|
formatDeltaStatusHuman,
|
|
5
9
|
formatDeltaStatusJson,
|
|
6
10
|
formatDeltaRepairHuman,
|
|
7
11
|
formatDeltaRepairJson,
|
|
12
|
+
formatDeltaDoctorHuman,
|
|
13
|
+
formatDeltaDoctorJson,
|
|
14
|
+
formatDeltaCompactHuman,
|
|
15
|
+
formatDeltaCompactJson,
|
|
16
|
+
formatDeltaPruneHuman,
|
|
17
|
+
formatDeltaPruneJson,
|
|
18
|
+
formatDeltaExportHuman,
|
|
19
|
+
formatDeltaExportJson,
|
|
8
20
|
} from "./status.ts";
|
|
9
21
|
export { runDeltaTimeline, formatDeltaTimelineHuman, formatDeltaTimelineJson } from "./timeline.ts";
|
|
10
22
|
export { runDeltaExplain, formatDeltaExplainHuman, formatDeltaExplainJson } from "./explain.ts";
|
|
@@ -260,6 +260,27 @@ async function recordSpecializedCommand(
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
if (command.kind === "cair") {
|
|
264
|
+
const cairKind = cairOperationKind(command);
|
|
265
|
+
await store.appendOperation({
|
|
266
|
+
sessionId,
|
|
267
|
+
actorId,
|
|
268
|
+
kind: cairKind,
|
|
269
|
+
summary: cairSummary(command, cairKind, exitCode),
|
|
270
|
+
data: {
|
|
271
|
+
subcommand: command.options.subcommand,
|
|
272
|
+
exitCode,
|
|
273
|
+
...(command.options.query ? { queryVerb: compactCairVerb(command.options.query) } : {}),
|
|
274
|
+
...(command.options.action ? { actionVerb: compactCairVerb(command.options.action) } : {}),
|
|
275
|
+
...(command.options.inputPath ? { inputPath: command.options.inputPath } : {}),
|
|
276
|
+
dryRun: Boolean(command.options.dryRun),
|
|
277
|
+
plan: Boolean(command.options.plan),
|
|
278
|
+
allowGenerated: Boolean(command.options.allowGenerated),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
263
284
|
if (command.kind === "check" || command.kind === "verify") {
|
|
264
285
|
await store.appendOperation({
|
|
265
286
|
sessionId,
|
|
@@ -276,6 +297,45 @@ async function recordSpecializedCommand(
|
|
|
276
297
|
}
|
|
277
298
|
}
|
|
278
299
|
|
|
300
|
+
function cairOperationKind(command: Extract<ForgeCommand, { kind: "cair" }>): string {
|
|
301
|
+
if (command.options.subcommand === "snapshot") {
|
|
302
|
+
return "cair.snapshot.created";
|
|
303
|
+
}
|
|
304
|
+
if (command.options.subcommand === "query") {
|
|
305
|
+
return "cair.query.run";
|
|
306
|
+
}
|
|
307
|
+
const actionVerb = compactCairVerb(command.options.action ?? "");
|
|
308
|
+
if (command.options.plan) {
|
|
309
|
+
return "cair.plan.created";
|
|
310
|
+
}
|
|
311
|
+
if (actionVerb === "A APPLY") {
|
|
312
|
+
return "cair.plan.applied";
|
|
313
|
+
}
|
|
314
|
+
if (command.options.dryRun) {
|
|
315
|
+
return "cair.action.previewed";
|
|
316
|
+
}
|
|
317
|
+
return "cair.action.run";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function cairSummary(command: Extract<ForgeCommand, { kind: "cair" }>, kind: string, exitCode: number): string {
|
|
321
|
+
const suffix = exitCode === 0 ? "completed" : "failed";
|
|
322
|
+
if (command.options.subcommand === "query") {
|
|
323
|
+
return `CAIR query ${compactCairVerb(command.options.query ?? "")} ${suffix}`;
|
|
324
|
+
}
|
|
325
|
+
if (command.options.subcommand === "action") {
|
|
326
|
+
return `CAIR ${kind.replace(/^cair\./, "").replace(/\./g, " ")} ${compactCairVerb(command.options.action ?? "")} ${suffix}`;
|
|
327
|
+
}
|
|
328
|
+
return `CAIR snapshot ${suffix}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function compactCairVerb(input: string): string {
|
|
332
|
+
const parts = input.trim().split(/\s+/u).filter(Boolean);
|
|
333
|
+
if (parts.length === 0) {
|
|
334
|
+
return "unknown";
|
|
335
|
+
}
|
|
336
|
+
return parts.slice(0, 2).join(" ");
|
|
337
|
+
}
|
|
338
|
+
|
|
279
339
|
const noopRecorder: AmbientDeltaRecorder = {
|
|
280
340
|
async recordRuntimeCall() {},
|
|
281
341
|
async recordAgentTool() {},
|