forgeos 0.1.0-alpha.30 → 0.1.0-alpha.32

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 (49) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +30 -0
  3. package/docs/changelog.md +55 -0
  4. package/package.json +1 -1
  5. package/src/forge/_generated/releaseManifest.json +1 -1
  6. package/src/forge/_generated/releaseManifest.ts +3 -3
  7. package/src/forge/agent-adapters/index.ts +28 -2
  8. package/src/forge/cli/auth.ts +134 -9
  9. package/src/forge/cli/baseline.ts +112 -0
  10. package/src/forge/cli/changed.ts +52 -15
  11. package/src/forge/cli/commands.ts +117 -31
  12. package/src/forge/cli/db.ts +92 -4
  13. package/src/forge/cli/dev.ts +229 -44
  14. package/src/forge/cli/doctor.ts +81 -0
  15. package/src/forge/cli/handoff.ts +63 -5
  16. package/src/forge/cli/last-run.ts +84 -0
  17. package/src/forge/cli/main.ts +3 -1
  18. package/src/forge/cli/new.ts +130 -12
  19. package/src/forge/cli/output.ts +94 -12
  20. package/src/forge/cli/parse.ts +72 -10
  21. package/src/forge/cli/progress.ts +51 -0
  22. package/src/forge/cli/studio.ts +15 -14
  23. package/src/forge/cli/verify.ts +31 -0
  24. package/src/forge/cli/windows.ts +23 -0
  25. package/src/forge/cli/workos.ts +5 -4
  26. package/src/forge/compiler/agent-contract/build.ts +3 -3
  27. package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
  28. package/src/forge/compiler/diagnostics/codes.ts +4 -0
  29. package/src/forge/compiler/integration/templates/render.ts +1 -0
  30. package/src/forge/compiler/integration/templates/workos.ts +5 -5
  31. package/src/forge/compiler/make-registry/build.ts +2 -2
  32. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  33. package/src/forge/compiler/recipes/definitions.ts +1 -1
  34. package/src/forge/delta/status.ts +79 -12
  35. package/src/forge/dev/server.ts +4 -4
  36. package/src/forge/dev-console/cycle.ts +55 -20
  37. package/src/forge/dev-console/types.ts +1 -0
  38. package/src/forge/make/fields.ts +26 -0
  39. package/src/forge/make/index.ts +6 -1
  40. package/src/forge/runtime/db/factory.ts +1 -3
  41. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  42. package/src/forge/runtime/db/pglite-adapter.ts +188 -2
  43. package/src/forge/ui/index.ts +174 -0
  44. package/src/forge/ui/types.ts +1 -0
  45. package/src/forge/version.ts +1 -1
  46. package/src/forge/workspace/baseline.ts +112 -0
  47. package/src/forge/workspace/change-summary.ts +8 -1
  48. package/src/forge/workspace/forge-cli.ts +29 -1
  49. package/src/forge/workspace/git-summary.ts +65 -2
@@ -4,6 +4,7 @@ import {
4
4
  filterCategorizedSummary,
5
5
  summarizeChangeTypes,
6
6
  } from "../workspace/change-summary.ts";
7
+ import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
7
8
  import { buildWorkspaceGitSummary, type WorkspaceGitSummary } from "../workspace/git-summary.ts";
8
9
 
9
10
  export interface ChangedCommandResult {
@@ -44,6 +45,7 @@ interface GeneratedChangeExplanation {
44
45
  }
45
46
 
46
47
  const AUTHORED_CHANGE_TYPES = ["source", "tests", "docs", "config", "assets", "other"] as const;
48
+ const REVIEW_CHANGE_TYPES = ["source", "tests", "docs", "config", "assets"] as const;
47
49
 
48
50
  function emptyCategory(summary: CategorizedFileSummary, category: keyof CategorizedFileSummary["byType"]): boolean {
49
51
  return summary.byType[category].count === 0;
@@ -87,10 +89,12 @@ function selectDerivedChangeSummary(summary: CategorizedFileSummary): DerivedCha
87
89
  function buildRisks(git: WorkspaceGitSummary): string[] {
88
90
  const risks: string[] = [];
89
91
  const changed = git.changeSummary.changed;
90
- if (!git.available) {
92
+ if (!git.available && git.source === "forge-baseline") {
93
+ risks.push("git status is unavailable; using Forge workspace baseline for non-git change tracking");
94
+ } else if (!git.available) {
91
95
  risks.push("git status is unavailable; using filesystem inventory as untracked-file analysis");
92
96
  }
93
- if (git.untracked.count > 0) {
97
+ if (git.untracked.count > 0 && git.source !== "forge-baseline") {
94
98
  risks.push(`${git.untracked.count} untracked file(s) are not in git history`);
95
99
  }
96
100
  if (changed.byType.other.count > 0) {
@@ -100,15 +104,24 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
100
104
  if (!emptyCategory(changed, "generated") && humanChanges.total === 0) {
101
105
  risks.push("only generated artifacts changed; verify the source edit, generator input, or intentional regeneration that produced them");
102
106
  }
107
+ return risks;
108
+ }
109
+
110
+ function buildAdvisories(git: WorkspaceGitSummary): string[] {
111
+ const advisories: string[] = [];
112
+ const changed = git.changeSummary.changed;
103
113
  if (changed.total.count > 50) {
104
- risks.push(`${changed.total.count} changed file(s) detected; use the grouped summaries before reviewing raw diffs`);
114
+ advisories.push(`${changed.total.count} changed file(s) detected; use grouped summaries before reviewing raw diffs`);
105
115
  }
106
- return risks;
116
+ return advisories;
107
117
  }
108
118
 
109
119
  function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
120
+ if (!git.available && git.source === "forge-baseline") {
121
+ return ["forge handoff --json", "forge test plan --changed --json", "forge verify --changed", "git init"];
122
+ }
110
123
  if (!git.available) {
111
- return ["forge status --json", "forge handoff --json", "git init"];
124
+ return ["forge baseline create --reason initial-scaffold --json", "forge status --json", "forge handoff --json", "git init"];
112
125
  }
113
126
  if (git.changeSummary.changed.total.count === 0) {
114
127
  return ["forge status --json", "forge dev --once --json"];
@@ -122,7 +135,12 @@ function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
122
135
  "forge generate --check --json",
123
136
  ];
124
137
  }
138
+ const authoredGeneratedInputs =
139
+ changed.byType.source.count +
140
+ changed.byType.config.count +
141
+ changed.byType.operational.count;
125
142
  return [
143
+ ...(authoredGeneratedInputs > 0 ? ["forge generate --check --json"] : []),
126
144
  "forge handoff --json",
127
145
  "forge test plan --changed --json",
128
146
  "forge verify --changed",
@@ -184,7 +202,7 @@ function buildGeneratedChangeExplanation(
184
202
  };
185
203
  }
186
204
 
187
- export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean } = {}): ChangedCommandResult {
205
+ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean; reviewOnly?: boolean } = {}): ChangedCommandResult {
188
206
  const git = buildWorkspaceGitSummary(workspaceRoot);
189
207
  const changed = git.changeSummary.changed;
190
208
  const humanChanges = selectHumanChangeSummary(changed);
@@ -193,20 +211,29 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
193
211
  const authoredStaged = filterCategorizedSummary(git.changeSummary.staged, [...AUTHORED_CHANGE_TYPES]);
194
212
  const authoredUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...AUTHORED_CHANGE_TYPES]);
195
213
  const authoredUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...AUTHORED_CHANGE_TYPES]);
196
- const viewHumanChanges = options.authoredOnly ? selectHumanChangeSummary(authoredChanged) : humanChanges;
197
- const viewChanged = options.authoredOnly ? authoredChanged : changed;
198
- const viewStaged = options.authoredOnly ? authoredStaged : git.changeSummary.staged;
199
- const viewUnstaged = options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
200
- const viewUntracked = options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
201
- const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly
214
+ const reviewChanged = filterCategorizedSummary(changed, [...REVIEW_CHANGE_TYPES]);
215
+ const reviewStaged = filterCategorizedSummary(git.changeSummary.staged, [...REVIEW_CHANGE_TYPES]);
216
+ const reviewUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...REVIEW_CHANGE_TYPES]);
217
+ const reviewUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...REVIEW_CHANGE_TYPES]);
218
+ const viewHumanChanges = options.reviewOnly
219
+ ? selectHumanChangeSummary(reviewChanged)
220
+ : options.authoredOnly
221
+ ? selectHumanChangeSummary(authoredChanged)
222
+ : humanChanges;
223
+ const viewChanged = options.reviewOnly ? reviewChanged : options.authoredOnly ? authoredChanged : changed;
224
+ const viewStaged = options.reviewOnly ? reviewStaged : options.authoredOnly ? authoredStaged : git.changeSummary.staged;
225
+ const viewUnstaged = options.reviewOnly ? reviewUnstaged : options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
226
+ const viewUntracked = options.reviewOnly ? reviewUntracked : options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
227
+ const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly || options.reviewOnly
202
228
  ? { total: 0, generated: { count: 0, sample: [], hidden: 0 } }
203
229
  : derivedChanges;
204
230
  const risks = buildRisks(git);
205
- const recommendedCommands = buildRecommendedCommands(git);
231
+ const advisories = buildAdvisories(git);
232
+ const recommendedCommands = forgeCliCommandsForWorkspace(workspaceRoot, buildRecommendedCommands(git));
206
233
  const reviewFocus = buildReviewFocus(viewHumanChanges, viewDerivedChanges);
207
234
  const generatedExplanation = buildGeneratedChangeExplanation(viewHumanChanges, viewDerivedChanges);
208
235
  const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
209
- const ok = git.available || git.source === "filesystem";
236
+ const ok = git.available || git.source === "filesystem" || git.source === "forge-baseline";
210
237
 
211
238
  return {
212
239
  ok,
@@ -216,7 +243,9 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
216
243
  summary: {
217
244
  branch: git.branch,
218
245
  commit: git.commit,
219
- view: options.authoredOnly ? "authored" : "all",
246
+ workspaceMode: git.workspaceMode ?? (git.available ? "git" : "nonGit"),
247
+ tracking: git.tracking ?? git.source,
248
+ view: options.reviewOnly ? "review" : options.authoredOnly ? "authored" : "all",
220
249
  changedFiles: viewChanged.total.count,
221
250
  humanFiles: viewHumanChanges.total,
222
251
  generatedFiles: viewDerivedChanges.total,
@@ -229,6 +258,9 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
229
258
  git: {
230
259
  available: git.available,
231
260
  source: git.source,
261
+ workspaceMode: git.workspaceMode,
262
+ tracking: git.tracking,
263
+ baseline: git.baseline,
232
264
  ...(git.error ? { error: git.error } : {}),
233
265
  branch: git.branch,
234
266
  commit: git.commit,
@@ -243,6 +275,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
243
275
  generatedExplanation,
244
276
  diffPlan,
245
277
  risks,
278
+ advisories,
246
279
  recommendedCommands,
247
280
  nextActions: recommendedCommands,
248
281
  },
@@ -258,6 +291,7 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
258
291
  const generatedExplanation = result.data.generatedExplanation as { summary?: string } | undefined;
259
292
  const diffPlan = result.data.diffPlan as { summary?: string; authoredDiffCommand?: string; generatedDiffCommand?: string; generatedCollapsedByDefault?: boolean } | undefined;
260
293
  const risks = (result.data.risks as string[] | undefined) ?? [];
294
+ const advisories = (result.data.advisories as string[] | undefined) ?? [];
261
295
  const nextActions = (result.data.nextActions as string[] | undefined) ?? [];
262
296
  const lines = [
263
297
  `Forge changed: ${result.ok ? "ready" : "git unavailable"}`,
@@ -300,6 +334,9 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
300
334
  if (risks.length > 0) {
301
335
  lines.push("", "Risks:", ...risks.map((risk) => ` ${risk}`));
302
336
  }
337
+ if (advisories.length > 0) {
338
+ lines.push("", "Notes:", ...advisories.map((advisory) => ` ${advisory}`));
339
+ }
303
340
 
304
341
  lines.push("", "Next:", ...nextActions.map((command) => ` ${command}`));
305
342
  return `${lines.join("\n")}\n`;
@@ -181,6 +181,9 @@ import {
181
181
  import {
182
182
  formatDoctorHuman,
183
183
  formatDoctorJson,
184
+ formatPgliteDoctorHuman,
185
+ formatPgliteDoctorJson,
186
+ runPgliteDoctorCommand,
184
187
  runDoctorCommand,
185
188
  } from "./doctor.ts";
186
189
  import {
@@ -202,6 +205,16 @@ import {
202
205
  formatWorkOSJson,
203
206
  runWorkOSCommand,
204
207
  } from "./workos.ts";
208
+ import {
209
+ formatLastHuman,
210
+ formatLastJson,
211
+ runLastCommand,
212
+ } from "./last-run.ts";
213
+ import {
214
+ formatBaselineHuman,
215
+ formatBaselineJson,
216
+ runBaselineCommand,
217
+ } from "./baseline.ts";
205
218
  import { formatRlsHuman, formatRlsJson, runRlsCommand } from "./rls.ts";
206
219
  import {
207
220
  formatSecurityHuman,
@@ -271,7 +284,9 @@ import { resolveAuthFromCli } from "../runtime/auth/resolve.ts";
271
284
  import { getActiveDbAdapter } from "../runtime/executor.ts";
272
285
  import { CLI_VERSION, FORGEOS_VERSION } from "../version.ts";
273
286
  import type { CategorizedFileSummary } from "../workspace/change-summary.ts";
287
+ import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
274
288
  import { buildWorkspaceGitSummary } from "../workspace/git-summary.ts";
289
+ import { startCommandHeartbeat } from "./progress.ts";
275
290
 
276
291
  function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
277
292
  const absolute = join(workspaceRoot, relative);
@@ -708,33 +723,69 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
708
723
  const driftClean = driftSummary.ok === true;
709
724
  const ok = driftClean && generatedReady;
710
725
  const handoffDefaultReady = handoffSummary.defaultReady === true;
726
+ const changed = gitSummary.changeSummary.changed;
727
+ const generatedGitFiles = changed.byType.generated.count;
728
+ const authoredGitFiles = changed.total.count - generatedGitFiles;
729
+ const authoredGeneratedInputs =
730
+ changed.byType.source.count +
731
+ changed.byType.config.count +
732
+ changed.byType.operational.count;
733
+ const generatedNeedsCheck = generatedReady && driftClean && generatedGitFiles === 0 && authoredGeneratedInputs > 0;
711
734
  const generatedState = !generatedReady
712
735
  ? "missing-artifacts"
736
+ : generatedNeedsCheck
737
+ ? "check-needed"
713
738
  : driftClean
714
739
  ? "ready"
715
740
  : "drift";
716
- const generatedNextActions = generatedState === "ready"
717
- ? ["forge dev", "forge generate --check --json"]
741
+ const generatedNextActionsRaw = generatedState === "ready"
742
+ ? ["forge dev", "forge dev --once --json", "forge generate --check --json"]
743
+ : generatedState === "check-needed"
744
+ ? ["forge generate --check --json", "forge handoff --json", "forge dev --once --json"]
718
745
  : ["forge generate", "forge check --json", "forge inspect drift --json"];
719
- const changed = gitSummary.changeSummary.changed;
720
- const generatedGitFiles = changed.byType.generated.count;
721
- const authoredGitFiles = changed.total.count - generatedGitFiles;
746
+ const generatedNextActions = forgeCliCommandsForWorkspace(workspaceRoot, generatedNextActionsRaw);
722
747
  const generatedGitExplanation = generatedGitFiles === 0
723
- ? "git status has no generated artifact changes"
748
+ ? generatedNeedsCheck
749
+ ? "git status has no generated artifact changes, but authored source/config changes mean freshness is unverified until forge generate --check runs"
750
+ : "git status has no generated artifact changes"
724
751
  : authoredGitFiles === 0
725
752
  ? "forge generate --check can be clean while git shows generated artifacts changed: generated files match current workspace inputs but differ from HEAD"
726
753
  : "git status includes generated artifacts alongside authored changes; review authored inputs first";
754
+ const [safeDevCommand, generatedCheckCommand, generatedRepairCommand] = forgeCliCommandsForWorkspace(workspaceRoot, [
755
+ "forge dev",
756
+ "forge generate --check --json",
757
+ "forge generate",
758
+ ]);
727
759
  const frontendPresent = summaryBlock.frontendPresent === true;
760
+ const [
761
+ studioOpenCommand,
762
+ studioAttachCommand,
763
+ studioSnapshotCommand,
764
+ studioWatchCommand,
765
+ studioBridgeCommand,
766
+ studioDoctorCommand,
767
+ studioStartTargetAppCommand,
768
+ studioProbeCommand,
769
+ ] = forgeCliCommandsForWorkspace(workspaceRoot, [
770
+ "forge studio open . --preview-port 5174 --target codex --json",
771
+ "forge studio attach . --preview-port 5174 --target codex --json",
772
+ "forge studio snapshot . --preview-port 5174 --target codex --json",
773
+ "forge studio watch . --preview-port 5174 --target codex --json",
774
+ "forge studio bridge . --preview-port 5174 --target codex --studio-url http://127.0.0.1:3765 --json",
775
+ "forge studio doctor . --preview-port 5174 --target codex --json",
776
+ "forge dev --port 3766 --web-port 5174",
777
+ "forge dev --once --json",
778
+ ]);
728
779
  const studio = {
729
- openCommand: "forge studio open . --preview-port 5174 --target codex --json",
730
- attachCommand: "forge studio attach . --preview-port 5174 --target codex --json",
731
- snapshotCommand: "forge studio snapshot . --preview-port 5174 --target codex --json",
732
- watchCommand: "forge studio watch . --preview-port 5174 --target codex --json",
733
- bridgeCommand: "forge studio bridge . --preview-port 5174 --target codex --studio-url http://127.0.0.1:3765 --json",
734
- doctorCommand: "forge studio doctor . --preview-port 5174 --target codex --json",
780
+ openCommand: studioOpenCommand,
781
+ attachCommand: studioAttachCommand,
782
+ snapshotCommand: studioSnapshotCommand,
783
+ watchCommand: studioWatchCommand,
784
+ bridgeCommand: studioBridgeCommand,
785
+ doctorCommand: studioDoctorCommand,
735
786
  targetPreviewUrl: "http://127.0.0.1:5174",
736
- startTargetAppCommand: "forge dev --port 3766 --web-port 5174",
737
- probeCommand: "forge dev --once --json",
787
+ startTargetAppCommand: studioStartTargetAppCommand,
788
+ probeCommand: studioProbeCommand,
738
789
  useful: frontendPresent,
739
790
  note: frontendPresent
740
791
  ? "Attach this app to Forge Studio as an external-agent workroom; Studio should preview the target app on 5174."
@@ -750,11 +801,13 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
750
801
  state: generatedState,
751
802
  ready: generatedReady,
752
803
  driftClean,
804
+ freshness: generatedNeedsCheck ? "unverified" : generatedState === "ready" ? "verified-or-unchanged" : "attention",
805
+ authoredGeneratedInputs,
753
806
  missingArtifacts,
754
807
  tableDrift,
755
- safeDevCommand: "forge dev",
756
- checkCommand: "forge generate --check --json",
757
- repairCommand: "forge generate",
808
+ safeDevCommand,
809
+ checkCommand: generatedCheckCommand,
810
+ repairCommand: generatedRepairCommand,
758
811
  git: {
759
812
  changedFiles: changed.total.count,
760
813
  authoredFiles: authoredGitFiles,
@@ -795,8 +848,11 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
795
848
  },
796
849
  },
797
850
  git,
798
- nextActions: ok
799
- ? [
851
+ nextActions: forgeCliCommandsForWorkspace(
852
+ workspaceRoot,
853
+ ok
854
+ ? [
855
+ ...(generatedState === "check-needed" ? ["forge generate --check --json"] : []),
800
856
  "forge handoff --json",
801
857
  "forge changed --json",
802
858
  "forge dev",
@@ -805,7 +861,7 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
805
861
  "forge inspect handoff --json",
806
862
  "forge verify --changed",
807
863
  ]
808
- : [
864
+ : [
809
865
  "forge generate",
810
866
  "forge handoff --json",
811
867
  "forge changed --json",
@@ -813,6 +869,7 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
813
869
  "forge inspect drift --json",
814
870
  "forge agent prepare --target codex --json",
815
871
  ],
872
+ ),
816
873
  },
817
874
  exitCode: ok ? 0 : 1,
818
875
  };
@@ -1558,6 +1615,16 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1558
1615
  }
1559
1616
  return 0;
1560
1617
  }
1618
+ case "last": {
1619
+ const result = runLastCommand({ workspaceRoot: command.workspaceRoot });
1620
+ process.stdout.write(command.json ? formatLastJson(result) : formatLastHuman(result));
1621
+ return result.exitCode;
1622
+ }
1623
+ case "baseline": {
1624
+ const result = runBaselineCommand(command);
1625
+ process.stdout.write(command.json ? formatBaselineJson(result) : formatBaselineHuman(result));
1626
+ return result.exitCode;
1627
+ }
1561
1628
  case "new": {
1562
1629
  const result = await runNewCommand({
1563
1630
  name: command.name,
@@ -1565,6 +1632,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1565
1632
  packageManager: command.packageManager,
1566
1633
  install: command.install,
1567
1634
  git: command.git,
1635
+ fieldTest: command.fieldTest,
1568
1636
  forgePackageSpec: command.forgePackageSpec,
1569
1637
  localForge: command.localForge,
1570
1638
  workspaceRoot: command.workspaceRoot,
@@ -1627,7 +1695,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1627
1695
  concurrency: 4,
1628
1696
  });
1629
1697
  if (command.json) {
1630
- process.stdout.write(formatJsonResult(buildGenerateJson(result)));
1698
+ process.stdout.write(formatJsonResult(buildGenerateJson(result, { workspaceRoot: command.workspaceRoot })));
1631
1699
  } else {
1632
1700
  process.stdout.write(formatAgentContractHuman(command.subcommand, result));
1633
1701
  writeHumanGenerate(result);
@@ -1668,6 +1736,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1668
1736
  process.stdout.write(command.json ? formatDeltaDoctorJson(result) : formatDeltaDoctorHuman(result));
1669
1737
  return result.exitCode;
1670
1738
  }
1739
+ if (command.target === "pglite") {
1740
+ const result = await runPgliteDoctorCommand({ workspaceRoot: command.workspaceRoot });
1741
+ process.stdout.write(command.json ? formatPgliteDoctorJson(result) : formatPgliteDoctorHuman(result));
1742
+ return result.exitCode;
1743
+ }
1671
1744
  const result = await runDoctorCommand({ workspaceRoot: command.workspaceRoot });
1672
1745
  if (command.json) {
1673
1746
  process.stdout.write(formatDoctorJson(result));
@@ -1930,13 +2003,23 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1930
2003
  return result.exitCode;
1931
2004
  }
1932
2005
  case "agent": {
1933
- const result = await runAgentCommand(command.options);
1934
- if (command.options.json) {
1935
- process.stdout.write(formatAgentJson(result));
1936
- } else {
1937
- process.stdout.write(formatAgentHuman(result));
2006
+ const heartbeat = command.options.subcommand === "onboard"
2007
+ ? startCommandHeartbeat({
2008
+ label: `agent onboard ${command.options.target ?? "codex"}`,
2009
+ initialPhase: "prepare-hooks-memory-and-dev-snapshot",
2010
+ })
2011
+ : null;
2012
+ try {
2013
+ const result = await runAgentCommand(command.options);
2014
+ if (command.options.json) {
2015
+ process.stdout.write(formatAgentJson(result));
2016
+ } else {
2017
+ process.stdout.write(formatAgentHuman(result));
2018
+ }
2019
+ return result.exitCode;
2020
+ } finally {
2021
+ heartbeat?.stop();
1938
2022
  }
1939
- return result.exitCode;
1940
2023
  }
1941
2024
  case "mcp": {
1942
2025
  return runMcpServe(command.workspaceRoot);
@@ -1976,7 +2059,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1976
2059
  });
1977
2060
 
1978
2061
  if (command.json) {
1979
- process.stdout.write(formatJsonResult(buildGenerateJson(result)));
2062
+ process.stdout.write(formatJsonResult(buildGenerateJson(result, { workspaceRoot: command.workspaceRoot })));
1980
2063
  } else {
1981
2064
  writeHumanGenerate(result);
1982
2065
  }
@@ -1986,7 +2069,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1986
2069
  case "add": {
1987
2070
  const result = await runAddCommand(command.alias, command.options);
1988
2071
  if (command.options.json) {
1989
- process.stdout.write(formatJsonResult(buildAddJson(result)));
2072
+ process.stdout.write(formatJsonResult(buildAddJson(result, { workspaceRoot: command.options.workspaceRoot })));
1990
2073
  } else {
1991
2074
  writeHumanAdd(result);
1992
2075
  }
@@ -2022,6 +2105,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2022
2105
  case "changed": {
2023
2106
  const result = runChangedCommand(command.workspaceRoot, {
2024
2107
  authoredOnly: command.authoredOnly,
2108
+ reviewOnly: command.reviewOnly,
2025
2109
  });
2026
2110
  if (command.json) {
2027
2111
  process.stdout.write(formatJsonResult(result.data));
@@ -2132,11 +2216,12 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2132
2216
  return result.exitCode;
2133
2217
  }
2134
2218
  case "check": {
2135
- const result = await runCheckCommand(process.cwd(), {
2219
+ const checkWorkspaceRoot = process.cwd();
2220
+ const result = await runCheckCommand(checkWorkspaceRoot, {
2136
2221
  strictSecrets: command.strictSecrets,
2137
2222
  });
2138
2223
  if (command.json) {
2139
- process.stdout.write(formatJsonResult(buildCheckJson(result)));
2224
+ process.stdout.write(formatJsonResult(buildCheckJson(result, { workspaceRoot: checkWorkspaceRoot })));
2140
2225
  } else {
2141
2226
  writeHumanGenerate(result);
2142
2227
  }
@@ -2286,6 +2371,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2286
2371
  workspaceRoot: command.workspaceRoot,
2287
2372
  db: command.db,
2288
2373
  databaseUrl: command.databaseUrl,
2374
+ local: command.local,
2289
2375
  json: command.json,
2290
2376
  });
2291
2377
 
@@ -7,6 +7,9 @@ import { buildAppGraph } from "../compiler/app-graph/build.ts";
7
7
  import { createDiagnostic } from "../compiler/diagnostics/create.ts";
8
8
  import {
9
9
  FORGE_RLS_APPLY_FAILED,
10
+ FORGE_DB_ADAPTER_UNAVAILABLE,
11
+ FORGE_PGLITE_STORE_ABORTED,
12
+ FORGE_PGLITE_STORE_ACTIVE,
10
13
  FORGE_RUNTIME_NOT_FOUND,
11
14
  } from "../compiler/diagnostics/codes.ts";
12
15
  import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
@@ -23,14 +26,20 @@ import {
23
26
  resetDatabase,
24
27
  } from "../runtime/db/migrate.ts";
25
28
  import type { DbAdapterKind } from "../runtime/db/adapter.ts";
29
+ import {
30
+ DEFAULT_PGLITE_DIR,
31
+ repairLocalPgliteStore,
32
+ } from "../runtime/db/pglite-adapter.ts";
33
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
26
34
 
27
- export type DbSubcommand = "diff" | "migrate" | "reset" | "status" | "doctor" | "rls-check";
35
+ export type DbSubcommand = "diff" | "migrate" | "reset" | "status" | "doctor" | "repair" | "rls-check";
28
36
 
29
37
  export interface DbCommandOptions {
30
38
  subcommand: DbSubcommand;
31
39
  workspaceRoot: string;
32
40
  db: DbAdapterKind;
33
41
  databaseUrl?: string;
42
+ local?: boolean;
34
43
  json: boolean;
35
44
  }
36
45
 
@@ -250,7 +259,7 @@ async function runDbDoctor(
250
259
  .sort();
251
260
  const ok = inspected.diagnostics.length === 0 && missingTables.length === 0 && missingColumns.length === 0;
252
261
 
253
- return {
262
+ return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
254
263
  ok,
255
264
  data: {
256
265
  schemaVersion: "0.1.0",
@@ -279,13 +288,92 @@ async function runDbDoctor(
279
288
  ...inspected.diagnostics,
280
289
  ],
281
290
  exitCode: ok ? 0 : 1,
282
- };
291
+ });
283
292
  } finally {
284
293
  await adapter.close();
285
294
  }
286
295
  }
287
296
 
297
+ async function runDbRepair(options: DbCommandOptions): Promise<DbCommandResult> {
298
+ if (!options.local) {
299
+ return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
300
+ ok: false,
301
+ data: {
302
+ schemaVersion: "0.1.0",
303
+ repaired: false,
304
+ adapter: options.db,
305
+ local: false,
306
+ nextActions: ["forge db repair --local --adapter pglite --json"],
307
+ },
308
+ diagnostics: [
309
+ createDiagnostic({
310
+ severity: "error",
311
+ code: "FORGE_CLI_USAGE",
312
+ message: "local database repair requires --local",
313
+ fixHint: "Pass --local to confirm you want Forge to repair the local development database store.",
314
+ suggestedCommands: ["forge db repair --local --adapter pglite --json"],
315
+ }),
316
+ ],
317
+ exitCode: 1,
318
+ });
319
+ }
320
+
321
+ if (options.db !== "pglite") {
322
+ return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
323
+ ok: false,
324
+ data: {
325
+ schemaVersion: "0.1.0",
326
+ repaired: false,
327
+ adapter: options.db,
328
+ nextActions: ["forge db repair --local --adapter pglite --json"],
329
+ },
330
+ diagnostics: [
331
+ createDiagnostic({
332
+ severity: "error",
333
+ code: FORGE_DB_ADAPTER_UNAVAILABLE,
334
+ message: "local repair currently supports only the pglite adapter",
335
+ fixHint: "Use --adapter pglite for the local Forge dev database store.",
336
+ suggestedCommands: ["forge db repair --local --adapter pglite --json"],
337
+ }),
338
+ ],
339
+ exitCode: 1,
340
+ });
341
+ }
342
+
343
+ const dataDir = join(options.workspaceRoot, DEFAULT_PGLITE_DIR);
344
+ const result = await repairLocalPgliteStore(dataDir);
345
+ const diagnostics = result.ok
346
+ ? []
347
+ : [
348
+ createDiagnostic({
349
+ severity: "error",
350
+ code: result.before.state === "active" ? FORGE_PGLITE_STORE_ACTIVE : FORGE_PGLITE_STORE_ABORTED,
351
+ message: result.message,
352
+ fixHint: result.before.state === "active"
353
+ ? "Stop the running forge dev process before repairing the PGlite store."
354
+ : "Archive the local PGlite store and retry forge dev, or use --db memory for non-persistent validation.",
355
+ suggestedCommands: result.nextActions,
356
+ }),
357
+ ];
358
+
359
+ return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
360
+ ok: result.ok,
361
+ data: {
362
+ schemaVersion: "0.1.0",
363
+ adapter: "pglite",
364
+ local: true,
365
+ ...result,
366
+ },
367
+ diagnostics,
368
+ exitCode: result.ok ? 0 : 1,
369
+ });
370
+ }
371
+
288
372
  export async function runDbCommand(options: DbCommandOptions): Promise<DbCommandResult> {
373
+ if (options.subcommand === "repair") {
374
+ return runDbRepair(options);
375
+ }
376
+
289
377
  const { plan, diagnostics: planDiagnostics } = await loadSqlPlan(options.workspaceRoot);
290
378
 
291
379
  if (!plan) {
@@ -427,7 +515,7 @@ export function formatDbHuman(subcommand: DbSubcommand, result: DbCommandResult)
427
515
  return "database reset complete\n";
428
516
  }
429
517
 
430
- if (subcommand === "status" || subcommand === "diff" || subcommand === "doctor") {
518
+ if (subcommand === "status" || subcommand === "diff" || subcommand === "doctor" || subcommand === "repair") {
431
519
  return `${JSON.stringify(result.data, null, 2)}\n`;
432
520
  }
433
521