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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
import { createDiagnostic } from "../compiler/diagnostics/create.ts";
|
|
5
5
|
import { normalizePath } from "../compiler/primitives/paths.ts";
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type DeltaStoreBusyInfo,
|
|
16
16
|
} from "./store.ts";
|
|
17
17
|
import { DELTA_SCHEMA_VERSION } from "./schema.ts";
|
|
18
|
+
import { redactDeltaPayload } from "./redaction.ts";
|
|
18
19
|
|
|
19
20
|
export type DeltaStatusResult =
|
|
20
21
|
| (DeltaStatus & { details?: DeltaStatusDetails; exitCode: 0 })
|
|
@@ -51,6 +52,88 @@ export interface DeltaRepairResult {
|
|
|
51
52
|
exitCode: 0 | 1;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export interface DeltaCompactOptions {
|
|
56
|
+
workspaceRoot: string;
|
|
57
|
+
dryRun?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DeltaPruneOptions {
|
|
61
|
+
workspaceRoot: string;
|
|
62
|
+
olderThan?: string;
|
|
63
|
+
dryRun?: boolean;
|
|
64
|
+
yes?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DeltaExportOptions {
|
|
68
|
+
workspaceRoot: string;
|
|
69
|
+
redacted?: boolean;
|
|
70
|
+
output?: string;
|
|
71
|
+
limit?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DeltaMaintenanceFileResult {
|
|
75
|
+
path: string;
|
|
76
|
+
exists: boolean;
|
|
77
|
+
beforeBytes: number;
|
|
78
|
+
afterBytes: number;
|
|
79
|
+
linesBefore: number;
|
|
80
|
+
linesAfter: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DeltaCompactResult {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
subcommand: "compact";
|
|
86
|
+
applied: boolean;
|
|
87
|
+
dryRun: boolean;
|
|
88
|
+
files: DeltaMaintenanceFileResult[];
|
|
89
|
+
diagnostics: ReturnType<typeof createDiagnostic>[];
|
|
90
|
+
nextActions: string[];
|
|
91
|
+
exitCode: 0 | 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DeltaPruneResult {
|
|
95
|
+
ok: boolean;
|
|
96
|
+
subcommand: "prune";
|
|
97
|
+
applied: boolean;
|
|
98
|
+
needsConfirmation: boolean;
|
|
99
|
+
olderThan?: string;
|
|
100
|
+
cutoff?: string;
|
|
101
|
+
files: Array<DeltaMaintenanceFileResult & { prunedLines: number }>;
|
|
102
|
+
diagnostics: ReturnType<typeof createDiagnostic>[];
|
|
103
|
+
nextActions: string[];
|
|
104
|
+
exitCode: 0 | 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DeltaExportResult {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
subcommand: "export";
|
|
110
|
+
redacted: boolean;
|
|
111
|
+
output?: string;
|
|
112
|
+
written: boolean;
|
|
113
|
+
data?: Record<string, unknown>;
|
|
114
|
+
busy?: DeltaStoreBusyInfo;
|
|
115
|
+
diagnostics: ReturnType<typeof createDiagnostic>[];
|
|
116
|
+
nextActions: string[];
|
|
117
|
+
exitCode: 0 | 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DeltaDoctorCheck {
|
|
121
|
+
name: string;
|
|
122
|
+
ok: boolean;
|
|
123
|
+
severity: "error" | "warning";
|
|
124
|
+
message: string;
|
|
125
|
+
evidence?: Record<string, unknown>;
|
|
126
|
+
suggestedCommands?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface DeltaDoctorResult {
|
|
130
|
+
ok: boolean;
|
|
131
|
+
checks: DeltaDoctorCheck[];
|
|
132
|
+
status?: DeltaStatusResult;
|
|
133
|
+
nextActions: string[];
|
|
134
|
+
exitCode: 0 | 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
54
137
|
async function openDeltaStoreForStatus(
|
|
55
138
|
workspaceRoot: string,
|
|
56
139
|
): Promise<{ store: DeltaStore | null; openError?: unknown }> {
|
|
@@ -127,6 +210,31 @@ function pgliteStatusDetails(workspaceRoot: string, storePath: string): DeltaSta
|
|
|
127
210
|
agentMemoryEvents: 0,
|
|
128
211
|
semanticEvents: 0,
|
|
129
212
|
},
|
|
213
|
+
operational: {
|
|
214
|
+
storeExists: existsSync(join(workspaceRoot, storePath)),
|
|
215
|
+
queuePath: ".forge/agent/events.ndjson",
|
|
216
|
+
queueExists: existsSync(join(workspaceRoot, ".forge", "agent", "events.ndjson")),
|
|
217
|
+
queueSizeBytes: existsSync(join(workspaceRoot, ".forge", "agent", "events.ndjson"))
|
|
218
|
+
? statSync(join(workspaceRoot, ".forge", "agent", "events.ndjson")).size
|
|
219
|
+
: 0,
|
|
220
|
+
queuePendingEvents: 0,
|
|
221
|
+
queueRedaction: "unknown",
|
|
222
|
+
queueHistoryPath: ".forge/agent/events.ndjson.history",
|
|
223
|
+
queueHistoryExists: existsSync(join(workspaceRoot, ".forge", "agent", "events.ndjson.history")),
|
|
224
|
+
queueHistorySizeBytes: existsSync(join(workspaceRoot, ".forge", "agent", "events.ndjson.history"))
|
|
225
|
+
? statSync(join(workspaceRoot, ".forge", "agent", "events.ndjson.history")).size
|
|
226
|
+
: 0,
|
|
227
|
+
queueHistoryLines: 0,
|
|
228
|
+
estimatedOverhead: "low",
|
|
229
|
+
},
|
|
230
|
+
health: {
|
|
231
|
+
status: "ok",
|
|
232
|
+
checks: [
|
|
233
|
+
{ name: "schema", status: "ok", message: `schema expected ${DELTA_SCHEMA_VERSION}` },
|
|
234
|
+
{ name: "locks", status: "ok", message: "PGlite postmaster indicates an active local runtime" },
|
|
235
|
+
{ name: "queue-redaction", status: "ok", message: "queue redaction is checked by the active writer" },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
130
238
|
};
|
|
131
239
|
}
|
|
132
240
|
|
|
@@ -375,6 +483,453 @@ export async function runDeltaRepair(options: DeltaRepairOptions): Promise<Delta
|
|
|
375
483
|
}
|
|
376
484
|
}
|
|
377
485
|
|
|
486
|
+
export async function runDeltaDoctor(workspaceRoot: string): Promise<DeltaDoctorResult> {
|
|
487
|
+
const status = await runDeltaStatus(workspaceRoot, { verbose: true });
|
|
488
|
+
const details = status.exitCode === 0 ? status.details : undefined;
|
|
489
|
+
const checks: DeltaDoctorCheck[] = [
|
|
490
|
+
{
|
|
491
|
+
name: "delta-status",
|
|
492
|
+
ok: status.exitCode === 0,
|
|
493
|
+
severity: "error",
|
|
494
|
+
message: status.exitCode === 0 ? "DeltaDB status is readable" : "DeltaDB status is unavailable",
|
|
495
|
+
evidence: { store: status.store },
|
|
496
|
+
suggestedCommands: status.exitCode === 0 ? undefined : status.nextActions,
|
|
497
|
+
},
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
const busy = probeDeltaStoreBusy(workspaceRoot);
|
|
501
|
+
if (busy) {
|
|
502
|
+
const busyInfo = describeDeltaStoreBusy(busy, workspaceRoot);
|
|
503
|
+
checks.push({
|
|
504
|
+
name: "delta-writable",
|
|
505
|
+
ok: false,
|
|
506
|
+
severity: "warning",
|
|
507
|
+
message: busyInfo.processAlive
|
|
508
|
+
? `Delta writer lock is held by pid ${busyInfo.pid ?? "unknown"}`
|
|
509
|
+
: `Delta writer lock is present at ${busyInfo.relativeLockPath}`,
|
|
510
|
+
evidence: { busy: busyInfo },
|
|
511
|
+
suggestedCommands: ["forge delta status --verbose --json"],
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
let writer: DeltaStore | null = null;
|
|
515
|
+
try {
|
|
516
|
+
writer = await DeltaStore.open(workspaceRoot, { access: "write" });
|
|
517
|
+
checks.push({
|
|
518
|
+
name: "delta-writable",
|
|
519
|
+
ok: true,
|
|
520
|
+
severity: "error",
|
|
521
|
+
message: "DeltaDB writer lock can be acquired",
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error instanceof DeltaStoreBusyError) {
|
|
525
|
+
const busyInfo = describeDeltaStoreBusy(error, workspaceRoot);
|
|
526
|
+
checks.push({
|
|
527
|
+
name: "delta-writable",
|
|
528
|
+
ok: false,
|
|
529
|
+
severity: "warning",
|
|
530
|
+
message: busyInfo.relativeLockPath.endsWith("postmaster.pid")
|
|
531
|
+
? "Delta writer is currently held by an active local PGlite runtime"
|
|
532
|
+
: busyInfo.processAlive
|
|
533
|
+
? `Delta writer lock is held by pid ${busyInfo.pid ?? "unknown"}`
|
|
534
|
+
: `Delta writer lock is present at ${busyInfo.relativeLockPath}`,
|
|
535
|
+
evidence: { busy: busyInfo },
|
|
536
|
+
suggestedCommands: ["forge delta status --verbose --json"],
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
checks.push({
|
|
540
|
+
name: "delta-writable",
|
|
541
|
+
ok: false,
|
|
542
|
+
severity: "error",
|
|
543
|
+
message: error instanceof Error ? error.message : "DeltaDB writer lock cannot be acquired",
|
|
544
|
+
suggestedCommands: ["forge delta repair --dry-run --json"],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
} finally {
|
|
548
|
+
await writer?.close().catch(() => undefined);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const schemaOk = !details?.schema.storedVersion || details.schema.storedVersion === details.schema.expectedVersion;
|
|
553
|
+
checks.push({
|
|
554
|
+
name: "schema-current",
|
|
555
|
+
ok: Boolean(details) && schemaOk,
|
|
556
|
+
severity: "error",
|
|
557
|
+
message: details
|
|
558
|
+
? `schema ${details.schema.storedVersion ?? "not initialized"}; expected ${details.schema.expectedVersion}`
|
|
559
|
+
: "schema details unavailable",
|
|
560
|
+
evidence: details?.schema,
|
|
561
|
+
suggestedCommands: schemaOk ? undefined : ["forge delta repair --dry-run --json"],
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const pendingEvents = details?.operational.queuePendingEvents ?? 0;
|
|
565
|
+
checks.push({
|
|
566
|
+
name: "queue-drain",
|
|
567
|
+
ok: Boolean(details) && pendingEvents === 0,
|
|
568
|
+
severity: "warning",
|
|
569
|
+
message: details
|
|
570
|
+
? pendingEvents === 0
|
|
571
|
+
? "agent queue has no pending events"
|
|
572
|
+
: `agent queue has ${pendingEvents} pending event${pendingEvents === 1 ? "" : "s"}`
|
|
573
|
+
: "queue details unavailable",
|
|
574
|
+
evidence: details
|
|
575
|
+
? {
|
|
576
|
+
queuePath: details.operational.queuePath,
|
|
577
|
+
pendingEvents,
|
|
578
|
+
queueSizeBytes: details.operational.queueSizeBytes,
|
|
579
|
+
}
|
|
580
|
+
: undefined,
|
|
581
|
+
suggestedCommands: pendingEvents > 0 ? ["forge agent ingest codex --file .forge/agent/events.ndjson --json"] : undefined,
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const redaction = details?.operational.queueRedaction ?? "unknown";
|
|
585
|
+
checks.push({
|
|
586
|
+
name: "queue-redaction",
|
|
587
|
+
ok: redaction === "none" || redaction === "redacted",
|
|
588
|
+
severity: "warning",
|
|
589
|
+
message: redaction === "none"
|
|
590
|
+
? "agent queue is empty or absent"
|
|
591
|
+
: redaction === "redacted"
|
|
592
|
+
? "agent queue contains redacted payloads"
|
|
593
|
+
: `agent queue redaction status is ${redaction}`,
|
|
594
|
+
evidence: details
|
|
595
|
+
? {
|
|
596
|
+
queuePath: details.operational.queuePath,
|
|
597
|
+
queueRedaction: redaction,
|
|
598
|
+
queueHistoryPath: details.operational.queueHistoryPath,
|
|
599
|
+
queueHistoryLines: details.operational.queueHistoryLines,
|
|
600
|
+
lastCompactionAt: details.operational.lastCompactionAt,
|
|
601
|
+
}
|
|
602
|
+
: undefined,
|
|
603
|
+
suggestedCommands: redaction === "legacy-raw-present" || redaction === "mixed"
|
|
604
|
+
? ["forge agent ingest codex --file .forge/agent/events.ndjson --json", "forge delta compact --json"]
|
|
605
|
+
: undefined,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const gitignore = readGitignore(workspaceRoot);
|
|
609
|
+
const requiredGitignore = [".forge/delta/", ".forge/agent/*.ndjson", ".forge/studio"];
|
|
610
|
+
const missingGitignore = requiredGitignore.filter((entry) => !gitignore.includes(entry));
|
|
611
|
+
checks.push({
|
|
612
|
+
name: "gitignore-operational-state",
|
|
613
|
+
ok: missingGitignore.length === 0,
|
|
614
|
+
severity: "warning",
|
|
615
|
+
message: missingGitignore.length === 0
|
|
616
|
+
? "local Delta, agent queue, and Studio state are ignored"
|
|
617
|
+
: `missing gitignore coverage: ${missingGitignore.join(", ")}`,
|
|
618
|
+
evidence: { required: requiredGitignore, missing: missingGitignore },
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const ok = checks.every((check) => check.ok || check.severity === "warning");
|
|
622
|
+
return {
|
|
623
|
+
ok,
|
|
624
|
+
checks,
|
|
625
|
+
status,
|
|
626
|
+
nextActions: uniqueDeltaDoctorNextActions(checks),
|
|
627
|
+
exitCode: ok ? 0 : 1,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function readGitignore(workspaceRoot: string): string {
|
|
632
|
+
try {
|
|
633
|
+
return readFileSync(join(workspaceRoot, ".gitignore"), "utf8");
|
|
634
|
+
} catch {
|
|
635
|
+
return "";
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function uniqueDeltaDoctorNextActions(checks: DeltaDoctorCheck[]): string[] {
|
|
640
|
+
const actions = checks.flatMap((check) => check.suggestedCommands ?? []);
|
|
641
|
+
return [...new Set(actions.length > 0 ? actions : ["forge delta status --verbose --json"])];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function agentQueueHistoryPath(workspaceRoot: string): string {
|
|
645
|
+
return join(workspaceRoot, ".forge", "agent", "events.ndjson.history");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function lineTimestamp(line: string): string | undefined {
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
651
|
+
for (const key of ["enqueuedAt", "capturedAt", "timestamp"]) {
|
|
652
|
+
if (typeof parsed[key] === "string") {
|
|
653
|
+
return parsed[key] as string;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const payload = parsed.payload;
|
|
657
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
658
|
+
const event = (payload as Record<string, unknown>).event;
|
|
659
|
+
if (event && typeof event === "object" && !Array.isArray(event) && typeof (event as Record<string, unknown>).timestamp === "string") {
|
|
660
|
+
return (event as Record<string, string>).timestamp;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function redactedJsonLine(line: string): string | undefined {
|
|
670
|
+
try {
|
|
671
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
672
|
+
return JSON.stringify(redactDeltaPayload(parsed).value);
|
|
673
|
+
} catch {
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function compactLines(text: string, maxBytes = 256_000): { text: string; linesBefore: number; linesAfter: number } {
|
|
679
|
+
const redactedLines = text
|
|
680
|
+
.split(/\r?\n/u)
|
|
681
|
+
.filter((line) => line.trim().length > 0)
|
|
682
|
+
.map(redactedJsonLine)
|
|
683
|
+
.filter((line): line is string => typeof line === "string");
|
|
684
|
+
const kept: string[] = [];
|
|
685
|
+
let bytes = 0;
|
|
686
|
+
for (const line of [...redactedLines].reverse()) {
|
|
687
|
+
const lineBytes = Buffer.byteLength(`${line}\n`);
|
|
688
|
+
if (kept.length > 0 && bytes + lineBytes > maxBytes) {
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
kept.push(line);
|
|
692
|
+
bytes += lineBytes;
|
|
693
|
+
}
|
|
694
|
+
const lines = kept.reverse();
|
|
695
|
+
return {
|
|
696
|
+
text: lines.length > 0 ? `${lines.join("\n")}\n` : "",
|
|
697
|
+
linesBefore: redactedLines.length,
|
|
698
|
+
linesAfter: lines.length,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function runDeltaCompact(options: DeltaCompactOptions): Promise<DeltaCompactResult> {
|
|
703
|
+
const historyPath = agentQueueHistoryPath(options.workspaceRoot);
|
|
704
|
+
const relativeHistoryPath = normalizePath(relative(options.workspaceRoot, historyPath));
|
|
705
|
+
if (!existsSync(historyPath)) {
|
|
706
|
+
return {
|
|
707
|
+
ok: true,
|
|
708
|
+
subcommand: "compact",
|
|
709
|
+
applied: false,
|
|
710
|
+
dryRun: Boolean(options.dryRun),
|
|
711
|
+
files: [{
|
|
712
|
+
path: relativeHistoryPath,
|
|
713
|
+
exists: false,
|
|
714
|
+
beforeBytes: 0,
|
|
715
|
+
afterBytes: 0,
|
|
716
|
+
linesBefore: 0,
|
|
717
|
+
linesAfter: 0,
|
|
718
|
+
}],
|
|
719
|
+
diagnostics: [],
|
|
720
|
+
nextActions: ["forge delta status --verbose --json"],
|
|
721
|
+
exitCode: 0,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const before = readFileSync(historyPath, "utf8");
|
|
725
|
+
const compacted = compactLines(before);
|
|
726
|
+
const beforeBytes = Buffer.byteLength(before);
|
|
727
|
+
const afterBytes = Buffer.byteLength(compacted.text);
|
|
728
|
+
if (!options.dryRun && compacted.text !== before) {
|
|
729
|
+
writeFileSync(historyPath, compacted.text, "utf8");
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
ok: true,
|
|
733
|
+
subcommand: "compact",
|
|
734
|
+
applied: !options.dryRun && compacted.text !== before,
|
|
735
|
+
dryRun: Boolean(options.dryRun),
|
|
736
|
+
files: [{
|
|
737
|
+
path: relativeHistoryPath,
|
|
738
|
+
exists: true,
|
|
739
|
+
beforeBytes,
|
|
740
|
+
afterBytes,
|
|
741
|
+
linesBefore: compacted.linesBefore,
|
|
742
|
+
linesAfter: compacted.linesAfter,
|
|
743
|
+
}],
|
|
744
|
+
diagnostics: [],
|
|
745
|
+
nextActions: ["forge delta status --verbose --json"],
|
|
746
|
+
exitCode: 0,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function parseOlderThan(value: string | undefined): { cutoff?: Date; error?: string } {
|
|
751
|
+
if (!value) {
|
|
752
|
+
return { error: "forge delta prune requires --older-than <duration>, for example 30d" };
|
|
753
|
+
}
|
|
754
|
+
const match = value.match(/^(\d+)(m|h|d|w)$/u);
|
|
755
|
+
if (!match) {
|
|
756
|
+
return { error: "--older-than supports minutes, hours, days, or weeks, for example 30d" };
|
|
757
|
+
}
|
|
758
|
+
const amount = Number(match[1]);
|
|
759
|
+
const unit = match[2];
|
|
760
|
+
const multiplier = unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : unit === "d" ? 86_400_000 : 604_800_000;
|
|
761
|
+
return { cutoff: new Date(Date.now() - amount * multiplier) };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export async function runDeltaPrune(options: DeltaPruneOptions): Promise<DeltaPruneResult> {
|
|
765
|
+
const parsed = parseOlderThan(options.olderThan);
|
|
766
|
+
if (!parsed.cutoff) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
subcommand: "prune",
|
|
770
|
+
applied: false,
|
|
771
|
+
needsConfirmation: false,
|
|
772
|
+
olderThan: options.olderThan,
|
|
773
|
+
files: [],
|
|
774
|
+
diagnostics: [createDiagnostic({
|
|
775
|
+
severity: "error",
|
|
776
|
+
code: "FORGE_DELTA_PRUNE_USAGE",
|
|
777
|
+
message: parsed.error ?? "invalid prune duration",
|
|
778
|
+
suggestedCommands: ["forge delta prune --older-than 30d --dry-run --json"],
|
|
779
|
+
})],
|
|
780
|
+
nextActions: ["forge delta prune --older-than 30d --dry-run --json"],
|
|
781
|
+
exitCode: 1,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const cutoffIso = parsed.cutoff.toISOString();
|
|
785
|
+
const historyPath = agentQueueHistoryPath(options.workspaceRoot);
|
|
786
|
+
const relativeHistoryPath = normalizePath(relative(options.workspaceRoot, historyPath));
|
|
787
|
+
if (!existsSync(historyPath)) {
|
|
788
|
+
return {
|
|
789
|
+
ok: true,
|
|
790
|
+
subcommand: "prune",
|
|
791
|
+
applied: false,
|
|
792
|
+
needsConfirmation: false,
|
|
793
|
+
olderThan: options.olderThan,
|
|
794
|
+
cutoff: cutoffIso,
|
|
795
|
+
files: [{
|
|
796
|
+
path: relativeHistoryPath,
|
|
797
|
+
exists: false,
|
|
798
|
+
beforeBytes: 0,
|
|
799
|
+
afterBytes: 0,
|
|
800
|
+
linesBefore: 0,
|
|
801
|
+
linesAfter: 0,
|
|
802
|
+
prunedLines: 0,
|
|
803
|
+
}],
|
|
804
|
+
diagnostics: [],
|
|
805
|
+
nextActions: ["forge delta status --verbose --json"],
|
|
806
|
+
exitCode: 0,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const before = readFileSync(historyPath, "utf8");
|
|
810
|
+
const lines = before.split(/\r?\n/u).filter((line) => line.trim().length > 0);
|
|
811
|
+
const kept = lines.filter((line) => {
|
|
812
|
+
const timestamp = lineTimestamp(line);
|
|
813
|
+
return !timestamp || Date.parse(timestamp) >= parsed.cutoff!.getTime();
|
|
814
|
+
});
|
|
815
|
+
const nextText = kept.length > 0 ? `${kept.join("\n")}\n` : "";
|
|
816
|
+
const needsConfirmation = !options.dryRun && !options.yes && kept.length !== lines.length;
|
|
817
|
+
if (!options.dryRun && !needsConfirmation && nextText !== before) {
|
|
818
|
+
writeFileSync(historyPath, nextText, "utf8");
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
ok: true,
|
|
822
|
+
subcommand: "prune",
|
|
823
|
+
applied: !options.dryRun && !needsConfirmation && nextText !== before,
|
|
824
|
+
needsConfirmation,
|
|
825
|
+
olderThan: options.olderThan,
|
|
826
|
+
cutoff: cutoffIso,
|
|
827
|
+
files: [{
|
|
828
|
+
path: relativeHistoryPath,
|
|
829
|
+
exists: true,
|
|
830
|
+
beforeBytes: Buffer.byteLength(before),
|
|
831
|
+
afterBytes: Buffer.byteLength(nextText),
|
|
832
|
+
linesBefore: lines.length,
|
|
833
|
+
linesAfter: kept.length,
|
|
834
|
+
prunedLines: lines.length - kept.length,
|
|
835
|
+
}],
|
|
836
|
+
diagnostics: [],
|
|
837
|
+
nextActions: needsConfirmation
|
|
838
|
+
? [`forge delta prune --older-than ${options.olderThan} --yes --json`, "forge delta status --verbose --json"]
|
|
839
|
+
: ["forge delta status --verbose --json"],
|
|
840
|
+
exitCode: 0,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function resolveExportPath(workspaceRoot: string, output: string): string {
|
|
845
|
+
const absolute = resolve(workspaceRoot, output);
|
|
846
|
+
const rel = relative(resolve(workspaceRoot), absolute);
|
|
847
|
+
if (rel.startsWith("..") || resolve(rel) === rel) {
|
|
848
|
+
throw new Error(`refusing to write Delta export outside workspace: ${output}`);
|
|
849
|
+
}
|
|
850
|
+
return absolute;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export async function runDeltaExport(options: DeltaExportOptions): Promise<DeltaExportResult> {
|
|
854
|
+
if (!options.redacted) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
subcommand: "export",
|
|
858
|
+
redacted: false,
|
|
859
|
+
written: false,
|
|
860
|
+
diagnostics: [createDiagnostic({
|
|
861
|
+
severity: "error",
|
|
862
|
+
code: "FORGE_DELTA_EXPORT_REDACTED_REQUIRED",
|
|
863
|
+
message: "Delta export only supports redacted output; pass --redacted.",
|
|
864
|
+
suggestedCommands: ["forge delta export --redacted --json"],
|
|
865
|
+
})],
|
|
866
|
+
nextActions: ["forge delta export --redacted --json"],
|
|
867
|
+
exitCode: 1,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const limit = Math.max(1, Math.min(Math.floor(options.limit ?? 100), 500));
|
|
871
|
+
const store = await DeltaStore.open(options.workspaceRoot, { access: "read" }).catch((error: unknown) => {
|
|
872
|
+
if (error instanceof DeltaStoreBusyError) {
|
|
873
|
+
return error;
|
|
874
|
+
}
|
|
875
|
+
throw error;
|
|
876
|
+
});
|
|
877
|
+
if (store instanceof DeltaStoreBusyError) {
|
|
878
|
+
const busy = describeDeltaStoreBusy(store, options.workspaceRoot);
|
|
879
|
+
return {
|
|
880
|
+
ok: false,
|
|
881
|
+
subcommand: "export",
|
|
882
|
+
redacted: true,
|
|
883
|
+
written: false,
|
|
884
|
+
busy,
|
|
885
|
+
diagnostics: [createDiagnostic({
|
|
886
|
+
severity: "error",
|
|
887
|
+
code: "FORGE_DELTA_BUSY",
|
|
888
|
+
message: `Forge Delta export cannot run while the local store is busy: ${summarizeDeltaStoreBusy(busy)}`,
|
|
889
|
+
suggestedCommands: ["forge delta status --json"],
|
|
890
|
+
})],
|
|
891
|
+
nextActions: ["forge delta status --json"],
|
|
892
|
+
exitCode: 1,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const status = await store.status();
|
|
897
|
+
const details = await store.statusDetails();
|
|
898
|
+
const timeline = await store.timeline({ limit });
|
|
899
|
+
const semanticTimeline = await store.semanticTimeline({ limit }, { refresh: false });
|
|
900
|
+
const agentMemory = await store.listAgentMemoryEvents({ limit });
|
|
901
|
+
const data = {
|
|
902
|
+
schemaVersion: "0.1.0",
|
|
903
|
+
redacted: true,
|
|
904
|
+
exportedAt: new Date().toISOString(),
|
|
905
|
+
status: { ...status, details },
|
|
906
|
+
timeline,
|
|
907
|
+
semanticTimeline,
|
|
908
|
+
agentMemory,
|
|
909
|
+
};
|
|
910
|
+
let output: string | undefined;
|
|
911
|
+
if (options.output) {
|
|
912
|
+
const absolute = resolveExportPath(options.workspaceRoot, options.output);
|
|
913
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
914
|
+
writeFileSync(absolute, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
915
|
+
output = normalizePath(relative(options.workspaceRoot, absolute));
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
ok: true,
|
|
919
|
+
subcommand: "export",
|
|
920
|
+
redacted: true,
|
|
921
|
+
...(output ? { output } : {}),
|
|
922
|
+
written: Boolean(output),
|
|
923
|
+
data,
|
|
924
|
+
diagnostics: [],
|
|
925
|
+
nextActions: ["forge delta status --verbose --json"],
|
|
926
|
+
exitCode: 0,
|
|
927
|
+
};
|
|
928
|
+
} finally {
|
|
929
|
+
await store.close();
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
378
933
|
export function formatDeltaStatusHuman(result: DeltaStatusResult): string {
|
|
379
934
|
const lines = ["Forge Delta", ""];
|
|
380
935
|
if (!result.ok) {
|
|
@@ -444,6 +999,25 @@ export function formatDeltaStatusHuman(result: DeltaStatusResult): string {
|
|
|
444
999
|
lines.push(` schema: ${result.details.schema.storedVersion ?? "unknown"} (expected ${result.details.schema.expectedVersion})`);
|
|
445
1000
|
lines.push(` lock: ${result.details.locks.forgeLockPresent ? "present" : "absent"} at ${result.details.paths.lock}`);
|
|
446
1001
|
lines.push(` postmaster: ${result.details.locks.postmasterPresent ? "present" : "absent"} at ${result.details.paths.postmaster}`);
|
|
1002
|
+
lines.push(` health: ${result.details.health.status}`);
|
|
1003
|
+
for (const check of result.details.health.checks) {
|
|
1004
|
+
lines.push(` ${check.status}: ${check.name} - ${check.message}`);
|
|
1005
|
+
}
|
|
1006
|
+
lines.push(" operational:");
|
|
1007
|
+
lines.push(` queue: ${result.details.operational.queueExists ? `${result.details.operational.queueSizeBytes} bytes` : "absent"} at ${result.details.operational.queuePath}`);
|
|
1008
|
+
lines.push(` pending events: ${result.details.operational.queuePendingEvents}`);
|
|
1009
|
+
lines.push(` queue redaction: ${result.details.operational.queueRedaction}`);
|
|
1010
|
+
lines.push(` queue history: ${result.details.operational.queueHistoryExists ? `${result.details.operational.queueHistorySizeBytes} bytes, ${result.details.operational.queueHistoryLines} lines` : "absent"} at ${result.details.operational.queueHistoryPath}`);
|
|
1011
|
+
if (result.details.operational.lastCompactionAt) {
|
|
1012
|
+
lines.push(` last compaction: ${result.details.operational.lastCompactionAt}`);
|
|
1013
|
+
}
|
|
1014
|
+
lines.push(` overhead: ${result.details.operational.estimatedOverhead}`);
|
|
1015
|
+
if (result.details.operational.oldestOperationAt) {
|
|
1016
|
+
lines.push(` oldest operation: ${result.details.operational.oldestOperationAt}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (result.details.operational.newestOperationAt) {
|
|
1019
|
+
lines.push(` newest operation: ${result.details.operational.newestOperationAt}`);
|
|
1020
|
+
}
|
|
447
1021
|
lines.push(" counts:");
|
|
448
1022
|
for (const [name, count] of Object.entries(result.details.counts)) {
|
|
449
1023
|
lines.push(` ${name}: ${count}`);
|
|
@@ -487,3 +1061,66 @@ export function formatDeltaRepairHuman(result: DeltaRepairResult): string {
|
|
|
487
1061
|
export function formatDeltaRepairJson(result: DeltaRepairResult): string {
|
|
488
1062
|
return `${JSON.stringify(result, null, 2)}\n`;
|
|
489
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
export function formatDeltaDoctorHuman(result: DeltaDoctorResult): string {
|
|
1066
|
+
const lines = ["Forge Delta doctor", ""];
|
|
1067
|
+
for (const check of result.checks) {
|
|
1068
|
+
const marker = check.ok ? "OK" : check.severity === "warning" ? "WARN" : "FAIL";
|
|
1069
|
+
lines.push(`${marker} ${check.name} - ${check.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
if (result.nextActions.length > 0) {
|
|
1072
|
+
lines.push("", "Next:");
|
|
1073
|
+
for (const action of result.nextActions) {
|
|
1074
|
+
lines.push(` ${action}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
lines.push("", result.ok ? "Delta operational state is usable." : "Delta operational state needs attention.");
|
|
1078
|
+
return `${lines.join("\n")}\n`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function formatDeltaDoctorJson(result: DeltaDoctorResult): string {
|
|
1082
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export function formatDeltaCompactHuman(result: DeltaCompactResult): string {
|
|
1086
|
+
const lines = [`Forge Delta compact ${result.applied ? "applied" : "planned"}`, ""];
|
|
1087
|
+
for (const file of result.files) {
|
|
1088
|
+
lines.push(`${file.path}: ${file.beforeBytes} -> ${file.afterBytes} bytes (${file.linesBefore} -> ${file.linesAfter} lines)`);
|
|
1089
|
+
}
|
|
1090
|
+
return `${lines.join("\n")}\n`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function formatDeltaCompactJson(result: DeltaCompactResult): string {
|
|
1094
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export function formatDeltaPruneHuman(result: DeltaPruneResult): string {
|
|
1098
|
+
const lines = [`Forge Delta prune ${result.applied ? "applied" : "planned"}`, ""];
|
|
1099
|
+
if (result.cutoff) {
|
|
1100
|
+
lines.push(`Cutoff: ${result.cutoff}`);
|
|
1101
|
+
}
|
|
1102
|
+
for (const file of result.files) {
|
|
1103
|
+
lines.push(`${file.path}: pruned ${file.prunedLines} lines`);
|
|
1104
|
+
}
|
|
1105
|
+
if (result.needsConfirmation) {
|
|
1106
|
+
lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
|
|
1107
|
+
}
|
|
1108
|
+
return `${lines.join("\n")}\n`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function formatDeltaPruneJson(result: DeltaPruneResult): string {
|
|
1112
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export function formatDeltaExportHuman(result: DeltaExportResult): string {
|
|
1116
|
+
if (!result.ok) {
|
|
1117
|
+
return `Forge Delta export failed\n${result.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("\n")}\n`;
|
|
1118
|
+
}
|
|
1119
|
+
return result.output
|
|
1120
|
+
? `Forge Delta export wrote ${result.output}\n`
|
|
1121
|
+
: `${JSON.stringify(result.data, null, 2)}\n`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export function formatDeltaExportJson(result: DeltaExportResult): string {
|
|
1125
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
1126
|
+
}
|