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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +30 -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/cair-protocol.md +103 -0
- package/docs/changelog.md +30 -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 +2 -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 +28 -0
- package/src/forge/agent-memory/context-pack.ts +134 -8
- package/src/forge/agent-memory/types.ts +10 -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/explain.ts +113 -1
- 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 +281 -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/delta/store.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
1038
|
-
|
|
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
|
-
|
|
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)) {
|
package/src/forge/version.ts
CHANGED
|
@@ -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,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,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
|
+
});
|