forgeos 0.1.0-alpha.22 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.22 input=d8837238db9cf8868eceae3e3cc5a7d9bb46a0955c55f0143caa802597d5a3a5 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.23 input=43fbd897d0f0a003e2f63e7048eb3281a5871034827653640e010161c0385d44 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.0-alpha.23
6
+
7
+ ### Patch Changes
8
+
9
+ - Tighten the post-alpha.22 release surface and package evidence.
10
+
11
+ - Add a dedicated Nuxt template smoke workflow that installs `nuxt-web`, runs Forge generation/checks, runs Nuxt typecheck, and probes `forge dev --once`.
12
+ - Include `nuxt-web` in the default field-test template matrix.
13
+ - Add explicit `scopeTarget` metadata and human-readable target output for `forge agent context --change`, `--proof`, and `--handoff`.
14
+ - Teach `forge explain` to fall back to the current generated agent contract when DeltaDB has no runtime history, while marking the result as contract-defined instead of executed.
15
+ - Downgrade read-only observation commands such as `forge status`, `forge changed`, `forge handoff`, `forge explain`, `forge timeline`, and CAIR queries to low-confidence context-gathering sessions in DeltaDB.
16
+ - Package `docs/cair-protocol.md` in the npm tarball and expand the public security/threat-model docs for DeltaDB, agent memory, CAIR, Studio bridge, brownfield import, and Nuxt surfaces.
17
+
5
18
  ## 0.1.0-alpha.22
6
19
 
7
20
  ### Patch Changes
@@ -0,0 +1,103 @@
1
+ # CAIR Protocol
2
+
3
+ CAIR is ForgeOS' compact agent interface for reading code structure, querying symbols, and planning safe edits without loading the whole repository into an agent context window.
4
+
5
+ CAIR is intentionally CLI-first. It does not add a second mutating tool surface. Agents should use CAIR to inspect structure and create reviewable plans, then apply those plans through the ForgeOS CLI with hash checks and rollback journals.
6
+
7
+ ## Read Workflow
8
+
9
+ Start with a compact snapshot:
10
+
11
+ ```bash
12
+ forge cair snapshot
13
+ forge cair query "Q STATUS"
14
+ forge cair query "Q ST"
15
+ ```
16
+
17
+ Use symbol, definition, reference, impact, and dependency API queries before opening large files:
18
+
19
+ ```bash
20
+ forge cair query "Q S name=createTicket"
21
+ forge cair query "Q D S#1"
22
+ forge cair query "Q R S#1"
23
+ forge cair query "Q I S#1"
24
+ forge cair query "Q DEP.API package=zod symbol=object"
25
+ ```
26
+
27
+ The goal is to make agent navigation evidence-backed: CAIR gives stable ids for modules, symbols, packages, APIs, and tests so an agent can select a small file set instead of scanning the whole project.
28
+
29
+ ## Action Safety
30
+
31
+ Mutating CAIR actions should be planned first:
32
+
33
+ ```bash
34
+ forge cair action --plan "A RN t=S#1 nn=openTicket"
35
+ ```
36
+
37
+ The plan lives under `.forge/cair/plans/` and records the target files and expected hashes. Applying a plan checks those hashes before editing:
38
+
39
+ ```bash
40
+ forge cair action "A APPLY plan=<P#|.forge/cair/plans/...json>"
41
+ ```
42
+
43
+ Applied plans write rollback journals under `.forge/cair/journal/`:
44
+
45
+ ```bash
46
+ forge cair action "A ROLLBACK journal=.forge/cair/journal/<journal>.json"
47
+ ```
48
+
49
+ Use `--dry-run` when exploring an action shape without creating a plan:
50
+
51
+ ```bash
52
+ forge cair action --dry-run "A CREATE.SYMBOL path=src/example.ts kind=function name=example export=true createFile=true"
53
+ ```
54
+
55
+ Generated files stay protected by default. Use `--include-generated` only when the edit is intentionally about generated artifacts.
56
+
57
+ ## DeltaDB Evidence
58
+
59
+ Successful CAIR CLI runs are recorded in DeltaDB as sanitized operational events:
60
+
61
+ | CAIR command | Delta event |
62
+ |--------------|-------------|
63
+ | `forge cair snapshot` | `cair.snapshot.created` |
64
+ | `forge cair query ...` | `cair.query.run` |
65
+ | `forge cair action --plan ...` | `cair.plan.created` |
66
+ | `forge cair action "A APPLY ..."` | `cair.plan.applied` |
67
+ | `forge cair action --dry-run ...` | `cair.action.previewed` |
68
+
69
+ The recorder stores compact verbs such as `Q ST` or `A APPLY`; it does not need to store full action bodies as first-class timeline data. Use:
70
+
71
+ ```bash
72
+ forge timeline cair:protocol --json
73
+ forge timeline --kind cair.plan.applied --json
74
+ ```
75
+
76
+ This makes CAIR navigation and guarded edits visible beside file changes, proofs, and agent activity.
77
+
78
+ ## Compact Aliases
79
+
80
+ | Long form | Compact |
81
+ |-----------|---------|
82
+ | `Q STATUS` | `Q ST` |
83
+ | `Q SYMBOL` | `Q S` |
84
+ | `Q DEF` | `Q D` |
85
+ | `Q REFS` | `Q R` |
86
+ | `Q IMPACT` | `Q I` |
87
+ | `A RENAME.SYMBOL target=S#1 newName=x` | `A RN t=S#1 nn=x` |
88
+
89
+ ## Agent Posture
90
+
91
+ Use this loop for code work:
92
+
93
+ ```bash
94
+ forge cair snapshot
95
+ forge cair query "Q ST"
96
+ forge cair query "Q S name=<symbol>"
97
+ forge cair query "Q I S#1"
98
+ forge cair action --plan "A RN t=S#1 nn=<newName>"
99
+ forge cair action "A APPLY plan=<P#|path>"
100
+ forge check --json
101
+ ```
102
+
103
+ Do not use CAIR as a bypass around ForgeOS rules. Commands remain transactional writes, queries and liveQueries remain read-only, side effects still belong in actions and workflows, and generated artifacts remain derived.
package/docs/changelog.md CHANGED
@@ -6,6 +6,23 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.23
10
+
11
+ - Tightened the post-alpha.22 release surface and package evidence:
12
+ added a dedicated Nuxt template smoke workflow, included `nuxt-web` in the
13
+ default field-test template matrix, packaged `docs/cair-protocol.md`, and
14
+ expanded the security/threat-model docs for DeltaDB, agent memory, CAIR,
15
+ Studio bridge, brownfield import, and Nuxt surfaces.
16
+ - `forge agent context` now returns explicit `scopeTarget` metadata and prints
17
+ the resolved context target for entry, change, proof, and handoff packs.
18
+ - `forge explain` now falls back to the current generated agent contract when
19
+ DeltaDB has no runtime history, marking the entry as contract-defined rather
20
+ than executed.
21
+ - DeltaDB work-session inference now treats read-only observation commands such
22
+ as `forge status`, `forge changed`, `forge handoff`, `forge explain`,
23
+ `forge timeline`, and CAIR queries as low-confidence context-gathering
24
+ sessions.
25
+
9
26
  ## 0.1.0-alpha.22
10
27
 
11
28
  - Added focused post-alpha.21 workflow improvements without expanding MCP tools:
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.23",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "adapters/",
8
8
  "bin/",
9
+ "docs/cair-protocol.md",
9
10
  "docs/forge-protocol.md",
10
11
  "examples/go-billing/",
11
12
  "examples/java-billing/",
@@ -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.22","releaseId":"forgeos@0.1.0-alpha.22+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.23","releaseId":"forgeos@0.1.0-alpha.23+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.22 input=d8837238db9cf8868eceae3e3cc5a7d9bb46a0955c55f0143caa802597d5a3a5 content=bb05ee3635ddeea2d84168f8bf728e07f82e896061e8494b17f56dea26000770
1
+ // @forge-generated generator=0.1.0-alpha.23 input=43fbd897d0f0a003e2f63e7048eb3281a5871034827653640e010161c0385d44 content=8b537f90580f28c1630850bc7b7199e17d0bc62d05e9565fdf84da4bdbf9f6dc
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.22",
23
- "releaseId": "forgeos@0.1.0-alpha.22+unknown",
22
+ "packageVersion": "0.1.0-alpha.23",
23
+ "releaseId": "forgeos@0.1.0-alpha.23+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -1021,6 +1021,7 @@ function formatAgentMemoryContextHuman(result: AgentMemoryContextPack): string {
1021
1021
  const lines = [
1022
1022
  `Forge Agent Context (${result.scope}${result.entry ? `: ${result.entry}` : ""})`,
1023
1023
  "",
1024
+ `target: ${formatAgentContextTarget(result)}`,
1024
1025
  `events: ${summary.events}`,
1025
1026
  `sources: ${summary.sources.length > 0 ? summary.sources.join(", ") : "none"}`,
1026
1027
  `tools: ${summary.tools.length > 0 ? summary.tools.join(", ") : "none"}`,
@@ -1067,6 +1068,21 @@ function formatAgentMemoryContextHuman(result: AgentMemoryContextPack): string {
1067
1068
  return `${lines.join("\n")}\n`;
1068
1069
  }
1069
1070
 
1071
+ function formatAgentContextTarget(result: AgentMemoryContextPack): string {
1072
+ const target = result.scopeTarget;
1073
+ const parts: string[] = [target.kind];
1074
+ if (target.value) {
1075
+ parts.push(target.value);
1076
+ }
1077
+ if (target.semanticTarget && target.semanticTarget !== target.value) {
1078
+ parts.push(`semantic=${target.semanticTarget}`);
1079
+ }
1080
+ if (target.currentSessionId) {
1081
+ parts.push(`session=${target.currentSessionId}`);
1082
+ }
1083
+ return parts.join(" ");
1084
+ }
1085
+
1070
1086
  function formatAgentMemoryEventsHuman(events: AgentMemoryEventRecord[]): string {
1071
1087
  const sources = uniqueStrings(events.map((event) => event.sourceName));
1072
1088
  const tools = uniqueStrings(events.flatMap((event) => {
@@ -48,6 +48,7 @@ export async function buildAgentMemoryContext(input: {
48
48
  return {
49
49
  ok: true,
50
50
  scope,
51
+ scopeTarget: contextScopeTarget(input, scope, target, currentSession?.id),
51
52
  entry: input.entry,
52
53
  change: input.change,
53
54
  proof: input.proof,
@@ -117,6 +118,33 @@ function contextTarget(
117
118
  return undefined;
118
119
  }
119
120
 
121
+ function contextScopeTarget(
122
+ input: { entry?: string; change?: string; proof?: string; handoff?: boolean },
123
+ scope: AgentMemoryContextPack["scope"],
124
+ semanticTarget: string | undefined,
125
+ currentSessionId: string | undefined,
126
+ ): AgentMemoryContextPack["scopeTarget"] {
127
+ if (scope === "entry") {
128
+ return { kind: "entry", value: input.entry, semanticTarget };
129
+ }
130
+ if (scope === "proof") {
131
+ return { kind: "proof", value: input.proof, semanticTarget };
132
+ }
133
+ if (scope === "change") {
134
+ const value = input.change ?? "current";
135
+ return {
136
+ kind: "change",
137
+ value,
138
+ ...(semanticTarget ? { semanticTarget } : {}),
139
+ ...(value === "current" && currentSessionId ? { currentSessionId } : {}),
140
+ };
141
+ }
142
+ if (scope === "handoff") {
143
+ return { kind: "handoff", value: "handoff", currentSessionId };
144
+ }
145
+ return { kind: "current-session", value: "current", currentSessionId };
146
+ }
147
+
120
148
  function eventTarget(
121
149
  input: { entry?: string; change?: string; proof?: string },
122
150
  scope: AgentMemoryContextPack["scope"],
@@ -93,6 +93,12 @@ export interface AgentMemoryContextEvent {
93
93
  export interface AgentMemoryContextPack {
94
94
  ok: true;
95
95
  scope: "current" | "entry" | "change" | "proof" | "handoff";
96
+ scopeTarget: {
97
+ kind: "current-session" | "entry" | "change" | "proof" | "handoff";
98
+ value?: string;
99
+ semanticTarget?: string;
100
+ currentSessionId?: string;
101
+ };
96
102
  entry?: string;
97
103
  change?: string;
98
104
  proof?: string;
@@ -1,3 +1,5 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { DeltaStore } from "./store.ts";
2
4
 
3
5
  export interface DeltaExplainResult {
@@ -13,10 +15,11 @@ export async function runDeltaExplain(input: {
13
15
  }): Promise<DeltaExplainResult> {
14
16
  const store = await DeltaStore.open(input.workspaceRoot, { access: "read" });
15
17
  try {
18
+ const explanation = await store.explain(input.thing);
16
19
  return {
17
20
  ok: true,
18
21
  thing: input.thing,
19
- explanation: await store.explain(input.thing),
22
+ explanation: enrichWithCurrentAgentContract(input.workspaceRoot, input.thing, explanation),
20
23
  exitCode: 0,
21
24
  };
22
25
  } finally {
@@ -68,6 +71,9 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
68
71
  lines.push("Runtime:");
69
72
  lines.push(` kind: ${String(runtime.entry_kind ?? "unknown")}`);
70
73
  lines.push(` result: ${String(runtime.result ?? "unknown")}`);
74
+ if (runtime.source) {
75
+ lines.push(` source: ${String(runtime.source)}`);
76
+ }
71
77
  if (runtime.diagnostic_code) {
72
78
  lines.push(` diagnostic: ${String(runtime.diagnostic_code)}`);
73
79
  }
@@ -94,6 +100,22 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
94
100
  }
95
101
  }
96
102
  }
103
+ const currentContract = explanation.currentContract as Record<string, unknown> | null | undefined;
104
+ if (currentContract) {
105
+ lines.push("");
106
+ lines.push("Current contract:");
107
+ lines.push(` kind: ${String(currentContract.kind ?? "unknown")}`);
108
+ lines.push(` name: ${String(currentContract.name ?? result.thing)}`);
109
+ if (currentContract.auth) {
110
+ lines.push(` auth: ${String(currentContract.auth)}`);
111
+ }
112
+ if (currentContract.policy) {
113
+ lines.push(` policy: ${String(currentContract.policy)}`);
114
+ }
115
+ if (currentContract.sourceFile) {
116
+ lines.push(` file: ${String(currentContract.sourceFile)}`);
117
+ }
118
+ }
97
119
  lines.push("");
98
120
  lines.push("Introduced in:");
99
121
  if (workSessions.length === 0) {
@@ -124,3 +146,93 @@ export function formatDeltaExplainHuman(result: DeltaExplainResult): string {
124
146
  export function formatDeltaExplainJson(result: DeltaExplainResult): string {
125
147
  return `${JSON.stringify(result, null, 2)}\n`;
126
148
  }
149
+
150
+ function enrichWithCurrentAgentContract(
151
+ workspaceRoot: string,
152
+ thing: string,
153
+ explanation: Record<string, unknown>,
154
+ ): Record<string, unknown> {
155
+ const currentContract = currentContractForThing(workspaceRoot, thing);
156
+ if (!currentContract) {
157
+ return explanation;
158
+ }
159
+ const runtime = explanation.runtime && typeof explanation.runtime === "object"
160
+ ? explanation.runtime as Record<string, unknown>
161
+ : null;
162
+ return {
163
+ ...explanation,
164
+ type: explanation.type === "unknown" ? "runtime-entry" : explanation.type,
165
+ runtime: runtime ?? {
166
+ entry_name: currentContract.name,
167
+ entry_kind: currentContract.kind,
168
+ result: "defined",
169
+ source: "agentContract",
170
+ ...(currentContract.policy ? { policy: currentContract.policy } : {}),
171
+ ...(typeof currentContract.tenantScoped === "boolean" ? { tenant_scoped: currentContract.tenantScoped } : {}),
172
+ ...(typeof currentContract.needsApproval === "boolean" ? { needs_approval: currentContract.needsApproval } : {}),
173
+ },
174
+ currentContract,
175
+ };
176
+ }
177
+
178
+ function currentContractForThing(workspaceRoot: string, thing: string): Record<string, unknown> | undefined {
179
+ const contractPath = join(workspaceRoot, "src", "forge", "_generated", "agentContract.json");
180
+ if (!existsSync(contractPath)) {
181
+ return undefined;
182
+ }
183
+ try {
184
+ const contract = JSON.parse(readFileSync(contractPath, "utf8")) as Record<string, unknown>;
185
+ const collections: Array<[keyof typeof runtimeKinds, string]> = [
186
+ ["commands", "command"],
187
+ ["queries", "query"],
188
+ ["liveQueries", "liveQuery"],
189
+ ["actions", "action"],
190
+ ["workflows", "workflow"],
191
+ ];
192
+ for (const [collection, kind] of collections) {
193
+ const entries = Array.isArray(contract[collection]) ? contract[collection] as Record<string, unknown>[] : [];
194
+ for (const entry of entries) {
195
+ const name = runtimeEntryName(entry);
196
+ if (!name) {
197
+ continue;
198
+ }
199
+ if (name === thing || `${kind}:${name}` === thing || entry.id === thing || entry.exportName === thing) {
200
+ return {
201
+ source: "src/forge/_generated/agentContract.json",
202
+ kind,
203
+ name,
204
+ ...(stringValue(entry.auth) ? { auth: stringValue(entry.auth) } : {}),
205
+ ...(stringValue(entry.policy) ? { policy: stringValue(entry.policy) } : {}),
206
+ ...(typeof entry.tenantScoped === "boolean" ? { tenantScoped: entry.tenantScoped } : {}),
207
+ ...(typeof entry.needsApproval === "boolean" ? { needsApproval: entry.needsApproval } : {}),
208
+ ...(stringValue(entry.risk) ? { risk: stringValue(entry.risk) } : {}),
209
+ ...(stringValue(entry.file) ? { sourceFile: stringValue(entry.file) } : {}),
210
+ ...(stringValue(entry.path) ? { sourceFile: stringValue(entry.path) } : {}),
211
+ };
212
+ }
213
+ }
214
+ }
215
+ } catch {
216
+ return undefined;
217
+ }
218
+ return undefined;
219
+ }
220
+
221
+ const runtimeKinds = {
222
+ commands: "command",
223
+ queries: "query",
224
+ liveQueries: "liveQuery",
225
+ actions: "action",
226
+ workflows: "workflow",
227
+ } as const;
228
+
229
+ function runtimeEntryName(entry: Record<string, unknown>): string | undefined {
230
+ return stringValue(entry.name)
231
+ ?? stringValue(entry.exportName)
232
+ ?? stringValue(entry.id)
233
+ ?? stringValue(entry.entryName);
234
+ }
235
+
236
+ function stringValue(value: unknown): string | undefined {
237
+ return typeof value === "string" && value.length > 0 ? value : undefined;
238
+ }
@@ -2809,6 +2809,12 @@ function scoreWorkSessionCandidate(
2809
2809
  ): { score: number; signals: DeltaWorkSessionSignal[] } {
2810
2810
  const signals: DeltaWorkSessionSignal[] = [];
2811
2811
  const metadata = candidate.metadata;
2812
+ if (isObservationOnlyContext(context) && !isObservationOnlyWorkSession(metadata)) {
2813
+ return {
2814
+ score: 0.15,
2815
+ signals: [{ signal: "observation-only", weight: 0.15, value: context.commands[0] }],
2816
+ };
2817
+ }
2812
2818
  addSignalIfOverlap(signals, "sameTraceId", 0.4, context.traces, metadata.traces);
2813
2819
  addSignalIfOverlap(signals, "sameManifestService", 0.35, context.services, metadata.services);
2814
2820
  addSignalIfOverlap(signals, "sameRuntimeEntry", 0.3, context.entries, metadata.entries);
@@ -2951,6 +2957,9 @@ function normalizeWorkSessionMetadata(value: Record<string, unknown>): DeltaWork
2951
2957
  }
2952
2958
 
2953
2959
  function inferWorkSessionTitle(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2960
+ if (isObservationOnlyWorkSession(metadata)) {
2961
+ return "Observe project context";
2962
+ }
2954
2963
  if (context.kind === "manifest.imported" && metadata.services[0]) {
2955
2964
  return `Import ${metadata.services[0]} external service`;
2956
2965
  }
@@ -2979,6 +2988,9 @@ function inferWorkSessionTitle(context: DeltaOperationContext, metadata: DeltaWo
2979
2988
  }
2980
2989
 
2981
2990
  function inferIntent(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2991
+ if (isObservationOnlyWorkSession(metadata)) {
2992
+ return "context-gathering";
2993
+ }
2982
2994
  if (context.kind === "manifest.imported" || metadata.fileClusters.includes("manifest.change")) {
2983
2995
  return "external-runtime-import";
2984
2996
  }
@@ -2999,6 +3011,9 @@ function inferIntent(context: DeltaOperationContext, metadata: DeltaWorkSessionM
2999
3011
 
3000
3012
  function summarizeWorkSession(metadata: DeltaWorkSessionMetadata): string {
3001
3013
  const parts: string[] = [];
3014
+ if (isObservationOnlyWorkSession(metadata)) {
3015
+ return `Observed project context with ${metadata.commands.slice(0, 3).join(", ")}.`;
3016
+ }
3002
3017
  if (metadata.services[0]) {
3003
3018
  parts.push(`worked on ${metadata.services[0]}`);
3004
3019
  }
@@ -3018,6 +3033,9 @@ function summarizeWorkSession(metadata: DeltaWorkSessionMetadata): string {
3018
3033
  }
3019
3034
 
3020
3035
  function initialWorkSessionConfidence(context: DeltaOperationContext): number {
3036
+ if (isObservationOnlyContext(context)) {
3037
+ return 0.36;
3038
+ }
3021
3039
  if (context.kind === "manifest.imported") {
3022
3040
  return 0.78;
3023
3041
  }
@@ -3043,6 +3061,65 @@ function isDiagnosticRepairChain(context: DeltaOperationContext, metadata: Delta
3043
3061
  );
3044
3062
  }
3045
3063
 
3064
+ const OBSERVATION_COMMAND_PREFIXES = [
3065
+ "agent context",
3066
+ "agent print-context",
3067
+ "agent timeline",
3068
+ "cair query",
3069
+ "cair snapshot",
3070
+ "changed",
3071
+ "delta status",
3072
+ "doctor",
3073
+ "explain",
3074
+ "handoff",
3075
+ "inspect",
3076
+ "live status",
3077
+ "status",
3078
+ "timeline",
3079
+ ];
3080
+
3081
+ function isObservationOnlyContext(context: DeltaOperationContext): boolean {
3082
+ return (
3083
+ context.commands.length > 0 &&
3084
+ context.files.length === 0 &&
3085
+ context.entries.length === 0 &&
3086
+ context.diagnostics.length === 0 &&
3087
+ context.proofs.length === 0 &&
3088
+ context.services.length === 0 &&
3089
+ context.traces.length === 0 &&
3090
+ context.commands.every(isObservationCommand)
3091
+ );
3092
+ }
3093
+
3094
+ function isObservationOnlyWorkSession(metadata: DeltaWorkSessionMetadata): boolean {
3095
+ return (
3096
+ metadata.commands.length > 0 &&
3097
+ metadata.files.length === 0 &&
3098
+ metadata.entries.length === 0 &&
3099
+ metadata.diagnostics.length === 0 &&
3100
+ metadata.proofs.length === 0 &&
3101
+ metadata.services.length === 0 &&
3102
+ metadata.traces.length === 0 &&
3103
+ metadata.commands.every(isObservationCommand)
3104
+ );
3105
+ }
3106
+
3107
+ function isObservationCommand(command: string): boolean {
3108
+ const normalized = normalizeForgeCommand(command);
3109
+ return OBSERVATION_COMMAND_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix} `));
3110
+ }
3111
+
3112
+ function normalizeForgeCommand(command: string): string {
3113
+ return command
3114
+ .trim()
3115
+ .replace(/^node\s+(?:\.\/)?bin\/forge\.mjs(?:\s+|$)/u, "")
3116
+ .replace(/^bun\s+run\s+forge(?:\s+|$)/u, "")
3117
+ .replace(/^npm\s+run\s+forge\s+--(?:\s+|$)/u, "")
3118
+ .replace(/^forge(?:\s+|$)/u, "")
3119
+ .replace(/\s+/gu, " ")
3120
+ .trim();
3121
+ }
3122
+
3046
3123
  function hasAnyOverlap(...groups: string[][]): boolean {
3047
3124
  for (let index = 0; index < groups.length; index += 2) {
3048
3125
  if (intersects(groups[index] ?? [], groups[index + 1] ?? [])) {
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.22";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.23";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;