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.
Files changed (49) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +17 -2
  3. package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
  4. package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
  5. package/docs/changelog.md +13 -0
  6. package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
  7. package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
  8. package/package.json +1 -1
  9. package/src/forge/_generated/releaseManifest.json +1 -1
  10. package/src/forge/_generated/releaseManifest.ts +3 -3
  11. package/src/forge/agent-adapters/types.ts +3 -0
  12. package/src/forge/agent-memory/bridge.ts +12 -0
  13. package/src/forge/agent-memory/context-pack.ts +106 -8
  14. package/src/forge/agent-memory/types.ts +4 -1
  15. package/src/forge/cli/commands.ts +47 -0
  16. package/src/forge/cli/main.ts +4 -0
  17. package/src/forge/cli/new.ts +3 -1
  18. package/src/forge/cli/parse.ts +64 -11
  19. package/src/forge/cli/studio.ts +54 -0
  20. package/src/forge/cli/verify.ts +2 -0
  21. package/src/forge/compiler/frontend-graph/build.ts +58 -2
  22. package/src/forge/delta/index.ts +12 -0
  23. package/src/forge/delta/recorder.ts +60 -0
  24. package/src/forge/delta/status.ts +639 -2
  25. package/src/forge/delta/store.ts +204 -5
  26. package/src/forge/delta/timeline.ts +75 -1
  27. package/src/forge/version.ts +1 -1
  28. package/templates/nuxt-web/.vscode/settings.json +14 -0
  29. package/templates/nuxt-web/README.md +30 -0
  30. package/templates/nuxt-web/forge.config.ts +3 -0
  31. package/templates/nuxt-web/package.json +33 -0
  32. package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
  33. package/templates/nuxt-web/src/commands/createNote.ts +26 -0
  34. package/templates/nuxt-web/src/forge/schema.ts +12 -0
  35. package/templates/nuxt-web/src/policies.ts +6 -0
  36. package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
  37. package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
  38. package/templates/nuxt-web/tsconfig.json +17 -0
  39. package/templates/nuxt-web/web/app.vue +67 -0
  40. package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
  41. package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
  42. package/templates/nuxt-web/web/composables/forge.ts +13 -0
  43. package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
  44. package/templates/nuxt-web/web/nuxt.config.ts +11 -0
  45. package/templates/nuxt-web/web/package.json +17 -0
  46. package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
  47. package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
  48. package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
  49. package/templates/nuxt-web/web/tsconfig.json +3 -0
@@ -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));
@@ -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 = detectUses(text, input.clientManifest);
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 call provideForge with runtimeConfig.public.forgeUrl and devAuth for local development."
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"],
@@ -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() {},