forgeos 0.1.0-alpha.61 → 0.1.0-alpha.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.63 input=6671cc4ce9f1aeab62768f037489379d8303a79d2681b1fae4cb42a84e818b30 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.63
4
+
5
+ ### Patch Changes
6
+
7
+ - Make generated freshness and Java/Spring brownfield analysis trustworthy.
8
+
9
+ - `forge status` now executes deterministic generation verification, so a
10
+ clean Git worktree can no longer hide committed generated drift that
11
+ `forge handoff` would reject.
12
+ - Report compact generation evidence in status output, including freshness,
13
+ changed artifact counts, samples, and diagnostic codes.
14
+ - Extend `forge import analyze` to inventory Maven dependencies, recognize
15
+ Spring Boot, scan Java source, discover Spring MVC controller mappings, and
16
+ detect `System.getenv(...)` usage.
17
+ - Keep Spring-derived command/query candidates static, review-required, and
18
+ hidden from agents under the existing brownfield safety model.
19
+ - Pin a modern Maven compiler plugin for the Java adapter and Spring starter,
20
+ so their declared Java 17 release compiles on the supported JDK 21 toolchain.
21
+ - Make example fixture seeding replace package directories cleanly, avoiding
22
+ cross-identity timestamp/ownership failures in shared development workspaces.
23
+
24
+ ## 0.1.0-alpha.62
25
+
26
+ ### Patch Changes
27
+
28
+ - Keep commit-ready change views from exposing local secrets or WorkOS state.
29
+
30
+ - Classify real `.env*` files as operational while keeping `.env.example`
31
+ and `.env.*.example` files commit-ready.
32
+ - Exclude `.workos-seed-state.json` and `.workos-fga-state.json` from
33
+ commit-ready summaries.
34
+ - Add regression coverage so deploy examples stay visible while
35
+ `deploy/.env.production` remains excluded.
36
+
3
37
  ## 0.1.0-alpha.61
4
38
 
5
39
  ### Patch Changes
package/README.md CHANGED
@@ -309,7 +309,7 @@ Common command groups:
309
309
  | `forge release` | Release/source-map bridge and symbolication |
310
310
  | `forge agent`, `forge agent-contract` | Agent-facing contract, adapter exports, context, install, ingest, and memory commands |
311
311
  | `forge mcp serve` | MCP server for external agents to read Forge context, memory, timeline, and inspect surfaces |
312
- | `forge import analyze`, `forge import inspect` | Brownfield TypeScript/JavaScript app inventory and reviewed migration planning |
312
+ | `forge import analyze`, `forge import inspect` | Brownfield TypeScript/JavaScript and Java/Spring app inventory with reviewed migration planning |
313
313
  | `forge inspect imported --json` | Inspect `.forge/import` analysis artifacts |
314
314
  | `forge self-host` | Self-host packaging and checks |
315
315
 
@@ -31,4 +31,17 @@
31
31
  <version>${jackson.version}</version>
32
32
  </dependency>
33
33
  </dependencies>
34
+
35
+ <build>
36
+ <plugins>
37
+ <plugin>
38
+ <groupId>org.apache.maven.plugins</groupId>
39
+ <artifactId>maven-compiler-plugin</artifactId>
40
+ <version>3.13.0</version>
41
+ <configuration>
42
+ <release>${maven.compiler.release}</release>
43
+ </configuration>
44
+ </plugin>
45
+ </plugins>
46
+ </build>
34
47
  </project>
@@ -33,4 +33,17 @@
33
33
  <version>6.1.14</version>
34
34
  </dependency>
35
35
  </dependencies>
36
+
37
+ <build>
38
+ <plugins>
39
+ <plugin>
40
+ <groupId>org.apache.maven.plugins</groupId>
41
+ <artifactId>maven-compiler-plugin</artifactId>
42
+ <version>3.13.0</version>
43
+ <configuration>
44
+ <release>${maven.compiler.release}</release>
45
+ </configuration>
46
+ </plugin>
47
+ </plugins>
48
+ </build>
36
49
  </project>
package/docs/changelog.md CHANGED
@@ -4,6 +4,20 @@ Release history for the `forgeos` npm package.
4
4
 
5
5
  The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
+ ## 0.1.0-alpha.63
8
+
9
+ - `forge status` now verifies generated output deterministically instead of
10
+ inferring freshness from Git state, preventing committed stale artifacts from
11
+ appearing ready while `forge handoff` rejects them.
12
+ - Brownfield import now understands Java/Spring Boot projects: Maven dependency
13
+ inventory, Spring MVC mappings, Java handler scoping, path variables, and
14
+ `System.getenv(...)` usage.
15
+ - Imported Spring operations retain the same conservative review and agent
16
+ visibility defaults as TypeScript/JavaScript candidates.
17
+ - Java adapter builds now pin a release-aware Maven compiler plugin, and example
18
+ fixture setup safely replaces generated package directories in shared
19
+ workspaces.
20
+
7
21
  ## 0.1.0-alpha.61
8
22
 
9
23
  - Added `forge golden-path plan/status` as the canonical
@@ -24,6 +24,14 @@
24
24
 
25
25
  <build>
26
26
  <plugins>
27
+ <plugin>
28
+ <groupId>org.apache.maven.plugins</groupId>
29
+ <artifactId>maven-compiler-plugin</artifactId>
30
+ <version>3.13.0</version>
31
+ <configuration>
32
+ <release>${maven.compiler.release}</release>
33
+ </configuration>
34
+ </plugin>
27
35
  <plugin>
28
36
  <groupId>org.apache.maven.plugins</groupId>
29
37
  <artifactId>maven-shade-plugin</artifactId>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.61",
3
+ "version": "0.1.0-alpha.63",
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.61","releaseId":"forgeos@0.1.0-alpha.61+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.63","releaseId":"forgeos@0.1.0-alpha.63+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=82fdee65e1219bbb3fa9572a4c904c7e1e4061b00430769528426ece3b3a4067
1
+ // @forge-generated generator=0.1.0-alpha.63 input=6671cc4ce9f1aeab62768f037489379d8303a79d2681b1fae4cb42a84e818b30 content=407b2f82bfffde0fd6fec96fc9130364878d1f2a6d0945e40da2b05d49aea75a
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.61",
23
- "releaseId": "forgeos@0.1.0-alpha.61+unknown",
22
+ "packageVersion": "0.1.0-alpha.63",
23
+ "releaseId": "forgeos@0.1.0-alpha.63+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -16,7 +16,7 @@ import type {
16
16
  } from "./types.ts";
17
17
 
18
18
  const IMPORT_DIR = ".forge/import";
19
- const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
19
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java"]);
20
20
  const IGNORED_DIRS = new Set([
21
21
  ".git",
22
22
  ".forge",
@@ -130,8 +130,10 @@ function buildDependencyInventory(workspaceRoot: string): {
130
130
  dependencies: ImportedDependencyInventory;
131
131
  } {
132
132
  const pkg = readPackageJson(workspaceRoot);
133
- const dependencies = objectKeys(pkg.dependencies);
133
+ const npmDependencies = objectKeys(pkg.dependencies);
134
134
  const devDependencies = objectKeys(pkg.devDependencies);
135
+ const pom = readMavenInventory(workspaceRoot);
136
+ const dependencies = [...new Set([...npmDependencies, ...pom.dependencies])].sort();
135
137
  const all = [...dependencies, ...devDependencies];
136
138
  const scripts = objectKeys(pkg.scripts);
137
139
  const frameworks = [
@@ -143,9 +145,11 @@ function buildDependencyInventory(workspaceRoot: string): {
143
145
  hasAny(all, ["@nestjs/core"]) ? "nest" : null,
144
146
  hasAny(all, ["fastify"]) ? "fastify" : null,
145
147
  hasAny(all, ["hono"]) ? "hono" : null,
148
+ pom.frameworks.includes("spring-boot") ? "spring-boot" : null,
146
149
  ].filter((value): value is string => value !== null);
147
150
  const dataPackages = all.filter((name) =>
148
- ["@prisma/client", "prisma", "drizzle-orm", "typeorm", "mongoose", "sequelize", "knex"].includes(name),
151
+ ["@prisma/client", "prisma", "drizzle-orm", "typeorm", "mongoose", "sequelize", "knex"].includes(name) ||
152
+ /(?:spring-boot-starter-data-|hibernate-core|mybatis)/u.test(name),
149
153
  );
150
154
  const externalPackages = all.filter((name) =>
151
155
  [
@@ -163,7 +167,7 @@ function buildDependencyInventory(workspaceRoot: string): {
163
167
  ].includes(name),
164
168
  );
165
169
  return {
166
- packageName: typeof pkg.name === "string" ? pkg.name : undefined,
170
+ packageName: typeof pkg.name === "string" ? pkg.name : pom.packageName,
167
171
  dependencies: {
168
172
  dependencies,
169
173
  devDependencies,
@@ -175,6 +179,34 @@ function buildDependencyInventory(workspaceRoot: string): {
175
179
  };
176
180
  }
177
181
 
182
+ function readMavenInventory(workspaceRoot: string): {
183
+ packageName?: string;
184
+ dependencies: string[];
185
+ frameworks: string[];
186
+ } {
187
+ const pomPath = join(workspaceRoot, "pom.xml");
188
+ if (!existsSync(pomPath)) {
189
+ return { dependencies: [], frameworks: [] };
190
+ }
191
+ const text = readFileSync(pomPath, "utf8");
192
+ const dependencies: string[] = [];
193
+ for (const match of text.matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gu)) {
194
+ const block = match[1] ?? "";
195
+ const groupId = block.match(/<groupId>\s*([^<]+?)\s*<\/groupId>/u)?.[1]?.trim();
196
+ const artifactId = block.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/u)?.[1]?.trim();
197
+ if (artifactId) {
198
+ dependencies.push(groupId ? `${groupId}:${artifactId}` : artifactId);
199
+ }
200
+ }
201
+ const projectWithoutParent = text.replace(/<parent\b[^>]*>[\s\S]*?<\/parent>/u, "");
202
+ const packageName = projectWithoutParent.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/u)?.[1]?.trim();
203
+ return {
204
+ ...(packageName ? { packageName } : {}),
205
+ dependencies: [...new Set(dependencies)].sort(),
206
+ frameworks: /\b(?:org\.springframework\.boot|spring-boot)[\w.:-]*/u.test(text) ? ["spring-boot"] : [],
207
+ };
208
+ }
209
+
178
210
  function stableId(prefix: string, parts: string[]): string {
179
211
  let hash = 2166136261;
180
212
  for (const char of parts.join("|")) {
@@ -185,7 +217,7 @@ function stableId(prefix: string, parts: string[]): string {
185
217
  }
186
218
 
187
219
  function stripExtension(segment: string): string {
188
- return segment.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/u, "");
220
+ return segment.replace(/\.(ts|tsx|js|jsx|mjs|cjs|java)$/u, "");
189
221
  }
190
222
 
191
223
  function normalizeRouteSegment(segment: string): string | null {
@@ -219,7 +251,56 @@ function routePathFromFile(relativePath: string, marker: string): string {
219
251
  }
220
252
 
221
253
  function joinRoutePath(base: string, child: string): string {
222
- return `/${[base, child].map((part) => part.replace(/^\/|\/$/gu, "")).filter(Boolean).join("/")}`.replace(/\/+/gu, "/");
254
+ return `/${[base, child].map((part) => part.replace(/^\/|\/$/gu, "")).filter(Boolean).join("/")}`
255
+ .replace(/\/+/gu, "/")
256
+ .replace(/\{([^}/]+)\}/gu, ":$1");
257
+ }
258
+
259
+ function springMappingPath(argumentsText: string): string {
260
+ return argumentsText.match(/(?:path|value)\s*=\s*["']([^"']*)["']/u)?.[1] ??
261
+ argumentsText.match(/["']([^"']*)["']/u)?.[1] ??
262
+ "";
263
+ }
264
+
265
+ function springHandlerName(text: string): string | undefined {
266
+ return text.match(/(?:public|protected|private)?\s*(?:static\s+)?(?:[\w.$<>?,\[\]]+\s+)+(\w+)\s*\(/u)?.[1];
267
+ }
268
+
269
+ function detectSpringRoutes(file: SourceFile, routes: ImportedRoute[]): void {
270
+ if (extname(file.relativePath) !== ".java" || !/@(?:RestController|Controller)\b/u.test(file.text)) {
271
+ return;
272
+ }
273
+ const classIndex = file.text.search(/\bclass\s+\w+/u);
274
+ if (classIndex < 0) {
275
+ return;
276
+ }
277
+ const classAnnotations = file.text.slice(0, classIndex);
278
+ const classMapping = [...classAnnotations.matchAll(/@RequestMapping\s*(?:\(([^)]*)\))?/gu)].at(-1);
279
+ const basePath = springMappingPath(classMapping?.[1] ?? "");
280
+ const methodMappings = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(([^)]*)\))?/gu;
281
+ methodMappings.lastIndex = classIndex;
282
+ for (const match of file.text.matchAll(methodMappings)) {
283
+ const annotation = match[1] ?? "RequestMapping";
284
+ const argumentsText = match[2] ?? "";
285
+ const handler = springHandlerName(file.text.slice((match.index ?? 0) + match[0].length, (match.index ?? 0) + match[0].length + 500));
286
+ if (!handler) {
287
+ continue;
288
+ }
289
+ const methods = annotation === "RequestMapping"
290
+ ? [...argumentsText.matchAll(/RequestMethod\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gu)].map((item) => item[1] ?? "ANY")
291
+ : [annotation.replace(/Mapping$/u, "").toUpperCase()];
292
+ for (const method of methods.length > 0 ? methods : ["ANY"]) {
293
+ addRoute(
294
+ routes,
295
+ method,
296
+ joinRoutePath(basePath, springMappingPath(argumentsText)),
297
+ file.relativePath,
298
+ "spring",
299
+ 0.86,
300
+ handler,
301
+ );
302
+ }
303
+ }
223
304
  }
224
305
 
225
306
  function pathIncludesRouteMarker(relativePath: string, marker: string): boolean {
@@ -251,6 +332,7 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
251
332
  const routes: ImportedRoute[] = [];
252
333
  const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
253
334
  for (const file of files) {
335
+ const isJava = extname(file.relativePath) === ".java";
254
336
  if (pathIncludesRouteMarker(file.relativePath, "/app/api/") && basename(file.relativePath).startsWith("route.")) {
255
337
  const path = routePathFromFile(file.relativePath, "/app/");
256
338
  for (const method of methods) {
@@ -265,12 +347,14 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
265
347
  addRoute(routes, "ANY", path, file.relativePath, "next-pages-api", 0.78, "default");
266
348
  }
267
349
 
268
- const expressRoute = /\b(?:app|router)\s*\.\s*(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/giu;
269
- for (const match of file.text.matchAll(expressRoute)) {
270
- addRoute(routes, match[1] ?? "all", match[2] ?? "/", file.relativePath, "express", 0.84);
350
+ if (!isJava) {
351
+ const expressRoute = /\b(?:app|router)\s*\.\s*(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/giu;
352
+ for (const match of file.text.matchAll(expressRoute)) {
353
+ addRoute(routes, match[1] ?? "all", match[2] ?? "/", file.relativePath, "express", 0.84);
354
+ }
271
355
  }
272
356
 
273
- const controller = file.text.match(/@Controller\s*\(\s*["'`]([^"'`]*)["'`]\s*\)/u);
357
+ const controller = !isJava ? file.text.match(/@Controller\s*\(\s*["'`]([^"'`]*)["'`]\s*\)/u) : null;
274
358
  if (controller) {
275
359
  const nestRoute = /@(Get|Post|Put|Patch|Delete|All)\s*\(\s*(?:["'`]([^"'`]*)["'`])?\s*\)/giu;
276
360
  for (const match of file.text.matchAll(nestRoute)) {
@@ -284,6 +368,7 @@ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
284
368
  );
285
369
  }
286
370
  }
371
+ detectSpringRoutes(file, routes);
287
372
  }
288
373
  const seen = new Set<string>();
289
374
  return routes
@@ -346,6 +431,9 @@ function collectEnv(workspaceRoot: string, files: SourceFile[]): ImportedInvento
346
431
  for (const match of file.text.matchAll(/\bprocess\.env\.([A-Z0-9_]+)/gu)) {
347
432
  names.add(match[1] ?? "");
348
433
  }
434
+ for (const match of file.text.matchAll(/\bSystem\.getenv\(\s*["']([A-Z0-9_]+)["']\s*\)/gu)) {
435
+ names.add(match[1] ?? "");
436
+ }
349
437
  }
350
438
  const envFiles = [".env", ".env.local", ".env.example", ".env.sample"]
351
439
  .filter((name) => existsSync(join(workspaceRoot, name)))
@@ -398,6 +486,13 @@ function scopedSourceTextForRoute(route: ImportedRoute, text: string): string |
398
486
  /@(Get|Post|Put|Patch|Delete|All)\s*\(/gu,
399
487
  );
400
488
  }
489
+ if (route.source === "spring" && route.handler) {
490
+ return sliceUntilNextMatch(
491
+ text,
492
+ new RegExp(`@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)[\\s\\S]{0,500}?\\b${escapeRegExp(route.handler)}\\s*\\(`, "u"),
493
+ /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\b/gu,
494
+ );
495
+ }
401
496
  return null;
402
497
  }
403
498
 
@@ -451,7 +546,7 @@ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCan
451
546
  if (tenant) {
452
547
  risks.add("tenant-sensitive");
453
548
  }
454
- if (/\bprocess\.env\./u.test(text)) {
549
+ if (/\bprocess\.env\.|\bSystem\.getenv\s*\(/u.test(text)) {
455
550
  risks.add("secret-sensitive");
456
551
  }
457
552
  if (methodUnknown) {
@@ -12,7 +12,7 @@ export interface BrownfieldImportCommandOptions {
12
12
  export type ImportedAssurance = "static-scan";
13
13
  export type ImportedReviewStatus = "needs-review" | "approved" | "rejected";
14
14
  export type ImportedEntryKind = "command" | "command-candidate" | "query" | "unknown";
15
- export type ImportedRouteSource = "next-app-router" | "next-pages-api" | "express" | "nest" | "unknown";
15
+ export type ImportedRouteSource = "next-app-router" | "next-pages-api" | "express" | "nest" | "spring" | "unknown";
16
16
 
17
17
  export interface ImportedDependencyInventory {
18
18
  dependencies: string[];
@@ -732,11 +732,18 @@ export interface StatusCommandResult {
732
732
  exitCode: 0 | 1;
733
733
  }
734
734
 
735
- export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
735
+ export async function runStatusCommand(workspaceRoot: string): Promise<StatusCommandResult> {
736
736
  const summary = buildInspectSummary(workspaceRoot);
737
737
  const drift = buildDriftInspect(workspaceRoot);
738
738
  const handoff = buildHandoffInspect(workspaceRoot);
739
739
  const gitSummary = buildWorkspaceGitSummary(workspaceRoot);
740
+ const generatedVerification = await runGenerateCommand({
741
+ workspaceRoot,
742
+ check: true,
743
+ dryRun: false,
744
+ json: true,
745
+ concurrency: 4,
746
+ });
740
747
  const git = {
741
748
  available: gitSummary.available,
742
749
  ...(gitSummary.error ? { error: gitSummary.error } : {}),
@@ -751,7 +758,8 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
751
758
  const missingArtifacts = Number(summaryBlock.missingArtifacts ?? 0);
752
759
  const tableDrift = Number(driftSummary.tableDrift ?? 0);
753
760
  const generatedReady = missingArtifacts === 0;
754
- const driftClean = driftSummary.ok === true;
761
+ const generatedFresh = generatedVerification.exitCode === 0;
762
+ const driftClean = driftSummary.ok === true && generatedFresh;
755
763
  const ok = driftClean && generatedReady;
756
764
  const handoffDefaultReady = handoffSummary.defaultReady === true;
757
765
  const changed = gitSummary.changeSummary.changed;
@@ -761,24 +769,19 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
761
769
  changed.byType.source.count +
762
770
  changed.byType.config.count +
763
771
  changed.byType.operational.count;
764
- const generatedNeedsCheck = generatedReady && driftClean && generatedGitFiles === 0 && authoredGeneratedInputs > 0;
765
772
  const generatedState = !generatedReady
766
773
  ? "missing-artifacts"
767
- : generatedNeedsCheck
768
- ? "check-needed"
769
774
  : driftClean
770
775
  ? "ready"
771
776
  : "drift";
772
777
  const generatedNextActionsRaw = generatedState === "ready"
773
778
  ? ["forge dev", "forge dev --once --json", "forge generate --check --json"]
774
- : generatedState === "check-needed"
775
- ? ["forge generate --check --json", "forge handoff --json", "forge dev --once --json"]
776
779
  : ["forge generate", "forge check --json", "forge inspect drift --json"];
777
780
  const generatedNextActions = forgeCliCommandsForWorkspace(workspaceRoot, generatedNextActionsRaw);
778
- const generatedGitExplanation = generatedGitFiles === 0
779
- ? generatedNeedsCheck
780
- ? "git status has no generated artifact changes, but authored source/config changes mean freshness is unverified until forge generate --check runs"
781
- : "git status has no generated artifact changes"
781
+ const generatedGitExplanation = !generatedFresh
782
+ ? "deterministic generation verification found stale artifacts; git cleanliness does not prove generated freshness"
783
+ : generatedGitFiles === 0
784
+ ? "git status has no generated artifact changes and deterministic generation verification passed"
782
785
  : authoredGitFiles === 0
783
786
  ? "forge generate --check can be clean while git shows generated artifacts changed: generated files match current workspace inputs but differ from HEAD"
784
787
  : "git status includes generated artifacts alongside authored changes; review authored inputs first";
@@ -832,10 +835,18 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
832
835
  state: generatedState,
833
836
  ready: generatedReady,
834
837
  driftClean,
835
- freshness: generatedNeedsCheck ? "unverified" : generatedState === "ready" ? "verified-or-unchanged" : "attention",
838
+ freshness: generatedState === "ready" ? "verified" : generatedFresh ? "attention" : "stale",
836
839
  authoredGeneratedInputs,
837
840
  missingArtifacts,
838
841
  tableDrift,
842
+ verification: {
843
+ checked: true,
844
+ fresh: generatedFresh,
845
+ changedFiles: generatedVerification.changed.length,
846
+ sampleChanged: generatedVerification.changed.slice(0, 8),
847
+ hiddenChanged: Math.max(0, generatedVerification.changed.length - 8),
848
+ diagnosticCodes: [...new Set(generatedVerification.errors.map((diagnostic) => diagnostic.code))],
849
+ },
839
850
  safeDevCommand,
840
851
  checkCommand: generatedCheckCommand,
841
852
  repairCommand: generatedRepairCommand,
@@ -883,7 +894,6 @@ export function runStatusCommand(workspaceRoot: string): StatusCommandResult {
883
894
  workspaceRoot,
884
895
  ok
885
896
  ? [
886
- ...(generatedState === "check-needed" ? ["forge generate --check --json"] : []),
887
897
  "forge handoff --json",
888
898
  "forge changed --json",
889
899
  "forge dev",
@@ -2469,7 +2479,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2469
2479
  return result.exitCode;
2470
2480
  }
2471
2481
  case "status": {
2472
- const result = runStatusCommand(command.workspaceRoot);
2482
+ const result = await runStatusCommand(command.workspaceRoot);
2473
2483
  if (command.json) {
2474
2484
  process.stdout.write(formatJsonResult(result.data));
2475
2485
  } else {
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.61";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.63";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;
@@ -78,6 +78,11 @@ export function classifyChangeType(file: string): ChangeType {
78
78
  const normalized = file.replace(/\\/g, "/");
79
79
  const lower = normalized.toLowerCase();
80
80
  const basename = lower.split("/").pop() ?? lower;
81
+ const isEnvExample =
82
+ basename === ".env.example" ||
83
+ (basename.startsWith(".env.") && basename.endsWith(".example"));
84
+ const isLocalEnvFile =
85
+ (basename === ".env" || basename.startsWith(".env.")) && !isEnvExample;
81
86
 
82
87
  if (
83
88
  lower.startsWith("src/forge/_generated/") ||
@@ -90,6 +95,13 @@ export function classifyChangeType(file: string): ChangeType {
90
95
  if (lower === ".codex/hooks.json") {
91
96
  return "config";
92
97
  }
98
+ if (
99
+ isLocalEnvFile ||
100
+ lower === ".workos-seed-state.json" ||
101
+ lower === ".workos-fga-state.json"
102
+ ) {
103
+ return "operational";
104
+ }
93
105
  if (lower.startsWith(".playwright-cli/")) {
94
106
  return "operational";
95
107
  }
@@ -134,7 +146,7 @@ export function classifyChangeType(file: string): ChangeType {
134
146
  }
135
147
  if (
136
148
  basename === "package.json" ||
137
- basename === ".env.example" ||
149
+ isEnvExample ||
138
150
  basename === "bun.lock" ||
139
151
  basename === "package-lock.json" ||
140
152
  basename === "pnpm-lock.yaml" ||
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "next": "^16.2.9",
11
+ "next": "16.3.0-preview.8",
12
12
  "react": "^19.0.0",
13
13
  "react-dom": "^19.0.0"
14
14
  },