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,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
+ }