forgeos 0.1.0-alpha.20 → 0.1.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +29 -2
- package/README.md +1 -1
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/docs/changelog.md +43 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/types.ts +3 -0
- package/src/forge/agent-memory/bridge.ts +69 -4
- package/src/forge/agent-memory/context-pack.ts +106 -8
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +191 -2
- package/src/forge/agent-memory/types.ts +4 -1
- package/src/forge/brownfield-import/index.ts +68 -3
- package/src/forge/brownfield-import/types.ts +1 -1
- package/src/forge/cli/commands.ts +47 -0
- package/src/forge/cli/main.ts +4 -0
- package/src/forge/cli/new.ts +3 -1
- package/src/forge/cli/parse.ts +64 -11
- package/src/forge/cli/studio.ts +54 -0
- package/src/forge/cli/verify.ts +2 -0
- package/src/forge/compiler/frontend-graph/build.ts +58 -2
- package/src/forge/delta/index.ts +12 -0
- package/src/forge/delta/recorder.ts +60 -0
- package/src/forge/delta/status.ts +639 -2
- package/src/forge/delta/store.ts +204 -5
- package/src/forge/delta/timeline.ts +75 -1
- package/src/forge/version.ts +1 -1
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +33 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +17 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +3 -0
|
@@ -92,9 +92,12 @@ export interface AgentMemoryContextEvent {
|
|
|
92
92
|
|
|
93
93
|
export interface AgentMemoryContextPack {
|
|
94
94
|
ok: true;
|
|
95
|
-
scope: "current" | "entry";
|
|
95
|
+
scope: "current" | "entry" | "change" | "proof" | "handoff";
|
|
96
96
|
entry?: string;
|
|
97
|
+
change?: string;
|
|
98
|
+
proof?: string;
|
|
97
99
|
currentState: Record<string, unknown>;
|
|
100
|
+
recommendedCommands: string[];
|
|
98
101
|
agentMemory: {
|
|
99
102
|
summary: {
|
|
100
103
|
events: number;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ImportedDependencyInventory,
|
|
9
9
|
ImportedFrontendCall,
|
|
10
10
|
ImportedInventory,
|
|
11
|
+
ImportedEntryKind,
|
|
11
12
|
ImportedRiskFinding,
|
|
12
13
|
ImportedRiskReport,
|
|
13
14
|
ImportedRoute,
|
|
@@ -365,7 +366,53 @@ function collectEnv(workspaceRoot: string, files: SourceFile[]): ImportedInvento
|
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
function sourceTextForRoute(route: ImportedRoute, files: SourceFile[]): string {
|
|
368
|
-
|
|
369
|
+
const text = files.find((file) => file.relativePath === route.file)?.text ?? "";
|
|
370
|
+
if (!text) {
|
|
371
|
+
return "";
|
|
372
|
+
}
|
|
373
|
+
return scopedSourceTextForRoute(route, text) ?? text;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function scopedSourceTextForRoute(route: ImportedRoute, text: string): string | null {
|
|
377
|
+
if (route.source === "next-app-router" && route.handler) {
|
|
378
|
+
return sliceUntilNextMatch(
|
|
379
|
+
text,
|
|
380
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${escapeRegExp(route.handler)}\\b`, "u"),
|
|
381
|
+
/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/gu,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (route.source === "express") {
|
|
385
|
+
const method = route.method.toLowerCase();
|
|
386
|
+
const matcher = new RegExp(`\\b(?:app|router)\\s*\\.\\s*${escapeRegExp(method)}\\s*\\(\\s*["'\`]${escapeRegExp(route.path)}["'\`]`, "u");
|
|
387
|
+
return sliceUntilNextMatch(
|
|
388
|
+
text,
|
|
389
|
+
matcher,
|
|
390
|
+
/\b(?:app|router)\s*\.\s*(?:get|post|put|patch|delete|all)\s*\(\s*["'`][^"'`]+["'`]/giu,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (route.source === "nest") {
|
|
394
|
+
const method = route.method.charAt(0).toUpperCase() + route.method.slice(1).toLowerCase();
|
|
395
|
+
return sliceUntilNextMatch(
|
|
396
|
+
text,
|
|
397
|
+
new RegExp(`@${escapeRegExp(method)}\\s*\\(`, "u"),
|
|
398
|
+
/@(Get|Post|Put|Patch|Delete|All)\s*\(/gu,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function sliceUntilNextMatch(text: string, startPattern: RegExp, nextPattern: RegExp): string | null {
|
|
405
|
+
const start = text.search(startPattern);
|
|
406
|
+
if (start < 0) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
nextPattern.lastIndex = start + 1;
|
|
410
|
+
const next = nextPattern.exec(text);
|
|
411
|
+
return text.slice(start, next?.index ?? text.length);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function escapeRegExp(value: string): string {
|
|
415
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
369
416
|
}
|
|
370
417
|
|
|
371
418
|
function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCandidateEntry, "kind" | "confidence" | "risks" | "evidence" | "needsApproval"> {
|
|
@@ -381,9 +428,17 @@ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCan
|
|
|
381
428
|
const auth = /(auth|session|currentuser|getserversession|clerk|nextauth|requireuser|requireauth)/u.test(lowerText);
|
|
382
429
|
const tenant = /(tenantid|tenant_id|organizationid|orgid|accountid)/u.test(lowerText);
|
|
383
430
|
const methodUnknown = method === "ANY" || method === "ALL";
|
|
384
|
-
|
|
431
|
+
const ambiguousPostQuery = method === "POST" &&
|
|
432
|
+
/(?:^|\/)(search|query|filter|lookup|graphql)(?:$|\/)/u.test(lowerPath) &&
|
|
433
|
+
!writes &&
|
|
434
|
+
!isDestructive &&
|
|
435
|
+
!external;
|
|
436
|
+
if ((!isQuery && !ambiguousPostQuery) || writes) {
|
|
385
437
|
risks.add("writes-state");
|
|
386
438
|
}
|
|
439
|
+
if (ambiguousPostQuery) {
|
|
440
|
+
risks.add("ambiguous-post-query");
|
|
441
|
+
}
|
|
387
442
|
if (isDestructive) {
|
|
388
443
|
risks.add("destructive");
|
|
389
444
|
}
|
|
@@ -403,6 +458,15 @@ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCan
|
|
|
403
458
|
risks.add("method-unknown");
|
|
404
459
|
}
|
|
405
460
|
const commandLike = !isQuery || writes || isDestructive || external;
|
|
461
|
+
if (ambiguousPostQuery) {
|
|
462
|
+
return {
|
|
463
|
+
kind: "command-candidate",
|
|
464
|
+
confidence: 0.55,
|
|
465
|
+
risks: Array.from(risks).sort(),
|
|
466
|
+
evidence,
|
|
467
|
+
needsApproval: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
406
470
|
return {
|
|
407
471
|
kind: commandLike ? "command" : "query",
|
|
408
472
|
confidence: commandLike ? (isDestructive ? 0.9 : 0.78) : 0.86,
|
|
@@ -412,7 +476,7 @@ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCan
|
|
|
412
476
|
};
|
|
413
477
|
}
|
|
414
478
|
|
|
415
|
-
function nameForCandidate(route: ImportedRoute, kind:
|
|
479
|
+
function nameForCandidate(route: ImportedRoute, kind: ImportedEntryKind): string {
|
|
416
480
|
const nouns = route.path
|
|
417
481
|
.replace(/^\/api\//u, "")
|
|
418
482
|
.replace(/:\w+\*?/gu, "byId")
|
|
@@ -424,6 +488,7 @@ function nameForCandidate(route: ImportedRoute, kind: "command" | "query" | "unk
|
|
|
424
488
|
const method = route.method.toUpperCase();
|
|
425
489
|
const action =
|
|
426
490
|
kind === "query" ? "read" :
|
|
491
|
+
kind === "command-candidate" ? "candidate" :
|
|
427
492
|
method === "POST" ? "create" :
|
|
428
493
|
method === "PUT" || method === "PATCH" ? "update" :
|
|
429
494
|
method === "DELETE" ? "delete" :
|
|
@@ -11,7 +11,7 @@ export interface BrownfieldImportCommandOptions {
|
|
|
11
11
|
|
|
12
12
|
export type ImportedAssurance = "static-scan";
|
|
13
13
|
export type ImportedReviewStatus = "needs-review" | "approved" | "rejected";
|
|
14
|
-
export type ImportedEntryKind = "command" | "query" | "unknown";
|
|
14
|
+
export type ImportedEntryKind = "command" | "command-candidate" | "query" | "unknown";
|
|
15
15
|
export type ImportedRouteSource = "next-app-router" | "next-pages-api" | "express" | "nest" | "unknown";
|
|
16
16
|
|
|
17
17
|
export interface ImportedDependencyInventory {
|
|
@@ -128,6 +128,14 @@ import {
|
|
|
128
128
|
import {
|
|
129
129
|
formatDeltaExplainHuman,
|
|
130
130
|
formatDeltaExplainJson,
|
|
131
|
+
formatDeltaCompactHuman,
|
|
132
|
+
formatDeltaCompactJson,
|
|
133
|
+
formatDeltaDoctorHuman,
|
|
134
|
+
formatDeltaDoctorJson,
|
|
135
|
+
formatDeltaExportHuman,
|
|
136
|
+
formatDeltaExportJson,
|
|
137
|
+
formatDeltaPruneHuman,
|
|
138
|
+
formatDeltaPruneJson,
|
|
131
139
|
formatDeltaRepairHuman,
|
|
132
140
|
formatDeltaRepairJson,
|
|
133
141
|
formatDeltaStatusHuman,
|
|
@@ -137,6 +145,10 @@ import {
|
|
|
137
145
|
formatDeltaTimelineHuman,
|
|
138
146
|
formatDeltaTimelineJson,
|
|
139
147
|
runDeltaExplain,
|
|
148
|
+
runDeltaCompact,
|
|
149
|
+
runDeltaDoctor,
|
|
150
|
+
runDeltaExport,
|
|
151
|
+
runDeltaPrune,
|
|
140
152
|
runDeltaRepair,
|
|
141
153
|
runDeltaSessionCommand,
|
|
142
154
|
runDeltaStatus,
|
|
@@ -1641,6 +1653,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1641
1653
|
}
|
|
1642
1654
|
return result.exitCode;
|
|
1643
1655
|
}
|
|
1656
|
+
if (command.target === "delta") {
|
|
1657
|
+
const result = await runDeltaDoctor(command.workspaceRoot);
|
|
1658
|
+
process.stdout.write(command.json ? formatDeltaDoctorJson(result) : formatDeltaDoctorHuman(result));
|
|
1659
|
+
return result.exitCode;
|
|
1660
|
+
}
|
|
1644
1661
|
const result = await runDoctorCommand({ workspaceRoot: command.workspaceRoot });
|
|
1645
1662
|
if (command.json) {
|
|
1646
1663
|
process.stdout.write(formatDoctorJson(result));
|
|
@@ -1808,6 +1825,34 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1808
1825
|
return result.exitCode;
|
|
1809
1826
|
}
|
|
1810
1827
|
case "delta": {
|
|
1828
|
+
if (command.subcommand === "compact") {
|
|
1829
|
+
const result = await runDeltaCompact({
|
|
1830
|
+
workspaceRoot: command.workspaceRoot,
|
|
1831
|
+
dryRun: command.dryRun,
|
|
1832
|
+
});
|
|
1833
|
+
process.stdout.write(command.json ? formatDeltaCompactJson(result) : formatDeltaCompactHuman(result));
|
|
1834
|
+
return result.exitCode;
|
|
1835
|
+
}
|
|
1836
|
+
if (command.subcommand === "prune") {
|
|
1837
|
+
const result = await runDeltaPrune({
|
|
1838
|
+
workspaceRoot: command.workspaceRoot,
|
|
1839
|
+
olderThan: command.olderThan,
|
|
1840
|
+
dryRun: command.dryRun,
|
|
1841
|
+
yes: command.yes,
|
|
1842
|
+
});
|
|
1843
|
+
process.stdout.write(command.json ? formatDeltaPruneJson(result) : formatDeltaPruneHuman(result));
|
|
1844
|
+
return result.exitCode;
|
|
1845
|
+
}
|
|
1846
|
+
if (command.subcommand === "export") {
|
|
1847
|
+
const result = await runDeltaExport({
|
|
1848
|
+
workspaceRoot: command.workspaceRoot,
|
|
1849
|
+
redacted: command.redacted,
|
|
1850
|
+
output: command.output,
|
|
1851
|
+
limit: command.limit,
|
|
1852
|
+
});
|
|
1853
|
+
process.stdout.write(command.json ? formatDeltaExportJson(result) : formatDeltaExportHuman(result));
|
|
1854
|
+
return result.exitCode;
|
|
1855
|
+
}
|
|
1811
1856
|
if (command.subcommand === "repair") {
|
|
1812
1857
|
const result = await runDeltaRepair({
|
|
1813
1858
|
workspaceRoot: command.workspaceRoot,
|
|
@@ -1842,6 +1887,8 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1842
1887
|
session: command.sessionId,
|
|
1843
1888
|
limit: command.limit,
|
|
1844
1889
|
rebuild: command.rebuild,
|
|
1890
|
+
causal: command.causal,
|
|
1891
|
+
staleProofs: command.staleProofs,
|
|
1845
1892
|
});
|
|
1846
1893
|
process.stdout.write(command.json ? formatDeltaTimelineJson(result) : formatDeltaTimelineHuman(result));
|
|
1847
1894
|
return result.exitCode;
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -18,6 +18,7 @@ function formatHelp(): string {
|
|
|
18
18
|
" forge handoff --json Compact work handoff for the next external code agent",
|
|
19
19
|
" forge agent onboard --target codex --json Prepare adapter, hooks, memory, and dev snapshot",
|
|
20
20
|
" forge doctor agent --target codex --json Check adapter, hooks, and Agent Memory readiness",
|
|
21
|
+
" forge doctor delta --json Check DeltaDB writability, queue drain, redaction, and gitignore posture",
|
|
21
22
|
" forge agent ingest codex --watch --file .forge/agent/events.ndjson --json",
|
|
22
23
|
" forge docs check --json Check public docs, ReadTheDocs config, links, and local MkDocs tooling",
|
|
23
24
|
" forge docs check --build --install-venv --json Build docs strictly in a local RTD-style venv",
|
|
@@ -25,6 +26,9 @@ function formatHelp(): string {
|
|
|
25
26
|
" forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
|
|
26
27
|
" forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
|
|
27
28
|
" forge delta status --verbose --json Include Delta schema, lock, and aggregate count details",
|
|
29
|
+
" forge delta compact --json Compact redacted local agent queue history",
|
|
30
|
+
" forge delta prune --older-than 30d --dry-run --json Plan local Delta operational retention",
|
|
31
|
+
" forge delta export --redacted --json Export redacted Delta status, timeline, and agent memory",
|
|
28
32
|
" forge studio open <app-path> --preview-port 5174 --target codex --json",
|
|
29
33
|
" forge studio snapshot <app-path> --preview-port 5174 --target codex --probe-codex-server --json",
|
|
30
34
|
" forge studio bridge <app-path> --preview-port 5174 --target codex --studio-url http://127.0.0.1:3765 --probe-codex-server --json",
|
package/src/forge/cli/new.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { run as runGenerate } from "../compiler/orchestrator/run.ts";
|
|
|
8
8
|
import { resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
|
|
9
9
|
import { moduleDir } from "../platform/module.ts";
|
|
10
10
|
|
|
11
|
-
export type NewTemplateName = "agent-workroom" | "b2b-support-web" | "minimal-web";
|
|
11
|
+
export type NewTemplateName = "agent-workroom" | "b2b-support-web" | "minimal-web" | "nuxt-web";
|
|
12
12
|
export type NewPackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
13
13
|
|
|
14
14
|
export interface NewCommandOptions {
|
|
@@ -46,6 +46,8 @@ const REQUIRED_GITIGNORE_PATHS = [
|
|
|
46
46
|
".forge/cache/",
|
|
47
47
|
".forge/pglite/",
|
|
48
48
|
".forge/delta/",
|
|
49
|
+
".forge/agent/*.ndjson",
|
|
50
|
+
".forge/agent/*.history",
|
|
49
51
|
".forge/local/",
|
|
50
52
|
".forge/test-cache/",
|
|
51
53
|
".forge/test-runs/",
|
package/src/forge/cli/parse.ts
CHANGED
|
@@ -112,7 +112,7 @@ export type ForgeCommand =
|
|
|
112
112
|
json: boolean;
|
|
113
113
|
workspaceRoot: string;
|
|
114
114
|
}
|
|
115
|
-
| { kind: "doctor"; target?: "project" | "windows" | "agent"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
|
|
115
|
+
| { kind: "doctor"; target?: "project" | "windows" | "agent" | "delta"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
|
|
116
116
|
| { kind: "setup"; target: "windows"; json: boolean; yes: boolean; workspaceRoot: string }
|
|
117
117
|
| {
|
|
118
118
|
kind: "security";
|
|
@@ -183,7 +183,19 @@ export type ForgeCommand =
|
|
|
183
183
|
| { kind: "ui"; options: UiCommandOptions }
|
|
184
184
|
| { kind: "manifest"; subcommand: "validate" | "import"; path: string; json: boolean; workspaceRoot: string }
|
|
185
185
|
| { kind: "import"; options: BrownfieldImportCommandOptions }
|
|
186
|
-
| {
|
|
186
|
+
| {
|
|
187
|
+
kind: "delta";
|
|
188
|
+
subcommand: "status" | "repair" | "compact" | "prune" | "export";
|
|
189
|
+
json: boolean;
|
|
190
|
+
workspaceRoot: string;
|
|
191
|
+
dryRun: boolean;
|
|
192
|
+
yes: boolean;
|
|
193
|
+
verbose: boolean;
|
|
194
|
+
olderThan?: string;
|
|
195
|
+
output?: string;
|
|
196
|
+
limit?: number;
|
|
197
|
+
redacted: boolean;
|
|
198
|
+
}
|
|
187
199
|
| { kind: "status"; json: boolean; workspaceRoot: string }
|
|
188
200
|
| { kind: "changed"; json: boolean; authoredOnly: boolean; workspaceRoot: string }
|
|
189
201
|
| { kind: "diff"; target: "authored" | "generated" | "full"; json: boolean; workspaceRoot: string }
|
|
@@ -212,7 +224,7 @@ export type ForgeCommand =
|
|
|
212
224
|
force: boolean;
|
|
213
225
|
workspaceRoot: string;
|
|
214
226
|
}
|
|
215
|
-
| { kind: "timeline"; target?: string; kindFilter?: string; sessionId?: string; limit?: number; json: boolean; rebuild: boolean; forAgent: boolean; workspaceRoot: string }
|
|
227
|
+
| { kind: "timeline"; target?: string; kindFilter?: string; sessionId?: string; limit?: number; json: boolean; rebuild: boolean; forAgent: boolean; causal: boolean; staleProofs: boolean; workspaceRoot: string }
|
|
216
228
|
| { kind: "explain"; thing: string; json: boolean; workspaceRoot: string }
|
|
217
229
|
| {
|
|
218
230
|
kind: "session";
|
|
@@ -480,7 +492,7 @@ export const INSPECT_TARGETS: InspectTarget[] = [
|
|
|
480
492
|
"map",
|
|
481
493
|
];
|
|
482
494
|
|
|
483
|
-
const NEW_TEMPLATES: NewTemplateName[] = ["agent-workroom", "b2b-support-web", "minimal-web"];
|
|
495
|
+
const NEW_TEMPLATES: NewTemplateName[] = ["agent-workroom", "b2b-support-web", "minimal-web", "nuxt-web"];
|
|
484
496
|
const NEW_PACKAGE_MANAGERS: NewPackageManager[] = ["bun", "npm", "pnpm", "yarn"];
|
|
485
497
|
const SELF_HOST_SUBCOMMANDS: SelfHostSubcommand[] = ["compose", "env", "check", "clean"];
|
|
486
498
|
const AGENT_CONTRACT_SUBCOMMANDS: AgentContractSubcommand[] = [
|
|
@@ -1015,6 +1027,22 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1015
1027
|
(subcommand === "timeline" ? rest[1] : undefined) ??
|
|
1016
1028
|
(subcommand === "timeline" ? "all" : undefined) ??
|
|
1017
1029
|
(subcommand === "hooks" || subcommand === "onboard" ? "codex" : "generic");
|
|
1030
|
+
const contextOptionValues = new Set(
|
|
1031
|
+
[
|
|
1032
|
+
parseOptionValue(argv, "--entry"),
|
|
1033
|
+
parseOptionValue(argv, "--change"),
|
|
1034
|
+
parseOptionValue(argv, "--proof"),
|
|
1035
|
+
parseOptionValue(argv, "--event"),
|
|
1036
|
+
parseOptionValue(argv, "--input"),
|
|
1037
|
+
parseOptionValue(argv, "--target"),
|
|
1038
|
+
parseOptionValue(argv, "--file"),
|
|
1039
|
+
limitRaw,
|
|
1040
|
+
pollIntervalRaw,
|
|
1041
|
+
].filter((value): value is string => typeof value === "string"),
|
|
1042
|
+
);
|
|
1043
|
+
const contextEntry = subcommand === "context"
|
|
1044
|
+
? rest.slice(1).find((part) => !part.startsWith("--") && !contextOptionValues.has(part))
|
|
1045
|
+
: undefined;
|
|
1018
1046
|
return {
|
|
1019
1047
|
command: {
|
|
1020
1048
|
kind: "agent",
|
|
@@ -1031,7 +1059,10 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1031
1059
|
eventName: parseOptionValue(argv, "--event"),
|
|
1032
1060
|
hookAction: subcommand === "hooks" ? rest[1] : undefined,
|
|
1033
1061
|
input,
|
|
1034
|
-
entry: parseOptionValue(argv, "--entry") ??
|
|
1062
|
+
entry: parseOptionValue(argv, "--entry") ?? contextEntry,
|
|
1063
|
+
change: parseOptionValue(argv, "--change"),
|
|
1064
|
+
proof: parseOptionValue(argv, "--proof"),
|
|
1065
|
+
handoff: parseFlag(argv, "--handoff"),
|
|
1035
1066
|
current: parseFlag(argv, "--current"),
|
|
1036
1067
|
limit: limit ? Math.floor(limit) : undefined,
|
|
1037
1068
|
watch: parseFlag(argv, "--watch"),
|
|
@@ -1152,14 +1183,14 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1152
1183
|
};
|
|
1153
1184
|
}
|
|
1154
1185
|
case "doctor":
|
|
1155
|
-
if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent") {
|
|
1156
|
-
errors.push("forge doctor supports subcommand: windows or
|
|
1186
|
+
if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent" && rest[0] !== "delta") {
|
|
1187
|
+
errors.push("forge doctor supports subcommand: windows, agent, or delta");
|
|
1157
1188
|
return { command: null, workspaceRoot, errors };
|
|
1158
1189
|
}
|
|
1159
1190
|
return {
|
|
1160
1191
|
command: {
|
|
1161
1192
|
kind: "doctor",
|
|
1162
|
-
target: rest[0] === "windows" ? "windows" : rest[0] === "agent" ? "agent" : "project",
|
|
1193
|
+
target: rest[0] === "windows" ? "windows" : rest[0] === "agent" ? "agent" : rest[0] === "delta" ? "delta" : "project",
|
|
1163
1194
|
agentTarget: rest[0] === "agent"
|
|
1164
1195
|
? (parseOptionValue(argv, "--target") as AgentAdapterTarget | undefined) ?? (rest[1] as AgentAdapterTarget | undefined) ?? "codex"
|
|
1165
1196
|
: undefined,
|
|
@@ -1882,10 +1913,12 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1882
1913
|
}
|
|
1883
1914
|
case "delta": {
|
|
1884
1915
|
const subcommand = rest[0];
|
|
1885
|
-
if (subcommand !== "status" && subcommand !== "repair") {
|
|
1886
|
-
errors.push("forge delta requires subcommand: status or
|
|
1916
|
+
if (subcommand !== "status" && subcommand !== "repair" && subcommand !== "compact" && subcommand !== "prune" && subcommand !== "export") {
|
|
1917
|
+
errors.push("forge delta requires subcommand: status, repair, compact, prune, or export");
|
|
1887
1918
|
return { command: null, workspaceRoot, errors };
|
|
1888
1919
|
}
|
|
1920
|
+
const limitRaw = parseOptionValue(argv, "--limit");
|
|
1921
|
+
const limit = limitRaw ? Number(limitRaw) : undefined;
|
|
1889
1922
|
return {
|
|
1890
1923
|
command: {
|
|
1891
1924
|
kind: "delta",
|
|
@@ -1894,6 +1927,10 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1894
1927
|
dryRun: parseFlag(argv, "--dry-run"),
|
|
1895
1928
|
yes: parseFlag(argv, "--yes"),
|
|
1896
1929
|
verbose: parseFlag(argv, "--verbose"),
|
|
1930
|
+
olderThan: parseOptionValue(argv, "--older-than"),
|
|
1931
|
+
output: parseOptionValue(argv, "--output"),
|
|
1932
|
+
limit: Number.isFinite(limit) ? limit : undefined,
|
|
1933
|
+
redacted: parseFlag(argv, "--redacted"),
|
|
1897
1934
|
workspaceRoot,
|
|
1898
1935
|
},
|
|
1899
1936
|
workspaceRoot,
|
|
@@ -1933,7 +1970,10 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1933
1970
|
const kindFilter = parseOptionValue(argv, "--kind");
|
|
1934
1971
|
const sessionId = parseOptionValue(argv, "--session");
|
|
1935
1972
|
const rebuild = rest[0] === "rebuild";
|
|
1936
|
-
const
|
|
1973
|
+
const optionValues = new Set([limitRaw, kindFilter, sessionId].filter((value): value is string => typeof value === "string"));
|
|
1974
|
+
const target = rebuild
|
|
1975
|
+
? undefined
|
|
1976
|
+
: rest.find((item) => !item.startsWith("--") && !optionValues.has(item));
|
|
1937
1977
|
const limit = limitRaw ? Number(limitRaw) : undefined;
|
|
1938
1978
|
if (limitRaw !== undefined && (!Number.isFinite(limit) || limit! < 1)) {
|
|
1939
1979
|
errors.push("--limit must be a number >= 1");
|
|
@@ -1948,6 +1988,8 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1948
1988
|
json: parseFlag(argv, "--json"),
|
|
1949
1989
|
rebuild,
|
|
1950
1990
|
forAgent: parseFlag(argv, "--for-agent"),
|
|
1991
|
+
causal: parseFlag(argv, "--causal"),
|
|
1992
|
+
staleProofs: parseFlag(argv, "--stale-proofs"),
|
|
1951
1993
|
workspaceRoot,
|
|
1952
1994
|
},
|
|
1953
1995
|
workspaceRoot,
|
|
@@ -2642,6 +2684,8 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
2642
2684
|
"--json",
|
|
2643
2685
|
"--human",
|
|
2644
2686
|
"--for-agent",
|
|
2687
|
+
"--causal",
|
|
2688
|
+
"--stale-proofs",
|
|
2645
2689
|
"--dry-run",
|
|
2646
2690
|
"--plan",
|
|
2647
2691
|
"--staged",
|
|
@@ -2698,6 +2742,9 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
2698
2742
|
"--emit",
|
|
2699
2743
|
"--event",
|
|
2700
2744
|
"--entry",
|
|
2745
|
+
"--change",
|
|
2746
|
+
"--proof",
|
|
2747
|
+
"--handoff",
|
|
2701
2748
|
"--current",
|
|
2702
2749
|
"--trigger",
|
|
2703
2750
|
"--component",
|
|
@@ -2794,6 +2841,8 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
2794
2841
|
"--env-file",
|
|
2795
2842
|
"--skip-startup-console",
|
|
2796
2843
|
"--redacted",
|
|
2844
|
+
"--older-than",
|
|
2845
|
+
"--output",
|
|
2797
2846
|
"--mock-ai",
|
|
2798
2847
|
"--ai",
|
|
2799
2848
|
"--provider",
|
|
@@ -2857,6 +2906,8 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
2857
2906
|
arg === "--emit" ||
|
|
2858
2907
|
arg === "--event" ||
|
|
2859
2908
|
arg === "--entry" ||
|
|
2909
|
+
arg === "--change" ||
|
|
2910
|
+
arg === "--proof" ||
|
|
2860
2911
|
arg === "--trigger" ||
|
|
2861
2912
|
arg === "--component" ||
|
|
2862
2913
|
arg === "--package" ||
|
|
@@ -2901,6 +2952,8 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
2901
2952
|
arg === "--db" ||
|
|
2902
2953
|
arg === "--database-url" ||
|
|
2903
2954
|
arg === "--limit" ||
|
|
2955
|
+
arg === "--older-than" ||
|
|
2956
|
+
arg === "--output" ||
|
|
2904
2957
|
arg === "--kind" ||
|
|
2905
2958
|
arg === "--session" ||
|
|
2906
2959
|
arg === "--input" ||
|
package/src/forge/cli/studio.ts
CHANGED
|
@@ -139,6 +139,15 @@ export interface StudioSnapshotResult {
|
|
|
139
139
|
commands: string[];
|
|
140
140
|
diffPlan?: DevConsoleDiffPlan;
|
|
141
141
|
};
|
|
142
|
+
handoff: {
|
|
143
|
+
previewUrl: string;
|
|
144
|
+
currentSession?: { id?: string; title?: string; status?: string; confidence?: number };
|
|
145
|
+
changedFiles?: number;
|
|
146
|
+
generatedState?: string;
|
|
147
|
+
deltaHealth?: string;
|
|
148
|
+
agentContextCommand: string;
|
|
149
|
+
recommendedCommands: string[];
|
|
150
|
+
};
|
|
142
151
|
proofs: {
|
|
143
152
|
preview: StudioAttachResult["preview"]["status"];
|
|
144
153
|
generated: StudioAttachResult["posture"]["generated"];
|
|
@@ -981,6 +990,13 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
981
990
|
: undefined;
|
|
982
991
|
const delta = await runDeltaStatus(appRoot);
|
|
983
992
|
const contextPacket = contextPacketFor({ appRoot, posture, commands });
|
|
993
|
+
const handoff = studioHandoffFor({
|
|
994
|
+
previewUrl: preview.url,
|
|
995
|
+
posture,
|
|
996
|
+
changed: changed.data,
|
|
997
|
+
delta,
|
|
998
|
+
commands,
|
|
999
|
+
});
|
|
984
1000
|
const gitState = (changed.data as { git?: { available?: boolean } }).git;
|
|
985
1001
|
const changedReadable = changed.ok || gitState?.available === false;
|
|
986
1002
|
const ok = posture.state !== "needs-attention" && changedReadable &&
|
|
@@ -1006,6 +1022,7 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
1006
1022
|
changed: changed.data,
|
|
1007
1023
|
commands,
|
|
1008
1024
|
contextPacket,
|
|
1025
|
+
handoff,
|
|
1009
1026
|
proofs: {
|
|
1010
1027
|
preview: preview.status,
|
|
1011
1028
|
generated: posture.generated,
|
|
@@ -1019,6 +1036,43 @@ export async function runStudioSnapshotCommand(options: StudioAttachOptions): Pr
|
|
|
1019
1036
|
};
|
|
1020
1037
|
}
|
|
1021
1038
|
|
|
1039
|
+
function studioHandoffFor(input: {
|
|
1040
|
+
previewUrl: string;
|
|
1041
|
+
posture: StudioSnapshotResult["posture"];
|
|
1042
|
+
changed: Record<string, unknown>;
|
|
1043
|
+
delta: unknown;
|
|
1044
|
+
commands: StudioSnapshotResult["commands"];
|
|
1045
|
+
}): StudioSnapshotResult["handoff"] {
|
|
1046
|
+
const changedSummary = input.changed.summary as { changedFiles?: unknown } | undefined;
|
|
1047
|
+
const deltaRecord = input.delta && typeof input.delta === "object" && !Array.isArray(input.delta)
|
|
1048
|
+
? input.delta as { workSession?: { id?: unknown; title?: unknown; status?: unknown; confidence?: unknown }; details?: { health?: { status?: unknown } } }
|
|
1049
|
+
: undefined;
|
|
1050
|
+
const workSession = deltaRecord?.workSession;
|
|
1051
|
+
return {
|
|
1052
|
+
previewUrl: input.previewUrl,
|
|
1053
|
+
...(workSession
|
|
1054
|
+
? {
|
|
1055
|
+
currentSession: {
|
|
1056
|
+
...(typeof workSession.id === "string" ? { id: workSession.id } : {}),
|
|
1057
|
+
...(typeof workSession.title === "string" ? { title: workSession.title } : {}),
|
|
1058
|
+
...(typeof workSession.status === "string" ? { status: workSession.status } : {}),
|
|
1059
|
+
...(typeof workSession.confidence === "number" ? { confidence: workSession.confidence } : {}),
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
: {}),
|
|
1063
|
+
...(typeof changedSummary?.changedFiles === "number" ? { changedFiles: changedSummary.changedFiles } : {}),
|
|
1064
|
+
generatedState: input.posture.generated?.state,
|
|
1065
|
+
deltaHealth: typeof deltaRecord?.details?.health?.status === "string" ? deltaRecord.details.health.status : undefined,
|
|
1066
|
+
agentContextCommand: "forge agent context --handoff --json",
|
|
1067
|
+
recommendedCommands: [
|
|
1068
|
+
input.commands.handoff,
|
|
1069
|
+
"forge agent context --handoff --json",
|
|
1070
|
+
input.commands.changed,
|
|
1071
|
+
input.commands.doctor,
|
|
1072
|
+
],
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1022
1076
|
export async function runStudioWatchCommand(options: StudioAttachOptions): Promise<StudioWatchResult> {
|
|
1023
1077
|
const snapshot = await runStudioSnapshotCommand(options);
|
|
1024
1078
|
const intervalMs = Math.max(1000, Math.floor(options.intervalMs ?? 5000));
|
package/src/forge/cli/verify.ts
CHANGED
|
@@ -695,6 +695,7 @@ const STRICT_TEST_FALLBACK_MS_BY_PATH: Array<{ pattern: RegExp; estimatedMs: num
|
|
|
695
695
|
{ pattern: /^tests\/templates\/new-b2b-support-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
696
696
|
{ pattern: /^tests\/templates\/new-agent-workroom\.test\.ts$/, estimatedMs: 12_000 },
|
|
697
697
|
{ pattern: /^tests\/templates\/new-minimal-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
698
|
+
{ pattern: /^tests\/templates\/new-nuxt-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
698
699
|
{ pattern: /^tests\/templates\/create-forge-app\.test\.ts$/, estimatedMs: 8_000 },
|
|
699
700
|
];
|
|
700
701
|
const STRICT_ISOLATED_TEST_PATTERNS = [
|
|
@@ -730,6 +731,7 @@ const STRICT_ISOLATED_TEST_PATTERNS = [
|
|
|
730
731
|
/^tests\/templates\/new-b2b-support-web\.test\.ts$/,
|
|
731
732
|
/^tests\/templates\/new-agent-workroom\.test\.ts$/,
|
|
732
733
|
/^tests\/templates\/new-minimal-web\.test\.ts$/,
|
|
734
|
+
/^tests\/templates\/new-nuxt-web\.test\.ts$/,
|
|
733
735
|
/^tests\/telemetry\/telemetry-dev-server\.test\.ts$/,
|
|
734
736
|
];
|
|
735
737
|
const STRICT_SERIAL_TEST_PATTERNS: RegExp[] = [];
|
|
@@ -229,6 +229,58 @@ function detectRouteUses(file: string, text: string, clientManifest: ClientManif
|
|
|
229
229
|
return uses;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function localComposableNames(file: string, text: string): string[] {
|
|
233
|
+
const relName = componentNameForFile(file);
|
|
234
|
+
const names = [relName.startsWith("use") ? relName : `use${relName.slice(0, 1).toUpperCase()}${relName.slice(1)}`];
|
|
235
|
+
for (const match of text.matchAll(/export\s+function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g)) {
|
|
236
|
+
if (match[1]) {
|
|
237
|
+
names.push(match[1]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return uniqueSorted(names);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildComposableUseIndex(
|
|
244
|
+
webRoot: string,
|
|
245
|
+
sourceFiles: string[],
|
|
246
|
+
clientManifest: ClientManifest,
|
|
247
|
+
): Map<string, ReturnType<typeof detectUses>> {
|
|
248
|
+
const index = new Map<string, ReturnType<typeof detectUses>>();
|
|
249
|
+
for (const file of sourceFiles) {
|
|
250
|
+
const rel = toPosix(relative(webRoot, file));
|
|
251
|
+
if (!rel.startsWith("composables/") && !rel.startsWith("src/composables/")) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (rel.endsWith("/forge.ts") || rel === "composables/forge.ts" || rel === "src/composables/forge.ts") {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const text = nodeFileSystem.readText(file) ?? "";
|
|
258
|
+
const uses = detectUses(text, clientManifest);
|
|
259
|
+
for (const name of localComposableNames(file, text)) {
|
|
260
|
+
index.set(name, uses);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return index;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function detectLocalComposableUses(
|
|
267
|
+
text: string,
|
|
268
|
+
composableUseIndex: Map<string, ReturnType<typeof detectUses>>,
|
|
269
|
+
): ReturnType<typeof detectUses> {
|
|
270
|
+
let uses: ReturnType<typeof detectUses> = {
|
|
271
|
+
usesCommands: [],
|
|
272
|
+
usesQueries: [],
|
|
273
|
+
usesLiveQueries: [],
|
|
274
|
+
rawForgeFetches: [],
|
|
275
|
+
};
|
|
276
|
+
for (const [name, composableUses] of composableUseIndex) {
|
|
277
|
+
if (new RegExp(`\\b${name}\\s*\\(`).test(text)) {
|
|
278
|
+
uses = mergeUses(uses, composableUses);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return uses;
|
|
282
|
+
}
|
|
283
|
+
|
|
232
284
|
function devCommandFor(webRoot: string, framework: FrontendGraph["framework"]): string {
|
|
233
285
|
const pkg = readJson<{ scripts?: Record<string, string> }>(join(webRoot, "package.json"));
|
|
234
286
|
if (pkg?.scripts?.dev) {
|
|
@@ -347,6 +399,7 @@ export function buildFrontendGraph(input: {
|
|
|
347
399
|
const bridgeFiles: string[] = [];
|
|
348
400
|
const diagnostics: FrontendGraph["diagnostics"] = [];
|
|
349
401
|
const textByRel = new Map<string, string>();
|
|
402
|
+
const composableUseIndex = buildComposableUseIndex(webRoot, sourceFiles, input.clientManifest);
|
|
350
403
|
|
|
351
404
|
for (const file of sourceFiles) {
|
|
352
405
|
const rel = toPosix(relative(input.workspaceRoot, file));
|
|
@@ -359,7 +412,10 @@ export function buildFrontendGraph(input: {
|
|
|
359
412
|
rel === "web/composables/forge.ts" ||
|
|
360
413
|
rel === "web/plugins/forge.ts";
|
|
361
414
|
textByRel.set(rel, text);
|
|
362
|
-
const uses =
|
|
415
|
+
const uses = mergeUses(
|
|
416
|
+
detectUses(text, input.clientManifest),
|
|
417
|
+
detectLocalComposableUses(text, composableUseIndex),
|
|
418
|
+
);
|
|
363
419
|
if (isComponentFile(webRoot, file, text)) {
|
|
364
420
|
components.push({ name: componentNameForText(file, text), file: rel, ...uses });
|
|
365
421
|
}
|
|
@@ -469,7 +525,7 @@ export function buildFrontendGraph(input: {
|
|
|
469
525
|
? "Nuxt app does not expose a Forge plugin; generated composables may not be wired"
|
|
470
526
|
: "web app does not expose a ForgeProvider; generated hooks may not be wired",
|
|
471
527
|
fixHint: framework === "nuxt"
|
|
472
|
-
? "Create web/plugins/forge.ts and
|
|
528
|
+
? "Create web/plugins/forge.client.ts and web/plugins/forge.server.ts and install ForgeVuePlugin with runtimeConfig.public.forgeUrl and devAuth for local development."
|
|
473
529
|
: "Mount ForgeProvider once in the web app root/provider layer and pass devAuth for local development.",
|
|
474
530
|
suggestedCommands: ["forge inspect frontend --json", `forge make ui --framework ${framework === "nuxt" ? "nuxt" : "vite"} --dry-run --json`],
|
|
475
531
|
docs: ["src/forge/_generated/frontendGraph.json", "AGENTS.md"],
|
package/src/forge/delta/index.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
export {
|
|
2
2
|
runDeltaStatus,
|
|
3
3
|
runDeltaRepair,
|
|
4
|
+
runDeltaCompact,
|
|
5
|
+
runDeltaPrune,
|
|
6
|
+
runDeltaExport,
|
|
7
|
+
runDeltaDoctor,
|
|
4
8
|
formatDeltaStatusHuman,
|
|
5
9
|
formatDeltaStatusJson,
|
|
6
10
|
formatDeltaRepairHuman,
|
|
7
11
|
formatDeltaRepairJson,
|
|
12
|
+
formatDeltaDoctorHuman,
|
|
13
|
+
formatDeltaDoctorJson,
|
|
14
|
+
formatDeltaCompactHuman,
|
|
15
|
+
formatDeltaCompactJson,
|
|
16
|
+
formatDeltaPruneHuman,
|
|
17
|
+
formatDeltaPruneJson,
|
|
18
|
+
formatDeltaExportHuman,
|
|
19
|
+
formatDeltaExportJson,
|
|
8
20
|
} from "./status.ts";
|
|
9
21
|
export { runDeltaTimeline, formatDeltaTimelineHuman, formatDeltaTimelineJson } from "./timeline.ts";
|
|
10
22
|
export { runDeltaExplain, formatDeltaExplainHuman, formatDeltaExplainJson } from "./explain.ts";
|