forgeos 0.1.0-alpha.31 → 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 (39) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +19 -0
  3. package/docs/changelog.md +40 -1
  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 +74 -4
  9. package/src/forge/cli/changed.ts +38 -11
  10. package/src/forge/cli/commands.ts +71 -25
  11. package/src/forge/cli/db.ts +9 -8
  12. package/src/forge/cli/dev.ts +137 -56
  13. package/src/forge/cli/doctor.ts +3 -2
  14. package/src/forge/cli/handoff.ts +50 -3
  15. package/src/forge/cli/main.ts +3 -1
  16. package/src/forge/cli/new.ts +130 -12
  17. package/src/forge/cli/output.ts +94 -12
  18. package/src/forge/cli/parse.ts +13 -1
  19. package/src/forge/cli/studio.ts +15 -14
  20. package/src/forge/cli/windows.ts +2 -1
  21. package/src/forge/cli/workos.ts +5 -4
  22. package/src/forge/compiler/agent-contract/build.ts +3 -3
  23. package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
  24. package/src/forge/compiler/integration/templates/render.ts +1 -0
  25. package/src/forge/compiler/integration/templates/workos.ts +5 -5
  26. package/src/forge/compiler/make-registry/build.ts +2 -2
  27. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  28. package/src/forge/compiler/recipes/definitions.ts +1 -1
  29. package/src/forge/delta/status.ts +79 -12
  30. package/src/forge/dev/server.ts +4 -4
  31. package/src/forge/dev-console/cycle.ts +55 -20
  32. package/src/forge/dev-console/types.ts +1 -0
  33. package/src/forge/make/fields.ts +26 -0
  34. package/src/forge/make/index.ts +6 -1
  35. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  36. package/src/forge/ui/index.ts +129 -0
  37. package/src/forge/version.ts +1 -1
  38. package/src/forge/workspace/change-summary.ts +7 -1
  39. package/src/forge/workspace/forge-cli.ts +29 -1
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
@@ -205,8 +205,8 @@ node bin/forge.mjs do verify --json
205
205
  Use:
206
206
 
207
207
  ```bash
208
- node bin/forge.mjs make resource <name> --fields title:text,status:enum(open,closed) --dry-run --json
209
- node bin/forge.mjs make resource <name> --fields title:text,status:enum(open,closed) --with-ui --yes
208
+ node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --dry-run --json
209
+ node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --with-ui --yes
210
210
  node bin/forge.mjs make ui --framework vite --dry-run --json
211
211
  node bin/forge.mjs make ui --framework nuxt --dry-run --json
212
212
  node bin/forge.mjs make ai-chat support --dry-run --json
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.32
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the alpha field-test loop after real Codex, WorkOS, PGlite, and app-server exercises.
8
+
9
+ - Accept `create-forgeos-app --git` as a supported/no-op compatibility flag when git initialization is already handled by the scaffold path, and keep `npm create forgeos-app@alpha .` working from empty current directories.
10
+ - Reject schema definitions that try to model `id` as a normal text field so generated SQL cannot silently create the wrong primary-key shape.
11
+ - Fix tenant-scoped updates in the in-memory database adapter and bring memory timestamp behavior closer to PGlite by rejecting empty timestamp strings and returning `Date` objects for timestamp columns.
12
+ - Improve smoke/generate drift diagnostics and keep `forge handoff` read-only with respect to generated artifacts.
13
+ - Copy generated WorkOS seed data to a root-level `workos-seed.yml` for app DX, serve `HEAD /auth.md` and `HEAD /.well-known/oauth-protected-resource`, and make local-vs-production auth posture more explicit.
14
+ - Normalize suggested Forge commands in framework checkouts so `agent`, `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global `forge`.
15
+ - Return structured JSON diagnostics for Delta/PGlite export/open failures instead of leaking raw PGlite stack output, and improve Windows/PGlite repair guidance.
16
+ - Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
17
+
3
18
  ## 0.1.0-alpha.31
4
19
 
5
20
  ### Patch Changes
@@ -10,6 +25,10 @@
10
25
  - Add `forge auth status` and production-focused `forge auth prove --prod` output so `dev-headers` is clearly labeled as local-only while JWT/OIDC proofs show production auth posture.
11
26
  - Expand `forge doctor windows` with local PGlite store posture and cleanup guidance, including safer PGlite abort inspection that does not poison the surrounding process exit code.
12
27
  - Add `forge ui audit` and run it during `forge verify --smoke` when a web app is present, catching missing UI scenarios, missing stable Forge test IDs, and missing policy-denied coverage for sensitive routes.
28
+ - Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
29
+ - Bring the in-memory DB adapter closer to PGlite for timestamp fields by rejecting empty timestamp values and returning `Date` objects for `timestamp`/`timestamptz` columns.
30
+ - Make `forge handoff` use a read-only generated-artifact check so preparing a handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or generated `AGENTS.md` noise; `forge dev --once` still self-heals stale artifacts.
31
+ - Allow `forge new .` / `npm create forgeos-app@alpha .` from an empty current directory, while refusing non-empty directories to avoid overwriting existing apps.
13
32
 
14
33
  ## 0.1.0-alpha.30
15
34
 
package/docs/changelog.md CHANGED
@@ -6,6 +6,32 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.32
10
+
11
+ - Accepted `create-forgeos-app --git` as a supported/no-op compatibility flag
12
+ when git initialization is already handled by the scaffold path, and kept
13
+ `npm create forgeos-app@alpha .` working from empty current directories.
14
+ - Added stronger schema validation for `id` so apps cannot accidentally model
15
+ the primary key as a normal text field and generate incorrect SQL.
16
+ - Fixed tenant-scoped updates in the in-memory database adapter and made memory
17
+ timestamp handling closer to PGlite by rejecting empty timestamp strings and
18
+ returning `Date` objects for timestamp columns.
19
+ - Improved smoke/generate drift diagnostics and kept `forge handoff` read-only
20
+ with respect to generated artifacts.
21
+ - Improved WorkOS/auth app DX: root-level `workos-seed.yml`, `HEAD /auth.md`,
22
+ `HEAD /.well-known/oauth-protected-resource`, and clearer local-vs-production
23
+ auth posture.
24
+ - Normalized suggested Forge commands in framework checkouts so `agent`,
25
+ `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth
26
+ helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global
27
+ `forge`.
28
+ - Returned structured JSON diagnostics for Delta/PGlite export/open failures
29
+ instead of raw PGlite stack output, and expanded Windows/PGlite repair hints.
30
+ - Expanded `forge ui audit` with static UX/auth-readiness warnings for missing
31
+ semantic landmarks, unlabeled form controls, unnamed buttons, missing
32
+ loading/error/empty states, and tenant/prod-auth apps that still only expose
33
+ local dev auth posture.
34
+
9
35
  ## 0.1.0-alpha.31
10
36
 
11
37
  - Added Forge workspace baselines for non-git app directories:
@@ -20,7 +46,20 @@ The canonical source file in the repository is `CHANGELOG.md`.
20
46
  exit code.
21
47
  - Added `forge ui audit` and wired it into `forge verify --smoke` for web apps
22
48
  to catch missing route scenarios, missing stable Forge test IDs, and missing
23
- policy-denied coverage for sensitive flows.
49
+ policy-denied coverage for sensitive flows. It now also reports static
50
+ UX/auth-readiness warnings for missing semantic landmarks, unlabeled form
51
+ controls, unnamed buttons, missing loading/error/empty states, and
52
+ tenant/prod-auth apps that still only expose local dev auth posture.
53
+ - Brought the in-memory DB adapter closer to PGlite for timestamp fields by
54
+ rejecting empty timestamp values and returning `Date` objects for
55
+ `timestamp`/`timestamptz` columns.
56
+ - Made `forge handoff` use a read-only generated-artifact check so preparing a
57
+ handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or
58
+ generated `AGENTS.md` noise; `forge dev --once` still self-heals stale
59
+ artifacts.
60
+ - Allowed `forge new .` / `npm create forgeos-app@alpha .` from an empty
61
+ current directory, while refusing non-empty directories to avoid overwriting
62
+ existing apps.
24
63
 
25
64
  ## 0.1.0-alpha.30
26
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.32",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.31","releaseId":"forgeos@0.1.0-alpha.31+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.32","releaseId":"forgeos@0.1.0-alpha.32+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=806e4e7f20264628107bf964331f13335175f065d6a8826b5defc2027d8047cd
1
+ // @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=ba652b73917e09bcc803cfea91f38e1aa2563b17eacc60f6faaebdc949997e9a
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.31",
23
- "releaseId": "forgeos@0.1.0-alpha.31+unknown",
22
+ "packageVersion": "0.1.0-alpha.32",
23
+ "releaseId": "forgeos@0.1.0-alpha.32+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -56,6 +56,7 @@ import {
56
56
  } from "../agent-memory/hook-runner.ts";
57
57
  import { releaseManifest } from "../_generated/releaseManifest.ts";
58
58
  import { runDevConsoleCycle } from "../dev-console/cycle.ts";
59
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
59
60
 
60
61
  export const AGENT_ADAPTER_VERSION = "agent-adapter-0.1.0";
61
62
  export const AGENT_FORMAT_VERSION = "2026-06";
@@ -77,6 +78,25 @@ function diagnostic(
77
78
  return createDiagnostic({ severity, code, message, ...(file ? { file } : {}) });
78
79
  }
79
80
 
81
+ function normalizeAgentCliCommandHints<T>(workspaceRoot: string, result: T): T {
82
+ if (!result || typeof result !== "object") {
83
+ return result;
84
+ }
85
+ const value = result as Record<string, unknown>;
86
+ const normalized: Record<string, unknown> = { ...value };
87
+ for (const key of ["nextActions", "diagnostics", "commands", "checks", "steps"]) {
88
+ if (key in normalized) {
89
+ normalized[key] = normalizeForgeCliCommandsInValue(workspaceRoot, normalized[key], key);
90
+ }
91
+ }
92
+ for (const key of ["installResult", "ingestResult"]) {
93
+ if (key in normalized) {
94
+ normalized[key] = normalizeAgentCliCommandHints(workspaceRoot, normalized[key]);
95
+ }
96
+ }
97
+ return normalized as T;
98
+ }
99
+
80
100
  function readText(workspaceRoot: string, relative: string): string | null {
81
101
  const path = join(workspaceRoot, relative);
82
102
  if (!nodeFileSystem.exists(path)) {
@@ -2222,9 +2242,11 @@ export async function runAgentHooksSmoke(options: AgentCommandOptions): Promise<
2222
2242
  };
2223
2243
  }
2224
2244
 
2225
- export async function runAgentCommand(options: AgentCommandOptions): Promise<
2245
+ type AgentCommandResult =
2226
2246
  AgentExportResult | AgentCheckResult | AgentTargetsResult | AgentPrintContextResult | AgentDoctorResult | AgentPrepareResult | AgentOnboardResult | AgentHooksSmokeResult | AgentHooksStatusResult | AgentTimelineResult | AgentMemoryCommandResult
2227
- > {
2247
+ ;
2248
+
2249
+ async function runAgentCommandRaw(options: AgentCommandOptions): Promise<AgentCommandResult> {
2228
2250
  if (options.subcommand === "list-targets") {
2229
2251
  return runAgentListTargets(options.workspaceRoot);
2230
2252
  }
@@ -2295,6 +2317,10 @@ export async function runAgentCommand(options: AgentCommandOptions): Promise<
2295
2317
  };
2296
2318
  }
2297
2319
 
2320
+ export async function runAgentCommand(options: AgentCommandOptions): Promise<AgentCommandResult> {
2321
+ return normalizeAgentCliCommandHints(options.workspaceRoot, await runAgentCommandRaw(options));
2322
+ }
2323
+
2298
2324
  export function formatAgentJson(result: Awaited<ReturnType<typeof runAgentCommand>>): string {
2299
2325
  if ("timeline" in result && result.timeline === "agent") {
2300
2326
  return `${JSON.stringify(result, null, 2)}\n`;
@@ -9,6 +9,9 @@ import { mapClaimsToAuthContext } from "../runtime/auth/claims.ts";
9
9
  import { ForgeAuthError } from "../runtime/auth/errors.ts";
10
10
  import { verifyJwtToken } from "../runtime/auth/verifier.ts";
11
11
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
12
15
 
13
16
  export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
14
17
 
@@ -18,6 +21,7 @@ export interface AuthCommandOptions {
18
21
  json: boolean;
19
22
  token?: string;
20
23
  prod?: boolean;
24
+ scenario?: string;
21
25
  }
22
26
 
23
27
  export interface AuthCommandResult {
@@ -88,7 +92,7 @@ function buildAuthPosture(workspaceRoot: string) {
88
92
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
89
93
  const errors = configErrors(config);
90
94
  const configReady = errors.length === 0;
91
- return {
95
+ return normalizeForgeCliCommandsInValue(workspaceRoot, {
92
96
  schemaVersion: "0.1.0",
93
97
  mode: config.mode,
94
98
  localOnly: config.mode === "dev-headers",
@@ -96,6 +100,15 @@ function buildAuthPosture(workspaceRoot: string) {
96
100
  requiresTenant: config.requiresTenant,
97
101
  bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
98
102
  tenantClaim: config.claims.tenantId ?? "tenant_id",
103
+ productionChecklist: [
104
+ { item: "auth mode is jwt or oidc", ok: productionMode },
105
+ { item: "FORGE_AUTH_ISSUER configured", ok: Boolean(config.issuer) },
106
+ { item: "FORGE_AUTH_AUDIENCE configured", ok: Boolean(config.audience) },
107
+ { item: "FORGE_AUTH_JWKS_URI or OIDC discovery configured", ok: config.mode === "oidc" || Boolean(config.jwksUri) },
108
+ { item: "tenant claim mapped", ok: Boolean(config.claims.tenantId) },
109
+ { item: "permission claim mapped", ok: Boolean(config.claims.permissions) },
110
+ { item: "dev-headers disabled for public runtime", ok: config.mode !== "dev-headers" },
111
+ ],
99
112
  reason: productionMode
100
113
  ? configReady
101
114
  ? "jwt/oidc production auth configuration is present"
@@ -106,7 +119,58 @@ function buildAuthPosture(workspaceRoot: string) {
106
119
  ? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
107
120
  : ["forge auth check --json", "configure FORGE_AUTH_ISSUER, FORGE_AUTH_AUDIENCE, and FORGE_AUTH_JWKS_URI or OIDC issuer"]
108
121
  : ["set FORGE_AUTH_MODE=jwt or oidc for production", "forge auth prove --prod --token <jwt> --json"],
109
- };
122
+ });
123
+ }
124
+
125
+ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
126
+ const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
127
+ const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
128
+ const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
129
+ const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
130
+ const permissions = ["onboarding:read", "invitations:create", "tasks:update"];
131
+ const permissionVocabularyPresent = workos.detected && workos.claimStatus.some((claim) => claim.name === "permissions" && claim.ok);
132
+ const checks = [
133
+ {
134
+ id: "tenant-claim",
135
+ ok: requiresTenant && workos.claimStatus.some((claim) => claim.name === "tenantId" && claim.ok),
136
+ evidence: "Forge tenant claim maps to WorkOS organization_id and tenant auth is required.",
137
+ },
138
+ {
139
+ id: "permission-claim",
140
+ ok: permissionVocabularyPresent,
141
+ evidence: "Forge permissions claim maps to WorkOS permissions.",
142
+ },
143
+ {
144
+ id: "seed-organizations",
145
+ ok: rootSeedPresent || generatedSeedPresent,
146
+ evidence: rootSeedPresent ? "workos-seed.yml exists at app root." : "generated WorkOS seed exists.",
147
+ },
148
+ {
149
+ id: "agent-auth-metadata",
150
+ ok: authMdPresent && metadataPresent,
151
+ evidence: "public/auth.md and protected resource metadata are present.",
152
+ },
153
+ ];
154
+ return normalizeForgeCliCommandsInValue(workspaceRoot, {
155
+ scenario: "multi-tenant",
156
+ ok: checks.every((check) => check.ok),
157
+ claims: {
158
+ acme: { organization_id: "org_acme", role: "owner", permissions },
159
+ globex: { organization_id: "org_globex", role: "member", permissions: ["onboarding:read", "tasks:update"] },
160
+ },
161
+ invariants: [
162
+ "Acme and Globex must use different organization_id claim values.",
163
+ "Tenant-scoped reads must include the active organization_id.",
164
+ "Tenant-scoped writes must verify the resource tenant before mutation.",
165
+ "Role-only UI affordances are not sufficient; policies must use permissions/claims.",
166
+ ],
167
+ checks,
168
+ nextActions: [
169
+ "forge workos doctor --json",
170
+ "forge workos seed --file workos-seed.yml --dry-run --json",
171
+ "run an HTTP E2E with Acme and Globex tokens before production",
172
+ ],
173
+ });
110
174
  }
111
175
 
112
176
  function validateConfig(workspaceRoot: string): AuthCommandResult {
@@ -258,8 +322,12 @@ export async function runAuthCommand(
258
322
  ? tokenProof.error
259
323
  : undefined;
260
324
  const proofOk = checked.ok && (!options.prod || (productionMode && tokenProof?.ok === true));
325
+ const multiTenantProof = options.scenario === "multi-tenant"
326
+ ? buildMultiTenantProof(options.workspaceRoot, workos, config.requiresTenant)
327
+ : null;
328
+ const scenarioOk = !multiTenantProof || multiTenantProof.ok;
261
329
  return {
262
- ok: proofOk,
330
+ ok: proofOk && scenarioOk,
263
331
  mode: config.mode,
264
332
  data: {
265
333
  schemaVersion: "0.1.0",
@@ -268,8 +336,10 @@ export async function runAuthCommand(
268
336
  mode: config.mode,
269
337
  productionReady: productionMode && checked.ok,
270
338
  prod: options.prod === true,
339
+ scenario: options.scenario ?? null,
271
340
  authPosture: posture,
272
341
  ...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
342
+ ...(multiTenantProof ? { multiTenantProof } : {}),
273
343
  invariants: [
274
344
  {
275
345
  id: "INV-001",
@@ -305,7 +375,7 @@ export async function runAuthCommand(
305
375
  checkedAt: "deterministic",
306
376
  },
307
377
  error: prodError ?? checked.error,
308
- exitCode: proofOk ? 0 : 1,
378
+ exitCode: proofOk && scenarioOk ? 0 : 1,
309
379
  };
310
380
  }
311
381
  if (options.subcommand === "config") {
@@ -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;
@@ -102,10 +104,16 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
102
104
  if (!emptyCategory(changed, "generated") && humanChanges.total === 0) {
103
105
  risks.push("only generated artifacts changed; verify the source edit, generator input, or intentional regeneration that produced them");
104
106
  }
107
+ return risks;
108
+ }
109
+
110
+ function buildAdvisories(git: WorkspaceGitSummary): string[] {
111
+ const advisories: string[] = [];
112
+ const changed = git.changeSummary.changed;
105
113
  if (changed.total.count > 50) {
106
- 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`);
107
115
  }
108
- return risks;
116
+ return advisories;
109
117
  }
110
118
 
111
119
  function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
@@ -127,7 +135,12 @@ function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
127
135
  "forge generate --check --json",
128
136
  ];
129
137
  }
138
+ const authoredGeneratedInputs =
139
+ changed.byType.source.count +
140
+ changed.byType.config.count +
141
+ changed.byType.operational.count;
130
142
  return [
143
+ ...(authoredGeneratedInputs > 0 ? ["forge generate --check --json"] : []),
131
144
  "forge handoff --json",
132
145
  "forge test plan --changed --json",
133
146
  "forge verify --changed",
@@ -189,7 +202,7 @@ function buildGeneratedChangeExplanation(
189
202
  };
190
203
  }
191
204
 
192
- export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean } = {}): ChangedCommandResult {
205
+ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean; reviewOnly?: boolean } = {}): ChangedCommandResult {
193
206
  const git = buildWorkspaceGitSummary(workspaceRoot);
194
207
  const changed = git.changeSummary.changed;
195
208
  const humanChanges = selectHumanChangeSummary(changed);
@@ -198,16 +211,25 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
198
211
  const authoredStaged = filterCategorizedSummary(git.changeSummary.staged, [...AUTHORED_CHANGE_TYPES]);
199
212
  const authoredUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...AUTHORED_CHANGE_TYPES]);
200
213
  const authoredUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...AUTHORED_CHANGE_TYPES]);
201
- const viewHumanChanges = options.authoredOnly ? selectHumanChangeSummary(authoredChanged) : humanChanges;
202
- const viewChanged = options.authoredOnly ? authoredChanged : changed;
203
- const viewStaged = options.authoredOnly ? authoredStaged : git.changeSummary.staged;
204
- const viewUnstaged = options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
205
- const viewUntracked = options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
206
- 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
207
228
  ? { total: 0, generated: { count: 0, sample: [], hidden: 0 } }
208
229
  : derivedChanges;
209
230
  const risks = buildRisks(git);
210
- const recommendedCommands = buildRecommendedCommands(git);
231
+ const advisories = buildAdvisories(git);
232
+ const recommendedCommands = forgeCliCommandsForWorkspace(workspaceRoot, buildRecommendedCommands(git));
211
233
  const reviewFocus = buildReviewFocus(viewHumanChanges, viewDerivedChanges);
212
234
  const generatedExplanation = buildGeneratedChangeExplanation(viewHumanChanges, viewDerivedChanges);
213
235
  const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
@@ -223,7 +245,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
223
245
  commit: git.commit,
224
246
  workspaceMode: git.workspaceMode ?? (git.available ? "git" : "nonGit"),
225
247
  tracking: git.tracking ?? git.source,
226
- view: options.authoredOnly ? "authored" : "all",
248
+ view: options.reviewOnly ? "review" : options.authoredOnly ? "authored" : "all",
227
249
  changedFiles: viewChanged.total.count,
228
250
  humanFiles: viewHumanChanges.total,
229
251
  generatedFiles: viewDerivedChanges.total,
@@ -253,6 +275,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
253
275
  generatedExplanation,
254
276
  diffPlan,
255
277
  risks,
278
+ advisories,
256
279
  recommendedCommands,
257
280
  nextActions: recommendedCommands,
258
281
  },
@@ -268,6 +291,7 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
268
291
  const generatedExplanation = result.data.generatedExplanation as { summary?: string } | undefined;
269
292
  const diffPlan = result.data.diffPlan as { summary?: string; authoredDiffCommand?: string; generatedDiffCommand?: string; generatedCollapsedByDefault?: boolean } | undefined;
270
293
  const risks = (result.data.risks as string[] | undefined) ?? [];
294
+ const advisories = (result.data.advisories as string[] | undefined) ?? [];
271
295
  const nextActions = (result.data.nextActions as string[] | undefined) ?? [];
272
296
  const lines = [
273
297
  `Forge changed: ${result.ok ? "ready" : "git unavailable"}`,
@@ -310,6 +334,9 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
310
334
  if (risks.length > 0) {
311
335
  lines.push("", "Risks:", ...risks.map((risk) => ` ${risk}`));
312
336
  }
337
+ if (advisories.length > 0) {
338
+ lines.push("", "Notes:", ...advisories.map((advisory) => ` ${advisory}`));
339
+ }
313
340
 
314
341
  lines.push("", "Next:", ...nextActions.map((command) => ` ${command}`));
315
342
  return `${lines.join("\n")}\n`;
@@ -284,6 +284,7 @@ import { resolveAuthFromCli } from "../runtime/auth/resolve.ts";
284
284
  import { getActiveDbAdapter } from "../runtime/executor.ts";
285
285
  import { CLI_VERSION, FORGEOS_VERSION } from "../version.ts";
286
286
  import type { CategorizedFileSummary } from "../workspace/change-summary.ts";
287
+ import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
287
288
  import { buildWorkspaceGitSummary } from "../workspace/git-summary.ts";
288
289
  import { startCommandHeartbeat } from "./progress.ts";
289
290
 
@@ -722,33 +723,69 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
722
723
  const driftClean = driftSummary.ok === true;
723
724
  const ok = driftClean && generatedReady;
724
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;
725
734
  const generatedState = !generatedReady
726
735
  ? "missing-artifacts"
736
+ : generatedNeedsCheck
737
+ ? "check-needed"
727
738
  : driftClean
728
739
  ? "ready"
729
740
  : "drift";
730
- const generatedNextActions = generatedState === "ready"
731
- ? ["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"]
732
745
  : ["forge generate", "forge check --json", "forge inspect drift --json"];
733
- const changed = gitSummary.changeSummary.changed;
734
- const generatedGitFiles = changed.byType.generated.count;
735
- const authoredGitFiles = changed.total.count - generatedGitFiles;
746
+ const generatedNextActions = forgeCliCommandsForWorkspace(workspaceRoot, generatedNextActionsRaw);
736
747
  const generatedGitExplanation = generatedGitFiles === 0
737
- ? "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"
738
751
  : authoredGitFiles === 0
739
752
  ? "forge generate --check can be clean while git shows generated artifacts changed: generated files match current workspace inputs but differ from HEAD"
740
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
+ ]);
741
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
+ ]);
742
779
  const studio = {
743
- openCommand: "forge studio open . --preview-port 5174 --target codex --json",
744
- attachCommand: "forge studio attach . --preview-port 5174 --target codex --json",
745
- snapshotCommand: "forge studio snapshot . --preview-port 5174 --target codex --json",
746
- watchCommand: "forge studio watch . --preview-port 5174 --target codex --json",
747
- bridgeCommand: "forge studio bridge . --preview-port 5174 --target codex --studio-url http://127.0.0.1:3765 --json",
748
- 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,
749
786
  targetPreviewUrl: "http://127.0.0.1:5174",
750
- startTargetAppCommand: "forge dev --port 3766 --web-port 5174",
751
- probeCommand: "forge dev --once --json",
787
+ startTargetAppCommand: studioStartTargetAppCommand,
788
+ probeCommand: studioProbeCommand,
752
789
  useful: frontendPresent,
753
790
  note: frontendPresent
754
791
  ? "Attach this app to Forge Studio as an external-agent workroom; Studio should preview the target app on 5174."
@@ -764,11 +801,13 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
764
801
  state: generatedState,
765
802
  ready: generatedReady,
766
803
  driftClean,
804
+ freshness: generatedNeedsCheck ? "unverified" : generatedState === "ready" ? "verified-or-unchanged" : "attention",
805
+ authoredGeneratedInputs,
767
806
  missingArtifacts,
768
807
  tableDrift,
769
- safeDevCommand: "forge dev",
770
- checkCommand: "forge generate --check --json",
771
- repairCommand: "forge generate",
808
+ safeDevCommand,
809
+ checkCommand: generatedCheckCommand,
810
+ repairCommand: generatedRepairCommand,
772
811
  git: {
773
812
  changedFiles: changed.total.count,
774
813
  authoredFiles: authoredGitFiles,
@@ -809,8 +848,11 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
809
848
  },
810
849
  },
811
850
  git,
812
- nextActions: ok
813
- ? [
851
+ nextActions: forgeCliCommandsForWorkspace(
852
+ workspaceRoot,
853
+ ok
854
+ ? [
855
+ ...(generatedState === "check-needed" ? ["forge generate --check --json"] : []),
814
856
  "forge handoff --json",
815
857
  "forge changed --json",
816
858
  "forge dev",
@@ -819,7 +861,7 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
819
861
  "forge inspect handoff --json",
820
862
  "forge verify --changed",
821
863
  ]
822
- : [
864
+ : [
823
865
  "forge generate",
824
866
  "forge handoff --json",
825
867
  "forge changed --json",
@@ -827,6 +869,7 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
827
869
  "forge inspect drift --json",
828
870
  "forge agent prepare --target codex --json",
829
871
  ],
872
+ ),
830
873
  },
831
874
  exitCode: ok ? 0 : 1,
832
875
  };
@@ -1589,6 +1632,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1589
1632
  packageManager: command.packageManager,
1590
1633
  install: command.install,
1591
1634
  git: command.git,
1635
+ fieldTest: command.fieldTest,
1592
1636
  forgePackageSpec: command.forgePackageSpec,
1593
1637
  localForge: command.localForge,
1594
1638
  workspaceRoot: command.workspaceRoot,
@@ -1651,7 +1695,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1651
1695
  concurrency: 4,
1652
1696
  });
1653
1697
  if (command.json) {
1654
- process.stdout.write(formatJsonResult(buildGenerateJson(result)));
1698
+ process.stdout.write(formatJsonResult(buildGenerateJson(result, { workspaceRoot: command.workspaceRoot })));
1655
1699
  } else {
1656
1700
  process.stdout.write(formatAgentContractHuman(command.subcommand, result));
1657
1701
  writeHumanGenerate(result);
@@ -2015,7 +2059,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2015
2059
  });
2016
2060
 
2017
2061
  if (command.json) {
2018
- process.stdout.write(formatJsonResult(buildGenerateJson(result)));
2062
+ process.stdout.write(formatJsonResult(buildGenerateJson(result, { workspaceRoot: command.workspaceRoot })));
2019
2063
  } else {
2020
2064
  writeHumanGenerate(result);
2021
2065
  }
@@ -2025,7 +2069,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2025
2069
  case "add": {
2026
2070
  const result = await runAddCommand(command.alias, command.options);
2027
2071
  if (command.options.json) {
2028
- process.stdout.write(formatJsonResult(buildAddJson(result)));
2072
+ process.stdout.write(formatJsonResult(buildAddJson(result, { workspaceRoot: command.options.workspaceRoot })));
2029
2073
  } else {
2030
2074
  writeHumanAdd(result);
2031
2075
  }
@@ -2061,6 +2105,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2061
2105
  case "changed": {
2062
2106
  const result = runChangedCommand(command.workspaceRoot, {
2063
2107
  authoredOnly: command.authoredOnly,
2108
+ reviewOnly: command.reviewOnly,
2064
2109
  });
2065
2110
  if (command.json) {
2066
2111
  process.stdout.write(formatJsonResult(result.data));
@@ -2171,11 +2216,12 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2171
2216
  return result.exitCode;
2172
2217
  }
2173
2218
  case "check": {
2174
- const result = await runCheckCommand(process.cwd(), {
2219
+ const checkWorkspaceRoot = process.cwd();
2220
+ const result = await runCheckCommand(checkWorkspaceRoot, {
2175
2221
  strictSecrets: command.strictSecrets,
2176
2222
  });
2177
2223
  if (command.json) {
2178
- process.stdout.write(formatJsonResult(buildCheckJson(result)));
2224
+ process.stdout.write(formatJsonResult(buildCheckJson(result, { workspaceRoot: checkWorkspaceRoot })));
2179
2225
  } else {
2180
2226
  writeHumanGenerate(result);
2181
2227
  }