@moretea-labs/forge 1.7.0 → 1.7.1
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 +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { AssistantPluginError } from './errors';
|
|
2
|
+
import {
|
|
3
|
+
closeMacOsBrowserOwnedTab,
|
|
4
|
+
listMacOsBrowserTabs,
|
|
5
|
+
macOsBrowserPageHandleStale,
|
|
6
|
+
readMacOsBrowserOwnedTabMetadata,
|
|
7
|
+
type MacOsBrowserProduct,
|
|
8
|
+
} from './browser-macos-bridge';
|
|
9
|
+
import { listSavedBrowserSessions, removeBrowserSession } from './browser-session-store';
|
|
10
|
+
import type { BrowserSessionInventoryItem, BrowserSessionState } from './browser-session-types';
|
|
11
|
+
|
|
12
|
+
export interface NativeOwnedSessionInspection {
|
|
13
|
+
items: Map<string, BrowserSessionInventoryItem>;
|
|
14
|
+
liveCount: number;
|
|
15
|
+
deadCount: number;
|
|
16
|
+
unverifiedCount: number;
|
|
17
|
+
prunedCount: number;
|
|
18
|
+
closedInvalidCount: number;
|
|
19
|
+
failedCleanupCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nativeTabInventoryUnsupported(error: unknown): boolean {
|
|
23
|
+
if (error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED') return true;
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
25
|
+
return /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isDiscardableNativeOwnedTabUrl(url: string): boolean {
|
|
29
|
+
const normalized = url.trim().toLowerCase();
|
|
30
|
+
return normalized === ''
|
|
31
|
+
|| normalized === 'about:blank'
|
|
32
|
+
|| normalized === 'chrome://newtab/'
|
|
33
|
+
|| normalized === 'chrome://new-tab-page/'
|
|
34
|
+
|| normalized === 'vivaldi://newtab/'
|
|
35
|
+
|| normalized === 'vivaldi://startpage/';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Native tab liveness/cleanup belongs to one service so the Browser adapter does
|
|
40
|
+
* not reinterpret stable native identity or prune policy action-by-action.
|
|
41
|
+
*/
|
|
42
|
+
export async function inspectNativeOwnedSessions(input: {
|
|
43
|
+
repoRoot: string;
|
|
44
|
+
savedSessions: BrowserSessionState[];
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
pruneDead?: boolean;
|
|
47
|
+
}): Promise<NativeOwnedSessionInspection> {
|
|
48
|
+
const { repoRoot, savedSessions, timeoutMs } = input;
|
|
49
|
+
const items = new Map<string, BrowserSessionInventoryItem>();
|
|
50
|
+
const inventories = new Map<MacOsBrowserProduct, Awaited<ReturnType<typeof listMacOsBrowserTabs>> | Error>();
|
|
51
|
+
const groups = new Map<string, BrowserSessionState[]>();
|
|
52
|
+
let liveCount = 0;
|
|
53
|
+
let deadCount = 0;
|
|
54
|
+
let unverifiedCount = 0;
|
|
55
|
+
let prunedCount = 0;
|
|
56
|
+
let closedInvalidCount = 0;
|
|
57
|
+
let failedCleanupCount = 0;
|
|
58
|
+
|
|
59
|
+
for (const session of savedSessions) {
|
|
60
|
+
const browser = session.browser;
|
|
61
|
+
const tab = browser?.tab;
|
|
62
|
+
const product = browser?.browserProduct;
|
|
63
|
+
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned' || !product || !tab.windowId || !tab.tabId) continue;
|
|
64
|
+
const key = `${product}:${tab.windowId}:${tab.tabId}`;
|
|
65
|
+
const group = groups.get(key) ?? [];
|
|
66
|
+
group.push(session);
|
|
67
|
+
groups.set(key, group);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const group of groups.values()) {
|
|
71
|
+
const canonical = group.reduce((latest, session) => session.updatedAt > latest.updatedAt ? session : latest);
|
|
72
|
+
const browser = canonical.browser!;
|
|
73
|
+
const tab = browser.tab!;
|
|
74
|
+
const product = browser.browserProduct!;
|
|
75
|
+
let inventory = inventories.get(product);
|
|
76
|
+
if (!inventory) {
|
|
77
|
+
try {
|
|
78
|
+
inventory = await listMacOsBrowserTabs(product, timeoutMs);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
inventory = error instanceof Error ? error : new Error(String(error));
|
|
81
|
+
}
|
|
82
|
+
inventories.set(product, inventory);
|
|
83
|
+
}
|
|
84
|
+
if (inventory instanceof Error) {
|
|
85
|
+
if (!nativeTabInventoryUnsupported(inventory)) {
|
|
86
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'unverified', evidence: 'native_inventory_unavailable', cleanupError: inventory.message });
|
|
87
|
+
unverifiedCount += group.length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ref = { windowId: tab.windowId!, tabId: tab.tabId! };
|
|
92
|
+
let metadata: Awaited<ReturnType<typeof readMacOsBrowserOwnedTabMetadata>>;
|
|
93
|
+
try {
|
|
94
|
+
metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
|
|
95
|
+
} catch (metadataError) {
|
|
96
|
+
if (macOsBrowserPageHandleStale(metadataError)) {
|
|
97
|
+
const pruned = input.pruneDead === true;
|
|
98
|
+
if (pruned) {
|
|
99
|
+
for (const session of group) removeBrowserSession(repoRoot, session.sessionId);
|
|
100
|
+
prunedCount += group.length;
|
|
101
|
+
}
|
|
102
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_missing', pruned });
|
|
103
|
+
deadCount += group.length;
|
|
104
|
+
} else {
|
|
105
|
+
const cleanupError = metadataError instanceof Error ? metadataError.message : String(metadataError);
|
|
106
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'unverified', evidence: 'native_inventory_unavailable', cleanupError });
|
|
107
|
+
unverifiedCount += group.length;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isDiscardableNativeOwnedTabUrl(metadata.url)) {
|
|
113
|
+
deadCount += group.length;
|
|
114
|
+
if (input.pruneDead === true) {
|
|
115
|
+
try {
|
|
116
|
+
await closeMacOsBrowserOwnedTab(product, ref, timeoutMs);
|
|
117
|
+
for (const session of group) removeBrowserSession(repoRoot, session.sessionId);
|
|
118
|
+
prunedCount += group.length;
|
|
119
|
+
closedInvalidCount += 1;
|
|
120
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_closed', pruned: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
failedCleanupCount += 1;
|
|
123
|
+
const cleanupError = error instanceof Error ? error.message : String(error);
|
|
124
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_close_failed', cleanupError });
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid' });
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'live', evidence: 'native_tab_live' });
|
|
131
|
+
liveCount += group.length;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const live = inventory.tabs.find((entry) => entry.windowId === tab.windowId && entry.tabId === tab.tabId);
|
|
137
|
+
if (!live) {
|
|
138
|
+
const pruned = input.pruneDead === true;
|
|
139
|
+
if (pruned) {
|
|
140
|
+
for (const session of group) removeBrowserSession(repoRoot, session.sessionId);
|
|
141
|
+
prunedCount += group.length;
|
|
142
|
+
}
|
|
143
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_missing', pruned });
|
|
144
|
+
deadCount += group.length;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isDiscardableNativeOwnedTabUrl(live.url)) {
|
|
149
|
+
deadCount += group.length;
|
|
150
|
+
if (input.pruneDead === true) {
|
|
151
|
+
try {
|
|
152
|
+
await closeMacOsBrowserOwnedTab(product, { windowId: tab.windowId!, tabId: tab.tabId! }, timeoutMs);
|
|
153
|
+
for (const session of group) removeBrowserSession(repoRoot, session.sessionId);
|
|
154
|
+
prunedCount += group.length;
|
|
155
|
+
closedInvalidCount += 1;
|
|
156
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_closed', pruned: true });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
failedCleanupCount += 1;
|
|
159
|
+
const cleanupError = error instanceof Error ? error.message : String(error);
|
|
160
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_close_failed', cleanupError });
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid' });
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const session of group) items.set(session.sessionId, { session, liveness: 'live', evidence: 'native_tab_live' });
|
|
169
|
+
liveCount += group.length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { items, liveCount, deadCount, unverifiedCount, prunedCount, closedInvalidCount, failedCleanupCount };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function closeTrackedNativeOwnedSession(
|
|
176
|
+
session: BrowserSessionState,
|
|
177
|
+
timeoutMs: number,
|
|
178
|
+
): Promise<{ resourceClosed: boolean; resourceAlreadyMissing: boolean }> {
|
|
179
|
+
const browser = session.browser;
|
|
180
|
+
const tab = browser?.tab;
|
|
181
|
+
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned') {
|
|
182
|
+
return { resourceClosed: false, resourceAlreadyMissing: false };
|
|
183
|
+
}
|
|
184
|
+
if (!browser.browserProduct || !tab.windowId || !tab.tabId) {
|
|
185
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Tracked plugin-owned native tab is missing stable browser identity; retaining session metadata instead of orphaning the tab.', {
|
|
186
|
+
retryable: false, details: { sessionId: session.sessionId, browserProduct: browser.browserProduct, windowId: tab.windowId, tabId: tab.tabId },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const ref = { windowId: tab.windowId, tabId: tab.tabId };
|
|
190
|
+
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
191
|
+
try {
|
|
192
|
+
inventory = await listMacOsBrowserTabs(browser.browserProduct, timeoutMs);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (!nativeTabInventoryUnsupported(error)) throw error;
|
|
195
|
+
try {
|
|
196
|
+
await readMacOsBrowserOwnedTabMetadata(browser.browserProduct, ref, timeoutMs);
|
|
197
|
+
} catch (metadataError) {
|
|
198
|
+
if (macOsBrowserPageHandleStale(metadataError)) return { resourceClosed: false, resourceAlreadyMissing: true };
|
|
199
|
+
throw metadataError;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (inventory && !inventory.tabs.some((entry) => entry.windowId === tab.windowId && entry.tabId === tab.tabId)) {
|
|
203
|
+
return { resourceClosed: false, resourceAlreadyMissing: true };
|
|
204
|
+
}
|
|
205
|
+
await closeMacOsBrowserOwnedTab(browser.browserProduct, ref, timeoutMs);
|
|
206
|
+
return { resourceClosed: true, resourceAlreadyMissing: false };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function nativeOwnedAliasSessionIds(repoRoot: string, session: BrowserSessionState): string[] {
|
|
210
|
+
const browser = session.browser;
|
|
211
|
+
const tab = browser?.tab;
|
|
212
|
+
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned' || !browser.browserProduct || !tab.windowId || !tab.tabId) {
|
|
213
|
+
return [session.sessionId];
|
|
214
|
+
}
|
|
215
|
+
return listSavedBrowserSessions(repoRoot)
|
|
216
|
+
.filter((candidate) => candidate.browser?.provider === 'macos-apple-events'
|
|
217
|
+
&& candidate.browser.browserProduct === browser.browserProduct
|
|
218
|
+
&& candidate.browser.tab?.ownership === 'plugin_owned'
|
|
219
|
+
&& candidate.browser.tab.windowId === tab.windowId
|
|
220
|
+
&& candidate.browser.tab.tabId === tab.tabId)
|
|
221
|
+
.map((candidate) => candidate.sessionId);
|
|
222
|
+
}
|
|
@@ -85,6 +85,16 @@ export function browserRuntimeActionPolicy(actionId: string): BrowserRuntimeActi
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Post-dispatch replay is a semantic BrowserRuntime decision. Only read-only
|
|
91
|
+
* actions fenced to an already-existing resource may be replayed after a
|
|
92
|
+
* transient provider failure; mutations retain unknown-outcome semantics.
|
|
93
|
+
*/
|
|
94
|
+
export function browserActionCanReplayAfterDispatch(actionId: string, requireExistingResource = true): boolean {
|
|
95
|
+
return requireExistingResource && browserRuntimeActionPolicy(actionId).replaySafety === 'read_only';
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
function stringArgument(args: Record<string, unknown>, key: string): string | undefined {
|
|
89
99
|
const value = args[key];
|
|
90
100
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, rmSync } from 'fs';
|
|
2
|
+
import { basename, join } from 'path';
|
|
3
|
+
import { writeJsonAtomic } from '../shared/json-files';
|
|
4
|
+
import { AssistantPluginError } from './errors';
|
|
5
|
+
import {
|
|
6
|
+
currentBrowserSessionAuthorityContext,
|
|
7
|
+
findBrowserSession as findBrowserSessionAuthority,
|
|
8
|
+
listAllBrowserSessionsForRepository,
|
|
9
|
+
saveBrowserSession as saveBrowserSessionAuthority,
|
|
10
|
+
tombstoneBrowserSession,
|
|
11
|
+
} from './browser-session-authority';
|
|
12
|
+
|
|
13
|
+
const BROWSER_STATE_ROOT = '.forge/browser';
|
|
14
|
+
|
|
15
|
+
import type { BrowserSessionState } from './browser-session-types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Repository-local browser paths remain compatibility storage only. During
|
|
19
|
+
* Runtime/plugin execution, BrowserSessionAuthority (SQLite) is the sole writer;
|
|
20
|
+
* the JSON files are read/written only when no Controller authority context is
|
|
21
|
+
* present (for legacy migration/tests/standalone helpers).
|
|
22
|
+
*/
|
|
23
|
+
export function browserStateDir(
|
|
24
|
+
repoRoot: string,
|
|
25
|
+
name: 'sessions' | 'screenshots' | 'profiles' | 'downloads' | 'diagnostics',
|
|
26
|
+
): string {
|
|
27
|
+
return join(repoRoot, BROWSER_STATE_ROOT, name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sessionPath(repoRoot: string, sessionId: string): string {
|
|
31
|
+
return join(browserStateDir(repoRoot, 'sessions'), `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLegacyBrowserSessionJson(path: string): BrowserSessionState | undefined {
|
|
35
|
+
let raw: string;
|
|
36
|
+
try {
|
|
37
|
+
raw = readFileSync(path, 'utf-8');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
|
|
40
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session metadata could not be read.', {
|
|
41
|
+
retryable: true,
|
|
42
|
+
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw) as BrowserSessionState;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_CORRUPT', 'Saved browser session metadata is malformed; refusing to treat corrupt state as a missing session.', {
|
|
49
|
+
retryable: false,
|
|
50
|
+
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function saveBrowserSession(repoRoot: string, session: BrowserSessionState): BrowserSessionState {
|
|
56
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
57
|
+
if (!authority) {
|
|
58
|
+
writeJsonAtomic(sessionPath(repoRoot, session.sessionId), session);
|
|
59
|
+
return session;
|
|
60
|
+
}
|
|
61
|
+
const saved = saveBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, session);
|
|
62
|
+
if (saved.sessionId === session.sessionId || !saved.browser?.sessionResume) return saved;
|
|
63
|
+
return {
|
|
64
|
+
...saved,
|
|
65
|
+
browser: {
|
|
66
|
+
...saved.browser,
|
|
67
|
+
sessionResume: { ...saved.browser.sessionResume, sessionId: saved.sessionId },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function findBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
|
|
73
|
+
if (!sessionId) return undefined;
|
|
74
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
75
|
+
return authority
|
|
76
|
+
? findBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, sessionId)
|
|
77
|
+
: readLegacyBrowserSessionJson(sessionPath(repoRoot, sessionId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listSavedBrowserSessions(repoRoot: string): BrowserSessionState[] {
|
|
81
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
82
|
+
if (authority) {
|
|
83
|
+
return listAllBrowserSessionsForRepository<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot);
|
|
84
|
+
}
|
|
85
|
+
const root = browserStateDir(repoRoot, 'sessions');
|
|
86
|
+
let names: string[];
|
|
87
|
+
try {
|
|
88
|
+
names = readdirSync(root);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
|
|
91
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session directory could not be read.', {
|
|
92
|
+
retryable: true,
|
|
93
|
+
details: { cause: error instanceof Error ? error.message : String(error) },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return names
|
|
97
|
+
.filter((name) => name.endsWith('.json'))
|
|
98
|
+
.map((name) => readLegacyBrowserSessionJson(join(root, name)))
|
|
99
|
+
.filter((session): session is BrowserSessionState => Boolean(session));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function removeBrowserSession(repoRoot: string, sessionId: string): void {
|
|
103
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
104
|
+
if (authority) {
|
|
105
|
+
tombstoneBrowserSession(authority.controllerHome, authority.repoId, repoRoot, sessionId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
rmSync(sessionPath(repoRoot, sessionId), { force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
|
|
112
|
+
return sessionId ? findBrowserSession(repoRoot, sessionId) : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function requireBrowserSession(repoRoot: string, sessionId: string): BrowserSessionState {
|
|
116
|
+
const session = findBrowserSession(repoRoot, sessionId);
|
|
117
|
+
if (!session) {
|
|
118
|
+
throw new AssistantPluginError('PLUGIN_SESSION_NOT_FOUND', `Browser session not found: ${sessionId}`, { retryable: false });
|
|
119
|
+
}
|
|
120
|
+
return session;
|
|
121
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export type BrowserMode = 'attach_preferred' | 'managed_persistent' | 'isolated';
|
|
2
|
+
export type BrowserProfileMode = 'repo_local' | 'custom';
|
|
3
|
+
export type BrowserCdpAttachFallback = 'managed_persistent' | 'fail_closed';
|
|
4
|
+
export type BrowserNativeProduct = 'chrome' | 'vivaldi';
|
|
5
|
+
export type BrowserTabMatchReason = 'owned_token' | 'recovered_tab' | 'saved_url_title' | 'saved_url' | 'exact_url' | 'blank' | 'new_page';
|
|
6
|
+
|
|
7
|
+
export interface CdpAttachAttempt {
|
|
8
|
+
endpoint: string;
|
|
9
|
+
discoveredEndpoint?: string;
|
|
10
|
+
probeUrl?: string;
|
|
11
|
+
browserVersion?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BrowserNativeAttachAttempt {
|
|
16
|
+
product: BrowserNativeProduct;
|
|
17
|
+
appName: string;
|
|
18
|
+
bundleId: string;
|
|
19
|
+
status: 'selected' | 'available' | 'not_installed' | 'not_running' | 'unavailable';
|
|
20
|
+
frontmost?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BrowserSessionResumeDiagnostic {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
status: 'matched' | 'stale_tab' | 'no_saved_tab';
|
|
27
|
+
reason: string;
|
|
28
|
+
savedTab?: Pick<BrowserTabResumeState, 'key' | 'url' | 'title' | 'index'>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BrowserConnectionFallback {
|
|
32
|
+
policy: BrowserCdpAttachFallback;
|
|
33
|
+
from: 'attach_preferred';
|
|
34
|
+
to: 'managed_persistent';
|
|
35
|
+
reason: string;
|
|
36
|
+
attempts: CdpAttachAttempt[];
|
|
37
|
+
nativeAttempts?: BrowserNativeAttachAttempt[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BrowserTabResumeState {
|
|
41
|
+
key: string;
|
|
42
|
+
index: number;
|
|
43
|
+
url: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
matchedBy: BrowserTabMatchReason;
|
|
46
|
+
inventoryCount: number;
|
|
47
|
+
capturedAt: string;
|
|
48
|
+
ownership?: 'plugin_owned' | 'user_owned';
|
|
49
|
+
ownerToken?: string;
|
|
50
|
+
windowId?: string;
|
|
51
|
+
tabId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BrowserSessionConnectionState {
|
|
55
|
+
mode: BrowserMode;
|
|
56
|
+
activeMode: BrowserMode;
|
|
57
|
+
provider: 'playwright-cdp' | 'playwright-persistent-context' | 'macos-apple-events';
|
|
58
|
+
endpoint?: string;
|
|
59
|
+
browserVersion?: string;
|
|
60
|
+
browserProduct?: BrowserNativeProduct;
|
|
61
|
+
nativeBrowserCandidates?: BrowserNativeProduct[];
|
|
62
|
+
fallback?: BrowserConnectionFallback;
|
|
63
|
+
tab?: BrowserTabResumeState;
|
|
64
|
+
sessionResume?: BrowserSessionResumeDiagnostic;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BrowserSessionState {
|
|
68
|
+
schemaVersion: 1;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
url: string;
|
|
71
|
+
title?: string;
|
|
72
|
+
createdAt: string;
|
|
73
|
+
updatedAt: string;
|
|
74
|
+
browser?: BrowserSessionConnectionState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type BrowserSessionLiveness = 'live' | 'unverified' | 'dead';
|
|
78
|
+
export type BrowserSessionLivenessEvidence =
|
|
79
|
+
| 'runtime_session_binding'
|
|
80
|
+
| 'runtime_owner_token'
|
|
81
|
+
| 'runtime_bound_page_missing'
|
|
82
|
+
| 'runtime_context_unavailable'
|
|
83
|
+
| 'runtime_owner_token_not_observed'
|
|
84
|
+
| 'provider_unverified'
|
|
85
|
+
| 'native_tab_live'
|
|
86
|
+
| 'native_tab_missing'
|
|
87
|
+
| 'native_tab_invalid'
|
|
88
|
+
| 'native_tab_invalid_closed'
|
|
89
|
+
| 'native_tab_invalid_close_failed'
|
|
90
|
+
| 'native_inventory_unavailable';
|
|
91
|
+
|
|
92
|
+
export interface BrowserSessionInventoryItem {
|
|
93
|
+
session: BrowserSessionState;
|
|
94
|
+
liveness: BrowserSessionLiveness;
|
|
95
|
+
evidence: BrowserSessionLivenessEvidence;
|
|
96
|
+
pruned?: boolean;
|
|
97
|
+
cleanupError?: string;
|
|
98
|
+
}
|
|
@@ -119,7 +119,7 @@ function desktopOperatorActions(): AssistantPluginActionDescriptor[] {
|
|
|
119
119
|
{
|
|
120
120
|
actionId: 'desktop_press',
|
|
121
121
|
title: 'Press desktop element',
|
|
122
|
-
description: '
|
|
122
|
+
description: 'Perform one bounded Accessibility semantic action in the background, including one-page list scrolling; pointer/coordinate and raw input fallback are intentionally unavailable.',
|
|
123
123
|
readOnly: false,
|
|
124
124
|
risk: 'workspace_write',
|
|
125
125
|
confirmation: 'authorization',
|
|
@@ -135,8 +135,8 @@ function desktopOperatorActions(): AssistantPluginActionDescriptor[] {
|
|
|
135
135
|
selector: SELECTOR_SCHEMA,
|
|
136
136
|
semantic_action: {
|
|
137
137
|
type: 'string',
|
|
138
|
-
enum: ['press', 'show_menu', 'pick', 'open', 'confirm'],
|
|
139
|
-
description: 'Bounded Accessibility action supported by the trusted Desktop Operator provider;
|
|
138
|
+
enum: ['press', 'show_menu', 'pick', 'open', 'confirm', 'scroll_down_page', 'scroll_up_page'],
|
|
139
|
+
description: 'Bounded Accessibility action supported by the trusted Desktop Operator provider; scroll actions move exactly one page and default to press when omitted.',
|
|
140
140
|
},
|
|
141
141
|
},
|
|
142
142
|
required: ['interaction_id', 'selector'],
|
|
@@ -170,7 +170,7 @@ function desktopOperatorActions(): AssistantPluginActionDescriptor[] {
|
|
|
170
170
|
{
|
|
171
171
|
actionId: 'desktop_key',
|
|
172
172
|
title: 'Press desktop keys',
|
|
173
|
-
description: 'Send one bounded key chord
|
|
173
|
+
description: 'Send one bounded key chord to the exact target session after Forge independently re-establishes and verifies that application as system-frontmost at the action boundary.',
|
|
174
174
|
readOnly: false,
|
|
175
175
|
risk: 'workspace_write',
|
|
176
176
|
confirmation: 'authorization',
|