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
@@ -1,4 +1,4 @@
1
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative } from "node:path";
3
3
  import { createPgliteAdapter } from "../runtime/db/pglite-adapter.ts";
4
4
  import type { DbAdapter } from "../runtime/db/adapter.ts";
@@ -160,6 +160,10 @@ export interface DeltaSemanticTimelineFilter extends DeltaTimelineFilter {
160
160
  until?: string;
161
161
  }
162
162
 
163
+ export interface DeltaSemanticTimelineReadOptions {
164
+ refresh?: boolean;
165
+ }
166
+
163
167
  export interface DeltaSemanticTimelineResult {
164
168
  entity?: DeltaTimelineEntityRef;
165
169
  currentState: Record<string, unknown>;
@@ -219,6 +223,27 @@ export interface DeltaStatusDetails {
219
223
  agentMemoryEvents: number;
220
224
  semanticEvents: number;
221
225
  };
226
+ operational: {
227
+ storeExists: boolean;
228
+ storeSizeBytes?: number;
229
+ queuePath: string;
230
+ queueExists: boolean;
231
+ queueSizeBytes: number;
232
+ queuePendingEvents: number;
233
+ queueRedaction: "none" | "redacted" | "legacy-raw-present" | "mixed" | "unknown";
234
+ queueHistoryPath: string;
235
+ queueHistoryExists: boolean;
236
+ queueHistorySizeBytes: number;
237
+ queueHistoryLines: number;
238
+ lastCompactionAt?: string;
239
+ oldestOperationAt?: string;
240
+ newestOperationAt?: string;
241
+ estimatedOverhead: "low" | "medium" | "high";
242
+ };
243
+ health: {
244
+ status: "ok" | "warning";
245
+ checks: Array<{ name: string; status: "ok" | "warning"; message: string }>;
246
+ };
222
247
  }
223
248
 
224
249
  export type DeltaWorkSessionKind = "auto" | "agent" | "human" | "ci" | "git" | "manual-corrected";
@@ -952,12 +977,59 @@ export class DeltaStore {
952
977
  this.adapter.query(`SELECT COUNT(*)::int AS count FROM agent_memory_events`),
953
978
  this.adapter.query(`SELECT COUNT(*)::int AS count FROM timeline_events`),
954
979
  ]);
980
+ const operationWindow = await this.adapter.query(
981
+ `SELECT MIN(timestamp) AS oldest, MAX(timestamp) AS newest FROM operations`,
982
+ );
955
983
  const countAt = (index: number) => Number(countQueries[index]?.rows[0]?.count ?? 0);
956
984
  const lockPath = getDeltaLockPath(this.workspaceRoot);
957
985
  const postmasterPath = join(this.storePath, "postmaster.pid");
986
+ const queuePath = join(this.workspaceRoot, ".forge", "agent", "events.ndjson");
987
+ const queueHistoryPath = join(this.workspaceRoot, ".forge", "agent", "events.ndjson.history");
988
+ const queueExists = existsSync(queuePath);
989
+ const queueSizeBytes = queueExists ? statSync(queuePath).size : 0;
990
+ const queueHistoryExists = existsSync(queueHistoryPath);
991
+ const queueHistoryStat = queueHistoryExists ? statSync(queueHistoryPath) : undefined;
992
+ const queueHistorySizeBytes = queueHistoryStat?.size ?? 0;
993
+ const operationCount = countAt(1);
994
+ const queueRedaction = inspectAgentQueueRedaction(queuePath);
995
+ const queuePendingEvents = countNdjsonLines(queuePath);
996
+ const queueHistoryLines = countNdjsonLines(queueHistoryPath);
958
997
  const storedVersion = meta.get("schemaVersion");
959
998
  const lastOperationId = meta.get("semantic.lastOperationId");
960
999
  const lastRebuildAt = meta.get("semantic.lastRebuildAt");
1000
+ const checks: DeltaStatusDetails["health"]["checks"] = [
1001
+ {
1002
+ name: "schema",
1003
+ status: !storedVersion || storedVersion === DELTA_SCHEMA_VERSION ? "ok" : "warning",
1004
+ message: storedVersion
1005
+ ? `schema ${storedVersion}, expected ${DELTA_SCHEMA_VERSION}`
1006
+ : `schema will initialize as ${DELTA_SCHEMA_VERSION}`,
1007
+ },
1008
+ {
1009
+ name: "locks",
1010
+ status: existsSync(lockPath) ? "warning" : "ok",
1011
+ message: existsSync(lockPath) ? `${normalizePath(relative(this.workspaceRoot, lockPath))} is present` : "no Forge writer lock present",
1012
+ },
1013
+ {
1014
+ name: "queue-redaction",
1015
+ status: queueRedaction === "legacy-raw-present" || queueRedaction === "mixed" ? "warning" : "ok",
1016
+ message: queueRedaction === "none"
1017
+ ? "agent queue is empty or absent"
1018
+ : queueRedaction === "redacted"
1019
+ ? "agent queue contains redacted hook entries"
1020
+ : queueRedaction === "mixed"
1021
+ ? "agent queue contains both redacted and legacy raw hook entries"
1022
+ : queueRedaction === "legacy-raw-present"
1023
+ ? "agent queue contains legacy raw hook entries; drain/compact it"
1024
+ : "agent queue redaction status is unknown",
1025
+ },
1026
+ {
1027
+ name: "semantic-projection",
1028
+ status: lastOperationId ? "ok" : operationCount > 0 ? "warning" : "ok",
1029
+ message: lastOperationId ? `projected through ${lastOperationId}` : operationCount > 0 ? "semantic timeline has not projected operations yet" : "no operations recorded yet",
1030
+ },
1031
+ ];
1032
+ const storeStat = existsSync(this.storePath) ? statSync(this.storePath) : undefined;
961
1033
  return {
962
1034
  schema: {
963
1035
  expectedVersion: DELTA_SCHEMA_VERSION,
@@ -976,7 +1048,7 @@ export class DeltaStore {
976
1048
  },
977
1049
  counts: {
978
1050
  sessions: countAt(0),
979
- operations: countAt(1),
1051
+ operations: operationCount,
980
1052
  fileChanges: countAt(2),
981
1053
  commandRuns: countAt(3),
982
1054
  runtimeCalls: countAt(4),
@@ -986,6 +1058,27 @@ export class DeltaStore {
986
1058
  agentMemoryEvents: countAt(8),
987
1059
  semanticEvents: countAt(9),
988
1060
  },
1061
+ operational: {
1062
+ storeExists: existsSync(this.storePath),
1063
+ ...(storeStat && storeStat.isFile() ? { storeSizeBytes: storeStat.size } : {}),
1064
+ queuePath: normalizePath(relative(this.workspaceRoot, queuePath)),
1065
+ queueExists,
1066
+ queueSizeBytes,
1067
+ queuePendingEvents,
1068
+ queueRedaction,
1069
+ queueHistoryPath: normalizePath(relative(this.workspaceRoot, queueHistoryPath)),
1070
+ queueHistoryExists,
1071
+ queueHistorySizeBytes,
1072
+ queueHistoryLines,
1073
+ ...(queueHistoryStat ? { lastCompactionAt: queueHistoryStat.mtime.toISOString() } : {}),
1074
+ ...(typeof operationWindow.rows[0]?.oldest === "string" ? { oldestOperationAt: String(operationWindow.rows[0].oldest) } : {}),
1075
+ ...(typeof operationWindow.rows[0]?.newest === "string" ? { newestOperationAt: String(operationWindow.rows[0].newest) } : {}),
1076
+ estimatedOverhead: estimatedDeltaOverhead(operationCount, queueSizeBytes),
1077
+ },
1078
+ health: {
1079
+ status: checks.some((check) => check.status === "warning") ? "warning" : "ok",
1080
+ checks,
1081
+ },
989
1082
  };
990
1083
  }
991
1084
 
@@ -1034,8 +1127,13 @@ export class DeltaStore {
1034
1127
  return result.rows.reverse().map(rowToTimelineEntry);
1035
1128
  }
1036
1129
 
1037
- async semanticTimeline(filter: DeltaSemanticTimelineFilter = {}): Promise<DeltaSemanticTimelineResult> {
1038
- await this.ensureSemanticTimelineFresh();
1130
+ async semanticTimeline(
1131
+ filter: DeltaSemanticTimelineFilter = {},
1132
+ options: DeltaSemanticTimelineReadOptions = {},
1133
+ ): Promise<DeltaSemanticTimelineResult> {
1134
+ if (options.refresh !== false) {
1135
+ await this.ensureSemanticTimelineFresh();
1136
+ }
1039
1137
  const limit = Math.max(1, Math.min(filter.limit ?? 50, 200));
1040
1138
  const entity = parseTimelineEntityTarget(filter.target);
1041
1139
  const params: unknown[] = [];
@@ -2254,6 +2352,9 @@ function semanticEventKindForOperation(
2254
2352
  if (context.kind === "git.commit.detected" || context.kind === "git.mapping.detected") {
2255
2353
  return "git.exported";
2256
2354
  }
2355
+ if (context.kind.startsWith("cair.")) {
2356
+ return context.kind;
2357
+ }
2257
2358
  if (context.kind.startsWith("agent.") || context.kind.startsWith("approval.")) {
2258
2359
  return context.kind;
2259
2360
  }
@@ -2304,6 +2405,15 @@ function semanticTitleForOperation(
2304
2405
  if (eventKind === "git.exported") {
2305
2406
  return "Exported to Git";
2306
2407
  }
2408
+ if (eventKind.startsWith("cair.")) {
2409
+ const verb = typeof context.data.actionVerb === "string"
2410
+ ? context.data.actionVerb
2411
+ : typeof context.data.queryVerb === "string"
2412
+ ? context.data.queryVerb
2413
+ : undefined;
2414
+ const label = eventKind.replace(/^cair\./, "").replace(/\./g, " ");
2415
+ return `CAIR ${label}${verb ? `: ${verb}` : ""}`;
2416
+ }
2307
2417
  if (eventKind === "agent.prompt.submitted") {
2308
2418
  return `${agentNameFromContext(context) ?? "Agent"} submitted a prompt`;
2309
2419
  }
@@ -2323,7 +2433,7 @@ function semanticSeverity(eventKind: string): string {
2323
2433
  if (eventKind === "failed" || eventKind === "denied" || eventKind === "proof.failed" || eventKind.endsWith(".failed") || eventKind.endsWith(".denied")) {
2324
2434
  return "error";
2325
2435
  }
2326
- if (eventKind === "proof.passed" || eventKind === "executed" || eventKind.endsWith(".completed")) {
2436
+ if (eventKind === "proof.passed" || eventKind === "executed" || eventKind.endsWith(".completed") || eventKind === "cair.plan.applied") {
2327
2437
  return "success";
2328
2438
  }
2329
2439
  if (eventKind === "policy.changed" || eventKind === "dependency.added" || eventKind === "dependency.upgraded") {
@@ -2372,6 +2482,12 @@ function timelineEntitiesFromContext(
2372
2482
  }
2373
2483
  add("agent", agentNameFromContext(context), "source", 0.95);
2374
2484
  add("agent-tool", context.data.toolName, eventKind.startsWith("agent.tool") ? "primary" : "affected", 0.95);
2485
+ if (context.kind.startsWith("cair.")) {
2486
+ add("cair", "protocol", "source", 0.95);
2487
+ add("cair-action", context.data.actionVerb, eventKind.includes("action") || eventKind.includes("plan") ? "primary" : "affected", 0.9);
2488
+ add("cair-query", context.data.queryVerb, eventKind === "cair.query.run" ? "primary" : "affected", 0.9);
2489
+ add("file", context.data.inputPath, "source", 0.75);
2490
+ }
2375
2491
  for (const service of context.services) {
2376
2492
  add("external-service", service, eventKind === "imported" ? "primary" : "source", 0.88);
2377
2493
  }
@@ -2582,6 +2698,89 @@ function latestEventTimestamp(events: DeltaSemanticTimelineEvent[], kinds: strin
2582
2698
  return [...events].reverse().find((event) => kinds.includes(event.kind))?.timestamp;
2583
2699
  }
2584
2700
 
2701
+ function inspectAgentQueueRedaction(queuePath: string): DeltaStatusDetails["operational"]["queueRedaction"] {
2702
+ if (!existsSync(queuePath)) {
2703
+ return "none";
2704
+ }
2705
+ try {
2706
+ const size = statSync(queuePath).size;
2707
+ if (size === 0) {
2708
+ return "none";
2709
+ }
2710
+ const fd = openSync(queuePath, "r");
2711
+ let sample = "";
2712
+ try {
2713
+ const buffer = Buffer.alloc(Math.min(size, 64_000));
2714
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
2715
+ sample = buffer.subarray(0, bytesRead).toString("utf8");
2716
+ } finally {
2717
+ closeSync(fd);
2718
+ }
2719
+ if (!sample.trim()) {
2720
+ return "none";
2721
+ }
2722
+ const hasRedacted = sample.includes("\"payloadRedacted\":true") || sample.includes("\"rawStored\":false");
2723
+ const hasLegacyRaw = sample.includes("\"raw\":") || sample.includes("\"rawStored\":true");
2724
+ if (hasRedacted && hasLegacyRaw) {
2725
+ return "mixed";
2726
+ }
2727
+ if (hasLegacyRaw) {
2728
+ return "legacy-raw-present";
2729
+ }
2730
+ return hasRedacted ? "redacted" : "unknown";
2731
+ } catch {
2732
+ return "unknown";
2733
+ }
2734
+ }
2735
+
2736
+ function countNdjsonLines(path: string): number {
2737
+ if (!existsSync(path)) {
2738
+ return 0;
2739
+ }
2740
+ try {
2741
+ const size = statSync(path).size;
2742
+ if (size === 0) {
2743
+ return 0;
2744
+ }
2745
+ const fd = openSync(path, "r");
2746
+ let count = 0;
2747
+ let previous = "";
2748
+ try {
2749
+ const buffer = Buffer.alloc(64_000);
2750
+ let position = 0;
2751
+ for (;;) {
2752
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, position);
2753
+ if (bytesRead === 0) {
2754
+ break;
2755
+ }
2756
+ const text = previous + buffer.subarray(0, bytesRead).toString("utf8");
2757
+ const lines = text.split(/\r?\n/u);
2758
+ previous = lines.pop() ?? "";
2759
+ count += lines.filter((line) => line.trim().length > 0).length;
2760
+ position += bytesRead;
2761
+ }
2762
+ if (previous.trim().length > 0) {
2763
+ count += 1;
2764
+ }
2765
+ } finally {
2766
+ closeSync(fd);
2767
+ }
2768
+ return count;
2769
+ } catch {
2770
+ return 0;
2771
+ }
2772
+ }
2773
+
2774
+ function estimatedDeltaOverhead(operationCount: number, queueSizeBytes: number): "low" | "medium" | "high" {
2775
+ if (operationCount > 50_000 || queueSizeBytes > 20_000_000) {
2776
+ return "high";
2777
+ }
2778
+ if (operationCount > 5_000 || queueSizeBytes > 2_000_000) {
2779
+ return "medium";
2780
+ }
2781
+ return "low";
2782
+ }
2783
+
2585
2784
  function semanticOpenQuestions(
2586
2785
  entity: DeltaTimelineEntityRef | undefined,
2587
2786
  currentState: Record<string, unknown>,
@@ -2610,6 +2809,12 @@ function scoreWorkSessionCandidate(
2610
2809
  ): { score: number; signals: DeltaWorkSessionSignal[] } {
2611
2810
  const signals: DeltaWorkSessionSignal[] = [];
2612
2811
  const metadata = candidate.metadata;
2812
+ if (isObservationOnlyContext(context) && !isObservationOnlyWorkSession(metadata)) {
2813
+ return {
2814
+ score: 0.15,
2815
+ signals: [{ signal: "observation-only", weight: 0.15, value: context.commands[0] }],
2816
+ };
2817
+ }
2613
2818
  addSignalIfOverlap(signals, "sameTraceId", 0.4, context.traces, metadata.traces);
2614
2819
  addSignalIfOverlap(signals, "sameManifestService", 0.35, context.services, metadata.services);
2615
2820
  addSignalIfOverlap(signals, "sameRuntimeEntry", 0.3, context.entries, metadata.entries);
@@ -2752,6 +2957,9 @@ function normalizeWorkSessionMetadata(value: Record<string, unknown>): DeltaWork
2752
2957
  }
2753
2958
 
2754
2959
  function inferWorkSessionTitle(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2960
+ if (isObservationOnlyWorkSession(metadata)) {
2961
+ return "Observe project context";
2962
+ }
2755
2963
  if (context.kind === "manifest.imported" && metadata.services[0]) {
2756
2964
  return `Import ${metadata.services[0]} external service`;
2757
2965
  }
@@ -2780,6 +2988,9 @@ function inferWorkSessionTitle(context: DeltaOperationContext, metadata: DeltaWo
2780
2988
  }
2781
2989
 
2782
2990
  function inferIntent(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2991
+ if (isObservationOnlyWorkSession(metadata)) {
2992
+ return "context-gathering";
2993
+ }
2783
2994
  if (context.kind === "manifest.imported" || metadata.fileClusters.includes("manifest.change")) {
2784
2995
  return "external-runtime-import";
2785
2996
  }
@@ -2800,6 +3011,9 @@ function inferIntent(context: DeltaOperationContext, metadata: DeltaWorkSessionM
2800
3011
 
2801
3012
  function summarizeWorkSession(metadata: DeltaWorkSessionMetadata): string {
2802
3013
  const parts: string[] = [];
3014
+ if (isObservationOnlyWorkSession(metadata)) {
3015
+ return `Observed project context with ${metadata.commands.slice(0, 3).join(", ")}.`;
3016
+ }
2803
3017
  if (metadata.services[0]) {
2804
3018
  parts.push(`worked on ${metadata.services[0]}`);
2805
3019
  }
@@ -2819,6 +3033,9 @@ function summarizeWorkSession(metadata: DeltaWorkSessionMetadata): string {
2819
3033
  }
2820
3034
 
2821
3035
  function initialWorkSessionConfidence(context: DeltaOperationContext): number {
3036
+ if (isObservationOnlyContext(context)) {
3037
+ return 0.36;
3038
+ }
2822
3039
  if (context.kind === "manifest.imported") {
2823
3040
  return 0.78;
2824
3041
  }
@@ -2844,6 +3061,65 @@ function isDiagnosticRepairChain(context: DeltaOperationContext, metadata: Delta
2844
3061
  );
2845
3062
  }
2846
3063
 
3064
+ const OBSERVATION_COMMAND_PREFIXES = [
3065
+ "agent context",
3066
+ "agent print-context",
3067
+ "agent timeline",
3068
+ "cair query",
3069
+ "cair snapshot",
3070
+ "changed",
3071
+ "delta status",
3072
+ "doctor",
3073
+ "explain",
3074
+ "handoff",
3075
+ "inspect",
3076
+ "live status",
3077
+ "status",
3078
+ "timeline",
3079
+ ];
3080
+
3081
+ function isObservationOnlyContext(context: DeltaOperationContext): boolean {
3082
+ return (
3083
+ context.commands.length > 0 &&
3084
+ context.files.length === 0 &&
3085
+ context.entries.length === 0 &&
3086
+ context.diagnostics.length === 0 &&
3087
+ context.proofs.length === 0 &&
3088
+ context.services.length === 0 &&
3089
+ context.traces.length === 0 &&
3090
+ context.commands.every(isObservationCommand)
3091
+ );
3092
+ }
3093
+
3094
+ function isObservationOnlyWorkSession(metadata: DeltaWorkSessionMetadata): boolean {
3095
+ return (
3096
+ metadata.commands.length > 0 &&
3097
+ metadata.files.length === 0 &&
3098
+ metadata.entries.length === 0 &&
3099
+ metadata.diagnostics.length === 0 &&
3100
+ metadata.proofs.length === 0 &&
3101
+ metadata.services.length === 0 &&
3102
+ metadata.traces.length === 0 &&
3103
+ metadata.commands.every(isObservationCommand)
3104
+ );
3105
+ }
3106
+
3107
+ function isObservationCommand(command: string): boolean {
3108
+ const normalized = normalizeForgeCommand(command);
3109
+ return OBSERVATION_COMMAND_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix} `));
3110
+ }
3111
+
3112
+ function normalizeForgeCommand(command: string): string {
3113
+ return command
3114
+ .trim()
3115
+ .replace(/^node\s+(?:\.\/)?bin\/forge\.mjs(?:\s+|$)/u, "")
3116
+ .replace(/^bun\s+run\s+forge(?:\s+|$)/u, "")
3117
+ .replace(/^npm\s+run\s+forge\s+--(?:\s+|$)/u, "")
3118
+ .replace(/^forge(?:\s+|$)/u, "")
3119
+ .replace(/\s+/gu, " ")
3120
+ .trim();
3121
+ }
3122
+
2847
3123
  function hasAnyOverlap(...groups: string[][]): boolean {
2848
3124
  for (let index = 0; index < groups.length; index += 2) {
2849
3125
  if (intersects(groups[index] ?? [], groups[index + 1] ?? [])) {
@@ -5,7 +5,16 @@ export interface DeltaTimelineResult {
5
5
  session?: string;
6
6
  target?: string;
7
7
  rebuilt?: boolean;
8
+ causal?: boolean;
9
+ staleProofs?: boolean;
8
10
  timeline: DeltaSemanticTimelineResult;
11
+ summary: {
12
+ events: number;
13
+ causalEdges: number;
14
+ proofStatus?: string;
15
+ staleProofs: Array<{ proof: string; lastRunAt?: string; lastRelevantChangeAt?: string }>;
16
+ causalChains: Array<{ kind: string; from: string; to: string; confidence: number; reason?: Record<string, unknown> }>;
17
+ };
9
18
  exitCode: 0;
10
19
  }
11
20
 
@@ -16,18 +25,24 @@ export async function runDeltaTimeline(input: {
16
25
  session?: string;
17
26
  limit?: number;
18
27
  rebuild?: boolean;
28
+ causal?: boolean;
29
+ staleProofs?: boolean;
19
30
  }): Promise<DeltaTimelineResult> {
20
31
  const store = await DeltaStore.open(input.workspaceRoot, { access: input.rebuild ? "write" : "read" });
21
32
  try {
22
33
  if (input.rebuild) {
23
34
  await store.rebuildSemanticTimeline();
24
35
  }
36
+ const timeline = await store.semanticTimeline({ target: input.target, kind: input.kind, workSessionId: input.session, limit: input.limit });
25
37
  return {
26
38
  ok: true,
27
39
  session: input.session,
28
40
  target: input.target,
29
41
  rebuilt: input.rebuild || undefined,
30
- timeline: await store.semanticTimeline({ target: input.target, kind: input.kind, workSessionId: input.session, limit: input.limit }),
42
+ causal: input.causal || undefined,
43
+ staleProofs: input.staleProofs || undefined,
44
+ timeline,
45
+ summary: summarizeTimeline(timeline),
31
46
  exitCode: 0,
32
47
  };
33
48
  } finally {
@@ -35,6 +50,55 @@ export async function runDeltaTimeline(input: {
35
50
  }
36
51
  }
37
52
 
53
+ function summarizeTimeline(timeline: DeltaSemanticTimelineResult): DeltaTimelineResult["summary"] {
54
+ const eventById = new Map(timeline.events.map((event) => [event.id, event]));
55
+ const proofStatus = typeof timeline.currentState.proofStatus === "string" ? timeline.currentState.proofStatus : undefined;
56
+ const rawProofEntities = uniqueStrings(
57
+ timeline.events.flatMap((event) =>
58
+ event.entities
59
+ .filter((entity) => entity.kind === "proof")
60
+ .map((entity) => entity.name)
61
+ ),
62
+ );
63
+ const proofEntities = timeline.entity?.kind === "proof"
64
+ ? [timeline.entity.name]
65
+ : rawProofEntities.filter((proof) => !/^forge\s/u.test(proof));
66
+ const lastProofRun = latestTimestamp(timeline.events.filter((event) => event.kind === "proof.passed" || event.kind === "proof.failed"));
67
+ const lastRelevantChange = latestTimestamp(timeline.events.filter((event) =>
68
+ event.kind === "modified" ||
69
+ event.kind === "policy.changed" ||
70
+ event.kind === "generated" ||
71
+ event.kind === "imported"
72
+ ));
73
+ return {
74
+ events: timeline.events.length,
75
+ causalEdges: timeline.causalEdges.length,
76
+ ...(proofStatus ? { proofStatus } : {}),
77
+ staleProofs: proofStatus === "stale"
78
+ ? (proofEntities.length > 0 ? proofEntities : ["unknown"]).map((proof) => ({
79
+ proof,
80
+ ...(lastProofRun ? { lastRunAt: lastProofRun } : {}),
81
+ ...(lastRelevantChange ? { lastRelevantChangeAt: lastRelevantChange } : {}),
82
+ }))
83
+ : [],
84
+ causalChains: timeline.causalEdges.slice(0, 12).map((edge) => ({
85
+ kind: edge.kind,
86
+ from: eventById.get(edge.from)?.title ?? edge.from,
87
+ to: eventById.get(edge.to)?.title ?? edge.to,
88
+ confidence: edge.confidence,
89
+ ...(edge.reason ? { reason: edge.reason } : {}),
90
+ })),
91
+ };
92
+ }
93
+
94
+ function latestTimestamp(events: DeltaSemanticTimelineResult["events"]): string | undefined {
95
+ return [...events].sort((left, right) => right.timestamp.localeCompare(left.timestamp))[0]?.timestamp;
96
+ }
97
+
98
+ function uniqueStrings(values: string[]): string[] {
99
+ return [...new Set(values)].sort();
100
+ }
101
+
38
102
  export function formatDeltaTimelineHuman(result: DeltaTimelineResult): string {
39
103
  const timeline = result.timeline;
40
104
  if (timeline.events.length === 0) {
@@ -76,6 +140,16 @@ export function formatDeltaTimelineHuman(result: DeltaTimelineResult): string {
76
140
  }
77
141
  lines.push("");
78
142
  }
143
+ if (result.summary.proofStatus || result.summary.staleProofs.length > 0) {
144
+ lines.push("Proof status");
145
+ if (result.summary.proofStatus) {
146
+ lines.push(` status: ${result.summary.proofStatus}`);
147
+ }
148
+ for (const proof of result.summary.staleProofs) {
149
+ lines.push(` stale: ${proof.proof}${proof.lastRelevantChangeAt ? ` after ${proof.lastRelevantChangeAt}` : ""}`);
150
+ }
151
+ lines.push("");
152
+ }
79
153
  if (timeline.causalEdges.length > 0) {
80
154
  lines.push("Causality");
81
155
  for (const edge of timeline.causalEdges.slice(0, 8)) {
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.21";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.23";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;
@@ -0,0 +1,14 @@
1
+ {
2
+ "files.exclude": {
3
+ "**/.forge": true,
4
+ "**/node_modules": true,
5
+ "**/src/forge/_generated": true,
6
+ "**/forge.lock": true
7
+ },
8
+ "search.exclude": {
9
+ "**/.forge": true,
10
+ "**/node_modules": true,
11
+ "**/src/forge/_generated": true,
12
+ "**/forge.lock": true
13
+ }
14
+ }
@@ -0,0 +1,30 @@
1
+ # __FORGE_APP_TITLE__
2
+
3
+ Nuxt + ForgeOS notes app.
4
+
5
+ Run the full-stack loop:
6
+
7
+ ```bash
8
+ __PACKAGE_MANAGER__ install
9
+ __PACKAGE_MANAGER__ run generate
10
+ __PACKAGE_MANAGER__ run dev
11
+ ```
12
+
13
+ `forge dev` starts both the Forge runtime API and the Nuxt web UI.
14
+
15
+ For agent/CI diagnostics:
16
+
17
+ ```bash
18
+ __PACKAGE_MANAGER__ run forge -- dev --once --json
19
+ ```
20
+
21
+ Generated files and local runtime state are gitignored and hidden from editor search by default. Recreate them with `forge generate`.
22
+
23
+ The Nuxt app reads the Forge API URL from `runtimeConfig.public.forgeUrl`, overrideable with `NUXT_PUBLIC_FORGE_URL`.
24
+
25
+ The web app includes:
26
+
27
+ - `web/plugins/forge.client.ts` and `web/plugins/forge.server.ts` for hydration-safe Forge Vue plugin setup.
28
+ - `web/composables/forge.ts` as the generated-client bridge.
29
+ - `web/composables/useNotes.ts` as a domain composable using `useForgeCommand` and `useForgeLiveQuery`.
30
+ - `web/server/api/forge-health.get.ts` as a minimal Nitro route that reads Nuxt runtime config.
@@ -0,0 +1,3 @@
1
+ export default {
2
+ sourceRoots: ["src"],
3
+ };
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "__FORGE_APP_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "workspaces": [
6
+ "web"
7
+ ],
8
+ "packageManager": "__PACKAGE_MANAGER_SPEC__",
9
+ "scripts": {
10
+ "forge": "forge",
11
+ "generate": "forge generate",
12
+ "check": "forge check",
13
+ "verify": "forge verify --smoke",
14
+ "dev": "forge dev",
15
+ "dev:api": "forge dev --api-only",
16
+ "dev:web": "cd web && __PACKAGE_MANAGER__ run dev",
17
+ "typecheck": "tsc --noEmit && cd web && __PACKAGE_MANAGER__ run typecheck"
18
+ },
19
+ "dependencies": {
20
+ "@electric-sql/pglite": "^0.2.17",
21
+ "forge": "__FORGE_PACKAGE_SPEC__"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "typescript": "^5.7.3"
26
+ },
27
+ "forge": {
28
+ "template": "nuxt-web",
29
+ "sourceRoots": [
30
+ "src"
31
+ ]
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ import { action } from "forge/server";
2
+
3
+ export const logNoteCreated = action({
4
+ event: "note.created",
5
+ handler: async (_ctx, event) => {
6
+ return {
7
+ ok: true,
8
+ event,
9
+ };
10
+ },
11
+ });
@@ -0,0 +1,26 @@
1
+ import { can, command } from "forge/server";
2
+
3
+ export const createNote = command({
4
+ auth: can("notes.create"),
5
+ handler: async (ctx, args) => {
6
+ const input = args as { title?: unknown; body?: unknown };
7
+ const title = typeof input.title === "string" ? input.title.trim() : "";
8
+ if (!title) {
9
+ throw new Error("title is required");
10
+ }
11
+
12
+ const note = await ctx.db.notes.insert({
13
+ title,
14
+ body: typeof input.body === "string" ? input.body : "",
15
+ status: "open",
16
+ createdAt: new Date().toISOString(),
17
+ });
18
+
19
+ await ctx.emit("note.created", {
20
+ id: note.id,
21
+ title: note.title,
22
+ });
23
+
24
+ return note;
25
+ },
26
+ });
@@ -0,0 +1,12 @@
1
+ import { defineTable } from "forge/server";
2
+
3
+ export const notes = defineTable({
4
+ name: "notes",
5
+ fields: {
6
+ id: "uuid",
7
+ title: "text",
8
+ body: "text",
9
+ status: "text",
10
+ createdAt: "timestamp",
11
+ },
12
+ });
@@ -0,0 +1,6 @@
1
+ import { canRole, definePolicies } from "forge/policy";
2
+
3
+ export const policies = definePolicies({
4
+ "notes.read": canRole("owner", "admin", "member"),
5
+ "notes.create": canRole("owner", "admin", "member"),
6
+ });
@@ -0,0 +1,8 @@
1
+ import { can, query } from "forge/server";
2
+
3
+ export const listNotes = query({
4
+ auth: can("notes.read"),
5
+ handler: async (ctx) => {
6
+ return ctx.db.notes.all();
7
+ },
8
+ });