forgeos 0.1.0-alpha.21 → 0.1.0-alpha.23

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 (51) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +30 -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/cair-protocol.md +103 -0
  6. package/docs/changelog.md +30 -0
  7. package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
  8. package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
  9. package/package.json +2 -1
  10. package/src/forge/_generated/releaseManifest.json +1 -1
  11. package/src/forge/_generated/releaseManifest.ts +3 -3
  12. package/src/forge/agent-adapters/types.ts +3 -0
  13. package/src/forge/agent-memory/bridge.ts +28 -0
  14. package/src/forge/agent-memory/context-pack.ts +134 -8
  15. package/src/forge/agent-memory/types.ts +10 -1
  16. package/src/forge/cli/commands.ts +47 -0
  17. package/src/forge/cli/main.ts +4 -0
  18. package/src/forge/cli/new.ts +3 -1
  19. package/src/forge/cli/parse.ts +64 -11
  20. package/src/forge/cli/studio.ts +54 -0
  21. package/src/forge/cli/verify.ts +2 -0
  22. package/src/forge/compiler/frontend-graph/build.ts +58 -2
  23. package/src/forge/delta/explain.ts +113 -1
  24. package/src/forge/delta/index.ts +12 -0
  25. package/src/forge/delta/recorder.ts +60 -0
  26. package/src/forge/delta/status.ts +639 -2
  27. package/src/forge/delta/store.ts +281 -5
  28. package/src/forge/delta/timeline.ts +75 -1
  29. package/src/forge/version.ts +1 -1
  30. package/templates/nuxt-web/.vscode/settings.json +14 -0
  31. package/templates/nuxt-web/README.md +30 -0
  32. package/templates/nuxt-web/forge.config.ts +3 -0
  33. package/templates/nuxt-web/package.json +33 -0
  34. package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
  35. package/templates/nuxt-web/src/commands/createNote.ts +26 -0
  36. package/templates/nuxt-web/src/forge/schema.ts +12 -0
  37. package/templates/nuxt-web/src/policies.ts +6 -0
  38. package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
  39. package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
  40. package/templates/nuxt-web/tsconfig.json +17 -0
  41. package/templates/nuxt-web/web/app.vue +67 -0
  42. package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
  43. package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
  44. package/templates/nuxt-web/web/composables/forge.ts +13 -0
  45. package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
  46. package/templates/nuxt-web/web/nuxt.config.ts +11 -0
  47. package/templates/nuxt-web/web/package.json +17 -0
  48. package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
  49. package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
  50. package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
  51. package/templates/nuxt-web/web/tsconfig.json +3 -0
@@ -112,7 +112,7 @@ export type ForgeCommand =
112
112
  json: boolean;
113
113
  workspaceRoot: string;
114
114
  }
115
- | { kind: "doctor"; target?: "project" | "windows" | "agent"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
115
+ | { kind: "doctor"; target?: "project" | "windows" | "agent" | "delta"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
116
116
  | { kind: "setup"; target: "windows"; json: boolean; yes: boolean; workspaceRoot: string }
117
117
  | {
118
118
  kind: "security";
@@ -183,7 +183,19 @@ export type ForgeCommand =
183
183
  | { kind: "ui"; options: UiCommandOptions }
184
184
  | { kind: "manifest"; subcommand: "validate" | "import"; path: string; json: boolean; workspaceRoot: string }
185
185
  | { kind: "import"; options: BrownfieldImportCommandOptions }
186
- | { kind: "delta"; subcommand: "status" | "repair"; json: boolean; workspaceRoot: string; dryRun: boolean; yes: boolean; verbose: boolean }
186
+ | {
187
+ kind: "delta";
188
+ subcommand: "status" | "repair" | "compact" | "prune" | "export";
189
+ json: boolean;
190
+ workspaceRoot: string;
191
+ dryRun: boolean;
192
+ yes: boolean;
193
+ verbose: boolean;
194
+ olderThan?: string;
195
+ output?: string;
196
+ limit?: number;
197
+ redacted: boolean;
198
+ }
187
199
  | { kind: "status"; json: boolean; workspaceRoot: string }
188
200
  | { kind: "changed"; json: boolean; authoredOnly: boolean; workspaceRoot: string }
189
201
  | { kind: "diff"; target: "authored" | "generated" | "full"; json: boolean; workspaceRoot: string }
@@ -212,7 +224,7 @@ export type ForgeCommand =
212
224
  force: boolean;
213
225
  workspaceRoot: string;
214
226
  }
215
- | { kind: "timeline"; target?: string; kindFilter?: string; sessionId?: string; limit?: number; json: boolean; rebuild: boolean; forAgent: boolean; workspaceRoot: string }
227
+ | { kind: "timeline"; target?: string; kindFilter?: string; sessionId?: string; limit?: number; json: boolean; rebuild: boolean; forAgent: boolean; causal: boolean; staleProofs: boolean; workspaceRoot: string }
216
228
  | { kind: "explain"; thing: string; json: boolean; workspaceRoot: string }
217
229
  | {
218
230
  kind: "session";
@@ -480,7 +492,7 @@ export const INSPECT_TARGETS: InspectTarget[] = [
480
492
  "map",
481
493
  ];
482
494
 
483
- const NEW_TEMPLATES: NewTemplateName[] = ["agent-workroom", "b2b-support-web", "minimal-web"];
495
+ const NEW_TEMPLATES: NewTemplateName[] = ["agent-workroom", "b2b-support-web", "minimal-web", "nuxt-web"];
484
496
  const NEW_PACKAGE_MANAGERS: NewPackageManager[] = ["bun", "npm", "pnpm", "yarn"];
485
497
  const SELF_HOST_SUBCOMMANDS: SelfHostSubcommand[] = ["compose", "env", "check", "clean"];
486
498
  const AGENT_CONTRACT_SUBCOMMANDS: AgentContractSubcommand[] = [
@@ -1015,6 +1027,22 @@ export function parseCli(argv: string[]): ParsedCli {
1015
1027
  (subcommand === "timeline" ? rest[1] : undefined) ??
1016
1028
  (subcommand === "timeline" ? "all" : undefined) ??
1017
1029
  (subcommand === "hooks" || subcommand === "onboard" ? "codex" : "generic");
1030
+ const contextOptionValues = new Set(
1031
+ [
1032
+ parseOptionValue(argv, "--entry"),
1033
+ parseOptionValue(argv, "--change"),
1034
+ parseOptionValue(argv, "--proof"),
1035
+ parseOptionValue(argv, "--event"),
1036
+ parseOptionValue(argv, "--input"),
1037
+ parseOptionValue(argv, "--target"),
1038
+ parseOptionValue(argv, "--file"),
1039
+ limitRaw,
1040
+ pollIntervalRaw,
1041
+ ].filter((value): value is string => typeof value === "string"),
1042
+ );
1043
+ const contextEntry = subcommand === "context"
1044
+ ? rest.slice(1).find((part) => !part.startsWith("--") && !contextOptionValues.has(part))
1045
+ : undefined;
1018
1046
  return {
1019
1047
  command: {
1020
1048
  kind: "agent",
@@ -1031,7 +1059,10 @@ export function parseCli(argv: string[]): ParsedCli {
1031
1059
  eventName: parseOptionValue(argv, "--event"),
1032
1060
  hookAction: subcommand === "hooks" ? rest[1] : undefined,
1033
1061
  input,
1034
- entry: parseOptionValue(argv, "--entry") ?? (subcommand === "context" && rest[1] !== "--current" ? rest[1] : undefined),
1062
+ entry: parseOptionValue(argv, "--entry") ?? contextEntry,
1063
+ change: parseOptionValue(argv, "--change"),
1064
+ proof: parseOptionValue(argv, "--proof"),
1065
+ handoff: parseFlag(argv, "--handoff"),
1035
1066
  current: parseFlag(argv, "--current"),
1036
1067
  limit: limit ? Math.floor(limit) : undefined,
1037
1068
  watch: parseFlag(argv, "--watch"),
@@ -1152,14 +1183,14 @@ export function parseCli(argv: string[]): ParsedCli {
1152
1183
  };
1153
1184
  }
1154
1185
  case "doctor":
1155
- if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent") {
1156
- errors.push("forge doctor supports subcommand: windows or agent");
1186
+ if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent" && rest[0] !== "delta") {
1187
+ errors.push("forge doctor supports subcommand: windows, agent, or delta");
1157
1188
  return { command: null, workspaceRoot, errors };
1158
1189
  }
1159
1190
  return {
1160
1191
  command: {
1161
1192
  kind: "doctor",
1162
- target: rest[0] === "windows" ? "windows" : rest[0] === "agent" ? "agent" : "project",
1193
+ target: rest[0] === "windows" ? "windows" : rest[0] === "agent" ? "agent" : rest[0] === "delta" ? "delta" : "project",
1163
1194
  agentTarget: rest[0] === "agent"
1164
1195
  ? (parseOptionValue(argv, "--target") as AgentAdapterTarget | undefined) ?? (rest[1] as AgentAdapterTarget | undefined) ?? "codex"
1165
1196
  : undefined,
@@ -1882,10 +1913,12 @@ export function parseCli(argv: string[]): ParsedCli {
1882
1913
  }
1883
1914
  case "delta": {
1884
1915
  const subcommand = rest[0];
1885
- if (subcommand !== "status" && subcommand !== "repair") {
1886
- errors.push("forge delta requires subcommand: status or repair");
1916
+ if (subcommand !== "status" && subcommand !== "repair" && subcommand !== "compact" && subcommand !== "prune" && subcommand !== "export") {
1917
+ errors.push("forge delta requires subcommand: status, repair, compact, prune, or export");
1887
1918
  return { command: null, workspaceRoot, errors };
1888
1919
  }
1920
+ const limitRaw = parseOptionValue(argv, "--limit");
1921
+ const limit = limitRaw ? Number(limitRaw) : undefined;
1889
1922
  return {
1890
1923
  command: {
1891
1924
  kind: "delta",
@@ -1894,6 +1927,10 @@ export function parseCli(argv: string[]): ParsedCli {
1894
1927
  dryRun: parseFlag(argv, "--dry-run"),
1895
1928
  yes: parseFlag(argv, "--yes"),
1896
1929
  verbose: parseFlag(argv, "--verbose"),
1930
+ olderThan: parseOptionValue(argv, "--older-than"),
1931
+ output: parseOptionValue(argv, "--output"),
1932
+ limit: Number.isFinite(limit) ? limit : undefined,
1933
+ redacted: parseFlag(argv, "--redacted"),
1897
1934
  workspaceRoot,
1898
1935
  },
1899
1936
  workspaceRoot,
@@ -1933,7 +1970,10 @@ export function parseCli(argv: string[]): ParsedCli {
1933
1970
  const kindFilter = parseOptionValue(argv, "--kind");
1934
1971
  const sessionId = parseOptionValue(argv, "--session");
1935
1972
  const rebuild = rest[0] === "rebuild";
1936
- const target = rebuild ? undefined : rest.find((item) => item !== kindFilter && item !== limitRaw && item !== sessionId);
1973
+ const optionValues = new Set([limitRaw, kindFilter, sessionId].filter((value): value is string => typeof value === "string"));
1974
+ const target = rebuild
1975
+ ? undefined
1976
+ : rest.find((item) => !item.startsWith("--") && !optionValues.has(item));
1937
1977
  const limit = limitRaw ? Number(limitRaw) : undefined;
1938
1978
  if (limitRaw !== undefined && (!Number.isFinite(limit) || limit! < 1)) {
1939
1979
  errors.push("--limit must be a number >= 1");
@@ -1948,6 +1988,8 @@ export function parseCli(argv: string[]): ParsedCli {
1948
1988
  json: parseFlag(argv, "--json"),
1949
1989
  rebuild,
1950
1990
  forAgent: parseFlag(argv, "--for-agent"),
1991
+ causal: parseFlag(argv, "--causal"),
1992
+ staleProofs: parseFlag(argv, "--stale-proofs"),
1951
1993
  workspaceRoot,
1952
1994
  },
1953
1995
  workspaceRoot,
@@ -2642,6 +2684,8 @@ export function hasUnknownOption(argv: string[]): string | null {
2642
2684
  "--json",
2643
2685
  "--human",
2644
2686
  "--for-agent",
2687
+ "--causal",
2688
+ "--stale-proofs",
2645
2689
  "--dry-run",
2646
2690
  "--plan",
2647
2691
  "--staged",
@@ -2698,6 +2742,9 @@ export function hasUnknownOption(argv: string[]): string | null {
2698
2742
  "--emit",
2699
2743
  "--event",
2700
2744
  "--entry",
2745
+ "--change",
2746
+ "--proof",
2747
+ "--handoff",
2701
2748
  "--current",
2702
2749
  "--trigger",
2703
2750
  "--component",
@@ -2794,6 +2841,8 @@ export function hasUnknownOption(argv: string[]): string | null {
2794
2841
  "--env-file",
2795
2842
  "--skip-startup-console",
2796
2843
  "--redacted",
2844
+ "--older-than",
2845
+ "--output",
2797
2846
  "--mock-ai",
2798
2847
  "--ai",
2799
2848
  "--provider",
@@ -2857,6 +2906,8 @@ export function hasUnknownOption(argv: string[]): string | null {
2857
2906
  arg === "--emit" ||
2858
2907
  arg === "--event" ||
2859
2908
  arg === "--entry" ||
2909
+ arg === "--change" ||
2910
+ arg === "--proof" ||
2860
2911
  arg === "--trigger" ||
2861
2912
  arg === "--component" ||
2862
2913
  arg === "--package" ||
@@ -2901,6 +2952,8 @@ export function hasUnknownOption(argv: string[]): string | null {
2901
2952
  arg === "--db" ||
2902
2953
  arg === "--database-url" ||
2903
2954
  arg === "--limit" ||
2955
+ arg === "--older-than" ||
2956
+ arg === "--output" ||
2904
2957
  arg === "--kind" ||
2905
2958
  arg === "--session" ||
2906
2959
  arg === "--input" ||
@@ -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,3 +1,5 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { DeltaStore } from "./store.ts";
2
4
 
3
5
  export interface DeltaExplainResult {
@@ -13,10 +15,11 @@ export async function runDeltaExplain(input: {
13
15
  }): Promise<DeltaExplainResult> {
14
16
  const store = await DeltaStore.open(input.workspaceRoot, { access: "read" });
15
17
  try {
18
+ const explanation = await store.explain(input.thing);
16
19
  return {
17
20
  ok: true,
18
21
  thing: input.thing,
19
- explanation: await store.explain(input.thing),
22
+ explanation: enrichWithCurrentAgentContract(input.workspaceRoot, input.thing, explanation),
20
23
  exitCode: 0,
21
24
  };
22
25
  } finally {
@@ -68,6 +71,9 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
68
71
  lines.push("Runtime:");
69
72
  lines.push(` kind: ${String(runtime.entry_kind ?? "unknown")}`);
70
73
  lines.push(` result: ${String(runtime.result ?? "unknown")}`);
74
+ if (runtime.source) {
75
+ lines.push(` source: ${String(runtime.source)}`);
76
+ }
71
77
  if (runtime.diagnostic_code) {
72
78
  lines.push(` diagnostic: ${String(runtime.diagnostic_code)}`);
73
79
  }
@@ -94,6 +100,22 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
94
100
  }
95
101
  }
96
102
  }
103
+ const currentContract = explanation.currentContract as Record<string, unknown> | null | undefined;
104
+ if (currentContract) {
105
+ lines.push("");
106
+ lines.push("Current contract:");
107
+ lines.push(` kind: ${String(currentContract.kind ?? "unknown")}`);
108
+ lines.push(` name: ${String(currentContract.name ?? result.thing)}`);
109
+ if (currentContract.auth) {
110
+ lines.push(` auth: ${String(currentContract.auth)}`);
111
+ }
112
+ if (currentContract.policy) {
113
+ lines.push(` policy: ${String(currentContract.policy)}`);
114
+ }
115
+ if (currentContract.sourceFile) {
116
+ lines.push(` file: ${String(currentContract.sourceFile)}`);
117
+ }
118
+ }
97
119
  lines.push("");
98
120
  lines.push("Introduced in:");
99
121
  if (workSessions.length === 0) {
@@ -124,3 +146,93 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
124
146
  export function formatDeltaExplainJson(result: DeltaExplainResult): string {
125
147
  return `${JSON.stringify(result, null, 2)}\n`;
126
148
  }
149
+
150
+ function enrichWithCurrentAgentContract(
151
+ workspaceRoot: string,
152
+ thing: string,
153
+ explanation: Record<string, unknown>,
154
+ ): Record<string, unknown> {
155
+ const currentContract = currentContractForThing(workspaceRoot, thing);
156
+ if (!currentContract) {
157
+ return explanation;
158
+ }
159
+ const runtime = explanation.runtime && typeof explanation.runtime === "object"
160
+ ? explanation.runtime as Record<string, unknown>
161
+ : null;
162
+ return {
163
+ ...explanation,
164
+ type: explanation.type === "unknown" ? "runtime-entry" : explanation.type,
165
+ runtime: runtime ?? {
166
+ entry_name: currentContract.name,
167
+ entry_kind: currentContract.kind,
168
+ result: "defined",
169
+ source: "agentContract",
170
+ ...(currentContract.policy ? { policy: currentContract.policy } : {}),
171
+ ...(typeof currentContract.tenantScoped === "boolean" ? { tenant_scoped: currentContract.tenantScoped } : {}),
172
+ ...(typeof currentContract.needsApproval === "boolean" ? { needs_approval: currentContract.needsApproval } : {}),
173
+ },
174
+ currentContract,
175
+ };
176
+ }
177
+
178
+ function currentContractForThing(workspaceRoot: string, thing: string): Record<string, unknown> | undefined {
179
+ const contractPath = join(workspaceRoot, "src", "forge", "_generated", "agentContract.json");
180
+ if (!existsSync(contractPath)) {
181
+ return undefined;
182
+ }
183
+ try {
184
+ const contract = JSON.parse(readFileSync(contractPath, "utf8")) as Record<string, unknown>;
185
+ const collections: Array<[keyof typeof runtimeKinds, string]> = [
186
+ ["commands", "command"],
187
+ ["queries", "query"],
188
+ ["liveQueries", "liveQuery"],
189
+ ["actions", "action"],
190
+ ["workflows", "workflow"],
191
+ ];
192
+ for (const [collection, kind] of collections) {
193
+ const entries = Array.isArray(contract[collection]) ? contract[collection] as Record<string, unknown>[] : [];
194
+ for (const entry of entries) {
195
+ const name = runtimeEntryName(entry);
196
+ if (!name) {
197
+ continue;
198
+ }
199
+ if (name === thing || `${kind}:${name}` === thing || entry.id === thing || entry.exportName === thing) {
200
+ return {
201
+ source: "src/forge/_generated/agentContract.json",
202
+ kind,
203
+ name,
204
+ ...(stringValue(entry.auth) ? { auth: stringValue(entry.auth) } : {}),
205
+ ...(stringValue(entry.policy) ? { policy: stringValue(entry.policy) } : {}),
206
+ ...(typeof entry.tenantScoped === "boolean" ? { tenantScoped: entry.tenantScoped } : {}),
207
+ ...(typeof entry.needsApproval === "boolean" ? { needsApproval: entry.needsApproval } : {}),
208
+ ...(stringValue(entry.risk) ? { risk: stringValue(entry.risk) } : {}),
209
+ ...(stringValue(entry.file) ? { sourceFile: stringValue(entry.file) } : {}),
210
+ ...(stringValue(entry.path) ? { sourceFile: stringValue(entry.path) } : {}),
211
+ };
212
+ }
213
+ }
214
+ }
215
+ } catch {
216
+ return undefined;
217
+ }
218
+ return undefined;
219
+ }
220
+
221
+ const runtimeKinds = {
222
+ commands: "command",
223
+ queries: "query",
224
+ liveQueries: "liveQuery",
225
+ actions: "action",
226
+ workflows: "workflow",
227
+ } as const;
228
+
229
+ function runtimeEntryName(entry: Record<string, unknown>): string | undefined {
230
+ return stringValue(entry.name)
231
+ ?? stringValue(entry.exportName)
232
+ ?? stringValue(entry.id)
233
+ ?? stringValue(entry.entryName);
234
+ }
235
+
236
+ function stringValue(value: unknown): string | undefined {
237
+ return typeof value === "string" && value.length > 0 ? value : undefined;
238
+ }
@@ -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() {},